地上の洞窟

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

【C#】ジャンプリストの作り方

ジャンプリスト
ジャンプリスト

タスクバーやスタートメニューに登録したアプリケーションを、右クリックした時に表示されるメニュー、それがジャンプリストである。
このジャンプリストの項目を、アプリケーション独自に変更する方法を紹介する。

概念

ジャンプリスト自体は、単にアプリケーションを起動する機能である。
各項目には、アプリケーションのパス、起動時引数が設定されている。
起動時引数は、そのアプリケーションに開かせたいファイルのパスや、アプリケーション固有の識別子(コマンド)が付けられたりする。

自分のアプリケーションの機能を呼び出したい場合は、以下のような仕組みを作る。

  • ジャンプリストの項目の起動時引数に、固有のコマンドを設定
  • 引数にコマンドが渡されているかを調べ、機能を実行する

既にアプリケーションが起動していて、そのプロセス上で機能を呼び出したいという場合。
例えば、ブラウザの「新しいタブを開く」「よく見るページを開く」といったもの。
この場合は、多重起動かを調べ、元プロセスにコマンドを送信する仕組みが必要になる。

実装例

ここでは、WindowsAPICodePackを使用したコード例を提示する。
必要があればNuGetで。

「タスク」に項目を追加

using Microsoft.WindowsAPICodePack.Taskbar;

// ジャンプリストを初期化
var jumpList = JumpList.CreateJumpList();

// 自分のアプリケーションのパスを調べる
var myAppPath = System.Reflection.Assembly.GetEntryAssembly().Location;

// [タスクA]
var taskA = new JumpListLink(myAppPath, "タスクA")
{
    Arguments = "/taskA"
};
            
// [タスクB]
var taskB = new JumpListLink(myAppPath, "タスクB")
{
    Arguments = "/taskB"
};

// 分割線
var separator = new JumpListSeparator();

// [タスクC]
var taskC = new JumpListLink(myAppPath, "タスクC")
{
    Arguments = "/taskC"
};

// ジャンプリストに項目を追加
jumpList.AddUserTasks(taskA);
jumpList.AddUserTasks(taskB);
jumpList.AddUserTasks(separator);
jumpList.AddUserTasks(taskC);

// ジャンプリストを更新
jumpList.Refresh();

「最近」に項目を追加

[System.Runtime.InteropServices.DllImport("shell32.dll")]
static extern void SHAddToRecentDocs(uint uFlags, string path);
private const uint SHARD_PATHW = 0x00000003;

public void AddRecentlyUsedFile(string path)
{
    SHAddToRecentDocs(SHARD_PATHW, path);
}

起動時引数の取得

ジャンプリストの項目に紐づけられた引数は、起動されるアプリの起動時引数に渡される。
そこでファイルパスだったり、固有のコマンドだったりの判別を行う。
起動時引数の取得は

  • Mainに引数を付けて受け取る
  • System.Environment.GetCommandLineArgsを使う

などがある。
以下の例を参考にしてほしい。

Mainに引数を付けて受け取る
static void Main(string[] args)
{
    // コマンドの判別と処理
    switch (args[0])
    {
        case "/taskA":
            break;
        case "/taskB":
            break;
        case "/taskC":
            break;
        default:
            break;
    }
}
System.Enviroment.GetCommandLineArgsを使う
// Environment.GetCommandLineArgs[0] は実行ファイルのパスが入っている
// 1番目以降が実際に渡された引数
var args = System.Environment.GetCommandLineArgs();

if (args.Length > 1)
{
    // コマンドの判別と処理
    switch (args[1])
    {
        case "/taskA":
            break;
        case "/taskB":
            break;
        case "/taskC":
            break;
        default:
            break;
    }
}

起動中のプロセスで機能を実行させる

既に起動しているアプリケーションで、ジャンプリストから機能を実行させる場合を考える。
この場合は、以下の手順を踏む。

  • 多重起動の検出
  • 元プロセスを探す
  • 元プロセスへのコマンド送信
  • 元プロセスでコマンドの受信
多重起動の検出
using System.Threading;

static void Main()
{
    using (var mutex = new Mutex(true, "ApplicationMutexName", out bool createdNew))
    {
        if (createdNew)
        {
            // 初回起動時
            // 通常通りアプリケーションを実行する処理
            Run();
        }
        else
        {
            // 多重起動時
            // ここで起動時引数にコマンドが渡されてるかを判別し、元プロセスに送信する
            return;
        }
    }
}

多重起動の検出には、Mutexを使う。
Mutexは一つのスレッドだけが利用できる個室のようなもので、元プロセスがこの部屋に入る。
同じ部屋を使おうとしても入れないので、自身が後のプロセスであると分かる、そんな感じ。

"ApplicationMutexName"は他のアプリと被らなさそうな固有の名前を付ける。

一連の操作が終わったら、通常起動時の処理は行わずにプログラムを終了させる。

元プロセスを探す
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 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*1という構造体に納める。

元プロセスでコマンドの受信
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);
}

受け取り側。
メインウィンドウでメッセージ処理を行う。
受け取ったメッセージにはCOPYDATASTRUCTのポインタが入っているので、それをもとに構造体を持ってきて、テキストを取り出す。

*1:64bit版はサイズが違うらしいが、ぶっちゃけよくわからない