ラベル WinRT の投稿を表示しています。 すべての投稿を表示
ラベル WinRT の投稿を表示しています。 すべての投稿を表示

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/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として公開しなければならないのは、時間的に勘弁してほしいとこです。

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/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の方になっています。

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の方が適当かなと思って参照しようとしたら、このパスが以前と変わってました。この、どのアセンブリを使えばいいのか分からん問題が、個人的にWinRTの大きな難点なんですけどね。

2015/06/18

WinRTでの設定の保存(続)

PasswordVaultを使った設定の保存を確認しているうち、アプリをアンインストールしてもPasswordVaultに保存された情報は消えずに残り、再度アプリをインストールすると読み出せることに気づきました。

残るといっても暗号化されているので大過ないといえばないですが、一応、暗号化したファイルをLocalFolder/RoamingFolderに保存する(LocalFolder/RoamingFolderはアンインストール時に削除される)ものも追加していたら全体がややこしくなってきたので、まとめ直しました。
これを使った設定クラスは、例えば以下のようになります。
以下の属性で保存先と暗号化の有無を指定します。
  • RoamingAttribute : ローミングする。
  • CryptVaultAttibute : PasswordVaultに保存する。
  • CryptFileAttribute : 暗号化したファイルをLocalFolder/RoamingFolderに保存する。
PasswordVaultはローミングされるので、CryptVaultAttibuteの場合はRoamingAttributeは影響しません。また、CryptVaultAttibuteはCryptFileAttributeより優先されます。

型は、直接扱えないものはシリアライズして保存するので制限はない、はずですが、シリアライズ特有の問題は起こり得るので、それぞれ対処を。各プロパティの初回アクセス時にリフレクションを使ってどう扱うかを決めますが、結果をキャッシュしたものを次回から使うので、コストはそんなには高くないと思います。

まあ実際はここまでやらずとも、DictionaryにまとめてシリアライズしてRoamingFolderに保存一本でもいいんですが、あるものは使ってみたいという性分なので。

[追記] LocalSettings/RoamingSettingsでサポートされる型

LocalSettings/RoamingSettingsはIDictionaryになっていて、この値の型はObjectですが、実際に保存できるのはLocalSettingsの説明によればWindows Runtime base data typesとされています。

このリストの型をテストしてみたところ(voidを除く)、なぜかUriだけはサポートされてない型という例外が出て駄目でした。一方、ここにはないbyte[]でも保存できるので、どんな型でもシリアライズしてbyte[]にするか、そのままbyte[]に変換すれば、直接出し入れは可能です。

[修正]

例のシングルトンインスタンスの生成方法を修正。C# 6.0の理解が足りてませんでした。

2015/06/07

WinRTでの資格情報の保存

WinRTで資格情報、パスワードであったりアクセストークンであったり、に限らず、秘密を要する情報を安全に保存するにはWindows.Security.Credentialsが使えます。
ざっくりまとめると、
  • リソース名(Resource)、ユーザー名(UserName)、パスワード(Password)から成るPasswordCredentialオブジェクトをPasswordVaultクラスのメソッドを使って書き込み・読み出しする。
  • リソース名は、Twitterのようなサービス名、あるいは複数のユーザー情報をまとめるためのグループ名のようなもの。とくに分類する必要がなければ何でもいい。
  • ユーザー名とパスワードは、そのままの意味で使ってもいいし、要はDictionaryにおけるKeyとValueの組み合わせと同じなので、別の意味を持たせてもいい。
  • 一つのアプリで使えるPasswordCredentialの上限は10(後述)。
これをどう使うかというと、
  • 保存するときは、リソース名、ユーザー名、パスワードでPasswordCredentialを作成して、PasswordVault.Addメソッドを使う。

  • 保存したPasswordCredentialを取得するときは、
    • リソース名とユーザー名が分かっていればPasswordVault.Retrieveメソッドを使う。指定したPasswordCredentialが存在しなければ例外(System.Exception)が飛ぶので、要try-catch。
    • リソース名でまとめて取得するにはPasswordVault.FindAllByResourceメソッド、ユーザー名でまとめて取得するにはPasswordVault.FindAllByUserNameメソッドを使う。これらも存在しなければ例外が飛ぶので、要try-catch。
    • 全部まとめて取得するにはPasswordVault.RetrieveAllメソッドを使う。これは存在しなくても例外は飛ばないので、実はこれからフィルターして探し出せばtry-catchなしで済む。
    • なお、まとめて取得したPasswordCredentialはパスワードが空なので、パスワードを見るにはその前にPasswordCredential.RetrievePasswordメソッドを実行する要あり。

  • 保存したPasswordCredentialのパスワードを修正するときは、同じリソース名、ユーザー名でPasswordCredentialを作成してPasswordVault.Addメソッドを使えば上書きされる(PasswordCredentialを取得して修正しても、保存したものには反映されない)。

  • 保存したPasswordCredentialを削除するときは、一旦取得してからPasswordVault.Removeメソッドを使う。
これらをまとめてみたもの。
ここからGet/Setメソッドを静的メソッドにして、WinRTでの設定を保存するクラスでプロパティのバッキングストアに使えるようにすれば、秘密を要する情報も設定クラスでまとめて管理できるようになるかなと思います。

ちなみに、PasswordCredentialの上限はPasswordVault.Addメソッドの説明には10と書かれてますが、10より少ない状態から一気に作成すれば10を超えて作成できたりしたので、上限をチェックするアルゴリズムに現状は穴があるっぽいです。

それはともかく、量が多い場合は暗号化したファイルをLocalFolderなりRoamingFolderに保存しろという話で、そういう例もあります。

2015/04/15

WinRTでの設定の保存

WinRTでの設定については、どこに、どのタイミングで保存するか考える必要があるわけですが、
  1. 設定が変更される都度、保存するようにした方が確実。
  2. 永続的にするにはApplicationDataのLocalSettingsかRoamingSettingsに保存するのが便利。
  3. 設定用クラスの設定用プロパティのアクセサー内でこれらにアクセスするようにすると管理がすっきりする。
  4. そもそもLocalSettingsかRoamingSettingsを設定用プロパティのバッキングストアにしてしまえばいい。
ということになって、@tmytさんがそういう例を出されてます。
この方法をベースに自作列挙型や自作クラスも保存できるように考えて、以下のようになりました。まずは設定用の基本クラス。
自作列挙型は基になる型に変換した上でそれを保存するように、自作クラスはDataContract属性を付ける前提でJSONにシリアライズした形で保存するようにしています。

これを継承した設定用クラスの例。
頻繁に参照されるプロパティでコストが気になる場合はアクセサー内でキャッシュするようにすればいいかと思います。

とくに目新しいことはないですが、一つの定型的方法として。

[修正]

GetValueメソッド中で自作クラスの値がまだ存在しなかった場合の処理を修正。

2014/12/31

デスクトップアプリでインターネット接続を確認する

確かデスクトップアプリの.NET Frameworkにはインターネット接続状況を直接確認できるAPIが何故か存在しません。一方、WinRTにはこれがあります。

NetworkInformationクラスのConnectionProfile.GetNetworkConnectivityLevelメソッドを使うとNetworkConnectivityLevelが取得できます。この値は以下のとおりですが、
  1. None
  2. LocalAccess
  3. ConstrainedInternetAccess
  4. InternetAccess
普通のインターネット接続はInternetAccessなので、インターネット接続の有無は以下のようなメソッドで確認できます。
// using Windows.Networking.Connectivity;
 
private bool CheckInternetConnection()
{
  var profile = NetworkInformation.GetInternetConnectionProfile();
  if (profile == null)
    return false;
 
  return (profile.GetNetworkConnectivityLevel() >= NetworkConnectivityLevel.InternetAccess);
}
インターネット接続状況の変化はNetworkInformation.NetworkStatusChangedイベントで分かるので、簡単なWPFのサンプルアプリとしては以下のようになります。
using System;
using System.ComponentModel;
using System.Windows;
using Windows.Networking.Connectivity;

public partial class MainWindow : Window
{
  public MainWindow()
  {
    InitializeComponent();
  }

  public bool IsInternetConnected
  {
    get { return (bool)GetValue(IsInternetConnectedProperty); }
    set { SetValue(IsInternetConnectedProperty, value); }
  }
  public static readonly DependencyProperty IsInternetConnectedProperty =
    DependencyProperty.Register(
      "IsInternetConnected",
      typeof(bool),
      typeof(MainWindow),
      new PropertyMetadata(false));

  protected override void OnSourceInitialized(EventArgs e)
  {
    base.OnSourceInitialized(e);

    IsInternetConnected = CheckInternetConnection();
    NetworkInformation.NetworkStatusChanged += OnNetworkStatusChanged;
  }

  protected override void OnClosing(CancelEventArgs e)
  {
    base.OnClosing(e);

    NetworkInformation.NetworkStatusChanged -= OnNetworkStatusChanged;
  }

  private void OnNetworkStatusChanged(object sender)
  {
    this.Dispatcher.Invoke(() => IsInternetConnected = CheckInternetConnection());
  }
  
