2016/09/18

WPFのPer-Monitor DPIサポート(その3)

先月のWindows 10 Anniversary Updateに合わせて.NET Framework 4.6.2がリリースされ、WPFのPer-Monitor DPI機能も正式公開になりました。この機能の正式名はとくにないようですが、区別のため、ここではBuilt-in Scalingと呼びます。


1. 条件

このエントリへの質問に対するRohit Agrawal(WPFチームの人)の回答と
例によってDeveloper Guideをまず参照。
まとめると、Built-in Scalingの前提条件は以下のとおりです。
  • OSがWindows 10 Anniversary Update(Redstone 1)以降であること
  • Target Frameworkが.NET Framework 4.6.2以降であること
  • マニフェストのdpiAwarenessがPerMonitorであること
最初に対応OSについて。Windows 8.1については触れられてませんが、サンプル等を見るとWindows 10 Anniversary Updateより前はまとめてSystem DPI Awareにしてしまいなよ、という扱いなので、Windows 8.1のPer-Monitor DPIについてはさっくり放置する方針のようです。

つまり、Windows 8.1については従来どおり独自対応で行くしかないという方向で確定と。まあMicrosoft全社挙げてWindows 10推しの現況では、それ以前のOSに対してリソースの割り当てがないのだろうと思います。今となってはWindows 8.1は過渡期のOSであることは否定できない感じはします。

次に新しいdpiAwarenessについて。従来のdpiAwareに対して、新しくマニフェスト(app.manifest)に導入されました。この二つは併用が可能で、OSがWindows 10 Anniversary Update以降で、かつdpiAwarenessが指定されていれば、この指定の方が有効になる、ということのようです。

例として、サンプルでの指定方法。

上のdpiAwarenessではPer-Monitor DPI Aware、下のdpiAwareではSystem DPI Awareの指定になっています。この場合、コメントにあるとおり、Windows 10 Anniversary Update以降ではPer-Monitor DPI Awareとなりますが、Windows 8.1ではdpiAwareのみが効いてSystem DPI Awareとなり、仮想スケーリングがかかります。

なぜわざわざdpiAwarenessを追加したのかを推測するに、dpiAwareでtrue/PM(またはPer-Monitor)で指定する方法のままだと、(独自対応なしでは)Windows 8.1上で全くスケーリングがかからなくなってしまうので、それを避けたかったのかなと思いますが、何か要らぬ手間がかかっている気がします。

また、これらの基本条件に対して、コンフィグ(App.config)の方でSwitch.System.Windows.DoNotScaleForDpiChangesを指定すると、その値によって設定をオーバーライドできます。
  • True - 上記の条件を満たしていてもBuilt-in Scalingを無効にする
  • False - Target Frameworkが4.6.2より前であってもBuilt-in Scalingを有効にする
例として、有効にする場合(supportedRuntimeは無関係)。

これの使いどころとしては、Target Frameworkは4.6.2に上げたくないけど、Built-in Scalingは使いたいような場合でしょうか。DPIに応じて変化するような独自コントロールがあるのでもなければ、4.6.2の機能がなくても困りませんし。

条件としては以上です。

2. 独自対応との兼ね合い

結局、Windows 8.1上で仮想スケーリングに甘んじたくなければ、引き続き独自対応が必要になります。あるいは、Windows 10以降も独自対応で通したい場合もあるかもしれません。

独自対応との兼ね合いでありそうな問題としては、
  • Built-in Scalingが不要な場合の抑止

    これは実は簡単で、WM_DPICHANGEDが来たときのハンドラー内で「handled = true」として処理済みにしてしまえば、Built-in Scalingは発動しないようです。 

  • DPI変化の伝播ルートの統一

    .NET Framework 4.6.2より前では、VisualTreeのルートたるWindowから傘下のコントロールにDPI変化を伝えるためには、独自に伝播ルートを張る必要がありました。これが4.6.2以降でBuilt-in Scalingが発動したときは、各コントロールのOnDpiChangedメソッドかImage等のDpiChangedイベントで自動的に伝播されます。

    であれば、独自対応の場合でもこのルートを利用した方が楽ですが、この伝播はVisualTreeHelper.SetRootDpiメソッドで発生させることができます。したがって、独自対応でスケーリングさせると同時に、これを実行するようにすることで伝播ルートを統一できます。
