BackacheEngineerの技術的な備忘録

技術系でいろいろ書けたらなーと

C# コンテキストメニューにサブメニュー表示していろいろしたい

目次

はじめに

VSCodeのRainbow CSVっていう幸せな拡張機能を見つけて喜んでいたところ、タブ区切りのくせして拡張子が「txt」のやつを扱う必要があった。

手動でtsvに変換するの面倒なので「C#でツール作ろ」となった。

挙動は以下のとおり。

  • 対象ファイルを右クリックするとコンテキストメニューに「変換して開く」のようなものを表示
  • 「変換して開く」のサブメニューに「csv」、「tsv」と拡張子を表示 (「csv」はなんとなく用意した)
  • 元ファイルをコピーし、指定した拡張子に変換してVSCodeで開く
  • セットアップも作って、レジストリ設定は自動でやる

実装

今回の主な実装は二つ。

  1. コンテキストメニューから起動できるようにするインストーラ
  2. コピーして開くツール本体

圧倒的に1がメイン。(コピーして開くなんてすぐだし…)

開発環境

  • Windows10 Pro 1909
  • Visual Studio 2019
  • .NET Framework 4.8 (.NET Core も考えないといかんなぁ)
  • C# 8.0 (switch構文が使いたかった…)

ツール:インストーラ

インストールしたらコンテキストメニューにサブメニューを表示しなくちゃならない。

ここで、サブメニューの表示にむちゃ苦戦した。記事が少ないうえに日本語と英語のサイトだとやり方違って、英語のほうでやってもできないという…何が正しいがわからなくなったが、とりあえず日本語のサイトを参考にしたらできた。

インストール時のレジストリ構造は下の画像のとおり(こうしないとサブメニューができない…)。Installerクラスを継承してInstallメソッドをオーバーライドしてもいいけど、今回はGUIつかってがちがちと…。

f:id:BackacheEngineer:20200221181548p:plain

それぞれの文字列値とかは以下のとおり。「""」は空文字。下の設定以外はなんも値を入れてない。

  • ExtensionConvert
    • Name:MUIVerb、Value:"拡張子を変更してVSCodeで開く"
    • Name:SubCommands、Value:""
  • csv
    • Name:(Default)、Value:”csv
      • command
        • Name:(Default)、Value:"[TARGETDIR]ExtensionConvert.exe" "%1" -csv
  • tsv
    • Name:(Default)、Value:”tsv”
      • command
        • Name:(Default)、Value:"[TARGETDIR]ExtensionConvert.exe" "%1" -tsv

「(Default)」のところは設定時に空欄でEnterすれば勝手にいれてくれる。インストールするフォルダはユーザごとで違うので[TARGETDIR]を使用する。

あとは「-csv」、「-tsv」を見てわかるように、オプションとしてファイルパスと「-tsv」とか受け取ったら拡張子変更コピーしてVSCodeで開くツールを作る。

ツール:本体

自分用なのでサクッとつくる。VSCodeのexeなんて絶対パスそのまんま。レジストリから探すのは面倒(というか知らない、調べてない←)

using System;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;

namespace ExtensionConvert
{
    #region 列挙
    /// <summary>
    /// 変換先の列挙
    /// </summary>
    enum Extension
    {
        TSV,
        CSV
    }
    #endregion

    class Program
    {
        #region プライベートメンバ
        static string path;
        static Extension convertExt;
        const string c_VsCodePath = @"VSCodeの絶対パス";
        #endregion

        #region Main
        static void Main(string[] args)
        {
            commandParser(args);
            var newPath = getCopyPath();
            File.Copy(args[0], newPath, true);

            var psi = new ProcessStartInfo(c_VsCodePath)
            {
                UseShellExecute = false,
                CreateNoWindow = true,
                Arguments = newPath
            };
            Process.Start(psi);
        }
        #endregion

        #region コマンドラインパーサー
        /// <summary>
        /// コマンドパーサー(これいる?)
        /// </summary>
        /// <param name="args">引数</param>
        static void commandParser(string[] args)
        {
            path = args[0];
            convertExt = args[1] switch
            {
                "-tsv" => Extension.TSV,
                "-csv" => Extension.CSV,
                _ => throw new InvalidOperationException()
            };
        }
        #endregion

        #region コピー先のファイルパス
        /// <summary>
        /// 新しいコピー先のパスを取得
        /// </summary>
        /// <returns>新しいコピー先のファイルパス</returns>
        static string getCopyPath()
        {
            var regNum = new Regex($@"{Path.GetFileNameWithoutExtension(path)}_copy[\d]*\.(csv|tsv)$");
            var name = Path.GetFileNameWithoutExtension(path);
            var retPath = convertExt switch
            {
                Extension.TSV => searchPath($"{Path.Combine(Path.GetDirectoryName(path), name)}_copy.tsv"),
                Extension.CSV => searchPath($"{Path.Combine(Path.GetDirectoryName(path), name)}_copy.csv"),
                _ => throw new InvalidOperationException()
            };
            return retPath;

            #region (ローカル関数) コピー可能なファイル名探索
            string searchPath(string path)
            {
                for (int i = 1; File.Exists(path); i++)
                {
                    path = regNum.Replace(path, $"{name}_copy{i}.tsv");
                }
                return path;
            }
            #endregion
        }
        #endregion
    }
}

実行結果

うまくいった。 f:id:BackacheEngineer:20200221193258p:plain f:id:BackacheEngineer:20200221193439p:plain

参考

http://michelloid.hatenablog.com/entry/2017/09/06/213718