
タスクバーやスタートメニューに登録したアプリケーションを、右クリックした時に表示されるメニュー、それがジャンプリストである。
このジャンプリストの項目を、アプリケーション独自に変更する方法を紹介する。
概念
ジャンプリスト自体は、単にアプリケーションを起動する機能である。
各項目には、アプリケーションのパス、起動時引数が設定されている。
起動時引数は、そのアプリケーションに開かせたいファイルのパスや、アプリケーション固有の識別子(コマンド)が付けられたりする。
自分のアプリケーションの機能を呼び出したい場合は、以下のような仕組みを作る。
- ジャンプリストの項目の起動時引数に、固有のコマンドを設定
- 引数にコマンドが渡されているかを調べ、機能を実行する
既にアプリケーションが起動していて、そのプロセス上で機能を呼び出したいという場合。
例えば、ブラウザの「新しいタブを開く」「よく見るページを開く」といったもの。
この場合は、多重起動かを調べ、元プロセスにコマンドを送信する仕組みが必要になる。
実装例
ここでは、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; }
自プロセスと同じ、元プロセスを探す。
- 自プロセスのファイルパスを取得する
- 自プロセスと同じプロセス名のプロセス一覧を取得する
- プロセス一覧を調べ上げ、元プロセスを探す
元プロセスかの判定は以下の通り。
- 自プロセスとは異なるプロセス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版はサイズが違うらしいが、ぶっちゃけよくわからない