2017/03/06

環境光センサーとSurface

現在のタブレットには環境光センサーが付いていて、ディスプレイの明るさを自動調整できるのが普通ですが、実際どういうものかについて。

1. 環境光センサーによる調整

環境光センサー(Ambient Light Sensor)による調整とは、周辺の環境光を感知してディスプレイの明るさを自動調整するものですが、細かくいうと、ディスプレイの設定可能な明るさの範囲内(0~100%で表される。なお、0%は調整可能な下限ということであって、明るさが0ということではない)で、設定された元の明るさからより明るい方に(または暗い方に)寄せて変更します。

この変更幅は明るさの値によって違い、0%と100%では0、すなわち元のままです。その間で、これらの両端から離れるにつれ変更幅は大きくなります。

以下はこれを模式的に示したもので、横軸が元の値、縦軸が調整後の値です。赤線は調整なしの場合で、元の値と調整後の値は同じなので傾き45度の直線になります。対して青線は調整あり(環境光は一定)の場合で、0%から離れるにつれ赤線との差が大きくなっていき、中間を超えて100%に近づくと差が小さくなっていきます。途中で100%に達して、後は水平にそのままということにはなりません。
この調整の計算式はACPI規格で決まっていて、環境光センサーの値、ユーザーが設定した値、ディスプレイに応じてベンダーが用意した値を元に計算が行われます。
この調整後の値を繋いだ線は、環境光が強くなるほど、より上に膨らむ形になります。つまり、0%から出発後に一気に上昇し,ある程度の高さに達すると、後はなだらかに100%に繋がる形になります。

これは結果的に、操作するユーザーの側からすると、値が小さいときは操作量以上に大きく値が変化する一方、一旦大きくなると操作量の割に値が変化しなくなることを意味します。これはユーザーの操作と結果が比例しないということで、UI的には望ましくないです。

2. Windows 10のアクションセンター

Windows 10 Anniversary Updateのアクションセンターでは、ディスプレイの明るさをボタンで変更できるようになっています。

環境光による調整なしの場合は、以下の5段階で、単純に100を4で割った25%刻みになっています。
  1. 0%
  2. 25%
  3. 50%
  4. 75%
  5. 100%
一方、環境光による調整ありの場合は、数字は出ませんが、実際に設定される値は以下のとおりです(これに環境光による調整が加わる)。
  1. 最も暗い: 0%
  2. 暗い: 40%
  3. おすすめ: 50%
  4. 明るい: 60%
  5. 最も明るい: 100%
調整なしの場合と違うのは、2.と4.が「おすすめ」の50%から10%の差になっていることです。これは、環境光センサーによって基本的に「いい感じ」に調整されることを前提として、そこからの微調整を想定しているのだと思います。

この方法の問題点は、容易に想像されるように、
  • そもそも50%が万人に合う値とは限らない。明るさの感じ方には個人差が大きいし、同じ人間でも状況によって好ましい明るさは違う。
  • 環境光による調整がある場合(かつ環境光が十分明るい場合)、上で見たように、50%±10%程度ではさほど変化しないので、調整の役に立たない。
ということです。Microsoftの開発者は環境光センサーに任せておけばいいだろうと、実地に詰めて考えなかったのかなという気がします。ディスプレイの明るさって、個人によって微妙な調整を必要とする、割とデリケートな問題なんですよね。

そのせいか、Creators Updateではアクションセンターに明るさの設定用のスライダーが入るらしいです。それも一つの手っ取り早い解決ではありますが、もう少し工夫する余地はありそうな気がします。

3. Surface Pro 4の場合

さて、Surface Pro 4では実際にどう調整されるかを計測してみたのが以下です。
一見して分かるとおり、50%の部分だけ値を決めて、後は直線で結んだだけです。これには自分も意表を突かれましたが、SurfaceのベンダーとしてのMicrosoftが用意した設定がこうだということです。「おすすめ」の50%で使われる想定なのかもしれませんが、それにしてもざっくりしてます。

一応それぞれの環境を説明すると、
  • 437lux: 昼に日差しの入った室内、明るいオフィス内
  • 207lux: 夜に照明を点けた住宅内
  • 43lux: 薄暗い室内
  • 8lux: 暗い室内
といったところです。大体45luxのときに調整がない場合と同じになりそうなので、普通に照明がある環境では常に明るくする方に調整が入ることが分かります。

計測時には、自分のディスプレイの光が環境光センサーになるべく入らないよう遮蔽しましたが、ディスプレイが明るくなるとどうしても2~3lux上がってしまうので、その程度の変動を含んでいます。使用したコードは以下に置きました。
4. UIとして

UIとして見ると、現在の操作方法には問題があります。
  • 実際の調整後の値が設定時に分からない。
  • 明るさによって操作量に対する調整後の値の変化量が違う。
このため、その時点の環境光下で適当に「いい感じ」に設定して、環境が変わっても自動調整でたまたま「いい感じ」になればそれでよし、ならなければまた設定し直す、の繰り返しという非効率なことになっています。

これを解決するには、最終的にはユーザーの嗜好を学習して調整のデータに加えるAI的なアプローチが必要な気がしますが,差しあたっては調整後の値も表示するようにして見通しをよくするのがいいかもしれません。

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メソッドより早くはできなくて、このときにはこのメッセージは通り過ぎた後らしくて捕捉できない、と手が出せない状態。

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