  private bool CheckInternetConnection()
  {
    var profile = NetworkInformation.GetInternetConnectionProfile();
    if (profile == null)
      return false;

    return (profile.GetNetworkConnectivityLevel() >= NetworkConnectivityLevel.InternetAccess);
  }
}
この他にもWinRTにはデスクトップアプリでも使える機能がちょこちょこあるので、Windowsストアアプリに興味がなくても知っておくと便利なことがあります。

2014/12/17

Windowsストアアプリの実機テスト

Windowsストアアプリを開発するときにタブレットなど実機でテストするためのメモ。

1. 選択


Windowsストアアプリでタブレットなど実機特有の機能のテストが必要になったとき、開発機とテスト機が同じであればいいが、そうでない場合にどうやってテスト機で動かすかというと、
  1. Visual Studioからテスト機をリモートコンピューターとしてデバッグ実行する
  2. テスト機にサイドローディングして実行する
の二通りがある。

2.A. リモートデバッグ


デバッグのしやすさではVisual Studioのデバッガーが使えるAの方が当然ベターで、手順も別に難しくはない。
ただ、これは開発機とテスト機がLANに繋がっていることが必須で、無線LANで接続状況があまりよくないと上手くいかなかったりする。

そういうときは有線LANにすればいいわけだが、自分が使っているDell Venue 8 ProはMicro USBコネクタで充電とUSB接続を兼ねる方式で、充電と(USBを介した)有線LANを同時使用できない(全く不可能ではないが、純正オプションの"Dell Micro USB Dongle for Data and Charging"は日本で売ってないので、ケーブルを自作したりすることが必要)。

そのせいで、ずっと有線LANを使っていると電池が切れるし、ならばと充電を始めてもある程度たまるまで有線LANは使えないしと、地味な面倒くささがある。こういうのは一見たいしたことないように見えて、じわじわ来る。

[追記]

Venue 8 Proに関して別エントリを書いた。

2.B. サイドローディング


そんな事情や、外に持ち出してテストするときのためにBのサイドローディングをすることがある。もちろんリモートデバッグ時にインストールが済んでいれば不要。

で、サイドローディングについて検索してみると、PowerShellを使うのは分かるが、企業内で展開するときの話とごちゃ混ぜになってたりして、テスト目的でするときに何がマストなのかよく分からなかったりする。

結論から言えば、自分でPowerShellを開いてコマンドを打ち込んだりする必要は、ない。

具体的な流れとしては、
  1. 開発機でサイドローディング用のアプリパッケージを作成する
  2. アプリパッケージをテスト機にコピーする
  3. テスト機で開発者用ライセンスを取得する
  4. アプリパッケージのルート証明書をインストールする
  5. アプリをインストールする
となるが、3と4と5は実は一発でできる。

アプリパッケージの作成

  1. Visual Studioのソリューションエクスプローラーで対象のプロジェクトを選択した状態にする
  2. ツールバーからプロジェクト -> ストア -> アプリパッケージの作成を選択
  3. 「Windowsストアにアップロードするパッケージを作成しますか?」に「いいえ」を選んで「次へ」
  4. バージョン番号などを確認して「作成」
  5. 作成が完了し、出力場所のリンクを開くと、作成されたパッケージがある

アプリパッケージのコピー


作成されたもののうちフォルダーの方をテスト機にコピーする。appxuploadの付いたファイルの方はテストには不要。

残り


残りは別々にやることもできるが、コピーしたフォルダー内に"Add-AppDevPackage.ps1"というPowerShellスクリプトが入っているので、これを右クリックして「PowerShellで実行」すると、このスクリプトがPowerShellを開いて自動的に判断して必要なことをY/N形式で聞いてくるので、それに従っていれば済む。

なお、開発者用ライセンス(というか、実質的に開発"機"用ライセンスだが)は1箇月で切れるが、その辺もスクリプトが判断してくれる。

参考というか、ほとんどそのまま。
以上、終わり。

2014/12/14

WinRTのScrollViewerで動かせる地図を作る

これはXAML Advent Calendar 2014の14日目の記事です。昨日は@saka_ponさんでした。皆さんネタが濃いです。この記事は残念ながら濃くはないです。長いですけど(すいません)。

1. 前置き


さて、XAMLのコントロールも色々ありますが、個人的にはItemsControlというか、ListViewが好きです。データソースを繋げたらバインディングが張られた子コントロールがだだっと自動生成されるのがたまらない、というか。アプリを作るときは初めてこれを見るのが楽しみだったりします。

このItemsControlの子コントロールは縦横に並べて配置するのが基本ですが、データソースに含まれる情報、例えば地理的な位置情報がある場合は、UI的にそれを生かした地理的な配置にすることも考えられます。それで地理的な配置にした場合、多様なサイズの画面で使えるようにするにはズームが必要になりますし、ズームすれば今度はムーブも必要になり、さらに画面の回転も考慮する必要が出てきます。

そういったものをWindowsストアアプリのWinRTで一式作ってみようというのが、この記事の目的です。

流れとしては以下のようになります。
  1. 地理的な配置にする
  2. ビヘイビアを用意する
  3. ズームできるようにする
  4. ムーブできるようにする
  5. 回転やサイズ変更や終了後の復元ができるようにする
最終的なものは以下のような感じです。記事とは関係ないですが、日本全国の気温(OpenWeatherMapによる)と各電力会社の電力使用状況をデータとして利用しています。

ソースコードはGitHubに置きました。

2.1. 地理的な配置にする


子コントロールを自由に配置するには、定番ですがListViewのItemsPanelにCanvasを使います。まず子コントロール用のViewModelとして以下のChildViewModelがあるとします。
このLeft、Top、ZIndexをそれぞれCanvasの添付プロパティにバインドしてやればいいわけです(ZIndexはもし必要があれば)。

このChildViewModelを要素とするCoreという名のObservableCollectionがあるとして、これをItemsSourceにしたListViewのXAMLがどうなるかというと、WPFの場合は以下のようになります。
一応これのポイントはItemContainerStyleのプロパティとしてCanvasの添付プロパティを設定し、これらにChildViewModelからバインドしていることです。ItemContainerStyleが適用されるListViewItemがItemTemplateが適用されるコントロールの親になるので、こちらに設定しないとCanvasに届かないわけですね。

で、この方法はWinRTでは通用しません。なぜならItemContainerStyleにバインディングが通らないから。

ではどうするかというと、いきなりコードビハインドですがItemsControl.PrepareContainerForItemOverrideメソッドを使ってバインディングを張ってやります。これを簡単にやるにはListViewの派生クラスを作ればいいので、CanvasListViewというクラスを作ります。
これを使ったWinRTのListViewのXAMLは以下のようになります。
まあ不要なItemContainerStyleのプロパティを削っただけですが。ItemContainerStyle自体は残しているのは標準のStyleを無効にするためです。

2.2. ビヘイビアを用意する


ズーム/ムーブできるようにするには、これも定番ですがListViewをScrollViewerで囲みます。その準備として、ScrollViewerに関係する処理をBehaviors SDK (XAML)のビヘイビア(要はBlendのビヘイビア)にまとめることにします。これは参照設定 -> 参照の追加 -> Windows 8.1 -> 拡張から追加できます。

作成したビヘイビアの基本部分は以下のようなものです。
このビヘイビアには標準でジェネリック版がないので、Attachしたときに対象のDependencyObjectがScrollViewerか確認した上でAssociatedObjectプロパティに格納し、とりあえずScrollViewerにキャストしたものをAssociatedViewerプロパティから参照できるようにしました。

その下のAssociatedSelectorプロパティの型はSelectorですが、これはListViewやGridViewの基底クラスに当たるものです。なるべく汎用性を持たせようとしたのですが、この記事では関係ないのでListViewのことだと思ってください。

この中のGetFirstDescendantOfType<T>メソッドはWinRT XAML ToolkitのVisualTreeHelperExtensions.GetFirstDescendantOfType<T>拡張メソッドです。これは子孫の中から指定された型の最初のものを取得するので、このプロパティからScrollViewer内にあるListViewを参照できます。

その下のCompositeDisposableは、イベント処理をReactive Extensionsでやるので、その後始末のためです。

また、この記事では直接触れませんが、方針として子コントロールをCanvas内に配置する座標を計算する際、ScrollViewerのサイズを基準にします。これに合わせるため起動時にScrollViewerとListViewのCanvasのサイズを揃えることとし(1倍の状態)、ScrollViewerからの座標の入力があればCanvas内の座標に変換し、さらに1倍のときの座標に変換したものを基準にします。でないと座標を間違えずに取り扱える気がしないので。

それでは、初めに処理の土台となるヘルパーメソッドを作ります。

計算を簡単にするため起動時にScrollViewerとListViewのCanvasのサイズを揃える前提で、ズームしたときの変化とScrollViewerのプロパティの関係を模式化したのが以下です。
ズームインすることによってCanvasの仮想的なサイズが拡大し、Canvasの位置(左上角)とScrollViewerの位置(左上角)がずれます。これからScrollViewer内の任意の座標をCanvas内の座標に変換するには、CanvasのScrollViewerに対する相対座標の値を加えればいいことが分かります。

