だるろぐ

明日できることは、今日しない。

WinRT:システムにインストールされた Windows ストア アプリを列挙する(3)

blog.daruyanagi.jp

3年前のブログ記事のソースコードを(ググったらでてきた……)そのままコピーしても動かなかったので。

復習しておくと、システムにインストールされた Windows ストア アプリを列挙するフローはこんな感じ。

  • Windows.Management.Deployment.PackageManager の FindPackages() などでパッケージ package を取得
  • package.DisplayName は残念ながら空
    • package.InstalledLocation.Path にある AppxManifest.xml を解析して取得する
    • Description や Logo なども同様の手段で取得
  • AppxManifest.xml から DisplayName を取得すると ms-resource:// で記述されている場合がある
    • SHLoadIndirectString() API で文字列を取得する

まず、AppxManifest.xml を解析。前回との違いは、ネームスペースが変わっていても大丈夫なよう LocalName で要素を探すようにしたところぐらい。

private string GetDisplayNameFromManifest(string installedPath)
{
    var xml = File.ReadAllText(Path.Combine(installedPath, "AppxManifest.xml"));
    var doc = XDocument.Parse(xml);
    var value = doc.Descendants().First(_ => _.Name.LocalName == "DisplayName").Value;

    return value;
}

次に ms-resource:// で記述されている場合の処理。ms-resource:// の書き方は数パターンあるようで、それに応じて処理を変えないといけない。

  • ms-resource://Microsoft.SkypeApp/Resources/SkypeVideo_ProductName
  • ms-resource:///Resources/AppStoreName
  • ms-resource:PackageDisplayName

最初のパターンに寄せてから、SHLoadIndirectString() を使うことにした。

[DllImport("shlwapi.dll", BestFitMapping = false, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = false, ThrowOnUnmappableChar = true)]
public static extern int SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, int cchOutBuf, IntPtr ppvReserved);

private string GetTextFromResource(string installedPath, string name, string key)
{
    var pri = Path.Combine(installedPath, "resources.pri");
    var resourceKey = string.Empty;

    if (key.Contains(name))
    {
        // キーにアプリ名が含まれているパターン → そのままリソースキーとして扱う
        // ms-resource://Microsoft.SkypeApp/Resources/SkypeVideo_ProductName
        resourceKey = key;
    }
    else
    {
        // リソースキーの解析が必要なパターン
        // → とりあえずプロトコルを省く
        // ms-resource:///Resources/AppStoreName
        // ms-resource:/manifest/DisplayName
        // ms-resource:PackageDisplayName
        resourceKey = key.Split(':').Last();

        if (resourceKey.StartsWith("/"))
        {
            // パスになっているパターン → アプリ名入りのパスに
            // :///Resources/AppStoreName
            // :/manifest/DisplayName
            resourceKey = resourceKey.TrimStart('/');
            resourceKey = $"ms-resource://{name}/{resourceKey}";
        }
        else
        {
            // リソース名になっているパターン → リソースへのパスに
            // :PackageDisplayName
            resourceKey = $"ms-resource://{name}/resources/{resourceKey}";
        }
    }

    var buffer = new StringBuilder(1024); // ← 適当

    var result = SHLoadIndirectString(
        $"@{{{pri}? {resourceKey}}}", 
        buffer, buffer.Capacity, 
        IntPtr.Zero
    );

    // 失敗したときはパスをそのまま返しておく
    return result == 0 ? buffer.ToString() : key; 
}

f:id:daruyanagi:20170702051825p:plain

うまくいっているみたい。「ストア」アプリと表記の違うパッケージもあるけど、今のところは気にしないことにする。