Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2018年10月15日] [更新 2021年11月07日] Delphi サンプルプログラム集
   
356_インストールされているアプリと Delphi のバージョン
動作確認等 Windows 7 U64(SP1) + Delphi XE5(UP2) Pro VCL-32
後半は Windows 10 ビルド 19042 + Delphi XE5(UP2) Pro VCL-32
356_InstalledAppList.zip [14.9 MB] 2021年11月07日版 (EXE 同梱)







インストールされているアプリと Delphi のバージョン


システムにインストールされているアプリケーションと,システムにインストールされている Delphi のパーションを,レジストリから取得するサンプルです.
後半は,Windows 10 にインストールされているアプリの一覧を取得するサンプルです.






01_インストールされているアプリの一覧を取得


システムにインストール済みのアプリは,レジストリの [HKEY_LOCAL_MACHINE] の以下の階層下のキーに情報があります.本サンプルはこのキーの階層下のキー名の一覧を取得して,[UninstallString] 項目値が有効なアプリだけを一覧表示します.
以下のキーは 32 ビットの Windows の場合です.

SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall

64 ビットの Windows では [ HKEY_LOCAL_MACHINE] の以下の階層下のキーが対象です.


64 ビットのアプリ
SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
32 ビットのアプリ
Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall

以下のサンプルで取得するアプリの一覧は [コントロールパネル] の [プログラムのアンインストールまたは変更] で表示されるリストとぼぼ同じです.これらのレジストリエントリーには,Windows に付属している電卓やメモ帳の情報は格納されていません.

アプリケーションの中には,[HKEY_CURRENT_USER] [Software] や [HKEY_LOCAL_MACHINE] [Software] の階層直下のキーに情報を格納していることがあります.また,アンインストールの機能も独自で実装しているアプリもあります.



図1
実行結果

リスト1
インストールされているアプリの一覧を取得
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, Generics.Collections, Generics.Defaults;

type
  TAppInfo = record
    DisplayName     : string;
    InstallLocation : string;
    UninstallString : string;
  end;

  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    Label1: TLabel;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private 宣言 }
    function Is64bitWindows: Boolean;
  public
    { Public 宣言 }
    FAppList : TList<TAppInfo>;
  end;

var
  Form1: TForm1;

implementation

uses
  Registry,
  StrUtils;

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//  ジェネリックスのTListを生成
//  リストに格納するのは各アプリの情報を格納するレコード型
//
// ジェネリックスの使用にはusesにGenerics.Collectionsが必要 
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  FAppList := TList<TAppInfo>.Create;
end;

//=============================================================================
//  フォーム破棄時の処理
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  FreeAndNil(FAppList);
end;

//=============================================================================
//  インストール済みのアプリの一覧をレコード型に格納して一覧表示
//  正確にはHKEY_LOCAL_MACHINEの以下のキーの階層下のキーの項目を検索
//  (それらのキーのサブキーは検索しない - 再帰的な検索はしない)
//
//  64ビットWindowsの場合は以下の2つの場所
//  SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
//  SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall
//
//  32ビットWindowsの場合は以下の場所
//  SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
//
//  レコード型に格納するのは各キーの以下の項目の値
//  Windowsのセキュリティ用の更新ソフト(KBXXXX)は除外
//  DisplayName
//  InstallLocation
//  UninstallString
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);

  //---------------------------------------------------------------------------
  //  引数のARegKey内のサブキー内の項目を検索
  //  ジェネリックのTListに各アプリの情報をレコード型として格納
  //---------------------------------------------------------------------------
  procedure GetAppInfo(AReg: TRegistry; ARegKey: string; AList: TList<TAppInfo>);
  var
    LKeyNameList : TStringList;
    LKeyEachApp  : string;
    LDisplayName : string;
    LInstLocate  : string;
    LUninstStr   : string;
    LLoopCnt     : Integer;
    LAppInfo     : TAppInfo;
  begin
    LKeyNameList := TStringList.Create;
    try
      if not AReg.OpenKey(ARegKey, False) then exit;
      // ARegKey階層下のキー名の一覧を取得
      AReg.GetKeyNames(LKeyNameList);

      AReg.CloseKey;
      // キー名の一覧の各キーの項目を調べる
      for LLoopCnt := 0 to LKeyNameList.Count - 1 do begin
        try
          LKeyEachApp := ARegKey + PathDelim + LKeyNameList[LLoopCnt];
          if AReg.OpenKey(LKeyEachApp, False) then begin
            if not AReg.ValueExists('DisplayName') then Continue;
            if not AReg.ValueExists('UninstallString') then Continue;

            LDisplayName := AReg.ReadString('DisplayName');
            if ContainsText(LDisplayName, '(KB') then Continue;
            LUninstStr := AReg.ReadString('UninstallString');

            if not AReg.ValueExists('InstallLocation') then begin
              LInstLocate  := '';
            end else begin
              LInstLocate  := AReg.ReadString('InstallLocation');
            end;

            if (LDisplayName <> '') and (LUninstStr <> '') then begin
              LAppInfo.DisplayName     := LDisplayName;
              LAppInfo.InstallLocation := LInstLocate;
              LAppInfo.UninstallString := LUninstStr;
              AList.Add(LAppInfo);
            end;
          end;
        finally
          AReg.CloseKey;
        end;
      end;
    finally
      FreeAndNil(LKeyNameList);
    end;
  end;
  //---------------------------------------------------------------------------

var
  LRegistry    : TRegistry;
  LKeyUnInst   : String;
  LAppInfo     : TAppInfo;
  LDisplayName : string;
  LInstLocate  : string;
  LLoopCnt     : Integer;
begin
  Memo1.Lines.Clear;

  LRegistry := TRegistry.Create;
  try
    if Is64bitWindows then begin
      // 64ビットWindowsの場合
      LRegistry.Access  := KEY_READ or KEY_WOW64_32KEY;
      LRegistry.RootKey := HKEY_LOCAL_MACHINE;
      LKeyUnInst := 'Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall';
      GetAppInfo(LRegistry, LKeyUnInst, FAppList);
      LRegistry.CloseKey;

      LRegistry.Access := KEY_READ or KEY_WOW64_64KEY;
      LKeyUnInst := 'Software\Microsoft\Windows\CurrentVersion\Uninstall';
      GetAppInfo(LRegistry, LKeyUnInst, FAppList);
    end else begin
      // 32ビットWindowsの場合
      LRegistry.Access  := KEY_READ;
      LRegistry.RootKey := HKEY_LOCAL_MACHINE;
      LKeyUnInst := 'Software\Microsoft\Windows\CurrentVersion\Uninstall';
      GetAppInfo(LRegistry, LKeyUnInst, FAppList);
    end;
    LRegistry.CloseKey;

    //-------------------------------------------------------------------------
    //  リストのソート
    //  TComparer関係の使用にはusesにGenerics.Defaultsが必要
    //-------------------------------------------------------------------------
    FAppList.Sort(TComparer<TAppInfo>.Construct(
      function(const Left, Right: TAppInfo): Integer
      begin
        //DisplayNameの値でソート
        Result := SysUtils.CompareText(Left.DisplayName, Right.DisplayName);
      end)
    );
    //-------------------------------------------------------------------------

    // 重複項目を削除
    for LLoopCnt := FAppList.Count - 2 downto 0 do begin
      LDisplayName := FAppList[LLoopCnt].DisplayName;
      LInstLocate  := FAppList[LLoopCnt].InstallLocation;
      if (LDisplayName = FAppList[LLoopCnt + 1].DisplayName)
          and (LInstLocate = FAppList[LLoopCnt + 1].InstallLocation) then begin
        FAppList.Delete(LLoopCnt + 1);
      end;
    end;


    // DisplayNameだけを一覧表示
    // [プログラムのアンインストールまたは変更]のリストとほぼ同じ 
    Memo1.Lines.BeginUpdate;
    for LAppInfo in FAppList do begin
      Memo1.Lines.Add(LAppInfo.DisplayName);
    end;
    Memo1.Lines.EndUpdate;

    Label1.Caption := IntToStr(FAppList.Count);
  finally
    FreeAndNil(LRegistry);
  end;
end;

//-----------------------------------------------------------------------------
//  32ビットのWindowsか64ビットのWindowsかを調べる関数
//  64ビット版のWindowsの場合はTrueを返す
//-----------------------------------------------------------------------------
function TForm1.Is64bitWindows: Boolean;
var
  Wow64Proc : function(hProcess: THandle; var Wow64: BOOL): BOOL stdcall;
  RetFlag   : LongBool;
begin
  @Wow64Proc := GetProcAddress(GetModuleHandle('Kernel32.dll'), 'IsWow64Process');
  if @Wow64Proc <> nil then begin
    Wow64Proc(GetCurrentProcess, RetFlag);

    if SizeOf(THandle) = 4 then begin
      Result := RetFlag;
    end else
    if SizeOf(THandle) = 8 then begin
      Result := True;
    end;
  end else begin
    //Windows XPの64ビット版か
    Result := CheckWin32Version(5, 2);
  end;
end;

end.




02_インストールされている Delphi のバージョンを取得


下の表は各バージョンの Delphi の情報が格納されているレジストリの場所です.
これらのキーの項目値,あるいはこれらの階層下のキーの項目値から Delphi の各バージョンの情報を取得して表示するサンプルです.



Delphi のバージョン HKEY_CURRENT_USER\Software HKEY_LOCAL_MACHINE\Software
Delphi 1 (Delphi) - -
Delphi 2 Borland\Delphi\2.0 Borland\Delphi\2.0
Delphi 3 / 3.1 Borland\Delphi\3.0 Borland\Delphi\3.0
Delphi 4 Borland\Delphi\4.0 Borland\Delphi\4.0
Delphi 5 Borland\Delphi\5.0 Borland\Delphi\5.0
Delphi 6 Borland\Delphi\6.0 Borland\Delphi\6.0
Delphi 7 / 7.1 Borland\Delphi\7.0 Borland\Delphi\7.0
Delphi 2005 Borland\BDS\3.0 Borland\BDS\3.0
BDS 2006 Borland\BDS\4.0 Borland\BDS\4.0
Delphi 2007 / 2007-R2 Borland\BDS\5.0 Borland\BDS\5.0
Delphi 2009 CodeGear\BDS\6.0 CodeGear\BDS\6.0
Delphi 2010 CodeGear\BDS\7.0 CodeGear\BDS\7.0
Delphi XE Embarcadero\BDS\8.0 Embarcadero\BDS\8.0
Delphi XE2 Embarcadero\BDS\9.0 Embarcadero\BDS\9.0

Delphi XE3

Embarcadero\BDS\10.0 Embarcadero\BDS\10.0
Delphi XE4 Embarcadero\BDS\11.0 Embarcadero\BDS\11.0
Delphi XE5 Embarcadero\BDS\12.0 Embarcadero\BDS\12.0
Delphi XE6 Embarcadero\BDS\14.0 Embarcadero\BDS\14.0
Delphi XE7 Embarcadero\BDS\15.0 Embarcadero\BDS\15.0
Delphi XE8 Embarcadero\BDS\16.0 Embarcadero\BDS\16.0
Delphi 10 Seattle Embarcadero\BDS\17.0 Embarcadero\BDS\17.0
Delphi 10.1 Berlin Embarcadero\BDS\18.0 Embarcadero\BDS\18.0
Delphi 10.2 Tokyo Embarcadero\BDS\19.0 Embarcadero\BDS\19.0


上の表を見ると,ある程度の規則性があることが確認できます.この規則性を利用します.
レジストリから取得した Delphi の各バージョンの情報は,レコード型のメンバの変数に代入していきます.そのレコード型の変数をジェネリックスの TList<T> の要素として格納します.
取得結果はプロダクトバージョンの値でソートして,古いバージョンの Delphi の情報から順番に表示するようにしています.



[備考 1]
筆者の開発環境には,Delphi 2005 / 2006 / 2007 / 2007-R2 / 2009 / 2010 がインストールされていません.したがって,これらのバージョンでの情報取得は確認していません.
Delphi 2005 で情報が取得できれば,それ以降のバージョンでも取得可能と思われます.
Delphi 1 (Delphi) はレジストリを使用していません.



図2
実行結果
  • Delphi XE 以降
図3
実行結果
  • Delphi 2 - Delphi 7

リスト2
インストールされている Delphi のバージョンを取得
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, Generics.Collections, Generics.Defaults;

type
  TDelphiVerInfo = record
    ProductVer  : Double;
    AppFullPath : string;
    RegKeyBase  : string;
    VersionStr  : string;
    Enable64bit : Boolean;
  end;

  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    Label1: TLabel;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    FAppList : TList<TDelphiVerInfo>;
  end;

  procedure GetDelphiVerList(AList: TList<TDelphiVerInfo>);

var
  Form1: TForm1;

implementation

uses
  Registry,
  StrUtils;

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//  ジェネリックスのリストを生成
//  リストに格納するのはDephiの各バージョンの情報を格納するレコード型
//
// ジェネリックスの使用にはusesにGenerics.Collectionsが必要 
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  FAppList := TList<TDelphiVerInfo>.Create;
end;

//=============================================================================
//  フォーム破棄時の処理
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  FreeAndNil(FAppList);
end;

//=============================================================================
//  インストール済みのDelphiの各バージョンの情報を取得して表示
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LAppInfo : TDelphiVerInfo;
begin
  Memo1.Lines.Clear;
  FAppList.Clear;

  GetDelphiVerList(FAppList);

  Memo1.Lines.BeginUpdate;
  for LAppInfo in FAppList do begin
    Memo1.Lines.Add(LAppInfo.VersionStr);
    Memo1.Lines.Add('ProductVersion ' + FloatToStr(LAppInfo.ProductVer));
    Memo1.Lines.Add(LAppInfo.AppFullPath);
    Memo1.Lines.Add(LAppInfo.RegKeyBase);
    if LAppInfo.Enable64bit then begin
      Memo1.Lines.Add('Enable 64 bit Application');
    end;
    Memo1.Lines.Add('');
  end;
  Memo1.Lines.EndUpdate;

  Label1.Caption := IntToStr(FAppList.Count);
end;

//-----------------------------------------------------------------------------
//  インストール済みDelphiの各バージョンの情報をジェネリックスのリストに格納
//  リストに格納するのはDephiの各バージョンの情報を格納するレコード型
//-----------------------------------------------------------------------------
procedure GetDelphiVerList(AList: TList<TDelphiVerInfo>);

  procedure InitializeRecord(var AInfo: TDelphiVerInfo);
  begin
    AInfo.ProductVer  := 0.0;
    AInfo.AppFullPath := 'Empty';
    AInfo.RegKeyBase  := 'Empty';
    AInfo.VersionStr  := 'Empty';
    AInfo.Enable64bit := False;
  end;

const
  LArrayBDS : array [0..2] of string
              = ('\Borland\BDS', '\CodeGear\BDS', '\Embarcadero\BDS');
var
  LRegistry    : TRegistry;
  LKeyBDS      : string;
  LKeyEeachVer : string;
  LKeyNameList : TStringList;
  LKeyItemList : TStringList;
  LAppInfo     : TDelphiVerInfo;
  LTempValue   : string;
  LLoopCnt     : Integer;
  LLoopBDS     : Integer;
