C#のプログラムで自分自身をPin止めする(1)


Windows7からサポートされ、またWindows8ではこの機能の利用を前提にスタートボタンが廃止されることとなった「タスクバーへのPin止め」機能をC#のプログラムから実行してみます。


Pin止めのためのAPIは公開されていませんが、既に先人たちがいろいろと調査してくれていて、たとえば、

などの情報がありますが、そのままでは英語OSでしか動かなかったり、Pin止めされたタスクバーアイコンから起動するとアイコンが分裂するなどの問題があるので、その辺を改善したものを作ろうと思います。


まず、「タスクバーへのPin止め」の実体とは以下の2つです。

  1. %APPDATA%\Roaming\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\フォルダに作成されたショートカット(.LNK)ファイル
  2. HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Taskbandに設定された各種属性値
このうち後者については解析が非常に困難なので、ほとんどのアプローチでは「シェルの右クリックメニュー『タスクバーにこのプログラムを表示する』(pin to taskbar)を呼び出して実行する」という方法で実現しています。
なので今回のプログラムも同じアプローチを取ります。


なお、以下のコードでは楽するためにC# 4.0とWindows7 API Code Packを使います。
また、参照設定にWindows7 API Code Pack及びWindows Script Host Object Model(COM)を追加してください。

using System.IO;
using System.Text.RegularExpressions;
using Microsoft.WindowsAPICodePack.Shell;

        /// <summary>
        /// プログラムによるピン止め(その1)
        /// </summary>
        /// <param name="filePath">Pin止めするプログラムのフルパス</param>
        /// <param name="pinTaskbar">Pin止め先:True=タスクバー, False=スタートメニュー</param>

        public void Pinned(string filePath, bool pinTaskbar)
        {
            if (!File.Exists(filePath)) throw new FileNotFoundException(filePath);

            // ショートカットファイル解析用
            IWshRuntimeLibrary.WshShell wshShell = new IWshRuntimeLibrary.WshShell();
            IWshRuntimeLibrary.WshShortcut shortcut;

            // UserPinnedフォルダの取得
            string userPinnedPath = Path.Combine(KnownFolders.UserPinned.Path,
                (pinTaskbar ? "TaskBar" : "StartMenu")
            );

            // 既にピン止めされている?
            foreach (string lnkFile in Directory.EnumerateFiles(userPinnedPath, "*.lnk"))
            {
                shortcut = (IWshRuntimeLibrary.WshShortcut)wshShell.CreateShortcut(lnkFile);
                if (filePath.Equals(shortcut.TargetPath, StringComparison.CurrentCultureIgnoreCase)) return;
            }

            // Shell.Applicationオブジェクトの作成
            dynamic shellApplication = Activator.CreateInstance(Type.GetTypeFromProgID("Shell.Application"));

            string path = Path.GetDirectoryName(filePath);
            string fileName = Path.GetFileName(filePath);

            dynamic directory = shellApplication.NameSpace(path);
            dynamic link = directory.ParseName(fileName);

            dynamic verbs = link.Verbs();
            for (int i = 0; i < verbs.Count(); i++)
            {
                dynamic verb = verbs.Item(i);
#if false
                //XXX - 英語環境のみで動作
                string verbName = verb.Name.Replace(@"&", string.Empty).ToLower();
                if (( pinTaskbar && verbName.Equals("pin to taskbar"))
                ||  (!pinTaskbar && verbName.Equals("pin to start menu"))
#else
                string verbName = verb.Name;
                if (( pinTaskbar && Regex.IsMatch(verbName, @"&[Kk]"))
                ||  (!pinTaskbar && Regex.IsMatch(verbName, @"&[Uu]"))
#endif
                   )
                {
                    // ピン止め
                    verb.DoIt();
                    break;
                }
            }
            shellApplication = null;
        }


まずタスクバーへのPin止めの実体であるショートカットファイルをチェックするためにIWshRuntimeLibrary.WshShelを使用し、IWshRuntimeLibrary.WshShortcut.TargetPathがPin止めしようとしているターゲットのプログラムと同一ならば既にPin止めされているということなので、何もせずにreturnします。

ここでWindows 7 API Code Packに存在するKnownFolders.UserPinned.Pathを使うとPin止めフォルダへのパスが一発で取得できます(後はタスクバーかスタートメニューかのサブフォルダ名を連結するだけ)。

そしてShell.Applicationオブジェクトを生成し、ターゲットプログラムに対してシェルの右クリックメニューを列挙して、それが「タスク バーに表示する(&K)」(または「スタート メニューに表示する(&U)」)ならばそれを実行する、ということになります。
なおShell.ApplicationオブジェクトはCOMなので参照を楽にするためにdymanic型を使っています。


ここで問題なのが、右クリックメニューの各要素を識別する方法が名称であるverb.Nameしかなく、しかもその内容がOSの言語毎に異なる点です。
このソースでは&K(または&U)のキーボードショートカットを持つものを条件にしていますが、実はこれでも完璧ではなく、言語によってはさらに別のキーボードショートカットを持つ場合もあるようです。

それでも#if falseでくくったほうの英語の名称をそのままマッチングするよりは遥かにマシなのですが、この辺のもっとうまいやり方を知ってる方はコメントいただけるとありがたいです。


これでPin止めそのものはできるようになりましたが、こうしてPin止めしたアイコンからプログラムを起動するとアイコンが分裂するという問題がありますので、次回はそれについて対応したいと思います。