ラベル プログラミング の投稿を表示しています。 すべての投稿を表示
ラベル プログラミング の投稿を表示しています。 すべての投稿を表示

2023/04/14

インストールされたアプリの情報取得

別に新しくも何ともないですが、パッケージされたアプリをインストールした後、登録された情報はPowerShellで簡単に確認できます。

PowerShellコンソールから以下です。
Get-AppxPackage

これをファイルに記録するには、デスクトップにpackages.txtというファイル名で保存するとして、こうです。
Get-AppxPackage > $env:USERPROFILE/Desktop/packages.txt

ちなみに、UWPをVisual StudioからLocal Machineで実行したときに自動インストールされたものは、IsDevelopmentModeがTrueになるようです。

2022/11/30

パッケージされたアプリのファイルの保存場所

AppDataフォルダーへのファイル作成について、発見があったのでメモしておきます。

1. 基本

Microsoftストア用にパッケージされたデスクトップアプリの場合、設定情報等の保存場所としては基本的に以下の3通りがあります。
  • WinRTのApplicationData.LocalSettings
  • WinRTのApplicationData.LocalFolderに作成したファイル
  • AppDataフォルダー(に作成したフォルダー)に作成したファイル
1番目と2番目はUWP用のものですが、パッケージされたアプリであればこれらを使えます。3番目は、使い方は普通のアプリと同じですが、実際のAppDataフォルダーではなく、OSが管理する別の場所に自動的にリダイレクトされるところが違います。

これらのファイルはアプリのアンインストール時に自動的に削除されます。つまり、アプリの実行ファイルのみならず、その作成する情報・ファイルもOSがまとめて管理することで、ゴミとなるファイルを残さずきれいに削除できるようにしているわけです。
ちなみに、AppDataフォルダー以外にもアクセス権に応じてファイルは作成できますが、あえてやることでもなし。

2. 発見

ここまではDesktop Bridgeの基本ですが、3番目について、ふとしたことから例外があるのに気づきました。

すなわち、アプリをインストールした後、最初にAppDataフォルダーにファイルを作成しようとした際に、
  1. AppDataフォルダーに指定のフォルダーが存在しない場合、自動的にリダイレクト先の場所が作成され、以後のアクセスはその場所になる。実際のAppDataフォルダーには何も作成されない。これは公式情報どおり。

  2. AddDataフォルダーに指定と同名のフォルダーが既に存在する場合、そのフォルダーにファイルが作成される。このファイルはアプリをアンインストールしても削除されない。
この2通りのどちらになるかは、最初にアクセスしたときに決まるようです。一旦、1.になった後は、AppDataフォルダーに同名のフォルダーを作成しても、ファイルはそこに作成されません。

開発環境では、パッケージされた状態で実行するときもあれば、されていない状態で実行するときもあり、2.のようなケースがあるのには何となく気づいていましたが、改めて確認したところ、こういうことだったと。

これがどういう意味を持つかというと、例えば、ユーザーごとのTempフォルダーであるAppData\Local\Tempに作成するファイルはリダイレクトされないし、作成された後はアプリをアンインストールしても自動的に削除されないということになります。

他人のことはあまり言えませんが、説明が足らないと思います。

2022/06/03

.NET 5でのアプリの更新

Microsoftストアで公開したアプリはストアのAPIを使って更新できますが、.NET 5以降は変わった点があるので、メモしておきます。 更新の際に出るダイアログのために、このオーナーとなるWindowを先に登録する必要がありますが、これは.NET 5より以前は以下のようなものでした。 見てのとおり、COMのIInitializeWithWindowを定義しておいて、StoreContextのインスタンスをこれにキャストするものですが、.NET 5以降はInvalidCastExceptionが出て実行できなくなります。

このための修正としては、usingにWinRTを加えた上でAsでIInitializeWithWindowキャストする。 もしくは、WinRT.Interop.InitializeWithWindow.Initializeを使う。これが公式に出ている方法で、StoreContextをキャストする必要がなく、したがってCOMの定義も要らなくなるので、やるならこちらだと思います。 これを含めた更新のためのヘルパークラスの全体は以下のようになります。

2021/10/08

Winget 1.1

CUIからソフトウェアパッケージのインストールができるWindows Package Manager、別名WingetのVer 1.1がリリースされています。
Wingetは活発に開発が進められていますが、今回の大きな変化としてMicrosoftストアのアプリにもアクセスできるようになっています。これがどういうことかというと、従来はアプリをWingetでインストールできるようにするには、Wingetのレポジトリに登録する作業が必要だったのですが、Microsoftストアにあるアプリはこれが不要になり、そのままでWingetから見えるようになりました。

したがって、Wingetが使える環境であれば、例えばMonitorianをインストールするのに、コマンドプロンプトから以下を実行するだけでできてしまいます。
winget install Monitorian

超簡単! 便利! 楽!

まあ実用的に使うには、どうオプションを付けるか考えて詰める必要はありますが。

既存のChocolateyもありますが、追加的な手間がかからないというのは大きいです。

2021/08/04

Microsoftストアからのアプリの更新

Microsoftストアで公開したアプリは、Desktop Bridgeの場合も含めて、新しいバージョンが公開されているときは「Microsoft Store」アプリでそのアプリのページを開くと自動的に更新がかかります。ただ、これは往々にしてエラーになり、一旦アンインストールしての再インストールを強いられたりします。

一方、ストアのAPIには、アプリからアプリ自身の更新ができるAPIが用意されています。
このAPIはてっきりUWP用かと思っていたのですが、Desktop Bridgeでも使えるようなので、試してみました。

上のページにあるサンプルを必須部分だけにして、Desktop Bridge用の処理を加えると以下のようになります。

見てのとおり、
  1. StoreContext.GetAppAndOptionalStorePackageUpdatesAsyncメソッドで新しいバージョンのあるパッケージ情報(StorePackageUpdate)を取得して、この有無をチェックする
  2. この情報を使ってStoreContext.RequestDownloadAndInstallStorePackageUpdatesAsyncメソッドを実行する