begin
  AList.Clear;

  LRegistry    := TRegistry.Create;
  LKeyNameList := TStringList.Create;
  LKeyItemList := TStringList.Create;

  try
    LRegistry.Access  := KEY_READ;
    LRegistry.RootKey := HKEY_LOCAL_MACHINE;

    //-------------------------------------------------------------------------
    // Delphi 2 - 7
    // Delphi 4 までは最初の項目がDelphi のバージョン文字列
    // Delphi 5 からは項目名の App がEXEのフルパス
    //-------------------------------------------------------------------------
    LKeyBDS := 'Software\Borland\Delphi';

    if LRegistry.OpenKey(LKeyBDS, False) then begin
      // 現在のキーの階層下のキー名の一覧を取得
      // このキーがDelphiの各バージョンごとのキー
      LRegistry.GetKeyNames(LKeyNameList);

      for LLoopCnt := 0 to LKeyNameList.Count - 1 do begin
        LKeyEeachVer := LKeyBDS + PathDelim + LKeyNameList[LLoopCnt];

        LRegistry.CloseKey;
        InitializeRecord(LAppInfo);

        if LRegistry.OpenKey(LKeyEeachVer, False) then begin
          LAppInfo.ProductVer := StrToFloat(LKeyNameList[LLoopCnt]);

          if LAppInfo.ProductVer < 5.0 then begin
            // 現在のDelphiのキー名の各項目名(名前:レジストリエントリ)一覧を取得
            LRegistry.GetValueNames(LKeyItemList);
            if LKeyItemList.Count < 1 then Continue;
            LAppInfo.AppFullPath  := LRegistry.ReadString(LKeyItemList[0]);
          end else begin
            if not LRegistry.ValueExists('App') then Continue;
            LAppInfo.AppFullPath := LRegistry.ReadString('App');
          end;

          // Delphi のバージョン文字列の作成
          // 小数点以下が 0 の時は整数部だけとする
          LTempValue := Trim(LKeyNameList[LLoopCnt]);
          if RightStr(LTempValue, 2) = '.0' then begin
            LTempValue := LeftStr(LTempValue, 1);
          end;

          LAppInfo.VersionStr := 'Delphi ' + LTempValue;
          LAppInfo.RegKeyBase := LKeyEeachVer;

          // ジェネリックスのリストにレコード型のデータを追加
          AList.Add(LAppInfo);
        end;
      end;
    end;
    LRegistry.CloseKey;


    //-------------------------------------------------------------------------
    // Delphi 2005 以降
    //-------------------------------------------------------------------------
    for LLoopBDS := 0 to Length(LArrayBDS) - 1 do begin
      LKeyBDS := 'Software' + LArrayBDS[LLoopBDS];

      if LRegistry.OpenKey(LKeyBDS, False) then begin
        // 現在のキーの階層下のキー名の一覧を取得
        // このキーがDelphiの各バージョンごとのキー
        LRegistry.GetKeyNames(LKeyNameList);

        for LLoopCnt := 0 to LKeyNameList.Count - 1 do begin
          LKeyEeachVer := LKeyBDS + PathDelim + LKeyNameList[LLoopCnt];

          LRegistry.CloseKey;
          InitializeRecord(LAppInfo);

          if LRegistry.OpenKey(LKeyEeachVer, False) then begin
            if not LRegistry.ValueExists('App') then Continue;
            LAppInfo.AppFullPath := LRegistry.ReadString('App');

            if LRegistry.ValueExists('ProductVersion') then begin
              LAppInfo.ProductVer := StrToFloat(LRegistry.ReadString('ProductVersion'));
            end;

            LRegistry.CloseKey;
            // Delphi XE,Delphi XE5,..等の文字列を取得
            if LRegistry.KeyExists(LKeyEeachVer + '\Personalities') then begin
              if LRegistry.OpenKey(LKeyEeachVer + '\Personalities', False) then begin
                if LRegistry.ValueExists('Delphi.Win32') then begin
                  LAppInfo.VersionStr := LRegistry.ReadString('Delphi.Win32');
                end;
              end;
            end;

            LAppInfo.RegKeyBase := LKeyEeachVer;

            // 64 ビットの開発が可能かを調査
            if ContainsText(LKeyBDS, 'Embarcadero\BDS') then begin
              LRegistry.CloseKey;
              if LRegistry.KeyExists(LKeyEeachVer + '\Library\Win64') then begin
                // 64ビット用のWindowsアプリが開発可能な場合はTrue
                LAppInfo.Enable64bit := True;
              end else begin
                LAppInfo.Enable64bit := False;
              end;
            end;

            AList.Add(LAppInfo);
          end;
        end;
      end;
      LRegistry.CloseKey;
    end;

    //---------------------------------------------------------
    //  プロダクトバージョンの値でリストをソート
    //  TComparer関係の使用にはusesにGenerics.Defaultsが必要
    //---------------------------------------------------------
    AList.Sort(TComparer<TDelphiVerInfo>.Construct(
      function(const Left, Right: TDelphiVerInfo): Integer
      begin
        Result := Trunc((Left.ProductVer - Right.ProductVer) * 10);
      end)
    );
  finally
    FreeAndNil(LRegistry);
    FreeAndNil(LKeyNameList);
    FreeAndNil(LKeyItemList);
  end;
end;

end.


[備考 2]
デバッグ中であれば,環境変数 $(DELPHI) または $(BDS) でインストール先のフォルダのパス名を取得することができます.

[備考 3]
IDE の機能拡張の実装やコンポーネントのプロパティエディタの実装の場合は,OpenToolsAPI (OTA) の機能を利用して IDE 関係の情報を取得することができます.

[備考 4]
Delphi のインストール先のフォルダ名は Delphi のバージョン番号,コンパイラバージョン等の値ではありません.これは別名 (Other names) とも呼ばれていますが,Delphi 2005 が BDS 3 です.そこからの連番になっています.
Delphi XE7 は 15.0, Delphi XE8 は 16.0 というようになっています (13.0 は欠番).





03_インストールされているアプリ一覧 - Windows 10


Windows 10 では,インストールされているアプリのリストが,仮想フォルダ AppsFolder にあります.そのフォルダ内のリストを取得して表示するサンプルです.
仮想フォルダ AppsFolder のリストには,下図のような [スタート] メニューの展開部分も含まれています.仮想フォルダ AppsFolder のリストには,[スタート] メニューに登録してないアプリはありません.



図4
[スタート] メニューのアプリのリスト
  • 展開リストもインストールされているアプリの一覧に含まれる

スタート] メニューに登録していないデスクトップアプリも,そのアプリの EXE ファイルのコンテキストメニューから [スタートメニューにピン留めする] を選択すれば一覧に表示できます.スタートメニューにピン留めすると,例えば以下の場所にその EXE のショートカットが作成されます.このフォルダ内にショートカットを作成すると,スタートメニューに登録されて本サンプルのアプリの一覧に表示されます.
このショートカットの名前 (表示名) を変更すれば,アプリの一覧に表示するアプリ名も変わります.


%APPDATA%\Microsoft\Windows\Start Menu\Programs

追加したショートカットがあるフォルダは,AppUserModelId のコンテキストメニューの [ファイルの場所を開く] で表示できます.



図5
スタートメニュー
  • TEAD というアプリの例
  • 左はショートカット作成
  • 右はスタートメニューにピン留めした結果

サンプルの実行結果です.左側がアプリ名,右側の AppUserModelId は,アプリケーション識別子と呼ばれる値です.[全コピー] は,それらの値をタブ区切りの文字列としてでクリップボードにコピーします.タブ区切りの文字列は,エクセルのセルにそのまま貼り付けすることができます.選択項目をダブルクリックすると,そのアプリが起動します.



図6
実行結果

リスト3
インストールされているアプリの一覧を取得 - Windows 10 用
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics,Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls,
   Winapi.ShellAPI,  Winapi.ActiveX, Winapi.ShlObj, Winapi.CommCtrl, Vcl.ImgList,
   Vcl.ComCtrls;

