【Windows10】C#から仮想デスクトップを触ってみた

遂にMicrosoftからWindows10が出ましたね。
調べると、MSDNから仮想デスクトップ関連のインターフェースが出ていたのでC#から触ってみました。
既出だったらごめんなさい。

* 注意！
コード上ではエラー処理をいくつか端折ってる部分があるので、コピペする際は気を付けてください。

# 試した環境
Windows 10 Pro 64bit (10.0 ビルド10240)

# 資料
[IVirtualDesktopManager interface (Windows)](https://goo.gl/tdc0Da)

#内容
## SDKの取得
まず、Windows10のSDKを取得します。
https://dev.windows.com/ja-jp/downloads/windows-10-sdk

デフォルトの設定のままインストールすると、
`C:\Program Files (x86)\Windows Kits\10\`
にSDKがインストールされます。

## IVirtualDesktopManager
仮想デスクトップ関係のインターフェースは、デフォルト設定なら
`C:\Program Files (x86)\Windows Kits\10\Include\10.0.******.*\um\ShObjIdl.h`
に記述されています。
**の部分はインストールしたSDKのバージョンによって変わります。

```cpp:ShObjIdl.h、33435行目から
EXTERN_C const IID IID_IVirtualDesktopManager;

#if defined(__cplusplus) && !defined(CINTERFACE)
    
    MIDL_INTERFACE("a5cd92ff-29be-454c-8d04-d82879fb3f1b")
    IVirtualDesktopManager : public IUnknown
    {
    public:
        virtual HRESULT STDMETHODCALLTYPE IsWindowOnCurrentVirtualDesktop( 
            /* [in] */ __RPC__in HWND topLevelWindow,
            /* [out] */ __RPC__out BOOL *onCurrentDesktop) = 0;
        
        virtual HRESULT STDMETHODCALLTYPE GetWindowDesktopId( 
            /* [in] */ __RPC__in HWND topLevelWindow,
            /* [out] */ __RPC__out GUID *desktopId) = 0;
        
        virtual HRESULT STDMETHODCALLTYPE MoveWindowToDesktop( 
            /* [in] */ __RPC__in HWND topLevelWindow,
            /* [in] */ __RPC__in REFGUID desktopId) = 0;
        
    };
```
どうやら仮想デスクトップ関係は、ウィンドウのハンドルと仮想デスクトップのGUIDで制御するみたいです。
GUIDとは：https://ja.wikipedia.org/wiki/GUID

これを元にC#側でインターフェースを作ります。

## 実装
まず属性を指定します。
IUnknownを継承しているので
`InterfaceType(ComInterfaceType.InterfaceIsIUnknown)` を記述し、上にあるGUIDを
`Guid("a5cd92ff-29be-454c-8d04-d82879fb3f1b")` という感じで書きます。
`ComVisible(true)` も必要っぽいですが、デフォルトでtrueらしいので今回は省略してます。
属性に関する詳しい説明はこちらから。
https://msdn.microsoft.com/ja-jp/library/ms227637(v=VS.90).aspx

中にメソッドを書く際、上のコードにある`/* [out] */`はそのまま戻り値として書きます。

そして出来上がったインターフェースが以下。

```csharp
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("a5cd92ff-29be-454c-8d04-d82879fb3f1b")]
public interface IVirtualDesktopManager
{
  bool IsWindowOnCurrentVirtualDesktop(IntPtr topLevelWindow);
  Guid GetWindowDesktopId(IntPtr topLevelWindow);
  void MoveWindowToDesktop(IntPtr topLevelWindow, ref Guid desktopId);
}
```
シンプル！
早速これを使って呼び出しましょう。

呼び出しにはSystem名前空間にある`Activator.CreateInstance`を使用しています。
取り敢えずVirtualDesktopManagerというクラスを用意して、GUIDを指定して呼び出します。

呼び出す際に気を付けて欲しいのが、指定するGUIDはクラスIDということです。
それに関してはShObjIdl.hに記述されているので、それをそのまま持ってきます。

```cpp:ShObjIdl.h、34102行目から
class DECLSPEC_UUID("aa509086-5ca9-4c25-8f95-589d3c07b48a")
VirtualDesktopManager;
#endif
```

こんな感じ。

```csharp
public class VirtualDesktopManager
{
	public static readonly IVirtualDesktopManager DesktopManager;

