Quantcast
Channel: 荒井省三のBlog
Viewing all 163 articles
Browse latest View live

Windows.Web.Http.HttpClient クラスについて

$
0
0

Windows 8.1 の Windows Runtimeには、新しい HTTP スタックとして Windows.Web.Http 名前空間にHttpClient クラスが追加されています。この HttpClient クラスは、Windows 8.1 の新機能ガイドによれば コネクト スタンバイのリアルタイム通信で利用できるとの説明があります。しかし、使って見ると注意しないといけない点がありました。それは、GetStringAsyncメソッドの使い方です。以下に簡単なサンプルを示します。

async Task<string> GetContent(string url)
{
  var uri = new Uri(url);
  var client = new HttpClient();
  var data = await client.GetStringAsync(uri);
  return data
}


このGetContentメソッドで戻ってくる文字列ですが、httpヘッダーに「Content-Type: text/html;charset=utf-8」などの文字エンコーディングが指定されている時は期待した文字列が返ります。しかし、「Content-Type : text/xml」のようにcharset指定がないとASCII文字が返ってくるので、文字化けが発生したような状態になります。この問題を回避するには、サーバー側でhttpヘッダーにcharsetを返すようにするか、取得したデータから正しいエンコーディングで処理しないといけません。RSSなどのXMLで簡易的に対処するには、以下のようにします。

async Task<string> GetContent(string url)
{
  var uri = new Uri(url);
  var client = new HttpClient();
  var content = response.Content;
  var stream = (await content.ReadAsBufferAsync()).AsStream();
  var reader = (TextReader)new StreamReader(stream);
  var data = reader.ReadToEnd();
  return data
}

コードを見れば分りますが、Contentプロパティからストリームを取り出して、TextReaderで読み込むという作業をすることで正しい文字列を取得することができます。もちろん、XMLであればXDocumentにストリームを読み込むなどの方法も考えられます。
もしくは、文字のエンコーディングを知っているのあれば、Encodingクラスを使用してバイト配列から正しいエンコードに変換することもできます。この用途では、「(await content.ReadAsBufferAsync()).ToArray()」のようにToArray拡張メソッドを呼び出すことでバイト配列を取得できるようになります。

使い方さえ間違えなければ、コネクテッド スタンバイのようなシナリオでhttp通信を行うことができるので、新しいHttpClientクラスも有効活用してみましょう。

追記:Content-Typeヘッダーを取得するには、レスポンスのContent.Headersプロパティ経由で取得します。レスポンス メッセージ オブジェクトのHeadersコレクションに含まれていないのでご注意ください。


About Windows Store App with Leap Motion device

$
0
0

Sorry, English later, first Japanse.
私の友人でもある初音さんが、Leap Motion必須というWindows ストア アプリを公開してくれました。このアプリは、Leap Simon Saysというものです。更に初音さんが素晴らしいのは、Windows ストアの審査に通す過程を色々とまとめて下さっていることです。初音さんの許可を頂いているので、どうやったかを簡単に解説したいと思います。まとめて読みたい方は、Leap Motion必須のWindows ストア アプリが審査に通りましたというエントリーをお読みください。

  1. Leap対応アプリで審査の壁が越えられない
    Leap Motion SDKが提供するLeap.dllやLeapCSharp.dllが、MSVCP100.DLL(Visual Studio 2010)に依存しているために、デバッグ環境で実行できても、WACKが失敗を返すというものです。この壁を完全にクリアーするには、Leap Motion SDKが Visual Studio 2013対応になるのを待たなければなりません。ですので、Leap MotionへWindows ストア アプリをサポートするようにリクエストを投げてみてください。
  2. Leap対応アプリがWACKチェックをクリアして審査中
    Leap Motion SDKが、HTMLアプリのために用意しているleap.jsがアクセスするWeb ServicesへプログラムからWebSocketを使用してアクセスするようにして、WACKにパスしたという話になります。まあ、Web Servicesが返すJSONに関してはleap.jsやデータをダンプして確認してから作成したということになります。
  3. Leap対応アプリがWACKチェックをクリアして審査リジェクト
    認定要件1.2、6.2、6.8を満たしていないという理由でリジェクトになりました。対応としては、アプリの説明にLeap Motionが必須であり、ループバックが必須と明記し、ループバックなので外部へアクセスしないことを明記し、アプリで英語表記も使っていることなどを明記して、もう一度申請したという流れになります。
  4. Leap Motion必須のWindows ストア アプリが審査に通りました
    結局は、年齢区分を12歳以上にして提出することでパスしました。

簡単にまとめると初音さんが行った方法は、以下の2種類に大別できます。最初に、開発時の対応は以下のことを実施しています。

  • Leap Motionとストア アプリの接続にWeb Servicesを利用した。
  • アプリが提供する情報として、Leap Motionが必須であることを明記。
  • また、ループバックを使用していることを情報として明記。
  • 最後に、ループバックを使用するためのガイドを情報として明記。
  • プライバシーポリシーの設置。

次に申請時に以下の対応を実施しています。

  • アプリの説明欄に以下を明記。
  • Leap Motion デバイスが必須。
  • ループバック を使用していることを明記。
  • 本来のアプリの説明。
  • 年齢区分を12歳以上に設定。

このアプリは、ストア アプリではデバイス アプリの一種として考えることができます。もちろん、利用する場合は利用者がループバックを使用できるようにする必要がありますが、それでもLeap Motionデバイスを使ったアプリを公開できたという素晴らしい経験を共有してくれたことに私は感謝しています。

もちろん、同じ作業を行ったからと言って100%審査に合格することを保証するものではありません。それでも、できたという経験が非常に大切なことだと私が考えるからです。皆さんは、如何でしょうか。是非、Leap Motionをお持ちの方は使ってフィードバックをしてあげてください。
最後に、初音さん、本当に有難うございました。感謝しています。

=====================================================================================
Last english:

Hatsune-san, my friend, published Windows store app required Leap Motion device. This app is Leap Simon Says. So he publish this work on his blogs.  I get his permission and explain his work.  His entry locate Passed a store app that required Leap Motion device. Of course, this entry written in Japanese.

  1. WACK report failed for store app with Leap Motion (written in Japanese)
    The library of Leap Motion SDK, such as Leap.dll and LeapCSharp.dll, depend on MSVCP100.DLL(Visual Studio 2010). So work fine on debug environment.  But WACK report failed. To perfect solution, we wait until Leap Motion SDK support Visual Studio 2013. Please you request "support Windows Store Apps" to Leap Motion. 
  2. Under store certification of a store app with Leap Motion(written in Japanese).
    Leap Motion SDK include leap.js for html app. This library access Web Services via loopback. An app use WebSocket via Web Services. So WACK report pass. For this, This must have investigated the JSON format.
  3. An store app with Leap Motion, WACK passed bun store certification rejected(written in Japanese).
    An app rejected via the cetification requirement 1.2 and 6.2 and 6.8. So I added app’s description that  require Leap Motion device, require loopback. So no access the external network. So written app use english characters in  submit’s comment. Then app submitted.
  4. Passed a store app that required Leap Motion device(written in Japanese).
    Finally,  Age rating is 12+, so the store certification passes.

His app support follow.

  • App use web servicces via Leap Motion device.
  • In App guide, require Leap Motion device.
  • In App guid, require loopback.
  • In app guid, how to use loopback.
  • App support a privacy policy.

Submission is follow.

  • App’s description include follow.
  • Require Leap Motion device.
  • Require loopback.
  • Of course, app’s description.
  • Age rating 12+.

This app is kind of device apps. So you must configration loopback for using this app.  But hatsune-san share his experiense of store certification. This is gread job!
Thak you for greate job. But this way no Guarantee. If you need, let’s try this way, then you share your way for everyone.

Finally, special thanks Htasune-san. Congratuation your great job!.

How to enable loopback for Windows Store Apps

$
0
0

Sorry, English later, First Japanese.

Windows 8.1 の IE11 について

私の環境ですが、Windows 8.1 に IISを有効にした状態で、 IE 11(デスクトップIE と モダンアプリとしての IE11)で「127.0.0.1」というループバックにアクセスすると、このページは表示できませんとなってしまいます。
IE11 not display
この問題を回避するには、[インターネット オプション]-[セキュリティ]-[信頼済みサイト]でサイトボタンをクリックして、127.0.0.1を信頼済みサイトに追加します。
Trust site dialog
これでブラウズすると、以下のように正常に表示されます。
IE11 display
Windows 8.1のIE11では、ループバックが制限されているのでご注意ください。

Windows ストア アプリについて

Windows ストア アプリも、標準ではループバックとの通信も禁止されています。ストア アプリを開発される方の環境の場合は、Visual Studioがデバッグ環境においてループバックを使用するように設定しています。これは、[プロジェクト プロパティ]-[デバック]に設定があります。
Debug Setting
ローカル ネットワークのループバックが有効になっていることで、デバッグ時は問題なくループバックを使った通信を行うことができます。デバッグ環境ではない時に、どのように調べて対応するかという情報は、「ループバックを有効にする方法とネットワーク分離のトラブルシューティングを行う方法」に記載されています。このドキュメントには、CheckNetIsolation.exeというユーティリティを使った確認と追加する方法が記述されています。CheckNetIsolation.exeユーティリティは、Windows 8/8.1ととも提供されていますので、開発ツールがなくても利用することができます。このツールの利用方法は、コマンドプロンプトを開いて以下のように利用します。

C:\Users\XXXX>CheckNetIsolation.exe LoopbackExempt -s

リスト ループバックは AppContainer を除外しました

[1] -----------------------------------------------------------------
    名前: bfe2a6a2-0cad-4251-87de-ac2f38f35ec4_ngfx0nz00mqn6
    SID:  S-1-15-2-1996650045-1801038662-1737393378-2727025871-1170270685-373450831-3470238151

[2] -----------------------------------------------------------------
    名前: app.abfb62353.ac3b8.a430f.ab2b0.ad9c3ace87830_8wekyb3d8bbwe
    SID:  S-1-15-2-2918410053-2341293364-2429605485-2955997936-2162362100-2763491358-3224060108

...


-s オプションは、ループバックが許可されているアプリの一覧が表示されます。名前が、アプリのパッケージ名になり、SIDがアプリのセキュリティ識別子になります。 LoopbackExempt -a -n=パッケージ名 オプションを使用すると、ループバックを使用するアプリを追加することができます。削除するには、 LoopbackExempt -d -n=パッケージ名 オプションを使用します。

次に、パッケージ名を確認する方法ですが、目的とするアプリを起動します。Windows + D キーを使ってデスクトップへ切り替えます。タスクバーで右クリックして、タスク マネージャーを起動します。
TaskManager 1
詳細をクリックしてから、目的のアプリをクリックして、右クリックで表示されるコンテキスト メニューから「ファイルの場所を開く」をクリックします。
TaskManager 2

こうすることで、Windows エクスプローラーが開きます。
Program Files
選択されているフォルダー名が、パッケージ名となります。ですから、このフォルダー名を控えておいて、CheckNetIsolationを使ってループバックを有効にするのに使用することになります。

Fiddlerを使う方法

Fiddler 4をお使いの場合は、FiddlerにWindows 8 用の AppContainer Loopback Utility が含まれていますので、これを使用するにが簡単でしょう。Fiddler 4 を持っていない場合や、CheckNetIsolation ユーティリティのようにコマンドラインではなく GUI で設定したい場合は、Fiddler Windows 8 AppContainer Loopback Utilityを使って下さい。Looback Utilityからダウンロードした「enableloopbackutility.exe」を実行することで、Loopback ユーティリティがインストールされます。インストール後に、全てのプログラムから「Enable AppContainer Loopback」 をクリックします。
AppContainer Loopback Exemption Utility Shorcut

起動すると以下のウィンドウが表示されます(このユーティリティは、管理者権限を必要とするので、UACのダイアログで「はい」を押してください)。
AppContainer Loopback Exemption Utility
アプリの一覧からループバックを有効にするアプリを見つけて、チェックボックスにチェックを付けてから、「Save Changes」ボタンをクリックします。これで、ループバックを有効にすることができます。

ちなみに、このユーティリティは、Win32 APIのファイアーウォール関係を使用しているようです。詳細を知りたい場合は、Fiddlerのサイトなどでドキュメントを調べてみてください。私のお勧めは、CheckNetIsolation.exe ユーティリティになります。その理由は、管理者権限を必要としないからです。

 

===================================================================
Last English.

About IE11 in Windows 8.1

My environment is to enable iis on Windows 8.1. I browse loopback(127.0.01) using IE 11(desctop IE and Modern IE11), then not displayed this page.
IE11 not display
To solution, [Internet option]-[Security]-[Trusted Sites]- Click the Site button, then add 127.0.0.1 to trusted sites.
Trust site dialog 
Browse Ok. 
IE11 display
Notice, limite using loopback of Windows 8.1’s IE11.

About Windows Store Apps

Default settings is Windows store apps deny loopback. But debug environment of Visual Studio enable loopback. This setting is [Project properties]-[debug].
Debug Setting
Enable loopback of local network, you can use loopback. For production environment , The information is  How to enable loopback and troubleshoot network isolation (Windows Store apps). This document describe “How to use CheckNetIsolation.exe”. CheckNetIsolation.exe is default tools onWindows 8/8.1. So not have Visual Studio, you can use this tool. You use this tool via command prompt. 

C:\Users\XXXX>CheckNetIsolation.exe LoopbackExempt -s

リスト ループバックは AppContainer を除外しました

[1] -----------------------------------------------------------------
    名前: bfe2a6a2-0cad-4251-87de-ac2f38f35ec4_ngfx0nz00mqn6
    SID:  S-1-15-2-1996650045-1801038662-1737393378-2727025871-1170270685-373450831-3470238151

[2] -----------------------------------------------------------------
    名前: app.abfb62353.ac3b8.a430f.ab2b0.ad9c3ace87830_8wekyb3d8bbwe
    SID:  S-1-15-2-2918410053-2341293364-2429605485-2955997936-2162362100-2763491358-3224060108

...


-s option is listing loopback’s Apps. Name is app’s package name, SID is security identifire. LoopbackExempt -a -n=package name option is to add loopback’s app. For delete, use LoopbackExempt -d -n=package name option.

Next, for confirm package name. First run target app, then  using Windows + D key, switch desktop. run Task Manager. 
TaskManager 1
Click detail, then click target app. The Context Menu by Right click , click [open place file].
TaskManager 2 

Opend Windows explorer window.
Program Files
Selected folder is package name. So you can use package name by CheckNetIsolation.

How to use Fiddler.

Fiddler 4  include the AppContainer Loopback Utility. If use Fiddler4, you use it. If you don’t have Fiddler 4 or want to use GUI, then please use Fiddler Windows 8 AppContainer Loopback Utility . Downloaded enableloopbackutility.exe , run, so installed Loopback utility. From all programe, click 「Enable AppContainer Loopback」.
AppContainer Loopback Exemption Utility Shorcut

Display under window( this utility need admin right, you click “yes” on UAC Dialog).
AppContainer Loopback Exemption Utility
Please find target app on Apps list, chek on checkbox, then click Save changes button. So enabled loopback.

I recommended CheckNetIsolation.exe Utility. Because not need Admin right.

Windows ストア アプリのタイポグラフィについて

$
0
0

Windows 8.1の Windows ストア アプリ開発では、合字が発生する場合があります。どのように対処するかは、高橋のBlogを参照して頂くとして、ここでは何で?なのかということを考えてみたいと思います。 確か、Visual Studio 2012までは特に問題を感じなかったのですが、これは StandardStyles.xamlというスタイル シートがプロジェクトに取り込まれていたお蔭とも考えられます。一方で、Visual Studio 2013は StandrdStyles.xamlが廃止されたことで、この問題が出てきました。Visual Studio で XAML 系のプロジェクトをビルドすると、「%WindowsSdkDir%\Include\winrt\xaml\design\generic.xaml」がリソースとしてアプリに取り込まれるようになっています。このリソース定義で、Typography.DiscretionaryLigaturesとTypography.StandardLigaturesの設定が各種のテキスト スタイルに指定されていることから、合字の問題が発生します。何故、標準が合字なのかという疑問を調べていくと、タイポグラフィのガイドラインという文書に行き当たります(日本語ドキュメントでは、タイポグラフィを文字体裁と訳しています)。このガイドラインに記述されている「優れた文字体裁を実現するための8つのヒント」の第1項に「OpenType機能をグローバルに適用する」があります。この記述の最初に

「推奨されるいずれかの UI フォントを使う場合、OpenType 機能のカーニング (kern)、随意の合字 (dlig)、Stylistic Set 20 (ss20) をすべてのテキストに適用します。」

とあります。つまり、UI フォントは合字、カーニングなどを適用することで優れたタイポグラフィが実現できるとと書かれているわけです。UI フォントを日本語の環境に置き換えると、メイリオ UI フォントになるわけで、メイリオ UI フォントの合字がこの問題に繋がっていることになるわけです。このガイドラインには、

推奨されるフォントを使わない場合は、随意の合字を適用しないでください。」

とも記載されているので、推奨フォントを使用しない場合は合字を適用しないことを標準にしなさいと言っているわけです。つまり、推奨フォント以外は、テキスト スタイルを自分で定義して generic.xamlのスタイルを適用しないようにして下さいということになります。

これは UX ガイドラインをチェックしていて見つけましたが、色々なガイドラインがあるので是非、ご自分で確認してみてください。ちなみに、フォントに関係する UX ガイドラインを以下に記述しておきます。

セマンティックズームの概要ビューをグラフ化するには

$
0
0

Windows ストア アプリでは、セマンティックズームが提供されています。ナビゲーションを容易にするために、詳細ビュー(ZoomIn)と概要ビュー(ZoomOut)に切り替えることができるコントロールが、セマンティックズームになります。Develpoper Campなどで、概要ビューをグラフ化したりという説明をしたりもします。が、実際にグラフ化するのはどうしたら良いか?というのが、今回の記事の趣旨になります。

最初に、次のような詳細ビュー(ZoomIn View)があるとします。
RssLandscape
これを概要ビュー(ZoomOut View)でグラフ化してみます。
RssZoomOut
簡単な棒グラフですが、これをどのように実現するかというと、色々なことを考える必要があります。たとえば、

  • グラフの最大値をどのように決めるか
  • グラフの長さに関係しますが、ポートレイトでの表現をどうするか。
  • グラフの色をどのように決めるか

などがあります。グラフの色に関しては、決め打ちしてしまえば良いのですが、グラフの100%の高さを様々な解像度に対応させようとすると、面倒なことが想像できることでしょう。たとえば、ランドスケープであれば 縦 < 横 という関係ですが、ポートレイトでは 横 < 縦 という反対の関係になりますし、データ バインドを使ってどのように表現するかというのも考えないといけないことになります。特にデータバインドを使わないと、詳細ビューと概要ビューの間のナビゲーションを実現できないので、データバインドの使用は必須と言えるでしょう。このように考えて、作成した詳細ビューと概要ビューの Grid View定義を次に示します。

<SemanticZoom.ZoomedInView><GridView x:Name="itemGridView"
            AutomationProperties.AutomationId="ItemGridView"
            AutomationProperties.Name="Grouped Items"
            ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}"
            ItemTemplate="{StaticResource ZoomIn250x250Template}"
            SelectionMode="Single"
            IsSwipeEnabled="True"
            IsItemClickEnabled="True"
            SelectionChanged="itemGridView_SelectionChanged"
            Loaded="itemGridView_Loaded"
            ItemClick="ItemView_ItemClick"><GridView.ItemsPanel><ItemsPanelTemplate><ItemsWrapGrid GroupPadding="0,0,70,0"/></ItemsPanelTemplate></GridView.ItemsPanel><GridView.GroupStyle><!-- 省略 --></GridView.GroupStyle></GridView></SemanticZoom.ZoomedInView><SemanticZoom.ZoomedOutView><GridView x:Name="zoomOutItemGridView"
            AutomationProperties.AutomationId="ItemGridView"
            AutomationProperties.Name="Grouped Items"
            ItemsSource="{Binding CollectionGroups, Source={StaticResource groupedItemsViewSource}}"
            ItemTemplate="{StaticResource zoomOutGraphTemplate}"
            SelectionMode="None"
            IsSwipeEnabled="false" ><GridView.ItemsPanel><ItemsPanelTemplate><ItemsWrapGrid GroupPadding="0,0,70,0"/></ItemsPanelTemplate></GridView.ItemsPanel></GridView></SemanticZoom.ZoomedOutView>

 

データソースがGroupedItemViewSource(CollectionViewSourceで定義)を指定しており、概要ビュー(ZoomOut)は CollectionGroups を指定しています。CollectionGroupsとは、データソースのが表現するコレクション全体を意味する指定になりますから、グループのコレクションということになります。詳細ビューは、CollectionViewSourceで定義している ItemPathが示すグループが持つアイテム コレクションを示しています。では、概要ビュー(ZoomOut)のDataTemplate定義を示します。

<DataTemplate x:Key="zoomOutGraphTemplate"><Grid HorizontalAlignment="Left" Width="100"
        Height="{Binding LandscapeHeight, Source={StaticResource GraphHeight}}"
        DataContext="{Binding Group}"><Border VerticalAlignment="Bottom" Margin="0"
            Background="{Binding GraphColor, Converter={StaticResource ColorToBrush}}" 
            Height="{Binding Converter={StaticResource CollectionToHeight}, ConverterParameter={Binding Source={StaticResource GraphHeight}}}" /><StackPanel VerticalAlignment="Bottom" Background="{ThemeResource ListViewItemOverlayBackgroundThemeBrush}"><TextBlock 
         Text="{Binding Items, Converter={StaticResource CollectionToCount}, FallbackValue=5}" 
         Foreground="{ThemeResource ListViewItemOverlaySecondaryForegroundThemeBrush}"
         Style='{StaticResource SubheaderTextBlockStyle}'
         TextWrapping='NoWrap' HorizontalAlignment='Center' Margin='10' /><TextBlock Text='{Binding Title}'
         Foreground='{ThemeResource ListViewItemOverlaySecondaryForegroundThemeBrush}'
         Style='{StaticResource TitleTextBlockStyle}'
         TextWrapping='NoWrap' HorizontalAlignment='Center' Margin='10' /><TextBlock Text='{Binding PubDate}'
         Foreground='{ThemeResource ListViewItemOverlaySecondaryForegroundThemeBrush}'
         Style='{StaticResource CaptionTextBlockStyle}'
         TextWrapping='NoWrap' HorizontalAlignment='Center' Margin='10' /></StackPanel><ToolTipService.ToolTip><ToolTip ><StackPanel Orientation='Vertical'><TextBlock Text='{Binding Title}'
             Style='{StaticResource TitleTextBlockStyle}'/><TextBlock
             Text='{Binding Items, Converter={StaticResource CollectionToCount}}'
             Style='{StaticResource TitleTextBlockStyle}' /></StackPanel></ToolTip></ToolTipService.ToolTip></Grid></DataTemplate>


このDataTemplate定義で説明する箇所は、ルートであるGrid要素のHeightプロパティ、Boder要素のHeightプロパティとBackgroundプロパティ、それからTextBlockのTextプロパティのバインディングの定義になります。

Grid要素

Heightプロパティのバインディングが「{Binding LandscapeHeight, Source={StaticResource GraphHeight}}」と指定しています。これが何を表現するためかと言えば、グラフ全体の高さを決めるものになります。このために、ローカルリソースのGraphHeightにWindowSizeHelperクラスを指定しています。このクラスのコードを次に示します。

class WindowSizeHelper : BindableBase
{
  const int TOP = 140;
  const int BOTTOM = 100;
  public WindowSizeHelper()
  {
    var window = Windows.UI.Xaml.Window.Current;
    CalcSize(window.Bounds.Width, window.Bounds.Height);
    window.SizeChanged += window_SizeChanged;
  }
  void window_SizeChanged(object sender,
              Windows.UI.Core.WindowSizeChangedEventArgs e)
  {
    CalcSize(e.Size.Width, e.Size.Height);
  }
  void CalcSize(double width, double height)
  {
    DisplayInformation display = null;
    double x, y;
    display = DisplayInformation.GetForCurrentView();
    if (display != null && (
        display.CurrentOrientation == DisplayOrientations.Portrait ||
        display.CurrentOrientation == DisplayOrientations.PortraitFlipped))
    {
        x = width;
        y = height;
    }
    else
    {
        x = height;
        y = width;
    }
    this.LandscapeHeight = x - TOP - BOTTOM;
    this.PortraitHeight = y - TOP - BOTTOM;
  }
  private double landscapeHeight;
  public double LandscapeHeight 
  {
    get { return this.landscapeHeight; }
    set { this.SetProperty(ref this.landscapeHeight, value); }
  }
  private double portraitHeight;
  public double PortraitHeight
  {
    get { return this.portraitHeight; }
    set { this.SetProperty(ref this.portraitHeight, value); }
  }
}


WindowsSizeHelperクラスは、Windowの縦と横の状態を判断した計算結果(TOPとBOTTOMを引いて、グラフ領域の高さ)をLandScapeHeightプロパティとして公開します。 このプロパティをGrid要素のHeightプロパティにバインドすることで、100%の大きさにおける高さを決定します。

Border要素

VerticalAlignmentプロパティに「Bottom」を指定することで、グラフを下を基準にすることを表現しています。また、Heightプロパティのバインディングを「{Binding Converter={StaticResource CollectionToHeight}, ConverterParameter={Binding Source={StaticResource GraphHeight}}}」と指定しています。このバインディングが行うことは、グラフの高さを計算することです。このために、バインディング パスを省略(グループ オブジェクトが渡る)して、コンバーターにローカルリソースのCollectionToHeightConverterクラスを指定して、コンバーター パラメーターにローカルリソースのWindowsSizeHelperを指定しています。それでは、CollectionToHeightConverterクラスのコードを示します。

class CollectionToHeightConverter : IValueConverter
{
  public object Convert(object value, Type targetType, 
                        object parameter, string language)
  {
    var group = (value as RssDataGroup);
    if (group == null)
      return 0;
    var sizeHelper = parameter as WindowSizeHelper;
    double x = 0;
    if (sizeHelper != null)
      x = sizeHelper.LandscapeHeight;
    else
      x = Constants.DEFAULT_GRAPH_HEIGHT;
    var height = x * group.Items.Count() / group.Max;
    if (x < height) height = x;
    return height;
  }
  public object ConvertBack(object value, Type targetType,
                            object parameter, string language)
  {
    throw new NotSupportedException();
  }
}

中心となる計算は、「height = x * group.Items.Count() / group.Max」です。この計算は、WindowsSizeHelperクラスを使ってグラフの最大値(100%が x)とコレクションの数(Items.Count)とコレクションの最大値(group.Max)によって棒グラフの長さを算出しています。このコンバーターには、既に説明したようにバインディング パスを指定していないのでグループオブジェクトが渡されます。そして、コンバーター パラメーターに WindowsSizeHelper クラスを指定しています。このコンバーター パラメーターも注意が必要です。なぜなら、コンバーター パラメーターには、データソースが持つメンバーを渡すことができないのです。この理由とグラフの最大値がデータに依存するのではなく解像度に依存することから、WindowsSizeHelperをローカルリソースに指定しています。後は、グラフ化のためだけにグループオブジェクト(RssDataGroup)に Maxプロパティ(アイテムの最大値)を設定しています。もちろん、グループオブジェクトの作成時に正しい最大値になるように計算していることは、言うまでもありません。ここまでの説明で、CollectionToHeightConverterによって棒グラフの高さが計算されることを理解することができたことでしょう。まだ説明していないのは、「if (x &lt; height) height = x」ですが、これはグラフの最大値を超えた場合になります(最後に、具体例を示します)。Backgroundプロパティに指定しているコンバーターは、バインドされたメンバーをSolidColorBrushオブジェクトに変換するものです。つまり、グループオブジェクトにGraphColorプロパティを持たせており、グループ毎にグラフの色を指定できるということです。
今度は、TextBlockのTextプロパティに指定している「{Binding Items, Converter={StaticResource CollectionToCount}, FallbackValue=5}」を説明します。指定しているコンバーターであるCollectionToCountConverterクラスの考え方は、Itemsコレクションを渡して件数を返すものになります。

public class CollectionToCountConverter : IValueConverter
{
  public object Convert(object value, Type targetType,
                        object parameter, string language)
  {
    var collection = (value as IEnumerable).Cast<object>();
    if (collection == null)
      return 0;

    if (!collection.Any())
      return 0;

    return collection.Count();
  }
  public object ConvertBack(object value, Type targetType,
                            object parameter, string language)
  {
    throw new NotSupportedException();
  }
}

 

ここまでの説明で、グラフ化する仕組みそのものの説明は完了です。最後に、ポートレイト時の概要ビュー(ZoomOut)を示します。
RssPortrait

グラフの描画方法自体は、他にも色々な手法が考えられます。でも、少し工夫することで、セマンティックズームの概要ビューをビジュアルにすることができます。皆さんも、色々と工夫をしてみてください。

de:code で担当するセッションについて

$
0
0

久し振りの投稿になります。de:codeでは、以下の2つのセッションを担当しています。

この準備に追われているのですが、連携技術の中で説明する予定の Windows ランタイム コンポーネント ブローカーの概要と情報の探し方を解説します。 Windows ランタイム コンポーネント ブローカーとは、Windows 8.1 Update で追加された新機能の1つで、Windows ストア アプリからデスクトップの既存ライブラリを使えるように技術のことです。
Windows Runtime Component Broker

Windows ストア アプリから利用する仕組みは、プロキシを利用してコンポーネント側のスタブを使ってラッパーとなる Windows ランタイム コンポーネントを経由して既存のマネージ・ライブラリーを使用します。プロキシ/スタブを介するということは、アプリとコンポーネント間は通信を行うのでデータのマーシャリングが行われることになります。プロキシ/スタブという用語を聞くと、思い出すのが COM コンポーネントではないでしょうか。COM コンポーネントを思い出した方がいらっしゃれば、それは的を得ています。なぜなら、この技術は COM コンポーネント技術の上で構築されているからです。
もちろん、この技術を使って作成された Windows ストア アプリは、Windows ストアの認定には通りません。なぜなら、利用する Windows ランタイム コンポーネント ブローカーを Windows ストアに提出する方法がないため、Windows ストア側でテストができないからです。その代わりに、Windows ストア アプリが実行されるアプリコンテナーという一種のサンドボックスの制限を超えることができます。サンドボックスを超えられる理由は、デスクトップ コンポーネントが動作するプロセスはアプリ コンテナーではなく、通常の Win32 プロセスになるからです。この技術が、4月に行われた Build 2014 カンファレンスで英語では「Brokered Windows Runtime Component」と表現されていました。また、「Respecting Your Investments: How to Leverage Your Existing Code In a New Windows Runtime LOB App」というセッションの中でデモも行われていました。

 

この技術をアプリ ブローカーと呼んでいますが、学習するには以下の3種類の方法があります。

ホワイトペーパー

日本語訳のホワイトペーパーは、日本語的に誤訳ではありませんが、少しおかしなところもありますが、機能を学習するには役立つものです。最新のリンクなどを確認される場合は、英語のホワイト ペーパーをお読みください。このホワイト ペーパーに該当するサンプルも公開されています。ホワイト ペーパーに従って、アプリ ブローカーを作成しようとした時に問題になるのが、プロジェクト テンプレートとして何を選択すれば良いかという点になります。これは、「Windows ランタイム コンポーネント」のテンプレートを使用すればスクラッチで作成することができます。

NorthwindRT サンプル

このサンプルでは、スクラッチでのプロジェクトの作成方法の説明はありません。このサンプルと同等のアプリ ブローカーのプロジェクトを作成するには、「クラス ライブラリ」テンプレートを使用してプロジェクトを作成します。プロジェクトが出来上がってから、プロジェクト ファイルをエディタで編集する必要があります。このサンプルでは、SQL Server Compactを使用して、Windows Forms アプリ用に作成したデータ アクセス コンポーネントを Windows ストア アプリでアプリ ブローカーで使用するようになっています。このようなサンプルですから、現実に即したサンプルと言うことができます。

Brokered WinRT Component Project Templates

アプリ ブローカーを作成するために、Visual Studio のプロジェクト テンプレートを提供するものです。このテンプレートを使用すると、プロジェクト ファイルの編集などという作業は不要になり、目的のコードのみに集中できることになります。このテンプレートを使用する上での注意点は、アプリ ブローカーとプロキシ スタブのプロジェクトにおける既定の言語が「英語(en-US)」になっていることです。これが問題になる場合は、プロジェクト ファイルを編集してください。ちなみに、アプリ ブローカーのプロジェクト テンプレートは、NorthwindRT サンプルと同じで「クラス ライブラリ」テンプレートを使用しています。

アプリ ブローカー プロジェクトの作成方法などは、de:code 終了後に情報をまとめる予定でいます。

追記

アプリ ブローカーの作り方 1

$
0
0

サイド ローディングされた Windows ストア アプリのための Windows ランタイム コンポーネント ブローカーホワイトペーパーに記載されている アプリ ブローカーの作り方を解説します。