さらにCanvas内の座標を1倍のときの座標に変換するにはScrollViewerの倍率で割ればいいので、ScrollViewer内の座標をCanvasの1倍のときの座標に変換するメソッドは以下のようになります。引数のinViewerPositionがScrollViewer内の座標、viewerZoomFactorがScrollViewerの倍率です。
selectorPositionはCanvasのScrollViewerに対する相対座標で(負の値になる)、それとScrollViewer内の座標を合計した上で倍率で割っています。

逆に、ScrollViewer内の座標とCanvas内の1倍のときの座標がScrollViewerのある倍率のときに一致するようScrollViewerを操作するメソッドは以下のようになります。引数のinSelectorPositionがCanvas内の座標です。
ScrollViewer.ChangeViewメソッドは水平と垂直の両スクロール量(オフセット)、倍率を一元的に操作するメソッドです。Windows 8.1で入ったメソッドで、従来のバラバラだったメソッドを置き換えるものです。

座標の計算はConvertToInSelectorPositionメソッドの逆になっているのが分かると思います。という意味で対になるメソッドです。

[追記]

ConvertToInSelectorPositionメソッドについて、CanvasとScrollViewerの相対座標の値はScrollViewerのHorizontalOffsetとVerticalOffsetに一致するから、わざわざメソッドを使って取得する必要はないのではと思った方もいるかもしれません。メソッドにするとこんな感じです。
そうしなかった理由は自分でも忘れてましたが、この方法は倍率が1より小さくなる、すなわちCanvasがScrollViewerより小さくなったときは無力になるからです。HorizontalOffsetとVerticalOffsetは0より小さくならないので。

と言いつつ、倍率の最小値を1に制限してしまえば問題にはならないので、これでも行けるんですけどね。

2.3. ズームできるようにする


ようやくズームですが、WinRTのScrollViewerの場合、ZoomModeをEnabledにするだけでタッチのピンチ/ムーブが可能になります。したがって、以下はマウスでこれをやるためのものです(タッチも受け付けますが)。

なお、ズームといっても中心点を固定して拡大/縮小する形式と、ズームイン/ズームアウトのモードを決めて任意の点をクリックするとその点を中心に拡大/縮小する形式がありますが、UI的に後者の方が優れていると思うので、そちらで行きます。

ということで、ズームイン/ズームアウトのモードを示すenumを用意しました。
このモード設定は他に任せ、このビヘイビアにはこの型のZoomDirection依存関係プロパティを置き、PageのViewModelとバインドして現在の設定を受け取れるようにします。

その上でScrollViewerのTappedイベントをRxで購読するメソッドを作り、ビヘイビアのOnLoadedメソッドで実行します。
Tappedイベントが来たときに処理するメソッドは以下のとおりです。
まずScrollViewer内の座標とCanvas内の1倍のときの座標を取得し、ZoomDirectionに従って振り分けます。zoomNotchFactorは1回の操作で変える倍率の刻みです。倍率は最小1倍、最大9倍でリミットをかけています。

面倒な計算はヘルパーメソッドに任せているので、こんなものです。

2.4. ムーブできるようにする


ムーブもイベントをRxで購読すればいいわけですが、これには二通りあります。
  • Manipulationイベント: ManinulationStartedで開始、ManipulationDeltaで移動、ManipulationCompletedで終了。
  • Pointerイベント: PointerPressedで開始、PointerMovedで移動、PointerReleasedなどで終了。
どちらでも行けますが、まずManipulationの方から。イベントを購読するメソッドは以下のようなものです。
移動量を捉えるだけならManipulationDeltaだけでいいですが、開始位置を保存しておくのと入力がマウスかどうか判別するにはManipulationStartedRoutedEventArgsを見る必要があるので、ManipulationStartedが必要になります。一方、移動が終わればManipulationDeltaも止まるので、ManipulationCompletedには実質的な意味はありません。

これらを受けた処理をするメソッドです。
開始時にその時点の状態を保存しておき、移動時に(スロットリングで間隔を挟みつつ)開始時からの累積量を加え、ScrollViewer.ChangeViewメソッドで反映させています。一応倍率(Scale)の変化も加えるようにしていますが、無駄かもしれません。

次にPointerの方を。こちらは少し注意が必要です。

第一に、PointerMovedイベントはカーソルが上を動いているときは常に発生しているので、区切るために開始時と終了時のイベントが必須ですが、終了時のイベントがItemsControlを囲んだScrollViewerでは返ってこないので(自分で試した結果)、ListViewの方でイベントを購読する必要があります。

第二に、終了時のイベントが何になるのか明示されていません。MSDNには「PointerReleasedの代わりの他のイベントが、アクション — たとえば、PointerCanceledまたはPointerCaptureLostの最後に発生する場合があります。常にペアで発生するPointerPressedおよびPointerReleasedイベントに依存しないでください。」とあり、PointerReleasedだけではダメなのは分かりますが、PointerCanceledとPointerCaptureLostも例示に過ぎないので、完全に止められる保証がありません。

ともあれ、イベントを購読するメソッドは以下のようなものです。
終了時はPointerReleasedとPointerCanceledとPointerCaptureLostをまとめて購読する形です。保険のためにPointerExitedを追加してもいいかもしれません。

これらを受けたメソッドです。
Manipulationの場合とやっていることは大体同じです。これで別に問題は起きなかったのですが、どちらかを選ぶとすればManipulationの方が面倒がないかなという気がします。

2.5. 回転やサイズ変更や終了後の復元ができるようにする


もう少し続きます。

回転への対応はストアアプリならではの要求ですが、このScrollViewerの場合は、今まで見ていたものが回転しても明後日の方に飛んでいかない、言い換えれば中心にあるものは回転しても中心のままにする、と捉えることができます。

これはサイズ変更への対応にも流用できて、アプリの横幅が変更されても中心にあるものは中心に維持される、となります。さらに終了後の復元への対応にも応用できて、終了時に中心にあったもの(と倍率)がそのまま復元される、となります。

したがって、これらは共通の処理で実現でき、違うのはタイミングだけです。このためにはScrollViewerの中心座標に対応するCanvasの1倍のときの座標とScrollViewerの倍率を記録しておいて、それぞれ適当なタイミングで復元すればいいわけです。

まずこの座標を記録するものとしてPoint型のInSelectorCenterPosition依存関係プロパティ、倍率を記録するものとしてFloat型のViewerZoomFactor依存関係プロパティを置きます。これらはPageのViewModelとバインドして変更がある度にLocalSettingsかRoamingSettingsに保存するようにすれば、終了後の復元にも使えます。

これらを記録するメソッドは以下のとおりです。
復元するメソッドの方は以下のとおりです。
この記録するタイミングにはScrollViewerのViewChangedイベントが利用できます。これはタッチによる変更も捉えることができます。また、回転とサイズ変更のときに復元するタイミングにはSizeChangedイベントが利用できます(回転で縦横が入れ替わる、すなわちサイズが変わるので)。

これらをRxで購読するにはOnLoadedイベントで以下のようにします。
終了後の復元のときは、起動中の適当なタイミングで復元を実行すればいいわけですが、注意点としてListViewの子コントロールがロードされた後でなければ正しく復元されないので工夫が必要です。

最終的なビヘイビアはレポジトリの方で。記事中で取り上げてない部分もありますが。

ScrollViewerのXAMLは以下のようになりました。
表示させるとこんな感じです。
デザインは作り込んでいませんが、フライアウトを付けたりしています。

以上で終わりです。お疲れ様でした。

3. 後書き


実はこの方法はTokyoSubwayViewで使ったものとほぼ同じだったりします。記事の材料にするために見直したりしてきましたが、まだ生煮えの感を免れません。強引にまとめようとすれば出来なくもないかもしれませんが、それがXAML的に正しいかという判断が付かない程度のXAML力でした。

ということで、もうXAML Advent Calendarも後半ですが、引き続き各位の記事で勉強させていただこうと思います。明日は@icchuさんです。期待してます。

2014/11/30

TokyoSubwayView

TokyoSubwayViewというWindowsストアアプリをリリースしています。「東京メトロオープンデータ活用コンテスト用公共交通データAPI」(長い……)を利用するアプリで、東京メトロオープンデータ活用コンテストに応募しています。

1. 機能


ごくシンプルに、東京メトロ地下鉄路線内の列車の運行状況をグラフィカルに表示します。

各駅の位置関係は実際の地理的な位置に沿っているため、都心部では密集して見にくくなっていますが、表示する路線と優先度(優先度の高いものが上に来る)をオプションで設定できます。

駅と線路の色は列車の運行状況を示します(各線のラインカラーとは別)。
  • 青 : 遅れなく運行中
  • オレンジ : 5分以上遅れて運行中
  • マゼンタ : 15分以上遅れて運行中
  • 黄緑: 列車予測線(線路のみに、隣接した駅にその方向に走る列車が停車中のときに出る)
