1. 背景
Windows 8のWindowsストアアプリでは常駐型アプリは難しいので、常駐型の監視アプリを使おうとする場合、(x86版であれば)デスクトップアプリを動かしておいて何かあればWindowsストアアプリの画面にトースト通知を出すのが一つの解になると思うが、このトースト通知をデスクトップアプリから送るにはAppUserModelIDを含んだショートカットがスタートメニューにあることが条件になっている。
このAppUserModelIDは普通に張ったショートカットには含まれないもので、これを含むショートカットの作成はアプリのインストーラにやらせることをMicrosoftは推奨している。が、そうは言ってもインストーラの作ったショートカットをユーザーが消してしまう可能性もあるので確実性に欠けるし、常にインストーラを必要とするのはあまり便利ではない。
一方で、AppUserModelIDを含んだショートカットを読み書きする方法は.NET Frameworkでは提供されておらず、ショートカットでよく使われるWSHのWshShortcutオブジェクトでもAppUserModelIDは対象に入っていない。一応C++での方法は示されているので、であればと、これに沿ってC#からWin32とCOMを使って操作するコード(ラッパークラス)を書いた次第。
2. コーディング
C#あるいはVBからCOMのIShellLinkインターフェイスを使ってショートカットを読み書きする例は既に幾つかあって、これをベースにさせていただいた。
- smdn: ショートカットファイル(.lnkファイル)を作成する
- vbAccelerator: Creating and Modifying Shortcuts
- .netPlus: ShellLink
- MSDN: How to enable desktop toast notifications through an AppUserModelID (Windows)
- アプローズの日記: System.ApplicationUserModel.ID
- win7appid
ただ、プロパティの値を格納するPROPVARIANT構造体が複雑で、これに難儀していたところ、そもそものデスクトップアプリからトースト通知を送るサンプルに(正確には、その利用するWindows API Code Packに)答えがあった。
- MSDN: Sending toast notifications from desktop apps sample
- MSDN: Windows API Code Pack for Microsoft .NET Framework
IPropertyStoreに必要なコードを整理すると、まずIPropertyStore自体は以下のとおり。
// IPropertyStore Interface [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")] private interface IPropertyStore { uint GetCount([Out] out uint cProps); uint GetAt([In] uint iProp, out PropertyKey pkey); uint GetValue([In] ref PropertyKey key, [Out] PropVariant pv); uint SetValue([In] ref PropertyKey key, [In] PropVariant pv); uint Commit(); }この中でプロパティのキーを格納するPropertyKey構造体。
// PropertyKey Structure // Narrowed down from PropertyKey.cs of Windows API Code Pack 1.1 [StructLayout(LayoutKind.Sequential, Pack = 4)] private struct PropertyKey { #region Fields private Guid formatId; // Unique GUID for property private Int32 propertyId; // Property identifier (PID) #endregion #region Public Properties public Guid FormatId { get { return formatId; } } public Int32 PropertyId { get { return propertyId; } } #endregion #region Constructor public PropertyKey(Guid formatId, Int32 propertyId) { this.formatId = formatId; this.propertyId = propertyId; } public PropertyKey(string formatId, Int32 propertyId) { this.formatId = new Guid(formatId); this.propertyId = propertyId; } #endregion }で、プロパティの値を格納するPropVariantクラス(とそれに必要な関数)。
// PropVariant Class (only for string value) // Narrowed down from PropVariant.cs of Windows API Code Pack 1.1 // Originally from http://blogs.msdn.com/b/adamroot/archive/2008/04/11 // /interop-with-propvariants-in-net.aspx [StructLayout(LayoutKind.Explicit)] private sealed class PropVariant : IDisposable { #region Fields [FieldOffset(0)] ushort valueType; // Value type // [FieldOffset(2)] // ushort wReserved1; // Reserved field // [FieldOffset(4)] // ushort wReserved2; // Reserved field // [FieldOffset(6)] // ushort wReserved3; // Reserved field [FieldOffset(8)] IntPtr ptr; // Value #endregion #region Public Properties // Value type (System.Runtime.InteropServices.VarEnum) public VarEnum VarType { get { return (VarEnum)valueType; } set { valueType = (ushort)value; } } // Whether value is empty or null public bool IsNullOrEmpty { get { return (valueType == (ushort)VarEnum.VT_EMPTY || valueType == (ushort)VarEnum.VT_NULL); } } // Value (only for string value) public string Value { get { return Marshal.PtrToStringUni(ptr); } } #endregion #region Constructor public PropVariant() { } // Construct with string value public PropVariant(string value) { if (value == null) throw new ArgumentException("Failed to set value."); valueType = (ushort)VarEnum.VT_LPWSTR; ptr = Marshal.StringToCoTaskMemUni(value); } #endregion #region Destructor ~PropVariant() { Dispose(); } public void Dispose() { PropVariantClear(this); GC.SuppressFinalize(this); } #endregion } [DllImport("Ole32.dll", PreserveSig = false)] private extern static void PropVariantClear([In, Out] PropVariant pvar);また、AppUserModelIDのキーはFormatIDとPropIDからPropertyKeyを使って以下のように定義できる。
// Name = System.AppUserModel.ID // ShellPKey = PKEY_AppUserModel_ID // FormatID = 9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3 // PropID = 5 // Type = String (VT_LPWSTR) private readonly PropertyKey AppUserModelIDKey = new PropertyKey("{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}", 5);これらを使ってAppUserModelIDをShellLinkクラスのプロパティとして加えた。なお、VerifySucceededは戻り値を見て失敗なら例外を出すメソッド。ショートカットにAppUserModelIDのプロパティが存在しない場合、Valueはnullとなる。
// AppUserModelID to be used for Windows 7 or later. public string AppUserModelID { get { using (PropVariant pv = new PropVariant()) { VerifySucceeded(PropertyStore.GetValue(AppUserModelIDKey, pv)); if (pv.Value == null) return "Null"; else return pv.Value; } } set { using (PropVariant pv = new PropVariant(value)) { VerifySucceeded(PropertyStore.SetValue(AppUserModelIDKey, pv)); VerifySucceeded(PropertyStore.Commit()); } } }このShellLinkクラスを使うアプリも合わせた全体のコードはこちらに載せた。なお、IShellLink自体の機能は本題ではないので、最低限必要なものに絞ってある。
アプリの見た目は以下のようなもので、ショートカット先のターゲットファイルのパス、実行時のオプション、AppUserModelIDの読み書きが可能。
実際に使うには作り込みの必要があると思うが、叩き台としてはこんなものかと。
2 コメント :
こんにちは。自分もC#からショートカットファイルのAppUserModelIDを設定したくて、Win7 API Code Packを使ったこんなプログラムを書いてみました。
ただ記事中にも書いた通りWin8ではうまくいかずに行き詰ってます。
何かコメントいただけると幸いです。
chi-bdさん
面白いことをされてますね。というより、そちらのやり方の方が正統的なように思います。
Win8での問題については、ショートカットファイルにAppUserModelIDを設定することには成功されていて、その先の話ですよね。すいません、自分はそこまで調べてなかったので、すぐには分かりません。
コメントを投稿