type
  TForm1 = class(TForm)
    ImageList1: TImageList;
    ListView1: TListView;
    Button1: TButton;
    Button2: TButton;
    Edit1: TEdit;
    procedure FormCreate(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure ListView1DblClick(Sender: TObject);
    procedure ListView1SelectItem(Sender: TObject; Item: TListItem;
      Selected: Boolean);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

  function GetDispNameFromPItemID(ShFolder: IShellFolder; ItemID:
    PItemIDList; Flag: DWORD=SHGDN_NORMAL): string;

var
  Form1: TForm1;

implementation

uses
  Vcl.Clipbrd;

{$R *.DFM}

//=============================================================================
//  フォーム生成時の処理
//  一覧表示用の TListView の設定
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  ImageList1.BkColor    := ListView1.Color;
  ListView1.ViewStyle   := vsReport;
  ListView1.SmallImages := ImageList1;

  ListView1.Columns.Add;
  ListView1.Columns[0].Caption := ' 名前';
  ListView1.Columns[0].Width   := 200;

  ListView1.Columns.Add;
  ListView1.Columns[1].Caption := 'AppUserModelId';
end;

//=============================================================================
//  インストールされているアプリをリスト - Windows 10
//  仮想フォルダ AppsFolder 内のリストを取得 
//  ( アプリ名と AppUserModelId の値を列挙 )
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LPath        : WideString;
  LSHDesktop   : IShellFolder;
  LSHAppFolder : IShellFolder;
  LpidlRoot    : PItemIDList;
  LAppIdList   : IEnumIDList;
  LFetched     : Cardinal;
  LpidlChild   : PItemIDList;
  LAppName     : string;
  LAppIDPath   : string;
  LpidlFull    : PItemIDList;
  LSHFileInfo  : TSHFileInfo;
  LIcon        : TIcon;
  LListItem    : TListItem;
begin
  Screen.Cursor := crHourGlass;
  ListView1.Items.Clear;
  ListView1.RowSelect := True;
  ListView1.Items.BeginUpdate;

  LpidlRoot   := nil;
  LpidlChild  := nil;

  LPath := 'shell:AppsFolder';
  try
    // ファイルを項目識別子に変換
    LpidlRoot := ILCreateFromPath(PChar(LPath));
    if ILIsEmpty(LpidlRoot) then Exit;

    // 対象の項目識別子を[デスクトップ]のシェルフォルダオブジェクトにバインド
    // 対象のフォルダの IShellFolder オブジェクトが取得できる
    SHGetDesktopFolder(LSHDesktop);
    LSHDesktop.BindToObject(LpidlRoot, nil, IShellFolder, Pointer(LSHAppFolder));

    // ファイル (項目識別子) のリストを取得
    LSHAppFolder.EnumObjects(Handle, SHCONTF_NONFOLDERS, LAppIdList);

    // リストの各項目の表示名を調べる
    // ここで取得した項目識別子は解放しないこと
    LAppIdList.Reset;
    while (LAppIdList.Next(1, LpidlChild, LFetched) = S_OK) do begin
      LAppName := GetDispNameFromPItemID(LSHAppFolder, LpidlChild, SHGDN_NORMAL);
      if LAppName = '' then Continue;

      LAppIDPath := GetDispNameFromPItemID(LSHAppFolder, LpidlChild, SHGDN_FORPARSING);
      if LAppIDPath = '' then Continue;

      LListItem         := ListView1.Items.Add;
      LListItem.Caption := LAppName;
      LListItem.SubItems.Add(LAppIDPath);

      // ファイルの項目識別子の値からアイコンを取得
      LpidlFull := ILCombine(LpidlRoot, LpidlChild);
      SHGetFileInfo(Pointer(LpidlFull),
                    0,
                    LSHFileInfo,
                    SizeOf(TSHFileInfo),
                    SHGFI_PIDL or SHGFI_ICON or SHGFI_SMALLICON);
      LIcon := TIcon.Create;
      try
        LIcon.Handle := LSHFileInfo.hIcon;
        LListItem.ImageIndex := ListView1.SmallImages.AddIcon(LIcon);
      finally
        FreeAndNil(LIcon);
        CoTaskMemFree(LpidlFull);
      end;
    end;
  finally
    LSHDesktop := nil;
    LSHAppFolder := nil;
    CoTaskMemFree(LpidlRoot);
    Screen.Cursor := crDefault;
  end;

  ListView1.SortType := stText;
  ListView1.Items.EndUpdate;
  FormResize(nil);
end;

//=============================================================================
//  表示しているアプリ名と AppUserModelId の値をクリップボードにコピー
//  TAB 区切りテキスト.エクセルに貼り付け可能
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LStrList : TStringList;
  LText    : string;
  Lindex   : Integer;
begin
  LStrList := TStringList.Create;
  try
    for Lindex := 0 to ListView1.Items.Count -1 do begin
      LText := ListView1.Items[Lindex].Caption + Char(VK_TAB)
             + ListView1.Items[Lindex].SubItems[0];
      LStrList.Add(LText);
    end;
    Clipboard.AsText := LStrList.Text;
  finally
    FreeAndNil(LStrList);
  end;
end;

//=============================================================================
//  TListView の OnDblClick イベント
//  ダブルクリックしたら選択しているアプリを起動
//  本 EXE が 32 ビットの場合,起動しない 64 ビットのアプリがある
//=============================================================================
procedure TForm1.ListView1DblClick(Sender: TObject);
var
  LItem  : TListItem;
  LAppId : string;
begin
  LItem := ListView1.Selected;
  if LItem = nil then Exit;

  LAppId := 'shell:AppsFolder' + PathDelim + Litem.SubItems[0];
  ShellExecute(0, '', PChar(LAppId), nil, nil, SW_SHOW);
end;

//=============================================================================
//  TListView の OnSelectItem イベント
//  TListView の Item を選択したら AppUserModelId の値を TEdit に表示
//=============================================================================
procedure TForm1.ListView1SelectItem(Sender: TObject; Item: TListItem;
  Selected: Boolean);
begin
  if Item = nil then Exit;
  Edit1.Text := Item.SubItems[0];
end;

//=============================================================================
//  フォームの幅を変更したらカラムの幅も変更
//=============================================================================
procedure TForm1.FormResize(Sender: TObject);
begin
  ListView1.Columns[1].Width := ListView1.ClientWidth - ListView1.Columns[0].Width;
end;

//-----------------------------------------------------------------------------
//  相対項目識別子からフォルダまたはファイル名を求める
//  仮想フォルダは GUID 文字列として求まる
//
//  第 1 引数  対象の項目識別子を取得する際に使用したシェルフォルダオブジェクト
//  第 2 引数  対象の項目識別子.ShFolder 階層下の相対値
//  第 3 引数  フルパスを取得する場合は,SHGDN_FORPARSING
//             ファイル名だけ取得する場合は,SHGDN_NORMAL また はSHGDN_INFOLDER
//-----------------------------------------------------------------------------
function GetDispNameFromPItemID(ShFolder: IShellFolder; ItemID:
  PItemIDList; Flag: DWORD=SHGDN_NORMAL): string;
var
  StrRet : TStrRet;
begin
  Result := '';
  if ShFolder = nil then exit;
  if ItemID = nil then exit;

  FillChar(StrRet, SizeOf(StrRet), 0);
  if NOERROR = ShFolder.GetDisplayNameOf(ItemID, Flag, StrRet) then begin
    Result := StrRet.pOleStr;
  end;
end;

end.


筆者の環境には 64 ビット版の Google Chrome ブラウザがインストールされています.その AppUserModelId の値は Chorme だけとなっています.
添付のサンプルの EXE は 32 ビットです.この Google Chrome を起動できません.EXE が 32 ビットなので 32 ビットの EXE を探しに行きます.そのため,下図のようなエラーとなります.本サンプルを 64 ビットの EXE としてコンパイルして実行すれば,64 ビット版の Google Chrpme は起動します.

AppUserModelId の値を指定しない場合,AppUserModelId の値は,そのアプリケーションのインストールの際にシステムが自動的に設定します.おそらく Google hrome は Chrome という値を指定しているものと思われます.



図7
32 ビットの EXE を探す例

Google Chrome が何故そのような設定にしているのか,筆者に知る術はありません.以下のように,Shell コマンドではなく,普通に Chrome というコマンドを実行すれば起動します.


  if Litem.SubItems[0] = 'Chrome' then begin
    LAppId := Litem.SubItems[0];
  end else begin
    LAppId := 'shell:AppsFolder' + PathDelim + Litem.SubItems[0];
  end;
  ShellExecute(0, '', PChar(LAppId), nil, nil, SW_SHOW);


Windows が 64 ビット版で,本サンプルが 32 ビットの EXE の場合,スクリーンキーボードも起動しません.64 ビット版の Windows には,32 ビット版のスクリーンキーボードはありません.メモ帳や電卓は 32 ビット版と 64 ビット版がありますから,本サンプルが 32 ビットの EXE でも 64 ビットの EXE でも起動します.

32 ビットの EXE から 64 ビットのスクリーンキーボードが起動できるようにするには,ファイルシステムのリダイレクト機能を無効にします.後の方に具体的なサンプルがあります.

Delphi 6 / Delphi 7 等の旧いバージョンの Delphi を IDE から起動すると,下図のようなモジュールの読み込みエラーが発生します.この場合は [はい] のボタンをクリックします.そうしないと次回の起動時に問題が発生する可能性があります.EXE を直接起動して実行すればエラーは発生しません.



図8
エラー

//=============================================================================
//  Delphi 6 / Delphi 7 を起動するとエラーが発生する
//  このコードの実行には uses に Winapi.ShellAPI が必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LFilepath : string;
begin
  LFilepath := 'C:\Program Files (x86)\Borland\Delphi6\Bin\delphi32.exe';
  ShellExecute(0, '', PChar(LFilepath), nil, nil, SW_SHOW);
end;


ソートの結果が,後の方の仮想リストビューを使用するサンプルと異なっています.仮想リストビューを使用したサンプルと同じ結果にするには,以下のように OnCompare イベントを使用して,Unocode の文字列としてソートします.


//=============================================================================
//  TListView の OnCompare イベント
//  Unicode の文字列としてソートする
//
//  TListView.SortType := stBoth にして,このイベントで
//  CompareText (System.SysUtils) 関数を使用してソートする
//=============================================================================
procedure TForm1.ListView1Compare(Sender: TObject; Item1, Item2: TListItem;
  Data: Integer; var Compare: Integer);
begin
  Compare := CompareText(Item1.Caption, Item2.Caption)
end;


[備考 1]
リストが表示されるまで少し時間がかかりますが,続けて実行すると,キャッシュが働いて少しだけ処理が速くなります.アイコン画像を取得する部分のコードを削除すれば高速になります.


[備考 2]
ShellExecuteEx 関数を使用すると,項目識別子の値でファイルを [開く] ことができます.


[備考 3]
UWP アプリは,Windows のランタイムライブラリである WinRT を使用する COM の拡張オブジェクトとしての位置づけとなっています.Delphi 10.2 では,WinRT の機能を利用するためのユニット類が実装されています.それらのユニットは,主に source\rtl\win\winrt 内にあります.例えば WinAPI.ApplicationModel.pas というユニットがあります.




04_インストールされているアプリ一覧 ( IShellItem ) - Windows 10


エクスプローラでフォルダ内のファイルを [詳細] で表示すると,一般的には複数の列があります.それらは [名前] [更新日時 ] [作成日時] [サイズ] [種類] 等です.前項のサンプルでは列方向の処理はしていません.[名前] に相当する列の表示名を変更しているだけです.列の処理が必要ない場合,IShellItem インターフェイスのオブジェクトが使用できます. 
本サンプルは IShellItem を使用する例です.実行結果は,前項のサンプルと同じです.


リスト4
インストールされているアプリの一覧を取得 - Windows 10 用
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics,Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls,
   Winapi.ShellAPI,  Winapi.ActiveX, Winapi.ShlObj, Winapi.CommCtrl, Vcl.ImgList,
   Vcl.ComCtrls;

type
  TForm1 = class(TForm)
    ImageList1: TImageList;
    ListView1: TListView;
    Button1: TButton;
    Button2: TButton;
    Edit1: TEdit;
    procedure FormCreate(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure ListView1DblClick(Sender: TObject);
    procedure ListView1SelectItem(Sender: TObject; Item: TListItem;
      Selected: Boolean);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses
  Vcl.Clipbrd;

{$R *.DFM}

//=============================================================================
//  フォーム生成時の処理
//  一覧表示用の TListView の設定
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  ImageList1.BkColor    := ListView1.Color;
  ListView1.ViewStyle   := vsReport;
  ListView1.SmallImages := ImageList1;

  ListView1.Columns.Add;
  ListView1.Columns[0].Caption := ' 名前';
  ListView1.Columns[0].Width   := 200;

  ListView1.Columns.Add;
  ListView1.Columns[1].Caption := 'AppUserModelId';
end;

//=============================================================================
//  インストールされているアプリをリスト - Windows 10
//  仮想フォルダ AppsFolder 内のリストを取得 
//  ( アプリ名と AppUserModelId の値を列挙 )
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LPath        : WideString;
  LpidlRoot    : PItemIDList;
  LShItemRoot  : IShellItem;
  LShItemChild : IShellItem;
  LEnumItems   : IEnumShellItems;
  LFetched     : LongInt;
  LAppName     : PWideChar;
  LAppIDPath   : PWideChar;
  LpidlFull    : PItemIDList;
  LSHFileInfo  : TSHFileInfo;
  LIcon        : TIcon;
  LListItem    : TListItem;
begin
  Screen.Cursor := crHourGlass;
  ListView1.Items.Clear;
  ListView1.RowSelect := True;
  ListView1.Items.BeginUpdate;

  LpidlRoot    := nil;
  LShItemRoot  := nil;
  LShItemChild := nil;

  LPath := 'shell:AppsFolder';
  try
    // ファイルを項目識別子に変換
    LpidlRoot := ILCreateFromPath(PChar(LPath));
    if ILIsEmpty(LpidlRoot) then Exit;

    // フォルダの IShellItem オブジェクトを取得
    SHCreateItemFromIDList(LpidlRoot, IShellItem, LShItemRoot);

    // ファイル (IShellItem のリストを LEnumItems として取得
   LShItemRoot.BindToHandler(nil, BHID_EnumItems, IEnumShellItems, LEnumItems);

    // リストの各項目の表示名を調べる
    while (LEnumItems.Next(1, LShItemChild, @LFetched) = S_OK) do begin
      LShItemChild.GetDisplayName(SIGDN_NORMALDISPLAY, LAppName);
      if StrLen(LAppName) = 0 then Continue;
      LShItemChild.GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, LAppIDPath);
    if StrLen(LAppIDPath) = 0 then Continue;

      LListItem         := ListView1.Items.Add;
      LListItem.Caption := LAppName;
      LListItem.SubItems.Add(LAppIDPath);

      // ファイルの項目識別子の値からアイコンを取得
      LPath := LAppIDPath;
      LPath := 'shell:AppsFolder' + PathDelim + LPath;
      LpidlFull := ILCreateFromPath(PWideChar(LPath));
      SHGetFileInfo(Pointer(LpidlFull),
                    0,
                    LSHFileInfo,
                    SizeOf(TSHFileInfo),
                    SHGFI_PIDL or SHGFI_ICON or SHGFI_SMALLICON);
      LIcon := TIcon.Create;
      try
        LIcon.Handle := LSHFileInfo.hIcon;
        LListItem.ImageIndex := ListView1.SmallImages.AddIcon(LIcon);
      finally
        FreeAndNil(LIcon);
        LShItemChild := nil;
        CoTaskMemFree(LpidlFull);
      end;
    end;
  finally
    LEnumItems := nil;
    LShItemRoot := nil;
    CoTaskMemFree(LpidlRoot);
    Screen.Cursor := crDefault;
  end;

  ListView1.SortType := stText;
  ListView1.Items.EndUpdate;
  FormResize(nil);
end;

//=============================================================================
//  表示しているアプリ名と AppUserModelId の値をクリップボードにコピー
//  TAB 区切りテキスト.エクセルに貼り付け可能
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LStrList : TStringList;
  LText    : string;
  Lindex   : Integer;
begin
  LStrList := TStringList.Create;
  try
    for Lindex := 0 to ListView1.Items.Count -1 do begin
      LText := ListView1.Items[Lindex].Caption + Char(VK_TAB)
             + ListView1.Items[Lindex].SubItems[0];
      LStrList.Add(LText);
    end;
    Clipboard.AsText := LStrList.Text;
  finally
    FreeAndNil(LStrList);
  end;
end;

//=============================================================================
//  TListView の OnDblClick イベント
//  ダブルクリックしたら選択しているアプリを起動
//  本 EXE が 32 ビットの場合,起動しない 64 ビットのアプリがある
//=============================================================================
procedure TForm1.ListView1DblClick(Sender: TObject);
var
  LItem  : TListItem;
  LAppId : string;
begin
  LItem := ListView1.Selected;
  if LItem = nil then Exit;

  LAppId := 'shell:AppsFolder' + PathDelim + Litem.SubItems[0];
  ShellExecute(0, '', PChar(LAppId), nil, nil, SW_SHOW);
end;

//=============================================================================
//  TListView の OnSelectItem イベント
//  TListView の Item を選択したら AppUserModelId の値を TEdit に表示
//=============================================================================
procedure TForm1.ListView1SelectItem(Sender: TObject; Item: TListItem;
  Selected: Boolean);
begin
  if Item = nil then Exit;
  Edit1.Text := Item.SubItems[0];
end;

//=============================================================================
//  フォームの幅を変更したらカラムの幅も変更
//=============================================================================
procedure TForm1.FormResize(Sender: TObject);
begin
  ListView1.Columns[1].Width := ListView1.ClientWidth - ListView1.Columns[0].Width;
end;

end.




05_TShell を使用して AppUserModelId の一覧を取得 - Windows 10


当サイトには,Windows のシェルの機能を利用するために,タイプライブラリの取り込みで作成して Shell32_TLB.pas を使用するサンプルがいくつかあります.参考として,この Shell32_TLB.pas を使用する例を掲載しておきます.
このサンプルでは,TShell コンポーネントを実行時に生成して使用しています.
このサンプルは,処理が非常に遅いコードです.



リスト5
TShell を使用して AppUserModelId の一覧を取得.処理に非常に時間がかかる例
implementation

uses
  Shell32_TLB;

{$R *.DFM}

//=============================================================================
//  フォーム生成時の処理
//  一覧表示用の TListView の設定
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Self.Caption    := 'AppUserModelId 一覧';

  ListView1.ViewStyle   := vsReport;
  ListView1.Columns.Add;
  ListView1.Columns[0].Caption := ' 名前';
  ListView1.Columns[0].Width   := 230;
  ListView1.Columns.Add;
  ListView1.Columns[1].Caption := 'AppUserModelId';
  ListView1.Columns[1].Width := ListView1.ClientWidth - ListView1.Columns[0].Width;
end;

//=============================================================================
//  インストールされているアプリをリスト - Windows 10
//  TShell を使用して仮想フォルダ AppsFolder 内のリストを取得 
//  ( アプリ名と AppUserModelId の値を列挙 )
//
//  処理が非常に遅いコード
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LShell       : Shell32_TLB.TShell;
  LShellFolder : Shell32_TLB.Folder;
  LFolderItem  : Shell32_TLB.FolderItem;
  LListItem    : TListItem;
  LIndex       : Integer;
begin
  Screen.Cursor := crHourGlass;
  ListView1.Items.BeginUpdate;
  ListView1.Items.Clear;
  ListView1.RowSelect := True;

  LShell := TShell.Create(Self);
  try
    LShellFolder := LShell.NameSpace('shell:AppsFolder');

    for LIndex := 0 to LShellFolder.Items.Count - 1 do begin
      LFolderItem := LShellFolder.Items.Item(LIndex);

      LListItem := ListView1.Items.Add;
      LListItem.Caption := LFolderItem.Name;
      LListItem.SubItems.Add(LFolderItem.Path);
    end;
  finally
    FreeAndNil(LShell);
  end;
  ListView1.SortType := stText;
  ListView1.Items.EndUpdate;
  Screen.Cursor := crDefault;
end;


LShell.NameSpace('shell:AppsFolder').Items; を実行して,あらかじめリストを取得しておくと処理が速くなります.



リスト6
TShell を使用して AppUserModelId の一覧を取得.処理を高速化した例
implementation

uses
  Shell32_TLB;

{$R *.DFM}

//=============================================================================
//  フォーム生成時の処理
//  一覧表示用の TListView の設定
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Self.Caption    := 'AppUserModelId 一覧';

  ListView1.ViewStyle   := vsReport;
  ListView1.Columns.Add;
  ListView1.Columns[0].Caption := ' 名前';
  ListView1.Columns[0].Width   := 230;
  ListView1.Columns.Add;
  ListView1.Columns[1].Width := ListView1.ClientWidth - ListView1.Columns[0].Width;
  ListView1.Columns[1].Caption := 'AppUserModelId';
end;

//=============================================================================
//  インストールされているアプリをリスト - Windows 10
//  仮想フォルダ AppsFolder 内のリストを取得 
//  ( TShell を使用してアプリ名と AppUserModelId の値を列挙 )
//
//  処理が速いコード
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LShell       : Shell32_TLB.TShell;
  LFolderItems : Shell32_TLB.FolderItems;
  LFolderItem  : Shell32_TLB.FolderItem;
  LListItem    : TListItem;
  LIndex       : Integer;
begin
  Screen.Cursor := crHourGlass;
  ListView1.Items.BeginUpdate;
  ListView1.Items.Clear;
  ListView1.RowSelect := True;

  LShell := TShell.Create(Self);
  try
    // あらかじめリストを作成しておく
    LFolderItems := LShell.NameSpace('shell:AppsFolder').Items;

    for LIndex := 0 to LFolderItems.Count - 1 do begin
      LFolderItem := LFolderItems.Item(LIndex);

      LListItem := ListView1.Items.Add;
      LListItem.Caption := LFolderItem.Name;
      LListItem.SubItems.Add(LFolderItem.Path);
    end;
  finally
    FreeAndNil(LShell);
  end;
  ListView1.SortType := stText;
  ListView1.Items.EndUpdate;
  Screen.Cursor := crDefault;
end;


仮想リストビューの機能を利用すると更に高速処理になります.



リスト7
TShell を使用して AppUserModelId の一覧を取得.仮想リストビューを使用して処理高速化する例
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics,Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls,
   Winapi.ShellAPI,  Winapi.ActiveX, Winapi.ShlObj, Winapi.CommCtrl, Vcl.ComCtrls;

type
  TForm1 = class(TForm)
    ListView1: TListView;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure ListView1Data(Sender: TObject; Item: TListItem);
    procedure FormDestroy(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses
  Generics.Collections,
  Generics.Defaults,
  Shell32_TLB;

{$R *.DFM}

type
  PAppIdItem = ^TAppIdItem;
  TAppIdItem = Record
    AppName : string;
    AppID   : string;
    IconIdx : Integer;
  end;

var
  FAppIDList : TList<TAppIdItem>;

//=============================================================================
//  フォーム生成時の処理
//  一覧表示用の TListView の設定
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
var
  LSHFileInfo : TSHFileInfo ;
  LhImgList   : THandle;
begin
  Self.Caption    := 'AppUserModelId 一覧';

  ListView1.ViewStyle   := vsReport;
  ListView1.Columns.Add;
  ListView1.Columns[0].Caption := ' 名前';
  ListView1.Columns[0].Width   := 230;
  ListView1.Columns.Add;
  ListView1.Columns[1].Caption := 'AppUserModelId';

  FAppIDList := TList<TAppIdItem>.Create;

  // システムのイメージリストのハンドルを取得
  LhImgList := SHGetFileInfo('',
                             0,
                             LSHFileInfo,
                             SizeOf(LSHFileInfo),
                             SHGFI_ICON or SHGFI_SMALLICON or SHGFI_SYSICONINDEX);
  ListView_SetImageList(ListView1.Handle, LhImgList, LVSIL_SMALL);
end;

//=============================================================================
//  フォーム破棄時の処理
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  FreeAndNil(FAppIDList);
end;

//=============================================================================
//  インストールされているアプリをリスト - Windows 10
//  仮想フォルダ AppsFolder 内のリストを取得 
//  ( アプリ名と AppUserModelId の値を列挙 )
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LPath        : WideString;
  LAppItem     : TAppIdItem;
  LShell       : Shell32_TLB.TShell;
  LFolderItems : Shell32_TLB.FolderItems;
  LFolderItem  : Shell32_TLB.FolderItem;
  LIndex       : Integer;
begin
  ListView1.Items.BeginUpdate;
  ListView1.Items.Clear;
  ListView1.RowSelect := True;

  FAppIDList.Clear;

  LPath := 'shell:AppsFolder';

  LShell := TShell.Create(Self);
  try
    LFolderItems := LShell.NameSpace(LPath).Items;

    for LIndex := 0 to LFolderItems.Count - 1 do begin
      LFolderItem := LFolderItems.Item(LIndex);

      LAppItem.AppName := LFolderItem.Name;
      LAppItem.AppID   := LFolderItem.Path;
      LAppItem.IconIdx := -1;
      FAppIDList.Add(LAppItem);
    end;
  finally
    FreeAndNil(LShell);
  end;

    //----------------------------------------------------
    //  リストのソート
    //  TComparer関係の使用には Generics.Defaults が必要
    //----------------------------------------------------
    FAppIDList.Sort(TComparer<TAppIdItem>.Construct(
      function(const Left, Right: TAppIdItem): Integer
      begin
        // AppName の値でソート
        Result := CompareText(Left.AppName, Right.AppName);
      end)
    );

  ListView1.Items.Count := FAppIDList.Count;

  ListView1.Items.EndUpdate;
  FormResize(nil);
end;

//=============================================================================
//  ListView1 の OnData イベント (仮想リストビューのイベント)
//  ListView の各 Item の描画が必要となった場合に発生
//  ListView の Item と SubItem に値をセットする
//  このイベントは ListView1.OwnerData := True; でないと機能しない
//=============================================================================
procedure TForm1.ListView1Data(Sender: TObject; Item: TListItem);
var
  LAppId      : string;
  LSHFileInfo : TSHFileInfo;
  LpidlFull   : PItemIDList;
begin
  if Item.Index  > FAppIDList.Count then Exit;

  Item.Caption := FAppIDList[Item.Index].AppName;
  Item.SubItems.Add(FAppIDList[Item.Index].AppID);

  if Item.ImageIndex > 0 then Exit;

  // アイコンの取得と設定
  LAppId := 'shell:AppsFolder' + PathDelim + Item.SubItems[0];
  LpidlFull := ILCreateFromPath(PChar(LAppId));
  SHGetFileInfo(Pointer(LpidlFull),
                        0,
                        LSHFileInfo,
                        SizeOf(LSHFileInfo),
                        SHGFI_PIDL or SHGFI_SYSICONINDEX);
  Item.ImageIndex := LSHFileInfo.iIcon;
  CoTaskMemFree(LpidlFull);
end;

//=============================================================================
//  フォームの幅を変更したらカラムの幅も変更
//=============================================================================
procedure TForm1.FormResize(Sender: TObject);
begin
  ListView1.Columns[1].Width := ListView1.ClientWidth - ListView1.Columns[0].Width;
end;

end.




06_AppUserModelId の値から UWP アプリかを判定


IShellItem を使用したサンプルに UWP アプリであるかの判定機能を追加したサンプルです.UWP アプリの判定用に以下の関数を作成して使用しています.
このコードは,システムにインストールされている UWP アプリのフルパッケージ名を列挙して検索しています.システムにインストールされている UWP アプリの全てが AppUserModelId として登録されているわけではありません.


//-----------------------------------------------------------------------------
//  引数の AppUserModelId の値から UWP アプリかを判定する関数
//
//  以下の値を比較して判定
//  (1) HKEY_CURRENT_USER\Software\Classes\ActivatableClasses\Package
//      階層下のキー名からバージョン関係文字列を除いた値
//  (2) 引数の AppUserModelId の値
//
//  完全には判定できない.UWP アプリによっては誤認識の可能性あり
//-----------------------------------------------------------------------------
function IsUWPApp(AppID: string): Boolean;
var
  LRegistry    : TRegistry;
  LPkgNameList : TStringList;
  LKeyPkgRoot  : string;
  LPkgName     : string;
  LIndex       : Integer;
begin
  Result := False;

  LPkgNameList := TStringList.Create;
  LRegistry := TRegistry.Create;
  try
    LRegistry.Access  := KEY_READ;
    LRegistry.RootKey := HKEY_CURRENT_USER;
    LKeyPkgRoot      := 'Software\Classes\ActivatableClasses\Package';
    if not LRegistry.OpenKey(LKeyPkgRoot, False) then Exit;

    LRegistry.GetKeyNames(LPkgNameList);
    LRegistry.CloseKey;

    // バージョン関係文字列を削除して検索
    for LIndex := 0 to LPkgNameList.Count - 1 do begin
      LPkgName := TRegEx.Replace(LPkgNameList[LIndex], '_(.+)_', '_');
      if SameText(LPkgName, LeftStr(AppID, Length(LPkgName)))
          or (Pos('!', AppID) > 0) then begin
        Result := True;
        Break;
      end;
    end;
  finally
    FreeAndNil(LRegistry);
    FreeAndNil(LPkgNameList);
  end;
end;


下図は,筆者の環境における実行結果です.フォームに配置した TLabel に UWP アプリであるかを表示します.UWP アプリでない場合は not UWP と表示します.


図9
UWP アプリかを判定

起動中のアプリの場合,UWP アプリであるかは以下のどちらかで判定できます.

  • ウィンドウのクラス名が ApplicationFrameWindow である
  • プロセスのハンドルから取得した EXE 名が ApplicationFrameHost.exe である





07_AppUserModelId のコンテキストメニュー


前項のサンプルにコンテキストメニューを表示する機能を追加したサンプルです.拙作の TplContextMenu コンポーネントを使用しています.
表示するメニューの項目は,[スタート] メニューの項目におけるコンテキストメニューとは少し違います.本サンプルで表示するコンテキストメニューの内容は,以下のコマンドで AppUserModelId のリストを,エクスプローラで表示した時のコンテキストメニューとほぼ同じです.


shell:AppsFolder


複数のバージョンの Delphi をインストールしている場合,[サンプル] の項目が複数ありますが,AppUserModelId の末尾の方の値で判定できます.[ファイルの場所を開く] でサンプルのフォルダ内を表示できます.フォルダなので [開く] でも動作は同じです.ショートカットは強制的にデスクトップに作成となります.



図10
デスクトップアプリの例
図11
UWP アプリかの例
  • UWP アプリには [ファイルの場所を開く] がない

本サンプルのコードです.このサンプルは,仮想リストビューを使用して体感的な表示速度を高速にしています.



リスト8
AppUserModelId の一覧.UWP アプリかの判定とコンテキストメニュー表示機能付き
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics,Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls,
   Winapi.ShellAPI,  Winapi.ActiveX, Winapi.ShlObj, Winapi.CommCtrl, Vcl.ComCtrls,
   Vcl.Menus, plContextMenu;

type
  TForm1 = class(TForm)
    ListView1: TListView;
    Button1: TButton;
    Button2: TButton;
    Edit1: TEdit;
    Label1: TLabel;
    plContextMenu1: TplContextMenu;
    procedure FormCreate(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure ListView1DblClick(Sender: TObject);
    procedure ListView1SelectItem(Sender: TObject; Item: TListItem;
      Selected: Boolean);
    procedure plContextMenu1Popup(Sender: TObject);
    procedure ListView1Data(Sender: TObject; Item: TListItem);
    procedure FormDestroy(Sender: TObject);
    procedure plContextMenu1BeforeContextShowMenu(Sender: TObject;
      MenuHandle: HWND);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

  function IsUWPApp(AppID: string): Boolean;

var
  Form1: TForm1;

implementation

uses
  Vcl.Clipbrd,
  System.Win.Registry,
  System.StrUtils,
  Generics.Collections,
  Generics.Defaults,
  System.RegularExpressions;

{$R *.DFM}

type
  PAppIdItem = ^TAppIdItem;
  TAppIdItem = Record
    AppName : string;
    AppID   : string;
    IconIdx : Integer;
  end;

var
  FAppIDList : TList<TAppIdItem>;

//=============================================================================
//  フォーム生成時の処理
//  一覧表示用の TListView の設定
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
var
  LSHFileInfo : TSHFileInfo ;
  LhImgList   : THandle;
begin
  Self.Caption    := 'AppUserModelId 一覧';
  Label1.Caption  := '';
  Button2.Enabled := False;

  ListView1.ViewStyle   := vsReport;
  ListView1.Columns.Add;
  ListView1.Columns[0].Caption := ' 名前';
  ListView1.Columns[0].Width   := 230;
  ListView1.Columns.Add;
  ListView1.Columns[1].Caption := 'AppUserModelId';

  FAppIDList := TList<TAppIdItem>.Create;

  // システムのイメージリストのハンドルを取得
  LhImgList := SHGetFileInfo('',
                             0,
                             LSHFileInfo,
                             SizeOf(LSHFileInfo),
                             SHGFI_ICON or SHGFI_SMALLICON or SHGFI_SYSICONINDEX);
  ListView_SetImageList(ListView1.Handle, LhImgList, LVSIL_SMALL);
end;

//=============================================================================
//  フォーム破棄時の処理
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  FreeAndNil(FAppIDList);
end;

//=============================================================================
//  インストールされているアプリをリスト - Windows 10
//  仮想フォルダ AppsFolder 内のリストを取得 
//  ( アプリ名と AppUserModelId の値を列挙 )
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LPath        : WideString;
  LpidlRoot    : PItemIDList;
  LShItemRoot  : IShellItem;
  LShItemChild : IShellItem;
  LEnumItems   : IEnumShellItems;
  LFetched     : LongInt;
  LAppName     : PWideChar;
  LAppIDPath   : PWideChar;
  LAppItem     : TAppIdItem;
begin
  ListView1.Items.BeginUpdate;
  ListView1.Items.Clear;
  ListView1.RowSelect := True;

  FAppIDList.Clear;

  LpidlRoot    := nil;
  LShItemRoot  := nil;
  LShItemChild := nil;

  LPath := 'shell:AppsFolder';
  try
    // ファイルを項目識別子に変換
    LpidlRoot := ILCreateFromPath(PChar(LPath));
    if ILIsEmpty(LpidlRoot) then Exit;

    // フォルダの IShellItem オブジェクトを取得
    SHCreateItemFromIDList(LpidlRoot, IShellItem, LShItemRoot);

    // ファイル (IShellItem のリストを LEnumItems として取得
   LShItemRoot.BindToHandler(nil, BHID_EnumItems, IEnumShellItems, LEnumItems);

    // リストの各項目の表示名を調べる
    while (LEnumItems.Next(1, LShItemChild, @LFetched) = S_OK) do begin
      LShItemChild.GetDisplayName(SIGDN_NORMALDISPLAY, LAppName);
      if StrLen(LAppName) = 0 then Continue;
      LShItemChild.GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, LAppIDPath);
      if StrLen(LAppIDPath) = 0 then Continue;

      LAppItem.AppName := LAppName;
      LAppItem.AppID   := LAppIDPath;
      LAppItem.IconIdx := -1;
      FAppIDList.Add(LAppItem);
    end;

    //----------------------------------------------------
    //  リストのソート
    //  TComparer関係の使用には Generics.Defaults が必要
    //----------------------------------------------------
    FAppIDList.Sort(TComparer<TAppIdItem>.Construct(
      function(const Left, Right: TAppIdItem): Integer
      begin
        // AppName の値でソート
        Result := CompareText(Left.AppName, Right.AppName);
      end)
    );

    ListView1.Items.Count := FAppIDList.Count;
  finally
    LEnumItems := nil;
    LShItemRoot := nil;
    CoTaskMemFree(LpidlRoot);
  end;

  ListView1.Items.EndUpdate;
  Button2.Enabled := FAppIDList.Count > 0;
  FormResize(nil);
end;

//=============================================================================
//  表示しているアプリ名と AppUserModelId の値をクリップボードにコピー
//  TAB 区切りテキスト.エクセルに貼り付け可能
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LStrList : TStringList;
  LText    : string;
  Lindex   : Integer;
begin
  LStrList := TStringList.Create;
  try
    for Lindex := 0 to FAppIDList.Count -1 do begin
      LText := FAppIDList[Lindex].AppName + Char(VK_TAB)
             + FAppIDList[Lindex].AppID;
      LStrList.Add(LText);
    end;
    Clipboard.AsText := LStrList.Text;
  finally
    FreeAndNil(LStrList);
  end;
end;

//=============================================================================
//  TListView の OnDblClick イベント
//  ダブルクリックしたら選択しているアプリを起動
//  本 EXE が 32 ビットの場合,起動しない 64 ビットのアプリがある
//=============================================================================
procedure TForm1.ListView1DblClick(Sender: TObject);
var
  LItem  : TListItem;
  LAppId : string;
begin
  LItem := ListView1.Selected;
  if LItem = nil then Exit;

  LAppId := 'shell:AppsFolder' + PathDelim + Litem.SubItems[0];
  ShellExecute(0, '', PChar(LAppId), nil, nil, SW_SHOW);
end;

//=============================================================================
//  TListView の OnSelectItem イベント
//  TListView の Item を選択したら AppUserModelId の値を TEdit に表示
//  UWP アプリか非 UWP アプリかの判定結果も表示
//=============================================================================
procedure TForm1.ListView1SelectItem(Sender: TObject; Item: TListItem;
  Selected: Boolean);
var
  LAppId : string;
begin
  if Item = nil then Exit;

  LAppId := Item.SubItems[0];
  Edit1.Text := LAppId;

  if IsUWPApp(LAppId) then begin
    Label1.Caption := 'UWP';
  end else begin
    Label1.Caption := 'not UWP';
  end;
end;

//=============================================================================
//  ListView1 の OnData イベント (仮想リストビューのイベント)
//  ListView の各 Item の描画が必要となった場合に発生
//  ListView の Item と SubItem に値をセットする
//  このイベントは ListView1.OwnerData := True; でないと機能しない
//=============================================================================
procedure TForm1.ListView1Data(Sender: TObject; Item: TListItem);
var
  LAppId      : string;
  LSHFileInfo : TSHFileInfo;
  LpidlFull   : PItemIDList;
begin
  if Item.Index  > FAppIDList.Count then Exit;

  Item.Caption := FAppIDList[Item.Index].AppName;
  Item.SubItems.Add(FAppIDList[Item.Index].AppID);

  if Item.ImageIndex > 0 then Exit;

  // アイコンの取得と設定
  LAppId := 'shell:AppsFolder' + PathDelim + Item.SubItems[0];
  LpidlFull := ILCreateFromPath(PChar(LAppId));
  SHGetFileInfo(Pointer(LpidlFull),
                        0,
                        LSHFileInfo,
                        SizeOf(LSHFileInfo),
                        SHGFI_PIDL or SHGFI_SYSICONINDEX);
  Item.ImageIndex := LSHFileInfo.iIcon;
  CoTaskMemFree(LpidlFull);
end;

//=============================================================================
//  フォームの幅を変更したらカラムの幅も変更
//=============================================================================
procedure TForm1.FormResize(Sender: TObject);
begin
  ListView1.Columns[1].Width := ListView1.ClientWidth - ListView1.Columns[0].Width;
end;

//=============================================================================
//  plContextMenu1 の OnPopup イベント
//  コンテキストメニューの表示
//
//  拙作の TplContextMenu コンポーネントをフォームに配置
//  フォームの PopupMenu プロパティを plContextMenu1 にしておく
//
//  表示項目は UWP アプリと非 UWP とでは異なる
//  [スタート] メニューの項目におけるコンテキストのメニューの項目とは異なる
//=============================================================================
procedure TForm1.plContextMenu1Popup(Sender: TObject);
var
  LItem  : TListItem;
  LAppId : string;
begin
  LItem := ListView1.Selected;
  if LItem = nil then Exit;

  LAppId :=  Litem.SubItems[0];

  plContextMenu1.Directory := 'shell:AppsFolder';
  plContextMenu1.Files.Clear;
  plContextMenu1.Files.Add(LAppId);
end;

//=============================================================================
//  plContextMenu1 の OnBeforeContextShowMenu イベント
//  [ピン留めする] 項目を削除
//=============================================================================
procedure TForm1.plContextMenu1BeforeContextShowMenu(Sender: TObject;
  MenuHandle: HWND);
var
  LCount   : Integer;
  LCmdIdx  : Cardinal;
  LBuffer  : array [0..MAX_COLUMN_NAME_LEN - 1] of Char;
  LIndex   : Integer;
begin
  LCount := GetMenuItemCount(MenuHandle);

  for LIndex := LCount - 1 downto 0 do begin
    LCmdIdx := GetMenuItemID(MenuHandle, LIndex);
    FillChar(LBuffer, SizeOf(LBuffer), #0);
    GetMenuString(MenuHandle, LCmdIdx, @LBuffer, Length(LBuffer), MF_BYCOMMAND);
    if ContainsText(LBuffer, 'ピン留めする') then begin
      RemoveMenu(MenuHandle, LCmdIdx, MF_BYCOMMAND);
    end;
  end;
end;

//-----------------------------------------------------------------------------
//  引数の AppUserModelId の値から UWP アプリかを判定する関数
//
//  以下の値を比較して判定
//  (1) HKEY_CURRENT_USER\Software\Classes\ActivatableClasses\Package
//      階層下のキー名からバージョン関係文字列を除いた値
//  (2) 引数の AppUserModelId の値
//
//  完全には判定できない.UWP アプリによっては誤認識の可能性あり
//-----------------------------------------------------------------------------
function IsUWPApp(AppID: string): Boolean;
var
  LRegistry    : TRegistry;
  LPkgNameList : TStringList;
  LKeyPkgRoot  : string;
  LPkgName     : string;
  LIndex       : Integer;
begin
  Result := False;

  LPkgNameList := TStringList.Create;
  LRegistry := TRegistry.Create;
  try
    LRegistry.Access  := KEY_READ;
    LRegistry.RootKey := HKEY_CURRENT_USER;
    LKeyPkgRoot      := 'Software\Classes\ActivatableClasses\Package';
    if not LRegistry.OpenKey(LKeyPkgRoot, False) then Exit;

    LRegistry.GetKeyNames(LPkgNameList);
    LRegistry.CloseKey;

    // バージョン関係文字列を削除して検索
    for LIndex := 0 to LPkgNameList.Count - 1 do begin
      LPkgName := TRegEx.Replace(LPkgNameList[LIndex], '_(.+)_', '_');
      if SameText(LPkgName, LeftStr(AppID, Length(LPkgName)))
          or (Pos('!', AppID) > 0) then begin
        Result := True;
        Break;
      end;
    end;
  finally
    FreeAndNil(LRegistry);
    FreeAndNil(LPkgNameList);
  end;
end;

end.


サンプルで使用しているコンテキストメニューコンポーネントのイベントを利用すると,ショートカットを任意の場所に作成できます.



//-----------------------------------------------------------------------------
//  指定場所にショートカットを作成
//
//  AszFileName : リンク元のファイルのフルパス名
//  ADirectory  : ショートカットを作成する場所(ディレクトリ名)
//  AWorkDie    : 作業ディレクトリ
//  AswFileName : ショートカットに付ける名前
//                空文字の場合はリンク先のファイル名+'へのショートカット.lnk'
//
//  CreateComObject の使用には uses に System.Win.ComObj が必要
//  IShellLink の 使用には uses に Winapi.ShlObj が 必要
//  IPersistFile の使用には uses に Winapi.ActiveX が必要
//-----------------------------------------------------------------------------
function CreateShortCutLink(AszFileName: string; ADirectory: string;
  AWorkDir: string; AswFileName: string): Boolean;
var
  LUnknownObject : IUnknown;
  LIShellLink    : IShellLink;
  LIPersistFile  : IPersistFile;
  LwFileName     : string;
begin
  Result := False;

  // IShellLink,IPersistFileインターフェイスのオブジェクトを生成
  LUnknownObject := CreateComObject(CLSID_SHELLLINK);
  LIShellLink    := LUnknownObject as IShellLink;
  LIPersistFile  := LUnknownObject as IPersistFile;
  if not Assigned(LIShellLink) or not Assigned(LIPersistFile) then exit;

  // リンク元の指定
  LIShellLink.SetPath(PChar(AszFileName));
  LIShellLink.SetWorkingDirectory(PChar(AWorkDir));

  // シュートカットに付ける名前を作成
  if AswFileName <> '' then begin
    // 本関数の引数に名前の指定があればそれを使用
    LwFileName := IncludeTrailingPathDelimiter(ADirectory) +
                  AswFileName + '.lnk';
  end else begin
    // 名前の指定がなければデフォルトで以下にする
    LwFileName := IncludeTrailingPathDelimiter(ADirectory) +
                  ExtractFileName(AszFileName) + 'へのショートカット.lnk';
  end;
  // リンク作成
  // IPersistFile.Save メソッドを実行すると結果が反映される
  if LIPersistFile.Save(PChar(LwFileName), False) = S_OK then begin
    Result := True;
  end;
end;

//-----------------------------------------------------------------------------
//  特殊フォルダのフルパスを CSIDL 値から取得する関数
//-----------------------------------------------------------------------------
function GetDirectoryFromCSIDL(AnFolder: Cardinal): string;
var
  LBuff : array [0..MAX_PATH-1] of Char;
  LiRet : HRESULT;
  LDir  : string;
begin
  Result := '';

  FillChar(LBuff, SizeOf(LBuff), #0);
  LiRet := SHGetFolderPath(0, AnFolder, 0, SHGFP_TYPE_CURRENT, @LBuff);
  if LiRet = S_OK then begin
    Result := LBuff;
  end;
end;

//=============================================================================
//  plContextMenu1 の OnBeforeContextInvoke イベント
//  ショートカットを別の場所に作成
//=============================================================================
procedure TForm1.plContextMenu1BeforeContextInvoke(Sender: TObject;
  CmdIndex: Integer; VerbNameStr: string; var InvokeFlag: Boolean);
var
  LAppIdPath : string;
  LDestDir   : string;
begin
  // 17 はショートカットの作成
  if CmdIndex = 17 then begin
    LAppIdPath := 'shell:AppsFolder\' + ListView1.Selected.SubItems[0];
    LDestDir := ExtractFilePath(Application.ExeName) + '000';
    ForceDirectories(LDestDir);

    CreateShortCutLink(LAppIdPath, LDestDir, '', ListView1.Selected.Caption);
    InvokeFlag := False;
  end;
end;


デスクトップにショートカットを作成する場合でも,上のようにコンポーネントのイベントで設定すれば,ショートカットに任意の名前を付けることができます.そのためにはテスクトップのパス名を取得します.


    LDestDir := GetDirectoryFromCSIDL(CSIDL_DESKTOP);


上の方のサンプルで,リストのソートに TListView.SortType := stText; を使用しています.そのソートの結果と本サンプルのソートの結果が異なっています.以下のように,AnsiCompareText 関数を使用して AnsiString 型の文字列としてソートすれば,上のサンプルと同じ結果になります.
AnsiString 型の文字列は,日本語版の Windows では Shift_JIS の文字コードの文字列を意味します.


    //----------------------------------------------------
    //  リストのソート
    //  TComparer関係の使用には Generics.Defaults が必要
    //  AnsiCompareText を使用すると
    //   TListView.SortType := stText; と同じになる
    //----------------------------------------------------
    FAppIDList.Sort(TComparer<TAppIdItem>.Construct(
      function(const Left, Right: TAppIdItem): Integer
      begin
        // AppName の値でソート
        Result := AnsiCompareText(Left.AppName, Right.AppName);
      end)
    );




08_AppUserModelId の UWP アプリだけを列挙 - Windows 10


仮想フォルダ AppsFolder のリストにある AppUserModelId の内,UWP アプリだけを列挙するサンプルです.これは,システムにインストールされている UWP アプリの列挙ではありません.アプリケーション識別子 AppUserModelId に User という文字列があるように,現在のユーザがスタートメニューに登録している UWP アプリの列挙です.

フォーム生成時に,システムにインストールされている UWP アプリのフルパッケージ名のリストを作成しておきます.そのリストの中から AppUserModelId の値があるかを検索していきます.



図12
UWP アプリだけを列挙

リスト9
AppUserModelId の UWP アプリのみを列挙
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics,Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls,
   Winapi.ShellAPI,  Winapi.ActiveX, Winapi.ShlObj, Winapi.CommCtrl, Vcl.ComCtrls;

type
  TForm1 = class(TForm)
    ListView1: TListView;
    Button1: TButton;
    Button2: TButton;
    Edit1: TEdit;
    procedure FormCreate(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure ListView1DblClick(Sender: TObject);
    procedure ListView1SelectItem(Sender: TObject; Item: TListItem;
      Selected: Boolean);
    procedure ListView1Data(Sender: TObject; Item: TListItem);
    procedure FormDestroy(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses
  Vcl.Clipbrd,
  System.Win.Registry,
  System.StrUtils,
  Generics.Collections,
  Generics.Defaults,
  System.RegularExpressions;

{$R *.DFM}

type
  PAppIdItem = ^TAppIdItem;
  TAppIdItem = Record
    AppName : string;
    AppID   : string;
    IconIdx : Integer;
  end;

var
  FAppIDList   : TList<TAppIdItem>;
  FPkgNameList : TStringList;

//=============================================================================
//  フォーム生成時の処理
//  一覧表示用の TListView の設定
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
var
  LSHFileInfo : TSHFileInfo ;
  LhImgList   : THandle;
  LRegistry   : TRegistry;
  LKeyPkgRoot : string;
  LIndex      : Integer;
begin

  Self.Caption    := 'AppUserModelId 一覧 ( UWP アプリ )';
  Button2.Enabled := False;

  ListView1.ViewStyle   := vsReport;
  ListView1.Columns.Add;
  ListView1.Columns[0].Caption := ' 名前';
  ListView1.Columns[0].Width   := 230;
  ListView1.Columns.Add;
  ListView1.Columns[1].Caption := 'AppUserModelId';

  FAppIDList := TList<TAppIdItem>.Create;

  // システムのイメージリストのハンドルを取得
  LhImgList := SHGetFileInfo('',
                             0,
                             LSHFileInfo,
                             SizeOf(LSHFileInfo),
                             SHGFI_ICON or SHGFI_SMALLICON or SHGFI_SYSICONINDEX);
  ListView_SetImageList(ListView1.Handle, LhImgList, LVSIL_SMALL);


  // レジストリの以下のキー名のリストを作成
  // これが UWP アプリのフルパッケージ名
  //  このパッケージの全てが AppUserModelId のリストにあるわけではない
  // HKEY_CURRENT_USER\Software\Classes\ActivatableClasses\Package
  FPkgNameList := TStringList.Create;
  LRegistry := TRegistry.Create;
  try
    LRegistry.Access  := KEY_READ;
    LRegistry.RootKey := HKEY_CURRENT_USER;
    LKeyPkgRoot      := 'Software\Classes\ActivatableClasses\Package';
    if not LRegistry.OpenKey(LKeyPkgRoot, False) then Exit;

    LRegistry.GetKeyNames(FPkgNameList);
    LRegistry.CloseKey;

    // バージョン関係文字列を削除しておく
    for LIndex := 0 to FPkgNameList.Count - 1 do begin
      FPkgNameList[LIndex] := TRegEx.Replace(FPkgNameList[LIndex], '_(.+)_', '_');
    end;
  finally
    FreeAndNil(LRegistry);
  end;
end;

//=============================================================================
//  フォーム破棄時の処理
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  FreeAndNil(FAppIDList);
  FreeAndNil(FPkgNameList);
end;

//=============================================================================
//  インストールされているアプリをリスト - Windows 10
//  仮想フォルダ AppsFolder 内の UWP アプリだけをリスト
//  UWP アプリの判定は一部誤認識あり
//  ( アプリ名と AppUserModelId の値を列挙 )
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LPath        : WideString;
  LpidlRoot    : PItemIDList;
  LShItemRoot  : IShellItem;
  LShItemChild : IShellItem;
  LEnumItems   : IEnumShellItems;
  LFetched     : LongInt;
  LAppName     : PWideChar;
  LAppIDPath   : PWideChar;
  LAppItem     : TAppIdItem;
  LPkgName     : string;
  LUWPFlag     : Boolean;
  LIndex       : Integer;
begin
  ListView1.Items.BeginUpdate;
  ListView1.Items.Clear;
  ListView1.RowSelect := True;

  FAppIDList.Clear;

  LpidlRoot    := nil;
  LShItemRoot  := nil;
  LShItemChild := nil;

  LPath := 'shell:AppsFolder';
  try
    // ファイルを項目識別子に変換
    LpidlRoot := ILCreateFromPath(PChar(LPath));
    if ILIsEmpty(LpidlRoot) then Exit;

    // フォルダの IShellItem オブジェクトを取得
    SHCreateItemFromIDList(LpidlRoot, IShellItem, LShItemRoot);

    // ファイル (IShellItem のリストを LEnumItems として取得
   LShItemRoot.BindToHandler(nil, BHID_EnumItems, IEnumShellItems, LEnumItems);

    // リストの各項目の表示名を調べる
    while (LEnumItems.Next(1, LShItemChild, @LFetched) = S_OK) do begin
      LShItemChild.GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, LAppIDPath);
      if StrLen(LAppIDPath) = 0 then Continue;
      LShItemChild.GetDisplayName(SIGDN_NORMALDISPLAY, LAppName);
      if StrLen(LAppName) = 0 then Continue;

      // レジストリにパッケージ名が存在するアプリ (UWP アプリ) のみリストに追加
      for LIndex := 0 to FPkgNameList.Count - 1 do begin
        LPkgName := FPkgNameList[LIndex];
        LUWPFlag := (Pos('!', LAppIDPath) > 0)
                 or SameText(LPkgName, LeftStr(LAppIDPath, Length(LPkgName)));

        if LUWPFlag then begin
          LAppItem.AppName := LAppName;
          LAppItem.AppID   := LAppIDPath;
          LAppItem.IconIdx := -1;
          FAppIDList.Add(LAppItem);
          Break;
        end;
      end;
    end;

    //----------------------------------------------------
    //  リストのソート
    //  TComparer関係の使用には Generics.Defaults が必要
    //----------------------------------------------------
    FAppIDList.Sort(TComparer<TAppIdItem>.Construct(
      function(const Left, Right: TAppIdItem): Integer
      begin
        // AppName の値でソート
        Result := CompareText(Left.AppName, Right.AppName);
      end)
    );

    ListView1.Items.Count := FAppIDList.Count;
  finally
    LEnumItems := nil;
    LShItemRoot := nil;
    CoTaskMemFree(LpidlRoot);
  end;

  ListView1.Items.EndUpdate;
  Button2.Enabled := FAppIDList.Count > 0;
  FormResize(nil);
end;

//=============================================================================
//  表示しているアプリ名と AppUserModelId の値をクリップボードにコピー
//  TAB 区切りテキスト.エクセルに貼り付け可能
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LStrList : TStringList;
  LText    : string;
  Lindex   : Integer;
begin
  LStrList := TStringList.Create;
  try
    for Lindex := 0 to FAppIDList.Count -1 do begin
      LText := FAppIDList[Lindex].AppName + Char(VK_TAB)
             + FAppIDList[Lindex].AppID;
      LStrList.Add(LText);
    end;
    Clipboard.AsText := LStrList.Text;
  finally
    FreeAndNil(LStrList);
  end;
end;

//=============================================================================
//  TListView の OnDblClick イベント
//  ダブルクリックしたら選択しているアプリを起動
//  本 EXE が 32 ビットの場合,起動しない 64 ビットのアプリがある
//=============================================================================
procedure TForm1.ListView1DblClick(Sender: TObject);
var
  LItem  : TListItem;
  LAppId : string;
begin
  LItem := ListView1.Selected;
  if LItem = nil then Exit;

  LAppId := 'shell:AppsFolder' + PathDelim + Litem.SubItems[0];
  ShellExecute(0, '', PChar(LAppId), nil, nil, SW_SHOW);
end;

//=============================================================================
//  TListView の OnSelectItem イベント
//  TListView の Item を選択したら AppUserModelId の値を TEdit に表示
//=============================================================================
procedure TForm1.ListView1SelectItem(Sender: TObject; Item: TListItem;
  Selected: Boolean);
var
  LAppId : string;
begin
  if Item = nil then Exit;

  LAppId := Item.SubItems[0];
  Edit1.Text := LAppId;
end;

//=============================================================================
//  ListView1 の OnData イベント (仮想リストビューのイベント)
//  ListView の各 Item の描画が必要となった場合に発生
//  ListView の Item と SubItem に値をセットする
//  このイベントは ListView1.OwnerData := True; でないと機能しない
//=============================================================================
procedure TForm1.ListView1Data(Sender: TObject; Item: TListItem);
var
  LAppId      : string;
  LSHFileInfo : TSHFileInfo;
  LpidlFull   : PItemIDList;
begin
  if Item.Index  > FAppIDList.Count then Exit;

  Item.Caption := FAppIDList[Item.Index].AppName;
  Item.SubItems.Add(FAppIDList[Item.Index].AppID);

  if Item.ImageIndex > 0 then Exit;

  // アイコンの取得と設定
  LAppId := 'shell:AppsFolder' + PathDelim + Item.SubItems[0];
  LpidlFull := ILCreateFromPath(PChar(LAppId));
  SHGetFileInfo(Pointer(LpidlFull),
                        0,
                        LSHFileInfo,
                        SizeOf(LSHFileInfo),
                        SHGFI_PIDL or SHGFI_SYSICONINDEX);
  Item.ImageIndex := LSHFileInfo.iIcon;
  CoTaskMemFree(LpidlFull);
end;

//=============================================================================
//  フォームの幅を変更したらカラムの幅も変更
//=============================================================================
procedure TForm1.FormResize(Sender: TObject);
begin
  ListView1.Columns[1].Width := ListView1.ClientWidth - ListView1.Columns[0].Width;
end;

end.




09_デスクトップ アプリの EXE 等のフルパスを取得


仮想フォルダ AppsFolder のリストにある AppUserModelId の内,デスクトップアプリの実行形式ファイル (EXE ファイル等) のフルパスを取得するサンプルです.
仮想フォルダ AppsFolder をエクスプローラで表示すると,名前と AppUserModelId の値があります.したがって,「shell:AppsFolder\AppUserModelId の値」が各々のアプリのシェル名前空間におけるフルパスということになりますが,ここでは,その AppUserModelId のアプリを起動した時,あるいは [ファイルの場所を開く] で表示するファイルのフルパスを取得します.

デスクトップアプリの AppUserModelId の値に GUID の文字列がありますが,それらがレジストリのキーとして登録されているわけではありません.システムが自動的に処理します.そのため,AppUserModelId の値から EXE 等のフルパスを直接知ることはできません.


// アプリケーション識別子としてのメモ帳のシェル名前空間における絶対パス
// 通常のファイルシステムにおけるパスは存在しない
shell:AppsFolder\{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\notepad.exe


仮想フォルダ AppsFolder のリストにあるアプリは,スタートメニューに登録されています.その内,非 UWP アプリ,つまりテスクトップアプリは以下の場所にショートカットファイルがあります.本サンプルは,それらのショートカットから AppUserModelId と同じ名前のファイルを探して,そのショートカットのリンク先を取得します.取得した値をレコード型の各メンバーにセットし,そのレコード型をリストに追加していきます.
以下のフォルダ内には,UWP アプリのショートカットはありません.


// 現在のユーザ固有
%APPDATA%\Microsoft\Windows\Start Menu\Programs
// ユーザ共通
%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs


以下は全て 64 版の Windows 10 における実行結果です.
選択したアプリの実行ファイルのフルパスを TEdit に表示します.ダブルクリックすると,ShellExecut 関数でその実行ファイルを起動するようになっています.
筆者はフォルトビューアをスタートメニューに追加しています (ピン留めではない).そのフォートビューアの EXE 名を取得したのが下図です.コマンドラインの引数も取得するようにしているので,フォトビューアを起動できます.



図13
フォトビューア
  • 起動するのは 64 ビット版のフォトビューア

64 ビット版の Google Chrome の例です.EXE のフルパスに Program Files (x86) があります.これは 32 ビットの EXE がある場所です.したがって,この Google Chrome は起動しません.本サンプルを 64 ビットの EXE にすれば起動します.
この仕様では寂しいので,2021年10月26日の更新で,ファイルシステムのリダイレクト機能を無効にして,システム関係のファイルの場合はフォルダ名を変更する処理を追加しました.その結果,本サンプルが 32 ビットの EXE でも 64 ビット版の Google Chrome が起動するようになっています.ただし,TEdit には,取得した EXE のフルパスをそのまま表示するようにしています.




図14
Google Chrome
  • 64 ビットの Google Chrome
  • 本サンプルは 32 ビット
  • 起動できない
  • 64 ビットの Windows10 での実行結果
図15
Google Chrome
  • 64 ビットの Google Chrome
  • 本サンプルは 64 ビット
  • 起動可能
  • 64 ビットの Windows10 での実行結果

メモ帳の例です.取得した EXE のフルパスに System32 があります.System32 階層下には 64 ビットのアプリの EXE があります.2021年10月26日の更新でリダイレクトの機能を無効したので,本サンプルが 32 ビットの EXE でも 64 ビットの EXE であっても,64 ビットの版のメモ帳が起動します.32 ビットのアプリの EXE は SysWOW64 フォルダ階層下にあります.
ファイルシステムのリダイレクトを無効ににしない場合は,本サンプルが 32 ビットの EXE の時は 32 ビット版メモ帳が,本サンプルが 64 ビットの EXE の時は 64 ビット版のメモ帳が起動します.


C:\windows\system32\notepad.exe   // 64 ビット版のメモ帳
C:\windows\sysWOW64\notepad.exe   // 32 ビット版のメモ帳


図16
メモ帳

下図は筆者の Windows 10 のスタートメニューの一部です.[設定] が 2 つあります.左の [設定] アイコンは UWP アプリの [設定] です.右の [設定] は従来のコントロールパネルが起動します.コントロールパネルは,確かに各種の設定用のダイアログですが,AppUerModelId の [設定] は UWP アプリです.この状態だと AppUserModelId のコントロールパネルが検出できません.そこで本サンプルでは,その対策用の処理をしています.
コントロールパネルと設定アプリは,以下のコマンドでも起動できます.


control                   // コントロールパネル
shell:ControlPanelFolder  // コントロールパネル
ms-settings:              // 設定


図17
2 つの [設定] とコントロールパネル
  • 左の [設定] アイコンは UWP アプリ
  • 右は従来の [コントロールパネル]

[ファイル名を指定して実行] の実行可能なファイルは取得できません.
[ファイル名を指定して実行] のダイアログは以下のコマンドで起動できます.このコマンドは,shell32.dll 内の 61 番目の関数を実行します.この 61 番目の関数が [ファイル名を指定して実行] のダイアログを表示する関数です.何番目かを示す値を序数と言います. [ファイル名を指定して実行] というプログラムのファイルは存在しません.
フォトビューアも rundll32.exe を使用して起動しますが,フォトビューアには具体的なファイルがあります.


rundll32.exe shell32.dll,#61


[ファイル名を指定して実行] のダイアログを ShellExecute 関数で起動する場合,以下のようなコードでも起動できます.


//=============================================================================
//  [ファイル名を指定して実行] のダイアログを ShellExecute 関数で起動
//  ダイアログの表示位置は画面の左下ではない
//  uses に Winapi.ShellAPI が必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LCmd : string;
  LArg : string;
begin
  LCmd := 'rundll32.exe';
  LArg := 'shell32.dll,#61';
  ShellExecute(0, '', PChar(LCmd), PChar(LArg), nil, SW_SHOW);
end;

//=============================================================================
//  [ファイル名を指定して実行] のダイアログを ShellExecute 関数で起動
//  Shell コマンドを使用する例
//  ダイアログの表示位置は画面の左下
//  uses に Winapi.ShellAPI が必要
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LCmd : string;
begin
  LCmd := 'shell:::{2559a1f3-21d7-11d4-bdaf-00c04f60b9f0}';
  ShellExecute(0, '', PChar(LCmd), nil, nil, SW_SHOW);
end;



リスト10
AppUserModelId の非 UWP アプリの EXE のフルパスを取得.スタートメニューのリストから検索
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics,Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls,
   Winapi.ShellAPI,  Winapi.ActiveX, Winapi.ShlObj, Winapi.CommCtrl, Vcl.ComCtrls,
   System.StrUtils, System.IOUtils, System.Types, System.Win.ComObj,
   Generics.Collections, Generics.Defaults;

type
  TForm1 = class(TForm)
    ListView1: TListView;
    Button1: TButton;
    Edit1: TEdit;
    procedure FormCreate(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure ListView1DblClick(Sender: TObject);
    procedure ListView1SelectItem(Sender: TObject; Item: TListItem;
      Selected: Boolean);
    procedure ListView1Data(Sender: TObject; Item: TListItem);
    procedure FormDestroy(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

  PStartMenuItem = ^TStartMenuItem;
  TStartMenuItem = Record
    Name     : string;
    FullPath : string;
  end;

  function Wow64DisableWow64FsRedirection(var flgRedirec: LongBool): LongBool; stdcall;
    external 'Kernel32.dll' Name 'Wow64DisableWow64FsRedirection';

  function GetStartMenuShortCutList(AShortCutList: TList<TStartMenuItem>): Integer;
  function GetLinkPathName(ALinkFilePath: string; var ArgStr: string;
    var WorkDir: string): string;
  function ExpandEnvPath(AEnvPath: string): string;

var
  Form1: TForm1;

implementation

uses
  System.Win.Registry,
  System.RegularExpressions;

{$R *.DFM}

type
  PAppIdItem = ^TAppIdItem;
  TAppIdItem = Record
    AppName  : string;
    AppID    : string;
    LinkPath : string;
    ExePath  : string;
    ArgStr   : string;
    WorkDir  : string;
    IconIdx  : Integer;
  end;

var
  FAppIDList : TList<TAppIdItem>;

//=============================================================================
//  フォーム生成時の処理
//  一覧表示用の TListView の設定等
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
var
  LSHFileInfo : TSHFileInfo ;
  LhImgList   : THandle;
  LDirect     : LongBool;
begin
  // ファイルシステムのリダイレクト機能を無効化
  Wow64DisableWow64FsRedirection(LDirect);

  Self.Caption    := 'AppUserModelId 一覧 ( デスクトップアプリの EXE )';

  ListView1.ViewStyle   := vsReport;
  ListView1.Columns.Add;
  ListView1.Columns[0].Caption := ' 名前';
  ListView1.Columns[0].Width   := 230;
  ListView1.Columns.Add;
  ListView1.Columns[1].Caption := 'AppUserModelId';

  FAppIDList := TList<TAppIdItem>.Create;

  // システムのイメージリストのハンドルを取得
  LhImgList := SHGetFileInfo('',
                             0,
                             LSHFileInfo,
                             SizeOf(LSHFileInfo),
                             SHGFI_ICON or SHGFI_SMALLICON or SHGFI_SYSICONINDEX);
  ListView_SetImageList(ListView1.Handle, LhImgList, LVSIL_SMALL);
end;

//=============================================================================
//  フォーム破棄時の処理
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  FreeAndNil(FAppIDList);
end;

//=============================================================================
//  インストールされているアプリをリスト - Windows 10
//  仮想フォルダ AppsFolder 内のリスト作成
//  アプリ名と AppUserModelId の値を列挙
//  スタートメニューのリストからアプリのリンク先も取得
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LShortCutList : TList<TStartMenuItem>;
  LPath         : WideString;
  LpidlRoot     : PItemIDList;
  LShItemRoot   : IShellItem;
  LShItemChild  : IShellItem;
  LEnumItems    : IEnumShellItems;
  LFetched      : LongInt;
  LAppName      : PWideChar;
  LAppIDPath    : PWideChar;
  LAppItem      : TAppIdItem;
  LShortName    : string;
  LExePath      : string;
  LArgStr       : string;
  LWorkDir      : string;
  LIndex        : Integer;
  LListIndex    : Integer;
begin
  ListView1.Items.BeginUpdate;
  ListView1.Items.Clear;
  ListView1.RowSelect := True;

  // スタートメニューのショートカットファイルのリストを作成
  LShortCutList := TList<TStartMenuItem>.Create;
  GetStartMenuShortCutList(LShortCutList);

  FAppIDList.Clear;

  LpidlRoot    := nil;
  LShItemRoot  := nil;
  LShItemChild := nil;

  LPath := 'shell:AppsFolder';
  try
    // ファイルを項目識別子に変換
    LpidlRoot := ILCreateFromPath(PChar(LPath));
    if ILIsEmpty(LpidlRoot) then Exit;

    // フォルダの IShellItem オブジェクトを取得
    SHCreateItemFromIDList(LpidlRoot, IShellItem, LShItemRoot);

    // ファイル (IShellItem のリストを LEnumItems として取得
    LShItemRoot.BindToHandler(nil, BHID_EnumItems, IEnumShellItems, LEnumItems);

    // リストの各項目の表示名を調べる
    while (LEnumItems.Next(1, LShItemChild, @LFetched) = S_OK) do begin
      LShItemChild.GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, LAppIDPath);
      if StrLen(LAppIDPath) = 0 then Continue;
      LShItemChild.GetDisplayName(SIGDN_NORMALDISPLAY, LAppName);
      if StrLen(LAppName) = 0 then Continue;

      // レコード型の初期化と AppUserModelId とその表示名の代入
      LAppItem := Default(TAppIdItem);
      LAppItem.AppName := LAppName;
      LAppItem.AppID   := LAppIDPath;
      LAppItem.IconIdx := -1;

      // ショートカットのリストから AppUserModelId と同じ名前を検出
      // 重複名を検出しないように,検出したショートカットの項目を削除
      for LIndex := LShortCutList.Count - 1 downto 0 do begin
        LListIndex := LShortCutList.Count - LIndex - 1;

        LShortName := LShortCutList[LListIndex].Name;

        // 筆者の環境ではショートカットの「設定」がコントロールパネルになっている
        // 「設定」は本来 UWP のアプリ名
        // その対策用の処理コード
        if (LShortName = 'コントロール パネル') or (LShortName = '設定') then begin
          LExePath := GetLinkPathName(LShortCutList[LListIndex].FullPath,
                                      LArgStr,
                                      LWorkDir);
          if LExePath = '' then Continue;
          LShortName := 'コントロール パネル';
        end;

        if SameText(LAppName, LShortName) then begin

          LAppItem.LinkPath := LShortCutList[LListIndex].FullPath;
          LExePath := GetLinkPathName(LAppItem.LinkPath, LArgStr, LWorkDir);
          LAppItem.ExePath := LExePath;
          LAppItem.ArgStr  := LArgStr;
          LAppItem.WorkDir := LWorkDir;

          LShortCutList.Delete(LListIndex);
          Break;
        end;
      end;
      FAppIDList.Add(LAppItem);
    end;

    //----------------------------------------------------
    //  リストのソート
    //  TComparer関係の使用には Generics.Defaults が必要
    //----------------------------------------------------
    FAppIDList.Sort(TComparer<TAppIdItem>.Construct(
      function(const Left, Right: TAppIdItem): Integer
      begin
        // AppName の値でソート
        Result := CompareText(Left.AppName, Right.AppName);
      end)
    );

    ListView1.Items.Count := FAppIDList.Count;
  finally
    LEnumItems := nil;
    LShItemRoot := nil;
    CoTaskMemFree(LpidlRoot);
    FreeAndNil(LShortCutList);
  end;

  ListView1.Items.EndUpdate;
  FormResize(nil);
end;

//=============================================================================
//  TListView の OnDblClick イベント
//  ダブルクリックしたら選択しているアプリを起動
//  TEdit に表示している EXE または実行可能ファイルを実行
//
//  筆者の 64 ビット版の Windows 10 では
//  Delhi 6 / 7 起動時に rtl60.bpl 等の読み込みエラーが発生
//  IDE からではなく EXE から起動すればエラーは発生しない
//  ショートカットを実行する場合も現象は同じ
//=============================================================================
procedure TForm1.ListView1DblClick(Sender: TObject);
var
  LItem    : TListItem;
  LExePath : string;
  LArgStr  : string;
  LRes     : HRESULT;
  LText    : string;
begin
  LItem := ListView1.Selected;
  if LItem = nil then Exit;

  LExePath := ExpandEnvPath(FAppIDList[LItem.Index].ExePath);
  LArgStr  := ExpandEnvPath(FAppIDList[LItem.Index].ArgStr);

  if LExePath = '' then begin
    LText := '実行可能なファイルがありません。';
    MessageBox(Handle, PChar(LText), '情報', MB_ICONINFORMATION);
  end else begin
    // システム関係フォルダ内のファイルが存在しない場合の処理
    // フォルダ名を決め打ちにしているので将来の保証なし
    if not FileExists(LExePath) then begin
      if SizeOf(THandle) = 4 then begin
        LExePath := ReplaceText(LExePath, 'Program Files (x86)', 'Program Files');
        LExePath := ReplaceText(LExePath, 'SysWOW64', 'System32');
      end else begin
        LExePath := ReplaceText(LExePath, 'Program Files', 'Program Files (x86)');
        LExePath := ReplaceText(LExePath, 'System32', 'SysWOW64');
      end;
    end;

    LRes := ShellExecute(0, '', PChar(LExePath), PChar(LArgStr), nil, SW_SHOW);
    if LRes <= 32 then begin
      LText := SysErrorMessage(GetLastError);
      MessageBox(Handle, PChar(LText), '情報', MB_ICONWARNING);
    end;
  end;
end;

//=============================================================================
//  TListView の OnSelectItem イベント
//  TListView の Item を選択したら EXE のフルパスを取得して TEdit に表示
//  UWP アプリは空白とする
//=============================================================================
procedure TForm1.ListView1SelectItem(Sender: TObject; Item: TListItem;
  Selected: Boolean);
var
  LExePath : string;
  LArgStr  : string;
begin
  if Item = nil then Exit;

  LExePath := FAppIDList[Item.Index].ExePath;
  LArgStr  := FAppIDList[Item.Index].ArgStr;
  Edit1.Text := LExePath;
  if LArgStr <> '' then Edit1.Text := LExePath + ' ' + LArgStr;
end;

//=============================================================================
//  ListView1 の OnData イベント (仮想リストビューのイベント)
//  ListView の各 Item の描画が必要となった場合に発生
//  ListView の Item と SubItem に値をセットする
//  このイベントは ListView1.OwnerData := True; でないと機能しない
//=============================================================================
procedure TForm1.ListView1Data(Sender: TObject; Item: TListItem);
var
  LAppId      : string;
  LSHFileInfo : TSHFileInfo;
  LpidlFull   : PItemIDList;
begin
  if Item.Index  > FAppIDList.Count then Exit;

  Item.Caption := FAppIDList[Item.Index].AppName;
  Item.SubItems.Add(FAppIDList[Item.Index].AppID);

  if Item.ImageIndex > 0 then Exit;

  // アイコンの取得と設定
  LAppId := 'shell:AppsFolder' + PathDelim + Item.SubItems[0];
  LpidlFull := ILCreateFromPath(PChar(LAppId));
  SHGetFileInfo(Pointer(LpidlFull),
                        0,
                        LSHFileInfo,
                        SizeOf(LSHFileInfo),
                        SHGFI_PIDL or SHGFI_SYSICONINDEX);
  Item.ImageIndex := LSHFileInfo.iIcon;
  CoTaskMemFree(LpidlFull);
end;

//=============================================================================
//  フォームの幅を変更したらカラムの幅も変更
//=============================================================================
procedure TForm1.FormResize(Sender: TObject);
begin
  ListView1.Columns[1].Width := ListView1.ClientWidth - ListView1.Columns[0].Width;
end;

//-----------------------------------------------------------------------------
//  スタートメニューのショートカットファイルのリストを作成
//  TStartMenuItem はショートカットの名前とパス名格納用のレコード型
//
//  このリストには UWP アプリはない
//-----------------------------------------------------------------------------
function GetStartMenuShortCutList(AShortCutList: TList<TStartMenuItem>): Integer;
const
  LINK_SUB_DIR = 'Microsoft\Windows\Start Menu\Programs';
var
  LRootDir   : string;
  LListItem  : TStartMenuItem;
  LOption    : TSearchOption;
  LFilesList : TStringDynArray;
  LpidlFull  : PItemIDList;
  LShelItem  : IShellItem;
  LFileName  : PWideChar;
  LDirIndex  : Integer;
  LIndex     : Integer;
begin
  AShortCutList.Clear;

  for LDirIndex := 0 to 1 do begin
    if LDirIndex = 0 then begin
      // 現在のユーザ固有
      LRootDir := GetEnvironmentVariable('APPDATA') + PathDelim + LINK_SUB_DIR;
    end else begin
      // ユーザ共通
      LRootDir := GetEnvironmentVariable('PROGRAMDATA') + PathDelim + LINK_SUB_DIR;
  end;

    LOption := TSearchOption.soAllDirectories;
    LFilesList := TDirectory.GetFiles(LRootDir, '*.lnk', LOption);

    // ショートカットのエクスプローラにおける表示名を取得
    // 例 : notepad.lnk --> メモ帳
    for LIndex := 0 to Length(LFilesList) - 1 do begin
      LpidlFull := ILCreateFromPath(PWideChar(LFilesList[LIndex]));
      SHCreateItemFromIDList(LpidlFull, IShellItem, LShelItem);
      LShelItem.GetDisplayName(SIGDN_NORMALDISPLAY, LFileName);

      CoTaskMemFree(LpidlFull);

      // レコード型のメンバに代入して,そのレコードをリストに追加
      LListItem.Name     := LFileName;
      LListItem.FullPath := LFilesList[LIndex];
      AShortCutList.Add(LListItem);
    end;
  end;

 //----------------------------------------------------
  //  リストのソート
  //  TComparer関係の使用には Generics.Defaults が必要
  //----------------------------------------------------
  AShortCutList.Sort(TComparer<TStartMenuItem>.Construct(
    function(const Left, Right: TStartMenuItem): Integer
    begin
      // Name の値でソート
      Result := CompareText(Left.Name, Right.Name);
    end)
  );

  Result := AShortCutList.Count;
end;

//-----------------------------------------------------------------------------
//  ショートカット (拡張子.lnk) のリンク先のフルパスとコマンドライン引数を取得
//  引数の ArgStr はコマンドラインの引数
//  引数の WorkDir は作業ディレクトリ
//
//  64 版の Windows の場合,32 ビットの EXE と 64 ビットの EXE から取得した場合,
//  以下の値が含まれる時の結果に違い (リダイレクト) があることに注意
//  %ProgramFiles% (Program Files と Program Files (x86)
//  System32 と SysWOW64
//-----------------------------------------------------------------------------
function GetLinkPathName(ALinkFilePath: string; var ArgStr: string;
  var WorkDir: string): string;
var
  LIShellLink   : IShellLink;
  LIPersistFile : IPersistFile;
  LFileData     : TWin32FindData;
  LFilePath     : array [0..MAX_PATH - 1] of Char;
  LArgPath      : array [0..MAX_PATH - 1] of Char;
  LWorkDir      : array [0..MAX_PATH - 1] of Char;
begin
  Result  := '';
  ArgStr  := '';
  WorkDir := '';

  // IShellLink, IPersistFile インターフェイスのオブジェクトを生成
  LIShellLink   := CreateComObject(CLSID_ShellLink) as IShellLink;
  LIPersistFile := LIShellLink as IPersistFile;

  if Succeeded(LIPersistFile.Load(PChar(ALinkFilePath), STGM_READ)) then begin

    // ショートカットファイルのリンク先を取得
    FillChar(LFilePath, SizeOf(LFilePath), #0);
    LIShellLink.GetPath(@LFilePath, MAX_PATH, LFileData, SLGP_UNCPRIORITY);

    // コマンドラインの引数の値を取得
    FillChar(LArgPath, SizeOf(LArgPath), #0);
    LIShellLink.GetArguments(@LArgPath, MAX_PATH);

    // 作業ディレクトリの値を取得
    FillChar(LWorkDir, SizeOf(LWorkDir), #0);
    LIShellLink.GetWorkingDirectory(@LWorkDir, MAX_PATH);

    // 関数の戻り値にセット
    ArgStr  := LArgPath;
    WorkDir := LWorkDir;
    Result := LFilePath;
  end;
end;

//-----------------------------------------------------------------------------
//  環境変数を含むパス名を実パスに変換する関数
//-----------------------------------------------------------------------------
function ExpandEnvPath(AEnvPath: string): string;
var
  LBuff : array [0..MAX_PATH - 1] of Char;
  LLen  : Integer;
begin
  FillChar(LBuff, SizeOf(LBuff), #0);
  LLen := Length(LBuff);
  ExpandEnvironmentStrings(PChar(AEnvPath), @LBuff, LLen);
  Result := LBuff;
end;

end.


上のコードでは項目識別子を使用して表示名を取得しています.実ディレクトリであるスタートメニューフォルダ内のリンクファイルの表示名は,SHGetFileInfo 関数で取得できます.



図18
SHGetFileInfo 関数で表示名を取得
  • 「表示名」は,エクスプローラの「名前」欄のテキストのこと

//=============================================================================
//  ファイルの表示名を取得
//  SHGetFileInfo 関数 (Winapi.ShellAPI) を使用
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LLinkPath : string;
  LFileInfo : TSHFileInfo;
  LDispName : string;
begin
  LLinkPath := 'C:\ProgramData\Microsoft\Windows\Start Menu'
             + '\Programs\Accessories\Notepad.lnk';

  //表示名の取得
  SHGetFileInfo(PChar(LLinkPath),
                0,
                LFileInfo,
                SizeOf(LFileInfo),
                SHGFI_DISPLAYNAME);
  LDispName := LFileInfo.szDisplayName;
  MessageBox(Handle, Pchar(LDispName), '結果', MB_ICONINFORMATION);
end;


デスクトップアプリは,EXE 名で起動しても AppUserMOdelId の値で起動しても起動するのは同じ EXE です.デスクトップアプリを ShellExecuteEx 関数で起動すると,プロセス ID の値が取得できます.プロセス ID の値がプロセスのハンドルが取得できます.プロセスのハンドルがら,起動した EXE のフルパスが取得できます.



[備考 1]
ファイルシステムのリダイレクトが問題となるのは 64 ビット版の Windows です.リダイレクトによる現象を回避するには,64 ビット版の Windows では 64 ビットの EXE を作成して,リダイレクトの機能を無効にします.

[備考 2]
ZIP 書庫には,特定の AppUserModelId の値から,デスクトップアプリの EXE のフルパスを取得するサンプルを同梱しています.




10_UWP アプリの EXE のフルパスを取得


UWP アプリの EXE のフルパスを取得するサンプルです.UWP アプリ関係ファイルはインストール先のパッケージ (フォルダ) 内にあります.以下のリンクの記事のサンプルは,パッケージ内のアイコン画像の場所を調べる例です.本サンプルは,アイコン画像がある場所ではなく,EXE のパス名を取得するサンプルとなっています.



手順は上のリンクの記事のサンプルと同じです.
電卓の AppUserModelId の値とパッケージ名は以下のようになっています.赤の下線部分を削除すると両者は同じ値になります.本サンプルはこのことを利用して AppUserModelId の値からパッケージ名を検索します.したがって,この規則に従っていない UWP アプリは検出できません.パッケージ名の下線部分はバージョン関係の値です.アプリのバージョンが変わると,この部分も変わる可能性があります.


図19
AppUserModelId の値とパッケージ名

下図が実行結果です.TEdit に別の AppUserModelId の値を入力して実行すれば,その UWP アプリのパッケージ名と EXE 名が取得できます.取得した EXE を起動しようとしても起動できません.UWP アプリは一部のアプリを除いて EXE からの起動はできません.

デスクトップアプリを識別するのは EXE 名です.プログラムからデスクトップアプリを起動するには EXE のパス名を使用します.UWP アプリをプログラムから起動する場合,一般的にはアプリケーション識別子である AppUserModelId を使用しますが,電卓の場合,以下のコマンドでも起動できます.


calc
calculator:
ms-calculator:


図20
UWP アプリの EXE 名
図21
EXE は起動できない

リスト11
UWP アプリのパッケージ名と EXE のパス名を,AppUserModelId の値から取得
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics,Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Edit1: TEdit;
    Memo1: TMemo;
    Button2: TButton;
    Button3: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses
  System.Win.Registry,
  Generics.Collections,
  Generics.Defaults,
  System.StrUtils,
  System.IOUtils,
  System.Types,
  Winapi.ShellAPI,
  System.RegularExpressions;

{$R *.DFM}

type
  PUwpPkgItem = ^TUwpPkgItem;
  TUwpPkgItem = record
    RegKeyName  : string;
    PgkFullPath : string;
  end;

  var
    FUwpPkgList     : TList<TUwpPkgItem>;
    FPkgExeFullPath : string;
    FPkgFullPath    : string;

//=============================================================================
//  フォーム生成時の処理
//
//  レジストリのパッケージのキー名のリストを作成
//  インストール先のフルパッケージ名 (ディレクトリ名) も取得しておく
//  結果はレコード型のデータとして格納
//  そのレコード型のデータをパッケージ情報格納用のリストに追加していく
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
var
  LRegistry   : TRegistry;
  LKeyPkgRoot : string;
  LKeyPkg     : string;
  LKeyList    : TStringList;
  LPkgPath    : string;
  LPkgItem    : TUwpPkgItem;
  LIndex      : Integer;
begin
  Button2.Enabled := False;
  Button3.Enabled := False;

  // UWP アプリ 電卓の AppUserModelId の値
  Edit1.Text:= 'Microsoft.WindowsCalculator_8wekyb3d8bbwe!App';


  FUwpPkgList := TList<TUwpPkgItem>.Create;

  LRegistry := TRegistry.Create;
  try
    LRegistry.Access  := KEY_READ;
    LRegistry.RootKey := HKEY_CLASSES_ROOT;
    LKeyPkgRoot := 'Local Settings\Software\Microsoft\Windows\CurrentVersion'
                 + '\AppModel\Repository\Packages';
    LKeyList := TStringList.Create;
    try
      if not LRegistry.OpenKey(LKeyPkgRoot, False) then Exit;

      LRegistry.GetKeyNames(LKeyList);
      LRegistry.CloseKey;

      for LIndex := 0 to LKeyList.Count - 1 do begin
        LKeyPkg := LKeyPkgRoot + PathDelim + LKeyList[LIndex];
        if LRegistry.OpenKey(LKeyPkg, False) then begin
          LPkgPath := LRegistry.ReadString('PackageRootFolder');
          if LPkgPath <> '' then begin
            LPkgItem.RegKeyName  := LKeyList[LIndex];
            LPkgItem.PgkFullPath := LPkgPath;
            FUwpPkgList.Add(LPkgItem);
          end;
        end;
        LRegistry.CloseKey;
      end;
    finally
      FreeAndNil(LKeyList);
    end;
  finally
    FreeAndNil(LRegistry);
  end;
end;

//=============================================================================
//  フォーム破棄時の処理
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  FreeAndNil(FUwpPkgList);
end;

//=============================================================================
//  指定した AppUserModelId のインストールのフルパッケージ名を調べる
//  UWP アプリによってはこのコードで取得できない場合あり
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LAppId     : string;
  LKeyPkg    : string;
  LFilesList : TStringDynArray;
  LpkgPath   : string;
  LOption    : TSearchOption;
  LIndex     : Integer;
  LExeIndex  : Integer;
  LText      : string;
begin
  Memo1.Lines.Clear;

  // アプリケーション識別子 AppUserModelId の値
  LAppId := Trim(Edit1.Text);

  for LIndex := 0 to FUwpPkgList.Count - 1 do begin
    // バージョン関係文字列を削除して検索
    LKeyPkg := TRegEx.Replace(FUwpPkgList[LIndex].RegKeyName, '_(.+)_', '_');
    if not SameText(LKeyPkg, LeftStr(LAppId, Length(LKeyPkg))) then Continue;

    // EXE のフルパスを取得
    // EXE は 2 つ以上ある場合がある
    LpkgPath := FUwpPkgList[LIndex].PgkFullPath;
    LOption := TSearchOption.soAllDirectories;
    LFilesList := TDirectory.GetFiles(LpkgPath, '*.exe', LOption);

    Memo1.Lines.Add(FUwpPkgList[LIndex].PgkFullPath);
    Memo1.Lines.Add('');
    for LExeIndex := 0 to Length(LFilesList) - 1 do begin
      Memo1.Lines.Add(ExtractFileName(LFilesList[LExeIndex]));
    end;
    FPkgExeFullPath := LFilesList[0];
    FPkgFullPath    := LpkgPath;
    Break;
  end;

  Button2.Enabled := Length(LFilesList) > 0;
  Button3.Enabled := Length(LFilesList) > 0;

  if Length(LFilesList) = 0 then begin
    LText := 'パッケージが見つかりませんでした。';
    MessageBox(Handle, PChar(LText), '情報', MB_ICONWARNING);
  end;
end;

//=============================================================================
//  パッケージ内の EXE を起動してみる
//  起動不可を確認
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LRes  : HRESULT;
  LText : string;
begin
  if FPkgExeFullPath = '' then Exit;

  LRes := ShellExecute(0, '', PChar(FPkgExeFullPath), nil, nil, SW_SHOW);
  if LRes <= 32 then begin
    LText := SysErrorMessage(GetLastError);
    MessageBox(Handle, PChar(LText), '情報', MB_ICONWARNING);
  end;
end;

//=============================================================================
//  パッケージ内をエクスプローラで表示してみる
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
var
  LRes  : HRESULT;
  LText : string;
begin
  if FPkgFullPath = '' then Exit;

  LRes := ShellExecute(0, '', PChar(FPkgFullPath), nil, nil, SW_SHOW);
  if LRes <= 32 then begin
    LText := SysErrorMessage(GetLastError);
    MessageBox(Handle, PChar(LText), '情報', MB_ICONWARNING);
  end;
end;

end.


UWP アプリは,ShellExecuteEx 関数で起動しても,UWP アプリ自身のプロセス ID の値が取得できません.
EnumWindos 関数で起動を監視すれば,そのコールバック関数内で,起動したアプリの AppUerModelId の値やパッケージ名,EXE 名を取得することができます.



[備考]
UWP アプリの開発者は,エイリアスとしての EXE を作成,設定したり,URL スキームを設定できます.そのような UWP アプリの場合は,電卓のように,それらのコマンドでアプリを起動できます.
URL スキームは,デスクトップアプリにも実装することができます.




11_UWP アプリの URL スキームを調べる


特定の UWP アプリの URL スキームの値を調べるサンプルです.
UWP アプリのインストール先のパッケージ内に AppxManifest.xml という設定ファイルがあります.このファイルの XML コードから URL スキームの設定を読み取ります.本サンプルでは, AppUserModelId の値からインストール先のパッケージ名 (パス名) を取得する部分を関数化して使用しています.

URL スキームは,どのアプリにも存在するとは限りません.テストに使用した「マップ」アプリには,以下の URL スキームがあります.


ms-walk-to:
ms-drive-to:
bingmaps:?where=西脇市
ms-default-location:



図22
「マップ」の URL スキームの値
  • 複数ある
  • 各々用途が異なる

リスト12
パッケージの AppxManifest.xml から,UWP アプリの URL スキームの値を取得
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Edit1: TEdit;
    ListBox1: TListBox;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure ListBox1DblClick(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

  function GetPackagePathFromAppId(AppId: string): string;

var
  Form1: TForm1;

implementation

uses
  System.Win.Registry,
  Generics.Collections,
  System.StrUtils,
  System.RegularExpressions,
  Winapi.ShellAPI,
  Winapi.msxml,
  System.Win.ComObj;

{$R *.DFM}

// =============================================================================
// フォーム生成時の処理
// =============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  // UWP アプリ マップの AppUserModelId の値
  Edit1.Text := 'Microsoft.WindowsMaps_8wekyb3d8bbwe!App';
end;

// ===========================================================================
//  UWP アプリのパッケージから URL スキームの値を取得
//  URL スキームは全ての UWP アプリにあるわけではない
//
//  uses に Winapi.msxml, System.Win.ComObj が必要
// ===========================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LPkgPath     : string;
  LFilePath    : string;
  LStrList     : TStringList;
  LXMLDomDoc   : IXMLDOMDocument;
  LDOMNodeList : IXMLDOMNodeList;
  LIndex       : Integer;
begin
   ListBox1.Items.Clear;

  // IXMLDOMDocument のインスタンスを生成
  LXMLDomDoc := CreateComObject(CLASS_DOMDocument) as IXMLDOMDocument;
  LXMLDomDoc.async := False;
  LXMLDomDoc.preserveWhiteSpace := True;


  // AppUserModelId の値がら UWP アプリのインストール先のパッケージを取得
  // 設定関係の XML ファイルを読み込む
  // その XML コードを IXMLDOMDocument の XML コードにする
  LPkgPath := GetPackagePathFromAppId(Trim(Edit1.Text));
  if LPkgPath = '' then Exit;

  LFilePath := LPkgPath + PathDelim + 'AppxManifest.xml';
  LStrList := TStringList.Create;
  try
    LStrList.LoadFromFile(LFilePath, TEncoding.UTF8);
    LXMLDomDoc.loadXML(LStrList.Text);

    LStrList.SaveToFile('AppxManifest.xml', TEncoding.UTF8);
  finally
    FreeAndNil(LStrList);
  end;


  // URL スキームの値があるノードを取得
  LDOMNodeList := LXMLDomDoc.getElementsByTagName('uap:Protocol/@Name');

  if LDOMNodeList <> nil then begin
    // URL スキームの値を列挙
    for LIndex := 0 to LDOMNodeList.length - 1 do begin
      // 末尾に : (コロン) を追加
      ListBox1.Items.Add(LDOMNodeList[LIndex].nodeValue + ':');
    end;
  end;
  ListBox1.Items.SaveToFile('URLScheme.txt');

  LDOMNodeList := nil;
  LXMLDomDoc := nil;
end;

// ===========================================================================
//  ListBox1 の OnDblClick イベント
//  URL スキームで UWP アプリを起動
// ===========================================================================
procedure TForm1.ListBox1DblClick(Sender: TObject);
var
  LURLScheme : string;
begin
  if ListBox1.ItemIndex < 0 then Exit;

  LURLScheme := ListBox1.Items[ListBox1.ItemIndex];
  ShellExecute(0, '', PChar(LURLScheme), nil, nil, SW_SHOW);
end;

// -----------------------------------------------------------------------------
// AppUserModelId の値からインストール先のフルパスを取得する関数
// レジストリのキー名のリストから探す
// -----------------------------------------------------------------------------
function GetPackagePathFromAppId(AppId: string): string;
var
  LRegistry   : TRegistry;
  LKeyPkgRoot : string;
  LPkgName    : string;
  LKeyPkg     : string;
  LKeyList    : TStringList;
  LPkgPath    : string;
  LIndex      : Integer;
begin
  Result := '';

  LRegistry := TRegistry.Create;
  try
    LRegistry.Access  := KEY_READ;
    LRegistry.RootKey := HKEY_CLASSES_ROOT;
    LKeyPkgRoot := 'Local Settings\Software\Microsoft\Windows\CurrentVersion'
                 + '\AppModel\Repository\Packages';
    LKeyList := TStringList.Create;
    try
      if not LRegistry.OpenKey(LKeyPkgRoot, False) then Exit;

      LRegistry.GetKeyNames(LKeyList);
      LRegistry.CloseKey;

      for LIndex := 0 to LKeyList.Count - 1 do begin
        LKeyPkg := LKeyPkgRoot + PathDelim + LKeyList[LIndex];
        if not LRegistry.OpenKey(LKeyPkg, False) then Continue;

        LPkgName := LKeyList[LIndex];
        LPkgPath := LRegistry.ReadString('PackageRootFolder');

        LRegistry.CloseKey;
        if LPkgPath = '' then Continue;

        LPkgName := TRegEx.Replace(LPkgName, '_(.+)_', '_');
        if not SameText(LPkgName, LeftStr(AppId, Length(LPkgName))) then Continue;

        Result := LPkgPath;
        Break;
      end;
    finally
      FreeAndNil(LKeyList);
    end;
  finally
    FreeAndNil(LRegistry);
  end;
end;

end.


上のコードではタグ名を検出していますが,XPATH の機能を使用した方が確実です.XPATH を使用する場合は,以下のように,XPATH の文字列の先頭に / を追加して // とします.
このコードは,現在のドキュメント内の指定階層下の,Name 属性を持つ全ての <uap:Protocol> タグを,IXMLDOMNode オブジェクトとして取得します.


  // URL スキームの値があるノードを取得
  // documentElement.selectNodes を使用する場合は先頭に / を追加する
  LXPath := '//Applications/Application'
          + '/Extensions/uap:Extension/uap:Protocol/@Name';
  LDOMNodeList := LXMLDomDoc.documentElement.selectNodes(LXPath);


[備考]
「設定」アプリの URL スキームは ms-settings: ですが,設定アプリの AppxManifest.xml には,この URL スキームの記述はありません.上のサンプルでは AppxManifest.xml ファイルを保存するようにしています.それをテキストエディタで表示して確認できます.