駅か線路をクリック/タップするとそこにいる列車の情報が表示されます。

ここで謝っておきますと、現在Windowsストアに出ているのはVer 1.1で、一部バグがあります(初期状態で表示される路線の優先度がオプションと逆になっているなど)。Ver 1.2で大体解消したと思いますが、コンテストのルールによりコンテスト終了(来年1月以降)まで修正ができないため、そのままになっています。上の画像はVer 1.2のものです。

2. 開発


このアプリの開発は@ch3coohさんのブログを見て意外と難しくなさそうと思ったところから始まりました。
そこから何を作るか考えて、要素技術を確認して、行けそうだと思ったのが10/19のOSC Tokyo/Fallの直前。11/17の締め切りまでWindowsストアの審査に1週間を見込むとして、最大3週間あれば形になるだろうと取り掛かったものの、WinRTではWPFで使ったテクニックが使えない場所が多々あって時間を取られるうちに最後の1週間を切り、既に諦めが入りつつも見切りでまとめたのが11/15のめとべや東京#6の朝(それでもこの期間でできたのは@biacさんの多数の記事のおかげ)。懇親会で「もう間に合わないんですけどね……」と言いつつ見せたら@openlibsysさんに後押しいただいて、残る問題を一応潰して全部動くようになったのが11/17の早朝。プライバシーポリシーは@snow_caitさん情報でGistで行くことにして、開発者登録からストア審査提出までどたばたと済ませて、自分ではこれで一区切り付けて終わり、のつもりだったんですが……

なあにーーーーーーーーーーーーーーーーーーーーっ

認定まで25分、公開まで1時間11分しかかかりませんでした。ストアの説明では審査には何段階かあって合わせて1週間ほどかかる感じでしたが、現在では短縮されたようです……。

ということで奇跡的にコンテストには間に合いましたが、後から見るとちらほら怪しい点が……。ただ、応募アプリをとにかく出さないとアクセストークンが停止されるので、APIを利用すること自体できなくなるんですよね……。

ソースコードはGitHubに置きました。Ver 1.2でかなり修正しています。
ただし、アクセストークンは各開発者が管理することなっているので、入れていません。で、現在有効なアクセストークンは自分でも応募アプリを出した人のものだけなので、実際に動かす人はまずいないと思いますが、一応。

コンテスト自体については、審査に2箇月かけてその間のアプリ修正を禁止するとか、今時のアプリ開発のスピード感からして正直どうかという感もあり、とりあえず忘れます。

2014/02/14

非.NET4.5でもトースト通知(補足)

トースト通知のテストアプリではトーストの文字色(=ショートカットのタイルの文字色)を背景色に応じて自動選択する形にしましたが、このアルゴリズムが色によっては今一つだったので、変えてみました。

1. アルゴリズム


MSDNの「デスクトップアプリのスタート画面のタイルをカスタマイズする方法」では、トーストの文字色と背景色の輝度の比率を4.5以上にすべしとしています。これは以下のW3Cのアクセシビリティガイドラインに従ったものです。
このガイドラインに色の輝度(relative luminance)とコントラスト比率(contrast ratio)の計算方法が示されているので、これを利用します。

