kkAyatakaのメモ帳。

誰かの役に立つかもしれない備忘録。

Win32 APIを使ってOSを再起動する

ちょっと必要になって調べてた。呼び出す関数はExitWindowsExなんだけど特権の調整が必要で単純に呼び出すだけでは成功しない。

ExitWindowsExから調べていって、LUID_AND_ATTRIBUTESのところで...

Its meaning is dependent on the definition and use of the LUID

LUID_AND_ATTRIBUTES

といわれて、SE_SHUTDOWN_NAMEで調べたらサンプルが出てきたというオチ。以下はサンプルベースでは無くて、それまでに作っていたコード。

#include <Windows.h>

int main() {
  HANDLE token = NULL;
  OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &token);
  if (token) {
    LUID luid = {};
    LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME, &luid);

    LUID_AND_ATTRIBUTES attr = { luid, SE_PRIVILEGE_ENABLED };

    TOKEN_PRIVILEGES p = {};
    p.PrivilegeCount = 1;
    p.Privileges[0] = attr;

    AdjustTokenPrivileges(token, FALSE, &p, 0, 0, 0);

    CloseHandle(token);
  }

  // 第2引数は値が豊富な上に、推奨される組み合わせがある
  ExitWindowsEx(EWX_REBOOT, SHTDN_REASON_MAJOR_SYSTEM | SHTDN_REASON_MINOR_NETWORK_CONNECTIVITY);
}

ExitWindowsExの第2引数はパターンが豊富なのできちっとリファレンスを参照したほうが良い。

ExitWindowsExにはなじみが無かったんだけど、手札として持っておくと何かあったときに使えそうな感じ。

結局SE_SHUTDOWN_NAMESE_PRIVILEGE_ENABLEDを結びつける文書って見つからなかったんだけど、他の値を使うときはどうなるのか...

参考

C#のラムダをWin32呼び出しで使う

あたり前っちゃーあたり前なんですが、Win32を呼び出す際の選択肢として、なぜかラムダを使うという選択肢が思いつかなかった。

delegate Boolean EnumWindowProc(IntPtr hwnd, IntPtr lp);

[DllImport("User32.dll")]
private static extern Boolean EnumWindows(EnumWindowProc handler, IntPtr lparam);

public void Enum()
{
  int count = 0;
  EnumWindows(
    (hwnd, lp) =>
    {
      ++count;
      return true;
    }, IntPtr.Zero);

    Console.WriteLine(count);
}

処理内容が小さい場合はもちろん、呼び出し処理を完全にラップして抽象化する場合にも使える。コールバック関数がそこだけ必要ってケースも多いので利便性高い。

C++のラムダがCのコールバックにつかえる

ためしにやったら動いた。さすがC++

#include <Windows.h>
#include <iostream>

int main() {
  HWINSTA sta = GetProcessWindowStation();

  EnumDesktops(
    sta,
    [](LPTSTR desktop, LPARAM lp)->BOOL {
      std::cout << desktop << std::endl;
      return TRUE;
    },
    0);
}

Win32は列挙系でコールバックを使うことが多いので、シンプルに書けてすごく助かる。

#include <Windows.h>
#include <iostream>

int main() {
  EnumDisplayMonitors(
    NULL,
    NULL,
    [](HMONITOR mon, HDC dc, LPRECT rc, LPARAM lp)->BOOL {
      std::cout << rc->left << ", " << rc->top << ", " <<
        rc->right << ", " << rc->bottom << std::endl;
      return TRUE;
    },
    0
    );
}

WPFでHotKeyBox

ホットキー用のコントロールを自前で作る際のポイント。TextBoxのオーバーライドとかは必要なくて、UserControlから十分作れそうな感じ。

ポイントはPreviewKeyDownで処理すること。KeyDownだとCtrl+Aで全選択されたり、特定のキーを拾えなかったりする。

this.textBox.PreviewKeyDown += textBox_PreviewKeyDown;

