MVP Global Summit 2016 で渡米した際、Microsoft のオールインワン PC “Surface Studio” とともに発表された Surface Dial を入手できたので、さっそくアプリから使ってみました。 アプリでの対応は簡単で、むしろ「どう使わせるか」のアイディア勝負になるデバイスという印象です。
発表時の映像での「Surface Studio の画面に置いて使う」インパクトが強いですが、デバイス自体は BLE (Bluetooth 4.0) で接続するシンプルなものです。 あくまで、Surface シリーズの場合は画面に置いて反応するというだけで、それ以外の PC でも机に置いて使うことができます。 また、今後同様のサードパーティ製品が発売されることも予想されます。
UWP アプリから使う
UWP API の Windows.UI.Input.RadialController
という型を使用して、メニュー項目の操作や回転・クリック (押し込み) イベントを購読します。
エントリー ポイントは RadialController.CreateForCurrentView()
という静的メソッドで、これが RadialController
のインスタンスを取得する唯一の方法です。
また、既定のメニュー項目を削除したりする場合に Windows.UI.Input.RadialControllerConfiguration
という型も使用します。
UWP、および Dial の基本的な実装方針に関しては下記エントリーが詳しいです。
http://blog.okazuki.jp/entry/2016/11/11/171706
https://blogs.msdn.microsoft.com/shintak/2016/11/15/dialprogramming/
WPF アプリから使う (基本)
.NET Framework の WPF や Windows Forms アプリでも、UWP アプリと同じ要領で Surface Dial に対応させることができます。
UWP API である RadialController
を使用することになるため、プロジェクトに WindowsRuntime を参照させる必要があります。
UWP アプリにおける RadialController
のエントリー ポイントが View であるように、デスクトップ アプリの場合はウィンドウ ハンドルです。
つまり、現時点において Surface Dial のコントローラーはウィンドウ単位で作る必要がある、ということです。
まず、RadialController
と RadialControllerConfiguration
インスタンスを取得するため、… の COM オブジェクトを取得するためのインターフェイスを用意します。
これは Microsoft のサンプルで公開されている方法です。
[Guid("1b0535c9-57ad-45c1-9d79-ad5c34360513")] [InterfaceType(ComInterfaceType.InterfaceIsIInspectable)] public interface IDesktopRadialController { RadialController CreateForWindow(IntPtr hWnd, [In] ref Guid iid); } [Guid("787cdaac-3186-476d-87e4-b9374a7b9970")] [InterfaceType(ComInterfaceType.InterfaceIsIInspectable)] public interface IDesktopRadialControllerConfiguration { RadialControllerConfiguration GetForWindow(IntPtr hWnd, [In] ref Guid iid); }
続いて以下は、上記インターフェイス経由で実際にインスタンスを取得するコードの例です。
このコードさえ用意してしまえば、デスクトップ アプリからも DesktopRadialController.Create(hWnd)
のようなシンプルな呼び出しで Surface Dial のコントローラーを操作できるようになります。
public static class DesktopRadialController { public static RadialController Create(IntPtr hWnd) { var controller = (IDesktopRadialController)WindowsRuntimeMarshal.GetActivationFactory(typeof(RadialController)); var iid = typeof(RadialController).GetInterface("IRadialController").GUID; return controller.CreateForWindow(hWnd, ref iid); } } public static class DesktopRadialControllerConfiguration { public static RadialControllerConfiguration Create(IntPtr hWnd) { var configration = (IDesktopRadialControllerConfiguration)WindowsRuntimeMarshal.GetActivationFactory(typeof(RadialControllerConfiguration)); var iid = typeof(RadialControllerConfiguration).GetInterface("IRadialControllerConfiguration").GUID; return configration.GetForWindow(hWnd, ref iid); } }
これ以降の手順は、メニュー項目のアイコンを独自に追加する場合 (後述) を除けば UWP と同じです。
WPF であれば HwndSource
経由でウィンドウ ハンドルを取得し、RadialController
のインスタンスであれこれしましょう。
// ウィンドウ ハンドルから RadialController と RadialControllerConfiguration のインスタンス取得 var controller = DesktopRadialController.Create(source.Handle); var configuration = DesktopRadialControllerConfiguration.Create(source.Handle); // 既定メニューのうちアプリで使いたいものを選んで設定 configuration.SetDefaultMenuItems(new[] { RadialControllerSystemMenuItemKind.Volume, }); // アプリ独自メニューを追加 controller.Menu.Items.Add(RadialControllerMenuItem.CreateFromKnownIcon("Tab", RadialControllerMenuKnownIcon.Scroll)); // Dial の開店イベントを購読 (args.RotationDeltaInDegrees で回転角度を取ったりする) controller.RotationChanged += (sender, args) => Action(args.RotationDeltaInDegrees);
さっそくですが、自分のプロダクト (KanColleViewer) を Surface Dial に対応させ、UI を操作できるようにしました。 タブを切り替えるだけの単純な機能ですが、Surface Dial の回転イベントを拾って切り替えています。 このコードは GitHub で公開しています。
なんと KanColleViewer は Surface Dial に対応しました!!!!111 pic.twitter.com/PS6T8C8dkv
— ぐらばく☪ (@Grabacr07) 2016年11月11日
勢い。【備忘録】Surface Dial 実装まとめ #win10jp pic.twitter.com/SnhioYoeu5
— しのぶ@窓電話エバンジェリスト (@shinoblogavi) 2016年11月15日
WPF アプリから使う (つらい)
RadialController
に追加するメニュー項目は、既定で用意されているアイコンから選択するか、もしくは独自のアイコンを設定することができます。
先述の KanColleViewer の例は既定のアイコンから選択しました。
.CreateFromKnownIcon()
: 既定のアイコンをRadialControllerMenuKnownIcon
から選択.CreateFromIcon()
: 独自のアイコンをIRandomAccessStreamReference
で指定
つまり、独自アイコンのメニュー項目を追加したい場合は、デスクトップ アプリの場合でも IRandomAccessStreamReference
を用意する必要があります。
Microsoft のサンプルにあるとおり、ファイル システム上に存在する画像ファイルをアイコンとして使用する場合は、StorageFile.GetFileFromPathAsync(path)
で StorageFile
を取得し、RandomAccessStreamReference.CreateFromFile(storage)
で OK です。
一方で、アセンブリ内にリソースとして配置した画像ファイルをアイコンとして使用する場合は、少々面倒な操作が必要になります。 先にコードを示しておきます。
async void InitializeController() { var controller = this.CreateRadialController(); // 中略 const string iconUri = "pack://application:,,,/SampleAssembly;Component/assets/Item0.png"; var menuItem = await CreateMenuItem("Sample", new Uri(iconUri, UriKind.Absolute)); controller.Menu.Items.Add(menuItem); } static async TaskCreateMenuItem(string displayText, Uri iconUri) { // リソースの URI からストリームを得る var resourceInfo = Application.GetResourceStream(iconUri); if (resourceInfo == null) throw new ArgumentException("Resource not found.", nameof(iconUri)); using (var stream = resourceInfo.Stream) { // ストリームからバイト配列を得る var array = new byte[stream.Length]; stream.Read(array, 0, array.Length); using (var randomAccessStream = new InMemoryRandomAccessStream()) { // バイト配列を UWP のストリームに書き込む await randomAccessStream.WriteAsync(array.AsBuffer()); return RadialControllerMenuItem.CreateFromIcon(displayText, RandomAccessStreamReference.CreateFromStream(randomAccessStream)); } } }
ご覧の通り、アイコン画像リソースの URI から Stream を作成し、バイト配列に読み込み、それを UWP の InMemoryRandomAccessStream
に書き込む、という手順です。
ストリームへの書き込みで非同期操作が出てくるため、たかだかメニュー項目の作成で async/await している微妙っぷり (まあ、.AsTask().Wait()
してしまえば同期的に書けるものの…)。
上記コードを動かす場合、Windows.Foundation.IAsyncOperation<T>
を await するために System.Runtime.WindowsRuntime.dll の参照が必要です。
ちなみに同アセンブリには .NET Framework の Stream を IRandomAccessStream
にするための .ToRandomAccessStream()
なる便利拡張メソッドがあるのですが、その方法だとアイコンが表示されませんでした。つらい。
UWP であれば RandomAccessStreamReference.CreateFromUri(new Uri("ms-appx:///Assets/Item3.png"))
のように URI を指定して一発で終わりなんですが、WPF アプリの場合はこの方法でしか追加できませんでした (ぐらばく調べ。もっといい方法があったら教えてください…)。
AppX 化したデスクトップ アプリなら ms-appx://
な URI でいけるんじゃないか、と思ってますが未確認。
Win32 アプリから使う
つらそう。
おわりに
場合によって若干面倒なことになるものの、デスクトップ アプリでも簡単に Surface Dial に対応させることができる、ということを紹介しました。
重ねて言っておきますが、これはアイディア勝負です。 UWP でも WPF でも実装自体は簡単で、ユーザーにどれほど有用な操作を提供できるかが勝負になると思っています。 先の KanColleViewer に実装した例はあくまでサンプルであって、私自身あの機能自体が有用であるとは微塵も思っていません。
なので、近々アイディアソンをしたいな、と企んでいます。 が、その前に日本発売いつなんだろう…