まず色の輝度について、RGBの各色を以下のように数値化します。
private static double CalcColorChannel(byte channel)
{
    var buff = (double)channel / 255d;

    if (buff <= 0.03928)
        return (buff / 12.92);
    else
        return Math.Pow((buff + 0.055) / 1.055, 2.4);
}
これを元に各色に加重を掛けて輝度を計算します。どうせなのでColorの拡張メソッドにしてみました。
public static double GetLuminance(this Color source)
{
    var r = CalcColorChannel(source.R);
    var g = CalcColorChannel(source.G);
    var b = CalcColorChannel(source.B);

    return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
この輝度の範囲は0(最も暗い場合)~1(最も明るい場合)になります。
次に2つの輝度からコントラスト比率を計算するには以下のようにします。明るい輝度の方が常に分子。
private double GetContrastRatio(double luminance1, double luminance2)
{
    if (luminance1 > luminance2)
        return (luminance1 + 0.05) / (luminance2 + 0.05);
    else
        return (luminance2 + 0.05) / (luminance1 + 0.05);
}
これらを使って、文字色のlight(白)とdark(黒)のそれぞれと背景色(background)とのコントラスト比率を計算し、より高い方を選択するようにします。戻り値は定義済みのForegroundColorType列挙型にしました。
internal ForegroundColorType GetHigherContrastColor(Color background)
{
    var luminanceLight = 1d; // = Colors.White.GetLuminance()
    var luminanceDark = 0d;  // = Colors.Black.GetLuminance()
    var luminanceBackground = background.GetLuminance();

    var ratioLight = GetContrastRatio(luminanceLight, luminanceBackground);
    var ratioDark = GetContrastRatio(luminanceDark, luminanceBackground);

    return (ratioLight >= ratioDark) ? ForegroundColorType.light : ForegroundColorType.dark;
}

2. テスト


前の方法ではとくに青系の色が苦手で、暗い色でもdark(黒)が選択されていました。

これがlight(白)が選択されるようになります。

他の色でも大体問題なさそうだったので、テストアプリを差し替えました。
実行ファイル
ソースコード

3. 輝度


参考までに、既定の16色の輝度を同じアルゴリズムで計算すると以下のようになります。
  •  black : 0 
  •  silver : 0.527 
  •  gray : 0.216 
  •  white : 1 
  •  maroon : 0.046 
  •  red : 0.213 
  •  purple : 0.061 
  •  fuchsia : 0.285 
  •  green : 0.154 
  •  lime : 0.715 
  •  olive : 0.200 
  •  yellow : 0.928 
  •  navy : 0.016 
  •  blue : 0.072 
  •  teal : 0.167 
  •  aqua : 0.787 
感覚的に合うような合わないような……色は難しいです。

2014/02/07

非.NET4.5でもトースト通知

Windows 8以降のトースト通知を.NET Framework 4.5以降でないアプリから使う方法について。特別新しい話はないです。

1. アウトライン


OSやアプリからユーザーに何か通知するとき、従来はよくバルーンメッセージが使われていましたが、Windows 8以降はトースト通知が使われるようになりました。このトースト通知はデスクトップアプリからでも出せますが、Windowsストアアプリ用のAPIであるWindows Runtimeを使うので、必然的に.NET Framework 4.5以降のアプリである必要があります。

ここで.NET Framework 4.5以降でアプリを作ってしまえば話は終わりですが、そうも行かないときにどうしようかな、ということで方法を考えたので、それを説明してみます。

先に、作成したテストアプリを示しておきます。このアプリ自体は.NET Framework 4.0で作成しています。

この"Headline"と"Body"の部分に入力して、"Background color"を適当に調整して、"Show a toast"を押すと、以下のようにトースト通知が出ます。

ここでトーストをクリックすると、もしこのアプリが最小化されていたり他のウィンドウの下にあっても最前面に表示し直されます。

これは割とチープな仕掛けで、
  1. トースト通知を指示するアプリ(この表示されているアプリ。.NET 4.0のWPFアプリ)
  2. トースト通知を実際に出すアプリ(.NET 4.5のコンソールアプリ)
を用意しておき、このWPFアプリ(親アプリ)からコンソールアプリ(子アプリ)を外部実行し、標準入力でトースト通知の内容を送り、それに従ってコンソールアプリはトースト通知を出し、トーストがクリックされたという反応が返ってくると、それを標準出力でWPFアプリに返し、それを受けてWPFアプリが自らを最前面に出す、という流れになっています。なぜライブラリでやらないかというと、.NETのバージョンが新しいものを古いものの方から参照して使えない(逆は可能)という問題があるからです。

こういうトースト用アプリを介するという発想は別に珍しくなくて、Lenovoのユーティティでも使っています。

テストアプリは共にC#でVisual Studio 2013により作成しています。
実行ファイル
ソースコード

2. 具体的方法


トースト通知についてはMSDNにしっかりした説明があるので、ポイントだけ触れます。

2.1 ガイドライン/h4>
初めにガイドラインを確認しておくと、出たばかりのWindows 8.1 UXガイドラインの日本語訳からトースト通知のガイドライン(内容的にはトースト通知のガイドライン (Windowsストアアプリ)と同じ)を見ると、
ユーザーがトーストをタップしたときに、アプリの適切な移動先に移動します。通知は厳密な情報更新ではなく、コンテキストの切り替えをユーザーに促すものと考えてください。

とあって、トーストは出しっ放しではなく、クリックされたとき(Activatedイベント)適切な情報に誘導すべきということが分かります。また、
バルーン通知のシナリオをトーストに自動的に移行しないでください。ユーザーが全画面表示のエクスペリエンス (デスクトップスタイルアプリのみ) に没入していないときには、バルーン通知の方が適している場合もあることを考慮します。

ともあってギクッとします。デスクトップにしか関係ないものはわざわざトーストにしなくていい場合があると。

その他さほど細々した指示はなくて、大意をまとめれば「トーストを濫発されると全体の迷惑だから、つまらない通知に使うな、なるべく絞れ」というプラットフォーム側としては当然の要請だと思います。

2.2 トーストテンプレート


トースト通知の出し方を簡単にいえば、専用書式のXMLに設定を乗せてAPIに渡す、それだけです。そのXMLのベースとなるテンプレートは8種類用意されていて、新規作成はできません。というより、このXMLからトーストを表示するためのCSSというかスタイル設定がこれら既定のテンプレートに対応したものだけで、自由に追加できないといった方が正しいかも。

このテンプレートはMSDNのトーストテンプレートカタログにあるとおりですが、実際のところ、たいして選択肢はありません。
  • 画像を入れるか否か。
  • 3行の文字スペースを件名(Headline)と本文(Body)にどう割り当てるか。
だけです(スタートメニューに置いたショートカットのアイコンは常に入る)。なお、色はショートカットのタイルのものが利用されるので、トーストの設定にはありません。

具体的には、
  1. ToastText01(文字のみ、本文のみ)
  2. ToastText02(文字のみ、件名1行、本文2行)
  3. ToastText03(文字のみ、件名2行、本文1行)
  4. ToastText04(文字のみ、件名1行、本文1行×2)
  5. ToastImageAndText01(画像入り、本文のみ)
  6. ToastImageAndText02(画像入り、件名1行、本文2行)
  7. ToastImageAndText03(画像入り、件名2行、本文1行)
  8. ToastImageAndText04(画像入り、件名1行、本文1行×2)
の通りですが、汎用性が高いのはToastText02だと思うので、以後これで進めます(テストアプリもこれを使用)。

まずToastNotificationManagerからテンプレートを取得します。型はWindows RuntimeのXMLであるWindows.Data.Xml.Dom.XmlDocumentです。
var document = ToastNotificationManager.GetTemplateContent(ToastTemplateType.ToastText02);
このXMLの内容はカタログにあるとおり以下のようなものです(改行を入れて整形したもの)。
<toast>
    <visual>
        <binding template="ToastText02">
            <text id="1"></text>
            <text id="2"></text>
        </binding>
    </visual>
</toast>
後はこれをこねこねすればいいわけです。書式はMSDNのToast schemaに説明があります。といってもデスクトップアプリから出すトーストの場合、使えるのは一部だけで、toastaudioぐらいです。

以下では最初に件名(headline)と本文(body)を入れた後、トーストの表示時間を長くし、それに合わせてオーディオはループするよう設定しています。
// Fill in text elements.
var textElements = document.GetElementsByTagName("text");
if (textElements.Length == 2)
{
    textElements[0].AppendChild(document.CreateTextNode(headline));
    textElements[1].AppendChild(document.CreateTextNode(body));
}

// Set duration attribute.
document.DocumentElement.SetAttribute("duration", "long");

// Add audio element.
var audioElement = document.CreateElement("audio");
audioElement.SetAttribute("src", "ms-winsoundevent:Notification.Looping.Alarm");
audioElement.SetAttribute("loop", "true");
document.DocumentElement.AppendChild(audioElement);
これでXMLは例えば以下のようになります(整形したもの。先にテストアプリで出したトーストと同じ内容)。この内容が毎回同じなら、これをテキストリソースとして持っておいてXMLを生成した方が早いですね。
<toast duration="long">
    <visual>
        <binding template="ToastText02">
            <text id="1">トースト通知のテスト</text>
            <text id="2">トーストの本気を見るのです!</text>
       </binding>
    </visual>
    <audio src="ms-winsoundevent:Notification.Looping.Alarm" loop="true"/>
</toast>
このXMLでToastNotificationオブジェクトを生成し、AppUserModelIDと合わせてToastNotificationManagerに託せば終わりです。
// Create a toast.
var toast = new ToastNotification(document);

// Show a toast.
ToastNotificationManager.CreateToastNotifier(AppId).Show(toast);

2.3 トーストイベント


トーストを出した結果はイベントで取得できます。というより、トーストがクリックされたかどうか知る必要があるので、トーストを出す前にイベントハンドラーを登録しておきます。イベントは3種類で、うち1つは理由が3つに分かれるので、計5種類の結果があります。
  • ToastNotification.Activated(トーストを出すのに成功し、ユーザーがクリックした)
  • ToastNotification.Dismissed
    • ToastDismissalReason.ApplicationHidden(アプリでトーストが抑止されていた)
    • ToastDismissalReason.UserCanceled(トーストを出すのに成功したが、ユーザーがクローズボタンで閉じた=積極的に無視した)
    • ToastDismissalReason.TimedOut(トーストを出すのに成功したが、クリックされないまま時間切れになった=ユーザーが気づかなかったか、消極的に無視した)
  • ToastNotification.Failed(トーストを出すのに失敗した)
このうちアプリがアクションを起こす必要があるのはActivatedイベントの場合だけなので、それだけで十分かもしれません。ちなみに、このイベントはサブスレッドで起きるので、UIを操作するにはUIスレッドに戻す必要がありますが、コンソールアプリでは関係ないです。

なお、コンソールアプリでイベントを受け取るにはイベントが返ってくるまで(表示時間を長くした場合、時間切れまで25秒)アプリが終了しないようにする必要があるので、テストアプリではトーストを出した後、System.Threading.Thread.Sleepメソッドを500ミリ秒ずつ60回ループさせて計30秒ほどUIスレッドをブロックするようにし、イベントが返ってきたらループを出るようにしています。

2.4 アプリのショートカット


上でも出てきましたが、トースト通知を出すにはそのアプリのAppUserModelIDの入ったショートカットがスタートメニューにある必要があります。このAppUserModelID入りのショートカットを作る機能は.NET Frameworkでは提供されていません。IShellLinkにもありません。Windows Script HostのWshShortcutにもありません。

となるとWin32APIとCOMでやるしかないわけですが、Windows API Code Packのライブラリにこれが使えるものあります。どうせならとこれとIShellLinkの機能をまとめたラッパークラスを作るというのを以前やりました。

このShellLinkクラスの機能を実用レベルに上げたものをテストアプリでは使っています。したがって一般的な説明にはならないのですが、こんな感じでショートカットを作成しています。
using (var shortcut = new ShellLink()
{
    TargetPath = Assembly.GetExecutingAssembly().Location;
    Arguments = StartmenuArgument,
    AppUserModelID = AppId,
    IconPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), ExecutableFile),
    IconIndex = 0,
    WindowStyle = ShellLink.SW.SW_SHOWMINNOACTIVE,
})
{
    shortcut.Save(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs", ShortcutFile));
}
各プロパティの設定内容は、
  • TargetPathはショートカットが示す実行ファイルのパスで、このコンソールアプリ(子アプリ)自身のパスを入れています。
  • Argumentsはショートカットの引数で、ショートカットから起動されたことを判別するための定数を入れています(意図は後述)。
  • AppUserModelIDはその名のとおり。
  • IconPathはショートカットのアイコンに使うアイコンで、ここにはWPFアプリ(親アプリ)のアイコンを利用するため、同じフォルダーにあるという前提で、そのパスを入れています(ExecutableFileは親アプリのファイル名)。
  • IconIndexはIconPathで指定されたファイルに含まれるアイコンのうちどれを使うかという指定ですが、1つだけなので0です。
  • WindowStyleはショートカットから起動されたときにウィンドウの状態をどうするかという指定ですが、最小化を指定しています(意図は後述)。
このShellLinkオブジェクトをSaveメソッドでスタートメニューに保存しています(ShortcutFileはショートカットのファイル名)。なお、ショートカットを置く場所の細かな違いはトーストには関係ないっぽいです。

ちなみに、AppUserModelIDには以下の命名ルールがあります。

CompanyName.ProductName.SubProduct.VersionInformation

が、実際はあまり真面目に付けられてなかったりします。例えばLenovoのユーティリティのショートカットをShellLinkクラスで見ると、いかにもサンプルのままでしたし、他のソフトもMicrosoft自身のものを含めて結構自由です。Visual Studio 2013はこんな感じ。

あまり気にしない方がいいようです。

2.5 アプリのショートカットのタイル


前述のとおりトーストの色(背景色と文字色)にはショートカットのタイルのものが利用されます。これを.VisualElementsManifest.xml拡張子を付与したファイルでカスタマイズする方法をぐらばく先生が説明されています。
このファイル(以下、マニフェストファイル)は予め作成しておいてもいいのですが、テストアプリでは色を変えたかったので実行時に作成するようにしています。で、親アプリから指示するのは背景色だけにして、文字色は子アプリの方で背景色に応じて自動選択するようにしています。

まず文字色(foregroundColor)を列挙型で定義。
private enum ForegroundColorType
{
    light,
    dark,
}

private ForegroundColorType foregroundColor;
これを背景色(backgroundColor)に応じて切り替えます。
foregroundColor = ((Color)ColorConverter.ConvertFromString(backgroundColor)).GetBrightness() < 0.5
    ? ForegroundColorType.light : ForegroundColorType.dark;
この背景色はHTMLカラーの文字列で、これをSystem.Windows.Media.Colorに変換した後、明るさをSystem.Drawing.Color.GetBrightnessメソッドに倣って作成したGetBrightness拡張メソッドで取得し、暗ければ文字色をlightに、明るければdarkにします。

ただ、実際に試してみるととくに青系の暗い色のときの結果が今一つで、アルゴリズムは改善の余地がある感じです。たぶん以下が参考になると思います。
文字色を決めた後、マニフェストファイルのXMLを組み立てて保存します。型はSystem.Xml.XmlDocumentを使っていますが、深い意味はありません。一応整形するようにしていますが、動作には関係なかったりします。
var document = new XmlDocument();

// Add Application element and set its attribute.
var applicationElement = (XmlElement)document.AppendChild(document.CreateElement("Application"));
applicationElement.SetAttribute("xmlns:xsi", @"http://www.w3.org/2001/XMLSchema-instance");

// Add VisualElements element and set its attributes.
var visualElementsElement = (XmlElement)applicationElement.AppendChild(document.CreateElement("VisualElements"));
visualElementsElement.SetAttribute("BackgroundColor", backgroundColor);
visualElementsElement.SetAttribute("ShowNameOnSquare150x150Logo", "on"); // on or off
visualElementsElement.SetAttribute("ForegroundText", foregroundColor.ToString());

// Create a manifest file (overwrite).
var targetPath = Assembly.GetExecutingAssembly().Location;
var manifestPath = Path.Combine(Path.GetDirectoryName(targetPath),
     String.Format("{0}.VisualElementsManifest.xml", Path.GetFileNameWithoutExtension(targetPath)));

using (var sw = new StreamWriter(manifestPath, false, Encoding.UTF8))
{
    var settings = new XmlWriterSettings()
    {
        OmitXmlDeclaration = true,
        Indent = true,
        NewLineOnAttributes = true,
    };

    using (var xw = XmlWriter.Create(sw, settings))
    {
        document.Save(xw);
        xw.Flush();
    }
}
また、色を変えるにはマニフェストファイルを書き換える必要がありますが、既に同じ内容のマニフェストファイルがあるか否かの判別は以下のようにしています。型はSystem.Xml.Linq.XDocumentで、またXMLの型が違いますが、こういうチェックはLINQ to XMLが楽なので。
XDocument document;

// Check and read a manifest file.
var targetPath = Assembly.GetExecutingAssembly().Location;
var manifestPath = Path.Combine(Path.GetDirectoryName(targetPath),
     String.Format("{0}.VisualElementsManifest.xml", Path.GetFileNameWithoutExtension(targetPath)));

if (!File.Exists(manifestPath))
    return false;

using (var sr = new StreamReader(manifestPath, Encoding.UTF8))
{
    document = XDocument.Parse(sr.ReadToEnd());
}

// Check Application element.
var applicatonElement = document.Elements()
    .FirstOrDefault(x => x.Name == "Application");
if (applicatonElement == null)
    return false;

// Check VisualElements elements and its attributes.
var visualElementsElement = applicatonElement.Elements()
    .FirstOrDefault(x => x.Name == "VisualElements");
if (visualElementsElement == null)
    return false;

if (!visualElementsElement.Attributes()
    .Any(x => (x.Name == "BackgroundColor") && (x.Value == backgroundColor)))
    return false;

if (!visualElementsElement.Attributes()
    .Any(x => (x.Name == "ForegroundText") && (x.Value == foregroundColor.ToString())))
    return false;

return true;
これでトーストの度に色を変えることもできるわけですが、色をトーストの要素として活用することの是非についてトースト通知のガイドラインには何も書いてないんですよね。まあ想定外なんでしょうけど。

2.6 待ち時間


アプリのインストール時や初回起動時などにショートカットを作成する場合は問題になりませんが、トースト通知を出す直前にショートカットを作成する場合、一定の時間を置かないとトーストに失敗するようです(Failedイベントが返ってくる)。この時間は環境依存かもしれませんが、自分の試した範囲では3秒が必要でした。

また、マニフェストファイルを書き換えた場合、それをショートカットのタイルに反映するにはショートカットを上書きする必要がありますが(ショートカットの設定内容はそのままでも)、この場合も新しいタイルの色がトーストにも反映されるには同じ待ち時間が必要なようです。

テストアプリでは既定を3秒として、調整できるようにしています。

2.7 トーストの設定内容と結果の伝達


親アプリからトーストの設定内容を子アプリに伝えるには、通常のコマンドのように引数オプションを使う方法でもいいですが、やや面倒なので共通の伝達用クラスを作って、これで送るようにし、結果も同じクラスで返すようにしています。

伝達用クラスはこんな感じで、各プロパティを設定後、送り側はDataContractSerializerでシリアライズして標準出力で送り、受け側は標準入力で入ってきたものをデシリアライズして使う、という流れです。
using System;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;

[DataContract()]
public class ToastPacket
{
    #region Property

    /// <summary>
    /// A toast's headline
    /// </summary>
    [DataMember()]
    public string Headline { get; set; }

    /// <summary>
    /// A toast's body
    /// </summary>
    [DataMember()]
    public string Body { get; set; }

    /// <summary>
    /// A toast's background color in HTML color string
    /// </summary>
    [DataMember()]
    public string BackgroundColor { get; set; }

    /// <summary>
    /// Whether a toast's duration is long
    /// </summary>
    [DataMember()]
    public bool IsLong { get; set; }

    /// <summary>
    /// Waiting time (sec) before showing a toast
    /// </summary>
    /// <remarks>This waiting time is for the case that the shortcut to child application is 
    /// newly installed or changed so that a waiting time before showing a toast is required. 
    /// The length of this waiting time seems to be a few sec in general.</remarks>
    [DataMember()]
    public int WaitTime { get; set; }

    /// <summary>
    /// Result of a toast
    /// </summary>
    [DataMember()]
    public ToastResult Result { get; set; }

    /// <summary>
    /// Note when showing a toast
    /// </summary>
    [DataMember()]
    public string Note { get; set; }

    #endregion

    #region Method

    /// <summary>
    /// Serialize this instance.
    /// </summary>
    /// <returns>Outcome string</returns>
    public string Serialize()
    {
        var serializer = new DataContractSerializer(typeof(ToastPacket));

        using (var ms = new MemoryStream())
        {
            serializer.WriteObject(ms, this);

            return Encoding.UTF8.GetString(ms.ToArray());
        }
    }

    /// <summary>
    /// Deserialize and copy to this instance.
    /// </summary>
    /// <param name="source">Source string</param>
    public void Deserialize(string source)
    {
        if (string.IsNullOrEmpty(source))
            throw new ArgumentNullException("source");

        var serializer = new DataContractSerializer(typeof(ToastPacket));

        using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(source)))
        {
            this.CopyFrom((ToastPacket)serializer.ReadObject(ms));
        }
    }

    /// <summary>
    /// Copy property values from other instance.
    /// </summary>
    /// <param name="other">Other instance</param>
    public void CopyFrom(ToastPacket other)
    {
        if (other == null)
            throw new ArgumentNullException("other");

        var properties = typeof(ToastPacket).GetProperties(BindingFlags.Public | 
                                                           BindingFlags.Instance | 
                                                           BindingFlags.Static | 
                                                           BindingFlags.DeclaredOnly);

        foreach (var p in properties)
        {
            p.SetValue(this, p.GetValue(other, null), null);
        }
    }

    #endregion
}
結果を示す列挙型はイベントに合わせたものを定義しています。
public enum ToastResult
{
    /// <summary>
    /// The user activated the toast.
    /// </summary>
    Activated,