これだけ見ると簡単です。が、テストしてみると全然ダメでした。

1.は、常にパッケージ情報が1つ返ってくるが、このパッケージのバージョン(StorePackageUpdate.Package.Id.Version)は、新しいパッケージのバージョンではなく、現在実行中のパッケージのバージョンを示すので、新しいパッケージがあるかは判別できない。

2.は、このパッケージ情報を使って実行しても現在のパッケージが再インストールされるだけのようで、更新にならない。

これは使い物にならないかなと思いつつ、数少ない先例を見てみると、少し気になることが。 この下の方で、Microsoftの人がどの環境(テスト用か、本番用か)で実行したか聞いているので、環境によって動作が違うのかも。と、自分もローカルでWindows Application Packaging Projectからインストールした場合と、ストアでPackage flight(特定ユーザーへの限定公開=テスト用)として公開した場合で試しただけだったので、この際、ストアでSubmission(一般ユーザーへの公開=本番用)として公開してみました(これを実行する古いパッケージと、新しいパッケージの両方)。

その結果、この状態であれば期待どおりに更新に使えることが分かりました。

1.は、新しいパッケージがないときはパッケージ情報が返ってこないので、その有無を捉えればよい。すなわち、以下のように、上のメソッドの前半だけで判別可能。

2.は、このパッケージ情報を使って実行すると新しいパッケージがインストールされる。

ということで、Desktop Bridgeでも使えることが確認できました。このAPIを利用すればアプリに手動/自動での更新機能を付けられるので便利、なのですが、デバッグを本番環境でやらなければならない、そのために修正の度にパッケージをSubmissonとして公開しなければならないのは、時間的に勘弁してほしいとこです。

2021/05/04

Hello Switcherのサービス化

ノートやタブレットPCでWindows Hello対応の内蔵カメラがある場合、Windows Hello対応のWebカメラを接続しても、Windows Helloに使われるカメラを切り換えられない問題は、Microsoftでも認識されているようです。 最近ようやくというか、周辺機器メーカーから普及帯のWindows Hello対応Webカメラが出ましたし(エレコムのUCAM-CF20FBBK)、既にDellやLenovoからWindows Hello対応カメラ付きのモニターも出ています。中でもMicrosoft Teams専用機能を謳ったC2422HEについて、Dellのサポートに問い合わせてみたところ、やはりPC本体にWindows Hello対応カメラがある場合はモニターのカメラは使えないとの回答でした。この問題にぶち当たるケースが増えるにつれ、Microsoftでもいつまでも放置はされないかもと想像しています。
ともあれ、この問題を解決するためのHello Switcherですが、これまでの実績上、切換え機能自体には問題なさそうです。 一方で、ログイン後という起動タイミングに起因する問題は如何ともしがたく、未解決の問題として残っていました。すなわち、電源オフの間やサスペンド中にUSBカメラの着脱があった場合、その後のログイン前にカメラの切換えは当然できません。

試しにタスクスケジューラでの開始タイミングをスタートアップ時に変えてみましたが(この状態だとUIを出せないので、実用的ではない)、ログイン前にカメラの切換えが間に合う場合もあれば合わない場合もあり、解決策にはならず。

よって、Windowsサービスの実行ファイルも作成し、通常のアプリと併用する(アプリの実行中はサービスはPausedにしておく)方針に変えました。
この結果、切換えが間に合わなくなる場面はほぼなくなりました。ということで、ようやくアプリとして所期の要求を満たすようになりました、パチパチ。

実際、サービスとして動かしている限り、表に出ることもなく自動的に切り換えるので、OSの標準機能と変わりません。これで、この問題は実質的には解決かなと。

2021/05/03

Windowsサービスでデバイスイベントを捕捉する

実行ファイルをWindowsサービスとして動かすために.NETではSystem.ServiceProcess名前空間にServiceContollerServiceBaseクラスが用意されていて、これらに則れば比較的容易にサービスを実装できますが、このServiceBaseの派生クラスからデバイスイベント(USBデバイスの着脱)を捕捉するにはどうすればいいか、鉄板的なものが見当たらなかったので、メモしておきます。

1. 基本

Win32的には、Windowsサービスは大体以下のような仕組みになっているようです。
  1. まずサービス名を指定してCreateService関数でサービスを生成する。
  2. このサービス名とコールバック関数を指定してRegisterServiceCtrlHandler関数を実行すると、サービス自体の管理用(開始、停止等)のコントロールコードがこのコールバック関数に流れてくるようになる。また、この関数の実行時にサービスステータスハンドルが返ってくるので、これをサービスステータスの管理等に利用する。
  3. または、このサービス名とコールバック関数を指定してRegisterServiceCtrlHandlerEx関数を実行すると、2のコントロールコードに加えて、デバイスイベントを含めたシステムイベントのコントロールコードも流れてくるようになる(デバイスイベントの場合は、さらにサービスステータスハンドルを指定してイベントへの登録が必要。この部分はWindowを持つアプリでWindowメッセージを処理するのと基本同じ)。具体的なイベントについては、このコールバック関数(LPHANDLER_FUNCTION_EX)を参照。
ServiceBaseはこの2と、3のシステムイベントについては電源イベント(SERVICE_CONTROL_POWEREVENT)とセッションイベント(SERVICE_CONTROL_SESSIONCHANGE)まで実装していますが、デバイスイベント(SERVICE_CONTROL_DEVICEEVENT)は実装していません。というか、ソースを見ると、実装しかけたまま放置されたようで、これも実装してあれば手間が省けたのですが。

2. 実装

デバイスイベントの捕捉については、ざっと探して以下の先例がありました。
このコードをダウンロードして見ると、ServiceBaseの派生クラス上で上記の3を行い、イベントを独自のコールバック関数で捕捉するようにしています。つまり、ServiceBase内と二重にRegisterServiceCtrlHandlerEx関数を実行しているわけですが、こうするとServiceBase内のコールバック関数へのポインターが上書きされるようで、ServiceBase内のサービスステータス管理用の処理が実行されなくなります。このため、独自のコールバック関数の方でSERVICE_CONTROL_STOPを捕捉してServiceBase.Stopメソッドを呼んでいます。