最初に、Windows ランタイム コンポーネントを作成します(ホワイト ペーパーでは、コントラクトの定義となっている箇所です)。

  1. C# で新規プロジェクトを作成します。
    [Visual C#]-[ストア アプリ]-[Windows アプリ]-[Windows ランタイム コンポーネント] テンプレートを選択します。
  2. ソリューション エクスプローラーでプロジェクトをアンロードします。
  3. プロジェクト ファイルを編集します。
    <TaergetPlatformVersion>要素の下に、以下のコードを追加します。
    <ImplicitlyExpandTargetFramework>false</ImplicitlyExpandTargetFramework>
  4. 次にホワイト ペーパーに記載されているように参照設定(ReferencePath要素)を追加します(デスクトップ IPC サーバーの詳細に記載されてい���す)。この作業によって、コア プロファイル(Windows ストア アプリ)とデスクトップ プロファイル(デスクトップのライブラリを使用)が1つのプロジェクトで混在することになります。
  5. プロジェクト ファイルを上書き保存してから、プロジェクトを再読み込みします。
  6. プロジェクト プロパティから、[ビルド]-[すべての構成]-[条件付きコンパイル シンボル]より「;WINDOWS_APP」を削除します。
    NETFX_CORE のみになります。
  7. プロジェクト プロパティから、[ビルド イベント]-[ビルド後に実行するコマンド ライン]に以下のコマンドを登録します(コントラクトの定義に記載されています)。 
    call "$(DevEnvDir)..\..\vc\vcvarsall.bat" x86
    REM 実装用のメタデータを格納するフォルダーを作成します
    md "$(TargetDir)"\impl
    REM 参照用のメタデータを格納するフォルダーを作成します
    md "$(TargetDir)"\reference
    REM 作成済みのファイルを削除します
    erase "$(TargetDir)\impl\*.winmd"
    erase "$(TargetDir)\impl\*.pdb"
    REM 実装用のメタデータとPDBファイルを impl フォルダーへコピーします
    xcopy /y "$(TargetPath)" "$(TargetDir)impl"
    xcopy /y "$(TargetDir)*.pdb" "$(TargetDir)impl"
    REM メタデータよりIDLを作成します
    winmdidl /nosystemdeclares /metadata_dir:C:\Windows\System32\Winmetadata "$(TargetPath)"
    REM IDLより各種のファイル(*.h,*_i.c,dlldatata.c,*_p.c)とメタデータを作成します
    midl /metadata_dir "%WindowsSdkDir%References\CommonConfiguration\Neutral" /iid "$(SolutionDir)SampleProxy\$(TargetName)_i.c" /env win32 /x86 /h "$(SolutionDir)SampleProxy\$(TargetName).h" /winmd "$(TargetName).winmd" /W1 /char signed /nologo /winrt /dlldata "$(SolutionDir)SampleProxy\dlldata.c" /proxy "$(SolutionDir)SampleProxy\$(TargetName)_p.c"  "$(TargetName).idl"
    REM メタデータより参照用のメタデータを作成します。
    mdmerge -n 1 -i "$(ProjectDir)bin\$(PlatformName)\$(ConfigurationName)" -o "$(TargetDir)reference" -metadata_dir "%WindowsSdkDir%References\CommonConfiguration\Neutral" -partial
  8. 後は、インターフェースの定義と実装コードを記述します。

後は、ホワイトペーパーに記述されているように プロキシ スタブのC++プロジェクトを作成し、アプリ ブローカーを利用するWindows ストア アプリを作成します。作成した Windows ストア アプリをデバッグするには、アプリ ブローカーとプロキシ スタブをインストール先のフォルダーへコピーしてから、ACL(ファイルのアクセス権)を設定して、プロキシ スタブをregsvr32.exeで登録しておく必要があります。

このプロジェクトの構造を簡単にまとめると、以下のようになっています。

  • Windows ランタイム コンポーネント プロジェクト
    プロジェクト ファイルを編集することで、デスクトップ ライブラリを使用できるようにする。
    ビルドのコマンドを使用することで、idlを作成し、IIDを定義したコード、ヘッダーファイル、dlldatata.c、プロキシ コードを作成てから、参照用のWindows メタデータを作成します。
  • プロキシ スタブ プロジェクト
    Windows ランタイム コンポーネント プロジェクトで作成した各種のファイルとモジュール定義ファイルから、プロキシ スタブ DLLを作成します。

このソリューションの特徴として、C++のコードを書く必要性はありませんが、C++コンパイラ オプションとリンカ オプションの設定を行う必要があります。必要なC/C++のコードは、ユーティリティで自動生成するということです。

実際にデバッグを行うと理解することができますが、サロゲート プロセス(dllhost.exe)がclrhost.dll(CLRのホスト モジュール)を読み込んでから、Windows メタデータを読み込むことで動作します。

アプリ ブローカの作り方 2

$
0
0

前回に解説したホワイトペーパーに基づくプロジェクトの作成方法では、1つだけ面倒な箇所があります。それは、使用するライブラリーに対する参照を追加するたびに、プロジェクト ファイルを手作業で編集しないといけないという点になります。また、ビルド イベントに登録したコマンドにおける面倒なことは、ビルドした Windows メタデータが上書きされるために、implフォルダーにコピーしているという点になります。この二つの使い難さを解消しているのが、NorthwindRT サンプルとなります。NorthwindRT サンプルの Windows ランタイム コンポーネントの作成方法は、ホワイトペーパーに記載されている手順に対して、Windows ランタイム コンポーネントの作成方法が異なります。具体的には、以下のような方法になります。

  1. C#で新規プロジェクトを作成します。
    [Visual C#]-[Windows デスクトップ]-[クラス ライブラリ] テンプレートを選択します。
  2. 作成したプロジェクトをアンロードします。
  3. プロジェクト ファイルを編集します。
    <OutputType>Library</OutputType> を以下のように編集します。
    <OutputType>winmdobj</OutputType>

    この作業で、ビルドする結果として Windows ランタイム コンポーネントが出力されるようになります。 
    続いて、<FileAlignment>512</FileAlignment>の次に以下の内容を追加します。
    <TargetPlatformVersion>8.1</TargetPlatformVersion><GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
  4. プロジェクト ファイルを上書き保存してから、プロジェクトを再読み込みします。
  5. ソリューション エクスプローラーの[参照]ノードを選択して、コンテキスト メニューより[参照の追加]を選択します。
  6. 参照マネージャーで参照設定を行います。
    [Windows 8.1]-[コア]-[Windows] への参照を追加します(この作業は必須です)。
    後は、必要なアセンブリに対する参照を追加したり、不要な参照を削除します。
    次に記載するアセンブリへの参照を追加した方が良いでしょう。
    %ProgramFiles(x86)%\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Runtime.dll
    %ProgramFiles(x86)%\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Threading.Tasks.dll
    %ProgramFiles(x86)%\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5.1\System.Runtime.WindowsRuntime.dll
  7. プロジェクト プロパティの[ビルド イベント]-[ビルド後に実行するコマンド ライン]に以下のコマンドを登録します。
    call "$(DevEnvDir)..\..\vc\vcvarsall.bat" x86
    REM winmdidlでWindowsメタデータよりidlを作成します
    md "$(ProjectDir)bin\idl"
    winmdidl /metadata_dir:C:\Windows\System32\Winmetadata /outdir:"$(ProjectDir)bin\idl" "$(TargetPath)"
    REM midlでidlより各種のファイルと参照用のWindowsメタデータを作成します
    md "$(ProjectDir)bin\proxystub"
    midl /metadata_dir "%WindowsSdkDir%References\CommonConfiguration\Neutral" /env win32 /W1 /char signed /nologo /winrt /dlldata "$(ProjectDir)bin\proxystub\dlldata.c" /proxy "$(ProjectDir)bin\proxystub\proxy.c" /iid "$(ProjectDir)bin\proxystub\iid.c" /header "$(ProjectDir)bin\proxystub\proxystub.h" /winmd "$(ProjectDir)bin\idl\$(TargetFileName)" "$(ProjectDir)bin\idl\$(TargetName).idl"
    REM mdmergeでWindows ストア アプリで使用する参照用のWindowsメタデータを作成します
    md "$(ProjectDir)bin\mdmerge"
    mdmerge -i "$(ProjectDir)bin\idl" -o "$(ProjectDir)bin\mdmerge" -metadata_dir "%WindowsSdkDir%References\CommonConfiguration\Neutral" -partial
      
    上記のコマンドで、idl、proxystub、mdmerge サブフォルダーが作成されて、各サブ フォルダーに出力結果が登録されるようになります。つまり、ビルドされた Windows メタデータを上書きすることが無くなります。
  8. 後は、インターフェースの定義と実装コードを記述します。

これで、アプリ ブローカーで使用する Windows ランタイム コンポーネントの作成は終了です。後は、プロキシ スタブの作成ですが、これはホワイト ペーパーと同じ方法で作成できます。VC++のプロジェクトから、proxystub サブフォルダーに作成されているコードを追加すれば良いのです。
作成したアプリ ブローカーを使用する Windows ストア アプリでは、mdmerge サブフォルダーに作成されているメタデータに参照設定を行うことを除けば、ホワイト ペーパーと同じです。

この方法のメリットは、使用するライブラリーに対する参照設定を参照マネージャーという GUI で行うことができることです。手作業で、プロジェクト ファイルを編集するよりも保守性に優れた方法になります。Visual Studioの使用上で問題になる箇所は、プロジェクト プロパティにおける出力の種類に何も表示されないことだけだと思います。このような表示になる理由は、デスクトップ用のクラス ライブラリーのプロジェクト テンプレートを利用しているからであり、出力の種類にWindows ランタイム コンポーネントと表示させるにはホワイト ペーパーと同じように Windows ランタイム コンポーネント プロジェクト テンプレートを使用する必要があるからです。表示がおかしいとしても、動作に問題はありませんので、現実のプロジェクトではこの方法をお勧めします。

de:codeでは、NorthwindRT サンプルをベースにしながら、SQL Server 2014を使用するようにカスタマイズしたプログラムをデモで使用しました。NorthwindRT サンプル自体は、SQL Compactを使用していますが、現実的なプロジェクトでは SQL Server を使用すると私が考えているからです。

このNorthwindRT サンプルの考え方を発展させたものとして、Brokered WinRT Component Project Templatesが公開されています。このテンプレートをインストールすると、Brokered WindowsRuntime Component とBrokered WindowsRuntime ProxyStub というテンプレートが追加されます。作り方は、以下のようにします。

  1. C# で新規プロジェクトを作成します。
    [Visual C#]-[Brokered WindowsRuntime Component]テンプレートを選択します。
  2. 参照や実装を作成します。
  3. 同一のソリューションに対して、VC++のプロジェクトを追加します。
    [Brokered WindowsRuntime ProxyStub]テンプレートを選択します。
  4. VC++ プロジェクトのプロパティより参照の追加を行います。
    [プロパティページ]/[共通プロパティ]/[参照]/[新しい参照の追加] を使って、C# プロジェクトに対する参照を行います。
  5. ソリューションをビルドすれば、アプリ ブローカーとプロキシ スタブの完成です。

作成したアプリ ブローカーを使用する Windows ストア アプリを作成する場合は、VC++のプロジェクト フォルダー内の Debug\Reference サブ フォルダーにある参照用のメタデータを参照するだけです。

このテンプレートとホワイト ペーパー、NorthwindRT サンプルの大きな違いは、winmdidl、midl、mdmergeをC#プロジェクトのビルド後に実行するのかと、C++のビルド前に実行するかという点です(厳密には、C++のプロジェクト参照の解決時です)。プロジェクト ファイルに記述されている既定の言語が英語(en-US)になっている点を除けば、このテンプレートを使用するのが、アプリ ブローカーを作成する方法として一番簡単でしょう。理由は、VC++プロジェクトの各種の設定を行う必要がないからです。
既定の言語が問題になるケースは、言語毎のリソース ファイルなどを扱う場合だと思われますので、このような場合はプロジェクト ファイルを編集して既定の言語を修正するようにしてください。


Windows 8 UX ガイド更新のお知らせ

$
0
0

Build 2014 以降に Windows 8 のユーザー エクスペリエンス ガイドが更新されているのに、気が付いていらっしゃるでしょうか。
ユニバーサル Windows アプリに発表に伴って、Windows Phone 8.1 アプリの UX ガイドが統合されて、大幅に内容に更新が行われています。
日本語版のUX ガイド(PDF)は、Windows ストアへの道にリンクがありますが、このPDFの更新作業を依頼したところです。よって、まもなく更新版のUX ガイドが公開される予定です。

少しだけ先行で、このエントリーからもダウンロードできるようにしておきます。
公開作業が済みましたので、ダウンロードはWindows ストアへの道からお願いします。

 

是非、一度は目を通すようにしてください。

XAML とは何か?

$
0
0

今更ですが、XAMLとな何なのかということを私なりに解説したいと思います。このような考えに至ったのは、色々な状況があるにせよ Windows Froms などの UI 技術が広く使われているのと、XAML を初めて学ぼうとしたときに書籍を含めて、新しい情報が少ないと感じたからです。

XAML が登場した背景

XAML(eXtensible Application Markup Language)は、.NET Framework 3.0 によって提供された技術になります。
ClientTechnology2014

.NET Framework 3.0 は、Windows Vista と同時に提供されたフレームワークになります。この時に、UI を作るための技術として WPF(Windows Presentation Foundation) とワークフローを実現する技術として WF(Windows Workflow Foundation) も提供されました。WPF は、UI を定義するために XAML を採用しており、XAML が UI 技術と考えがちですが、名前にアプリケーションが含まれていることから WF でもワークフロー定義として XAML を採用していました(この採用は、限定的であり、XAMLでなくともワークフローを定義できました)。XAMLが、広範なアプリケーションで利用可能になるには、.NET Framework 4.0(System.XAML 名前空間が提供されました)になります。
追記:.NET Framework 3.0では、WPFとWFだけなくWCF(Windows Communication Foundatipon) と デジタル アイデンティティを管理する Windows CardSpace も含まれています。説明では、UI 技術として焦点を当てていますので、XAML に関係する技術として WPF と WF を取り上げています。

Windows Vista では、新しいエアロ テーマなどに代表されるように美しいグラフィックスを提供した OS となります。商業的に成功したと言えませんが、技術的には Windows XP と大きな違いが何か所もあります。代表的なものを以下に示します。

  • 新しいディスプレイ ドライバー モデル(WDDM-Windows Display Driver Model-)
  • デスクトップ ウィンドウ マネージャー(dwm)
  • ユーザー アカウント制御(UAC)
  • 64 ビット OS

Windows XP までのディスプレイ ドライバーは XPDM(Windows XP display Driver Model)と呼ばれており、Windows Vista 以降の WDDM との互換性はありません。新しディスプレイ ドライバーになったことで、ディスプレイ ドライバー レベルでの Direct 3D に対する新しいプログラミング モデルを提供できるようになりました。つまり、OS レベルで GPU の恩恵を受けられるようになったことを意味しますし、このためにデスクトップ ウィンドウ マネージャーにより コンポジションなどが意味を持ってきます。
Windows XP までは、GPU を活用するにはDirectX などを採用したアプリケーションのみに限定されており、広範なアプリケーションへ GPU を活用できる道を開いたのが Windows Vista から採用された WDDM になります。しかし、新しいドライバー モデルへの移行には、様々な障害がつきものです。その大きな一つが、ディスプレイ ドライバーの性能が向上しないというものです。この理由から、Windows Vista ではダブル バッファリングという技術を採用しました(これが、Windows Vistaは遅いと呼ばれた原因です)。ダブル バッファリングは、描画内容をメモリ上に展開してから、GPU などを使って一気に描画する技術ですから、スムーズな描画をするには大量のメモリがあった方が良いのは自明のことになります。しかし、Windows Vistaリリース当時のハードウェアでは、メモリ1GというPCがローエンドで販売されていましたし、メモリ4Gとなると高価格帯であるハイエンド機種のみとなっていたことも、商業的に成功しなかった理由であろうと私は考えています。しかし、Windows Vista によって64 ビット対応のドライバーの拡充であったり、UACへの対応であったりと、次の OS である Windows 7 に対するドライバーなどの整備が順調に進んだと言えるでしょう。当時のMacOS X は、32ビット カーネルのLeopardであり、限定的な 64 ビット対応をしていたと記憶しています。MacOS X と比べると、Windows OS は、64 ビット化へ道をビッグバンで進めてきたとも言えるでしょう。
Windows 7 になった時点で、WDDM は1.1 となり、ダブルバッファリングを廃止しました。もちろん、これだけではありませんが、様々な改良を経て、多くの方に使っていただける OS となりました。ここまでの説明で明らかなことは、Windows OSの描画機能が、OS レベルで Direct X をサポートするようになったのが Windows Vista 以降だということなのです。

一方で、Windows OS のグラフィカル ユーザー インターフェースはウィンドウが主体となっています。古くは、16 ビット時代の Windows 1.0 に始まった GDI(Graphics Design Interface)によるものであり、Windows 9x までは多くのコントロールなどを多用したアプリではリソース不足に悩まされたことが何度もありました。リソース不足は、Windows XP に代表される 32 ビット OS の時代では、お目にかかる機会も少なくなりましたが、一部のアプリケーションではテクニックによってリソース不足を解消していたと私は聞いています。つまり、GDI を採用した技術である Windows Forms などは、16ビット Windows 時代から慣れ親しんだ ウィンドウ ハンドル(hWnd)を使用する Win32 API との親和性が高いのです(ベースになっている技術が一緒だからです)。それでは、WPFのに代表される XAML を使った UI 技術はどうでしょうか? Windows Forms などの GDI とは、全くの別物と呼ぶことができます。私が別物と呼ぶ理由は、後で説明しますが、GPU を有効活用して GDI とは異なる新しい ユーザー体験を作るための技術が WPF だということになります。

今度は、アクセシビリティというか、表示の見易さという観点から 最新 OSである Windows 8.1 を考えてみます。Windows 8 では、ピクセル密度という考え方がデザイン ガイドとして導入されました。画面表示におけるスケーリングを、100%、140%、180%を自動的に行うというものです。たとえば、Surface Pro は10.6インチで1920x1080(フルHD)という解像度になります。このサイズだと、スケーリングが100%だと文字が小さ過ぎて、読み難い人もでてきます。このため、出荷時設定として文字サイズが150%になっていたりします(DPI)。Surface Proから理解できることは、解像度を向上させてもモニター サイズによっては、可読性を損なうことから、DPI設定を調整する必要があるということです。DPI 設定は、Windows 8.1 よりモニターごとに設定できるようになりました。Windows XP時代のDPI設定は再起動が必要でしたが、Windows 8.1ではサインオフだけで済みようになっています。DPI設定の改良は、Windows Vista でも実施されており、Windows 7 で更なる改良が実施され、Windows 8/8.1 で地道で改良がされているものの 1つになります。
アプリを高 DPI に対応しようとすると、それなりに工数がかかります。この点を、Windows Forms と WPF で解説した記事が、田中達彦さんのブログにありますので、読むことをお勧めします。
今年(2014年)に入って、4Kモニターの販売が始まりました。様々な記事で、4Kモニターの使用記事を読むことができますが、量販店などで販売されるいる PCやタブレットの解像度はフルHD サポートが一般化していると思います。解像度が向上すれば、表現できる領域が増えるので、アプリを作ったりする開発者にとっては良いことなのかも知れません(昔に、仕事でアプリの仕様を決めるのに、1024x768を前提としていたのが嘘のようです)。でも、5年後を想像してみてください。4Kモニターは、現在のところ安いもので10万円を切っていますが、5年後には価格はどうなっているでしょうか。また、ノートPCやタブレットPCの解像度もどうなっているでしょうか? アップルさんのRetainaに代表される高解像度がもたらしたものの一つに、スケーリングへの柔軟な対応というものがあります。これは、iPad Retaina やAndroid タブレットなどで顕著になってきています。これと同じことが、きっと PC というフォームファクターでも起きると私は考えています。皆さんは、どのように考えるでしょうか?先に紹介した田中達彦さんのブログを読めば、高DPI への対応が、以下のようになることが理解できます。

  • Windows Forms などの GDI 系のアプリは、アプリ側での対応が必要になる。
    対応しないと、文字が切れたりなどの表示上の不具合などが起きる可能性が高くなります。
  • WPF は、対応済みである(モニターごとの DPI 設定には、アプリ側の対応が必要)。
  • Windows ストア アプリは、スケーリングにはフル対応している。

ここまでの内容をまとめると、WPF は GPU を活用することを視野に入れて開発された UI 技術であること。そして、高 DPI への対応に迫られる可能性が高いということが言えます(解像度の向上などから避けられなくなっていくことでしょう)。

WPF という UI 技術とは

WPF と Windows Form では、考え方自体が異なります。顕著な例としては、ウィンドウ ハンドル(hWnd)の取り扱いにあると言えます。
hWndWindow

WPF の場合は、Window、もしくは NavigationWindow のみが ウィンドウ ハンドルを持ちます。言い換えると、GDI との接点を持ち、ウィンドウ内のコントロールはウィンドウ ハンドルを持ちません。一方で Windows Formsでは、Form だけでなく全てのコントロールがウィンドウ ハンドルを持ちます。つまり、WPF は、ウィンドウ ハンドルを使用しない方が良いとも言えます。ウィンドウの中に配置されるコントロールも、Windows Forms と同じ名前のものもありますが、異なる名前になっているものもあります。コントロールの対比としては、Windows フォーム コントロールおよび同等の WPF コントロールというドキュメントがあります。また、WPF はトップ レベルのウィンドウ以外のレンダリングには、GPU(DirectX 9以降)を使用します。この関係で、GPU を使用しないソフトウェア レンダリングもサポートしています。ソフトウェア レンダリングは、Windows XP では有効になっています(これは、最初に説明した通りディスプレイ ドライバーがWDDMではないからです)。ソフトウェア レンダリングか GPU レンダリングかは、WPFのランタイムが自動的に決定します。レンダリングを意識的に設定するには、レジストリを変更する必要があります。
Windows Vista 以降の WDDM 対応のドライバーでは、WPF は GPU レンダリングが行われることから、すべてのケースではないにしろ高速にレンダリングされる可能性が高くなります。これらの描画の違いから、良く言われるのが GDI と違い過ぎるという話です。

  • GDI 系では、同期的に描画される。
  • WPFでは、コンストラクター、Loaded の順に呼び出される。
  • Loaded は、自分自身のみで内部のコントロールが描画されているとは限らない。
    たとえば、Window_Loadedイベントで、Window内部に配置したコントロールが描画されているとは限らない。

これは、WPF の特徴的なもので、コンストラクターが呼び出されたことでクラスのインスタンスは作成されますが、実際の描画がされたことを意味しません。また、内部に配置したコントロールが描画されたことも意味しません。一方で、Windows FormsなどのForm_Loadedイベントであれば全てのコントロールが描画されたと考えても問題はありません。これは、WPFがオブジェクト インスタンスを作成してから、描画をエンジンへ依頼してエンジンが実際のレンダリングを行うまでの差と考えても良いものになります。このことから、描画を含めるとタイミングを取るのが難しいので、どうしたら良いでしょうかという質問を昔から頂きます。この答えは、Loadedイベントを待ってから、処理を続けるようにして下さいというものになります(たとえば、Silverlightではこの理由からJS側でLoadedを処理してから、フォーカスを移動するなどをしていました)。
ここまでの説明を読むと、WPF って何か面臭そうという認識を持った人もいらっしゃることでしょう。面倒臭さを乗り越えてまで、利用する価値があるのかなという疑問です。多分、使用する人によって感じるメリットは違うと思いますが、私が考えるメリットは以下のようなものになります。

  • 柔軟なレイアウト システム。
  • UI 定義とコードを完全に分離できる。
  • 柔軟なコントロールのコンテンツ モデル
  • UI オブジェクトの永続化。
  • アニメーションのサポート。
  • などなど。

柔軟なレイアウト システム

Windows Formsに代表される GDI 技術は、良くも悪くも座標位置を固定します。つまり、ウィンドウ サイズが変化したときに表示位置を調整しようとすれば、何らかのコードが必要になります。このことは、DPI設定が変更されると表示位置が崩れる原因になります。このため、WPFではフレキシブル レイアウトと固定レイアウトという概念が導入されています。フレキシブル レイアウトで��、利用するパネル コントロールによって、表示位置を柔軟に調整するようになっています。この顕著な例として、Width とHeight プロパティに「Auto」という値を設定できることを示します。Autoは何かと言えば、コントロールに設定した内容に応じてWidthやHeightを自動的に調整するというものです。これが意味することは、Width や Height プロパティに設定した値は、描画後に変化しないことを意味します。この理由から、ドキュメントで Width や Height を推奨値という表現をしており、コントロールの実際の大きさを取得するには ActualWidth と ActualHeight プロパティを使用します。一方で、Windows Formsでは、Size.Width や Size.Height プロパティは常に固定となります。また、コントロールの表示位置を表す左上の座標は、Windows Forms では Location.X や Location.Y プロパティとなりますが、WPF では利用する パネル コントロールによって異なることになります。たとえば、Windows Forms のように固定座標にするために Canvas コントロールを利用すると、Canvas.Top と Canvas.Left 添付プロパティになり、Grid コントロールを利用する場合は、Margin プロパティになります。このような違いは、フレキシブル レイアウトか固定レイアウトのどちらを採用するかによって変化します。それでも、解像度が向上すればするほど、フレキシブル レイアウトを採用した方がユーザーにとって便利だと私は考えます。なぜなら、ユーザーにとって使い易いサイズとは、ユーザーごとに違うものだからです。

UI 定義とコードを完全に分離できる

パーシャル クラスが提供されるまで、Windows Formsでは UI 定義とイベント コードなどを分離することはできませんでした。WPF では、UI 定義を XAML という XML で定義し、イベント コードはコード ファイルに記述することで、完全に分離されています。このことは、UI 定義用の XAML のみをデータ ファイルとして提供できることも意味します。つまり、プログラムを再ビルドすることなく、XAMLというデータ ファイルを変更することで UI レイアウトを変更できるプログラムを容易に実現すできることになります。また、UI 定義を XAML という XML 形式で記述できるということは、WPF のオブジェクトは XAML との間で容易にシリアライズできることを意味しています。Windows Forms のコントロールは、シリアライズが容易にできないので、シリアライズしようと考えるのであれば、独自に作りこむ必要があります。一方で、XAML が記述するのが面倒という話を良く聞きます。これは、XAML を使ったデザイナー サポート機能が Visual Studio で貧弱だったことが一因です。
XAML のデザイナー サポートは、Visual Studio 2008 から始まりました(ベータという意味では、Visual Studio 2005からです)。この当時は、XAMLによるデザイン ツールとして Expression Blendというデザイナー向け製品がありました。そして、Visual Studio 2010のデザイナーが向上したと言っても、Expression Blend を使った方が柔軟なデザインができることには変更はありませんでした。これが、Visual Studio 2012になると、Expression Blend との統合が行われて、Visual Studio 2012 にExpression Blend が同梱されるようになりました(WPF で Blendが使用できるには、Visual Studio Update を待つ必要がありましたが)。つまり、Visual Studio 2012からは、XAML を編集するのに Expression Blend という製品を購入する必要がなくなったのです。Visual Studio 2012 への Expression Blend の統合によって、柔軟な XAML デザインをできるようになっていますし、XAML を使ったユーザー コントロールの作成などは Expression Blend を使うことで GUI のみでできるようになりました。
追記:実は、Visual Studio 2010から IDE 自体が WPF化されています。Visual Studio 2012では、IDEの動きが Visual Studio 2010 よりも高速化されています。Visual Studio という IDEにおいては、Visual Studio 2008 から Managed Package という機能拡張がなされていまして、WPF などとの統合がしやすい環境が整備されてきています。つまり、Visual Studio 2012/2013 が高速に動作するという事実からも、WPFが遅い描画技術ではないことも理解して頂けることでしょう。

柔軟なコントロールのコンテンツ モデル

Windows Forms のコントロールが持つプロパティとの大きな違いとして、WPF コントロールが持つコンテンツ モデルがあります。たとえば、Windows Forms の Form コントロールでは疑似的にコントロール 配列を扱うために Controls プロパティを持っています。これが、WPF では Window オブジェクトでは Content プロパティであり、Canvas や Grid コントロールでは Children プロパティになっています。Content プロパティは、1つの子要素(コントロール)を意味しており、Children プロパティがコントロール コレクションを意味します。これらを総称して、コンテンツ モデルと呼んでいます。XAML で Content と言った場合に設定できるものは、Object 型のオブジェクトになります。つまり、パネル コントロールでも良いし、Image オブジェクトでも良いし、要はどんなオブジェクトでも設定できることを意味します。たとえば、Windows Forms の Button コントロールは Image プロパティを使ってイメージを設定できますが、WPF の Button コントロールは Content プロパティしかありませんから、Content に Image オブジェクトを設定すれば同じことができますし、パネル コントロールを使えば Imageだけでなく、様々なコントロールをボタンの中に設定することも可能になります。
また、XAMLには強力なスタイル機能があり、スタイルを切り替えるだけでコントロールの見え方を変更することが可能になっています。Windows Forms でユーザー コントロールを使ってオーナー ドローを使ってオリジナルの外観を持つコントロールを作る必要性は、WPFにはありません。

UI オブジェクトの永続化

既に説明したように WPF では、UI 定義を XAML という XML 形式で記述します。この XML 形式が、WPF オブジェクトのシリアライズを行う形式ともなっています。ユーザー コントロールやカスタム コントロールを容易に作成できるようにするために、WPF では依存関係プロパティ(Dependency Property)システムが用意されています。これは、.NET Framework のリフレクションだと、高価な実行コストがかかりますが、WPF に最適化した形でリフレクションを実現する仕組みと考えれば良いでしょう。独自に作成したプロパティを依存関係プロパティとすることで、容易に XAML という XML 形式に表現できるようになっています。この依存関係プロパティを使って、添付プロパティも作成されており、添付プロパティを使ってビヘイビアという使い方(コーディング レスで振る舞いを定義します)もできるようになっています。

アニメーションのサポート

提供するアプリが複雑になればなるほど、アプリの使い方をユーザーが気付くようにすることは重要です。このような用途で、アニメーションを利用することができます。WPF で用意されているアニメーションは、アニメーションを実現するためだけのメカニズムであって、組み込みで用意されている アニメーション ライブラリは非常に限定的となっています。この理由は、WPF が提供された当時では、ユーザー エクスペリエンスにおける統一的なアニメーションの解釈方法が決まっていなかったからです。

他にも XAML ならではの機能として、データ バインディングがあります。XAML のデータ バインディングには、次の3種類があります。

  • 一回限り
    バインディングしてから、データの変更は起きない場合
  • 一方向
    データからコントロールへの一方向
  • 双方向
    データとコントロールの双方向であり、コントロールがフォーカスを失うとデータが変更される

このような強力なデータ バインディングが用意されており、データ バインディングをより有効に活用するためのデザイン パターンとしてMVVM(Model View ViewModel) などの使用が推奨されています。MVVMの使用は必須というわけではありませんが、利用できるのであればデータバインディングを有効活用するためにも利用するようにした方が良いでしょう。

サード パーティ製のコントロール

WPF が提供された当時は、サード パーティ製のコントロールも貧弱でした。でも、現在はサード パーティ製のコントロールも充実しています。

WPF コントロールのコンテンツ モデルは、とても強力です。ですから、自分でカスタム コントロールを作成することもできます。独自にコントロールを作成する場合の学習には、WPF Toolkitが役立つことでしょう。WPF Toolkit は、マイクロソフトが提供している オープン ソースのソフトウェアで、WPF が提供された当時のコントロール不足を補う目的とカスタム コントロールを作り方のヒントを提供するためのものです。説明した市販のコントロールは、日本で代表的なものだけとなります。海外で販売されているものも含めれば、Windows Forms と遜色がないサード パーティ製のコントロールが市販されています。サード パーティ製のコントロールを使用するかどうかは、カスタム コントロールを作成する労力とコストを比較して考えれば良いでしょう。

 

GDI技術とは全くことなる技術である WPF を今から学習するには、どうしたら良いでしょうか。私の場合は、.NET Framework 3.0 の頃から使用していますし、もう覚えてしまっているので、これから学習する人にどのように説明したら良いのかを考えてみたいと思います。私が、最初にお勧めしたいのは「プログラミング Windows 第6版」という書籍です。
pgm_windows6_J

この書籍は、俗に言うペゾルド本であり、Windows プログラミングを学ぶ時の王道のような書籍です。私も、Windows 3.1の頃に読んで GDI を使った Windows プログラミングを勉強しました。昨年に発売した第6版は、Windows ストア アプリ、つまり Windows Runtime と XAML になっています。Windows Runtime XAML(以下、WinRT XAML と略します)は、WPF の XAML とは微妙に異なる箇所がありますが、XAML としての文法などを学習するには、きっと役立つことでしょう。

最後に、XAML による Presentation 技術で私が良く聞かれる質問として、「なぜ、同じ XAML なのに挙動が違うのですか?」というものがあります。この答えは、マイクロソフトは複数の XAML 用のレンダリング エンジンを持っているからですというものになります。具体的には、WPF、Silverlight、Windows Phone、Windows ストア アプリという4種類のレンダリング エンジンがあり、フル機能は WPF ですが、Silverlight の発展形として Windows Phone と Windows ストア アプリがあり、様々なフィードバックから一部の機能は Silverlight、Windows Phone、Windows ストア アプリの方が WPF よりも使い易くなっていることから、構文を含めて微妙な違いを生んでいます。どの XAML レンダリング 技術を使ったとしても、完璧ということはなく、用途によっては GDI の方がパフォーマンスが良くなる場合もあることでしょう。WPF に代表される XAML という Presentation 技術は、2D を含めてビデオやドキュメントなどの様々なメディアを統合してユーザー体験を向上させることを目的にしています。3D を使ったパフォーマンスが良いアプリを作成するのであれば、DirectX を使用した方が良いのは自明のことです。あくまでも、ユーザー体験を実現するための技術が、XAML という Presentation 技術の本質となります。

プログラミング Windows 第6版 第1章 WPF編

$
0
0

前回のエントリーで、WPF を学習するのに「プログラミング Windows 第6版」という書籍を紹介しました。この書籍は、Windows ストア アプリを中心にしている関係で、WinRT XAML という WPF XAML のサブセットを使った説明となっています。したがって、WPF XAML とは色々な面で異なる箇所があります。この異なる差異を乗り越えて WPF を学習するために、サンプルを移植しながら解説を試みるのが今回のテーマとなります。目標は、プログラミング Windows 第6版 の上巻をカバーすることで、下巻は自分で WPF に置き換えて学習ができるようになることです。このように考えていますので、以下のようなエントリーを考えています。

  1. XAML とは何か
  2. 第1章 マークアップとコード
  3. 第2章 XAML 構文
  4. 第3章 基本的なイベント処理
  5. 第4章 パネルを使った表示
  6. 第5章 コントロールとのやりとり
  7. 第6章 WinRT と MVVM
  8. 第7章 非同期性
  9. 第8章 アプリ バーとポップアップ
  10. 第9章 アニメーション
  11. 第10章 座標変換
  12. 第11章 3つのテンプレート
  13. 第12章 ページとナビゲーション

利用するサンプルは、以下で公開されています。

今回の記事は、私がプログラミング Windows 第6版 の Windows 8.1 対応作業を実施した時に考えていた目的でもあります。WPF XAML を学習する書籍などで、最新環境(.NET Framework 4.5x)に対応した日本語の書籍がほぼ無い状況だったことから、Windows プログラミングを学ぶ上での王道とも言える書籍が WinRT XAML に対応したことが理由です(個人的には、時代の変遷を感じています)。せっかくなので、WPF XAML の学習に利用できたらと考えていました。

第1章 マークアップとコード

1.1(P3) 最初のプロジェクト

WPF アプリケーションを作成するには、Visual Studio 2013 を起動してから、[新しいプロジェクト]-[Visual C#]-[Windows デスクトップ]-[WPF アプリケーション] を選択します。
NewProjectWizard
※プロジェクト テンプレートには、「WPF アプリケーション」の他に「WPF カスタム コントロール ライブラリ」「WPF ユーザー コントロール ライブラリ」があります。

書籍では、Hello プロジェクトを作成し、プロジェクトの構造とコード、および XAML の説明が続きますので、WPF で作成した場合の基本的な要素を説明します。最初に、ソリューション エクスプローラーの状態を示します。
Ch01 Solution Explorer
Windows ストア アプリとの違いは、「Assetsフォルダー」、「Common フォルダー」、「Hello_Temporarykey.pfx」および「Package.appxmanifest」が無いことになります。また、参照設定の内容が大きく異なっています。具体的には、WPF では次の3 種類のアセンブリが参照設定されています。

  • PresentationCore.dll
  • PresentationFramework.dll
  • WindowsBase.dll

これは、Windows Forms アプリケーションが「System.Windows.Forms.dll」と「System.Drawing.dll」へ参照を持つことと同じで、WPF にとって必要なものだと理解すれば良いでしょう。

それでは、MainWindow.xaml.cs(WinRT XAMLでは MainPage.xaml.csです) のコードを次に示します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace Hello
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

 

WinRT XAML との大きな違いは、MainWindow クラスが Window クラスを継承しているところになります。WinRT XAML では、Window クラスを提供しておらず、Page クラスを使用するのが普通だからです。一方で、WPF ではナビゲーションを使用する場合のみに Page クラスを使用するだけで(具体的に第11章で説明する予定です)、基本になるウィンドウとして Window クラスを使用します。名前空間は、WinRT と違って System で始まっています。たとえば、System.Windows、System.Windows.Controls などになります。また、MainWindow クラスに partial キーワードが付与されている点に注意してください。これは、MainWindow.xaml がビルド時にパーシャル クラスである MainWindowのコードを生成し、生成したコードの中に InitializeComponent メソッドが含まれていることを意味するからです。

今度は、MainWindow.xaml を示します。

<Window x:Class="Hello.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Hello" Height="350" Width="525" WindowState="Maximized"><Grid Background="Black"><TextBlock Text="Hello, Windows 8!"
                   FontFamily="Times New Roman"
                   FontSize="96"
                   FontStyle='Italic'
                   Foreground='Yellow'
                   HorizontalAlignment='Center'
                   VerticalAlignment='Center' /></Grid></Window>

 

ルート要素が、WinRT XAML と異なり Window になっています。また、WindowsState プロパティ(属性)に「Maximized(最大化)」が設定されている点にもご注意ください。これは、Windows ストア アプリが常に全画面であるのに対して、WPF は任意のウィンドウ サイズを設定できることから、意図的にフルスクリーンに設定しています。これ以外には、Grid 要素の Background属性を静的リソースから「Black」に変更しています。これは、WPF は WinRT XAML と違って標準でスタイル シートを組み込まないためです。次に XAML 構文で WinRT XAML との大きな違いは、名前空間の宣言方法になります(MainWindow.xaml には示していません)。

  • WinRT XAMLでは、「xmlns:local=”using Hello” 」
  • WPF XAML では、「xmlns:local=”clr-namespace:Hello”」
    より正確には、「xmlns:プレフィックス=”clr-namespace:使用する名前空間;assembly=アセンブリ名”」と指定します。

上記で示したように、自分で定義したクラスなどを使用する場合の名前空間の指定方法に大きな違いがあります。今度は、Windows Forms と比較したものを示します。

  • Form クラスに相当するのが、Window クラス
    Controls コレクション に相当するプロパティを Window クラスは持ちません。
    Text プロパティに相当するのが、Title プロパティになります。
  • Label クラスに相当するのが、TextBlock クラス
  • Grid クラスに相当するものは、Windows Forms にはありません。
    Controls コレクションに相当するプロパティとして Children があります。

次に、App.xaml のプロパティを示します。
Ch01 App.xaml Property

注目して欲しいのが、ビルド アクションが「ApplicationDefinition」になっていることです。次に、App.xaml.cs を示します。

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;

namespace Hello
{
    /// <summary>
    /// App.xaml の相互作用ロジック
    /// </summary>
    public partial class App : Application
    {
    }
}

 

App クラスは、Application クラスを継承するだけで、コード自体は何も記述されていません。実は、App クラスが WPF アプリケーションのエントリー ポイントになっています。一般的にエントリー ポイントであれば、Main メソッドが存在します。実は、App.xamlのビルド アクションが「ApplicationDefinition」になっているのは、ビルド時にMainメソッドを自動生成するようになっているためなのです。エントリー ポイントで何らかの処理を行うのであれば、ビルド アクションを「Page」に変更して、自分で Main メソッドを定義する必要があります。これは、第1章の最後で説明します。Hello プロジェクトの実行結果を示します。
Hello
ウィンドウの形式を除けば、WinRT XAML と同じ結果が得られました。ここまでの説明で、WinRT XAML との大きな違いを以下の点に集約できると思います。

  • Window クラスを基本とする
  • 名前空間の記述方法が異なる
  • ウィンドウ サイズは、自分で設定する
  • 参照するアセンブリが異なる

今度は、Windows Forms の Form クラスと WPF のWindow クラス のサイズ関係の違いを次に示します。

 FormWindow説明
Width幅(Size)描画時の推奨値描画時に指定する幅で、描画後には変化しません。
Height高さ(Size)描画時の推奨値描画時に指定する幅で、描画後には変化しません。
ActualWidth無し実際の幅描画された大きさです。
ActualHeight無し実際の高さ描画された大きさです。

さらに、Window クラスには「SizeToContent」プロパティがあり、これを「WidthAndHeight」に設定することでWindow 内部に配置したコントロールに応じてウィンドウ サイズを決定することができるようになっています。また、WPF において Windows Forms と決定的に異なるのは座標の取り扱いにあります。

  • Windows Forms は、ピクセル座標を扱います。
  • WPFは、論理座標(1/96 DPI)を扱います。

たとえば、Width や Height プロパティには「Auto」という値を設定することもできます。これは、設定されたコントロールが持つコンテンツに応じて描画時の推奨サイズを決定してくださいという値になります。つまり、WPF では Width や Height を使ってレンダリング エンジンに対して描画して欲しいサイズを伝えることはできますが、最終的に描画されるサイズはコントロールが配置されたパネル コントロールなどによって変化することを意味します。よって、最終的に描画されたサイズを取得するには ActualWidth やActualHeight プロパティを使用するしかないことになります。これは、Windows Forms などの GDI 技術と大きく違う点になりますし、論理座標を使うことで 高DPI などに対応するスケーリングのメリットを受けられることに他なりません。

この節の最後では、ビルドした結果の生成物の違いを少しだけ説明します。WPF XAMLでは、ビルド結果として XAML が BAML という埋め込みリソースとして作成されます。BAMLは、XAMLをビルドして作成されたバイナリーになり、起動時間の短縮などを目的としています。一方で、WinRT XAML では、ビルド結果として XAML が XBF というパッケージ コンテンツ(Windows 8.1)になります。XBFは、BAML と同じ目的でWindows ストア アプリの起動時間などの短縮を目的としています。

1.2(P10) グラフィカルなプロジェクト:XAML

HelloImage プロジェクトの MainWindow.xaml の抜粋を以下に示します。

<Grid Background="Black"><Image Source="http://www.charlespetzold.com/pw6/PetzoldJersey.jpg" /></Grid>

 

XAML そのものは、WinRT XAML と同じになります。もちろん、すでに説明したように Page クラスを Window クラスに置き換えています。逆に、Windows Forms の PictureBox クラスが Image クラスに置き換わります。さらに、プロパティの構造も大きく異なります。たとえば、PictureBox では ImageLocation とImage プロパティがありますが、Image は Source プロパティのみになります。また、SizeMode プロパティに相当するものとして、Stretch プロパティがあります。Stretch プロパティは、書籍に説明がありますので内容をご確認ください。HelloImage の実行結果を次に示します。
HelloImage

次にローカル リソースである画像を使ったHelloLocalImage プロジェクトを説明します。 WPF でプロジェクト内に含める画像などのアセットは、2 種類の扱い方があります。1 つは、プロジェクトのリソースとする方法と、もう1つは WinRT XAML で説明しているコンテンツにする方法です。WPF では、コンテンツとは、ローカルに配置したファイルへアクセスすることと同じ意味になりますので、ここではリソースに設定しているプロパティを示します。
Ch01 LocalImage Property
プロジェクトに画像を配置して、ビルド アクションを「Resource」に設定するだけになります。こうすることで、出力するアセンブリ内に画像が埋め込まれることになります。この時の、MainWindow.xaml の抜粋を示します。

<Grid Background="Black"><Image Source="Images/Greeting.png"
           Stretch="None" /></Grid>


Source プロパティに URL を指定した時と同じように、埋め込みリソースを記述することもできます。リソースなどを指定する URI の詳細については、リソース(WPF)を参照してください。

1.3(P14) テキストのアレンジ

この節には、WrappedText、OverlappedStackedText、InternatinalHelloWorld プロジェクトがありますが、すでに説明した内容(Page を Windowへ、組み込みのスタイルを個別指定へ)で読み替えることで問題なく WPF に置き換えることができます。例として、実行結果だけを示します。
WrappedTextOverlappedStackedTextInternationalHelloWorld

1.4(P23) メディアを使ったプロジェクト

この節には、HelloAudio、HelloVideo プロジェクトがありますが、すでに説明した内容(Page を Windowへ、組み込みのスタイルを個別指定へ)で読み替えることで問題なく WPF に置き換えることができます。WinRT XAML の MediaElement クラスには、Windows 8.1 よりAreTransportControlsEnabledプロパティが追加されています。このプロパティは、WPF XAML はサポートしていませんので注意してください。

Windows Forms では、ビデオやオーディオを統合するには工夫をする必要があります。一方で、XAML 環境では MediaElement クラスを使うことで、アプリケーションに統合することが簡単にできます。

1.5(P25) コードを使ったプロジェクト

XAML で UI を定義することと同じことが、コードだけでも実現することができます。最初に HelloCode プロジェクトの MainWindow.xaml.cs(MainPage.xaml.cs)の抜粋を示します。

public MainWindow()
{
    InitializeComponent();

    TextBlock txtblk = new TextBlock();
    txtblk.Text = "Hello, Windows 8!";
    txtblk.FontFamily = new FontFamily("Times New Roman");
    txtblk.FontSize = 96;
    txtblk.FontStyle = FontStyles.Italic; // FontStyle.Italic を変更
    txtblk.Foreground = new SolidColorBrush(Colors.Yellow);
    txtblk.HorizontalAlignment = HorizontalAlignment.Center;
    txtblk.VerticalAlignment = VerticalAlignment.Center;

    contentGrid.Children.Add(txtblk);

}

 

WinRT XAML との違いは、FontStyle プロパティに設定して値を変更したことです。WinRT XAMLでは、FontStyle列挙になるりますが、WPF では FontStyles 列挙となります。Windows Forms では、Font.Style プロパティを使用しますが、WPF では Font プロパティではなく、FontFamily、FontSize、FontStyle プロパティが用意されており、しかも、System.Drawing.FontStyle 列挙ではなく、System.Windows.FontStyles 列挙となります。これは、WPF が System.Drawing 名前空間を使用するのではなく、WPF 独自の名前空間を使用しているからです。クラス名や列挙名だけを比べると同じ名前が WPF にありますが、実態は別の名前空間で定義されていることがありますので、ご注意ください。

1.6(P28) グラフィカルなプロジェクト:コード

この節には、HelloImageCode、HelloLocalImageCode プロジェクトがあります。基本的には、すでに説明した内容(Page を Windowへ、組み込みのスタイルを個別指定へ)で読み替えることで問題なく WPF に置き換えることができます。しかしながら、HelloLocalImageCode プロジェクトでは、イメージへのパスを書き換える必要がありますので、MainWindow.xaml.cs の抜粋を示します。

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

        // パスを変更 ms-appx:///Images/Greeting.png
        image.Source = new BitmapImage(new Uri("Images/Greeting.png", UriKind.Relative));
    }
}

 

これは、WinRT XAML と WPF XAML におけるリソースの取り扱い方による違いになります。WinRT XAML プロジェクトでのイメージはビルド アクションがコンテンツに指定されており、アプリ パッケージ内のリソースとなっていることから、「ms-appx:///」プレフィックスを使用しています。これに対して、WPF XAML プロジェクトではHelloLocalImage プロジェクトと同じように埋め込みリソースにしています。このため、XAML に記述するとのと同じパス表記をすることができています。

1.7(P31) ページですらない

WinRT XAMLでは、Page クラスが基本ですが、WPF では Window クラスを基本にするという説明をすでにしました。このことをコードのみでページを作って説明するというのが、この節の主題になっています。ここでは、App.xaml.cs を示します。

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace StrippedDownHello
{
    /// <summary>
    /// App.xaml の相互作用ロジック
    /// </summary>
    public partial class App : Application
    {
        [STAThreadAttribute()]
        public static void Main(string[] args)
        {
            var app = new App();
            TextBlock txtblk = new TextBlock
            {
                Text = "Stripped-Down Windows 8",
                FontFamily = new FontFamily("Lucida sans Typewriter"),
                FontSize = 96,
                Foreground = new SolidColorBrush(Colors.Red),
                HorizontalAlignment = HorizontalAlignment.Center,
                VerticalAlignment = VerticalAlignment.Center
            };
            var window = new Window
            {
                Title = "StrippedDownHello",
                WindowState = WindowState.Maximized,
                Content = txtblk
            };

            app.Run(window);
        }
    }
}

 

すでに説明してきた考え方をコードで示しています。このコードは、次のようなことを行っています。

  • App クラスのインスタンスを作成。
  • TextBlock クラスのインスタンスを作成。
  • Window クラスのインスタンスを作成し、Content プロパティに TextBlock のインスタンスを設定。
  • App クラスのRun メソッドに Window クラスのインスタンスを引数として渡す。

こうすることで、UI 定義の XAML がないアプリケーションを作成することができます。今度は、このようなプロジェクトの作成手順を示します。

  • 新しいプロジェクトで、WPF アプリケーション テンプレートを選択してプロジェクトを作成します。
  • MainWindow.xaml を削除します。
  • App.xaml のプロパティでビルド アクションを「ApplicationDefinition」から「Page」に変更します。
  • App.xaml を開いて、StartupUri プロパティを削除します。
  • App.xaml.cs へ Main メソッドの定義と必要なコードを記述します。

このようにすることで、アプリケーションのエントリー ポイントである Main メソッドをビルド タスクに任せることなく自分で定義できるようになります。この方法を使えば、Window を表示する前に何らかの処理を行うことができるようになります。

ここまで説明してた違いを意識しながら、第1章を読むことで WPF にも書籍の内容を応用することができるようになることでしょう。

プログラミング Windows 第6版 第2章 WPF編

$
0
0

この記事では、「プログラミング Windows 第6版」を使って WPF XAML の学習を支援することを目的にしています。この目的から、書籍と併せて読まれることをお勧めします。

最初に、WPF アプリケーションを開発する上で Visual Studio 2013 で設定しておいた方が良いトピックを紹介します。 Visual Studio 2013 を起動して、[ツール]-[外部ツール] を使って Blend for Visual Studio からプロジェクトを開けるように設定します。設定方法は、以下のようにします。

  • コマンド:C:\Program Files (x86)\Microsoft Visual Studio 12.0\Blend\Blend.exe
  • 引数:$(SolutionDir)$(SolutionFileName)
  • 初期ディレクトリー:$(SolutionDir)

Ch02 VS External Tools
このように設定しておくことで、Visual Studio で開いているプロジェクトをいつでも Blend で開けるようになります。Windows ストア アプリ プロジェクトの場合は、ソリューション エクスプローラーのコンテキスト メニューから Blend で開けるようになっていますが、 WPF アプリケーション プロジェクトでは Blend で開くという コンテキスト メニューが表示されないための対策です。

第2章 XAML 構文

2.1(P33) グラデーションブラシ:コード

この節で説明している GradientBrushCode サンプルは、第1章で説明したようにPage を Windowへ、組み込みのスタイルを個別指定へと読み替えることで WPF へと置き換えることができます。次に実行結果を示します。
GradientBrushCode
WinRT XAML との大きな違いは、名前空間を読み替えることです。WinRT XAML は Windows.UI.Xaml で始まる名前空間で定義をしており、 WPF は System.Windows で始まる名前空間で定義しています。したがって、SolidColorBrush などのブラシは、System.Windows.Media 名前空間で定義されており、WebView クラスは WPF では WebBrowser クラスとなり System.Windows.Controls 名前空間で定義されているという形式になります。 WebViewBrush 相当のクラスは、WPF では定義されていませんので、ご注意ください。

2.2(P36) プロパティ要素構文

この節では、XAML におけるプロパティの記述方法を説明しています。基本的な記述方法は、「<TextBlock Background="Red" />」のように XML の属性を使用します。属性に使用するのは、単一の値と覚えておけば良いでしょう。例として記述した Background プロパティを明示的に SolidColorBrush オブジェクトとして定義するには、次のように記述します。

<TextBlock><TextBlock.Backgound><SolidColorBrush Color="Red" /></TextBlock.Backgound></TextBlock>

 

「<要素名.プロパティ名>」タグを使うことで��新しいタグを使ってプロパティを設定できるようになります。XML の属性を使った記述方法でも、リソースやバインディング構文を使うことで他のオブジェクトなどを設定することもできますが、これらの記述方法は本書を読み進めていくことで理解できるようになります。

2.3(P40) コンテント プロパティ

この節では、最初に Content プロパティの説明が行われています。Windows Forms と考え方が大きく違う点でもあるので、具体的に説明します。WPF のコンテンツ モデルは、MSDN にドキュメントがありますので参照してください。

コントロール名プロパティ名説明
ContentControlContent1つの任意のオブジェクトを設定するすることができます。
たとえば、Window クラスは Contentを持ちますし、Button コントロールもContent を持ちます。よって、Button.Content="ボタン"とすれば文字列オブジェクトを設定したので、ボタンに文字列が表示されるようになります。つまり、Windows Forms のButton.Text プロパティや Image プロパティに相当することになります。
ItemsControlChildrenオブジェクトのコレクションを設定することができます。
たとえば、Grid クラス、Canvasクラス、StackPanel クラスなどは Children を持ちます。このプロパティが、Windows Forms の Form.Controls プロパティに相当します。

これらのことを踏まえて書籍を読めば良いでしょう。この節で使用している GradientBrushMarkup サンプルも、第1章で説明したようにPage を Windowへ、組み込みのスタイルを個別指定へと読み替えることで WPF へと置き換えることができます。

2.4(P44) TextBlock のコンテント プロパティ

この節では、TextBlock クラスの Content プロパティを詳細に説明しています。Windows Forms では Label クラスに相当すると説明しましたが、この説明はとても単純化したものになります。より正確に表現するのであれば、Windows Forms のRitchTextBox クラスを読み取り専用にしたようなクラスと考えるのが妥当になります。なぜなら、Inline オブジェクトのコレクションを持っており、設定するテキストに対する装飾も行うことができるからです。もちろん、WPF にも RitchTextBox クラスは存在します。

この節で使用している TextFormatting サンプルも、第1章で説明したようにPage を Windowへ、組み込みのスタイルを個別指定へと読み替えることで WPF へと置き換えることができます。次に実行例を示します。
TextFormatting

これが、TextBlock クラスで表現できています。Windows Forms の Label クラスで同じことをしようとすれば、手間がかかることを容易に想像できることでしょう。このことは、XAML という UI 技術の豊かな表現力の一端を示しています。

2.5(P47) ブラシの共有

この節では、XAML におけるリソースの考え方を説明しています。XAML のリソース システムは、柔軟であり、XML のツリー構造を利用したスコープの概念も備えています。
Ch02 XAML Resource
図に示したオブジェクトごとにリソースを定義することができます。リソースを定義したオブジェクトによって、 そのオブジェクトより下の階層(Content やChildren)がリソースを参照することができるようになります(スコープ)。つまり、アプリケーション全体で使用するリソースであれば、App.xaml で定義すれば良いということを意味します。

それでは、SharedBrush プロジェクトのMainWindow.xaml の抜粋を示します。

<Window ...
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:clr="clr-namespace:System;assembly=mscorlib"
        ...><Window.Resources><clr:String x:Key="appName">Shared Brush App</clr:String><LinearGradientBrush x:Key="rainbowBrush"><GradientStop Offset="0" Color="Red" /><GradientStop Offset="0.17" Color="Orange" /><GradientStop Offset="0.33" Color="Yellow" /><GradientStop Offset="0.5" Color="Green" /><GradientStop Offset="0.67" Color="Blue" /><GradientStop Offset="0.83" Color="Indigo" /><GradientStop Offset="1" Color="Violet" /></LinearGradientBrush><FontFamily x:Key="fontFamily">Times New Roman</FontFamily><clr:Double x:Key="fontSize">96</clr:Double></Window.Resources>

    ...

</Window>

 

すでに説明した Page を Windows クラスへ変更しただけでなく、この変更に伴って WinRT XAML の 「<Page.Resources>」というリソース定義を「<Window.Resources>」に変更しています。また、名前空間に「xmlns:clr="clr-namespace:System;assembly=mscorlib"」を追加している点も注目してください。この名前空間の追加に伴って、WinRT XAML の「<x:String x:Key="appName">」定義を「<clr:String x:Key="appName">」へと変更しています。

WinRT XAML における「x:String」という型が何なのかというところから説明します。この記法は、「x:型」という記法で共通の XAML 言語プリミティブの組み込み型で説明されています。このドキュメントで、x:String を調べると XAML 2009の言語プリミティブで定義されていると記述されていますが、同じドキュメント上で WPF のサポートにおいては「マークアップ コンパイルされない XAML のみのサポート」とあります。つまり、コンパイルする WPF アプリケーションでは未サポートということになります。これが、名前空間として「xmlns:clr」を追加した理由になります。一方で、WinRT XAML ではx:Stringの記法が許可されていることから、XAML 2009 言語プリミティブをサポートしているように見えますが、実は WinRT XAML だけの限定的なサポートであることが、XAML 名前空間(x:)の言語機能というドキュメントに記載されています。要は、WPF XAML をリリースしてから、Silverlight や Windows Phone などの経験からプログラミングとして必要と思われる言語機能だけを XAML 2009 より WinRT XAML で実装したということになります。これなどは、WPF XAML と互換性を持たない機能になりますので、「x:Type」という記述を見つけたら読み替えないといけないということになるのです。

ここまで説明したこと以外は、SharedBrush プロジェクトのMainWindow.xaml は同じですから次に実行結果を示します。
SharedBrush

この節で補足するとすれば、スタイル リソースとテーマ リソースということになります。WinRT XAML では、generic.xaml(Windows 8 プロジェクトでは StandardStyles.xaml) というスタイル シートが Visual Studio 2013 の Windows 8.1 用の Windows ストア アプリ プロジェクトでは、ビルド時に自動的に組み込まれます(Windows 8 プロジェクトでは、プロジェクトに含まれます)。また、利用できる テーマ リソースとして、themeresources.xaml も自動的に組み込まれることになります。これが、RequestedTheme プロパティに指定できるテーマ リソースを定義しています(デフォルトでは、Dark テーマ)。一方で、WPF の場合は標準のスタイル シートの提供はなく、テーマ リソースが提供されているだけになります。含まれているテーマは、以下のアセンブリになります。

  • PresentationFramework.Luna.dll
  • PresentationFramework.Aero.dll
  • presentationframework.aero2.dll
  • presentationframework.aerolite.dll
  • PresentationFramework.Classic.dll
  • PresentationFramework.Royale.dll

これらのアセンブリに対する名前空間を定義することで、テーマ リソースを WPF で利用できるようになります。または、オープン ソースとして公開されているスタイル シートやテーマもありますので、自分が使いたいと思えるスタイル シートやテーマを探すか、作成してもよいでしょう。

2.6(P51) リソースは共有される

この節では、SharedBrush サンプルに対してコードで操作することで、リソースが共有されることを確認しています。この動きは、WPF でも同じなので自分で確認してみてください。

2.7(P52) ベクター グラフィックスの操作

この節で説明している Spiral プロジェクトの MainWindpow.xaml の抜粋を示します。

<Grid><Polyline Name="polyline"
              Stroke="Black"
              StrokeThickness="3"
              HorizontalAlignment="Center"
              VerticalAlignment="Center" /></Grid>

 

ここまで説明してきた通りで、Stroke プロパティに指定していたリソースを「Black」に変更するだけで WPF で動作します。実行結果を示します。
Spiral
この描画されている内容が、ベクター グラフィックスになっている点が、Windows Forms と異なる点になります。Windows Forms で描画した図形は、GDI ベースになっていますからピクセルをベースにしたビットマップ画像であると言っても良いでしょう。ビットマップ画像に対して、WPF の描画ではベクター グラフィックスになります。ベクター グラフィックスのメリットは、拡大や縮小しても画像が滑らかになるという点です。UX の観点から、読み難いとユーザーが感じればピンチ操作などで拡大しようと試みることでしょう。拡大した時に、ドットが表示されてカクカクになったコーナーを目にすることは、ユーザー エクスペリエンスを損ないます。つまり、ベクター グラフィックスの採用そのもの ユーザー エクスペリエンスを向上させる目的を達成するためなのです。

次の StretchedSpiral プロジェクトも Spiral プロジェクトと同じ対応を行うことで、WPF で動作します。実行結果を示します。
StretchedSpiral
Spiral サンプルに Stretch プロパティを設定したのが、StreachedSpiral サンプルですが、このサンプルによってベクター グラフィックスのメリットを確認することができることでしょう。

この節で説明しているサンプルとして、ImageBrushedSpiral、HelloVectorGraphics、HelloVectorGraphicsPath プロジェクトはここまでに説明してきた内容を踏まえて書き換えれば(Page を Windowへ、スタイルを固定値へ)、問題なく WPF で動作します。以下に実行結果を示します。
ImageBrushedSpiralHelloVectorGraphicsHelloVectorGraphicsPath

次に説明している Path マークアップ構文を使った PathMarkupSyntaxCode サンプルでは、ここまでに説明してきた内容に加えてコードを少しだけ書き換える必要がありますので、MainWindow.xaml.cs の抜粋を示します。

Geometry PathMarkupToGeometry(string pathMarkup)
{
    string xaml =
        "<Path " +
        "xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>" +
        "<Path.Data>" + pathMarkup + "</Path.Data></Path>";

    Path path = XamlReader.Parse(xaml) as Path;  // Load を Parseへ

    // Detach the PathGeometry from the Path
    Geometry geometry = path.Data;
    path.Data = null;
    return geometry;
}

 

書き換えたのは、XamlReaderクラスの Load メソッドを Parse メソッドにしたことです。これは、WinRT XAML における XamlReader クラスと WPF の XamlReader クラスの違いによるものです。正確に表現するのであれば、WPF の XamlReader クラスのほうが高機能であり、WinRT XAML の XamlReader は機能を限定していることが原因になります。もちろん、書き換えても実行結果は同じになります。
PathMarkupSyntaxCode

2.8(P62) ViewBox による拡大と縮小

この節で説明している TextStretch、VectorGraphicsStretch サンプルも、ここまでの説明に従って書き換えることで、問題なく WPF で動作します。節のタイトルにある ViewBox コントロールとは、配置したコントロールを自動的に枠内に拡大・縮小することで枠からはみ出さないようにするものになります。
TextStretchVectorGraphicsStretch

2.9(P65) スタイル

この節では、XAML のスタイル定義に関して説明しています。2.5 ブラシの共有で説明した内容に従って、リソース定義などを書き換えるだけで WPF で動作させることができます。書き換えた SharedBrushWithStyle プロジェクトの MainWindow.xaml の抜粋を示します。

<Window.Resources><clr:String x:Key="appName">Shared Brush with Style</clr:String><LinearGradientBrush x:Key="rainbowBrush"><GradientStop Offset="0" Color="Red" /><GradientStop Offset="0.17" Color="Orange" /><GradientStop Offset="0.33" Color="Yellow" /><GradientStop Offset="0.5" Color="Green" /><GradientStop Offset="0.67" Color="Blue" /><GradientStop Offset="0.83" Color="Indigo" /><GradientStop Offset="1" Color="Violet" /></LinearGradientBrush><Style x:Key="rainbowStyle" TargetType="TextBlock"><Setter Property="FontFamily" Value="Times New Roman" /><Setter Property="FontSize" Value="96" /><Setter Property="Foreground" Value="{StaticResource rainbowBrush}" /></Style></Window.Resources>

 

2.5 ブラシの共有と同じように、名前空間で clr を定義することで x:Type という記述を書き換えています。そして、ImplicitStyle サンプルも同様に書き換えることで、WPF で動作させることができます。
SharedBrushWithStyleImplicitStyle

2.10(P71) データ バインディング

この節では、データ バインディングの基本的な記述方法を説明しています。データ バインディングの基本的な記述方法は、WinRT XAML も WPF も同じになります。従って、ここまでに説明したきたように書き換えることで WPF で問題なく動作させることができます(Page を Window へ、リソースの定義方法、x:Type 記述)。特に注意して頂きたいのは、2.5 ブラシの共有で説明した XAML 名前空間(x:) の言語機能になります。これは、WinRT XAML 固有の機能になりますので、WPF では .NET Framework のデータ型を利用しなければならないという点になります。
しかしながら、「Background="{x:Null}"」という null を指定する場合は、WPF も WinRT XAML と同じ指定をすることになります。
SharedBrushWithBinding

ここまで説明してた違いを意識しながら、第2章を読むことで WPF にも書籍の内容を応用することができるようになることでしょう。

プログラミング Windows 第6版 第3章 WPF編

$
0
0

この記事では、「プログラミング Windows 第6版」を使って WPF XAML の学習を支援することを目的にしています。この目的から、書籍と併せて読まれることをお勧めします。

第3章 基本的なイベント処理

3.1(P75) タップ イベント

この節では、タッチ、マウス、ペンによるイベントに基本的な事項を説明しています。特に、タッチ系のイベントに関して WinRT XAML と WPF XAML では異なるので注意が必要です。

  • タッチ、マウス、ペンによる入力は、名前が Pointerで始まる 8 つのイベントにまとめられています(書籍より引用)。
    WPF XAML では、Mouseで始まる 10 個のイベントになっており、Pointer で始まるイベントはありません。
  • 複数の指を使った入力は、名前が Manipulation で始まる 5 つのイベントにまとまれています(書籍より引用)。
    WPF XAML でも基本は同じで Manipulation で始まるイベントになっており、Win RT XAML の 5つに加えて、ManipulationboundaryFeedbackイベントが追加されています。
  • キーボード入力は、WinRT XAML も WPF XAML も同じで名前が Key で始まる 2つのイベントにまとめられています。
  • この他に、Tapped、DoubleTapped、RightTapped、Holding というより高度なイベントがあります(書籍より引用)。
    WPF XAML では、Tapped イベントはサポートされていないので注意が必要です。

WPF XAML のタッチに関するイベントは、名前が Touch で始まる 5 つのイベントにまとめられています(WinRT XAML は、名前が Touch で始まるイベントはありません)。ここで、WinRT XAML と WPF XAML におけるタッチ サポートの違いを理解しておくことが重要です。

  • 複数の指を使ったジェスチャー操作は、WinRT XAML と WPF XAML は 名前が Manipulation で始まるイベントになっています。
  • WinRT XAML でタッチの低レベル操作を扱う時は、名前が Pointer で始まるイベントを使用します(タッチ、マウスで共通です)。
  • WPF XAML でタッチ固有の処理を行う時は、名前が Touch で始まるイベントを使用します。
  • WPF XAML でタッチとマウスを区別しないでポインターに関する操作を行う時は、名前が Mouse で始まるイベントを使用します。
  • タップなどの Windows のタッチ操作を直接的に処理できるのは、WinRT XAML となります。つまり、イベントが用意されているという意味になります。
    WPF XAML では、名前が Touch で始まるイベントを使って自分でタップなどの操作を実現する必要があります。もちろん、タップなどのタッチ 操作を判定するコンポーネントが市販されていれば、そのようなコンポーネントを使っても良いでしょう。

Windows OS は、OS 側の機能として タッチ 入力をマウス入力と見做して処理できるようになっています。これを、タッチ操作として処理するには Windows Touch APIを使用する必要があります。この Windows Touch サポートを盛り込んだのが、WPF XAML におけるタッチ サポートになります。一方で、WinRT XAML は Windows のタッチ操作を提示しているようにタッチ操作を前提に用意された環境だと言えます。
余談になりますが、Windows Touch API サポートは Windows 7 からになります。Windows Vista では、テーブル型のマルチタッチ コンピューターの Surface のための独自拡張になっていました。もちろん、Silverlight もタッチ サポートが入りましたが、名前が Touch で始まるイベントのみのサポートになっており、Windows Phone 向けの Silverlight で DoubleTap のサポートが追加されています。

タッチの違いが理解できれば、TapTextBlock サンプルをどのように書き換えれば良いかが理解できたことでしょう(もちろん、第1章などで説明した、Page をWindows へ、組み込みスタイルの変更なども必要です)。それでは、MainWindow.xaml の抜粋を示します。

<Grid><TextBlock Name="txtblk"
               Text="Touch Text!"
               FontSize="96"
               HorizontalAlignment="Center"
               VerticalAlignment="Center"
               TouchDown="txtblk_TouchDown" /></Grid>

 

TouchDown イベントに書き換えていますが、もちろん TouchUp イベントでも構いません。要は、どのタイミングでコードを実行するかという違いだからです。それでは、MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    Random rand = new Random();
    byte[] rgb = new byte[3];

    public MainWindow()
    {
        InitializeComponent();
    }

    private void txtblk_TouchDown(object sender, TouchEventArgs e)
    {
        rand.NextBytes(rgb);
        Color clr = Color.FromArgb(255, rgb[0], rgb[1], rgb[2]);
        txtblk.Foreground = new SolidColorBrush(clr);
    }
}

 

3.2(P79) ルーティング イベントの処理

この節では、ルーティング イベントを説明しています。ルーティング イベントは、WPF XAML でも同じ機能となります。一方で Windows Forms には、無い考え方になります。あえて Windows Forms に当てはめるのであれば、たとえば KeyDown イベントに対する PreviewKeyDown イベントが全てのイベントでサポートされていると考えることができます。ルーティング イベントは、別名としてバブル イベントと呼ばれています。何故、バブルかと言えば、XAML 定義の中でイベントが発生したら タグを親に向かって連鎖的にイベントを発生させることができるからです(つまり、泡が広がるのと同じです)。それでは、RoutedEvents0 プロジェクトの Mainwindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    Random rand = new Random();
    byte[] rgb = new byte[3];

    public MainWindow()
    {
        InitializeComponent();
    }

    private void TextBlock_TouchDown(object sender, TouchEventArgs e)
    {
        TextBlock txtblk = sender as TextBlock;
        rand.NextBytes(rgb);
        Color clr = Color.FromArgb(255, rgb[0], rgb[1], rgb[2]);
        txtblk.Foreground = new SolidColorBrush(clr);
    }
}

 