    /// <summary>
    /// The application hid the toast using ToastNotifier.hide method.
    /// </summary>
    ApplicationHidden,

    /// <summary>
    /// The user dismissed the toast.
    /// </summary>
    UserCanceled,

    /// <summary>
    /// The toast has timed out.
    /// </summary>
    TimedOut,

    /// <summary>
    /// The toast encountered an error.
    /// </summary>
    Failed,
}
この他、ユーザーが子アプリを直接、引数オプションを付けて起動できるようにもしています。ただし、伝達用クラスは親アプリにあるものを子アプリが参照して使うようになっているので、親アプリの実行ファイルがないと子アプリ単独では動かなかったりしますが、使わなければ削ればいい話です。

2.8 親アプリの起動


トースト通知のためにスタートメニューにショートカットを置くわけですが、そういういわばアリバイのような話とは無関係に、ユーザーとしてはスタートメニューにある以上、そこから意味のあるものが起動することを期待するわけで、クリックしても適切な反応を返さないショートカットはよろしくない気がします。

というか、Windows 8以降だとスタートメニューにつまらないショートカットを作られるのは全くもって迷惑なので、存在するならせめて意味のある方がいい、さりとてトースト用アプリが起動しても嬉しくも何ともないので、テストアプリでは本体の親アプリを起動するようにしています。