	static VirtualDesktopManager()
	{
		var type = Type.GetTypeFromCLSID(new Guid("aa509086-5ca9-4c25-8f95-589d3c07b48a"));
		DesktopManager = (IVirtualDesktopManager)Activator.CreateInstance(type);
	}
}
```

これをVirtualDesktopManager.csという名前でまとめて保存します。
出来上がったコードが以下。

```csharp:VirtualDesktopManager.cs
using System;
using System.Runtime.InteropServices;

public class VirtualDesktopManager
{
	public static readonly IVirtualDesktopManager DesktopManager;

	static VirtualDesktopManager()
	{
		var type = Type.GetTypeFromCLSID(new Guid("aa509086-5ca9-4c25-8f95-589d3c07b48a"));
		DesktopManager = (IVirtualDesktopManager)Activator.CreateInstance(type);
	}
}

[InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("a5cd92ff-29be-454c-8d04-d82879fb3f1b")]
public interface IVirtualDesktopManager
{
	bool IsWindowOnCurrentVirtualDesktop(IntPtr topLevelWindow);
	Guid GetWindowDesktopId(IntPtr topLevelWindow);
	void MoveWindowToDesktop(IntPtr topLevelWindow, ref Guid desktopId);
}

```
System名前空間はActivatorやTypeを使うために、
System.Runtime.InteropServices名前空間は属性の指定に使用しています。

## 使ってみる
今回は確認のためにWindowsフォームアプリケーションを作成します。
筆者はVisual Studio 2013 for Windows Desktop を使用して作成しました。
手順は以下。
1. Visual Studioを起動
2. 左メニュー「テンプレート」->「Visual C#」から「Windows フォーム アプリケーション」を選択
3. プロジェクトにVirtualDesktopManager.csを追加
4. Form1のデザインに適当にコントロールを配置
5. 大体こんな感じになってれば準備OK
![image](https://qiita-image-store.s3.amazonaws.com/0/88562/170c79f0-160f-7ec4-1a64-89efb56fc72f.png)
上の画像では、ウィンドウハンドルを書くTextBoxとデスクトップのGUIDを書くTextBoxの二つとIVirtualDesktopManagerの中身を実行するボタンを置いています。
今回、ウィンドウハンドルのTextBoxにはWindowHandleBoxという名前を、GUIDのTextBoxにはGuidBoxという名前をつけています。
以降の説明は、上の画像の状態であることを前提として進めます。

### bool IsWindowOnCurrentVirtualDesktop(IntPtr topLevelWindow)
指定したウィンドウが今表示している仮想デスクトップ上にあるか確認します。

topLevelWindowにはウィンドウハンドルを与えます。
ウィンドウハンドルは、フォームのHandleプロパティで取得できます。

ボタンを押すと自分自身がアクティブな仮想デスクトップ上にいるか確認するコードは以下。

```csharp:Form1.csの一部
// IsWindowOnCurrentVirtualDesktopボタン
private void button1_Click(object sender, EventArgs e)
{
	var result = VirtualDesktopManager.DesktopManager.IsWindowOnCurrentVirtualDesktop(this.Handle);
	MessageBox.Show(result.ToString());
}
```
これをビルドしてWindows10上で実行するとメッセージボックスが表示され、「True」と表示されます。
この`this.Handle`部分を指定のウィンドウハンドルに置き換えることで、そのウィンドウが今アクティブな仮想デスクトップ上に存在するか確認できます。

```csharp:Form1.csの一部
// IsWindowOnCurrentVirtualDesktop
private void button1_Click(object sender, EventArgs e)
{
	// !! WindowHandleBox.Textには16進数でウィンドウハンドルが入っていること前提 !! //
	var wndHandle = new IntPtr(Convert.ToInt32(WindowHandleBox.Text, 16));

	var result = VirtualDesktopManager.DesktopManager.IsWindowOnCurrentVirtualDesktop(wndHandle);
	MessageBox.Show(result.ToString());
}
```
全てのウィンドウハンドルから自分が今見ているデスクトップのウィンドウハンドルだけ欲しい！って時に使えそうです。
（そんな場面あるのかな・・・？）

ちなみにウィンドウハンドルは、Inspectというツールを使用することによって取得できます。
Inspectは先ほどインストールしたSDKに一緒についてきます。場所はデフォルトの状態だと、
`C:\Program Files (x86)\Windows Kits\10\bin\***\` に`inspect.exe`という名前で存在するはずです。
***の部分はお使いのWindowsの環境によって変えてください。
32bitならばarmまたはx86、64bitならばarm64またはx64になります。
起動してマウスポインタを調べたいウィンドウの上に置くだけでハンドルが表示される便利ツールです。
他にも様々なツールがあるみたいなので、適宜使いやすいソフトを探してみてください。

### Guid GetWindowDesktopId(IntPtr topLevelWindow)
指定したウィンドウが所属している仮想デスクトップのGuidを取得します。

IsWindowOnCurrentVirtualDesktopと同様、topLevelWindowにはウィンドウハンドルを与えます。

ボタンを押すとWindowHandleBox.Textに記述されたウィンドウハンドルの仮想デスクトップが持つGUIDを取得し、GuidBox.Textに入れるコードは以下。

```csharp:Form1.csの一部
// GetWindowDesktopId
private void button2_Click(object sender, EventArgs e)
{
	// !! WindowHandleBox.textには16進数でウィンドウハンドルが入っていること前提 !! //
	var wndHandle = new IntPtr(Convert.ToInt32(WindowHandleBox.Text, 16));

	GuidBox.Text = VirtualDesktopManager.DesktopManager.GetWindowDesktopId(wndHandle).ToString();
}
```
wndHandleをthis.Handleの値に置き換えれば、自分自身が所属している仮想デスクトップのGUIDが取得できます。
実行結果は以下。この例では、ウィンドウハンドルに自分自身を指定しています。
![スクリーンショット (4).png](https://qiita-image-store.s3.amazonaws.com/0/88562/9fe9e7b2-8bb7-f7c2-1fa0-edc61283ece9.png)
ウィンドウハンドルの値やGUIDの値は実行環境によって変わります。
ちなみにGUIDの値は、仮想デスクトップを削除して再生成させると違う値になります。

### void MoveWindowToDesktop(IntPtr topLevelWindow, ref Guid desktopId)
指定したウィンドウを別の仮想デスクトップへ移動します。

第2引数のdesktopIdにはGetWindowDesktopIdで取得したGUIDを指定します。

ボタンを押すとウィンドウハンドルに指定された値を持つウィンドウを、指定されたGUIDを持つ仮想デスクトップ上へ移動します。
コードは以下。

```csharp:Form1.csの一部
// MoveWindowToDesktop
private void button3_Click(object sender, EventArgs e)
{
	// !! WindowHandleBox.Textには16進数でウィンドウハンドルが入っていること前提 !! //
	var wndHandle = new IntPtr(Convert.ToInt32(WindowHandleBox.Text, 16));
	var guid = new Guid(GuidBox.Text);

	VirtualDesktopManager.DesktopManager.MoveWindowToDesktop(wndHandle, ref guid);
}
```
例えばGetWindowDesktopIdの項目で貼った画像のような状態で、別の仮想デスクトップにウィンドウを移動してからボタンを押すと、移動前の仮想デスクトップへ戻っていきます。

## 最後に
取り敢えず仮想デスクトップの操作をサクっと書いてみました。
仮想デスクトップ周りの詳しい内容はそのうちMSDNとかで出るんですかね？

使うにはちょっと問題もあったりします。
例えばすべての仮想デスクトップのGUIDが欲しいとなった場合、取得方法がGetWindowDesktopIdしかないのでウィンドウがない仮想デスクトップのGUIDは取れません。
なので、ウィンドウがない仮想デスクトップがあった場合、仮想デスクトップの数とか数えられません（多分）。

理想はどこかに仮想デスクトップが列挙されてて、そこにアクセスすればハンドルなりを取得できることですかね。
実はちゃんとどこかに書いてあったり・・・？

あと、仮想デスクトップ関係のプログラムは当然Windows10以降でないと動作しません。
ちゃんとバージョン判定しましょう。Windows10のメジャーバージョンは10です。

```csharp
if(Environment.OSVersion.Version.Major >= 10) { ... }
```

以上。