kkAyatakaのメモ帳。

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

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);

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

参考

Window Stationとデスクトップ

Windowsのデスクトップはそもそも一つではありません。デスクトップはWindow Stationによって管理されており、既定でDefault、WinLogon、ScreenSaverがあることになっています。

プロセスはWindow Stationに、スレッドはデスクトップにひも付く感じになっており、それぞれ制御用の関数で切り替えが可能です。

f:id:kkAyataka:20141010213758j:plain

こんな感じなんで、1プロセスで複数のデスクトップに関連付いたアプリを書くことが出来ます。まあ、デスクトップを切り替えるアプリ以外不要でしょうけど...

スレッドはSetThreadDesktopで動作するデスクトップを切り替え可能ですが、UIスレッド(メッセージループが動いているもの)は切り替えることが出来ません(関数が失敗する)。ただし、切り替えが出来ないだけであり、新しく作ることはできるため、複数のデスクトップで動作するウィンドウを制御することは可能です。

Windows Formsで(いろいろはしょってますが)書くと、次のような感じになります。スレッドを生成後、デスクトップを指定して、メッセージループを回すことで、複数のデスクトップにウィンドウを表示できます。

var th = new Thread(
  () =>
  {
  var desk = OpenDesktop("AltDesktop", 0, false, 0x10000000);
  SetThreadDesktop(desk);

  var form = new Form();

  Application.Run(form);
  });

th.SetApartmentState(ApartmentState.STA);
th.Start();

これを利用して、複数のデスクトップで常駐する(通知領域にアイコンを登録する)、設定画面を表示するといったことが可能になります。

通常のアプリに比べて設計が複雑になりますが、これはこれで面白いです。

なお、WPFではうまくいきませんWPFレンダリングの仕組みと、デスクトップ間でWindow Messageがやり取り出来ないことが起因していると思われます。あまり詳細に調べられなかったのですが、デスクトップが異なる場合、レンダリングスレッドの指示が届いて無いのかなーという感じでした。ウィンドウの枠とかは表示されるんですが、中身は全然ダメです。

参考

CreateDesktopによる仮想デスクトップ

Windows 10になって仮想(マルチ)デスクトップが入るみたいですが、しばらく現行Windowsにて仮想デスクトップが出来ないかいじっていました。

CreateDesktopに関しては、前々から気にしつつも見ていなかったのですが、調べてみると結構簡単でした。

/*
  1. 新しいデスクトップを作成して
  2. 切り替えて、
  3. 3秒間まって
  4. 元に戻す。
*/
#include <Windows.h>
#include <tchar.h>

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

  HDESK desk = CreateDesktop(_T("NewDesktop"), NULL, NULL, 0, GENERIC_ALL, NULL); // 1.

  SwitchDesktop(desk); // 2.

  Sleep(3 * 1000); // 3.

  SwitchDesktop(current); // 4.

  CloseDesktop(desk);
  CloseDesktop(current);
}

不用意に動かすと元のデスクトップに戻ってこれないので注意です。Explorerが無いので詰みます。...まあ、何度もやりました。

MSDNにまとまったドキュメントがあります。分量もそれほど多くありません。調べてみるまで全然知らなかったんですが、面白い構造になっています。

Logonとかスクリーンセーバーとか別のデスクトップだったんすねぇ~と。

ただ、まあ、コレで出来る仮想デスクトップはあまりうまくない(万人向けではない)です。一通りくみ上げて、しばらく使っていたのですが、

  1. アプリがすでに動いている場合、別のデスクトップで新しく起動しないものがある(Chrome等)
  2. デフォルトのデスクトップ以外だとNAS上のOfficeファイルが開けない場合がある

1.はアプリのつくりに依存します。UIスレッドは所属するデスクトップを切り替えられないといった制限もあるので(つまり、ウィンドウはデスクトップ間を移動できない)、使い方に制限が出来ます。

2.に関して、通常設定のOfficeはNAS上のファイルを開く際、警告が出て編集できない状態で開きます。この状態のファイルは、デフォルト以外のデスクトップではうまく開けません。一旦警告を確認して、編集可能状態にすれば問題なく開けるようになります。

こんな感じで、分かって使う分には問題ないんですが、広く一般に使ってもらうのは厳しい感じになります。ただ、分かって使う分には作業コンテキストをズバッと切り替えられるので、便利なんですけどね。

CreateDesktopを使わずに、仮想デスクトップを実現するアプリもあって、Windowsではそっちのが主流っぽいです。実装のアイデアが浮かんだので、そちらも軽く実装してみましたが、確かに筋はよさそうに感じました。まあ、個人的にはCreateDesktopを用いたほうが好みですね。

Windows 10はまだ見ていませんが、どんな感じに実装されてるのかなーと見てみるのも面白そうです。