TextBlock_TouchDown イベントを呼び出す MainWindow.xaml の抜粋を示します。

<Window x:Class="RoutedEvents0.MainWindow"
        ...
        FontSize="48"><Grid><TextBlock Text="Left / Top"
                   HorizontalAlignment="Left"
                   VerticalAlignment="Top"
                   TouchDown="TextBlock_TouchDown" />
        ...<TextBlock Text="Right / Bottom"
                   HorizontalAlignment="Right"
                   VerticalAlignment="Bottom"
                   TouchDown="TextBlock_TouchDown" /></Grid></Window>

 

実行結果は、同じように TextBlock をタッチすることで色が変化するようになります。
RoutedEvents0

次の RoutedEvents1 プロジェクトでは、ルーティング イベントを理解するために TextBlock ではなく、Grid にイベントを設定します。

<Grid TouchDown="Grid_TouchDown"><TextBlock Text="Left / Top"
               HorizontalAlignment="Left"
               VerticalAlignment="Top" />
    ...<TextBlock Text="Right / Bottom"
               HorizontalAlignment="Right"
               VerticalAlignment="Bottom" /></Grid>


そして、RoutedEvents1 プロジェクトの MainWindow.xaml.cs の抜粋を示します。

private void Grid_TouchDown(object sender, TouchEventArgs e)
{
    if (e.OriginalSource is TextBlock)
    {
        TextBlock txtblk = e.OriginalSource as TextBlock;
        rand.NextBytes(rgb);
        Color clr = Color.FromArgb(255, rgb[0], rgb[1], rgb[2]);
        txtblk.Foreground = new SolidColorBrush(clr);
    }
}