とりあえず確認したところ、デバイスイベントの捕捉は問題なくできましたが、致命的な問題が。サスペンド時(Windows 10では、標準では「シャットダウン」がサスペンドになる)にAccessViolationExceptionが発生します。これは少し確認したところ、RegisterServiceCtrlHandlerEx関数を実行しただけで起こり、たぶんアンマネージドなハンドル絡みだと思うので、ほぼ対処不能。

ではどうするかというと、既にServiceBase内にコールバックの仕組みはあるので、これを利用できないかとソースを改めて見ると、コールバック関数に来たコントロールコードは途中で捕捉されなければ最終的にServiceBase.OnCustomCommandメソッドに流れてくるので、これを捕捉すればよいと発見(LPHANDLER_FUNCTION_EXにおけるdwControl = ServiceBase.OnCustomCommandにおけるcommand)。カスタムコマンドは128から255までという決まりがあり、SERVICE_CONTROL_DEVICEEVENTは11なので、この範囲に入りませんが、わざわざフィルターしているわけでもないので。

ということで、これなら簡単に実装できます。デバイスイベントへの登録はWindowメッセージの場合とDEVICE_NOTIFY_SERVICE_HANDLE以外は同じ。
なお、OnStartでRegisterの前にUnregisterしているのは、OnStartの後に必ずOnStopが来るとは限らないため。

惜しむらくは、一緒に来るdwEventType(WM_DEVICECHANGEにおけるwParamと同じ)とlpEventData(同じくlParamと同じ)は流れてこないので、デバイスイベントが起きたことしか分からないことですが、イベントがあったことが分かればやりようはあるので。

このサンプルのレポジトリは以下。 以上で用には足りますが、ServiceBaseクラスは.NETでサービスを使うには必須の基底クラスなので、もう少し拡張にオープンにしてほしかったというのが率直な印象でした。

2021/04/10

ストライプ付きプログレスバー

プログレスバーの最近の流行りは、コンテンツを邪魔しないように細くして、存在感を主張しないものだと思いますが、控えめにアクセントを付ける方法として、ストライプを付けるものがあります。それで、進行中はストライプをアニメーションさせると。

WPFというかXAMLのアニメーションはその辺柔軟なので、それほど難しくはないです。

ポイントとしては、連続して描画するタイプの要素をアニメーションさせるときは、その開始位置の値を変えていくようにすれば、比較的簡単にできます。上の例ではDrawingBrushをTileMode="Tile"を指定して敷き詰め、位置をViewportで決めていますが、このXの値を変えてアニメーションさせています。

ここでもう一つポイントとして、アニメーションにはプリミティブな値を変えるDoubleAnimationやColorAnimationをよく使いますが、Viewportの型はRectなので、この中のXはDoubleAnimationでは対象に指定できません。こういう場合は、複合的な値であるRectを変えるRectAnimationが用意されているので、これを使えばいいわけです。

同じようにThickness(Marginの型)を変えるThicknessAnimation、Pointを変えるPointAnimation、Sizeを変えるSizeAnimationもあるので、幾何学的に変化させるアニメーションは大体カバーできます。これら以外にも、AnimationTimelineの派生クラスを見ると、予め用意されているAnimationをチェックできます。

2021/02/11

マルチタッチによるクリックの判別

WPF上のタッチ操作でシングルタッチによるものか、マルチタッチによる(指を複数使う)ものかは、Manipulation系のイベントであれば直接判別できるようになっていますが、移動を伴わないTouch系のイベントのときは直接分かるものがないので、どうするか考えてみた話です。

そんなに難しいことでもなく、Touch系のイベントで来るTouchEventArgsのTouchDeviceがイベントを起こした個々のデバイス(指)を示すので、このIdを記録して、これが一連のイベントが終わるまでに一つしか来ていなければシングルタッチ、複数来ていればマルチタッチと判別できます。

以下のButtonの例では、PreviewTouchDownイベントごとにTouchDeviceのIdをHashSetに記録していき、Clickイベントのときに複数来ているか判別した後で、HashSetを初期化しています。なお、タッチ操作の終わりに常にClickイベントが来るわけではないので、PreviewTouchUpイベントを引っ掛けた上で、これはClickイベントより先に来るので、1秒の猶予をおいてから初期化するようにしています。
なお、一つのTouchDeviceがPreviewTouchDownで何回も来ることはないと割り切れば、単なるカウンターでも十分な気はします。

より実用的に、ClickイベントからBehaviorのCallMethodActionを実行するようにしている場合、そのIsEnabled依存関係プロパティとbindingを張ってもいいですが、そのために妙にコードが増えるのもうまくないので、CallMethodActionの分も含めてBehaviorにまとめたのが以下。
サンプル全体は以下。 以上でシングルタッチかマルチタッチかに応じて実行するメソッドを切り替えられるようになりましたが、実際に試してみると、2つの指を合わせて、それぞれ認識されるまでタッチしてから離す(完全に同時にタッチする必要はない)操作は慣れが必要で、タッチデバイスにもよるでしょうが、この操作の実用性自体が少し微妙なことに気づきました。

2021/01/31

階層的なロケール

.NETアプリではリソースファイル(.resx)をロケールごとに用意しておけば、実行時にユーザーの表示言語に合ったリソースが自動的に選択されます。ロケールは基本的に[言語名]-[地域名]の構成ですが、中国語だけはこれが簡体字と繁体字のために階層的になっているので、確認してみました。