ということで、Windows 8.1上だけ独自対応にし、Windows 10 Anniversary Update以降はBuilt-in Scalingに任せるように切り替えることは意外と難しくないです。

自作ライブラリでは、XAMLから指定可能なWillForbearScalingIfUnnecessaryプロパティでこれが可能なようにしています。
3. Non-client areaの新API

やや蛇足になりますが、Anniversary Updateの後にNon-client area(NCA、ウィンドウの枠のクローム部分)のスケーリングに関してエントリが出ています。
既存デスクトップアプリのPer-Monitor DPI対応を促すために遅まきながらWin32 APIを追加するという話ですが、これまで手つかずだったNCAのスケーリングの話もあります。

WPFに関しては、
For the Windows 10 Anniversary Update WPF is being updated to support automatic NCA scaling.

とありますが、実際に試してみると「いや、効いてないし……」という状態。

そこで、NCAのスケーリングを有効にする新APIのEnableNonClientDpiScalingを実行しようとすると、WPFでウィンドウハンドルを取得できるタイミングとしてはおそらく最早のWindow.OnSourceInitializedメソッド内では成功しないし、エラーコードにも有意な情報がなく。

以下のスレッドのコメントによれば、WM_NCCREATEが来たときのハンドラーで実行すればいいらしいですが、
WPFでウィンドウメッセージを捕捉するにはウィンドウハンドルが必要なので、いずれにしてもWindow.OnSourceInitializedメソッドより早くはできなくて、このときにはこのメッセージは通り過ぎた後らしくて捕捉できない、と手が出せない状態。

まあ出せるものから出していくという姿勢は否定しませんが、既にとっ散らかる兆候を見せているので、出来上がったときにはなるべくまとまった形になっているといいな、というのがささやかな希望です。

[追記]

EnableNonClientDpiScalingの効果は、WinFormsであれば簡単に確認できます。

2016/05/29

WPFのPer-Monitor DPIサポート(その2)

少し前になりますが、.NET Framework 4.6.2 PreviewでWPFのプラットフォームによるPer-Monitor DPI機能が試せるようになっています。といっても普通に動きすぎて特記することもなく、またプレビュー段階であまり深入りするのも意味がある気がしないので、簡単にまとめてみます。


1. 概要

まずはWPF-SamplesにあるDeveloper Guideから。View Rawでダウンロードできます。
これを読むと、試すには以下が条件になっています。
  • .NET Framework 4.6.2 Previewのインストール
  • Windows 10限定(Windows 8.1については記載なし)
方法としては、基本的にapp.manifestのdpiAwareをtrue/PMにしておけば終わりです。WPFのレイアウトシステムに乗ったUIであれば、他に何も要りません。それでは対応できない場合のために個別に救済策が用意されていますが、引っ掛かることがなければ使う機会はないと思います。

DPI変化は、VisualのレベルでOnDpiChangedメソッドが用意されているので、これをオーバーライドすることで感知できます。さらにWindowやImageにはDpiChangedイベントが追加されているので、これでも感知できます。

細かくは、DpiScaleInfoはDpiScaleに名前が変わりました。またDpiChangedイベント用にDpiChangedEventHandlerとDpiChangedEventArgsが追加されたので、これだけから新旧のDPIを取得できます。

[追記] VisualTreeHelper.GetDpiメソッド

.NET Framework 4.6.2で追加されたVisualTreeHelper.GetDpiメソッドで対象VisualのDpiScaleを簡単に取得することができますが、これが正確に機能するのはAnniversary Update以降の場合です。それ以前の場合は従来どおり自前の方法で取得する必要があります。

2. テスト

簡単に動作をテストするアプリを書きました。

気づいた点を挙げると、
  • 実際のスケーリングとOnDpiChangedやDpiChangedは結びついていない。つまり、OnDpiChangedをオーバーライドして変えてもスケーリングは操作できない。
  • VisualTreeHelper.SetRootDpiを実行するとVisualTreeで波及的にOnDpiChangedを実行させることができるが、これも実際のスケーリングとは関係しない。
  • WM_DPICHANGEDメッセージを受信したときにhandledをtrueにするとスケーリングを抑止できる。つまり、実行時にスケーリングさせないよう動作を変えることも可能。