イベント ハンドラーのコードは、WinRT XAML と同じだということがわかります。

今度は、XAML でイベントを定義しない RoutedEvents2 プロジェクトの MainWindow.xaml の抜粋を示します。

<Window x:Class="RoutedEvents2.MainWindow"
        ...
        FontSize="48"><Grid><TextBlock Text="Left / Top"
                   HorizontalAlignment="Left"
                   VerticalAlignment="Top" />
        ...<TextBlock Text="Right / Bottom"
                   HorizontalAlignment="Right"
                   VerticalAlignment="Bottom" /></Grid></Window>

 

イベントの定義を示すために、MainWindow.xaml.cs の抜粋を示します。

protected override void OnTouchDown(TouchEventArgs e)
{
    if (e.OriginalSource is TextBlock)
    {
        TextBlock txtblk = e.OriginalSource as TextBlock;
        rand.NextBytes(rgb);
        Color clr = Color.FromArgb(255, rgb[0], rgb[1], rgb[2]);
        txtblk.Foreground = new SolidColorBrush(clr);
    }
    base.OnTouchDown(e);
}

 

WinRT XAML と同じように、OnTouchDown イベントをオーバーライドすることでイベントを処理しています。

今度は、イベント引数の OriginalSource のオブジェクトによって、色を変える RoutedEvents3 プロジェクトの MainWindow.xaml.cs の抜粋を示します。

protected override void OnTouchDown(TouchEventArgs e)
{
    rand.NextBytes(rgb);
    Color clr = Color.FromArgb(255, rgb[0], rgb[1], rgb[2]);
    SolidColorBrush brush = new SolidColorBrush(clr);

    if (e.OriginalSource is TextBlock)
        (e.OriginalSource as TextBlock).Foreground = brush;

    else if (e.OriginalSource is Grid)
        (e.OriginalSource as Grid).Background = brush;

    base.OnTouchDown(e);
}

 

また、RoutedEvents3 プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid Background="White"><TextBlock Text="Left / Top"
               HorizontalAlignment="Left"
               VerticalAlignment="Top" />
    ...<TextBlock Text="Right / Bottom"
               HorizontalAlignment="Right"
               VerticalAlignment="Bottom" /></Grid>


ここで注意して欲しいのが、Grid 要素の Background プロパティを「White」に指定している点です。コードでは、OriginalSource によって TextBlock か Grid かを識別して背景色を変更していますが、Grid の Background プロパティを指定しないと「Background="{x:Null}"」を指定したことと同義になり、Grid コントロールが不可視となり、イベントを発生させることができなくなります。これは、XAML を使った UI 技術の特徴にもなります。WinRT XAML でこのことが問題にならないのは、Background に組み込みのスタイルを指定しているからです。 

RoutedEvents3 から、Grid の背景色と TextBlock の背景色をイベントによってランダムに設定するようにした RoutedEvents4 の MainWindow.xaml の抜粋を示します。

<Grid Name="contentGrid"><TextBlock Text="Left / Top"
               HorizontalAlignment="Left"
               VerticalAlignment="Top"
               TouchDown="TextBlock_TouchDown" />
    ...<TextBlock Text="Right / Bottom"
               HorizontalAlignment="Right"
               VerticalAlignment="Bottom"
               TouchDown="TextBlock_TouchDown" /></Grid>

 

そして、MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    Random rand = new Random();
    byte[] rgb = new byte[3];

    public MainWindow()
    {
        InitializeComponent();
    }

    private void TextBlock_TouchDown(object sender, TouchEventArgs e)
    {
        TextBlock txtblk = sender as TextBlock;
        txtblk.Foreground = GetRandomBrush();
    }

    protected override void OnTouchDown(TouchEventArgs e)
    {
        contentGrid.Background = GetRandomBrush();
        base.OnTouchDown(e);
    }

    Brush GetRandomBrush()
    {
        rand.NextBytes(rgb);
        Color clr = Color.FromArgb(255, rgb[0], rgb[1], rgb[2]);
        return new SolidColorBrush(clr);
    }
}

RoutedEvents4 プロジェクトでは、RoutedEvents3 プロジェクトと異なり Grid 要素の Background プロパティを指定していません。それでも、RoutedEvents3 プロジェクトと同じように動作する理由は、OnTouchDown イベントにあります。このイベントで、無条件に Grid の背景色を変更しているから問題なく動作します。このことは、RoutedEvents3 プロジェクトの説明と合わせると、次に示す特徴を表しています。

  • TouchEventArgs.OriginalSource には、背景色が Null な Grid オブジェクトが含まれない。
  • OnTouchDown イベントは、Window オブジェクトをオーバーライドしているので必ず発生する。

次のサンプルでは、バブル イベントという性格を持つルーティング イベントを停止するかどうかを試します。では、RoutredEvents5 プロジェクトの MainWindow.xaml.cs  の抜粋を示します。

private void TextBlock_TouchDown(object sender, TouchEventArgs e)
{
    TextBlock txtblk = sender as TextBlock;
    txtblk.Foreground = GetRandomBrush();
    e.Handled = true;
}


イベント引数が持つ Handled プロパティを設定しているだけで、Handled プロパティの説明は次の節で紹介しています。

3.3(P85) Handled 設定の上書き

この節で最初に説明する RoutedEvents6 プロジェクトでは、Grid要素の背景色を変更するイベントと TextBlock の色を変更するイベントを定義しています。この MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
  Random rand = new Random();
  byte[] rgb = new byte[3];

  public MainWindow()
  {
      InitializeComponent();

      this.AddHandler(UIElement.TouchDownEvent,
                      new EventHandler(OnWindowTouchDown), true);
  }

  private void TextBlock_TouchDown(object sender, TouchEventArgs e)
  {
      TextBlock txtblk = sender as TextBlock;
      txtblk.Foreground = GetRandomBrush();
      e.Handled = true;
  }

  private void OnWindowTouchDown(object sender, TouchEventArgs e)
  {
      contentGrid.Background = GetRandomBrush();
  }

  Brush GetRandomBrush()
  {
      rand.NextBytes(rgb);
      Color clr = Color.FromArgb(255, rgb[0], rgb[1], rgb[2]);
      return new SolidColorBrush(clr);
  }

}


コンストラクターで行っているイベント ハンドラーの登録は、WinRT XAML と同じように AddHandler メソッドを使用しています。AddHandler メソッドを使用している理由は、本書を読んで確認してください。

3.4(P87) 入力、位置合わせ、背景

この節では、RoutedEventns3 と比較して TextBlock がどのように配置されるかを ToutedEvents7 プロジェクトを使って説明しています。それでは、MainWindow.xaml の抜粋を示します。

<Window x:Class="RoutedEvents7.MainWindow"
        ...
        FontSize="48"><Grid Background="White">>TextBlock Text="Hello, Windows 8!"
                   Foreground="Red" /></Grid></Window>

そして、MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    Random rand = new Random();
    byte[] rgb = new byte[3];

    public MainWindow()
    {
        InitializeComponent();
    }

    protected override void OnTouchDown(TouchEventArgs e)
    {
        rand.NextBytes(rgb);
        Color clr = Color.FromArgb(255, rgb[0], rgb[1], rgb[2]);
        SolidColorBrush brush = new SolidColorBrush(clr);

        if (e.OriginalSource is TextBlock)
            (e.OriginalSource as TextBlock).Foreground = brush;

        else if (e.OriginalSource is Grid)
            (e.OriginalSource as Grid).Background = brush;
        base.OnTouchDown(e);
    }
}

もちろん。RoutedEvents3 プロジェクトと同じように Grid 要素の Background プロパティを設定しています。実行結果を示します。
RoutedEvents7

自分で実行して、ウィンドウをタップしてみるとわかりますが、ウィンドウのどこをタップしても TextBlock の色が変化されます。つまり、TextBlock の表示位置などの指定していないので、ウィンドウ一杯の領域が TextBlock と扱われて、OnTouchDown イベント ハンドラーの Grid を判定するロジックが動作しないことを確認できます。この問題を解決するには、書籍に記述されているように TextBlock の表示位置や表示サイズを指定して、Grid の背景色に何かの色(たとえば、Whiteなど)を指定するか、「Transparent」を指定することになります(Transparent と Null は異なりますので、注意してください)。

3.5(P90) サイズと向きの変更

この節では、Windows ストア アプリが画面の向きやサイズ変更にどのように対応しているかをサンプルを使って説明しています。最初に説明しているのは、WhatSize プロジェクトを使って、画面サイズを確認することです。それでは、WhatSize プロジェクトの MainWindow.xaml の抜粋を示します。

<Window x:Class="WhatSize.MainWindow"
        ...
        SizeChanged="Window_SizeChanged"><Grid><TextBlock HorizontalAlignment="Center"
                   VerticalAlignment="Top">↤ <Run x:Name="widthText" /> pixels ↦
        </TextBlock><TextBlock HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   TextAlignment="Center">↥
            <LineBreak /><Run x:Name="heightText" /> pixels<LineBreak />↧
        </TextBlock></Grid></Window>

 

続いて、MainWindow.xaml.cs の抜粋を示します。

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

    private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        widthText.Text = e.NewSize.Width.ToString();
        heightText.Text = e.NewSize.Height.ToString();
    }
}

 

WinRT XAML と同じように SizeChanged イベントを処理することで、画面のサイズを取得することができます。
WhatSize

書籍では、続いて、画面の向きが変化したかどうかを ScalableInternationalHelloWorld プロジェクトを使って説明しています。この時に使用しているクラスが、DisplayProperties (Windows 8.1では、DisplayInformation) になりますが、このクラスは Windows Runtime 固有であり、WPF などのデスクトップ アプリから使用することはできません。

それでは、WPF などのデスクトップ向けのアプリでタブレット PC 固有の画面の向きに対応するにはどうしたら良いでしょうか。これには、いつかの手段を考えることができます。

  • システム イベントの DisplaySettingsChangedイベントで処理する(もしくは、WM_DISPLAYCHANGED メッセージを ウィンドウ プロシージャで処理します)。
  • Screenクラスの PrimaryScreent プロパティを使って、Bounds プロパティ経由で縦横を比較して画面の向きを処理する。

これらの説明が、「Detecting Screen Orientation and Screen Rotation in Tablet PC Applications」という ドキュメントに記載されていますので、必要に応じて処理する必要があります。よって、 ScalableInternationalHelloWorld プロジェクトはサンプルに含まれていません。

Windows ストア アプリでは、画面の向きやスケーリングを完全にサポートしています。一方で、デスクトップ上のアプリは WPFのみがスケーリングのに対応し、画面の向きなどの対応はアプリケーションに任せるという考え方になっている点にご注意ください。

3.6(P95) Run でのバインディング

この節で最初に説明している TextBlock 内に「<Run Text="{Binding ElementName=window, Path=ActualWidth}" />」という記述方法を WPF XAML で行うと、Visual Studio 2013 の IDE は警告を表示します。警告を無視して実行すると、実行時に初回例外が発生します。この動きは、WinRT XAML とは違いますが、Run 要素を使わないバインディングでは正常に動作します。

<Window x:Class="WhatSizeBinding.MainWindow"
        ...
        x:Name="window"
        FontSize="36"><Grid><TextBlock HorizontalAlignment="Center"
                   VerticalAlignment="Top"
                   Text="{Binding ElementName=window, Path=ActualWidth}" /><TextBlock HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   TextAlignment="Center"
                   Text="{Binding ElementName=window, Path=ActualHeight}" /></Grid></Window>

 

実行結果も書籍と同じようになります。なぜ、Run 要素にバインディングができないかの理由は書籍を読めば答えがわかるようになっています。ここでは、WinRT XAML と WPF XAML の挙動の違いを説明するだけにします。

3.7(P98) タイマーとアニメーション

この節では、DispatcherTimer クラスを使った説明になります。DispatcherTimer クラスは、XAML という UI 技術を使った場合に使える共通のタイマー クラスとなっています。一方 Windows Forms では、System.Windows.Forms.Timer クラスが用意されていて、デザイナーにツールボックスからドラッグ&ドロップして使用することができますが、WPF を初めとする XAML 系の UI 技術ではデザイナー サポートは用意されていません。つまり、タイマーを使用したい場合は、自分でコードを記述する必要がある点にご注意ください。

最初に DigitalClock プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><TextBlock Name="txtblk"
               FontFamily="Lucida Console"
               FontSize="120"
               HorizontalAlignment="Center"
               VerticalAlignment="Center" /></Grid>

 

xaml は WinRT と同じであり、分離コードも同じになります。次にMainWindow.xaml.cs の抜粋を示します。

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

        DispatcherTimer timer = new DispatcherTimer();
        timer.Interval = TimeSpan.FromSeconds(1);
        timer.Tick += OnTimerTick;
        timer.Start();
    }

    void OnTimerTick(object sender, object e)
    {
        txtblk.Text = DateTime.Now.ToString("h:mm:ss tt");
    }
}

もち���ん、実行した結果も同じになります。
DigitalClock

次のサンプルは、CompositionTarget.Rendering イベント ハンドラーを利用した ExpandingText プロジェクトになります。MainWindow.xaml の抜粋を示します。

<Grid><TextBlock Name="txtblk"
               Text="Hello, Windows 8!"
               HorizontalAlignment="Center"
               VerticalAlignment="Center" /></Grid>

ExpandingText プロジェクトは、WPF でも同じコードを使用できますので、MainWindow.xaml.cs の抜粋を示します。

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

        CompositionTarget.Rendering += OnCompositionTargetRendering;
    }

    void OnCompositionTargetRendering(object sender, object args)
    {
        RenderingEventArgs renderArgs = args as RenderingEventArgs;
        double t = (0.25 * renderArgs.RenderingTime.TotalSeconds) % 1;
        double scale = t < 0.5 ? 2 * t : 2 - 2 * t;
        txtblk.FontSize = 1 + scale * 143;
    }
}

このコードで WinRT XAML と同じように フォント サイズが変化するアニメーションになります。

今度は、色をアニメーション化する ManualBrushAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid Name="contentGrid"><TextBlock Name="txtblk"
               Text="Hello, Windows 8!"
               FontFamily="Times New Roman"
               FontSize="96"
               FontWeight="Bold"
               HorizontalAlignment="Center"
               VerticalAlignment="Center" /></Grid>

色に基づいてアニメーションするコードも同じになります。MainWindow.xaml.cs の抜粋を示します。

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

        CompositionTarget.Rendering += OnCompositionTargetRendering;
    }

    void OnCompositionTargetRendering(object sender, object args)
    {
        RenderingEventArgs renderingArgs = args as RenderingEventArgs;
        double t = (0.25 * renderingArgs.RenderingTime.TotalSeconds) % 1;
        t = t < 0.5 ? 2 * t : 2 - 2 * t;

        // Background
        byte gray = (byte)(255 * t);
        Color clr = Color.FromArgb(255, gray, gray, gray);
        contentGrid.Background = new SolidColorBrush(clr);

        // Foreground
        gray = (byte)(255 - gray);
        clr = Color.FromArgb(255, gray, gray, gray);
        txtblk.Foreground = new SolidColorBrush(clr);
    }
}

今度は、 SolidColorBrush をコードではなく XAML で定義した ManualColorAnimation プロジェクトになります。MainWindow.xaml の抜粋を示します。

<Grid><Grid.Background><SolidColorBrush x:Name="gridBrush" /></Grid.Background><TextBlock Text="Hello, Windows 8!"
               FontFamily="Times New Roman"
               FontSize="96"
               FontWeight="Bold"
               HorizontalAlignment="Center"
               VerticalAlignment="Center"><TextBlock.Foreground><SolidColorBrush x:Name="txtblkBrush" /></TextBlock.Foreground></TextBlock></Grid>

この XAML を使用する MainWindow.xaml.cs の抜粋を示します。

void OnCompositionTargetRendering(object sender, object args)
{
    RenderingEventArgs renderingArgs = args as RenderingEventArgs;
    double t = (0.25 * renderingArgs.RenderingTime.TotalSeconds) % 1;
    t = t < 0.5 ? 2 * t : 2 - 2 * t;

    // Background
    byte gray = (byte)(255 * t);
    gridBrush.Color = Color.FromArgb(255, gray, gray, gray);

    // Foreground
    gray = (byte)(255 - gray);
    txtblkBrush.Color = Color.FromArgb(255, gray, gray, gray);
}

もちろん、WPF でも WinRT XAML と同じように動作します。

最後に扱うサンプルは、虹色に変化するアニメーションである RainbowEight プロジェクトになります。最初に、MainWindow.xaml の抜粋を示します。

<Grid><TextBlock Name="txtblk"
               Text="8"
               FontFamily="CooperBlack"
               FontSize="1"
               HorizontalAlignment="Center"><TextBlock.Foreground><LinearGradientBrush x:Name="gradientBrush"><GradientStop Offset="0.00" Color="Red" /><GradientStop Offset="0.14" Color="Orange" /><GradientStop Offset="0.28" Color="Yellow" /><GradientStop Offset="0.43" Color="Green" /><GradientStop Offset="0.57" Color="Blue" /><GradientStop Offset="0.71" Color="Indigo" /><GradientStop Offset="0.86" Color="Violet" /><GradientStop Offset="1.00" Color="Red" /><GradientStop Offset="1.14" Color="Orange" /><GradientStop Offset="1.28" Color="Yellow" /><GradientStop Offset="1.43" Color="Green" /><GradientStop Offset="1.57" Color="Blue" /><GradientStop Offset="1.71" Color="Indigo" /><GradientStop Offset="1.86" Color="Violet" /><GradientStop Offset="2.00" Color="Red" /></LinearGradientBrush></TextBlock.Foreground></TextBlock></Grid>

WinRT XAML との違いは、何もありません。TextBlock 要素に LinearGridentBrush を設定していることも同じです(このブラシが、虹色に変化する雰囲気を醸し出します)。それでは、MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    double txtblkBaseSize;  // ie, for 1-pixel FontSize

    public MainWindow()
    {
        InitializeComponent();

        Loaded += MainWindow_Loaded;
    }

    void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        //txtblkBaseSize = txtblk.ActualHeight; // ActualHeight が異なるため
        txtblkBaseSize = txtblk.DesiredSize.Height;
        CompositionTarget.Rendering += OnCompositionTargetRendering;
    }

    void OnCompositionTargetRendering(object sender, object args)
    {
        // Set FontSize as large as it can be
        txtblk.FontSize = this.ActualHeight / txtblkBaseSize;

        // Calculate t from 0 to 1 repetitively
        RenderingEventArgs renderingArgs = args as RenderingEventArgs;
        double t = (0.25 * renderingArgs.RenderingTime.TotalSeconds) % 1;

        // Loop through GradientStop objects
        for (int index = 0; index < gradientBrush.GradientStops.Count; index++)
            gradientBrush.GradientStops[index].Offset = index / 7.0 - t;
    }

}

コメントを記述していることから違いを理解できると思いますが、WPF XAML では、このようなケースでは ActualHeight を使用するのは望ましくありません。このため、DesiredSize プロパティを使用して描画しようとするサイズを取得しています。DesiredSize プロパティは、レイアウトを描画する時の必要な描画サイズを計算した値になります。WinRT XAML では、ActualHeight プロパティで問題ないことから、WPF で ConpositionTarget.Rendering イベントで オブジェクトの描画サイズにアクセスするには、DesiredSize を使用すると覚えていただければ結構です。実行結果を示します。
RainbowEight

ここまで説明してた違いを意識しながら、第3章を読むことで WPF にも書籍の内容を応用することができるようになることでしょう。

プログラミング Windows 第6版 第4章 WPF編

$
0
0

この記事では、「プログラミング Windows 第6版」を使って WPF XAML の学習を支援することを目的にしています。この目的から、書籍と併せて読まれることをお勧めします。

最初に、第2章の記事で Visual Studio より Blend を簡単に起動するための設定を説明しました。この時は、Visual Studio 2013 のエディションを記載していませんでしたが、有償のエディションを前提としていました。私は確認していませんが、無償の Express エディションでも可能になると考えられる組み合わせがあります。それは、Visual Studio Express 2013 for WindowsVisual Studio Express for Window Desktopの両方をインストールした環境になります。この理由は、Visual Studio Express 2013 for Windows という Windows ストア アプリ用の開発ツールに Blend for Visual Studio が含まれているからです。この関係で、インストールできるオペレーティング システムが Windows 8/8.1 という制限がありますが、WPF XAML の編集などに Blend を利用できるので環境を構築する価値があると私は考えています。

第4章 パネルを使った表示

書籍では、「WinRT アプリケーションは、Page クラスを継承する 1つ以上のクラスで構成されています(引用)」で始まります。が、WPF アプリケーションは、Windows クラスを継承する 1つ以上のクラスか、NavigationWindow クラスを継承する 1つ以上のクラスで構成されています。と、読み替えることができます(NavigationWindow クラスを継承した場合に、コンテンツとして Page クラスを使用します)。そして、Panel の説明にと続きます。Panel の派生クラスの例として、VariableSizedWrapGridの説明がありますが、VariableSizedWrapGrid は WinRT XAML に固有のクラスであり、WPF XAML には含まれていませんので、ご注意ください。VariableSizedWrapGrid に相当するパネルとして、WPF には WrapPanelがあります。これ以外にも、Panel を継承するクラスには WinRT XAML と WPF XAML には違いがありますが、総じて WPF XAML の方が高機能だと私は感じています。多分、WinRT XAML のようなサブセットは、その目的に応じて拡張すべき機能と実装すべき機能を絞っているためだと考えられます。このように考えた方が、あれができないと悩むよりも、ではこうしようという風に割り切って使うことができるのではないでしょうか。

4.1(P108) Border

この節では、パネルを使って表示がどのようになるかを説明しています。最初に、NativeBorderText プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><Border BorderBrush="Red"
                BorderThickness="12"
                CornerRadius="24"
                Background="Yellow"><TextBlock Text="Hello Windows 8!"
                       FontSize="96"
                       Foreground="Blue"
                       HorizontalAlignment="Center"
                       VerticalAlignment="Center" /></Border></Grid></Window>

HorizontalAlignment と VeryicalAlignment プロパティが TextBlock 要素に設定されています。この NativeBorderText の実行結果を示します。
NaiveBorderedText

Border 要素が、Window 一杯に広がって、Border 要素の中で TextBlock に設定した文字列が HorizontalAlignment と VerticalAlignment プロパティ によってセンタリングされています。今度は、BetterBorderedText プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><Border BorderBrush="Red"
            BorderThickness="12"
            CornerRadius="24"
            Background="Yellow"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"><TextBlock Text="Hello Windows 8!"
                   FontSize="96"
                   Foreground="Blue"
                   Margin="24" /></Border></Grid>

NativeBorder との違いは、HorizontalAlignment と VerticalAlignment プロパティを Border 要素へ移動したことです。実行結果を示します。
BetterBorderedText

今度は、Border 要素が Window の中心に配置されて、TextBlock 要素を取り囲んでいます。これが、パネル(この例では、Grid)に要素を配置する場合の HorizontalAlignment と VerticalAlignment プロパティの挙動になります。そして書籍では、Margin プロパティの説明が行われます。ここでは、レイアウトの配置に使用する Margin プロパティと Padding プロパティの考え方を少しだけ説明します。
ch04 Margin Padding

Marign プロパティは、余白を設定するもので Windows Forms にもあったプロパティになります。XAML のコントロールには、Windows Forms のコントロールと違い Location プロパティがありません。このために良く使われるのが、Margin プロパティとなります。設定できる値は、コントロールの左上の角座標と右下の角座標になります(Margin は、オーバーロードされていることから X1 のみや、X1,Y1 なども設定できます)。もう 1つの Padding プロパティは、一部のコントロールが持つプロパティになりますが、Margin プロパティとは基準点が異なる点に注意が必要です。それぞれの基準点を示します。

  • Margin: コントロールを配置するパネルなどを基準座標とします。
  • Padding:コントロールが配置される座標を基準とします。つまり、本来の配置位置から Content の表示位置を指定する用途に使用します。

XAML のコントロールを配置する場合に、Margin と Padding プロパティは、最初に悩むプロパティの 1つでもあります。Margin プロパティは、FrameworkElement で定義されており、全ての要素で使用することができます。Padding プロパティは、コントロールの Content (コントロールのコンテンツ モデル)が、Padding プロパティをサポートしている場合にだけ使用できるようになっています。この理由から、全てのコントロールが Padding プロパティをサポートしていないのです。よって、通常は Margin プロパティを配置に使うと考えれば良いということになります。

4.2(P101) Rectangle と Ellipse

この節では、Shape クラス(WPF XAML では System.Windows.Shapes 名前空間)から派生したクラスを使って図形を描画することを説明しています。最初に円弧をレンダリングする SimpleEllipse プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><Ellipse Stroke="Red"
             StrokeThickness="24"
             Fill="Blue" /></Grid>

実行結果を示します。WinRT XAML と同じように、コンテナー(Grid)一杯にレンダリングされています。
SimpleEllipse
Height や Width プロパティ、Stretch プロパティによってどのように描画が変化するかは、書籍を参照してください。

4.3(P113) StackPanel

本節では、StackPanel を説明しています。説明の通り、StackPanel は使うことが多いパネル コントロールです(これ以外は、Window に配置する Grid や Canvas なども使う頻度が高いパネル コントロールです)。StackPanel の子要素にどのように高さが割り当てるかを確認するために SimpleVerticalStack プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><StackPanel><TextBlock Text="Right-Aligned Text"
                   FontSize="48"
                   HorizontalAlignment="Right" /><Image Source="http://www.charlespetzold.com/pw6/PetzoldJersey.jpg"
               Stretch="None" /><TextBlock Text="Figure 1. Petzold heading to the basketball court"
                   FontSize="24"
                   HorizontalAlignment="Center" /><Ellipse Stroke="Red"
                 StrokeThickness="12" 
                 Fill="Blue" /><TextBlock Text="Left-Aligned Text"
                   FontSize="36"
                   HorizontalAlignment="Left" /></StackPanel></Grid>

実行結果を示します。
SimpleVerticalStack

書籍の実行結果と比べると、大きな違いは Ellipse の描画状態でしょう。これは、書籍に記述されているように Height プロパティを設定していない(つまり、ゼロ)ためです。StackPanel の子要素の高さは、その子要素が必要とする高さが割り当てられていることが理解できます。また、Orientation プロパティを指定していない StackPanel は子要素を縦方向に配置することも理解できます。書籍に従って、様々なプロパティを変更してレンダリング結果を確かめてみることをお勧めします。

4.4(P117) 横方向への積み重ね

本節では、StackPanel の Orientation プロパティを使って横方向へ子要素を配置することを説明しています。このために、SimpleHorizontalStack プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><StackPanel Orientation="Horizontal"
                VerticalAlignment="Center"
                HorizontalAlignment="Center"><TextBlock Text="Rectangle: "
                   VerticalAlignment="Center" /><Rectangle Stroke="Blue"
                   Fill="Red"
                   Width="72"
                   Height="72"
                   Margin="12 0"
                   VerticalAlignment="Center" /><TextBlock Text="Ellipse: "
                   VerticalAlignment="Center" /><Ellipse Stroke="Red"
                 Fill="Blue"
                 Width="72"
                 Height="72"
                 Margin="12 0"
                 VerticalAlignment="Center" /><TextBlock Text="Petzold: "
                   VerticalAlignment="Center" /><Image Source="http://www.charlespetzold.com/pw6/PetzoldJersey.jpg" 
               Stretch="Uniform"
               Width="72"
               Margin="12 0"
               VerticalAlignment="Center" /></StackPanel></Grid>

実行結果を示します。
SimpleHorizontalStack

書籍で説明しているように、各種のプロパティを変更して子要素の配置がどのように変化するかを確認することをお勧めします。

4.5(P119) WhatSize とバインディング

本節では、第2章で使用した WhatSize プロジェクトを StackPanel とバインディングを使って説明しています。最初に、WhatSizeBinding プロジェクトの MainWindow.xaml の抜粋を示します。

<Window x:Name="page" ... ><Grid><StackPanel Orientation="Horizontal"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Top"><TextBlock Text="↤ " /><TextBlock Text="{Binding ElementName=page, Path=ActualWidth}" /><TextBlock Text=" pixels ↦" /></StackPanel><StackPanel HorizontalAlignment="Center"
                    VerticalAlignment="Center"><TextBlock Text="↥" TextAlignment="Center" /><StackPanel Orientation="Horizontal"
                        HorizontalAlignment="Center"><TextBlock Text="{Binding ElementName=page, Path=ActualHeight}" /><TextBlock Text=" pixels" /></StackPanel><TextBlock Text="↧" TextAlignment="Center" /></StackPanel></Grid></Window>

実行結果を示します。
WhatSizeWithBindings
書籍では、画面の向きを変えたりスナップにしたりすると値が変更されない(WinRT XAML)とありますが、WPF 版ではウィンドウ サイズを変更することによって値が更新されていきます。この点も、WinRT XAML と WPF XAML の違いとなります。この両者の違いを正確に説明するのは難しいですが、あえて説明するとすれば、「WinRT XAML は WPF XAML のサブセットであり、WPF XAML がフルセットであり高機能のため」となります。

書籍では、この問題を解決するためにコンバーターを作成します。コンバーターは、WPF XAML でも必要になることから、WhatSizeWithBindingConverter プロジェクトの FormattedStringConverter.cs を示します。

using System;
using System.Globalization;
using System.Windows.Data;

namespace WhatSizeWithBindingConverter
{
    public class FormattedStringConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is IFormattable &&
                parameter is string &&
                !String.IsNullOrEmpty(parameter as string) &&
                targetType == typeof(string))
            {
                if (culture == null)
                    return (value as IFormattable).ToString(parameter as string, null);

                return (value as IFormattable).ToString(parameter as string,
                                                        culture);
            }

            return value;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return value;
        }
    }
}


WinRT XAML との違いは、IValueConverter インターフェースのメンバーである Convert メソッドと ConvertBack メソッドの引数になります。WinRT XAML では、最後の引数が文字列(string)型ですが、WPF XAML では CultureInfo 型となっています。この点に注意して書き換えれば、WPF でも問題なく動作します。 作成した FormattedStringConverter クラスを使用するようにした MainWindow.xaml の抜粋を示します。

<Window ...
        x:Name="page"><Window.Resources><local:FormattedStringConverter x:Key="stringConverter" /></Window.Resources>    <Grid><StackPanel Orientation="Horizontal"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Top"><TextBlock Text="↤ " /><TextBlock Text="{Binding ElementName=page, 
                                      Path=ActualWidth, 
                                      Converter={StaticResource stringConverter},
                                      ConverterParameter=N0}" /><TextBlock Text=" pixels ↦" /></StackPanel><StackPanel HorizontalAlignment="Center"
                    VerticalAlignment="Center"><TextBlock Text="↥" TextAlignment="Center" /><StackPanel Orientation="Horizontal"
                        HorizontalAlignment="Center"><TextBlock Text="{Binding ElementName=page, 
                                          Path=ActualHeight, 
                                          Converter={StaticResource stringConverter},
                                          ConverterParameter=N0}" /><TextBlock Text=" pixels" /></StackPanel><TextBlock Text="↧" TextAlignment="Center" /></StackPanel></Grid></Window>

 

最初にリソースとして、FormattedStringConverter を定義(xmlns:local を定義しています)し、バインディング構文でコンバーター(Converter)とコンバーター パラメーター(この例では、文字列の「NO」)を指定しています。実行しても、WPF XAML では結果は同じになりますが、WinRT XAML では画面の向きの変化などによって値が更新されるようになります。
WhatSizeWithBindingConverter

4.6(p124) ScrollViewer

本節では、StackPanel などの子要素が多い場合に表示されない子要素をどうするかという観点で ScrollViewer コントロールを説明しています。ScrollViewer コントロールは、Windows Forms に存在しないコントロールとなります。Windows Forms のコントロールとしては、HScrollBar と VScrollBar コントロールが合体したイメージのコントロールが ScrollViewer になります。この説明で理解できると思いますが、HScrollBar と VScrollBar コントロールは XAML のコントロールにはありません。理由は、ScrollViewer コントロールが、水平スクロールバーと垂直スクロールバーを兼ね備えているからです。それでは、StackPanelWithScrolling プロジェクトの MainWindow.xaml の抜粋を示します。

<Window x:Class="StackPanelWithScrolling.MainWindow"
        ... ><Grid><ScrollViewer><StackPanel Name="stackPanel" /></ScrollViewer></Grid></Window>


そして、StackPanel の子要素を生成する MainWindows.xaml.cs の抜粋を示します。

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

        IEnumerable properties =
                                typeof(Colors).GetTypeInfo().DeclaredProperties;

        foreach (PropertyInfo property in properties)
        {
            Color clr = (Color)property.GetValue(null);
            TextBlock txtblk = new TextBlock();
            txtblk.Text = String.Format("{0} \x2014 {1:X2}-{2:X2}-{3:X2}-{4:X2}",
                                        property.Name, clr.A, clr.R, clr.G, clr.B);
            stackPanel.Children.Add(txtblk);
        }

    }
}

 

