C#でUACに対応する

Windows Vistaで一部の処理で管理者権限が必要だが、起動時に管理者権限を要求するまでもないようなアプリがある。タスクマネージャやProcess Explorerはその典型例だが、こういうアプリにはシールドアイコンが付加されたボタンやメニューがある。

f:id:espresso3389:20080725051757j:image

このアイコン自体は、Visual Studio 2008がインストールされていれば、

%ProgramFiles%\Microsoft Visual Studio 9.0\Common7\VS2008ImageLibrary\1033

というディレクトリに存在するVS2008ImageLibrary.zipをshieldで検索すればすぐに出てくる。そのため、この画像をそのままアプリで利用しても良いが、ボタンで利用する場合には、OSからもらってきた方が手っ取り早い。

const uint BCM_SETSHIELD = 0x160CU;
SendMessage(buttonHwnd, BCM_SETSHIELD, IntPtr.Zero, new IntPtr(1 /* TRUE */));

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
static extern int SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

また、ボタン以外の場合には、アイコンとして取得してから設定する必要があるが、.NETのイメージとして取得するには、次のようなコードを書けばよい。

Image GetShieldIcon()
{
  try
  {
    const int SIID_SHIELD = 77;
    const uint SHGFI_ICON = 0x000000100U;
    const uint SHGFI_SMALLICON = 0x000000001U; // 16x16
    SHSTOCKICONINFO sii = new SHSTOCKICONINFO();
    sii.cbSize = Marshal.SizeOf(typeof(SHSTOCKICONINFO));
    sii.hIcon = IntPtr.Zero;
    SHGetStockIconInfo(SIID_SHIELD, SHGFI_ICON | SHGFI_SMALLICON, ref sii);
    return Bitmap.FromHicon(sii.hIcon); // hIconの所有権はBitmapに移る
  }
  catch
  {
    // なんか問題が起きたらnullを返す
    return null;
  }
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct SHSTOCKICONINFO
{
  public int cbSize;
  public IntPtr hIcon;
  public int iSysImageIndex;
  public int iIcon;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
  public string szPath;
}
[DllImport("shell32.dll")]
static extern int SHGetStockIconInfo(int siid, uint uFlags, ref SHSTOCKICONINFO psii);

[DllImport("shell32.dll")]
static extern bool DestroyIcon(IntPtr hIcon);

のようなコードを使えば良い(コードはC#)。とはいえ、XPや2000においては、この処理は無意味だし、既に昇格済みならば、このシールドを表示する必要もない。そのため、できれば、Vistaなのかどうかと昇格済みなのかどうかを調べたい。これについては、かなり他力本願ではあるけど、

"C# code to detect UAC elevation on Vista" - Tim Anderson’s ITWriting
Tech writing blog
にあるコードを利用するのが手っ取り早い。これを使えば、シールド表示のコードは、

if (VistaTools.IsReallyVista() && !VistaTools.IsElevated())
{
  // シールド表示が必要
  menuItem.Image = GetShieldIcon();
}

のように書くことが出来る。

そして、タスクマネージャなどのように自分自身を昇格して起動し直したいならば、次のようにShellExecuteを"runas"というverb(動詞)で実行する

void RunElevated()
{
  SHELLEXECUTEINFO info = new SHELLEXECUTEINFO();
  info.cbSize = Marshal.SizeOf(info);
  info.hwnd = IntPtr.Zero;
  info.lpVerb = "runas";
  info.lpFile = Assembly.GetExecutingAssembly().Location;
  info.lpParameters = "-evaluated"; // パラメータは適宜編集してくれ
  info.lpDirectory = null;
  info.nShow = SW.SHOWNORMAL;
  info.hInstApp = IntPtr.Zero;
  info.fMask = SEE_MASK.WAITFORINPUTIDLE;
  ShellExecuteEx(ref info);
}

[DllImport("shell32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern bool ShellExecuteEx(ref SHELLEXECUTEINFO lpExecInfo);

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
struct SHELLEXECUTEINFO
{
  public int cbSize;
  public SEE_MASK fMask;
  public IntPtr hwnd;
  [MarshalAs(UnmanagedType.LPTStr)]
  public string lpVerb;
  [MarshalAs(UnmanagedType.LPTStr)]
  public string lpFile;
  [MarshalAs(UnmanagedType.LPTStr)]
  public string lpParameters;
  [MarshalAs(UnmanagedType.LPTStr)]
  public string lpDirectory;
  public SW nShow;
  public IntPtr hInstApp;
  public IntPtr lpIDList;
  [MarshalAs(UnmanagedType.LPTStr)]
  public string lpClass;
  public UIntPtr hKeyClass;
  public uint dwHotKey;
  public IntPtr hIcon;
  public IntPtr hProcess;
}

enum SW : int
{
  HIDE = 0,
  SHOWNORMAL = 1,
  NORMAL = 1,
  SHOWMINIMIZED = 2,
  SHOWMAXIMIZED = 3,
  MAXIMIZE = 3,
  SHOWNOACTIVATE = 4,
  SHOW = 5,
  MINIMIZE = 6,
  SHOWMINNOACTIVE = 7,
  SHOWNA = 8,
  RESTORE = 9,
  SHOWDEFAULT = 10,
  FORCEMINIMIZE = 11,
  MAX = 11
}

[Flags]
enum SEE_MASK
{
  NOCLOSEPROCESS = 0x00000040,
  NOASYNC = 0x00000100,
  WAITFORINPUTIDLE = 0x02000000,
}