実はWindows 8.1に対応させる気があるかどうか分からないので、保険としてWM_DPICHANGEDを受けてスケーリングを直接起こさせる手もあるかと思ったのですが、そうは行かないようで。

一応、このアプリのレポジトリ。
何にせよ、すべては現段階での話です。

2016/05/26

デスクトップアプリからインタラクティブなトースト通知

Windows 10のインタラクティブなトースト通知はデスクトップアプリからも出せます。その前提として、アクションセンター用のINotificationActivationCallbackを実装する必要があります。なお、別にトースト通知がアクションセンターに入っている状態に限らないので、正確な表現ではありませんが。

1. インタラクティブなトーストのXMLを生成する

トーストはXMLで生成する必要がありますが、Windows 10ではWindows 8から形式が変わっています。といっても、Windows 8式でもそのまま出せるので、インタラクティブな機能が必要ならこちらを使うということです。説明は以下を参照。
このXMLは一から組み立ててもいいですが、MicrosoftがNotificationsExtensionsというライブラリを出していて(デスクトップアプリから使用可)、これでWindows 10式のXMLを生成できます。
これを使った例としては以下のようなもの。
この中でインタラクティブな機能はToastActionCustomの部分にあります。
  • InputsのToastTextBoxでテキストボックスを指定し、そのコンストラクタ―でidとして埋め込まれる文字列を決めています。このidがユーザーからの反応を受け取るときに意味を持ちます。
  • ButtonsのToastButtonでボタンを指定し、そのコンストラクタ―のcontentでボタンに表示される文字列を与え、同時にargumentsとして埋め込まれる文字列を決めています。
これで生成されたXMLが以下。
先頭のヘッダーはなくても構いません。actionsの部分を見ると、inputが上のInputsに、actionが上のButtonsに対応しています。

これぐらいならたいした長さのXMLでもないので、アプリ内に文字列として持っておいて、変わる部分を実行時に嵌め込む方が早いかもしれません。

これを使ってトーストを出すとこうなります。

2. ユーザーの反応を受け取る

このトーストにユーザーが反応するとINotificationActivationCallbackのActivateメソッドが実行されます。この引数から型変換を経て得られる情報は以下のとおり。
  • appUserModelId: AppUserModelID
  • invokedArgs: ユーザーが反応したaction(この場合はボタン)に埋め込まれたargumentsの文字列
  • data: 各input(この場合はテキストボックス)に埋め込まれたidをキーの文字列、inputに入力された内容を値の文字列とした構造体の配列
  • count: dataの配列の長さ
したがって、invokedArgsを見ることでどのボタンが押されたか、dataを見ることでどのテキストボックスに何が入力されていたか判別できるわけです。

なお、トーストの下半分のactions部分は地の部分を押しても反応しませんが、上半分のvisual部分は従来のトーストと同様に地の部分を押しただけでも反応し、その場合はinvokedArgsには何も返ってきません。また、この例でIgnoreとしているボタンはactivationTypeに何も指定していないので、押しても何も起こらず、トーストが消えるだけです。

具体例はレポジトリのサンプル(WPF)を見てください。
3. まとめ

以上のようにインタラクティブなトースト自体はさほど難しくはないですが、実際にどう利用するかというと、
  • トーストはユーザーが出さないよう設定できるので、これだけにルートを頼れない。
  • トーストからできることと、アプリ本体からできることをよく整理しないと、ユーザーに余分な学習コストがかかる。複雑なことはアプリ本体を呼び出してから行った方が、たぶんコストがかからない。
という問題があるので、あまり凝らずにシンプルな方がいいのかなと思います。所詮さっと通り過ぎていくのが本来の通知ですし。

2016/04/26

デスクトップアプリのトースト通知とアクションセンター

Windows 10ではユーザーへの通知インターフェイスとしてアクションセンターが中心に位置づけられています。これにWindows 8式のデスクトップアプリからのトースト通知では十分対応できませんが、対応方法のサンプルがMicrosoftから公開されていたので、整理してみます。

なお、言語はC#で、トースト内のインタラクティブ機能については別途

1. 概要