コードを見れば、Colors 列挙を使って作成した TextBlock オブジェクトを StackPanel に追加しています。実行結果を示します。
StackPanelWithScrolling
実行して操作すれば理解ができますが、スクロールバーをマウスか指でタッチすることでスクロールさせることができます。つまり、WinRT XAML と違ってScrollViewer コントロールは完全なタッチ対応ではないことになります。タッチ対応の WPF コントロールについては、入力の概要ドキュメントのタッチに応答するコントロールを参照してください。

今度は、DependencyObject の階層構造を出力する DependencyObjectClassHierarchy プロジェクトの MainWindow.xaml を示します。

<Window ... ><Grid><ScrollViewer><StackPanel Name="stackPanel" /></ScrollViewer></Grid></Window>

 

WinRT XAMLとの違いは、FontSize を指定していないことです。理由は、組み込みスタイルがないためです。そして、ClassAndSubclasses.cs は同じコードを使用することができます。WPF XAML へ移植するために必要な変更は、MainWindow.xaml.cs のコードだけなので抜粋を示します。

public partial class MainWindow : Window
{
  Type rootType = typeof(DependencyObject);
  TypeInfo rootTypeInfo = typeof(DependencyObject).GetTypeInfo();
  List<Type> classes = new List<Type>();
  Brush highlightBrush;

  public MainWindow()
  {
    InitializeComponent();

    //highlightBrush = new SolidColorBrush(new UISettings().UIElementColor(UIElementType.Highlight));
    highlightBrush = new SolidColorBrush(SystemColors.HighlightColor);

    // Accumulate all the classes that derive from DependencyObject 
    AddToClassList(typeof(System.Windows.DependencyObject));
    AddToClassList(typeof(System.Windows.UIElement));   // 追加
    AddToClassList(typeof(System.Windows.Window));      // 追加

    // Sort them alphabetically by name
    classes.Sort((t1, t2) =>
    {
      return String.Compare(t1.GetTypeInfo().Name, t2.GetTypeInfo().Name);
    });
    // Put all these sorted classes into a tree structure
    ClassAndSubclasses rootClass = new ClassAndSubclasses(rootType);
    AddToTree(rootClass, classes);

    // Display the tree using TextBlock's added to StackPanel
    Display(rootClass, 0);
  }

  void AddToClassList(Type sampleType)
  {
    Assembly assembly = sampleType.GetTypeInfo().Assembly;
    var types = assembly.GetTypes();
    foreach (Type type in assembly.ExportedTypes)   // ExportedTypes
    {
      TypeInfo typeInfo = type.GetTypeInfo();
      System.Diagnostics.Debug.WriteLine(typeInfo.Name);
      if (typeInfo.IsPublic && rootTypeInfo.IsAssignableFrom(typeInfo))
         classes.Add(type);
    }
  }

  void AddToTree(ClassAndSubclasses parentClass, List<Type> classes)
  {
    foreach (Type type in classes)
    {
      Type baseType = type.GetTypeInfo().BaseType;

      if (baseType == parentClass.Type)
      {
        ClassAndSubclasses subClass = new ClassAndSubclasses(type);
        parentClass.Subclasses.Add(subClass);
        AddToTree(subClass, classes);
      }
    }
  }

  void Display(ClassAndSubclasses parentClass, int indent)
  {
    TypeInfo typeInfo = parentClass.Type.GetTypeInfo();

    // Create TextBlock with type name
    TextBlock txtblk = new TextBlock();
    txtblk.Inlines.Add(new Run { Text = new string(' ', 8 * indent) });
    txtblk.Inlines.Add(new Run { Text = typeInfo.Name });

    // Indicate if the class is sealed
    if (typeInfo.IsSealed)
      txtblk.Inlines.Add(new Run
      {
        Text = " (sealed)",
        Foreground = highlightBrush
      });

    // Indicate if the class can't be instantiated
    IEnumerable<ConstructorInfo> constructorInfos = typeInfo.DeclaredConstructors;
    int publicConstructorCount = 0;

    foreach (ConstructorInfo constructorInfo in constructorInfos)
      if (constructorInfo.IsPublic)
        publicConstructorCount += 1;

    if (publicConstructorCount == 0)
      txtblk.Inlines.Add(new Run
      {
        Text = " (non-instantiable)",
        Foreground = highlightBrush
      });

    // Add to the StackPanel
    stackPanel.Children.Add(txtblk);

    // Call this method recursively for all subclasses
    foreach (ClassAndSubclasses subclass in parentClass.Subclasses)
      Display(subclass, indent + 1);
  }
}

 

コードで変更した箇所は、数か所があります。最初に、highlightBrush に設定するシステムが用意するハイライト用のブラシ定義を WPF 用に変更しています。続いて、変更しているのが AddToClassList メソッドの呼び出しになります。 WinRT XAMLでは、DependencyObject だけの呼び出しですが、WPF XAML では UIElement と Window の呼び出しを追加しています。これは、WinRT XAML が使用するアセンブリ(メタデータ) と WPF XAML が使用するアセンブリ(PresentationCore、PresentationFramework、WindowsBase)の違いによるものです。このアセンブリの違いも、WinRT XAML が WPF XAML のサブセットであることから 1つのアセンブリだけでメタデータを定義していることを表しています。それでは、実行結果を示します。
DependencyObjectClassHierarchy
WinRT XAML と同じように DependecyObject の階層構造を表示することができます。

4.7(P130) レイアウトの是非

この節では、レイアウト メカニズムを理解するために StackPanelWithScrollong プロジェクトを書き換えてレイアウトへの影響を説明しています。考え方は、WPF XAML も同じなので自分で試してみてください。

4.8(P132) 電子書籍の作成

本節では、「The Tale of Tom Kitten」という童話を題材に電子書籍ビューワーのようなアプリを作ることを説明しています。この TheTaleOfTomKitten プロジェクトを WPF XAML へ移植するのは簡単です。具体的には、ここまでに説明してきた内容(Page を Window へ、組み込みスタイルを変更)を反映するだけなので、MainWindow.xaml のリソース定義を示します。

<Window.Resources><Style x:Key="commonTextStyle" TargetType="TextBlock"><Setter Property="FontFamily" Value="Century Schoolbook" /><Setter Property="FontSize" Value="36" /><Setter Property="Foreground" Value="Black" /><Setter Property="Margin" Value="0 12" /></Style><Style x:Key="paragraphTextStyle" TargetType="TextBlock" 
           BasedOn="{StaticResource commonTextStyle}"><Setter Property="TextWrapping" Value="Wrap" /></Style><Style x:Key="frontMatterTextStyle" TargetType="TextBlock" 
           BasedOn="{StaticResource commonTextStyle}"><Setter Property="TextAlignment" Value="Center" /></Style><Style x:Key="imageStyle" TargetType="Image"><Setter Property="Stretch" Value="None" /><Setter Property="HorizontalAlignment" Value="Center" /></Style></Window.Resources>

 

次に TextBlock 要素と Image 要素を交互に配置した XAML を示します(MainWindow.xamlより)。

<Grid Background="White"><ScrollViewer><StackPanel MaxWidth="640"
                    HorizontalAlignment="Center">

            ...
            <!-- pg. 38 --><TextBlock Style='{StaticResource paragraphTextStyle}'>  Mittens laughed so that she fell off the 
                wall. Moppet and Tom descended after her; the pinafores 
                and all the rest of Tom's clothes came off on the way down.</TextBlock><TextBlock Style='{StaticResource paragraphTextStyle}'>  “Come! Mr. Drake Puddle-Duck,” said Moppet — “Come and help us to dress him! Come and button up Tom!”</TextBlock><Image Source='Images/tom39.jpg' Style='{StaticResource imageStyle}' /><!-- pg. 41 --><TextBlock Style='{StaticResource paragraphTextStyle}'>  Mr. Drake Puddle-Duck advanced in a slow 
                sideways manner, and picked up the various articles.</TextBlock><Image Source='Images/tom40.jpg' Style='{StaticResource imageStyle}' />
            ...</StackPanel></ScrollViewer></Grid>

 

WinRT XAML との違いは、Grid 要素の Background プロパティだけになります。それでは、実行結果を示します。
TheTaleOfTomKitten

4.9(P135) StackPanel の子要素のカスタマイズ

この節では、ScrollViewer 要素内に配置した StackPanel の子要素を順序を追ってカスタマイズしていく方法を説明しています。最初に、WinRT で利用可能な 141 種類の色を表示することから始めています。WPF XAML へ移植するのは簡単で、ここまでに説明してきた内容と使用している各クラスの名前空間を変更(WPF アプリケーション プロジェクトで作業する限りは、意識する必要はありません)するだけです。それでは、ColorList1 プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><ScrollViewer><StackPanel Name="stackPanel"
                    HorizontalAlignment="Center" /></ScrollViewer></Grid>


今度は、MainWindow.xaml.cs の抜粋を示します。

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

        IEnumerable<PropertyInfo> properties = typeof(Colors).GetTypeInfo().DeclaredProperties;

        foreach (PropertyInfo property in properties)
        {
            Color clr = (Color)property.GetValue(null);

            StackPanel vertStackPanel = new StackPanel
            {
                VerticalAlignment = VerticalAlignment.Center
            };

            TextBlock txtblkName = new TextBlock
            {
                Text = property.Name,
                FontSize = 24
            };
            vertStackPanel.Children.Add(txtblkName);

            TextBlock txtblkRgb = new TextBlock
            {
                Text = String.Format("{0:X2}-{1:X2}-{2:X2}-{3:X2}",
                                     clr.A, clr.R, clr.G, clr.B),
                FontSize = 18
            };
            vertStackPanel.Children.Add(txtblkRgb);

            StackPanel horzStackPanel = new StackPanel
            {
                Orientation = Orientation.Horizontal
            };

            Rectangle rectangle = new Rectangle
            {
                Width = 72,
                Height = 72,
                Fill = new SolidColorBrush(clr),
                Margin = new Thickness(6)
            };
            horzStackPanel.Children.Add(rectangle);
            horzStackPanel.Children.Add(vertStackPanel);
            stackPanel.Children.Add(horzStackPanel);
        }
    }
}


実行結果を示します。
ColorList1

4.10(P138) UserControl の継承

この節では、ColorList1 プロジェクトの StackPanel の子要素を UserControl 化することを説明しています。WPF XAML でも WinRT XAML と同じ操作で ユーザー コントロールを作成することができます。それでは、ColorList2 プロジェクトの ColorItem.xaml の抜粋を示します。

<UserControl x:Class="ColorList2.ColorItem"
             ...
             d:DesignHeight="300" d:DesignWidth="300"><Grid><StackPanel Orientation="Horizontal"><Rectangle Name="rectangle"
                       Width="72"
                       Height="72"
                       Margin="6" /><StackPanel VerticalAlignment="Center"><TextBlock Name="txtblkName"
                           FontSize="24" /><TextBlock Name="txtblkRgb"
                           FontSize="18" /></StackPanel></StackPanel></Grid></UserControl>


次に ColorItem.xaml.cs の抜粋を示します。

public partial class ColorItem : UserControl
{
    public ColorItem(string name, Color clr)
    {
        this.InitializeComponent();

        rectangle.Fill = new SolidColorBrush(clr);
        txtblkName.Text = name;
        txtblkRgb.Text = String.Format("{0:X2}-{1:X2}-{2:X2}-{3:X2}",
                                       clr.A, clr.R, clr.G, clr.B);
    }
}

 

WinRT XAML と同じコードになっていることが理解できると思います。それでは、MainWindow.xaml.cs の抜粋を示します。

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

        IEnumerable<PropertyInfo> properties = typeof(Colors).GetTypeInfo().DeclaredProperties;

        foreach (PropertyInfo property in properties)
        {
            Color clr = (Color)property.GetValue(null);
            ColorItem clrItem = new ColorItem(property.Name, clr);
            stackPanel.Children.Add(clrItem);
        }

    }
}

 

コードは、ColorList1 プロジェクトよりも単純になっています。これは、ユーザーコントロール化したことによって記述するコードが減少したためです。もちろん、実行しても ColorList1 と同じ結果が得られます。

4.11(P141) WinRT ライブラリの作成

この節では、WinRT ライブラリの作成と題していますが、クラス ライブラリ プロジェクトの作成方法を説明しています。タイトルに「WinRT ライブラリ」とある理由は、ユーザー コントロールを含むライブラリを作成するためです。このため、WPF でも同じ方法を利用することがでいます。違いがあるとすれば、Windows ス���ア アプリのプロジェクトでは必要な参照設定が事前に行われていることに対して、デスクトップのクラス ライブラリ プロジェクトでは自分で参照設定を行う必要があることです(WPF のユーザー コントロールを作成しますので、PresentationCore、PresentationFramework、WindowsBase アセンブリへ参照を設定してください)。それでは、PetzoldWindows8Controls プロジェクトの ColorItem.xaml の抜粋を示します。

<UserControl x:Class="PetzoldWindows8Controls.ColorItem"
             ...
             d:DesignHeight="300" d:DesignWidth="300"><Grid><Border BorderBrush="Black"
                BorderThickness="1"
                Width="336"
                Margin="6"><StackPanel Orientation="Horizontal"><Rectangle Name="rectangle"
                           Width="72"
                           Height="72"
                           Margin="6" /><StackPanel VerticalAlignment="Center"><TextBlock Name="txtblkName"
                               FontSize="24" /><TextBlock Name="txtblkRgb"
                               FontSize="18" /></StackPanel></StackPanel></Border></Grid></UserControl>

 

組み込みのスタイルを変更しただけで、WinRT XAML と同じになっています。ColorItem.xaml.cs は、ColorList2 プロジェクトの ColorItem.xaml.cs と同じになります。次に、ColorList3 プロジェクトの MainWindow.xaml.cs の抜粋を示します。

using PetzoldWindows8Controls;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace ColorList3
{
  public partial class MainWindow : Window
  {
    public MainWindow()
    {
      InitializeComponent();

      IEnumerable<PropertyInfo> properties =
                        typeof(Colors).GetTypeInfo().DeclaredProperties;
      foreach (PropertyInfo property in properties)
      {
        Color clr = (Color)property.GetValue(null);
        ColorItem clrItem = new ColorItem(property.Name, clr);
        stackPanel.Children.Add(clrItem);
      }
    }
  }
}

 

コードは WinRT XAML と同じになります。もちろん、実行結果も同じになります。
ColorList3

書籍では、Windows ランタイム コンポーネントに対する言及がありますが、デスクトップ アプリの世界である WPF の世界には Windows ランタイム コンポーネントはありませんので、気にする必要はありません。WPF で作成したクラス ライブラリも .NET Framework のアセンブリですから、Windows Forms と同じようにクラス ライブラリとして使用することができます。作成したライブラリに対する使用方法の制限は、Windows Forms と WPF で相互に GUI コンポーネントを利用する場合になります。相互に利用するには、移行と相互運用のドキュメントを参照してください。

4.12(P144) スクロールの向きの変更

この節では、PetzoldWindows8Controls プロジェクトで作成したクラス ライブラリの利用方法を様々な角度から説明してから、ScrollViewer クラスで水平方向にスクロールさせる方法を説明しています。この説明の中で、VariableSizedWrapGrid パネルという WinRT XAML 固有のパネルを使用します。つまり、WPF で動作させるには、VariableSizedWrapGrid パネルに相当するパネルへ書き換える必要があることになります。このために、WrapPanel を使用して書き換えた ColorWrap プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><ScrollViewer HorizontalScrollBarVisibility="Visible"
                  VerticalScrollBarVisibility="Disabled"><WrapPanel Name="wrapPanel" Orientation="Vertical"/></ScrollViewer></Grid>

 

ScrollViewer の HorizontalScrollBarVisibility と VerticalScrollBarVisibility プロパティを設定しています。このプロパティによって、水平スクロール バーが表示されるようになります。このことが、Windows Forms の VScrollBar と HScrollBar を統合したような機能を持つコントロールとして ScrollViewer があると説明した理由になります。また、WrapPanel の Orientation プロパティを「Vertical」に設定することで、子要素を縦に並べて、あふれた子要素を右側の列へ並べるようになります。結果として、画面をあふれる子要素を表示するために水平スクロールバーが機能するということになります。今度は、ColorWrap プロジェクトの MainWindow.xaml.cs の抜粋を示します。

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

        IEnumerable<PropertyInfo> properties = typeof(Colors).GetTypeInfo().DeclaredProperties;

        foreach (PropertyInfo property in properties)
        {
            Color clr = (Color)property.GetValue(null);
            ColorItem clrItem = new ColorItem(property.Name, clr);
            wrapPanel.Children.Add(clrItem);
        }

    }
}

 

次に実行結果を示します。
ColorWrap

スクロールが横方向になっていることが、確認できると思います。

4.13(P147) Canvas と添付プロパティ

本節で説明するのは、Canvas パネルになります。Canvas パネルを使用すると、Windows Forms の Location プロパティに似た方法で絶対座標をでコントロールを配置できるようになります(絶対座標といっても、XAML という UI 技術が論理座標になっていることを忘れないでください)。この場合に利用するプロパティが、添付プロパティ(Attached Property)と呼ばれるものになります。これらの説明も、書籍で行っています。それでは、TextOnCanvas プロジェクトの MainWindow.xaml の抜粋を示します。

<Window x:Class="TextOnCanvas.MainWindow"
        ...
        FontSize="48"><Grid><Canvas><TextBlock Text="Text on Canvas at (0, 0)"
                       Canvas.Left="0"
                       Canvas.Top="0" /><TextBlock Text="Text on Canvas at (200, 100)"
                       Canvas.Left="200"
                       Canvas.Top="100" /><TextBlock Text="Text on Canvas at (400, 200)"
                       Canvas.Left="400"
                       Canvas.Top="200" /></Canvas></Grid></Window>


TextBlock 要素に Canvas.Top と Canvas.Left プロパティを設定しています。この二つのプロパティが、Windows Forms の Location.X と Location.Y に相当します。そして、Canvas パネル内にコントロールを配置した場合に、Visual Studio のデザイナー上のプロパティ ウィンドウにも Left と Top プロパティが設定できるようになります。
ch04 Canvas Property 

今度は実行結果を示します。
TextOnCanvas

今度は、Canvas の添付プロパティの使い方を確認するために TapAndShowPoint プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><Canvas Name="canvas" /></Grid>


そして、コードで添付プロパティを操作する MainWindow.xaml.cs の抜粋を示します。

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

    protected override void OnTouchDown(TouchEventArgs e)
    {

        //Point pt = e.GetPosition(this);   // 変更
        Point pt = e.GetTouchPoint(this).Position;

        // Create dot
        Ellipse ellipse = new Ellipse
        {
            Width = 3,
            Height = 3,
            Fill = this.Foreground
        };

        Canvas.SetLeft(ellipse, pt.X);
        Canvas.SetTop(ellipse, pt.Y);
        canvas.Children.Add(ellipse);

        // Create text
        TextBlock txtblk = new TextBlock
        {
            Text = String.Format("({0})", pt),
            FontSize = 24,
        };

        Canvas.SetLeft(txtblk, pt.X);
        Canvas.SetTop(txtblk, pt.Y);
        canvas.Children.Add(txtblk);

        e.Handled = true;
        base.OnTouchDown(e);
    }
}


WPF XAML で動作させるために変更したのは、  OnTapped イベントを OnTouchDown イベントにしたことです。XAML に記述していた Canvas.Top などの添付プロパティは、コード上では Canvas オブジェクトの SetTop 静的メソッドと SetLeft 静的メソッドになっていることを確認できることでしょう。また、WinRT XAML の OnTapped イベントを OnTouchDown イベントに変更したことで、座標の取得方法も変更しています。具体的には、TouchEventArgs の GetTouchPoint メソッドを使用することでタッチされた座標を取得しています。実行結果を示します。
TapAndShowPoint

残りの記述も WPF XAML と WinRT XAML では同じですから、書籍を読みことで使い方を理解できることでしょう。

4.14(P152) Z インデックス

本節では、Z インデックス(もしくは、Z オーダー)の使い方を説明しています。基本的に XAML では、記述した順序で Z インデックスが決定されます。つまり、最初が Z インデックスがゼロとなり、次の要素が上になります。要は、記述した順序で要素が上側に配置されるのです。この順序を変更するのが、Z インデックスになります。個人的な考えですが、何らかのアクションで Z インデックスを変更する必要がない限りは、XAML の記述順序を Z インデックスに合わせておく方がトラブルが少ないように考えます(あくまでも、個人の考えです)。書籍では、Z インデックスの使い方を Canvas.ZIndex 添付プロパティを使って説明しています。

4.15(P153) Canvas の注意点

本節では、他のパネルと違って Canvas パネルを使用する上での違いを踏まえて説明していますので、書籍に従って自分で試してみることをお勧めします。

Canvas をどのような場合に使用するかに関しては、書籍にはガイドのような記述はありません。が、私がお勧めするのは自分でグラフィックスなどを描画する場合です。たとえば、ペンを使って描画するアプリなどです。このような描画アプリでは、描画した図形を相対座標より絶対座標で固定する方が利用者の期待に応えることになるからです。つまり、アプリがユーザーに提供する機能によってパネルを選択すべきだと言うことです。その中で、基本的には フレキシブル レイアウトを基本にしながら、必要な箇所には固定レイアウトを組み合わせるということを意識して UI をデザインした方が良いことは、言うまでもありません。

パネルに関して WinRT XAML と WPF XAML には、説明していない違いがあります。具体的には、提供されるパネル コントロールの数は WPF XAML の方が多くある点です。たとえば、DocPanelUniformGridTabPanelなどがあります。一方で、WinRT XAML にしかない WrapGridパネルなどもあります。これらのパネルは、ドキュメントを読んだり自分で試して使い方を学習すると良いでしょう。

ここまで説明してた違いを意識しながら、第4章を読むことで WPF にも書籍の内容を応用することができるようになることでしょう。

プログラミング Windows 第6版 第5章 WPF編

$
0
0

この記事では、「プログラミング Windows 第6版」を使って WPF XAML の学習を支援することを目的にしています。この目的から、書籍と併せて読まれることをお勧めします。

第5章 コントロールとのやりとり

この章では、コントロールとユーザーの操作(タッチ、マウス、スタイラス、キーボードのようなユーザー入力に伴う操作) がどのような関係にあるかについての説明を基本となる FrameworkElement を使って説明しています。

5.1(P155) コントロールの特徴

この節では、FrameworkElement 派生クラスと Control 派生クラスの違いを説明しています。考え方は、WinRT XAML 固有ではなく XAML 系の UI 技術に共通しており、WPF XAML にも適用できますので、書籍を読まれることをお勧めします。若干、WinRT XAML 固有のコントロールやイベントが記述されていますが、この記述は重要ではなく、Control 派生クラスの位置づけを補足するものでしかありません。

5.2(P158) Slider

この節では、RangeBase クラスを継承するコントロールの説明をしています。この説明の中で、WinRT XAML の ScrollBar クラスには、Indeterminate モードがあるとの記述があります。 しかし、WPF XAML の ScrollBar クラスは、Indeterminate プロパティを持たない点に注意してください。WPF XAML では、Indeterminate は ProgerssBar の表示状態として定義されているだけになります。また、ProgressRing コントロールも WPF XAML に無いことにも注意してください。それでは、Silder コントロールの基本的な使い方を示す SlideEvents プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><StackPanel><Slider ValueChanged="OnSliderValueChanged" /><TextBlock HorizontalAlignment="Center" 
                   FontSize="48" /><Slider ValueChanged="OnSliderValueChanged" /><TextBlock HorizontalAlignment="Center" 
                   FontSize="48" /></StackPanel></Grid>

 ValueChanged イベントも同じなので、イベント ハンドラーを定義する MainWindow.xaml.cs の抜粋を示します。

private void OnSliderValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
    Slider slider = sender as Slider;
    Panel parentPanel = slider.Parent as Panel;
    int childIndex = parentPanel.Children.IndexOf(slider);
    TextBlock txtblk = parentPanel.Children[childIndex + 1] as TextBlock;
    txtblk.Text = e.NewValue.ToString();

}

 WinRT XAML と違うのは、イベント引数が RageBaseValueChangedEventArgs 型から RoutedPropertyChangedEventArgs<T> になっていることです。この点を除けば、WPF 版でも一緒になります。従って、書籍の説明もそのまま WPF XAML にも当てはまります。それでは、実行結果を示します。
SliderEvents
スライダーの描画イメージは、WinRT XAML と異なりますが、スライダー コントロールとしての使い方は同じになります。

今度は、イベント ハンドラーではなくバインディングによって、TextBlock の値を更新する SliderBindings プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><Grid.Resources><Style TargetType="TextBlock"><Setter Property="FontSize" Value="48" /><Setter Property="HorizontalAlignment" Value="Center" /></Style></Grid.Resources><StackPanel><Slider x:Name="slider1" /><TextBlock Text="{Binding ElementName=slider1, Path=Value}" /><Slider x:Name="slider2"
                IsDirectionReversed="True"
                TickFrequency="0.01" /><TextBlock Text="{Binding ElementName=slider2, Path=Value}" /><Slider x:Name="slider3"
                Minimum="-1"
                Maximum="1"
                TickFrequency="0.01"
                SmallChange="0.01"
                LargeChange="0.1" /><TextBlock Text="{Binding ElementName=slider3, Path=Value}" /></StackPanel></Grid>

 WinRT XAML との違いは、Slider の StepFrequency プロパティが WPF XAML では StepFrequency プロパティになることです。それでは、実行結果を示します。
SliderBindings

書籍を読むうえで、StepFrequency プロパティを StepFrequency プロパティと読み替えるだけです。TickPlacement プロパティは WPF XAML でも定義されていますので、説明がそのまま WPF XAML でも利用することができます。

5.3(P162) Grid

本節では、基本になるパネルとして良く使われる Grid コントロールを説明しています(WPF アプリケーションで作成される MainWindow.xaml でも Grid が定義されています)。詳しい説明は、書籍を読んで頂くとして、Visual Studio のデザイナーにおける Grid の GUI 操作を簡単に説明します。操作を次のようにします。デザイナーで Grid をクリックしてから、マウス カーソルを左側、もしくは上側の境界へ移動します。そうすると、グリッド線が表示されます(画像は、左側へマウス カーソルを移動したところ)。
ch05 vs designer1
そして、マウスをクリックすると Grid の行(RowDefinition)が追加されます。
ch05 vs designer2
こうすることで、Grid の行(RowDefinition)や列(ColumnDefinition)を GUI 操作で作成することができます。また、デザイナー上でマウス カーソルを行や列の大きさを表示する数字の上へ移動することで、行や列の大きさを GUI で変更することもできます。後は、目的の位置へコントロールをドラッグ & ドロップして配置することで、表の中にコントロールを GUI で配置できるようになります。

それでは、SimpleColorScroll プロジェクトの MainWindow.xaml よスタイル定義を抜粋して示します。

<Window.Resources><Style TargetType="TextBlock"><Setter Property="Text" Value="00" /><Setter Property="FontSize" Value="24" /><Setter Property="HorizontalAlignment" Value="Center" /><Setter Property="Margin" Value="0 12" /></Style><Style TargetType="Slider"><Setter Property="Orientation" Value="Vertical" /><Setter Property="IsDirectionReversed" Value="True" /><Setter Property="Maximum" Value="255" /><Setter Property="HorizontalAlignment" Value="Center" /></Style></Window.Resources>

 サンプルの WPF 化で何度も説明してきたように、リソース定義を Page より Window へ変更しただけになります。それでは、MainWindow.xaml の抜粋を示します。

<Grid><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /><ColumnDefinition Width="3*" /></Grid.ColumnDefinitions><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="*" /><RowDefinition Height="Auto" /></Grid.RowDefinitions><!-- Red --><TextBlock Text="Red"
               Grid.Column="0"
               Grid.Row="0"
               Foreground="Red" /><Slider x:Name="redSlider"
            Grid.Column="0"
            Grid.Row="1"
            Foreground="Red"
            ValueChanged="OnSliderValueChanged" /><TextBlock x:Name="redValue"
               Grid.Column="0"
               Grid.Row="2"
               Foreground="Red" /><!-- Green --><TextBlock Text="Green"
               Grid.Column="1"
               Grid.Row="0"
               Foreground="Green" /><Slider x:Name="greenSlider"
            Grid.Column="1"
            Grid.Row="1"
            Foreground="Green"
            ValueChanged="OnSliderValueChanged" /><TextBlock x:Name="greenValue"
               Grid.Column="1"
               Grid.Row="2"
               Foreground="Green" /><!-- Blue --><TextBlock Text="Blue"
               Grid.Column="2"
               Grid.Row="0"
               Foreground="Blue" /><Slider x:Name="blueSlider"
            Grid.Column="2"
            Grid.Row="1"
            Foreground="Blue"
            ValueChanged="OnSliderValueChanged" /><TextBlock x:Name="blueValue"
               Grid.Column="2"
               Grid.Row="2"
               Foreground="Blue" /><!-- Result --><Rectangle Grid.Column="3"
               Grid.Row="0"
               Grid.RowSpan="3"><Rectangle.Fill><SolidColorBrush x:Name="brushResult"
                             Color="Black" /></Rectangle.Fill></Rectangle></Grid>

 WinRT XAML と同じで 3つの行と 4つの列を定義して、Slider などを Grid.Row と Grid.Coloum 添付プロパティで指定しています。次に、MainWindow.xaml.cs の抜粋を示します。

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

    private void OnSliderValueChanged(object sender, RoutedPropertyChangedEventArgs e)
    {
        byte r = (byte)redSlider.Value;
        byte g = (byte)greenSlider.Value;
        byte b = (byte)blueSlider.Value;
        
        redValue.Text = r.ToString("X2");
        greenValue.Text = g.ToString("X2");
        blueValue.Text = b.ToString("X2");

        brushResult.Color = Color.FromArgb(255, r, g, b);

    }
}

 ここまでに説明した通り、ValueChanged イベントのイベント引数を書き換えただけになります。それでは、実行結果を示します。
SimpleColorScroll

5.4(P169) 向き���アスペクト比

本節では、画面の向きやアスペクト比が変化した(スナップなど)ことに対応する手法を説明しています。この目的で、OrientableColorScroll プロジェクトを使って説明しています。それでは、 MainWindow.xaml の抜粋を示します。

<Grid SizeChanged="OnGridSizeChanged"><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition x:Name="secondColDef" Width="*" /></Grid.ColumnDefinitions><Grid.RowDefinitions><RowDefinition Height="*" /><RowDefinition x:Name="secondRowDef" Height="0" /></Grid.RowDefinitions><Grid Grid.Row="0"
          Grid.Column="0">
        ...</Grid><!-- Result --><Rectangle x:Name="rectangleResult"
               Grid.Column="1"
               Grid.Row="0"><Rectangle.Fill><SolidColorBrush x:Name="brushResult"
                             Color="Black" /></Rectangle.Fill></Rectangle></Grid>

 XAML では Grid の SizeChanged イベントを設定しています。もちろん、内容は WinRT XAML と同じになります。それでは、MainWindow.xaml.cs の イベント ハンドラーを示します。

private void OnGridSizeChanged(object sender, SizeChangedEventArgs e)
{
    // Landscape mode
    if (e.NewSize.Width > e.NewSize.Height)
    {
        secondColDef.Width = new GridLength(1, GridUnitType.Star);
        secondRowDef.Height = new GridLength(0);

        Grid.SetColumn(rectangleResult, 1);
        Grid.SetRow(rectangleResult, 0);
    }
    // Portrait mode
    else
    {
        secondColDef.Width = new GridLength(0);
        secondRowDef.Height = new GridLength(1, GridUnitType.Star);

        Grid.SetColumn(rectangleResult, 0);
        Grid.SetRow(rectangleResult, 1);
    }

}

 コードを読めば書籍と同じことが理解できますが、画面の向きに合わせるために SizeChanged イベントで横幅と縦幅を比較して Grid.Row と Grid.Column 添付プロパティを SetRow と SetColumn メソッドで設定しています。それでは、実行結果を示します。
OrientableColorScroll2
SimpleColorScroll プロジェクトに対して縦長(ポートレイト)の場合に色の変化する場所が下側に移動しています。もちろん、横長(ランドスケープ)であれば同じデザインになります。

5.5(P171) Silder と FormattedStringConverter

本節では、SimpleColorScroll プロジェクトなどで使用している Slider コントールの値を 16 進数で表示していることに、疑問を提示しています。そして、コンバーターを作成してバインディングした方が合理的ではないかという問題提起をしています。これは、次節などで具体例を説明するための問いかけになっていますので、書籍を熟読されることをお勧めします。

5.6(P172) ツールチップと変換

本節では、コンバーターを作成して 5.5 で説明した考え方の実践を説明しています。コンバーターに関しては、第4章で作り方を説明していますので、ここでは WPF XAML でとの違いなどの説明は省略します。それでは、ColorScrollWithValueConverter プロジェクトの DoubleToStringHexByteConverter.cs を示します。

using System;
using System.Windows.Data;

namespace ColorScrollWithValueConverter
{
    public class DoubleToStringHexByteConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return ((int)(double)value).ToString("X2");
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value;
        }
    }
}

 コンバーターを使用するためのリソース定義を MainWindow.xaml より抜粋して示します。

<Window.Resources><local:DoubleToStringHexByteConverter x:Key="hexConverter" />
    ...</Window.Resources>

 この定義も第4章で説明したものになります。それでは、バインディングを書き換えた箇所を MainWindow.xaml より抜粋して示します。

<!-- Red --><TextBlock Text="Red"
           Grid.Column="0"
           Grid.Row="0"
           Foreground="Red" /><Slider x:Name="redSlider"
        Grid.Column="0"
        Grid.Row="1"
        AutoToolTipPlacement="BottomRight"
        Foreground="Red"
        ValueChanged="OnSliderValueChanged" /><TextBlock Text="{Binding ElementName=redSlider, 
                          Path=Value,
                          Converter={StaticResource hexConverter}}"
           Grid.Column="0"
           Grid.Row="2"
           Foreground="Red" />

 WinRT XAML と異なる箇所は、ThumbToolTipValueConverter プロパティが無いことです。ThumbToolTipValueConverter プロパティは、WinRT XAML 固有であり、スライダーの Thumb を動かす場合に表示するツールチップに対するコンバーターを指定するものになります。このプロパティが WPF XAML の Slider には無いので、どうしても同じようなツールチップを作成するには、2つの方法が考えられます。1つは、Silder の値を整数にして、コンバーターなどで 16進数に変換するという方法です。もう 1つは、独自の Slider コントロールを作成して、コンバーターなどを指定できるようにする方法になります。それでは、MainWindow.xaml.cs の ValueChanged イベント ハンドラーを示します。

