読者です 読者をやめる 読者になる 読者になる

tmytのらくがき

個人の日記レベルです

C++/Win32からSurface Dialをさわってみたよ

TL;DR

  • C++/Win32からRadialControllerなんて触るもんじゃない
  • ABI::Windows::Foundation::Collectionsに対する実装はSDKに含まれない
  • MIDLコンパイラ/ns_prefixが必須

AzureaでSurface Dialを扱えるプラグインを書きました

Surface Studioと同時に発表されたSurface Dial、回せて、クリックできるだけのデバイスなのです。 これがMicrosoft Global Summitへ参加したタイミングでちょうど発売されて運よく入手することができたので、 UWP*1も、WPF*2も、やってる人がすでにいるので面白そうなのでC++から触ってみました。

ただただ触ってみるだけじゃ面白くないので、せっかくだから2010年にWindows Mobile 5/6向けにリリースしたTwitterクライアントでSurface Dialが使えるようになるプラグインを書きました。

Surface Dialを制御するCOMオブジェクトを実装して、さらにそれをJScriptから制御してるって感じです。

gist.github.com

デスクトップアプリからSurface Dialを操作する

Surface DialをはじめとするRadialControllerははWinRT経由でOSに統合されているので、UWPからならとても簡単に扱えます。たとえばこんな感じに。

var controller = RadialController.CreateForCurrentView();
controller.RotationChanged += (sender, e) => { }
controller.ButtonClicked += (sender, e) => { }

このAPIですが、シグネチャを見てわかるように、UWPのViewを必要としています。 しかし、デスクトップアプリからだとViewが存在しないのでウィンドウハンドルを使ってインスタンスを生成できるなにがしが必要になってきます。 といっても、.NETからならそれほど大変なものでもなく、COMオブジェクトを経由してインスタンスを生成することができます。*3

[System.Runtime.InteropServices.Guid("1B0535C9-57AD-45C1-9D79-AD5C34360513")]
[System.Runtime.InteropServices.InterfaceType(System.Runtime.InteropServices.ComInterfaceType.InterfaceIsIInspectable)]
interface IRadialControllerInterop
{
    RadialController CreateForWindow(
    IntPtr hwnd,
    [System.Runtime.InteropServices.In]ref Guid riid);
}

これでインスタンスを取得すればだいたいどうにかなります。ですが、なかなかそれなりにつらいよ。ってことが以下で共有されています。

grabacr.net

ところがどっこい、C++だともう少したくさんめんどい。まず、ヘッダファイルとかはWindows SDK 14393に含まれています。 デスクトップC#だと自前定義していたIRadialControllerInteropもSDKで定義があります。なので、インスタンスの取得はこういう感じ。

// IRadialControllerIntropを取得して
Microsoft::WRL::ComPtr<ABI::Windows::UI::Input:IRadialControllerInterop> controllerIntrop;
Windows::Foundation::GetActivationFactory(
    HStringReference(RuntimeClass_Windows_UI_Input_RadialController).Get(),
    &controllerInterop);

// HWNDに対するIRadialControllerを取得する
Microsoft::WRL::ComPtr<ABI::Windows::UI::Input::IRadialController> controller;
controllerInterop->CreateForWindow(hwnd, IID_PPV_ARGS(&controller));

イベントハンドラの追加がめんどくさめなことを除けば、だいたい同じ感じでどうにかなります。

システム定義のアイコンを消したい

Surface Dialを長押ししたときにでるメニューは、デフォルトだとシステム定義のアイコンがいくつかならんでます。

https://i-msdn.sec.s-msft.com/en-us/windows/uwp/input-and-devices/images/windows-wheel/surface-dial-menu-offscreen.png

これを消したりするには、RadialControllerConfiguration::SetDefaultMenuItemsを呼び出すことでできます。 ちなみにMSDNに特に書いてないですが、カスタムアイコンを追加せずにシステムアイコンをすべて消そうとすると、呼び出しは失敗して システム設定された状態にリセットされます。どうやら仕様だそうです。