private void textBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
  e.Handled = true;
  
  var mod = e.KeyboardDevice.Modifiers;
  mod |= e.KeyboardDevice.IsKeyDown(Key.LWin) ? ModifierKeys.Windows : ModifierKeys.None;
  mod |= e.KeyboardDevice.IsKeyDown(Key.RWin) ? ModifierKeys.Windows : ModifierKeys.None;

  // 使えるキーは絞ったほうが良いかも 
  var key = IsValidKey(e.Key) ? e.Key : Key.None;

  // ...本来はこのあたりで、登録可能かチェックする
  
  // キーの押下状態からテキストを作る
  textBox.Text = GetHotKeyText(mod, key);
}

WPFだとイベントオブジェクトにKeyboardDeviceがあるのでキー状態を見るのに使える。Windows Formsだと代替するものが無いので、Win32GetKeyboardStateを使ってしまうのが楽だった。

標準だとWinキーが取れないので、キーボードの状態を調べて修飾キーに加えてやる。XPの時ほど空いてないけど、個人的にこれが無いと始まらない感じ。

このやり方でそれなりにうまく動くところまでは確認できているけど、厳密にチェックしていくと穴はあるかも。ただ、ある程度はフォローできそうだと思っているし、機能的にも十分かなーとは思ってます。

WPFでWindow MessageのHook

Windows Forms同じくオーバーライドかなぁと思っていたら全然違いました。

using System.Windows.Interop;

var wnd = new Window();
wnd.Show();

// Show()の後で無いと取れません。
var handle = new WindowInteropHelper(wnd).Handle;
var src = HwndSource.FromHwnd(handle);
src.AddHook(MessageHook);

private IntPtr MessageHook(IntPtr hwnd, int msg, IntPtr wp, IntPtr lp, ref bool handle)
{
  return IntPtr.Zero;
}

特にコンストラクタ時点でハンドルが取れません。Window Messageのハンドリング用にウィンドウを作ったりする場合は(Hot Keyとかね)コンストラクタ内でShow()を呼び出しておけばよい感じ。

Messageハンドリング専用の場合、いろいろと設定して、表示されないように細工が必要でしょう。

private class MsgWnd : Window
{
  public MsgWnd()
  {
    this.Top = -100000;
    this.Left = -100000;
    this.Width = 0;
    this.Height = 0;
    this.WindowStyle = WindowStyle.None;
    this.AllowsTransparency = true;
    this.Background = Brushes.Transparent;

    this.ShowInTaskbar = false;
    this.ShowActivated = false;

    this.SourceInitialized += MsgWnd_SourceInitialized;
    this.Show();
  }

  public IntPtr Handle
  {
    get
    {
      if (handle == IntPtr.Zero)
      {
        handle = new WindowInteropHelper(this).Handle;
      }
      return handle;
    }
  }
  private IntPtr handle;
  
  private void MsgWnd_SourceInitialized(object sender, EventArgs e)
  {
    try
    {
      HwndSource src = HwndSource.FromHwnd(Handle);
      src.AddHook(MessageHook);
    }
    catch (Exception ex)
    {
      Console.WriteLine(ex.Message);
    }
  }

  private IntPtr MessageHook(IntPtr hwnd, int msg, IntPtr wp, IntPtr lp, ref bool handle)
  {
    return IntPtr.Zero;
  }
}

WPFでHot Key

WPFでHot Keyを取り扱う方法です。登録自体はWin32を用いますが、仮想キーの取り扱いやWindow Handleの取得方法なども課題になります。

順番に。登録・解除はWin32

using System.Runtime.InteropServices;

[DllImport("User32.dll")]
private static extern bool RegisterHotKey(IntPtr wnd, int id, int modifiers, int vk);

[DllImport("User32.dll")]
private static extern bool UnregisterHotKey(IntPtr wnd, int id);

キーコードは組み込みのEnumを用い、関数を使ってRegisterHotKeyに渡す仮想キーを得ます。逆変換の関数もあるので、Hot Keyのマネージメントをするクラスのみ仮想キーに依存するようにするのが良いでしょう。

using System.Windows.Input;

ModifierKeys mod;
Key key;

var vkey = KeyInterop.VirtualKeyFromKey(key);
key = KeyInterop.KeyFromVirtualKey(vkey);

