地上の洞窟

どこにも行かず、液晶と「にらめっこ」し続ける人の物語。

【C#】多重起動の防止・元プロセスにファイルパスを渡す方法など

エクスプローラー上でファイルを開き、アプリケーションを起動する場合。
既に起動しているアプリケーションにファイルを渡したい時、どうしたらよいか。
主に、以下の手順を組むことになるだろう。

  1. 多重起動の検出
  2. 元プロセスを探す
  3. 元プロセスへファイルパスを送る
  4. 元プロセスでファイルパスを受け取る

これら各手順の方法をざっくり解説していこうと思う。

多重起動の検出

using System.Threading;

static void Main()
{
    using (var mutex = new Mutex(true, "ApplicationMutexName", out bool createdNew))
    {
        if (createdNew)
        {
            // 初回起動時
            // 通常通りアプリケーションを実行する処理
            Run();
        }
        else
        {
            // 多重起動時
            // ここで元プロセスにデータを送信する処理を入れたり、単に終了したりする
            return;
        }
    }
}

多重起動かどうかを判別するには、Mutexを使う。
Mutexは、排他制御、同期のための機構…らしい。
「マルチスレッドで、同じリソースを扱うけど、同時に処理されると困る…」
といった場合に用いられる、スレッドのロックの一種と言えば、分かりやすいだろうか?

性質は以下の通り。

  • 一スレッドだけが取得・所有できる
  • 固有の識別子を持たせて、他のアプリケーションからもアクセスできる
  • OSとかカーネルとかそういうレベルでの操作が入るので、速度は遅い

"ApplicationMutexName"はMutexを識別するための任意の文字列を自分で指定する。
他のアプリと重複すると困ったことになるので、被らなさそうなものをきちんと設定しよう。

元プロセスを探す

private Process FindPrevProcess()
{
    var currentProcess = Process.GetCurrentProcess();
    var currentPath = currentProcess.MainModule.FileName;
    var allProcesses = Process.GetProcessesByName(currentProcess.ProcessName);

    foreach (var process in allProcesses)
    {
        if (process.Id != currentProcess.Id)
        {
            try
            {
                if (string.Compare(process.MainModule.FileName, currentPath, true) == 0)
                {
                    return process;
                }
            }
            catch (Exception)
            {
                continue;
            }
        }
    }
    return null;
}

多重起動時、自プロセスと同じ、元プロセスを探す。

ロジックは以下の通り。

  1. 自プロセスのファイルパスを取得する
  2. 自プロセスと同じプロセス名のプロセス一覧を取得する
  3. プロセス一覧を調べ上げ、元プロセスを探す

元プロセスであるかの判定は以下の通り。

  • 自プロセスとは異なるプロセスIDである
  • 自プロセスと同じファイルパスのプロセスである

ファイル絡みでエラーが出たり出なかったりするので、一応例外処理を挟んでたり。

元プロセスへファイルパスを送る

private const int WM_COPYDATA = 0x004A;

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int SendMessage(IntPtr hWnd, int msg, IntPtr wParam, ref COPYDATASTRUCT lParam);

[StructLayout(LayoutKind.Sequential)]
private struct COPYDATASTRUCT
{
    public IntPtr dwData;   // (IntPtr.Zero)
    public uint cbData;     // lpDataが指すデータのサイズ
    public string lpData;   // 渡されるデータ
}

public static void SendTextToPrevProcess(string text)
{
    var prevProcess = FindPrevProcess();
    if (prevProcess != null)
    {
        var copyData = new COPYDATASTRUCT
        {
            dwData = IntPtr.Zero,
            cbData = (uint)Encoding.Default.GetByteCount(text) + 1,
            lpData = text
        };
        SendMessage(prevProcess.MainWindowHandle, WM_COPYDATA, IntPtr.Zero, ref copyData);
    }
}

ファイルパス、に限定されず、文字列データを送信する方法である。

元プロセスにファイルパスなどデータを送るのは、プロセス間通信…という考え方になってくる。
まぁ実際にはもっと簡単に考えて、ウィンドウメッセージのやり取りで送信ができる。

元プロセスに、Win32APIのSendMessage関数で、WM_COPYDATAメッセージを送信する。
送信するデータは、COPYDATASTRUCTという構造体に納めなければならない。
dwDataはIntPtr.Zeroでいいっぽい*1、cbDataは送信するデータのバイト数。
lpDataは本来、送信するデータのポインタらしいが、文字列はそのままでも良さそう。

元プロセスでファイルパスを受け取る

protected override void WndProc(ref Message m)
{
    if (m.Msg == WM_COPYDATA)
    {
        // 構造体から文字列を取り出す
        var copyData = Marshal.PtrToStructure<COPYDATASTRUCT>(m.LParam);
        
        // 受け取った文字列
        var text = copyData.lpData;
    }
    base.WndProc(ref m);
}

受け取り側、メインウィンドウのメッセージ処理。
送られてきたメッセージのデータはポインタである。
なのでポインタの先にある構造体を引っ張ってきて、実際の文字列を取り出す。

*1:32bit・64bitアプリケーションでサイズが変わるらしいので注意