@ledsun blog

無味の味は佳境に入らざればすなわち知れず

AppUserModelID を設定したショートカットを作るPoweShellスクリプトをC#にする

15分タイマー - @ledsun blog AppUserModelID を設定したショートカットをつくるPoweShellスクリプトを使いました。

スクリプトの中には明らかにC#と思われるコードが含まれています。 C#へ変換すると理解が進みそうです。 ChatGPTに変換して貰いました。

プロジェクトファイル

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <!-- Windows 10 (19041) 以降のWindows専用APIを使うためのTFM -->
    <TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
    <!-- x64固定(win-x64) -->
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <OutputType>Exe</OutputType>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
  </PropertyGroup>
</Project>

Program.cs

internal static class Program
{
  static void Main(string[] args)
  {
    if (args.Length != 3)
    {
      Console.WriteLine("使い方: ShortcutAumidSetter <ショートカット名(拡張子なし)> <実行ファイルの実パス> <AppUserModelID>");
      return;
    }

    string rawName = args[0];
    string shortcutName = Path.GetFileNameWithoutExtension(rawName); // .lnk が付いていても削除
    string exePath = args[1];
    string appUserModelId = args[2];

    string startMenu = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
    string programs = Path.Combine(startMenu, "Programs");
    Directory.CreateDirectory(programs);

    string lnkPath = Path.Combine(programs, shortcutName + ".lnk");

    // 既存ショートカットは削除
    if (File.Exists(lnkPath))
    {
      var attr = File.GetAttributes(lnkPath);
      if (attr.HasFlag(FileAttributes.ReadOnly))
        File.SetAttributes(lnkPath, attr & ~FileAttributes.ReadOnly);
      File.Delete(lnkPath);
    }

    // ヘルパーに処理を委譲
    LnkAumidHelper.WriteShortcutWithAumid(lnkPath, exePath, appUserModelId);

    Console.WriteLine($"OK: AppUserModelID set -> {lnkPath}");
  }
}

LnkAumidHelper.cs

using System.Runtime.InteropServices;

public static class LnkAumidHelper
{
  /// <summary>
  /// lnk を新規作成または上書きし、AppUserModelID を設定して保存する
  /// </summary>
  public static void WriteShortcutWithAumid(string lnkPath, string exePath, string appUserModelId)
  {
    var link = (IShellLinkW)new CShellLink();
    var persist = (IPersistFile)link;

    // 必須情報を設定
    link.SetPath(exePath);
    link.SetWorkingDirectory(Path.GetDirectoryName(exePath) ?? "");

    // AUMID を設定
    var store = (IPropertyStore)link;
    SetAumidOnStore(store, appUserModelId);

    // 一度だけ保存
    persist.Save(lnkPath, true);
    persist.SaveCompleted(lnkPath);

    Marshal.ReleaseComObject(store);
    Marshal.ReleaseComObject(persist);
    Marshal.ReleaseComObject(link);
  }

  // === 以下は内部実装なので private ===

  [ComImport, Guid("00021401-0000-0000-C000-000000000046")]
  private class CShellLink { }

  [ComImport, Guid("000214F9-0000-0000-C000-000000000046"),
   InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  private interface IShellLinkW
  {
    int GetPath(IntPtr pszFile, int cch, IntPtr pfd, uint fFlags);
    int GetIDList(out IntPtr ppidl);
    int SetIDList(IntPtr pidl);
    int GetDescription(IntPtr pszName, int cch);
    int SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
    int GetWorkingDirectory(IntPtr pszDir, int cch);
    int SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
    int GetArguments(IntPtr pszArgs, int cch);
    int SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
    int GetHotkey(out short wHotkey);
    int SetHotkey(short wHotkey);
    int GetShowCmd(out int iShowCmd);
    int SetShowCmd(int iShowCmd);
    int GetIconLocation(IntPtr pszIconPath, int cch, out int iIcon);
    int SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
    int SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, uint dwReserved);
    int Resolve(IntPtr hWnd, uint fFlags);
    int SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
  }

  [ComImport, Guid("0000010b-0000-0000-C000-000000000046"),
   InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  private interface IPersistFile
  {
    int GetClassID(out Guid pClassID);
    int IsDirty();
    int Load([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, int dwMode);
    int Save([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, bool fRemember);
    int SaveCompleted([MarshalAs(UnmanagedType.LPWStr)] string pszFileName);
    int GetCurFile([MarshalAs(UnmanagedType.LPWStr)] out string ppszFileName);
  }

  [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
   Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
  private interface IPropertyStore
  {
    int GetCount(out uint cProps);
    int GetAt(uint iProp, out PROPERTYKEY pkey);
    int GetValue(ref PROPERTYKEY key, out PROPVARIANT pv);
    int SetValue(ref PROPERTYKEY key, ref PROPVARIANT pv);
    int Commit();
  }

  [StructLayout(LayoutKind.Sequential, Pack = 4)]
  private struct PROPERTYKEY { public Guid fmtid; public uint pid; }

  private static readonly PROPERTYKEY PKEY_AppUserModel_ID =
      new PROPERTYKEY { fmtid = new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), pid = 5 };

  [StructLayout(LayoutKind.Sequential)]
  private struct PROPVARIANT
  {
    public ushort vt, w1, w2, w3;
    public IntPtr p;
    public int i1, i2;
  }

  private const ushort VT_LPWSTR = 31;

  [DllImport("ole32.dll")] private static extern int PropVariantClear(ref PROPVARIANT pvar);

  private static void SetAumidOnStore(IPropertyStore store, string appId)
  {
    PROPVARIANT pv = new PROPVARIANT
    {
      vt = VT_LPWSTR,
      p = Marshal.StringToCoTaskMemUni(appId)
    };
    var key = PKEY_AppUserModel_ID;
    store.SetValue(ref key, ref pv);
    store.Commit();
    PropVariantClear(ref pv);
  }
}

これがちゃんと動くのです。すごい。

おまかせで書かせると結構余計な機能を入れがちです。 前提条件を厳しくしてエラー処理を省かせる必要があります。 また、コードの設計はあまり上手くないので、クラス分けの仕方は明確に指示する必要があります。 とはいえ対話だけでリファクタリングできました。

動かしたらエラーは出ました。 が、一箇所だけでした。 随分、賢くなったように思います。

AIが書いてくれなかったらCOMを使うプログラムをビルドして実行していませんでした。 最初の一歩を踏み出すための障壁が減るのがとても良いです。