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を設定することには成功されていて、その先の話ですよね。すいません、自分はそこまで調べてなかったので、すぐには分かりません。
コメントを投稿