各ロケールのCultureInfoのParentプロパティを辿るとInvariantCultureに辿り着きますが、その一つ前までをまとめてMarkdownの表を生成するコードが以下のとおり。
これを.NET Framework 4.8の上で実行した結果。
自動選択では表示言語と同じリソースがあればそれが、なければ上に辿って存在するリソースが選択されるはずなので、簡体字の場合、表示言語がzh-CN(中国本土)のときはzh-CNのリソースがあればそれが、なければ(zh-CHSは古いものなのでスルーするとして)zh-Hansのリソースがあればそれが選択されることになります。ここで、簡体字なのは中国本土だけだろうと思っていたら、実はシンガポールも簡体字を使っていて、リソースをzh-CNで作成すると表示言語がzh-SGのときには選択されません。

同じように、繁体字の場合、表示言語がzh-TW(台湾)のときはzh-TWのリソースがあればそれが、なければ(zh-CHTはスルーするとして)zh-Hantが存在すればそれが選択されますが、リソースをzh-TWで作成すると香港かマカオで表示言語に繁体字を使っているときには選択されないことになります。

したがって、簡体字のリソースはzh-Hansで、簡体字のリソースはzh-Hantで作成するのが正解で、もし分ける必要が出てきた場合に、その下のロケールで該当部分を上書きすればいいわけです。

念のため、.NET 5.0の上で実行した結果。
これも古いzh-CHSとzh-CHTが消えた以外は同じですが、説明に簡体字か繁体字か明記していないのが不親切。

以上、中国語のロケールはそれぞれzh-Hansかzh-Hantにしておけばよい、という確認まで。

2020/12/31

アプリ実行エイリアス

Microsoftストアにアプリを出すときに、app package manifestで「アプリ実行エイリアス」というものを指定できます。この効能はストアからインストールしたアプリ(デスクトップアプリを含む)をコマンドプロンプトからファイルパスなしのファイル名だけで起動できるというものです。 一応OSの設定にも出てきます。 ストアからインストールしたアプリの実行ファイル等はOSが管理するので、アプリの起動は基本的にスタートメニューに作成されるアイコンからか、自動起動に登録するか以外にはできませんが、そのコマンドプロンプト向けの救済策のようなものです。

これだけならコマンドプロンプトを使わせたいケース以外には関係ない話ですが、Hash Padを開発しているときに別の利用法があるのを見つけました。すなわち、これを宛先に指定することでアプリを起動するショートカットを自由に作成できます。

このアプリ実行エイリアスは以下のパスに自動的に作成されるようです。
C:\Users\[ユーザー名]\AppData\Local\Microsoft\WindowsApps

タネ明かしとしては、PATHを一覧表示するとこのパスが入っているので、それでパスなして起動できるというわけですね。

これはフルパスとしては以下のコードで取得できます。 これ自体はエイリアスというかシンボリックリンクみたいなもので、実体のあるファイルではないですが、これから直接アプリを起動できるほか、ショートカットのターゲットパスに指定してアプリを起動することもできます。

もう一つ重要な点は、WinRTのAPIにはパッケージされたアプリからしか利用できないものがありますが、これから起動するとパッケージからの起動になることです。実行ファイルを探し出して直接起動した場合はパッケージからの起動にならないので、この点は大きな違いです。

というわけで、アプリ実行エイリアスを使うことでストアにアプリを出すときの難点の一つを解決できます。Hash Padでは、これによりコンテキストメニューの「送る」フォルダーにショートカットを作成しています。

2020/10/14

GitHubのWikiブランチへのアクセス

特に難しい話ではないですが、GitHubのレポジトリでWikiを作成すると、wikiというブランチが作成されます。このブランチにリモートのWindowsからアクセスする方法としてはGitHub Desktopが手軽ですが、ぱっと見では分からなかったので、メモです。

初めにパスとしては、そのレポジトリのパスの.gitの前に.wikiを挿入したものになります。したがって、自分のMonitorianを例にとると、レポジトリ自体は
https://github.com/emoacht/Monitorian.gitなので、
https://github.com/emoacht/Monitorian.wiki.gitとなります。

後はGitHub DesktopのAddからClone repositoryを選んで、URLのタブで直接指定してしまいます。

これでクローンしてしまえば、後は普通のレポジトリと同じです。

GitHubの機能や他のサービスを活かしたレポジトリは機能的でいいのですが、それぞれノウハウがあってなかなか面倒です。

2020/06/12

Lenovo 500 FHD Webcam

数少ないWindows Hello対応のUSBカメラの新製品であるLenovo 500 FHD Webcamが到着したので、レビューします。この製品に関しては、Lenovo公式から以外の情報があまりないので。

1. 外観


パッケージをLogicool C920sと並べたところ。店頭陳列用に派手なデザインのLogicoolに対して、色もサイズもおとなし目です。

最初にこの製品の名称ですが、Lenovo公式では「Lenovo 500 Full HD Windows Hello対応 Webカメラ」という、どこまでが製品名でどこからが機能説明なのか分からない表記になっていますが、パッケージにある表記が本来の名称なのでしょう。というわけで、「Lenovo 500 FHD Webcam」です。

Windows Hello対応のUSBカメラとしては、長らくマウスコンピューターのCM01とその後継のCM02が実質的に唯一の選択肢で(Logicool Brio Webcamはこれだけのためには高すぎ)、ようやく選択肢が増えたことになります。

DellのU2720QMの上に設置したところ。C920sはカメラ本体が台座の前にあるのに対して、500 FHDは台座の上にあるので、カメラの位置は少し奥になります。

デザイン的には、レンズとマイクのモチーフが盛り込まれたC920sとは対照的に、500 FHDは余分な装飾も凹凸も一切ないミニマルなデザインになっています。

高さはC920sは本来もっと低いのですが、台座のモニターの前に掛ける爪が長く、そのままだと表示域(縁から6mmぐらい)にかかってしまうので、3mmのゴムを挟んで上に上げています。これはC920sの元設計が古くてベゼルレスのモニターが考慮されてないのでしょう。500 FHDの方は台座の爪が短く、そのままで問題ありません。ちなみに、台座の幅はどちらもきっかり45mmです。

