2012/11/14

AppUserModelIDをC#から操作する

Windows 7以降で利用されるAppUserModelIDを含んだショートカットをC#から操作するコードを書いてみた。

1. 背景

Windows 8のWindowsストアアプリでは常駐型アプリは難しいので、常駐型の監視アプリを使おうとする場合、(x86版であれば)デスクトップアプリを動かしておいて何かあればWindowsストアアプリの画面にトースト通知を出すのが一つの解になると思うが、このトースト通知をデスクトップアプリから送るにはAppUserModelIDを含んだショートカットがスタートメニューにあることが条件になっている。

このAppUserModelIDは普通に張ったショートカットには含まれないもので、これを含むショートカットの作成はアプリのインストーラにやらせることをMicrosoftは推奨している。が、そうは言ってもインストーラの作ったショートカットをユーザーが消してしまう可能性もあるので確実性に欠けるし、常にインストーラを必要とするのはあまり便利ではない。

一方で、AppUserModelIDを含んだショートカットを読み書きする方法は.NET Frameworkでは提供されておらず、ショートカットでよく使われるWSHのWshShortcutオブジェクトでもAppUserModelIDは対象に入っていない。一応C++での方法は示されているので、であればと、これに沿ってC#からWin32とCOMを使って操作するコード(ラッパークラス)を書いた次第。

2. コーディング

C#あるいはVBからCOMのIShellLinkインターフェイスを使ってショートカットを読み書きする例は既に幾つかあって、これをベースにさせていただいた。
また、C++でAppUserModelIDを読み書きする例は以下のとおり。
方針としては、IShellLinkを使う例を基本的に踏襲しつつ(IPersistFileインターフェイスはSystem.Runtime.InteropServices.ComTypes.IPersistFileの方を使う)、IShellLinkはAppUserModelIDに対応してないので、さらに下のIPropertyStoreインターフェイスを使って直にプロパティをいじるというもの。

ただ、プロパティの値を格納するPROPVARIANT構造体が複雑で、これに難儀していたところ、そもそものデスクトップアプリからトースト通知を送るサンプルに(正確には、その利用するWindows API Code Packに)答えがあった。
これにあるPROPVARIANTを扱うクラスは長いものだが、出し入れする値の内容を文字列に限定してしまえば(AppUserModelIDは文字列)、さほどでもない。

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 コメント :

chi-bd さんのコメント...

こんにちは。自分もC#からショートカットファイルのAppUserModelIDを設定したくて、Win7 API Code Packを使ったこんなプログラムを書いてみました。
ただ記事中にも書いた通りWin8ではうまくいかずに行き詰ってます。
何かコメントいただけると幸いです。

EMO さんのコメント...

chi-bdさん

面白いことをされてますね。というより、そちらのやり方の方が正統的なように思います。

Win8での問題については、ショートカットファイルにAppUserModelIDを設定することには成功されていて、その先の話ですよね。すいません、自分はそこまで調べてなかったので、すぐには分かりません。