SetDefaultMenuItemsは、.NETの世界からだとIEnumerable<RadialControllerSystemMenuItemKind>を引数として取るので、 new List<RadialControllerSystemMenuItemKind>();とか適当にやればインスタンスが作れるのですが、C++だと微妙に話が違ってきます。

C++での引数はABI::Windows::Foundation::Collections::IVector<ABI::Windows::UI::Input::RadialControllerSystemMenuItemKind>*となっています。 ちなみに似たようなの3種類あります。

  1. ABI::Windows::Foundation::Collections::IVector
  2. Windows::Foundation::Collections::IVector
  3. Platform::Collections::Vector

似たようなのがいろいろあってめんどくさいですが、1はWinRT ABIでやり取りされる型のインターフェース。2はWinRT内部でやり取りされる型のインターフェース。3は2に対する実装。です。 2が指示されている場合は、3をインスタンス化して使えばOKですが、1が指示されているばあいは、対応する型を手書きする必要があるようです。*4

今回は1が指示されているので、適当に実装するなりなんなりして使えばおっけーです。

MIDLを要求される

ABI::Windows::Foundation::Collections::IVector<T> を実装したクラスを作った。Microsoft::WRL::Make<T>経由でインスタンスも作った。 これで完璧。とおもったらそうでもなく。ABI::Windows::Foundation::Collections配下のインターフェースに対応する実装はMIDLでの定義が必要です。 プロジェクトにIDLファイルを追加して、次のようなコードが必要です。

import "windows.foundation.idl";
import "windows.idl";

namespace Dokoka
{
  declare {
    interface Windows::Foundation::Collections::IVector<Windows::UI::Input::RadialControllerSystemMenuItemKind>;
  }
}

これをビルドすると、たぶん失敗します。プロジェクトを右クリックして、プロパティを開いて、MIDLの設定を少し変えます。

  • General -> Additional Metadata Directories
    • C:\Program Files (x86)\Windows Kits\10\References\Windows.Foundation.FoundationContract\1.0.0.0*5
  • Advanced -> Prepend with 'ABI' namespace
    • Yes

これでビルドすると、IDLファイルのファイル名に _h.h を追加したファイルが生成されます。 このヘッダをincludeして、ABI::Windows::Foundation::Collections::IVector<T>の実装をMicrosoft::WRL::Make<T>で作ってあげれば やっとABI空間のIVector実装のインスタンスが手に入るわけです。

このあたりをやらないと

midlrt : error MIDL4034: Failed to load a dependency file. Windows.winmd (HRESULT:0x80070002 - The system cannot find the file specified. )

と言われたり、

error C2338: This interface instance has not been specialized by MIDL. This may be caused by forgetting a '*' pointer on an interface type, by omitting a necessary 'declare' clause in your idl file, by forgetting to include one of the necessary MIDL generated headers.

と言われたりします。

それから、Prepend with 'ABI' namespaceを付けてないと、MIDLコンパイラが出力したヘッダファイルのコンパイルに失敗します。

おわりに

C++/Win32からSurface Dialを使うのは、IVectorさえ出会わなければそんなに大変じゃないです。IVectorもわかってしまえば大したことないです。 私のぐぐり力が低いばかりに、解決まで3日もかかってしまったのが非常にあれですが…

カスタムアイコンを追加するのはまだ試してないですが、たぶんWPFのつらいあれよりさらにつらそうだな…とおもいつつ、後日試してみます。

*1:http://blog.okazuki.jp/entry/2016/11/11/171706

*2:http://grabacr.net/archives/7141

*3:Windows classic samplesから引用 https://github.com/Microsoft/Windows-classic-samples/blob/master/Samples/RadialController/cs/winforms/RadialControllerInterop.cs

*4:Win2Dも手書きしてたのでたぶんそういうもんらしい

*5:たぶんもっとちゃんとした指定があるはずです…