Window HandleとWindow Messageの受け取りは次のとおり。Windows Formsと違ってちょっと面倒。

using System.Windows.Interop;

var wnd = new Window();
wnd.Show();

// Show()の後で無いと取れません。
var handle = new WindowInteropHelper(wnd).Handle;
var src = HwndSource.FromHwnd(handle);
src.AddHook(MessageHook);

private IntPtr MessageHook(IntPtr hwnd, int msg, IntPtr wp, IntPtr lp, ref bool handle)
{
  if (msg == 0x0312) // WM_HOTKEY
  {
    handle = true;

    int id = wp.ToInt32();
    int vkey = ((lp.ToInt32() >> 16) & 0x0000FFFF);
    int mod  = ((lp.ToInt32() >>  0) & 0x0000FFFF);
  }
  return IntPtr.Zero;
}

以上を組み合わせれば実現できます。組み上げ方はいろいろあるとは思いますが、私はdelegateを登録できるようにして、使用しました。

public Boolean Register(ModifierKeys mod, Key key, HotKeyEventHandler handler);

デスクトップを指定してプロセスを起動する

プロセスを起動する際は、通常、元のWindow Stationとデスクトップを引き継ぎますが、CreateProcessではこれらを明示的に指定できるようになっています。

STARTUPINFO si = {};
si.cb = sizeof(si);
si.lpDesktop = "WinSta0\\AltDesktop";

PROCESS_INFORMATION pa = {};

CreateProcess(
  NULL,
  "C:\\Windows\\explorer.exe",
  NULL,
  NULL,
  FALSE,
  0x00000020,
  NULL,
  NULL,
  &si,
  &pa
  );

あんまSTARTUPINFOの中身までみませんよね。ただ、これでDesktopを指定して動かすことが出来ます。

新しいデスクトップでExplorerを動かす

CreateDesktopで作成したデスクトップはまっさらで、何も表示されず、普通に使うことが出来ません。たまーにWindowsはこんな感じになりますが、これは、そのデスクトップ用のExplorerが動いていないことが起因しています。

そこで、上記を用いて、Explorerを動かしてやることで、まともに使えるデスクトップにしてやります。

また、3秒で戻ってきましょう(ただ、今回は、がんばれば戻ってこれます)。

#include <Windows.h>

int main() {
  HDESK current = OpenInputDesktop(0, FALSE, GENERIC_ALL);

  HDESK desk = CreateDesktop("AltDesktop", NULL, NULL, 0, GENERIC_ALL, NULL);

  SwitchDesktop(desk);

  STARTUPINFO si = {};
  si.cb = sizeof(si);
  si.lpDesktop = "WinSta0\\AltDesktop";

  PROCESS_INFORMATION pa = {};

  CreateProcess(
    NULL,
    "C:\\Windows\\explorer.exe", // 64-bit OSは64-bit版を
    NULL,
    NULL,
    FALSE,
    0x00000020, // NORMAL_PRIORITY_CLASS
    NULL,
    NULL,
    &si,
    &pa
    );

  Sleep(3 * 1000);

  SwitchDesktop(current);

  CloseHandle(pa.hThread);
  CloseHandle(pa.hProcess);
}

少し注意するのは64-bit OSの場合は64-bit版のExplorerを起動することです。上記のようにフルパス指定なら問題ないですが、explorer.exeと指定した場合、プロセスのアーキテクチャに依存して起動します。タスクバーが表示されないので、間違えるとやや詰みます。

Windows 7の場合、追加したデスクトップではAeroが切れます。Windows 8ではAeroも動きますが、まあ、逆に見分けづらかったりしますね。

魔法のWindow Message

Windows 8の場合、単にExplorerを起動しただけではタスクバーがうまく表示されません。ただ、動く条件はありそうだったので、Spy++で調べたりしながら試したところ、次のようにWindow Messageを送ることでうまくいきました。

HWND wnd = FindWindow("Shell_TrayWnd", NULL);
PostMessage(wnd, 0x574, 0x02, 0x00);

まあ、無理やり探し出しているので、良い方法とは言えんです。製品作ったりするならサポートにきいたほうが良いでしょうね。

参考