2016/05/29

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

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


1. 概要


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

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

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

[追記] VisualTreeHelper.GetDpiメソッド

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

2. テスト


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

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

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

2016/05/26

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

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

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


トーストはXMLで生成する必要がありますが、Windows 10ではWindows 8から形式が変わっています。といっても、Windows 8式でもそのまま出せるので、インタラクティブな機能が必要ならこちらを使うということです。説明は以下を参照。
このXMLは一から組み立ててもいいですが、MicrosoftがNotificationsExtensionsというライブラリを出していて(デスクトップアプリから使用可)、これでWindows 10式のXMLを生成できます。
これを使った例としては以下のようなもの。
// using System.IO;
// using NotificationsExtensions;
// using NotificationsExtensions.Toasts;
private const string MessageId = "Message";
private string ComposeInteractiveToast()
{
var toastVisual = new ToastVisual
{
BindingGeneric = new ToastBindingGeneric
{
Children =
{
new AdaptiveText { Text = "DesktopToast WPF Sample" }, // Title
new AdaptiveText { Text = "This is an interactive toast test." }, // Body
},
AppLogoOverride = new ToastGenericAppLogo
{
Source = string.Format("file:///{0}", Path.GetFullPath("Resources/toast128.png")),
AlternateText = "Logo"
}
}
};
var toastAction = new ToastActionsCustom
{
Inputs =
{
new ToastTextBox(id: MessageId) { PlaceholderContent = "Input a message" }
},
Buttons =
{
new ToastButton(content: "Reply", arguments: "action=Replied") { ActivationType = ToastActivationType.Background },
new ToastButton(content: "Ignore", arguments: "action=Ignored")
}
};
var toastContent = new ToastContent
{
Visual = toastVisual,
Actions = toastAction,
Duration = ToastDuration.Long,
Audio = new NotificationsExtensions.Toasts.ToastAudio
{
Loop = true,
Src = new Uri("ms-winsoundevent:Notification.Looping.Alarm4")
}
};
return toastContent.GetContent();
}
この中でインタラクティブな機能はToastActionCustomの部分にあります。
  • InputsのToastTextBoxでテキストボックスを指定し、そのコンストラクタ―でidとして埋め込まれる文字列を決めています。このidがユーザーからの反応を受け取るときに意味を持ちます。
  • ButtonsのToastButtonでボタンを指定し、そのコンストラクタ―のcontentでボタンに表示される文字列を与え、同時にargumentsとして埋め込まれる文字列を決めています。
これで生成されたXMLが以下。
<?xml version="1.0" encoding="utf-8"?>
<toast duration="long">
<visual>
<binding template="ToastGeneric">
<text>DesktopToast WPF Sample</text>
<text>This is an interactive toast test.</text>
<image src="file:///[executive folder path]\Resources\toast128.png" alt="Logo" placement="appLogoOverride" />
</binding>
</visual>
<audio src="ms-winsoundevent:Notification.Looping.Alarm4" loop="true" />
<actions>
<input id="Message" type="text" placeHolderContent="Input a message" />
<action content="Reply" arguments="action=Replied" activationType="background" />
<action content="Ignore" arguments="action=Ignored" />
</actions>
</toast>
view raw Toast.xml hosted with ❤ by GitHub
先頭のヘッダーはなくても構いません。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. まとめ


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