首振りは上下のみのC920sに対して、500 FHDはボール雲台式で自由度は高いです。ただ、C920sは俯角が大きく取れるようになっているので、それには及びませんが、台座自体の設置角でも調整幅は増やせるので(油気圧サスペンションの戦車の如く)、問題にはならないかなと。

上面にはLenovoのロゴがうっすら入っています。これは表面仕上げの違いでロゴだけツヤを付けて見せているだけなので、角度によって見えることがあるという程度です。

USBケーブルはC920sが直出しなのに対して、500 FHDはUSB-Cコネクタに挿す形式(USB-C to USB-Aケーブルが付属)で、この方が自由度は高いですが、ケーブルが宙を走ることになって収まりが悪い面もあり、良し悪しという気がします。

2. 機能


カメラには4つ窓があって、中央から左側に向かって普通のカメラ、赤外線カメラ、赤外線ライト、プライバシーライトです。右側にも何かありそうに見えますが、ここには中にプライバシーシャッターがあるので、センサーはないです。

このシャッターは上のつまみを押さえて横にスライドさせると、中のシャッターも動く仕組みです。

シャッターには赤丸があるので、よく見れば閉じているのが分かります。ただ、カメラに赤丸は一般的には録画中を示すサインなので、閉じている状態を示すにはUI的にどうかなという気がします。つまみの大きさから見て、頻繁に変えるというより念のために付いている感じですが。

普通のカメラの撮影中はプライバシーライトが点灯します。

Windows Helloの認識中は、赤外線ライトが点滅します。色はSurface Proと同じく赤ですが、光はもっと強いです。認識はうまく行けば一瞬なので、Surface Proと変わりません。

画質


500 FHDの普通のカメラは、基本スペックとしてはFull HD(1920×1080)で、C920sの1080pモード、Surface Pro 4のフロントカメラと変わりません。

画質については……自分ではそれほど差が分からないというのが実際のところです。C920sはさすがに自動調整がうまく効いているぐらいは分かりますが……一番古いSurface Pro 4のカメラでも映りは悪くないと感じるので。試していて気づいたのは、むしろ映りにダイレクトに影響するのは、カメラというより照明の方かなと。

なお、LogicoolのLogi Captureのようなカスタマイズソフトは付属しません。余談ですが、Logi Captureは、使ってみるまで知らなかったのですが、カメラの設定ソフトではなく、それ自体がカメラ入力を受けて画像をリアルタイムに加工して他のソフトに出力するもので、当然リソースを結構使うのですよね。カメラ入力は他のソフトで直接受けられるので、画像にこだわりがなければ使わなくていいかなと。

互換性


試した範囲では、Microsoft Teams、Cisco Webex、Zoomで入力元に使えるのを確認しました。

なお、C920sとは違ってマイクはありませんが、個人的には別に求めていないので問題ありません。

3. Windows Helloカメラの切換え


自分は数年来、Surface Pro 4に外部モニターを接続して使っていますが、そのときにはSurface Pro 4の画面に被せる形にするので、Surface Pro 4に内蔵のWindows Helloカメラは使えなくなります。これが500 FHDを購入した理由ですが、実はWindows 10には複数のWindows Helloカメラを切り替える機能がないらしく、500 FHDを接続してもそのままではWindows Helloに内蔵カメラを使おうとします。

対処療法として、内蔵カメラをデバイスマネージャーで無効にしてしまえば、代わりに500 FHDが使われるようになるのが分かったので、500 FHDの有無に応じてこれを自動化するツールとしてHello Switcherを作成しました。 注意点として、OSのサインイン前には動作できないので、前回サインアウト時の最後の状態は次回サインイン前には変更できません。具体的なケースで言うと、前回サインアウト時に500 FHDがある状態だった場合、内蔵カメラは無効化されているので、次回サインイン時に500 FHDがない場合は、有効なWindows Helloカメラが存在しないことになり、Windows Helloが使えません。こういう場合はPIN認証などでサインインして下さい。サインイン後にこのツールが実行された際に内蔵カメラは有効化されます。

4.評価


まとめると、良い点は以下のとおりです。
  • ミニマルですっきりしたデザインがとても良い。
  • Windows Helloカメラとして問題なく使える。
  • 普通のカメラとしても使える。
個人的に悪い点はないですが、一般的な注意点としては以下のとおりです。
  • カスタマイズソフトは付いてこない。
  • マイクはない。
以上です。

2020/01/13

Gitのcore.autocrlfの設定

改行コードはWindowsアプリでは便宜的にCrLfに揃えることにしていて、.gitattributesでは「* text=auto」はコメントアウトし、.editorconfigでは「end_of_line = crlf」と設定していますが、にもかかわらず自分が認識しないところでLfに変わっていることがあり、さらにCrLfに修正してVisual Studio上でStageしようとするとStage対象に移動しないまま消える、という問題がしばらく前から起こっていて、少々困っていました。

Git絡みの問題であろうことは想像できましたが、VSの問題か、GitHub Extension for Visual Studioの問題か、Git自体の問題かよく分からず、Gitの設定にある「core.autocrlf」が関係ありそうな気がする一方、説明を読んだ限り関係なさそうに思えたので特に触らずにいました。

それが、まとまった数のファイルをCrLfに修正してcommitした後、PowerShellのCUIからそのcommitを含めてrebaseし、VSでリロードしたら全部またLfに戻っていたという問題が発生したので、思い切って以下のコマンドを実行。
git config --global core.autocrlf false

そうしたら、変な動作が全部消えました。

この設定を意識してtrueにした記憶はないものの、Git環境の構築は何度もやり直してるので、正確なところは不明。Gitの設定への理解不足といわれればそうかもしれませんが、こういう問題が起こるとは想像し難いものがあり、謎動作されるぐらいなら切っておいた方が面倒がないと思われます。

2019/05/12

Microsoftストアのサブスクリプション情報の取得