private void OnSliderValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
    byte r = (byte)redSlider.Value;
    byte g = (byte)greenSlider.Value;
    byte b = (byte)blueSlider.Value;

    brushResult.Color = Color.FromArgb(255, r, g, b);

}

 既に説明したイベント引数の変更を除けば、WinRT XAML と同一なのがわかることでしょう。もちろん、実行しても同じになります。書籍では、brushResult に対してバインディングで記述が可能かどうかの検討過程を説明していますので、書籍で確認してみてください。

5.7(P175) スライダーによるスケッチ

本節では、Silder コントロールを使って Polyline によって図形を描画する方法を説明しています。最初に SliderSketch プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><Grid.RowDefinitions><RowDefinition Height="*" /><RowDefinition Height="Auto" /></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="Auto" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><Slider x:Name="ySlider"
            Grid.Row="0"
            Grid.Column="0"
            Orientation="Vertical"
            IsDirectionReversed="True"
            Margin="0 18"
            ValueChanged="OnSliderValueChanged" /><Slider x:Name="xSlider"
            Grid.Row="1"
            Grid.Column="1"
            Margin="18 0"
            ValueChanged="OnSliderValueChanged" /><Border Grid.Row="0"
            Grid.Column="1"
            BorderBrush="Black"
            BorderThickness="3 0 0 3"
            Background="#C0C0C0"
            Padding="24"
            SizeChanged="OnBorderSizeChanged"><Polyline x:Name="polyline"
                  Stroke="#404040"
                  StrokeThickness="3"
                  Points="0 0" /></Border></Grid>

 WPF 用に変更しているのは、Grid の Background と Border の BorderBrush に設定した組み込みスタイルのみになります。書籍では、WinRT XAML には DockPanel が無いと説明していますが、この記事は WPF XAML ですから DockPanel は存在します。でも、書籍のようにな考え方で、同じようなパネルにすることができるということを学ぶことは、とても大切です。それでは、MainWindow.xaml.cs の抜粋を示します。

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

    private void OnSliderValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
        polyline.Points.Add(new Point(xSlider.Value, ySlider.Value));

    }

    private void OnBorderSizeChanged(object sender, SizeChangedEventArgs e)
    {
        Border border = sender as Border;
        xSlider.Maximum = e.NewSize.Width - border.Padding.Left
                                          - border.Padding.Right
                                          - polyline.StrokeThickness;

        ySlider.Maximum = e.NewSize.Height - border.Padding.Top
                                           - border.Padding.Bottom
                                           - polyline.StrokeThickness;

    }
}

 OnSliderValueChanged イベント ハンドラーの引数の型が異なるだけで、その他のコードは同じになります。書籍には、実行結果が掲載されていませんが、実行結果を示します。
SliderSketch

スライダーを動かすことで線が描画されていくのを確認することができます。

5.8(P176) さまざまなボタン

本節では、WinRT XAML がサポートする様々なボタンを具体的に説明しています。そのボタンを使ってみるために ButtonVarieties プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><StackPanel><Button Content="Just a plain old Button" /><!-- HyperlinkButton Content="HyperlinkButton" /--><TextBlock><Hyperlink><Run Text="Hyplerlink Text" /></Hyperlink></TextBlock><RepeatButton Content="RepeatButton" Width="100" HorizontalAlignment="Left" /><ToggleButton Content="ToggleButton" Width="100" HorizontalAlignment="Left" /><CheckBox Content="CheckBox" /><RadioButton Content="RadioButton #1" /><RadioButton>RadioButton #2</RadioButton><RadioButton><RadioButton.Content>
                RadioButton #3</RadioButton.Content></RadioButton><RadioButton><RadioButton.Content><TextBlock Text="RadioButton #4" /></RadioButton.Content></RadioButton><!--ToggleSwitch /--></StackPanel></Grid>

 XAML の定義では RepeatButton と Toggle Button にHorizontalAlignment を設定してるのは、配置が中央になってしまうためです。そして、WinRT XAML と WPF XAML では、使用できるボタンに違いがあります。この理由は、WinRT XAML がタッチに最適化したボタンなどを提供しているからです。ButtonVarieties プロジェクトにおいて WPF XAML と異なるボタンを示します。

WinRTWPF説明
HyperLinkButtonHyperLinkWPF には HyperLinkButton はありません。
ToggleSwitch無しWPF には ToggleSwitch はありません。簡易な方法としては、Slider をゼロと 1 にして使用する方法があります。また、サードパーティー コントロールなどを使用することも考えられます。

実行結果を示します。
ButtonVarieties

ToggleSwitch コントロール以外の説明は、WPF XAML でも同じなので書籍を熟読すれば、これらのコントロールの使い方を理解できることでしょう。RadioButton は、Border でグループ化することを説明していますが、StackPanel などのパネル コントロールを使ってもグループ化することができます。そして、書籍では Button コントロールの Content として StackPanel、Image、TextBlock を設定する説明が続きますので、自分で試してみることをお勧めします。

それでは、SimpleKeypad プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><Grid HorizontalAlignment="Center"
          VerticalAlignment="Center"
          Width="288"><Grid.Resources><Style TargetType="Button"><Setter Property="ClickMode" Value="Press" /><Setter Property="HorizontalAlignment" Value="Stretch" /><Setter Property="Height" Value="72" /><Setter Property="FontSize" Value="36" /></Style></Grid.Resources><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions><Border Grid.Column="0"
                    HorizontalAlignment="Left"><TextBlock x:Name="resultText"
                           HorizontalAlignment="Right"
                           VerticalAlignment="Center"
                           FontSize="24" /></Border><Button x:Name="deleteButton"
                    Content="⇦"
                    Grid.Column="1"
                    IsEnabled="False"
                    FontFamily="Segoe Symbol"
                    HorizontalAlignment="Left"
                    Padding="0"
                    BorderThickness="0"
                    Click="OnDeleteButtonClick" /></Grid><Button Content="1"
                Grid.Row="1" Grid.Column="0"
                Click="OnCharButtonClick" /><Button Content="2"
                Grid.Row="1" Grid.Column="1"
                Click="OnCharButtonClick" /><Button Content="3"
                Grid.Row="1" Grid.Column="2"
                Click="OnCharButtonClick" /><Button Content="4"
                Grid.Row="2" Grid.Column="0"
                Click="OnCharButtonClick" /><Button Content="5"
                Grid.Row="2" Grid.Column="1"
                Click="OnCharButtonClick" /><Button Content="6"
                Grid.Row="2" Grid.Column="2"
                Click="OnCharButtonClick" /><Button Content="7"
                Grid.Row="3" Grid.Column="0"
                Click="OnCharButtonClick" /><Button Content="8"
                Grid.Row="3" Grid.Column="1"
                Click="OnCharButtonClick" /><Button Content="9"
                Grid.Row="3" Grid.Column="2"
                Click="OnCharButtonClick" /><Button Content="*"
                Grid.Row="4" Grid.Column="0"
                Click="OnCharButtonClick" /><Button Content="0"
                Grid.Row="4" Grid.Column="1"
                Click="OnCharButtonClick" /><Button Content="#"
                Grid.Row="4" Grid.Column="2"
                Click="OnCharButtonClick" /></Grid></Grid>

 XAML の定義自体は、WPF XAML でも同じになります。今度は、MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
  string inputString = "";
  char[] specialChars = { '*', '#' };

  public MainWindow()
  {
      InitializeComponent();
  }

  private void OnCharButtonClick(object sender, RoutedEventArgs e)
  {
    Button btn = sender as Button;
    inputString += btn.Content as string;
    FormatText();
  }

  private void OnDeleteButtonClick(object sender, RoutedEventArgs e)
  {
    inputString = inputString.Substring(0, inputString.Length - 1);
    FormatText();
  }

  void FormatText()
  {
    bool hasNonNumbers = inputString.IndexOfAny(specialChars) != -1;
    if (hasNonNumbers || inputString.Length < 4 || inputString.Length > 10)
      resultText.Text = inputString;
    else if (inputString.Length < 8)
      resultText.Text = String.Format("{0}-{1}", inputString.Substring(0, 3),
                                                 inputString.Substring(3));
    else
      resultText.Text = String.Format("({0}) {1}-{2}", inputString.Substring(0, 3),
                                                       inputString.Substring(3, 3),
                                                       inputString.Substring(6));
    deleteButton.IsEnabled = inputString.Length > 0;
  }
}

 コードも WinRT XAML と同じになりますので、実行結果を示します。
SimpleKeypad
書籍に詳しい説明が記述されていますので、書籍を熟読してください。

5.9(P185) 依存関係プロパティの定義

本節では、依存関係プロパティ(Dependency Property)を説明するためにカスタム ボタン コントロールを使って説明しています。本当の意味で、カスタム コントロールの作り方を説明するものではありませんが、依存関係プロパティはユーザー コントロールなどを含めて利用する機会が多いので、基本となる考え方を学習するのは、とても重要です。それでは、DependecyProperties プロジェクトの GradientButton.cs を示します。

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace DependencyProperties
{
    public class GradientButton : Button
    {
        GradientStop gradientStop1, gradientStop2;

        static GradientButton()
        {
            Color1Property =
                DependencyProperty.Register("Color1",
                    typeof(Color),
                    typeof(GradientButton),
                    new PropertyMetadata(Colors.White, OnColorChanged));

            Color2Property =
                DependencyProperty.Register("Color2",
                    typeof(Color),
                    typeof(GradientButton),
                    new PropertyMetadata(Colors.Black, OnColorChanged));
        }

        public static DependencyProperty Color1Property { private set; get; }

        public static DependencyProperty Color2Property { private set; get; }

        public GradientButton()
        {
            gradientStop1 = new GradientStop
            {
                Offset = 0,
                Color = this.Color1
            };

            gradientStop2 = new GradientStop
            {
                Offset = 1,
                Color = this.Color2
            };

            LinearGradientBrush brush = new LinearGradientBrush();
            brush.GradientStops.Add(gradientStop1);
            brush.GradientStops.Add(gradientStop2);

            this.Foreground = brush;
        }

        public Color Color1
        {
            set { SetValue(Color1Property, value); }
            get { return (Color)GetValue(Color1Property); }
        }

        public Color Color2
        {
            set { SetValue(Color2Property, value); }
            get { return (Color)GetValue(Color2Property); }
        }

        static void OnColorChanged(DependencyObject obj,
                                   DependencyPropertyChangedEventArgs args)
        {
            (obj as GradientButton).OnColorChanged(args);
        }

        void OnColorChanged(DependencyPropertyChangedEventArgs args)
        {
            if (args.Property == Color1Property)
                gradientStop1.Color = (Color)args.NewValue;

            gradientStop1.Color = this.Color1;

            if (args.Property == Color2Property)
                gradientStop2.Color = (Color)args.NewValue;
        }
    }
}

 書籍に記述されていますが、依存関係プロパティの定義は、2つのことから成り立ちます。

  • DependencyProperty 型の静的フィールドの定義
    フィールドの初期化として、DependencyProperty の Register メソッドの呼び出し。第一引数の文字列が、定義すべきプロパティになります。
  • プロパティの定義
    Getter と Setter の実装は、DependencyProperty の GetValue と SetValue メソッドを使用します。

それでは、作成した GradientButton カスタム コントロールを使用する MainWindow.xaml の抜粋を示します。

<Window 
        ...
        xmlns:local="clr-namespace:DependencyProperties"
        ... ><Window.Resources><Style x:Key="baseButtonStyle" TargetType="local:GradientButton"><Setter Property="FontSize" Value="48" /><Setter Property="HorizontalAlignment" Value="Center" /><Setter Property="Margin" Value="0 12" /></Style><Style x:Key="blueRedButtonStyle" 
               TargetType="local:GradientButton"
               BasedOn="{StaticResource baseButtonStyle}"><Setter Property="Color1" Value="Blue" /><Setter Property="Color2" Value="Red" /></Style></Window.Resources><Grid><StackPanel><local:GradientButton Content="GradientButton #1"
                                  Style='{StaticResource baseButtonStyle}' /><local:GradientButton Content='GradientButton #2'
                                  Style='{StaticResource blueRedButtonStyle}' /><local:GradientButton Content='GradientButton #3'
                                  Style='{StaticResource baseButtonStyle}'
                                  Color1='Aqua'
                                  Color2='Lime' /></StackPanel></Grid></Window>

ここまでに説明してきた Page を Window へ、組み込みスタイルを変更という書き換えを行っただけで、カスタム コントロールの使用方法が全く同じことがわかります。それでは、実行結果を示します。
 DependencyProperties

今度は、GridentButton を XAML 定義との併用で作成するために UserControl を出発点としてカスタム コントロールとして実装する方法を説明しています。それでは、DependencyPropertiesWithBindings プロジェクトの GradientButton.xaml.cs を示します。

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace DependencyPropertiesWithBindings
{
    /// 
    /// GradientButton.xaml の相互作用ロジック
    /// 
    public partial class GradientButton : Button
    {
        static GradientButton()
        {
            Color1Property =
                DependencyProperty.Register("Color1",
                    typeof(Color),
                    typeof(GradientButton),
                    new PropertyMetadata(Colors.White));

            Color2Property =
                DependencyProperty.Register("Color2",
                    typeof(Color),
                    typeof(GradientButton),
                    new PropertyMetadata(Colors.Black));
        }

        public static DependencyProperty Color1Property { private set; get; }

        public static DependencyProperty Color2Property { private set; get; }

        public GradientButton()
        {
            this.InitializeComponent();
        }

        public Color Color1
        {
            set { SetValue(Color1Property, value); }
            get { return (Color)GetValue(Color1Property); }
        }

        public Color Color2
        {
            set { SetValue(Color2Property, value); }
            get { return (Color)GetValue(Color2Property); }
        }
    }
}

 このコードは、GridentButton.cs の OnColorChanged イベント ハンドラーが無いことと DependencyProperty の Register メソッドの引数が異なることを除けば同じになります(もちろん、WinRT XAML と同じです)。今度は、UI を定義する GradientButton.xaml を示します。

<Button x:Class="DependencyPropertiesWithBindings.GradientButton"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
        mc:Ignorable="d" 
        d:DesignHeight="300" d:DesignWidth="300"
        x:Name="root"><Button.Foreground><LinearGradientBrush><GradientStop Offset="0" 
                          Color="{Binding ElementName=root, 
                                          Path=Color1}" /><GradientStop Offset="1" 
                          Color="{Binding ElementName=root, 
                                          Path=Color2}" /></LinearGradientBrush></Button.Foreground></Button>

GridentButton.cs の OnColorChanged イベント ハンドラーで実装していた LinearGradientBrush に対する操作が XAML での定義になっています(もちろん、WinRT XAML と同じです)。そして、実行結果も GridentButton と同じになります。このようにカスタム コントロールを定義するにしても、コードのみで実装する手法や XAML 定義と併用する手法を選択することができることも、XAML 系の UI 技術の柔軟性を表しています。

5.10(P196) RadioButton

本節では、RadioButton の使い方を示すことを目的に PolyLine のコーナーを RadioButton で変化させるさせて説明しています。この目的の、LineCapsAndJoins プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><Grid.RowDefinitions><RowDefinition Height="*" /><RowDefinition Height="Auto" /></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="Auto" /><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions><StackPanel x:Name="startLineCapPanel"
                Grid.Row="0" Grid.Column="0"
                Margin="24"><RadioButton Content="Flat start"
                     Checked="OnStartLineCapRadioButtonChecked"><RadioButton.Tag><PenLineCap>Flat</PenLineCap></RadioButton.Tag></RadioButton><RadioButton Content="Round start"
                     Checked="OnStartLineCapRadioButtonChecked"><RadioButton.Tag><PenLineCap>Round</PenLineCap></RadioButton.Tag></RadioButton><RadioButton Content="Square start"
                     Checked="OnStartLineCapRadioButtonChecked"><RadioButton.Tag><PenLineCap>Square</PenLineCap></RadioButton.Tag></RadioButton><RadioButton Content="Triangle start"
                     Checked="OnStartLineCapRadioButtonChecked"><RadioButton.Tag><PenLineCap>Triangle</PenLineCap></RadioButton.Tag></RadioButton></StackPanel><StackPanel Name="endLineCapPanel"
                Grid.Row="0" Grid.Column="2"
                Margin="24"><RadioButton Content="Flat end"
                     Checked="OnEndLineCapRadioButtonChecked"><RadioButton.Tag><PenLineCap>Flat</PenLineCap></RadioButton.Tag></RadioButton><RadioButton Content="Round end"
                     Checked="OnEndLineCapRadioButtonChecked"><RadioButton.Tag><PenLineCap>Round</PenLineCap></RadioButton.Tag></RadioButton><RadioButton Content="Square end"
                     Checked="OnEndLineCapRadioButtonChecked"><RadioButton.Tag><PenLineCap>Square</PenLineCap></RadioButton.Tag></RadioButton><RadioButton Content="Triangle End"
                     Checked="OnEndLineCapRadioButtonChecked"><RadioButton.Tag><PenLineCap>Triangle</PenLineCap></RadioButton.Tag></RadioButton></StackPanel><StackPanel x:Name="lineJoinPanel"
                Grid.Row="1" Grid.Column="1"
                HorizontalAlignment="Center"
                Margin="24"><RadioButton Content="Bevel join"
                     Checked="OnLineJoinRadioButtonChecked"><RadioButton.Tag><PenLineJoin>Bevel</PenLineJoin></RadioButton.Tag></RadioButton><RadioButton Content="Miter join"
                     Checked="OnLineJoinRadioButtonChecked"><RadioButton.Tag><PenLineJoin>Miter</PenLineJoin></RadioButton.Tag></RadioButton><RadioButton Content="Round join"
                     Checked="OnLineJoinRadioButtonChecked"><RadioButton.Tag><PenLineJoin>Round</PenLineJoin></RadioButton.Tag></RadioButton></StackPanel><Polyline x:Name="polyline"
              Grid.Row="0"
              Grid.Column="1"
              Points="0 0, 500 1000, 1000 0"
              Stroke="Black"
              StrokeThickness="100"
              Stretch="Fill"
              Margin="24" /></Grid>

 WPF 向けに変更しているのは、これまでに説明しているものと同じです。つまり、組み込みのスタイル(Grid の Background と Polyline の Stroke) を変更しています。3 つの StackPanel で RadioButton をグループ化しており、Checked イベント ハンドラーを設定しているのも同じです。それでは、MainWindow.xaml.cs の抜粋を示します。

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

        Loaded += (sender, args) =>
        {
            foreach (UIElement child in startLineCapPanel.Children)
                (child as RadioButton).IsChecked =
                    (PenLineCap)(child as RadioButton).Tag == polyline.StrokeStartLineCap;

            foreach (UIElement child in endLineCapPanel.Children)
                (child as RadioButton).IsChecked =
                    (PenLineCap)(child as RadioButton).Tag == polyline.StrokeEndLineCap;

            foreach (UIElement child in lineJoinPanel.Children)
                (child as RadioButton).IsChecked =
                    (PenLineJoin)(child as RadioButton).Tag == polyline.StrokeLineJoin;
        };
    }

    private void OnStartLineCapRadioButtonChecked(object sender, RoutedEventArgs e)
    {
        polyline.StrokeStartLineCap = (PenLineCap)(sender as RadioButton).Tag;
    }

    private void OnEndLineCapRadioButtonChecked(object sender, RoutedEventArgs e)
    {
        polyline.StrokeEndLineCap = (PenLineCap)(sender as RadioButton).Tag;
    }

    private void OnLineJoinRadioButtonChecked(object sender, RoutedEventArgs e)
    {
        polyline.StrokeLineJoin = (PenLineJoin)(sender as RadioButton).Tag;
    }
}

 コードも WinRT XAML と同じになりますので、実行結果を示します。
LineCapsAndJoins

今度は、マークアップの冗長な記述を削減するためにカスタム コントロールを作成する方法を説明しています。それでは、LineCapsAndJoinsWithCustomClass プロジェクトの LineCapRadioButton.cs を示します。

using System.Windows.Controls;
using System.Windows.Media;

namespace LineCapsAndJoinsWithCustomClass
{
    public class LineCapRadioButton : RadioButton
    {
        public PenLineCap LineCapTag { set; get; }
    }
}

 コードは WinRT XAML と同じになります。それでは、LineCapsAndJoinsWithCustomClass プロジェクトの LineJoinRadioButton.cs を示します。

using System.Windows.Controls;
using System.Windows.Media;

namespace LineCapsAndJoinsWithCustomClass
{
    public class LineJoinRadioButton : RadioButton
    {
        public PenLineJoin LineJoinTag { set; get; }
    }
}

このコードも WinRT XAML と同じです。それでは、作成したカスタム コントロールを使用する MainWindow.xaml の抜粋を示します。

<StackPanel x:Name="lineJoinPanel"
            Grid.Row="1" Grid.Column="1"
            HorizontalAlignment="Center"
            Margin="24"><local:LineJoinRadioButton Content="Bevel join"
                               LineJoinTag="Bevel"
                               Checked="OnLineJoinRadioButtonChecked" /><local:LineJoinRadioButton Content="Miter join"
                               LineJoinTag="Miter"
                               Checked="OnLineJoinRadioButtonChecked" /><local:LineJoinRadioButton Content="Round join"
                               LineJoinTag="Round"
                               Checked="OnLineJoinRadioButtonChecked" /></StackPanel>

 それでは、MainWindow.xaml.cs の抜粋を示します。

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

        Loaded += (sender, args) =>
        {
            foreach (UIElement child in startLineCapPanel.Children)
                (child as LineCapRadioButton).IsChecked =
                    (child as LineCapRadioButton).LineCapTag == polyline.StrokeStartLineCap;

            foreach (UIElement child in endLineCapPanel.Children)
                (child as LineCapRadioButton).IsChecked =
                    (child as LineCapRadioButton).LineCapTag == polyline.StrokeEndLineCap;

            foreach (UIElement child in lineJoinPanel.Children)
                (child as LineJoinRadioButton).IsChecked =
                    (child as LineJoinRadioButton).LineJoinTag == polyline.StrokeLineJoin;
        };
    }

    private void OnStartLineCapRadioButtonChecked(object sender, RoutedEventArgs e)
    {
        polyline.StrokeStartLineCap = (sender as LineCapRadioButton).LineCapTag;
    }

    private void OnEndLineCapRadioButtonChecked(object sender, RoutedEventArgs e)
    {
        polyline.StrokeEndLineCap = (sender as LineCapRadioButton).LineCapTag;
    }

    private void OnLineJoinRadioButtonChecked(object sender, RoutedEventArgs e)
    {
        polyline.StrokeLineJoin = (sender as LineJoinRadioButton).LineJoinTag;
    }
}

 コードも XAML も WinRT XAML と同じになります。ここまで説明を読めば、一部の違いを乗り越えれば WinRT XAML と WPF XAML では共通の知識が利用可能であることを理解できたのではないでしょうか。

5.11(P203) キーボード入力と TextBox

本節では、キーボード入力を扱う方法に関して説明をしています。ここでは、WinRT XAML の世界が中心になることから、ソフトウェア キーボードなどをどのように制御するかという説明になっていることから、WPF XAML と異なることが多くなります。それでは、タッチ キーボードのキーボード レイアウトを制御する InputScope を使った TextBoxInputScopes プロジェクトの MainWindow.xaml の抜粋を示します。

<Window 
        ... ><Window.Resources><Style TargetType="TextBlock"><Setter Property="FontSize" Value="24" /><Setter Property="VerticalAlignment" Value="Center" /><Setter Property="Margin" Value="6" /></Style><Style TargetType="TextBox"><Setter Property="FontSize" Value="24" /><!-- 追加 --><Setter Property="Width" Value="320" /><Setter Property="VerticalAlignment" Value="Center" /><Setter Property="Margin" Value="0 6" /></Style></Window.Resources><Grid><Grid HorizontalAlignment="Center"><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="Auto" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions><!-- Multiline with Return, no wrapping --><TextBlock Text="Multiline (accepts Return, no wrap):"
                       Grid.Row="0" Grid.Column="0" /><TextBox AcceptsReturn="True"
                     Grid.Row="0" Grid.Column="1" /><!-- Multiline with no Return, wrapping --><TextBlock Text="Multiline (ignores Return, wraps):"
                       Grid.Row="1" Grid.Column="0" /><TextBox TextWrapping="Wrap"
                     Grid.Row="1" Grid.Column="1" /><!-- Multiline with Return and wrapping --><TextBlock Text="Multiline (accepts Return, wraps):"
                       Grid.Row="2" Grid.Column="0" /><TextBox AcceptsReturn="True"
                     TextWrapping="Wrap"
                     Grid.Row="2" Grid.Column="1" /><!-- Default input scope --><TextBlock Text="Default input scope:"
                       Grid.Row="3" Grid.Column="0" /><TextBox Grid.Row="3" Grid.Column="1"
                     InputScope="Default" /><!-- Email address input scope --><TextBlock Text="Email address input scope:"
                       Grid.Row="4" Grid.Column="0" /><TextBox Grid.Row="4" Grid.Column="1"
                     InputScope="EmailSmtpAddress" /><!-- Number input scope --><TextBlock Text="Number input scope:"
                       Grid.Row="5" Grid.Column="0" /><TextBox Grid.Row="5" Grid.Column="1"
                     InputScope="Number" /><!-- Search input scope --><TextBlock Text="全角ひらがな:"
                       Grid.Row="6" Grid.Column="0" /><TextBox Grid.Row="6" Grid.Column="1"
                     InputMethod.PreferredImeState="On"
                     InputMethod.PreferredImeConversionMode="FullShape,Native" /><!-- Telephone number input scope --><TextBlock Text="Telephone number input scope:"
                       Grid.Row="7" Grid.Column="0" /><TextBox Grid.Row="7" Grid.Column="1"
                     InputScope="TelephoneNumber" /><!-- URL input scope --><TextBlock Text="URL input scope:"
                       Grid.Row="8" Grid.Column="0" /><TextBox Grid.Row="8" Grid.Column="1"
                     InputScope="Url" /><!-- PasswordBox --><TextBlock Text="PasswordBox:"
                       Grid.Row="9" Grid.Column="0" /><PasswordBox Grid.Row="9" Grid.Column="1" /></Grid></Grid></Window>

 WinRT XAML と WPF XAML の大きな違いは、InputScope に「Search」が指定できないことです。このため WPF XAML では、InputMethod の 添付プロパティを設定して IME の制御方法を記述しています。

  • PreferredImeState によって、IME のオンやオフを制御できます。
  • PreferredImeConversionMode によって、IME のモードを制御できます。
    FullShape,Native は、全角(FullShape)ひらがな(Naitive)になります。

この IME を制御する添付プロパティは、残念ながら Visual Studio のプロパティ ウィンドウで設定することができませんので、直接 XAML を編集する必要があります。Windows Forms であれば、ImeMode プロパティを設定できますが、この点が Windows Forms と大きく違うところなので注意してください(これは、XAML 系の UI 技術が ウィンドウ ハンドルを使用していないことによる制限です)。一方で、WinRT XAML では IME の制御を行うことはできません。できることは、ソフトウェア キーボードを表示するか、非表示にするかというだけになります。

また、入力系のアプリを作成する上で問題になるのが、入力順序となるタブ オーダー(TabIndex)の設定になります。Windows Forms では、タブ オーダーをデザイナーでビジュアルに設定することもできます。
ch05 tab order

WPF アプリケーションでは、TabIndex プロパティを設定するしかありません。プロパティ ウィンドウの共通カテゴリの中にあります(共通カテゴリとは、Control に共通するという意味です)。
ch05 tab index

この意味では、可能な限り XAML の定義順序が入力順序になるように編集することをお勧めします。 また、フォーカスの概要というドキュメントを参照してください。それから、Windows 8/8.1 の IME 設定は、デフォルトがシステム全体で有効になるように設定されています。つまり、IME の オンとオフがシステム全体で統一されています(多分、Windows ストア アプリで統一的に IME を使えるようにしたかったのではないかと考えられます)。この設定を変更するには、コントロール パネルで使用します([言語]-[詳細設定]-[入力方式の切り替え])。
ch05 ime control panel

タブ オーダーの設定方法が、Windows Forms と XAML 系で異なる理由は、ウィンドウ ハンドルを使用する GDI をベースにしているかどうかに左右されています。しかし、アプリの基本的なタブ オーダーは、デザイナーにコントロールをドラッグ & ドロップしていった順序であることを思い出してください。Windows Forms では、不可能ではありませんが配置したコントロールの定義順序を変更するのは非常に面倒な作業になります。ですから、タブ オーダーの編集機能がデザイナーで提供されています。一方で、XAML 系のデザイナーはタブ オーダーのビジュアルな編集機能を提供していませんが、XAML エディターを使用すればコピー・ペーストでコントロールの定義順序を入れ替えることは簡単にできます。できないことをマイナスに考えるのではなく、別の方法があるのではないかというプラス思考で学習することをお勧めします。

5.12(P207) タッチと Thumb

本節では、ドラッグ可能な Thumb コントロールを説明しています。タイトルにタッチとあるのは、WinRT XAML がタッチにネイティブに対応しているからであり、WPF XAML ではマウスやタッチを OS が統一的に使用できるようにしていますから、コントロールをドラッグできると理解すれば良いでしょう。それでは、AlphabetBlocks プロジェクトの Block.xaml を示します。

<UserControl x:Class="AlphabetBlocks.Block"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="144" d:DesignWidth="144"
             Height="{Binding ActualWidth, ElementName=root}"
             x:Name="root"><Grid><Thumb DragStarted="OnThumbDragStarted"
               DragDelta="OnThumbDragDelta"
               Margin="18 18 6 6" /><!-- Left --><Polygon Points="0 6, 12 18, 12 138, 0 126"
                 Fill="#E0C080" /><!-- Top --><Polygon Points="6 0, 18 12, 138 12, 126 0"
                 Fill="#F0D090" /><!-- Edge --><Polygon Points="6 0, 18 12, 12 18, 0 6"
                 Fill="#E8C888" /><Border BorderBrush="{Binding ElementName=root, Path=Foreground}"
                BorderThickness="12"
                Background="#FFE0A0"
                CornerRadius="6"
                Margin="12 12 0 0"
                IsHitTestVisible="False" /><TextBlock FontFamily="Courier New"
                   FontSize="156"
                   FontWeight="Bold"
                   Text="{Binding ElementName=root, Path=Text}"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   Margin="12 18 0 0"
                   IsHitTestVisible="False" /></Grid></UserControl>

XAML の定義は、WinRT と同じになります。それでは、Text という依存関係プロパティを定義した、Block.xaml.cs を示します。

using System.Windows;
using System.Windows.Controls;

namespace AlphabetBlocks
{
    public partial class Block : UserControl
    {
        static int zindex;

        public Block()
        {
            InitializeComponent();
        }

        static Block()
        {
            TextProperty = DependencyProperty.Register("Text",
                typeof(string),
                typeof(Block),
                new PropertyMetadata("?"));
        }

        public static DependencyProperty TextProperty { private set; get; }

        public static int ZIndex
        {
            get { return ++zindex; }
        }

        public string Text
        {
            set { SetValue(TextProperty, value); }
            get { return (string)GetValue(TextProperty); }
        }

        private void OnThumbDragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e)
        {
            Canvas.SetZIndex(this, ZIndex);
        }

        private void OnThumbDragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
        {
            Canvas.SetLeft(this, Canvas.GetLeft(this) + e.HorizontalChange);
            Canvas.SetTop(this, Canvas.GetTop(this) + e.VerticalChange);
        }

    }
}

WPF XAML へ移植するために変更したのは、インスタンス コンストラクターを残した点だけです。それ以外は、同じになっています。この理由は、UserControl プロジェクトを作成するとインスタンス コンストラクターが存在するからという理由と WPF XAML では WinRT XAML と違ってインスタンス コンストラクターがないとユーザー コントロールが描画されなくなるからです。次に、AlphabetBlocks プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid SizeChanged="OnGridSizeChanged"><TextBlock Text="Alphabet Blocks"
               FontStyle='Italic'
               FontWeight='Bold'
               FontSize='96'
               TextWrapping='Wrap'
               HorizontalAlignment='Center'
               VerticalAlignment='Center'
               TextAlignment='Center'
               Opacity='0.1' /><Canvas x:Name='buttonCanvas' /><Canvas x:Name='blockcanvas' /></Grid>

 XAML の定義は、組み込みのスタイル定義を除けば WinRT XAML と同じになります。今度は、MainWindows.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    const double BUTTON_SIZE = 60;
    const double BUTTON_FONT = 18;
    const double BOTTOM_MARGIN = 20;    // マージン
    string blockChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!?-+*/%=";
    Color[] colors = { Colors.Red, Colors.Green, Colors.Orange, Colors.Blue, Colors.Purple };
    Random rand = new Random();

    public MainWindow()
    {
        InitializeComponent();
    }

    private void OnGridSizeChanged(object sender, SizeChangedEventArgs e)
    {
        buttonCanvas.Children.Clear();

        var newHeight = e.NewSize.Height - BOTTOM_MARGIN;   // マージン
        double widthFraction = e.NewSize.Width /
                        (e.NewSize.Width + newHeight);      // マージン
        int horzCount = (int)(widthFraction * blockChars.Length / 2);
        int vertCount = (int)(blockChars.Length / 2 - horzCount);
        int index = 0;

        double slotWidth = (e.NewSize.Width - BUTTON_SIZE) / horzCount;
        double slotHeight = (newHeight - BUTTON_SIZE) / vertCount + 1;  // マージン

        // Across top
        for (int i = 0; i < horzCount; i++)
        {
            Button button = MakeButton(index++);
            Canvas.SetLeft(button, i * slotWidth);
            Canvas.SetTop(button, 0);
            buttonCanvas.Children.Add(button);
        }

        // Down right side
        for (int i = 0; i < vertCount; i++)
        {
            Button button = MakeButton(index++);
            Canvas.SetLeft(button, this.ActualWidth - BUTTON_SIZE);
            Canvas.SetTop(button, i * slotHeight);
            buttonCanvas.Children.Add(button);
        }

        // Across bottom from right
        for (int i = 0; i < horzCount; i++)
        {
            Button button = MakeButton(index++);
            Canvas.SetLeft(button, this.ActualWidth - i * slotWidth - BUTTON_SIZE);
            Canvas.SetTop(button, newHeight - BUTTON_SIZE); //
            buttonCanvas.Children.Add(button);
        }

        // Up left side
        for (int i = 0; i < vertCount; i++)
        {
            Button button = MakeButton(index++);
            Canvas.SetLeft(button, 0);
            Canvas.SetTop(button, newHeight - i * slotHeight - BUTTON_SIZE);    //
            buttonCanvas.Children.Add(button);
        }
    }

    Button MakeButton(int index)
    {
        Button button = new Button
        {
            Content = blockChars[index].ToString(),
            Width = BUTTON_SIZE,
            Height = BUTTON_SIZE,
            FontSize = BUTTON_FONT,
            Tag = new SolidColorBrush(colors[index % colors.Length]),
        };
        button.Click += OnButtonClick;
        return button;
    }

    void OnButtonClick(object sender, RoutedEventArgs e)
    {
        Button button = sender as Button;

        Block block = new Block
        {
            Text = button.Content as string,
            Foreground = button.Tag as Brush
        };
        Canvas.SetLeft(block, this.ActualWidth / 2 - 144 * rand.NextDouble());
        Canvas.SetTop(block, this.ActualHeight / 2 - 144 * rand.NextDouble());
        Canvas.SetZIndex(block, Block.ZIndex);
        blockcanvas.Children.Add(block);
    }
}

 コードも WinRT XAML と同じにしても良いのですが、デスクトップで動く WPF アプリの場合はタスクバーの大きさなどを考える必要があります。そのために、BOTTOM_MARGIN という定数を追加して、newHeight という変数を計算し、widthFraction の計算を変更し、高さに影響する箇所を変更しています。この変更をしなくても動作しますが、見栄えが悪いというだけです。それでは、実行結果を示します。