ショートカットの作成時に設定したArgumentsの定数とWindowStyleの最小化はこのためのもので、Argumentsによって親アプリから起動されたものか、ユーザーから直接コマンドで起動されたものか、ショートカットから起動されたものかを判別し、ショートカットの場合はサイレントに親アプリを起動して子アプリは終了します。

ただ、そうするなら初めからショートカットのTargetPathを親アプリにしてしまうという手も考えられますが、どちらが正しいのかはよく分かりません。

3. まとめ


以上のように、.NET 4.5以降のアプリでなくてもトースト通知を使うことはできます。と言いつつ、自分が元々これを考えたのは.NET 4.0のアプリを開発していて、この対応環境からXPを外さないためには.NET 4.5に上げられなかったという理由があるのですが、気づいてみればXPのサポート期限切れまで後わずかですね。

ううむ、微妙……。

4. 最後に残った疑問


トースト通知の「トースト」の語源が分からなくて、画像検索したらこんがり焼けたトーストばかりだったので洒落のつもりでトーストのアイコンにしてみたのですが、もしかしてこれが語源……? トースターから焼き上がったトーストがポンっと出てくるのに引っ掛けた? いやいやいや……まさか、ね。

2012/11/14

AppUserModelIDをC#から操作する

Windows 7以降で利用されるAppUserModelIDを含んだショートカットをC#から操作するコードを書いてみた。

1. 背景


Windows 8のWindowsストアアプリでは常駐型アプリは難しいので、常駐型の監視アプリを使おうとする場合、(x86版であれば)デスクトップアプリを動かしておいて何かあればWindowsストアアプリの画面にトースト通知を出すのが一つの解になると思うが、このトースト通知をデスクトップアプリから送るにはAppUserModelIDを含んだショートカットがスタートメニューにあることが条件になっている。

このAppUserModelIDは普通に張ったショートカットには含まれないもので、これを含むショートカットの作成はアプリのインストーラにやらせることをMicrosoftは推奨している。が、そうは言ってもインストーラの作ったショートカットをユーザーが消してしまう可能性もあるので確実性に欠けるし、常にインストーラを必要とするのはあまり便利ではない。

一方で、AppUserModelIDを含んだショートカットを読み書きする方法は.NET Frameworkでは提供されておらず、ショートカットでよく使われるWSHのWshShortcutオブジェクトでもAppUserModelIDは対象に入っていない。一応C++での方法は示されているので、であればと、これに沿ってC#からWin32とCOMを使って操作するコード(ラッパークラス)を書いた次第。

2. コーディング


C#あるいはVBからCOMのIShellLinkインターフェイスを使ってショートカットを読み書きする例は既に幾つかあって、これをベースにさせていただいた。
また、C++でAppUserModelIDを読み書きする例は以下のとおり。
方針としては、IShellLinkを使う例を基本的に踏襲しつつ(IPersistFileインターフェイスはSystem.Runtime.InteropServices.ComTypes.IPersistFileの方を使う)、IShellLinkはAppUserModelIDに対応してないので、さらに下のIPropertyStoreインターフェイスを使って直にプロパティをいじるというもの。

ただ、プロパティの値を格納するPROPVARIANT構造体が複雑で、これに難儀していたところ、そもそものデスクトップアプリからトースト通知を送るサンプルに(正確には、その利用するWindows API Code Packに)答えがあった。
これにあるPROPVARIANTを扱うクラスは長いものだが、出し入れする値の内容を文字列に限定してしまえば(AppUserModelIDは文字列)、さほどでもない。