Microsoftストアでは、しばらく前からアプリのアドオンのサブスクリプション販売が可能になっています。アプリ自体を販売する場合は、アプリ側の対応はそれほど手間ではないですが、アドオン(アプリ内販売となる)でかつサブスクリプションとなると、割と面倒な対応が必要になります。

ストアの販売用のAPIは新旧の2種類ありますが、Desktop Bridgeのアプリを前提とするならば、新しいWindows.Services.Storeの方を使うことになります。このAPIはStoreContextオブジェクトのメソッド群で構成され、それなりにサンプルがありますが、肝心な情報の取得方法が抜けているので、その補完がお題です。

1. サブスクリプションのライフサイクル


サブスクリプションの状態によってアドオンの動作を切り替えるだけなら、
  • 使用権があるか否か
が分かれば十分で、詳しいことはウェブ上のMicrosoftアカウントを見てくれ、と割り切るやり方もなくはないです。

ただ、使用期間といっても、サブスクリプションのライフサイクルを考えると、以下のような期間に分かれます(サブスクリプションは自動更新が既定で、現在の期間の使用期限までにキャンセルされない限りは自動更新され、また、キャンセルされた後も現在の期間の使用期限までは使用できる)。
  1. 試用期間
  2. 試用期間中にキャンセルされた後の使用期限までの期間
  3. 試用期間/有償期間後の自動更新により開始された有償期間
  4. 有償期間中にキャンセルされた後の使用期限までの期間
サブスクリプション販売の場合、ユーザーの当然の関心事は意図せずに課金されないことだと思います。使用期限前にキャンセルするつもりだったのが、しそこねるといったケースですが、これを避けるにはユーザーが以下の情報を簡単に確認できるようにするのが望ましいです。
  • 現在が(無償)試用期間か有償期間か
  • 現在の期間の使用期限
  • 自動更新が有効になっているか(キャンセルされていないか)否か
これらの正確な情報の取得方法が、このAPIのサンプルでは説明されていません。仕方ないので試行錯誤した結果が以下ですが、これもストアのサーバー次第でいつ変わってもおかしくないので、そのつもりで。

2.1. 使用権があるか否か


これだけなら簡単で、GetAppLicenseAsyncメソッドで可能です。
これで取得できるStoreAppLicenseオブジェクトのうち、アドオンのライセンス情報はAddOnLicensesプロパティのStoreLicenseオブジェクトにあります。ここに目的のアドオンのものがあれば、使用権があるということです。なお、このSkuStoreIdプロパティはStore IDの後にSKU番号が引っ付いたフォーマットで、Store IDそのままではないので要注意。

このメソッドだけは結果がキャッシュされていて、オフラインでも使うことができます。したがって、使用権の確認だけできればいいという場合は、アプリの起動時にこれを実行するだけというシンプルなやり方もあり得ます。

2.2. 現在が試用期間か有償期間か


これにはGetUserCollectionAsyncメソッドが使えます。
これで取得できるStoreProductオブジェクトに含まれるStoreCollectionDataオブジェクトのIsTrialプロパティが目的のものです。サンプルは以下のとおりです。

この方法に辿り着く前にStack Overflowで質問したのですが、答えを付けたMSFTの人には理解されなかったようで。
このメソッドは経験的には、有償期間に入った後にキャンセルされた後は使用期限前であっても(上記の期間4.のケース)そのStoreProductを返してきません。また、オフラインでは使えないので(実行の度にストアのサーバーと通信するらしい)、必要ならローカルに記録しておく必要があります。

2.3. 現在の期間の使用期限


上記のStoreLicenseにあるExpirationDateプロパティがいかにも使えそうに見えますが、これが曲者で、これ単体では使えません。経験的に整理したところでは、サブスクリプションの自動更新が有効のときは正しい日時から3日後の日時になり、無効のときは大体正しい日時(1日のズレあり)になるようです。

この点からすると、自動更新が有効のときは課金処理の遅延を見越して数字をいじっているのではないかという合理的な疑いがありますが、こういう辻褄合わせのために大元のAPIのデータをいじるのはやってはならないことだと思います。さらに問題なのは、StoreCollectionDataのEndDateプロパティも含め、他のメソッドで取得できる使用期限の日時にも同じ問題があり、どれも信用できないことです。

したがって、自前で計算するしかないわけですが、そのためには、
  • 現在の期間の開始日
  • 現在の期間の長さ
が必要になってきます。

まず現在の期間の開始日は、StoreCollectionDataのStartDateプロパティが使えます(上記サンプルのとおり)。なお、これにはAcquiredDateプロパティもありますが、これは実際に購入した日時を指すようで、StartDateとは微妙にズレがあります。このズレの法則性はよく分かりませんが、この計算のための開始日としてはStartDateの方が正しいようです。

次に現在の期間の長さは、先に現在が試用期間か有償期間かを判別する必要がありますが、これは上記のとおりStoreCollectionDataのIsTrialプロパティで分かります。期間の長さは、GetStoreProductsAsyncメソッドで取得できるStoreProductオブジェクトで分かります。
なお、この例にあるGetAssociatedStoreProductsAsyncメソッドでは有償期間の長さは分かりますが、一旦試用期間に入った後は試用期間の長さは分かりません。というのも、このメソッドが返すのは現在の期間の次に購入可能なSKUだけなので、一旦試用期間に入った後は、次にはもう試用期間はなく、試用期間のSKUは取得できないからです。

この2つが分かれれば、後はStoreDurationUnitに気を付けて計算するだけです。

なお、上記のとおり期間4.のケースではUserCollectionDataは取得できませんが、このケースでは自動更新が無効となっている結果、StoreLicenseのExpirationDateプロパティが大体正しい日時を示すので、それをそのまま使えばいいでしょう。

[追記]

自分で計算した日時とMicrosoftのサーバーで期限切れまたは自動延長の処理が行われる日時にはズレがあるので(1日以内)、アプリで期限切れに伴う処理を行う前にはこのズレを考慮する必要があります。

2.4. 自動更新が有効になっているか否か


これはどこにも説明はないですが、情報はあります。