AlphabetBlocks

外周のボタンをクリックすることでブロックが追加されて、ブロックをドラッグすることで移動することができます。このようにドラッグ可能なコントロールを作成しする場合は、Thumb コントロールは非常に便利です。

なぜ、このようなコードにしたかなどは書籍に記述されていますので、書籍を熟読してください。この記事では、WinRT XAML で記述されたコードであっても、適切な対応をすることで問題なく WPF にも利用できることを説明しただけになります。

ここまで説明してた違いを意識しながら、第5章を読むことで WPF にも書籍の内容を応用することができるようになることでしょう。


プログラミング Windows 第6版 第6章 WPF編

$
0
0

この記事では、「プログラミング Windows 第6版」を使って WPF XAML の学習を支援することを目的にしています。この目的から、書籍と併せて読まれることをお勧めします。

第6章 WinRT と MVVM

この章では、MVVM(Model View ViewModel) パターンを説明する意欲的な内容になっています。書籍では説明がされていませんが、MVVM というデザイン パターンは、Composite Application Guidance(別名として、PRISM) というドキュメント(日本語英語WinRT)で解説されたデザイン パターンです。WinRT XAML や WPF XAML において、MVVM パターンの使用が必須ではありませんが、データ バインディングを有効活用するという観点ではビューモデル(ViewModel)を使用した方が望ましいので、PRISM ではないにしても MVVM パターンを暗黙的に利用するようになることでしょう。PRISM では、MVVM パターンだけでなく依存性の注入(Dependency Injection)だったり、ナビゲーション フレームワークなどの多くの技術要素を盛り込んだガイダンスとなっています。ここでは、簡単になぜビューモデルという概念が導入されたのかという点を説明します。

マイクロソフトの開発ツールは、古く(Visual Basic 6.0 以前)からデータ バインディングという技術をサポートしてきました。.NET Framework が提供されても、データ バインディングのサポートは続いており、元になるデータに対するアクセス技術はデータベース、Web サービス、REST サービスや Entity Framework など多岐に渡りながら機能強化や進化を続けています。一般的にデータ バインディングは、データを表現するオブジェクトと UI を表示するコントロールとをコーディング レスで繋げるものです。コーディング レスでデータと UI コントロールが繋がるということは、プログラムの中からコントロールとデータを関連付ける大量のコードが削減できることに他なりません。コード削減に繋がる技術がデータ バインディングなのですが、利用は非常に限定的だったと私は考えています。その理由は、次のようなものがあります。

  • データベースなどのデータ ストア上のデータ表現と UI コントロールの表現が、直接的にならない(値の変換などの操作が必要になる)。
  • データの表現には、データを参照するのは良くても、データの変更を許可しないなどのビジネス ルール表現が含まれており、UI コントロールと対応しない(対応しないというよりも、ビジネス ルールの表現方法が異なる)。
  • データ バインディングしたコントロールでは、要求される機能(参照のウィンドウ表示など)の実現が難しい。
  • などなど

つまり、データ表現とコントロールとしての UI 表現、そしてビジネス ルールや要求される機能などによって、木目細かな制御が可能なコードを使って実装する方が、データ バインディングを使うよりも実装がしやすいという判断に基づいた結果として、データ バインディングの利用が限定的になっていたのはないでしょうか。
ch06 view model

この問題を解決するために、ビューモデルを導入したらどのようになるでしょうか。
ch06 view viewmodel

ビューモデルに、実装する主要な機能を次のようにします。

  • データ バインディングを前提にする(UI コントロールと 1:1 に対応)。
  • データ モデルとビューの差異を吸収する(モデルが持つビジネス ルールに対する表現も吸収)。

ビューモデルは、UI コントロールとのバインディングを前提に考えますから、データ表現と UI コントロール上の表現が異なる場合は一致させるためにコンバーター(第4章第5章で説明しています)の利用を前提とします。つまり、バインディングを利用する上で問題となっていたモデルとビューのギャップを解消することを主眼としたモデルをビューモデルと呼んでいます。こうしたビューモデルを導入することで、バインディングを利用していなかった場合に使用していたモデルとビュー間の接着剤的(グル)コードを削減することになります。また、書籍にも記述されていますが、ユーザー コントロールやカスタム コントロールなどにバインディング可能なプロパティを実装する場合は、依存関係プロパティとして実装する必要があります。つまり、依存関係プロパティは、 XAML 系の UI 技術において、UI 定義のシリアライズ/デシリアライズだけでなくデータ バインディングを活用する上でも重要なプロパティ システムになっています。是非、依存関係プロパティの実装をマスターするようにしてください。

6.1(P215) 速習:MVVM

本節では、モデル、ビュー、ビューモデルという階層の役割を説明していますので、熟読をお願いします。

6.2(P216) データ バインディングの通知

本節では、データ バインディング機能を支えるための仕組みを説明しています。データ バインディングは、1回限り(OneTime)、一方向(OneWay、モデルからビュー)、双方向(TwoWay)というバインディングの方法があります。一方向、双方向のバインディングを支えるのが、通知メカニズムです。この通知メカニズムを実装する契約として、INotificationPropertyChangedインターフェースがあります。これらの具体的な説明が記述されていまし、WPF XAML にも適用できますので、書籍を熟読してください。

6.3(P218) ColorScroll のビューモデル

本節では、第5章の ColorScroll サンプルを題材にバインディングが可能かを説明しています。バインディングを可能にするために導入したビューモデルである RgbViewModel.cs (ColorScrollWithViewModel プロジェクト)を示します。

using System.ComponentModel;
using System.Windows.Media;

namespace ColorScrollWithViewModel
{
    public class RgbViewModel : INotifyPropertyChanged
    {
        double red, green, blue;
        Color color = Color.FromArgb(255, 0, 0, 0);

        public event PropertyChangedEventHandler PropertyChanged;

        public double Red
        {
            set
            {
                if (red != value)
                {
                    red = value;
                    OnPropertyChanged("Red");
                    Calculate();
                }
            }
            get
            {
                return red;
            }
        }

        public double Green
        {
            set
            {
                if (green != value)
                {
                    green = value;
                    OnPropertyChanged("Green");
                    Calculate();
                }
            }
            get
            {
                return green;
            }
        }

        public double Blue
        {
            set
            {
                if (blue != value)
                {
                    blue = value;
                    OnPropertyChanged("Blue");
                    Calculate();
                }
            }
            get
            {
                return blue;
            }
        }

        public Color Color
        {
            protected set
            {
                if (color != value)
                {
                    color = value;
                    OnPropertyChanged("Color");
                }
            }
            get
            {
                return color;
            }
        }

        void Calculate()
        {
            this.Color = Color.FromArgb(255, (byte)this.Red, (byte)this.Green, (byte)this.Blue);
        }

        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

コードは、WinRT と同じものになっています。この ViewModel は、Red、Green、Blue プロパティが変更されると PropertyChanged イベントが発生します。PropertyChanged イベントは、OnPropertyChanged メソッドが発生させていますが、実装コードを見れば PropertyChanged という PropertyChnagedEventHandler 型のフィールドを使用していることがわかります。このフィールドに対して、購読(Subscribe)するのがバインディング定義を記述した UI コントロールの役割でもあります。今度は、作成した RgbViewModel を使用するための MainWindow.xaml よりリソース定義を抜粋して示します。

<Window.Resources><local:RgbViewModel x:Key="rgbViewModel" />
    ...</Window.Resources>

今度は Slider コントロールの定義を MainWindow.xaml より抜粋して示します。

<Slider Grid.Column="0"
        Grid.Row="1"
        Value="{Binding Source={StaticResource rgbViewModel}, 
                        Path=Red, 
                        Mode=TwoWay}"
        Foreground="Red" /><TextBlock Text="{Binding Source={StaticResource rgbViewModel}, 
                          Path=Red, 
                          Converter={StaticResource hexConverter}}"
           Grid.Column="0"
           Grid.Row="2"
           Foreground="Red" />

コード自体は、WinRT XAML と同じですが、Binding に 「Mode=TwoWay」と記述されていることに注目してください。バインディングで Mode が記述されていない場合は、「OneWay(一方向)」バインディングになりますので、ここでは双方向を示すためにバインディング モードを記述しています。

今度は、第5章で検討した Color プロパティに対するバインディングを記述した箇所を MainWindow.xaml より抜粋して示します。

<Rectangle Grid.Column="3"
           Grid.Row="0"
           Grid.RowSpan="3"><Rectangle.Fill><SolidColorBrush Color="{Binding Source={StaticResource rgbViewModel}, 
                                     Path=Color}" /></Rectangle.Fill></Rectangle>

RgbViewModel を導入したことで、バインディングを記述できるようになりました(コードは、WinRT XAML と同じです)。バインディングで記述することができましたので、分離コードファイル(MainWindow.xaml.cs) に記述していた SolodColorBrush 操作は必要がなくなり、Rectangle の定義から x:Name 属性も削除しています。ColorScrollWithViewModel プロジェクトを WPF XAML へ移植する場合、既に説明していますが、MainWindow.xaml のリソース定義で一部のコードの書き換え化が必要になります。それは、ThumbToolTipValueConverter の定義です。ThumbToolTipValueConverter が、WPF XAML に含まれないことから削除するだけになります。もちろん、動きは第5章のものと同じとなります。

書籍では、RgbViewModel のプロパティ実装や様々な解説が記述されていますので、ビューモデル実装の知識を学習するために熟読をお願いします。

6.4(P224) 構文のショートカット

本節では、ビューモデルを INotifyPropertyChanged インターフェースを使って実装することが面倒ではないかという投げ掛けに始まって、Windows 8 用の Windows ストア アプリ プロジェクトに含まれる BindableBase 抽象クラスを使って、ビューモデルを実装することを説明しています。

BindaleBase 抽象クラスとは、Visual Studio 2012 の Windows ストア アプリ プロジェクト テンプレートに含まれているクラスになります。また、Visual Studio 2013 の Windows ストア アプリ(Windows 8.1 用)プロジェクト テンプレートには含まれていないクラスになります。が、このクラスは、WPF アプリでも使用ができます。私は、WPF アプリでビュー モデルを作成する時に使用していたりもします。皆さんも、ビュー モデルの作成が面倒だなと感じたのならば、自分でビュー モデルを作成しやすいような部品を用意することを考えてみては如何でしょうか。

それでは、BindableBase 抽象クラスの考え方を利用した ColorScrollWithDataContext プロジェクトの RgbViewModel.cs を示します。

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Media;

namespace ColorScrollWithDataContext
{
    public class RgbViewModel : INotifyPropertyChanged
    {
        double red, green, blue;
        Color color = Color.FromArgb(255, 0, 0, 0);

        public event PropertyChangedEventHandler PropertyChanged;

        public double Red
        {
            set
            {
                if (SetProperty<double>(ref red, value, "Red"))
                    Calculate();
            }
            get
            {
                return red;
            }
        }

        public double Green
        {
            set
            {
                if (SetProperty<double>(ref green, value))
                    Calculate();
            }
            get
            {
                return green;
            }
        }

        public double Blue
        {
            set
            {
                if (SetProperty<double>(ref blue, value))
                    Calculate();
            }
            get
            {
                return blue;
            }
        }

        public Color Color
        {
            set
            {
                if (SetProperty<Color>(ref color, value))
                {
                    this.Red = value.R;
                    this.Green = value.G;
                    this.Blue = value.B;
                }
            }
            get
            {
                return color;
            }
        }

        void Calculate()
        {
            this.Color = Color.FromArgb(255, (byte)this.Red, (byte)this.Green, (byte)this.Blue);
        }

        protected bool SetProperty<T>(ref T storage, T value,
                                      [CallerMemberName] string propertyName = null)
        {
            if (object.Equals(storage, value))
                return false;

            storage = value;
            OnPropertyChanged(propertyName);
            return true;
        }

        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

コード自体は、WinRT と同じになります。

6.5(P228) DataContext プロパティ

本節では、DataContext プロパティについて説明しています。DataContext プロパティは、バインディングを使用する場合のバインディング ソースを指定する方法の 1つです。それでは、ColorScrollWithDataContext プロジェクトの MainWindow.xaml.cs を抜粋して示します。

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

        this.DataContext = new RgbViewModel();

        // Initialize to highlight color
        (this.DataContext as RgbViewModel).Color = SystemColors.HighlightColor;
    }
}

WPF XAML へ移植するために、Color プロパティのハイライト カラー設定だけを書き換えています。これは、既に説明していますが、WinRT XAML と WPF XAML でハイライト カラーの定義が異なっているためです。そして、DataContext プロパティを使用していることから、MainWindow.xaml を抜粋して示します。

<Window.Resources>
    ...<!--<Setter Property="ThumbToolTipValueConverter" Value="{StaticResource hexConverter}" />--></Style></Window.Resources><Grid>
    ...<!-- Red --><TextBlock Text="Red"
               Grid.Column="0"
               Grid.Row="0"
               Foreground="Red" /><Slider Grid.Column="0"
            Grid.Row="1"
            Value="{Binding Red, Mode=TwoWay}"
            Foreground="Red" /><TextBlock Text="{Binding Red, Converter={StaticResource hexConverter}}"
               Grid.Column="0"
               Grid.Row="2"
               Foreground="Red" />
    ...<!-- Result --><Rectangle Grid.Column="3"
               Grid.Row="0"
               Grid.RowSpan="3"><Rectangle.Fill><SolidColorBrush Color="{Binding Color}" /></Rectangle.Fill></Rectangle></Grid>


WPF XAML に移植する上で変更したのは、リソース定義から ThumbToolTipValueConverter を削除しただけです。これは、WinRT だけがこのプロパティをサポートしているからです。後は、WinRT XAML と一緒になっており、DataContext プロパティを使用したことからバインディングの記述が簡単になっています。Path 記述がなくなっているのは、Path がデフォルトを意味するからです。書籍では、バインディングの記述方法の詳細な説明や、DataContext プロパティをコードではなくリソース定義と組み合わせて使用する方法などを説明していますし、WPF XAML に適用できますので、熟読をお願いします。

6.6(P230) データバインディングと TextBox

本節では、ユーザーが入力に使用する TextBox を使用した場合のバインディングについて説明しています。この目的のために、ColorTextBoxes プロジェクトを使用しています。それでは、ColorTextBoxes プロジェクトの MainWindow.xaml.cs の抜粋を示します。

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

        this.DataContext = new RgbViewModel();

        // Initialize to highlight color
        (this.DataContext as RgbViewModel).Color = SystemColors.HighlightColor;

    }
}

このコードは、6.5 で使用した ColorScrollWithDataContext プロジェクトと同じですから、WPF へ移植するためにハイライト カラーを書き換えています。そして、XAML では TextBox へ RgbViewModel に対するバインディングを記述した MainWindow.xaml の抜粋を示します。

<Window 
        ... ><Window.Resources><Style TargetType="TextBlock"><Setter Property="FontSize" Value="24" /><Setter Property="Margin" Value="24 0 0 0" /><Setter Property="VerticalAlignment" Value="Center" /></Style><Style TargetType="TextBox"><Setter Property="Margin" Value="24 48 96 48" /><Setter Property="VerticalAlignment" Value="Center" /></Style></Window.Resources><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><Grid Grid.Column="0"><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="Auto" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><TextBlock Text="Red: "
                       Grid.Row="0"
                       Grid.Column="0" /><TextBox Text="{Binding Red, Mode=TwoWay}"
                     Grid.Row="0"
                     Grid.Column="1" /><TextBlock Text="Green: "
                       Grid.Row="1"
                       Grid.Column="0" /><TextBox Text="{Binding Green, Mode=TwoWay}"
                     Grid.Row="1"
                     Grid.Column="1" /><TextBlock Text="Blue: "
                       Grid.Row="2"
                       Grid.Column="0" /><TextBox Text="{Binding Blue, Mode=TwoWay}"
                     Grid.Row="2"
                     Grid.Column="1" /></Grid><!-- Result --><Rectangle Grid.Column="1"><Rectangle.Fill><SolidColorBrush Color="{Binding Color}" /></Rectangle.Fill></Rectangle></Grid></Window>

既に説明した内容に基づいて、必要な変更をしただけで WPF XAML 用にしています。それでは、実行結果を示します。
ColorTextBoxes

このサンプルでは、TextBox に対する双方向バインディングにおける TextBox から ビューモデルへデータが反映されるタイミングを確認しています。具体的には、TextBox を値(0から255)を入力してフォーカスを移動したタイミングでビューモデルへ反映されるというものです。

データが更新されるタイミングを改善するために、ColorTextBoxesWithEvents プロジェクトの MainWindow.xaml の抜粋を示します。

<TextBlock Text="Red: "
           Grid.Row="0"
           Grid.Column="0" /><TextBox x:Name="redTextBox" 
         Grid.Row="0"
         Grid.Column="1"
         Text="0"
         TextChanged="OnTextBoxTextChanged" /><TextBlock Text="Green: "
           Grid.Row="1"
           Grid.Column="0" /><TextBox x:Name="greenTextBox"
         Grid.Row="1"
         Grid.Column="1"
         Text="0"
         TextChanged="OnTextBoxTextChanged" /><TextBlock Text="Blue: "
           Grid.Row="2"
           Grid.Column="0" /><TextBox x:Name="blueTextBox"
         Grid.Row="2"
         Grid.Column="1"
         Text="0"
         TextChanged="OnTextBoxTextChanged" />


このコードは、WinRT XAML と同じものになり、TextBox の TextChanged イベント ハンドラーを指定しています。そして、入力値の検証も追加した MainWindow.xaml.cs を抜粋して示します。

public partial class MainWindow : Window
{
    RgbViewModel rgbViewModel;
    Brush textBoxTextBrush; 
    Brush textBoxErrorBrush = new SolidColorBrush(Colors.Red);

    public MainWindow()
    {
        InitializeComponent();

        // Get TextBox brush
        //textBoxTextBrush = this.Resources["TextBoxForegroundThemeBrush"] as SolidColorBrush;
        textBoxTextBrush = new SolidColorBrush(Colors.Brown); // 初期値を変更 

        // Create RgbViewModel and save as field
        rgbViewModel = new RgbViewModel();
        rgbViewModel.PropertyChanged += OnRgbViewModelPropertyChanged;
        this.DataContext = rgbViewModel;

        // Initialize to highlight color
        rgbViewModel.Color = SystemColors.HighlightColor;
    }

    void OnRgbViewModelPropertyChanged(object sender, PropertyChangedEventArgs args)
    {
        switch (args.PropertyName)
        {
            case "Red":
                redTextBox.Text = rgbViewModel.Red.ToString("F0");
                break;

            case "Green":
                greenTextBox.Text = rgbViewModel.Green.ToString("F0");
                break;

            case "Blue":
                blueTextBox.Text = rgbViewModel.Blue.ToString("F0");
                break;
        }
    }

    private void OnTextBoxTextChanged(object sender, TextChangedEventArgs e)
    {
        if (rgbViewModel == null) return;   // 追加
        byte value;

        if (sender == redTextBox && Validate(redTextBox, out value))
            rgbViewModel.Red = value;

        if (sender == greenTextBox && Validate(greenTextBox, out value))
            rgbViewModel.Green = value;

        if (sender == blueTextBox && Validate(blueTextBox, out value))
            rgbViewModel.Blue = value;

    }

    bool Validate(TextBox txtbox, out byte value)
    {
        bool valid = byte.TryParse(txtbox.Text, out value);
        txtbox.Foreground = valid ? textBoxTextBrush : textBoxErrorBrush;
        return valid;
    }

}

WPF XAML へ移植する上で変更したのは、3か所になります。

  • textBoxTextBrush の設定で、組み込みスタイルを変更しました。
  • ハイライト カラーを WPF XAML 用に変更しました。
  • OnTextBoxTextChanged イベント ハンドラーで、RgbViewModel が null の条件を追加しました。

WPF XAML 版で注意して欲しいのが、OnTextBoxTextChanged イベント ハンドラーです。このような変更イベントは、コントロールのインスタンスが作成された直後(InitializeComponent メソッド) にも発生します。この理由で、RgbViewModel が null になるケースが発生します。一方で、WinRT XAML ではコントロールのインスタンスが作成された直後に変更イベントが発生していません。これは、WPF XAML と WinRT XAML の違いにもなりますが、変更イベントでは操作するオブジェクトが null かどうかをチェックされることをお勧めします。このようにすれば、同じコードを WinRT XAML と WPF XAML で使用することができるからです。

書籍では、このように ビュー側のコードから ビューモデルを操作した場合に起きる問題などを詳細に説明していますし、WPF XAML にも適用できる内容ですので、熟読をお願いします。

6.7(P236) ボタンと MVVM

本節では、コマンド パターンの説明をしています。コマンド パターンとは、ICommand インターフェースを継承したオブジェクトを使ってボタン コントロールのクリック イベントによって実行されるコマンドを実現するパターンになります。このコマンド パターンを使用する上で必要となる、考え方や知識を説明していますので、書籍を熟読してください。コマンド パターンは、WinRT XAML と WPF XAML で同じように利用できるもので、コマンド実行の概要で説明が記述されています。

6.8(P238) DelegateCommand クラス

DelegateCommand クラスとは、KeypadWithViewModel プロジェクトで使用するために用意したクラスになります。日本語では委譲コマンドであり、ICommand インターフェースと KeypadWithViewModel プロジェクトで使用するコマンドで必要となるインターフェースを実装したクラスになります。このDelegateCommand を実装するためのインターフェースとして、KeypadWithViewModel プロジェクトの IDelegateCommand cs を示します。

using System.Windows.Input;

namespace KeypadWithViewModel
{
    public interface IDelegateCommand : ICommand
    {
        void RaiseCanExecuteChanged();
    }
}

IDelegateCommand インターフェースは、DelegateCommand を実装するために定義したインターフェースになります。それでは、DelegateCommand.cs を示します。

using System;

namespace KeypadWithViewModel
{
    public class DelegateCommand : IDelegateCommand
    {
        Action<object> execute;
        Func<object, bool> canExecute;

        // Event required by ICommand
        public event EventHandler CanExecuteChanged;

        // Two constructors
        public DelegateCommand(Action<object> execute, Func<object, bool> canExecute)
        {
            this.execute = execute;
            this.canExecute = canExecute;
        }
        public DelegateCommand(Action<object> execute)
        {
            this.execute = execute;
            this.canExecute = this.AlwaysCanExecute;
        }

        // Methods required by ICommand
        public void Execute(object param)
        {
            execute(param);
        }
        public bool CanExecute(object param)
        {
            return canExecute(param);
        }

        // Method required by IDelegateCommand
        public void RaiseCanExecuteChanged()
        {
            if (CanExecuteChanged != null)
                CanExecuteChanged(this, EventArgs.Empty);
        }

        // Default CanExecute method
        bool AlwaysCanExecute(object param)
        {
            return true;
        }
    }
}

 

DelegateCommand クラスは、コンストラクターで execute という Action<object> デリゲートと canExecute という Func<object, bool> デリゲートを受け取ります。execute が、コマンド実行に使用するデリゲートであり、canExecute がキャンセル時に実行するデリゲートになります。定義した DelegateCommand クラスを使って、ビューモデルでコマンドを定義しますので、KyepadViewModel.cs を示します。

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace KeypadWithViewModel
{
    public class KeypadViewModel : INotifyPropertyChanged
    {
        string inputString = "";
        string displayText = "";
        char[] specialChars = { '*', '#' };

        public event PropertyChangedEventHandler PropertyChanged;

        // Constructor
        public KeypadViewModel()
        {
            this.AddCharacterCommand = new DelegateCommand(ExecuteAddCharacter);
            this.DeleteCharacterCommand =
                new DelegateCommand(ExecuteDeleteCharacter, CanExecuteDeleteCharacter);
        }

        // Public properties
        public string InputString
        {
            protected set
            {
                bool previousCanExecuteDeleteChar = this.CanExecuteDeleteCharacter(null);

                if (this.SetProperty<string>(ref inputString, value))
                {
                    this.DisplayText = FormatText(inputString);

                    if (previousCanExecuteDeleteChar != this.CanExecuteDeleteCharacter(null))
                        this.DeleteCharacterCommand.RaiseCanExecuteChanged();
                }
            }

            get { return inputString; }
        }

        public string DisplayText
        {
            protected set { this.SetProperty<string>(ref displayText, value); }
            get { return displayText; }
        }

        // ICommand implementations
        public IDelegateCommand AddCharacterCommand { protected set; get; }

        public IDelegateCommand DeleteCharacterCommand { protected set; get; }

        // Execute and CanExecute methods
        void ExecuteAddCharacter(object param)
        {
            this.InputString += param as string;
        }

        void ExecuteDeleteCharacter(object param)
        {
            this.InputString = this.InputString.Substring(0, this.InputString.Length - 1);
        }

        bool CanExecuteDeleteCharacter(object param)
        {
            return this.InputString.Length > 0;
        }

        // Private method called from InputString
        string FormatText(string str)
        {
            bool hasNonNumbers = str.IndexOfAny(specialChars) != -1;
            string formatted = str;

            if (hasNonNumbers || str.Length < 4 || str.Length > 10)
            {
            }
            else if (str.Length < 8)
            {
                formatted = String.Format("{0}-{1}", str.Substring(0, 3),
                                                     str.Substring(3));
            }
            else
            {
                formatted = String.Format("({0}) {1}-{2}", str.Substring(0, 3),
                                                           str.Substring(3, 3),
                                                           str.Substring(6));
            }
            return formatted;
        }

        protected bool SetProperty<T>(ref T storage, T value,
                                      [CallerMemberName] string propertyName = null)
        {
            if (object.Equals(storage, value))
                return false;

            storage = value;
            OnPropertyChanged(propertyName);
            return true;
        }

        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}


KeyPadViewModel が、AddCharacterCommand と DeleteCharacterCommand プロパティを実装している点に注意してください。それぞれのコマンドが、入力した文字列を追加するコマンドであり、文字列を削除するコマンドになっています。
それでは、コマンドを定義した KeypadWithViewModel プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ...
        xmlns:local="clr-namespace:KeypadWithViewModel"
        ... ><Window.Resources><local:KeypadViewModel x:Key="viewModel" /></Window.Resources><Grid DataContext="{StaticResource viewModel}"><Grid HorizontalAlignment="Center"
              VerticalAlignment="Center"
              Width="288"><Grid.Resources><Style TargetType="Button"><Setter Property="ClickMode" Value="Press" /><Setter Property="HorizontalAlignment" Value="Stretch" /><Setter Property="Height" Value="72" /><Setter Property="FontSize" Value="36" /></Style></Grid.Resources><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions><Border Grid.Column="0"
                        HorizontalAlignment="Left"><TextBlock Text="{Binding DisplayText}"
                               HorizontalAlignment="Right"
                               VerticalAlignment="Center"
                               FontSize="24" /></Border><Button Content="&#x21E6"
                        Command="{Binding DeleteCharacterCommand}"
                        Grid.Column="1"
                        FontFamily="Segoe Symbol"
                        HorizontalAlignment="Left"
                        Padding="0"
                        BorderThickness="0" /></Grid><Button Content="1"
                    Command="{Binding AddCharacterCommand}"
                    CommandParameter="1"
                    Grid.Row="1" Grid.Column="0" />
            ...<Button Content="#"
                    Command="{Binding AddCharacterCommand}"
                    CommandParameter="#"
                    Grid.Row="4" Grid.Column="2" /></Grid></Grid></Window>


Button の Command 属性に、バインディングによって DeleteCharacterCommand と AddCharacterCommand を指定しています。AddCharacterCommand に対しては、CommandParameter によってコマンドに渡すパラメーターを指定しています。このバインディングによって、MainWindow.xaml.cs の分離コードに対すて記述する必要性がなくなります。それでは、実行結果を示します。
KeypadWithViewModel

 

このように、ビュー モデル と コマンド パターンを組み合わせることで、必要なコードを分離コードなどから削減する可能性が生まれます。これらの記述が何を意味しているかなどは、書籍で解説していますので、書籍を熟読してください。

ViewModel について

ビュー モデルは、ビューとモデルのセマンティック ギャップを埋めるために導入されたビュー専用のモデルです。つまり、ビューと 1:1 に対応するモデルであり、その実装は退屈なものになりがちです。なぜなら、INotifyPropertyChanged インターフェースを実装し、ビューと 1:1 になるようにプロパティを記述し、ビュー表現とプロパティ表現が異なればコンバーターを実装するというように、極論するればビューが100種類あれば、ビュー モデルも 100種類あるという状況になりかねません。しかしながら、ビュー モデルを作成することでデータ バインディングを活用できるようになり、ビューとモデルを対応付けるグル(接着剤)コードが無くなり、実装すべき本来の機能へ集中できるようになります。その一方で、ユーザーのキー入力をチェックするようなビュー モデルは、現実的ではありません。なぜなら、可能限り、ビュー モデル から ビューを操作することを避けるべきだからです。色々な考え方があると思いますが、キー入力を何らかの形で処理する必要があるのであれば、ユーザー コントロールやカスタム コントロールなどを使うのが望ましいのではないでしょうか。

ここまで説明してた内容を意識しながら、第6章を読むことで WPF にも書籍の内容を応用することができるようになることでしょう。

Windowsストアアプリにおける グリッドアプリケーションについて(1)

$
0
0

久し振りのエントリーになります。これから、何回かに渡ってVisual Studio 2012に含まれる Windows ストア アプリケーションのグリッド アプリケーション テンプレートを紐解いて行きます。

グリッド アプリケーションのナビゲーションは、以下に示すように3階層になっています。
Grid Application Navigation

ナビゲーションに含まるXAMLページは、以下のようになっています。

  • GroupedItemsPage.xaml
    ハブとなるページで、GridViewコントロールとListViewコントロールを持ちます。
  • GroupDetailPage.xaml
    セクションとなるページで、GridViewコントロールとListViewコントロールを持ちます。
  • ItemDetailPage.xaml
    詳細となるページで、FlipViewコントロールを持ちます。

また、各ページとLayoutAwarePageクラスの関係は、以下のようになっています。
Page UML

各ページは、LayoutAwarePageという抽象クラスを継承した構造になっています。このことから、明確になることが1つあります。それは、新しいページを作成した場合は、必ずLayoutAwarePageクラスを継承するようにした方が良いということです。具体的には、検索コントラクトで追加されるSearchResultsPage.xamlなどです。この理由は、LayoutAwarePageを説明していくことで解説をしていきます。
それでは、GroupedItemPage.xaml にある、LayoutAwrePageの定義を見ていきます。

<common:LayoutAwarePage
    x:Name="pageRoot"
    x:Class="App6.GroupedItemsPage"
    DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App6"
    xmlns:data="using:App6.Data"
    xmlns:common="using:App6.Common"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"><Page.Resources><!--このページで表示されるグループ化されたアイテムのコレクションは、グループ内のアイテムを
            仮想化できないため、完全なアイテム リストのサブセットにバインドされます
        --><CollectionViewSource
            x:Name="groupedItemsViewSource"
            Source="{Binding Groups}"
            IsSourceGrouped="true"
            ItemsPath="TopItems"
            d:Source="{Binding AllGroups, Source={d:DesignInstance Type=data:SampleDataSource, IsDesignTimeCreatable=True}}"/></Page.Resources>

この定義で注目したいのが、「DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"」 です。ここで指定されているDefaultViewModel が何処で定義されているかといえば、LayoutAwarePage クラスになります。その定義を以下に示します。

/// <summary>
/// <see cref="DefaultViewModel"/> 依存関係プロパティを識別します。
/// </summary>
public static readonly DependencyProperty DefaultViewModelProperty =
         DependencyProperty.Register("DefaultViewModel", 
           typeof(IObservableMap<String, Object>),
           typeof(LayoutAwarePage), null);



/// <summary>
/// <see cref="IObservableMap<String, Object>"/> の実装です。これは、
/// 単純なビュー モデルとして使用されるように設計されています。
/// </summary>
protected IObservableMap<String, Object> DefaultViewModel
{
    get
    {
       return this.GetValue(DefaultViewModelProperty)
                 as IObservableMap<String, Object>;
    }

    set
    {
       this.SetValue(DefaultViewModelProperty, value);
    }
}


この定義をみれば理解できるように、DefaultViewModelは依存プロパティとして、IObservableMap<string, Object> 型として定義されています。そして、IObservableMap<string, Object>の実装として ObservableDictionary<K, V>クラスを定義しています。この型を簡単に説明するとすれば、ディクショナリー型と同じであり、キーに文字列でデータソースの名前を指定して、値にオブジェクトのコレクションを設定して使用します。DefaultViewModelが、Dictionary<string, object>を使って実装していない理由は、ディクショナリーに変更があった場合の変更通知にあります。変更通知を実装するために、 IObservableMap<string, Object>を継承してObservableDictionary<K, V>クラス を定義しています。GroupedItemPage.xamlのCollectionViewSource定義のSource属性に「{Binding Groups}」と定義されていたことを思い出してください。ここで定義されている「Groups」と文字列が、ObservableDictionary<K, V>クラスのキーになっています。このことから、理解できることは以下のようになります。

  • DataContextで定義しているDefaultViewModel は、依存プロパティであり、LayoutAwarePageクラスで定義している。
  • CollectionViewSourceのSource属性に、IObservableMap<string, Object>型のキーを指定する。
  • ページ定義XAMLのコードビハインドで、DefaultViewModel ["キー"] に対してオブジェクトコレクションを設定する必要がある

このように理解することができれば、次にLayoutAwarePageクラスのOnNavigatedToメソッドを読み解いてみましょう。このメソッドは、Frameを使ったナビゲーションによって呼び出されるメソッドになります。

/// <summary>
/// このページがフレームに表示されるときに呼び出されます。
/// </summary>
/// <param name="e">このページにどのように到達したかを説明するイベント データ。Parameter 
/// プロパティは、表示するグループを示します。</param>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
    // ナビゲーションを通じてキャッシュ ページに戻るときに、状態の読み込みが発生しないようにします
    if (this._pageKey != null) return;
    var frameState = SuspensionManager.SessionStateForFrame(this.Frame);
    this._pageKey = "Page-" + this.Frame.BackStackDepth;

    if (e.NavigationMode == NavigationMode.New)
    {
        // 新しいページをナビゲーション スタックに追加するとき、次に進むナビゲーションの
        // 既存の状態をクリアします
        var nextPageKey = this._pageKey;
        int nextPageIndex = this.Frame.BackStackDepth;
        while (frameState.Remove(nextPageKey))
        {
            nextPageIndex++;
            nextPageKey = "Page-" + nextPageIndex;
        }
        // ナビゲーション パラメーターを新しいページに渡します
        this.LoadState(e.Parameter, null);
    }
    else
    {
        // ナビゲーション パラメーターおよび保存されたページの状態をページに渡します。
        // このとき、中断状態の読み込みや、キャッシュから破棄されたページの再作成と同じ対策を
        // 使用します
        this.LoadState(e.Parameter, 
            (Dictionary<String, Object>)frameState[this._pageKey]);
    }
}