まず基本情報とサンプル(C++とC#)は以下のとおり。
トーストがアクションセンターに対応すると以下のメリットがあります。
  1. トーストがタイムアウトなどで消えた後もアクションセンターに残るので、後から見てアクティベートできる。
  2. トーストを出したアプリが終了した後でも、トーストからアプリを直接起動して処理を続けることができる。
一応Windows 8式のトーストでもトーストが出ている間にアクションセンターを開くとアクションセンターに移動しますが、ただ移動するだけのようです。

コード的には、実はトーストの出し方は基本的にWindows 8と変わりません。これらの機能はトーストの設定によってではなく、アプリでCOMのINotificationActivationCallbackインターフェイスを実装したクラスを使って行います。というより、UWP用に用意された機能を(ラップした関数は提供されないので)剥き出しで使っちゃいなよという感じ。

INotificationActivationCallbackのメンバーはActivateメソッドだけで、トーストがアクティベートされるとこのメソッドが実行されます。これを利用するレベルとしては、
  1. 下準備として、INotificationActivationCallbackを実装したCOMクラスのCLSIDをアプリのショートカットに含める。
  2. そのクラスの型をCOMに登録すると、トーストがアクションセンターに残るようになる。
  3. そのクラスのCLSIDとアプリの実行ファイルのパスをレジストリに登録してCOMサーバーで起動できるようにすると、トーストからアプリを起動できるようになる。
トーストからアプリを起動できるようにする場合、アプリ本体のUIを開く前にバックグラウンドで処理を分岐させたり、アプリ終了時の状態を復元させたり、UXの観点から色々なやり方があると思います。

なお、元々のToastNotificationクラスのイベントも発生するので、両方をうまく組み合わせる必要があります。もしくはActivatedの場合だけ捉えるのであれば、INotificationActivationCallbackのActivateは常に実行されるので、こちらだけ処理する手もあります。

2. コード

先に、INotificationActivationCallbackを実装したCOMクラスのCLSIDについては、ショートカットのプロパティにSystem.AppUserModel.ToastActivatorCLSIDが追加されています(Windows 10 SDKのpropkey.hにある)。読み書きは型がGuidになる以外はAppUserModelIDの場合と同様にできるので省略。

具体的なCOMクラスとしては以下のようなものです(INotificationActivationCallbackの宣言などは省略)。
基底クラスとしてNotificationActivatorBaseをライブラリに置き、その派生クラスのNotificationActivatorをアプリ本体に置く想定です。これはこのクラスのCLSIDはアプリごとに一意である必要があるため。

アプリの起動時にNotificationActivatorの型を引数としてRegisterComTypeメソッドを実行し、COMに登録します。終了時にはUnregisterComTypeメソッドで登録を解除します。トーストがアクティベートされるとActivateメソッドが実行され、RegisterComTypeの引数で与えられていたActionが実行されます。Activateの引数のinvokedArgsとdataはインタラクティブなトーストから返ってくる情報ですが、必要がなければ無視していいです。

次に、アプリをCOMサーバーで起動できるようにレジストリに登録するヘルパークラス。これは普通にレジストリを読み書きするだけです。
アプリの初回起動時にRegisterComServerを実行します。引数はCOMクラスの型と実行ファイルのパス、起動時のコマンドライン引数(もしあれば)。このレジストリのキーはアプリを使わなくなれば当然不要になるので、削除を。

INotificationActivationCallbackの動作は、アプリが実行中に(アクションセンター内に限らず)トーストがアクティベートされると、そのままActivateが実行されます。アプリが終了後にアクティベートされると、レジストリの情報に従ってアプリが起動された後にActivateが実行されます。その際、アプリのコマンドライン引数に自動的に"-Embedding"が追加されるので、その有無でトーストから起動されたか否か判別できます。

細かい実装とサンプル(WPF)はレポジトリを見てください。ToastNotificationのイベントとINotificationActivationCallbackのActivateメソッドのタイミングが分かるようになっています。
3. まとめ

以上のように、単に通知を出すのとは違って、アクションセンターへの対応はアプリのライフサイクルに関わってくるので少し大事になります。またアクティベート時の状態をどうするかによって動作の練り直しが必要になるかもしれません。

ちなみに、Windows 10で実行する場合は、WinRTのアセンブリはWindows.winmdではなくWindows.Foundation.UniversalApiContract.winmdの方が適当かなと思って参照しようとしたら、このパスが以前と変わってました。こういうのは固定されてないと使いづらいんですけどね。

2016/01/06

Trim Copyリリース

Visual Studio拡張機能のTrim Copyをリリースしました。
一言でいうと、コードブロック単位のTrimStart(行頭の空白削除)付きのコピーです。以下の二種類のコピーができます。
  • Copy (no indent)
  • Copy (fixed indent)
Copy (no indent)では行頭の空白(スペース、タブ等)を全削除します。Copy (fixed indent)では行頭の空白を固定幅に調整します(元の幅が大きければ削除し、小さければ挿入する)。行頭のタブはスペースに置換します。

これの何が嬉しいかというと、Visual Studioで書いたコードをMarkdown等にコピーする場面がありますが、このときに行頭の余分な空白は無駄だし、見映えも悪いしで削除したりします。まず名前空間から始まって、クラス、メソッド、メソッド中のブロックとインデントが深くなるにつれ、その一部を切り出したいときにインデント削除が地味に面倒だったりします。標準のMarkdownだとコードブロック指定は4スペース空けですが、これに合わせて調整するのも同じく。

そこは一括置換するなり色々工夫されていると思いますが、そもそもVisual Studioからコピーした時点で処理されてればそういう手間も要らないよね、というのがこの拡張機能です。拡張機能ならではの特長として、自分で選択範囲を行頭や行末にきっちり合わせなくても、自動的に開始行の行頭から終了行の行末までに選択し直してからコピーするので、変にずれたり切れたりすることがありません。

個人的にVisual Studioのコード補完や整形の効いた快適な環境でコードを書いた後の、MarkdownやStackOverflow用の整形が割と億劫だったのですが、これを使えば一発ですし、後から一部修正しても全部コピーし直せばいいので楽です。

具体例を挙げると、以下は拡張機能からユーザーのタブサイズ設定を取得するクラスです。
この中のGetValueFromTextEditorメソッドをCopy (no indent)したものがこれ。
全然たいしたことはしてませんが、こういうものが一発で出るとMarkdown書きの面倒さが緩和されます。

もしMarkdown等へのコピーでフラストレーションを感じていれば試してみてください。

2016/01/01

UniRxでUnityWebRequest

UnityのWWWに代わる新しいUnityWebRequestが5.3で実プラットフォームで使えるようになったので、いつもの如くRxにしようとしたら、まだUniRxにラップしたクラスがなかったので、勉強がてら作ってみました。
といっても、neueccさんご本人がIssueを立てられているので、公式でもすぐサポートされると思いますが、そこはそれ。

流れとしては、UniRxにはCoroutineをObservableに変換するObservable.FromCoroutineがあるので、UnityWebRequestオブジェクトをいい感じにCoroutineにして、これをObservableに変換します。というか、ObservableWWWをベースにUnityWebRequestに合わせて修正したものを、UnityWebRequestのメソッドと引数に合わせてラップしていくだけです。

UnityWebRequestの基本的なCoroutineはこんな感じです。
UnityWebRequest.Getはファクトリーメソッドで、これにurlほかの引数を与えるとUnityWebRequestオブジェクトが返ってくるので、Sendメソッドを実行してyield returnで待ち、結果をisErrorプロパティで判別して、成功ならdownloadHandlerプロパティにアタッチされているDownloadHandlerへの参照が入っているので(ファクトリーメソッドによる)、ここからダウンロードされたものを取得します。

ファクトリーメソッドはGetのほか、Post、Put、Delete、HeadでRESTをカバーしていて、基本のGet以外にTexture用のGetTextureとAssetBundle用のGetAssetBundleもあります。実行はすべてSendで行われ、WWWのようにコンストラクト即実行とは違います。これに引っ掛かって少し悩みました。

これと同じことを、作成したObservableWebRequestでやるとこんな感じです。
ObservableWWWとクラス名が違うだけでした。

基本的なテストは基本のGetで確認しましたが、他はこれからです。といっても、UnityWebRequestは標準で使う限りシンプルなので、間違える要素は少ないと思いますが。