このAPIで取得できるオブジェクトにはExtendedJsonDataプロパティがあり、ストアのサーバーから取得したらしきJSONが格納されています。この中にはそのオブジェクトのプロパティに出てこないものもあり、そういうのはこれを直接見て使えということですね。
StoreCollectionDataのJSONはこのスキーマのcollectionDataに相当するようですが、この中にautoRenewの要素があります。これが経験的には自動更新の状態を示していて、これが存在してtrueなら有効、存在しないかfalseなら無効です。上記のサンプルでは、JSON全体をパースする必要もないので、該当部分を正規表現で抽出して判別しています。

3. まとめ


以上のように必要な情報は取得できるようになりましたが、ここに至るまでの試行錯誤に相当な時間を費やさざるを得なかったので、正直どうかと思います。わざわざ新APIを作ってサンプル等もそれなりに用意したにもかかわらず、詰めが甘いというか。結局、ストアのサーバーにある情報をどう引っ張ってくるかという問題なので、一発で必要な情報が揃うようにした方がクエリ数も減っていいと思いますが。

こんな当てにならないAPIに頼るより、購入日をローカルに記録しておけば後は何とでもなるのではないか、と思ったこともありましたが、調べていくにつれそのやり方ではむしろ色々なケースで整合性を保つのが大変すぎると気づいて断念しました。

なお、Desktop Bridgeのアプリの場合でも、Windows Application Packaging Projectを使えばこのAPIのデバッグ実行はUWPと同じようにできるので、それ自体は難しくないです。

2018/09/08

Desktop App Converterの完全削除

Desktop App Converter(DAC)を使ったことがあれば知ってのとおり、DACは対象アプリのインストーラをコンテナ技術を使って実行するので、DACを実行するOSと同じバージョンのBaseImageを必要とします。

このため、DACを使うには初めに、
  1. MicrosoftストアからDACをインストールする。
  2. OSに合ったBaseImageをダウンロードし、DACでSetupする。
という手順が必要となります。

さて、DACによる対象アプリのAppxパッケージ化が完了し、不要となったDACを削除するときは逆に、
  1. DACをアンインストールする(ストアアプリなので簡単)。
  2. BaseImageを削除する。
で終了かといえば、実はそんなことはありません。

というのも、DACをSetupしたときにBaseImageから展開されたファイル群はDACをアンインストールしても削除されないので。具体的には以下のパスに存在します。
C:\ProgramData\Microsoft\Windows\Images
これらのファイルは現在のBuild 17134のもので約1.5GBありますが、アクセス権限の関係で簡単には削除できません。これを美しく消す方法はないかと調べてみたら、ちゃんと公式にありました。
このCleanupオプションを使えば、全部きれいに消すには以下のコマンドとなります。
DesktopAppConverter.exe -Cleanup All

これは当然にDACをアンインストール前に行う必要があるので、手順を改めれば以下のようになります。
  1. DACでCleanupする。
  2. DACをアンインストールする。
  3. BaseImageを削除する。
ちなみに、BaseImageはSetup後はなくてもConvertは可能なので、Setupした時点でもう不要かもしれません。

そもそも、MicrosoftがWindows 10の細かなバージョン向けのBaseImageを継続的に提供しなくなっているので(歯抜け状態)、DAC自体が使い物にならなくなってきていますが。

2018/03/21

MonitorianとWifinian

しばらく前にMonitarianWifinianをWindowsストアでリリースしました。どちらもWindows用で、OSの機能で行けてないところ(モニターの明るさ調整、Wi-Fi設定)を補完するものです。中身的にはC#によるWPFアプリ(ただし、Win32を多用)で、Desktop Bridgeを利用してストアに出しています。これらの使い方自体は、そんなに難しくないと思うので、開発について簡単に説明しておきます。

1. Monitorian


以前の環境光センサーに関するエントリで、Windows 10のアップデートでアクションセンターにモニターの明るさ調整のスライダーが入るらしいと書きましたが、これまでのところは入っていません。
Monitorianの設定中の「調整後の明るさを表示する」は、このエントリで触れた問題に対応するもので、(環境光センサーがある場合に)設定値に加えて実際の調整後の値も表示することで、設定を多少やりやすくします。

内部的には、モニターの明るさに関して利用可能なWin32、WMIの機能をほぼ全て動員し、Device Instance IDをキーに使って統合して利用しています。少々面倒なことに、それぞれに含まれる機能が断片的なので、統合しないと必要な機能が揃わないのですよね。

外部モニターについては、DDC/CIが有効であることが条件ですが、これはモニターとその設定によるので、開発側としてはどうしようもありません。この問題に引っ掛かったらしきレビューが付いてますが、必要条件には初めから書いてあるので。一応、それと直接表示する機能を追加しました。

名前については、昔アプリには機能が分かりやすい名前を付けるべしという話を読んだことがあって、それを考慮していたこともありますが、そんなお行儀に囚われる必要などないと悟ったので、造語しています。

2. Wifinian


Native Wifiのマネージド実装であるManaged Native Wifiを利用するアプリから機能を強化・リファインの上、改名したものです。
機能としては、前のアプリと比べて、Wi-Fiの状態変化に応じて自動的に更新するようにした、Auto ConnectとAuto Switchの設定に合わせて自動的に介入して接続先を切り変える機能(Engage)を付けた、というのが主な違いです。後者はWindows 7まではOS標準であった機能が、Windows 8以降で簡略化されたものを、再び使えるようにしたことになります。

この辺はMicrosoftのデザイン上の判断で、「Wi-Fiセンサー」の顛末も見るに、ユーザーはWi-Fiに繋がりさえすれば何だって気にしないだろうという判断があったものと想像しますが、まあ実際そうかもとは思いつつ、ユーザーの意図で決めたい場合もあるので。

内部的には、このためにManaged Native Wifiを強化し、WlanClientを保持してWi-Fiの状態変化を監視できるようにしています。引き続きReactivePropertyを使わせていただいてます。その点で再確認した注意事項は、ReactiveCommandに繋げる前にはUIスレッドに戻しておけということです。