/// <summary>
/// このページには、移動中に渡されるコンテンツを設定します。前のセッションからページを
/// 再作成する場合は、保存状態も指定されます。
/// </summary>
/// <param name="navigationParameter">このページが最初に要求されたときに
/// <see cref="Frame.Navigate(Type, Object)"/> に渡されたパラメーター値。
/// </param>
/// <param name="pageState">前のセッションでこのページによって保存された状態の
/// ディクショナリ。ページに初めてアクセスするとき、状態は null になります。</param>
protected virtual void LoadState(Object navigationParameter, 
                        Dictionary<String, Object> pageState)
{
}

OnNavigatedToメソッドで重要なことは、ページの状態をチェックすることとLoadStateメソッドを呼び出すことです。LoadStateメソッドの定義を見ると仮想メソッドとして定義されています。よって、LayoutAwarePageクラスを継承したクラスで実装する必要があります。GroupedItemPage.xaml.csのLoadStateメソッドを以下に示します。

/// <summary>
/// このページには、移動中に渡されるコンテンツを設定します。前のセッションからページを
/// 再作成する場合は、保存状態も指定されます。
/// </summary>
/// <param name="navigationParameter">このページが最初に要求されたときに
/// <see cref="Frame.Navigate(Type, Object)"/> に渡されたパラメーター値。
/// </param>
/// <param name="pageState">前のセッションでこのページによって保存された状態の
/// ディクショナリ。ページに初めてアクセスするとき、状態は null になります。</param>
protected override void LoadState(Object navigationParameter, 
                        Dictionary<String, Object> pageState)
{
    // TODO: 問題のドメインでサンプル データを置き換えるのに適したデータ モデルを作成します
    var sampleDataGroups = SampleDataSource.GetGroups((String)navigationParameter);
    this.DefaultViewModel["Groups"] = sampleDataGroups;
}

このコードでは、SampleDataSource.GetGroupsメソッドを使ってグループ コレクションを取得してから、DefaultViewModelにキーと値を設定することで、Groupsというキーのデータソースを設定していることを理解することができます。

  • LoadStateメソッドは、目的のページにおけるデータソースを作成する責務を持つ
  • OnNavigatedToメソッドをオーバーライドする場合は、LayoutAwarePageクラスのOnNavigatedToメソッドを呼び出すか、同等の処理を記述しなければならない

ここまでで、GroupedItemsPageにおけるXAMLとコードを使ったデータソースの設定に関することを説明しました。まだまだ、説明しきれていないことがありますので、その辺りは次回に説明する予定です。 

Windowsストアアプリにおける グリッドアプリケーションについて(2)

$
0
0

前回は、GroupedItemsPageのLoadStateメソッドまでを説明しました。今回は、GroupedItemsPageのビューに関して説明します。ビューとしては、前回に説明したようにGridViewコントロールとListViewコントロールの2つを用意しています。最初に、GridViewコントロールの定義とデザイン画面を以下に示します。

<!-- ほとんどのビューステートで使用される水平スクロール グリッド--><GridView
    x:Name="itemGridView"
    AutomationProperties.AutomationId="ItemGridView"
    AutomationProperties.Name="Grouped Items"
    Grid.RowSpan="2"
    Padding="116,137,40,46"
    ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}"
    ItemTemplate="{StaticResource Standard250x250ItemTemplate}"
    SelectionMode="None"
    IsSwipeEnabled="false"
    IsItemClickEnabled="True"
    ItemClick="ItemView_ItemClick"><GridView.ItemsPanel><ItemsPanelTemplate>                        <VirtualizingStackPanel Orientation="Horizontal"/></ItemsPanelTemplate></GridView.ItemsPanel><GridView.GroupStyle><GroupStyle><GroupStyle.HeaderTemplate><DataTemplate><Grid Margin="1,0,0,6"><Button
                            AutomationProperties.Name="Group Title"
                            Click="Header_Click"
                            Style='{StaticResource TextPrimaryButtonStyle}' ><!-- 以下 省略 --></Grid></DataTemplate></GroupStyle.HeaderTemplate><GroupStyle.Panel><ItemsPanelTemplate><VariableSizedWrapGrid Orientation='Vertical' Margin='0,0,80,0'/></ItemsPanelTemplate></GroupStyle.Panel></GroupStyle></GridView.GroupStyle></GridView>


Grid Landscape

GridViewの定義で注目するのが、ItemsSourceとItemsTemplate属性の定義になります。

  • ItemSource : CollectionViewSourceの名前であるgroupedItemsViewSourceをデータソースとして指定しています。
  • ItemTemplate : StandardStyle.xamlで定義されているデータテンプレートである Standard250x250ItemTemplateを指定しています

今度は、ListViewコントロールの定義とデザインを以下に示します。

<!-- スナップの場合のみ使用される垂直スクロール リスト --><ListView
    x:Name="itemListView"
    AutomationProperties.AutomationId="ItemListView"
    AutomationProperties.Name="Grouped Items"
    Grid.Row="1"
    Visibility="Collapsed"
    Margin="0,-10,0,0"
    Padding="10,0,0,60"
    ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}"
    ItemTemplate="{StaticResource Standard80ItemTemplate}"
    SelectionMode="None"
    IsSwipeEnabled="false"
    IsItemClickEnabled="True"
    ItemClick="ItemView_ItemClick"><ListView.GroupStyle><GroupStyle><GroupStyle.HeaderTemplate><DataTemplate><Grid Margin="7,7,0,0"><Button
                            AutomationProperties.Name="Group Title"
                            Click="Header_Click"
                            Style='{StaticResource TextPrimaryButtonStyle}'><StackPanel Orientation='Horizontal'><TextBlock Text='{Binding Title}' Margin='3,-7,10,10' Style='{StaticResource GroupHeaderTextStyle}' /><!-- 以下省略 --/></StackPanel></Button></Grid></DataTemplate></GroupStyle.HeaderTemplate></GroupStyle></ListView.GroupStyle></ListView>


GridSnapped 

デザインを見れば理解できるようにListViewコントロールは、スナップビューとして使用するものになっています。従って、ListViewの定義で注目するのは、ItemSourceとItemTemplateとVisibility属性になります。

  • Visibility :Collapsedを指定して非表示にしています。
  • ItemSource :GridViewと同じgroupedItemsViewSourceをデータソースとして指定しています
  • ItemTemplate :StandardStyle.xamlで定義されているデータテンプレートである Standard80ItemTemplateを指定しています

GridViewコントロールとListViewコントロールに対して同じデータソースを指定して、ランドスケープとスナップにおいて表示・非表示を切り替えることで2つのレイアウトに対応しています。Visual Studio 2012 と Blend for Visual Studio のデバイスタブを使って、ランドスケープ、スナップなどを切り替える場合に重要な役割を持っているのが、VisualStateManagerになります。

<VisualStateManager.VisualStateGroups><!--表示状態には、アプリケーションのビューステートが反映されます --><VisualStateGroup x:Name="ApplicationViewStates"><VisualState x:Name="FullScreenLandscape"/><VisualState x:Name="Filled"/><!-- ページ全体では、縦方向に対して、より狭い 100 ピクセルの余白の規則を優先します --><VisualState x:Name="FullScreenPortrait"><Storyboard><!-- 省略 --></Storyboard></VisualState><!--スナップの場合、[戻る] ボタンとタイトルには異なるスタイルが使用され、
            他のすべてのビューステートで表示されるグリッドに対して一覧の表現が置き換えられます
        --><VisualState x:Name="Snapped"><Storyboard><!-- 省略 --><ObjectAnimationUsingKeyFrames 
                        Storyboard.TargetName="itemListView" 
                        Storyboard.TargetProperty="Visibility"><DiscreteObjectKeyFrame KeyTime="0" 
                        Value="Visible"/></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames 
                        Storyboard.TargetName="itemGridView" 
                        Storyboard.TargetProperty="Visibility"><DiscreteObjectKeyFrame KeyTime="0" 
                        Value="Collapsed"/></ObjectAnimationUsingKeyFrames></Storyboard></VisualState></VisualStateGroup></VisualStateManager.VisualStateGroups>


VisualState の Snappedの定義をみれば、Visibilityを切り替えていることは明らかです。これらの表示状態に関わらずに一貫したデータを表示しているのが、同じデータソースにデータバインドして いる効果になります。実は、VisualStateManagerは Visual Studio 2012やBlend for Visual Studioのデザイナーがデバイスタブで使用しており、実行時にはユーザーコードでXAMLに定義した情報を呼び出す必要があります。この呼び出しを行っているのが、LayoutAwarePageクラスになります。LayoutAwarePageクラスのコンストラクタを以下に示します。

/// <summary>
/// <see cref="LayoutAwarePage"/> クラスの新しいインスタンスを初期化します。
/// </summary>
public LayoutAwarePage()
{
    if (Windows.ApplicationModel.DesignMode.DesignModeEnabled) return;

    // 空の既定のビュー モデルを作成します
    this.DefaultViewModel = new ObservableDictionary<String, Object>();

    // このページがビジュアル ツリーの一部である場合、次の 2 つの変更を行います:
    // 1) アプリケーションのビューステートをページの表示状態にマップする
    // 2) キーボードおよびマウスのナビゲーション要求を処理する
    this.Loaded += (sender, e) =>
    {
        this.StartLayoutUpdates(sender, e);

        // キーボードおよびマウスのナビゲーションは、ウィンドウ全体を使用する場合のみ適用されます
        if (this.ActualHeight == Window.Current.Bounds.Height &&
            this.ActualWidth == Window.Current.Bounds.Width)
        {
            // ウィンドウで直接待機するため、フォーカスは不要です
            Window.Current.CoreWindow.Dispatcher.AcceleratorKeyActivated +=
                CoreDispatcher_AcceleratorKeyActivated;
            Window.Current.CoreWindow.PointerPressed +=
                this.CoreWindow_PointerPressed;
        }
    };

    // ページが表示されない場合、同じ変更を元に戻します
    this.Unloaded += (sender, e) =>
    {
        this.StopLayoutUpdates(sender, e);
        Window.Current.CoreWindow.Dispatcher.AcceleratorKeyActivated -=
            CoreDispatcher_AcceleratorKeyActivated;
        Window.Current.CoreWindow.PointerPressed -=
            this.CoreWindow_PointerPressed;
    };
}


コンストラクタで Loadedイベントハンドラで StartLayoutUpdatesメソッドを呼び出しているのが確認できます。StartLayoutUpdatesメソッドを以下に示します。

/// <summary>
/// イベント ハンドラーとして呼び出されます。これは通常、ページ内の <see cref="Control"/> の
/// <see cref="FrameworkElement.Loaded"/> イベントで呼び出され、送信元が
/// アプリケーションのビューステートの変更に対応する表示状態管理の変更を受信開始する必要があることを
/// 示します。
/// </summary>
/// <param name="sender">ビューステートに対応する表示状態管理をサポートする 
/// <see cref="Control"/> のインスタンス。</param>
/// <param name="e">要求がどのように行われたかを説明するイベント データ。</param>
/// <remarks>現在のビューステートは、レイアウトの更新が要求されると、
/// 対応する表示状態を設定するためすぐに使用されます。対応する 
/// <see cref="FrameworkElement.Unloaded"/> イベント ハンドラーを
/// <see cref="StopLayoutUpdates"/> に接続しておくことを強くお勧めします。
/// <see cref="LayoutAwarePage"/> のインスタンスは、Loaded および Unloaded イベントでこれらのハンドラーを自動的に
/// 呼び出します。</remarks>
/// <seealso cref="DetermineVisualState"/>
/// <seealso cref="InvalidateVisualState"/>
public void StartLayoutUpdates(object sender, RoutedEventArgs e)
{
    var control = sender as Control;
    if (control == null) return;
    if (this._layoutAwareControls == null)
    {
        // 更新の対象となるコントロールがある場合、ビューステートの変更の待機を開始します
        Window.Current.SizeChanged += this.WindowSizeChanged;
        this._layoutAwareControls = new List<Control>();
    }
    this._layoutAwareControls.Add(control);

    // コントロールの最初の表示状態を設定します
    VisualStateManager.GoToState(
           control, 
           DetermineVisualState(ApplicationView.Value), 
           false);
}

private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs e)
{
    this.InvalidateVisualState();
}


/// <summary>
/// 適切な表示状態を使用した表示状態の変更を待機しているすべてのコントロールを更新し
/// ます。
/// </summary>
/// <remarks>
/// 通常、ビューステートが変更されていない場合でも、別の値が返される可能性がある事を知らせるために
/// <see cref="DetermineVisualState"/> をオーバーライドすることで
/// 使用されます。
/// </remarks>
public void InvalidateVisualState()
{
    if (this._layoutAwareControls != null)
    {
        string visualState = DetermineVisualState(ApplicationView.Value);
        foreach (var layoutAwareControl in this._layoutAwareControls)
        {
            VisualStateManager.GoToState(
                   layoutAwareControl, visualState, false);
        }
    }
}


StartLayoutUpdatesメソッドを見ることで、VisualStateManager.GoToStateメソッドを呼び出しているのが、このメソッドとWindowSizeChangedイベントハンドラだということが理解できます。ApplicationViewStateが、現在のビューの状態(Snappedなど)を表しています。コメントにも記載されていますが、VisualStateManagerで切り替えるビューステートを変更したい場合は、DetermineVisualStateメソッドをオーバーライドして対象のビューステート名(VisualState のx:Name属性)を返すようにすれば良いということになります。ここまでの動きから、VisualStateManagerの動作をまとめると、以下のようになります。

  • デバイスタブは、XAML内に定義されたVisualStateManagerを使用する
  • 実行時は、自分でVisualStateManager.GoToStateメソッドを呼び出す必要がある。
  • LayoutAwarePagaeクラスは、Window.SizeChangedイベントハンドラーでVisualStateManager.GoToStateメソッドを呼び出す 
    (ポートレイトやスナップなどに切り替えると、Window.SizeChangedイベントが発生します)

このような仕組みで表示状態を切り替えていることから、グリッドアプリケーション テンプレートで作成したプロジェクトに新しいページ(xaml)を追加する時に「必ずLayoutAwarePageを継承してください」という説明をしていました。 当然のこととして、ここまでのビューの切替などの仕組みを理解して、自分でビューの切替を実装するのであれば、LayoutAwarePageクラスを継承する必要はありません。実際には、継承することを強くお勧めします。この理由は、次回以降で説明します。

最後に、VisualStateMamangerを使用したコントロールのカスタマイズに関して、簡単に説明します。VisualStateManagerは、最初にSilverlightに導入されて、WPF 4.0でデスクトップ環境にも導入されました。WPF4.0などのドキュメントを読めば理解することができますが、コントロールの外観をマウスポインターが入った時などをxamlだけでカスタマイズすることができます。コントロールの外観をカスタマイズするには、ControlTemplateを定義して、VisualStateManagerを定義する必要があります。またControlTemplate属性とは、Controlが持つプロパティなので、Controlを継承したコントロール(Buttonなど)がVisualStateManagerを使ったノンプログラミングの外観カスタマイズを行うことができます。これ以外のコントロール(たとえば、Gridなどのレイアウト系コントロール)は、何らかのコードを書かないと外観カスタマイズを行うことはできませんので、注意する必要があります。

Windowsストアアプリにおける グリッドアプリケーションについて(3)

$
0
0

前回にLayoutAwarePage.StartLayoutUpdateメソッドが、Loadedイベントで呼び出されることを説明しました。このメソッドは、以下の定義になっていました。。

public void StartLayoutUpdates(object sender, RoutedEventArgs e)
{
    var control = sender as Control;
    if (control == null) return;
    if (this._layoutAwareControls == null)
    {
        // 更新の対象となるコントロールがある場合、ビューステートの変更の待機を開始します
        Window.Current.SizeChanged += this.WindowSizeChanged;
        this._layoutAwareControls = new List(); 
}
this._layoutAwareControls.Add(control);
// コントロールの最初の表示状態を設定します
VisualStateManager.GoToState(control,
      DetermineVisualState(ApplicationView.Value), false);
}


ここで気を付けておくべきことは、「_layoutAwareControls」というメンバー変数です。この変数は「List<Control>」型になっており、LayoutAwarePageクラスがLoadedイベントにより登録されます。これは、LayoutAwarePageがPageクラスを継承しており、Pageクラスが UserControl を継承し、UserControlが Controlクラスから派生しているためです。この機能により、LayoutAwarePageクラスを継承したGroupedItemsPage.xamlで定義されたVisualStateManagerのVisualStateが呼び出されるようになっているのです。 この特徴を理解しておくことは、とても重要です。何故なら、ユーザーコントロールを自作して組み合わせる場合に、LoadedとUnLoadedイベントで「StartLayoutUpdatesメソッドとStopLayoutUpdatesメソッド」を呼び出すように設定することで、ユーザーコントロール内に記述したVisualStateManagerの定義が呼び出されるようになるからです。実際に、グリッドアプリケーションのItemDetailPage.xamlではUserControlを定義してLoadedとUnLoadedイベントハンドラーを設定しています。

GroupedItemsPageクラスで次に説明することは、データソースについてです。データソースの説明に入る前に、GroupedItemsPage.xamlのCollectionViewSourceの定義を以下に再掲します。

<CollectionViewSource
    x:Name="groupedItemsViewSource"
    Source="{Binding Groups}"
    IsSourceGrouped="true"
    ItemsPath="TopItems"
    d:Source=
       "{Binding AllGroups, 
         Source={d:DesignInstance Type=data:SampleDataSource, 
         IsDesignTimeCreatable=True}}"/>


d:Source」属性は、デザイン時のデータソースを指定しています(コード上は意図的に改行しています)。ここに指定されていることの意味は、以下のようになります。

  • Binding:AllGroupsというプロパティ名を指定しています。
  • Source:デザイン時のデータソースを指定しています。
    d:DesignInstance で、型を「SampleDataSource」クラスと指定し、IsDesignTimeCreatableをtrueと指定することで、デザイン時にデータソースのインスタンスを作成することを指示しています。

このことから理解できるのは、SampleDataSource.AllGroupsプロパティが返すコレクションをカスタマイズすることで、デザイン時に表示されるサンプルデータを変更することができるということです。それでは、SampleDataSourceクラスの構造を以下に示します。
SampleDataSource

d:Source属性で説明したように、SampleDataSourceクラスがAllGroupsプロパティを持っていることを確認することができます。また、ObservableCollection<SampleDataGroup>を返すこともクラス図から把握することができます。それでは、SampleDataSourceクラスのコードを以下に示します。

/// <summary>
/// ハードコーディングされたコンテンツを使用して、グループおよびアイテムのコレクションを作成します。
/// 
/// SampleDataSource はライブ プロダクション データではなくプレースホルダー データを使用して初期化するので
/// サンプル データは設計時と実行時の両方に用意されています。
/// </summary>
public sealed class SampleDataSource
{
    private static SampleDataSource _sampleDataSource = new SampleDataSource();

    private ObservableCollection<SampleDataGroup> _allGroups = 
             new ObservableCollection<SampleDataGroup>();
    public ObservableCollection<SampleDataGroup> AllGroups
    {
        get { return this._allGroups; }
    }

    public static IEnumerable<SampleDataGroup> GetGroups(string uniqueId)
    {
        if (!uniqueId.Equals("AllGroups")) throw 
             new ArgumentException("Only 'AllGroups' is supported as a collection of groups");
        return _sampleDataSource.AllGroups;
    }

    public static SampleDataGroup GetGroup(string uniqueId)
    {
        // サイズの小さいデータ セットでは単純な一方向の検索を実行できます
        var matches = _sampleDataSource.AllGroups.
                     Where((group) => group.UniqueId.Equals(uniqueId));
        if (matches.Count() == 1) return matches.First();
        return null;
    }

    public static SampleDataItem GetItem(string uniqueId)
    {
        // サイズの小さいデータ セットでは単純な一方向の検索を実行できます
        var matches = _sampleDataSource.AllGroups.
                      SelectMany(group => group.Items).
                      Where((item) => item.UniqueId.Equals(uniqueId));
        if (matches.Count() == 1) return matches.First();
        return null;
    }

    public SampleDataSource()
    {
        String ITEM_CONTENT = 
                String.Format(
                 "Item Content: {0}\n\n{0}\n\n{0}\n\n{0}\n\n{0}\n\n{0}\n\n{0}",
                    "コンテンツ文字列");

        var group1 = new SampleDataGroup("Group-1",
                "Group Title: 1",
                "Group Subtitle: 1",
                "Assets/DarkGray.png",
                "Group Description: グループ説明");
        group1.Items.Add(new SampleDataItem("Group-1-Item-1",
                "Item Title: 1",
                "Item Subtitle: 1",
                "Assets/LightGray.png",
                "Item Description: アイテム説明",
                ITEM_CONTENT,
                group1));
        // 以下省略
        this.AllGroups.Add(group1);
        // 以下省略
    }
}


コードを見れば理解が進むと思いますが、コンストラクタ内でサンプルデータを作成して、AllGroupsプロパティを使ってサンプルデータを追加しています。これが、ObservableCollection<SampleDataGroup>コレクションへの追加となっているのです。これが、d:Source属性でデザイン時のデータソースを指定していることの意味になります。また、SampleDataSourceクラスが提供する静的メソッドには、以下のものがあります。

  • GetGroups:引数は"AllGroups"のみで、全てのコレクションを取得します。
  • GetGroup:引数はグループのユニークIDとなり、1つのグループを取得します。
    matches.Count() == 1」は間違いで、正しくは「matches.Count() >= 1」となります。
  • GetItem:引数はアイテムのユニークIDとなり、1つのアイテムを取得します。
    matches.Count() == 1」は間違いで、正しくは「matches.Count() >= 1」となります。

静的メソッドは、_sampleDataSourceという静的なメンバー変数を使ってSampleDataSourceクラスのインスタンスへアクセスしています。現実的なアプリに改造するには、特に以下の点に注意します。

  • コンストラクタで作成しているサンプルデータをWindows.ApplicationModel.DesignMode.DesignModeEnsbled が trueの場合だけに作成するようにします。
  • アプローチは色々とありますが、実際のデータを読み込むメソッドを追加します。この時に、読み込みが終了したかどうかを判定するために「IsInitialized」などのプロパティを追加した方が良いでしょう。

開発体験テンプレートのNewsReaderでは、説明した意図でIsInitializedプロパティとLoadRemoteDataAsyncメソッドを用意しています。SampleDataGroupは、Itemsプロパティ(ObservableCollection<SampleDataItem>)を持っています。これが、グループが持つアイテムのコレクションとなっています。また、クラス図から理解できるように、SampleDataGroupクラスとSampleDataItemクラスは、SampleDataCommonクラスを継承しており、SampleDataCommonクラスはBindableBaseクラスを継承しています。BindableBaseクラスは、「INotifyPropertyChanged」インターフェースを実装しています。またクラス図から理解できますが、ObservableCollectionもINotifyPropertyChangedインターフェースを実装しています。XAMLとのデータバインディングを行う上で、INotifyPropertyChangedインターフェースを実装していることが重要なポイントなります。この理由は、一回限りのバインディング(OneTime)だけではなく、一方向(OneWay、これがデフォルト)、双方向(TwoWay)バインディングを実現するために必須となるからです。データバインドしたコレクションのメンバーに対して、データを変更したり、メンバーの追加や削除を行う場合に、XAMLに対して変更通知を行うことでビュー(XAMLの視覚要素で、表示コントロール)が更新されるようになるのです。この変更通知を実現するのが、 INotifyPropertyChangedインターフェースを実装するという意味になります。これが、MVVMパターンにおけるビューモデル(VM)の特徴になっています。

SampleDataGroup、SampleDataItemの関係は、循環参照となっています。具体的には、SampleDataItemのプロパティにGroupが定義されており、これがSampleDataGroupのインスタンスを示すようになっています。このような構造になっている理由は、ItemDetailPage.xamlからGroupDetailPage.xamlへのナビゲーションを実現するためです。データソースを現実のアプリに合わせて改造するには、SampleDataGroup、SampleDataItem、SampleDataCommonクラスなどに対して、メンバーの追加や変更を行うことが多いことでしょう。この場合の行うべき定石のコードを以下にしめします。

        private string _description = string.Empty;
        public string Description
        {
            get { return this._description; }
            set { this.SetProperty(ref this._description, value); }
        }


プロパティ セッターで、必ず「 this.SetProperty(ref メンバー変数, value)」を記述します。SetPropertyメソッドは、BindableBaseクラスで定義されており、OnPropertyChangedメソッドを使ってプロパティの変更イベントを呼び出します。この変更通知が行われることで、XAMLの視覚要素へ変更通知が送られるようになります。ですから、SetPropertyメソッドをセッターに記述することが重要なのです。また、SampleDataCommonクラスは、Imageプロパティを持っていますが、永続化を考えるとImageSourceのインスタンスをシリアライズすることはできません。よって、SetImage(イメージファイルへのパス)メソッドをデシリアイズでは呼び出すようにします。もしくは、ImagePathプロパティを追加して、セッターでImageプロパティに対してOnPropertyChangedを呼び出すようにしても良いでしょう。

Windowsストアアプリにおける グリッドアプリケーションについて(4)

$
0
0

前回にデータソースの説明をしました。SampleDataSourceの構造が理解できましたので、改めてGroupedItemPage.xaml.csのLoadStateのコードを振り返ります。

protected override void LoadState(
                        Object navigationParameter, 
                        Dictionary<String, Object> pageState)
{
    // TODO: 問題のドメインでサンプル データを置き換えるのに適したデータ モデルを作成します
    var sampleDataGroups = SampleDataSource.GetGroups(
                               (String)navigationParameter);
    this.DefaultViewModel["Groups"] = sampleDataGroups;
}


SampleDataSource.GetGroupsメソッドは、静的メソッドになっていますから、呼び出されるとメンバー変数の_sampleDataSourceのAllGroupsプロパティの値を返します。そして、 _sampleDataSourceメンバー変数は静的変数となっており、SampleDataSourceクラスのインスタンスを保持しています。_sampleDataSourceメンバー変数が静的変数となっている点が重要です。何故なら、静的変数はアプリケーションドメイン単位に保持されますから、異なるページのインスタンスへナビゲーションしたとしても_sampleDataSourceは、同一のインスタンスを保持し続けるからです(要は、キャッシュの効果があるのです)。

また、AllGroupsメソッドは引数が"AllGroups"であることが要求されますので、このパラメータがどのように渡されるかを確認するためにApp.xaml.cs の OnLaunchedイベントハンドラーを以下に示します。

/// <summary>
/// アプリケーションがエンド ユーザーによって正常に起動されたときに呼び出されます。他のエントリ ポイントは、
/// アプリケーションが特定のファイルを開くために呼び出されたときに
/// 検索結果やその他の情報を表示するために使用されます。
/// </summary>
/// <param name="args">起動要求とプロセスの詳細を表示します。</param>
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
    Frame rootFrame = Window.Current.Content as Frame;

    // ウィンドウに既にコンテンツが表示されている場合は、アプリケーションの初期化を繰り返さずに、
    // ウィンドウがアクティブであることだけを確認してください
    
    if (rootFrame == null)
    {
        // ナビゲーション コンテキストとして動作するフレームを作成し、最初のページに移動します
        rootFrame = new Frame();
        //フレームを SuspensionManager キーに関連付けます                                
        SuspensionManager.RegisterFrame(rootFrame, "AppFrame");

        if (args.PreviousExecutionState == 
                 ApplicationExecutionState.Terminated)
        {
            // 必要な場合のみ、保存されたセッション状態を復元します
            try
            {
                await SuspensionManager.RestoreAsync();
            }
            catch (SuspensionManagerException)
            {
                //状態の復元に何か問題があります。
                //状態がないものとして続行します
            }
        }

        // フレームを現在のウィンドウに配置します
        Window.Current.Content = rootFrame;
    }
    if (rootFrame.Content == null)
    {
        // ナビゲーション スタックが復元されていない場合、最初のページに移動します。
        // このとき、必要な情報をナビゲーション パラメーターとして渡して、新しいページを
        // を構成します
        if (!rootFrame.Navigate(typeof(GroupedItemsPage), "AllGroups"))
        {
            throw new Exception("Failed to create initial page");
        }
    }
    // 現在のウィンドウがアクティブであることを確認します
    Window.Current.Activate();
}

/// <summary>
/// アプリケーションの実行が中断されたときに呼び出されます。アプリケーションの状態は、
/// アプリケーションが終了されるのか、メモリの内容がそのままで再開されるのか
/// わからない状態で保存されます。
/// </summary>
/// <param name="sender">中断要求の送信元。</param>
/// <param name="e">中断要求の詳細。</param>
private async void OnSuspending(object sender, SuspendingEventArgs e)
{
    var deferral = e.SuspendingOperation.GetDeferral();
    await SuspensionManager.SaveAsync();
    deferral.Complete();
}

OnLaunchedイベントハンドラーでは、「if (rootFrame == null)」 で2つのことをしています。

  • SuspensionManagerのRegisterFrameメソッドを呼び出している。
    Frame オブジェクトのナビゲーション履歴を保存(Frame.GetNavigationState)するためです。
  • 強制終了させられた場合の状態の復旧
    これは、強制終了させられた時に表示されていたページやナビゲーション履歴を復旧(Frame.SetNavigationState)しています。

SuspensionManagerのコードの提示を行いませんが、上記の処理とOnSuspendingイベントハンドラーのSuspensionManager.SaveAsyncメソッドにより、Frameオブジェクトのナビゲーション履歴などが保存されると理解しておいてください。このようになっている理由は、Windowsストアアプリの望ましい起動方法に起因しています。具体的には、以下のような動作が望まれます。

  • 以前に起動していない場合は、トップページが表示される。
  • 強制終了させられた場合は、ユーザーが作業を再開できるように最後に操作をしていたページが表示される。

OnLaunchedイベントハンドラーの後半の「if rootFrame.Content == null」で、通常起動(強制終了後の起動であれば、この条件は不成立)でナビゲーション パラメータとして「AllGroups」を指定してGroupedItemPageへナビゲートします。

残ったGroupedItemPageで説明していないことは、GroupDetailPageとItemDetailPageへのナビゲーションになります。ナビゲーションのイベントハンドラーを以下に示します。

/// <summary>
/// グループ ヘッダーがクリックされたときに呼び出されます。
/// </summary>
/// <param name="sender">ボタンは、選択されたグループのグループ ヘッダーとして使用されます。</param>
/// <param name="e">クリックがどのように開始されたかを説明するイベント データ。</param>
void Header_Click(object sender, RoutedEventArgs e)
{
    // ボタン インスタンスがどのグループを表すかを確認します
    var group = (sender as FrameworkElement).DataContext;

    // 適切な移動先のページに移動し、新しいページを構成します。
    // このとき、必要な情報をナビゲーション パラメーターとして渡します
    this.Frame.Navigate(typeof(GroupDetailPage), 
                        ((SampleDataGroup)group).UniqueId);
}

/// <summary>
/// グループ内のアイテムがクリックされたときに呼び出されます。
/// </summary>
/// <param name="sender">クリックされたアイテムを表示する GridView (アプリケーションがスナップ
/// されている場合は ListView) です。</param>
/// <param name="e">クリックされたアイテムを説明するイベント データ。</param>
void ItemView_ItemClick(object sender, ItemClickEventArgs e)
{
    // 適切な移動先のページに移動し、新しいページを構成します。
    // このとき、必要な情報をナビゲーション パラメーターとして渡します
    var itemId = ((SampleDataItem)e.ClickedItem).UniqueId;
    this.Frame.Navigate(typeof(ItemDetailPage),
                        itemId);
}


Header_Clickでは、グループヘッダーのDataContextがSampleGroupオブジェクトであることから、グループのIDを指定してGroupDetailPageへナビゲートしています。ItemView_ItemClickでは、ClickedItemがSampleDataItemオブジェクトであることから、アイテムのIDを指定してItemDetailPageへナビゲートしています。これは、データバインドしていることから、データバインドされた特徴を生かしたコードになっています。

ここまでで、GroupedDetailPageの説明ができました。説明してきたことから、カスタマイズする場合の特徴を以下の記載します。

  • 新しいページを作成する場合は、LayoutAwarePageを継承させる。
  • データソースをカスタマイズする場合は、SampleDataItem、SampleDataGroupへのプロパティの追加や修正と一緒に、本番データを読み込むメソッドを用意する。
  • データソースを変更すれば、ItemTemplateなどを変更内容に応じて変更する。
  • ビューの切替は、VisualStateManagerを使って行っている(実行時とVisual Studioのデバイスタブ)。
    ユーザーコントロールには、LoadedとUnloadedイベントハンドラーにLayoutAwarePageのイベントハンドラーを設定する。
  • SuspensionManager.RegisterFrameメソッドで、Frameのナビゲーション履歴が保存され、復旧される。
    復旧時はナビゲーション履歴だけなので、データソースは自分で復旧させる必要があることを意味しています。
    また、検索コントラクトのように新しいページで起動される場合は、SuspensionManager.RegisterFrameメソッドでFrameを登録する必要があることを意味しています。(認定要件 3.6 システムが提供するメカニズムを備えた機能をアプリがサポートする場合は、システムが提供するメカニズムを使用しなければならない)。

認定要件3.6は必須事項ではありませんが、グリッドアプリケーションがSuspensionManager.RegisterFrameメソッドでナビゲーション履歴を使った強制終了時の状態復旧に対応しているため、データソースの復旧をした方が良いでしょう。逆に、ナビゲーション履歴を復旧させなければ、データソースを復旧させる必要もありません。次回以降は、GroupDetailPageの解説を行う予定です。

Viewing all 163 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>