IPropertyStoreに必要なコードを整理すると、まずIPropertyStore自体は以下のとおり。
// IPropertyStore Interface
[ComImport,
 InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
 Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
private interface IPropertyStore
{
    uint GetCount([Out] out uint cProps);
    uint GetAt([In] uint iProp, out PropertyKey pkey);
    uint GetValue([In] ref PropertyKey key, [Out] PropVariant pv);
    uint SetValue([In] ref PropertyKey key, [In] PropVariant pv);
    uint Commit();
}
この中でプロパティのキーを格納するPropertyKey構造体。
// PropertyKey Structure
// Narrowed down from PropertyKey.cs of Windows API Code Pack 1.1 
[StructLayout(LayoutKind.Sequential, Pack = 4)]
private struct PropertyKey
{
    #region Fields

    private Guid formatId;    // Unique GUID for property
    private Int32 propertyId; // Property identifier (PID)

    #endregion

    #region Public Properties

    public Guid FormatId
    {
        get
        {
            return formatId;
        }
    }

    public Int32 PropertyId
    {
        get
        {
            return propertyId;
        }
    }

    #endregion

    #region Constructor

    public PropertyKey(Guid formatId, Int32 propertyId)
    {
        this.formatId = formatId;
        this.propertyId = propertyId;
    }

    public PropertyKey(string formatId, Int32 propertyId)
    {
        this.formatId = new Guid(formatId);
        this.propertyId = propertyId;
    }

    #endregion
}
で、プロパティの値を格納するPropVariantクラス(とそれに必要な関数)。
// PropVariant Class (only for string value)
// Narrowed down from PropVariant.cs of Windows API Code Pack 1.1
// Originally from http://blogs.msdn.com/b/adamroot/archive/2008/04/11
// /interop-with-propvariants-in-net.aspx
[StructLayout(LayoutKind.Explicit)]
private sealed class PropVariant : IDisposable
{
    #region Fields

    [FieldOffset(0)]
    ushort valueType;     // Value type 

    // [FieldOffset(2)]
    // ushort wReserved1; // Reserved field
    // [FieldOffset(4)]
    // ushort wReserved2; // Reserved field
    // [FieldOffset(6)]
    // ushort wReserved3; // Reserved field

    [FieldOffset(8)]
    IntPtr ptr;           // Value

    #endregion

    #region Public Properties

    // Value type (System.Runtime.InteropServices.VarEnum)
    public VarEnum VarType
    {
        get { return (VarEnum)valueType; }
        set { valueType = (ushort)value; }
    }

    // Whether value is empty or null
    public bool IsNullOrEmpty
    {
        get
        {
            return (valueType == (ushort)VarEnum.VT_EMPTY ||
                    valueType == (ushort)VarEnum.VT_NULL);
        }
    }

    // Value (only for string value)
    public string Value
    {
        get
        {
            return Marshal.PtrToStringUni(ptr);
        }
    }

    #endregion

    #region Constructor

    public PropVariant()
    { }

    // Construct with string value
    public PropVariant(string value)
    {
        if (value == null)
            throw new ArgumentException("Failed to set value.");

        valueType = (ushort)VarEnum.VT_LPWSTR;
        ptr = Marshal.StringToCoTaskMemUni(value);
    }

    #endregion

    #region Destructor

    ~PropVariant()
    {
        Dispose();
    }

    public void Dispose()
    {
        PropVariantClear(this);
        GC.SuppressFinalize(this);
    }

    #endregion
}

[DllImport("Ole32.dll", PreserveSig = false)]
private extern static void PropVariantClear([In, Out] PropVariant pvar);
また、AppUserModelIDのキーはFormatIDとPropIDからPropertyKeyを使って以下のように定義できる。
// Name = System.AppUserModel.ID
// ShellPKey = PKEY_AppUserModel_ID
// FormatID = 9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3
// PropID = 5
// Type = String (VT_LPWSTR)
private readonly PropertyKey AppUserModelIDKey = 
    new PropertyKey("{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}", 5);
これらを使ってAppUserModelIDをShellLinkクラスのプロパティとして加えた。なお、VerifySucceededは戻り値を見て失敗なら例外を出すメソッド。ショートカットにAppUserModelIDのプロパティが存在しない場合、Valueはnullとなる。
// AppUserModelID to be used for Windows 7 or later.
public string AppUserModelID
{
    get
    {
        using (PropVariant pv = new PropVariant())
        {
            VerifySucceeded(PropertyStore.GetValue(AppUserModelIDKey, pv));

            if (pv.Value == null)
                return "Null";
            else
                return pv.Value;
        }
    }
    set
    {
        using (PropVariant pv = new PropVariant(value))
        {
            VerifySucceeded(PropertyStore.SetValue(AppUserModelIDKey, pv));
            VerifySucceeded(PropertyStore.Commit());
        }
    }
}
このShellLinkクラスを使うアプリも合わせた全体のコードはこちらに載せた。なお、IShellLink自体の機能は本題ではないので、最低限必要なものに絞ってある。

アプリの見た目は以下のようなもので、ショートカット先のターゲットファイルのパス、実行時のオプション、AppUserModelIDの読み書きが可能。

実際に使うには作り込みの必要があると思うが、叩き台としてはこんなものかと。

2012/08/08

Metroアプリ

デモとして作っていたMetroアプリが一応形になったので。

1. Raidar Metro Demo


Windows 8 RP上のVisual Studio 2012 RCで作ったが、もうWindows 8もVisual Studio 2012もRTMが一般の開発者向けに出る直前で、RTMでまた動作に変更がある可能性があるが……。言語はC#とXAML。

機能的にはNAS Herderのモニター機能のサブセットで、ロジック部分はVisual BasicからC#に大体そのまま移植。このメインページと設定ページの構成で、この間のページ遷移は本来スワイプで出すNavigationBarかAppBarを使うべきだが、とりあえずAppBar用のSettingsのアイコンを置いている。

対象のNASを指定する方法はHost NameかIP Addressの選択式。Scanで見つかったNASを表示する部分はGridViewで、唯一Metroらしい見た目。トースト通知はStatusにWarningが出た場合か温度が閾値を上回った場合に出るが、音声設定がうまく行かないのでデフォルト音声のまま。

これらの設定項目は、変更がある度にApplicationData.LocalSettingsに保存するようにしているので、このアプリのサスペンド時には何もしない。

Metro特有の要求の一つが4つのレイアウト(デフォルトの全画面横長のFullScreenLandscape、Snappedになった別のアプリが横に入って幅が狭まったFilled、全画面縦長のFullScreenPortrait、横に寄せたSnapped)への対応だが、とくにSnappedは要素の配置を大きく変える必要がある。

ちなみに、このレイアウト遷移をアニメーションと呼んでいるが、実際にはアニメーションするわけでも何でもなく、ただ配置が切り替わるだけ(XAMLのアニメーション機能で、いきなり最終値に変えるメソッドを待ち時間0で実行)という。

メインページを左側にSnappedにすると、こうなる。右側はデスクトップ。

やっていることは、
  • タイトルのアイコンを非表示にする。
  • Ping NASのButtonの列は、各要素を入れたGridをStackPanelに入れているが、この配置方向を横にしていたものから縦にし、サイズも合わせて変える。
  • Settingsのアイコンを非表示にする。
  • StatusのTextBoxの位置を下げる。
  • CPUなどの各項目は、各要素を3つに分けて入れたGridをStackPanelに入れているが、これも配置方向を横から縦にし、サイズを変えて位置を下げる。
  • WarningのTextBoxのサイズを変えて位置を下げる。
  • 一番下のRaw responseのTextBoxを非表示にする。
なお、このページは基本ページ(Basic Page)のテンプレートから作ったもので、タイトルのフォントが小さくなっているのはテンプレートによるもの。また、タイトルのアイコンの位置に、元のページに戻るbackButtonのアイコンが要素として存在する。これは最初のページの場合は実際には表示されないが、不要かといえばそうでもなく、ページ履歴の管理に関係しているようで、削ってしまうとSnapped状態のときにこのページに戻れなくなる。

ついでに、デスクトップの壁紙を見ていて気づいたが、Filled状態になったデスクトップは、アイコンは移動しても壁紙は移動しないらしい。

次にSnapped状態の設定ページ。Snappedになったら自動的にメインページに遷移することも考えたが、一応用意した。

やっていることは、
  • Scan NASのButtonの列も、同じくStackPanelの配置方向を横から縦にし、サイズを変える。
  • 見つかったNASを表示するGridViewは、実は同じ内容のListViewも要素として存在していて、このGridViewを表示にしてListViewを非表示にしていたものを逆にする。
  • Ping Intervalなどの各項目も、同じくStackPanelの配置方向を横から縦にし、サイズと位置を変える。
  • 一番下のRaw responseのTextBoxを非表示にする。
このGridViewとListViewを両方持っていて切り替えるという方法は、非効率な気もするが、Grid AppとSplit Appのテンプレートでやっている方法なので。なお、これらの中のGridはItemTemplateでサイズを指定しているが、実際に表示されるサイズは外のGridViewとListViewのサイズにも影響される模様。

以上、このプロジェクトファイルはNAS Herderのプロジェクトサイトに置いてある。ただし、Microsoftの審査は受けてないので開発者ライセンスのあるPCでしか実行できない。そもそも常駐できない監視アプリに何の意味があるのか、という致命的問題があるが……。

2. プロジェクトのテンプレート


アプリを作り始めるのに、どのテンプレートをベースにするのがいいのか、ということを調べるのに時間を食ったので、そのメモ。

新しいプロジェクトをWindows Metro styleで作成するときは、Blank App、Grid App、Split Appのテンプレートから選ぶわけだが、アプリの内容的にGrid AppでもSplit AppでもないとなるとBlank Appになる。ただ、Blank AppにはMetroアプリとして必須のレイアウト切り替え機能なども入ってないので、あまりベースとして使えない。

そこで、プロジェクト作成後に追加できる基本ページ(Basic Page)のテンプレートにはこれらの機能が入っているので、最初のページをこれに差し替える手順。に、プロジェクト作成時に入る、XAMLのテンプレートの大元たるStandardStyles.xamlには後々にBlendで開いたときにエラーを出す問題があるので、これをMSDNのサンプルのものに差し替えることを含めた手順が以下。
  1. Visual Studioで、ファイル -> 新規作成 -> プロジェクト -> テンプレート -> Visual C# -> Windows Metro style -> Blank App (XAML) を選び、プロジェクトを作成したら一旦終了。
  2. 作成されたプロジェクトフォルダーのCommonフォルダーを開き、StandardStyles.xamlを、MSDNのいずれかのサンプルにある同名ファイルで上書き。
  3. Visual Studioでこのプロジェクトを開き、プロジェクト -> 新しい項目の追加 -> Visual C# -> Windows Metro style -> 基本ページ を選び、追加(名前はここではBasicPage1.xamlとする)。
  4. App.xamlのコードの表示を開き(C#)、 if (!rootFrame.Navigate(typeof(MainPage))) 中の MainPageBasicPage1 に修正し、ビルドして最初にBasicPage1が表示されることを確認。(ファイルの差し替えまでしない場合は、ここまで。)
  5. ソリューションエクスプローラーで(元の)MainPage.xamlを削除。
  6. BasicPage1.xamlのデザイナーの表示を開き(XAML)、 x:Class="[名前空間名].BasicPage1" 中の BasicPage1MainPage に修正。
  7. 同じくBasicPage1.xamlのコードの表示を開き(C#)、 public sealed partial class BasicPage1 中の BasicPage1MainPage に修正し、 public BasicPage1() 中の BasicPage1MainPage に修正。
  8. ソリューションエクスプローラーでBasicPage1.xamlの名前をMainPage.xamlに変更。
ただ、Visual Studio 2012もRTMで変わるだろうから、この手順もたぶん変わる。

3. よしなしごと


本格的にXAMLを使ったアプリは初めてで、最初は「何かややこしいことしてるな」という印象だったが、慣れてくるとこれはこれで合理的で面白い。スタイルやテンプレートのかけ方は、CSSを多重にかけたウェブページに似ている。

一方で、Metroアプリ作成のガイドラインなど改めて読んでいると、Metroはやはりタブレット向けのUIとの印象を強くした。その辺り、Microsoftの偉い人が話すことと、実際のMetroにはズレがある感じがする。

Metroでは比較的狭いタブレット画面でタッチ操作するために色々なガイドラインがあって、一言でいうと画面が窮屈になりがちだが、そこを(タッチ操作を駆使して)使いやすいUIにデザインすることが求められる。

また、Metroアプリの重要な方針として、「Content not Chrome」がある。つまり、従来のウィンドウの枠部分(Chrome)は表示せず(必要なときだけ出す)、ユーザーを内容(Content)に集中させよ、というものだが、これにはそもそもの前提として窮屈になりがちな画面の有効活用という要求があると思う。

これは見方を変えると、ユーザーに(OSの存在を意識させず)シングルタスクをさせろ、ということでもある。Metroではユーザーが同時に見られるアプリは基本的に1つで、Snappedで他のアプリも出しておけば一応2つになるが、まあシングルタスク+のUIといえる。

で、従来どおりのPCでは当然にウィンドウを幾つも開いてマルチタスクで使えるわけで(大型化するディスプレイを使えばなおさら)、それをシングルタスク+で使えと言われても無理がある。例えば、よく例に出る旅行サイトを使うアプリの場合、1つのアプリで見るより、色々な会社の旅行サイトを開いて見比べたり、Google Street Viewで現地の写真を見たり、交通の便を確かめたり、他の人の体験を読んだり、そういうことが同時にできた方が効率がいい。

いや、そんなことは当然で、リラックスしながらタブレットで見られることに価値がある、という言い方もできる。実際、従来はPCでやっていたがタブレットでも可能で、むしろそちらの方が便利という作業はあって、そこはタブレットが伸していくのだと思う。

ただ、そうすると対立軸はデスクトップかMetroかではなく、従来のPCかタブレットかになると思うので、それはつまり、従来のPCではデスクトップを使い、タブレットではMetroを使えばいい、という面白くも何ともない話に落ち着く。一周回って「元々そういう話だったのでは」という地点に戻るというか。

したがって、使う人、使う場面を考えて、タブレットがよければMetroアプリで行き、従来のPCがよければデスクトップアプリで行く、ということでいいのではないか、と改めて思う。