名前については、同上。Wifiの後は語感です。

3. ScreenFrame


タスクバーの通知領域にアイコンを出しつつ、タスクバーに引っ付いたウィンドウと通知領域アイコンに重なるウィンドウを(NotifyIconのContextMenuと同じ位置に)出すためのライブラリです。Wifinianの前のアプリで開発したものをベースに、Monitorian用に開発し、後にWifinian用に拡充しています。Wifinianではウィンドウのタスクバーからの脱着ができるようになっています。

このコードの肝は、
  • WM_WINDOWPOSCHANGINGメッセージに引っ掛けてウィンドウのサイズ・位置調整をすること。これにより、ウィンドウのサイズ・位置変更を全て捕捉し、状態に応じてそのWINDOWPOSを改変することでサイズ・位置変更に自然に介入できる。

  • NotifyIcon中のNativeWindowへのWM_DPICHANGEDメッセージを監視することで、通知領域のあるスクリーンのDPI変化を捕捉すること。マルチモニター環境で通知領域アイコンのあるスクリーンのDPI変化をどう検知するかが課題になっていたが、これによりそのスクリーンにウィンドウがない場合でも検知できる。なお、NativeWindowへのウィンドウメッセージ監視は、そもそもNotifyIconがそうしていることに倣ったもの。
ちなみに、非公開メンバーにReflectionでアクセスしている部分がありますが、もはやほとんど開発は行われていないだろうし、実際上、気にする必要はないだろうと。NotifyIconがWinFormsだという点は、それを避けてWin32を直接使うよりマネージドの方がましです。

そういえば、先日、LGの42.5インチのモニターの実物を見てきましたが、広すぎてスクリーンの端に付いたタスクバーを起点とするUIにはもう無理があると感じました。既にこのサイズのモニターが普及価格帯に入っていますが、Microsoftはどうする気ですかね。

[追記]

後から気づきましたが、NotifyIcon中のNativeWindowはプライマリーモニターに存在する扱いになるようで、これではプライマリータスクバーのあるモニターのDPI変化を必ずしも検知できません。また、プライマリータスクバーはどのモニターにも移動できるので、これを完全に追いかけるのはほぼ無理です。

4. StartupAgency/StartupBridge


常駐アプリとして必要な自動起動のためのライブラリがStartupAgencyです。自動起動の方法は幾つかありますが、コード的に一番簡単なのはレジストリに登録することで、インストーラによるアンインストールで掃除できることを前提にすれば、それで十分なのでその方法を取っています。

ただし、Desktop Bridgeの場合はその方法ではダメで、UWPのStartupTaskを使う必要があるので、そのための別ライブラリがStartupBridgeという構成になっています。
問題は、これによる自動起動とユーザーによる手動起動をどう判別するかで(自動起動のときはウィンドウを出さないようにしたい)、自動起動のときにArgumentsを付けることができれば簡単ですが、StartupTaskでこれをやる方法が見つかりませんでした。

代替策として、完全な方法ではないですが、アプリの起動時間を記録するようにし、自動起動が有効な場合に、起動時に前回の起動時間とOSの起動時間(セッション開始時間)を比較して前者の方が前であれば自動起動と判断する方法を考えましたが、どこに記録するかという選択が残ります。

この記録には、ライブラリのモジュール性を高めたかったので、UWPのLocalSettingsを使うようにしました。Desktop Bridgeの場合は実はこの手が使えます。といっても、TaskIdはAppxManifest.xmlのものと一致する必要があるので、そこのアプリ本体への依存は避けられませんが。

[追記]

過去のStack OverflowでのMSFTの人の回答を見ても、Argumentsを付ける方法はないようです。

5. Desktop Bridge


Desktop Bridgeのパッケージ作成は今ではVisual Studioでも直接できますが、Desktop App Converterのやり方に慣れたので、そちらを使っています。面倒な画像作成を自動でやってくれます(ただし、サイズが妙に奇数なので、輪郭がぼける)。コマンドはメモしておいてPowerShellに張ればいいので、さほどには。

このパッケージ作成を14393より新しいバージョンでやりつつ(BaseImageのバージョンは実際のOSのバージョンと一致している必要がある)最低動作バージョンを14393としておくことは可能で、パッケージ作成のためだけに14393の環境を維持する必要はありません。一応、14393のときのISOで環境を作って動作確認はしましたが。

ちなみに、Desktop Bridgeでインストールしたアプリには、UWP同様に以下のパスに専用フォルダーが作成されるので、この中を探せばAppDataに作成したファイルを直接見ることは可能です。
[system drive]\Users\[user name]\AppData\Local\Packages

Windowsストアに出すことのメリットは、正直それだけで認知度が上がる感じではないですが、クラッシュ報告が自動的に集計されてくるのは参考になります。

なお、2018年2月現在で、Desktop Bridgeはまだ専用窓口から申し込んでMicrosoftの担当者とのやり取りを経てから通常のストア申請に移る方式でした。その担当者は現在は既に日本Microsoftの方になっています。

2017/04/09

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

完結編。


Windows 10 Creators Updateがリリースされましたが、Windows 8.1から3年経過して、なぜか分かりませんがMicrosoftがようやくデスクトップアプリのPer-Monitor DPI対応に力を入れてきています。今更WinFormsまで対応してくるとは予想外でした。
新APIの追加など色々ありますが、WPFの場合は特別なことをしていない限り、関係するのはマニフェストへのPerMonitorV2の追加だけです。
これでWPFでも、例のNon-client areaのスケーリングが自動的にかかるようになります(Creators Update上では。なお、この例はAnniversary Updateより前の対応は自前でやる前提のもの)。

これによって、WPFのPer-Monitor DPI対応はひとまず完成ということで、何か特別なことをしない限りマニフェストに書くだけです。まあUIを作っていると、その何か特別なことが往々にして必要になりますが。

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

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

[追記]

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