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

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

$
0
0

前回まででGroupedItemPageの説明が終了したので、今回はGroupDetailPageの説明を行います。最初に、GroupedItemsPage.HeaderClickで説明したナビゲーションで呼び出されるLoadStateメソッドを見てみましょう。

protected override void LoadState(Object navigationParameter, 
                       Dictionary<String, Object> pageState)
{
    // TODO: 問題のドメインでサンプル データを置き換えるのに適したデータ モデルを作成します
    var group = SampleDataSource.GetGroup((String)navigationParameter);
    this.DefaultViewModel["Group"] = group;
    this.DefaultViewModel["Items"] = group.Items;
}

       
navigationParameterに、SampleDataGroup.UniqueIdが格納されていて、SampleDataSourceの静的メソッドであるGetGroupメソッドを呼び出しています。このメソッドにより、目的のSampleDataGroupのインスタンスを取得して、DefaultViewModelのGroupキーとItemsキーを設定しています。今までの説明を読んできた人には理解できると思いますが、GroupキーとItemsキーの設定を確認するために、GroupDetailPage.xamlのCollectionViewSourceとGridViewを見ていきます。最初に、CollectionViewSourceを以下に示します。

<CollectionViewSource
    x:Name="itemsViewSource"
    Source="{Binding Items}"
    d:Source=
      "{Binding AllGroups[0].Items, 
                Source={d:DesignInstance 
                Type=data:SampleDataSource, 
                IsDesignTimeCreatable=True}}"/>


Source属性に記述されている「Items」がDefaultViewModelのキーであり、d:Source属性に記述されているSampleDataSource.AllGroups[0]がデザイン時のデータソースになります。デザイン時は、AllGroupsプロパティが返すコレクションの最初の要素をバインドしています。次に、GridViewとListViewの定義を以下に示します。

<Grid
    Style='{StaticResource LayoutRootStyle}'
    DataContext='{Binding Group}'
    d:DataContext='{Binding AllGroups[0], 
                    Source={d:DesignInstance 
                    Type=data:SampleDataSource, 
                    IsDesignTimeCreatable=True}}'><Grid.RowDefinitions><RowDefinition Height='140'/><RowDefinition Height='*'/></Grid.RowDefinitions><!-- ほとんどのビューステートで使用される水平スクロール グリッド--><GridView
        x:Name='itemGridView'
        AutomationProperties.AutomationId='ItemGridView'
        AutomationProperties.Name='Items In Group'
        TabIndex='1'
        Grid.RowSpan='2'
        Padding='120,126,120,50'
        ItemsSource='{Binding Source={StaticResource itemsViewSource}}'
        ItemTemplate='{StaticResource Standard500x130ItemTemplate}'
        SelectionMode='None'
        IsSwipeEnabled='false'
        IsItemClickEnabled='True'
        ItemClick='ItemView_ItemClick'><GridView.Header><StackPanel Width='480' Margin='0,4,14,0'><TextBlock Text='{Binding Subtitle}' 
                    Margin='0,0,18,20' 
                    Style='{StaticResource SubheaderTextStyle}' MaxHeight='60'/><!-- 以下省略 --></StackPanel></GridView.Header><GridView.ItemContainerStyle><Style TargetType='FrameworkElement'><Setter Property='Margin' Value='52,0,0,10'/></Style></GridView.ItemContainerStyle></GridView><!-- スナップの場合のみ使用される垂直スクロール リスト --><ListView
        x:Name='itemListView'
        AutomationProperties.AutomationId='ItemListView'
        AutomationProperties.Name='Items In Group'
        TabIndex='1'
        Grid.Row='1'
        Visibility='Collapsed'
        Padding='10,0,0,60'
        ItemsSource='{Binding Source={StaticResource itemsViewSource}}'
        ItemTemplate='{StaticResource Standard80ItemTemplate}'
        SelectionMode='None'
        IsSwipeEnabled='false'
        IsItemClickEnabled='True'
        ItemClick='ItemView_ItemClick'><ListView.Header><StackPanel><TextBlock Text='{Binding Subtitle}' 
                   Margin='10,0,18,20' 
                   Style='{StaticResource TitleTextStyle}' MaxHeight='60'/><!-- 以下省略 --></StackPanel></ListView.Header></ListView>


GridのDataContext属性の指定している「Group」がDefaultViewModelのキーであり、GridViewやListViewのHeaderにバインドされるデータソースになっています。また、Gridのd:DataContext属性にCollectionViewSourceと同じ値を設定していることにも注意してください。これもデザイン用のデータソースの指定です。
GridViewのItemsSource属性に「{Binding Source={StaticResource itemsViewSource}}」が指定されており、CollectionViewSourceがデータソースとしてバインディングされていることが解ります。ListViewもGridViewと同じ値がItemSourceに指定されていることも解ることでしょう。
GridViewのItemTemplate属性に「{StaticResource Standard500x130ItemTemplate}」が指定されており、ListViewのItemTemplateには「{StaticResource Standard80ItemTemplate}」が指定されています。ItemTemplateは、StandardStyle.xamlで定義されているDataTemplateとなり、カスタマイズする場合はStandardStyle.xamlからApp.xamlへコピーして編集を行うことで、独自のスタイルだったり、追加したプロパティなどを表示する項目を作成したりすることができます。

GroupDetailPageもGroupedItemPageと同じで、フル表示をGridViewで行い、スナップ表示をListViewで行っています。これが、ListviewのVisibility属性が「Collapsed」と指定されている意味であり、VisualStateManagerの定義もGroupedItemPageと同じパターンで定義されています。

GroupDetailPageのアイテムをクリックすると、ItemDetailPageへナビゲーションしますので、このイベントハンドラーを以下に示します。

void ItemView_ItemClick(object sender, ItemClickEventArgs e)
{
    // 適切な移動先のページに移動し、新しいページを構成します。
    // このとき、必要な情報をナビゲーション パラメーターとして渡します
    var itemId = ((SampleDataItem)e.ClickedItem).UniqueId;
    this.Frame.Navigate(typeof(ItemDetailPage), itemId);
}

このコードは、GroupedItemPageで示したイベントハンドラーと同じであり、クリックされたClickedItemがSampleDataItemであり、そのUniqueIdをパラメータとしてItemDetailPageへナビゲーションしています。

今度は、トップページであるGroupedDetailPageへ戻るイベントハンドラーを確認してみましょう。このイベントハンドラーは、LayoutAwarePage.GoBackと定義されています。

/// <summary>
/// イベント ハンドラーとして呼び出され、このページの <see cref="Frame"/> に関連付けられた
/// ナビゲーション スタックで前に戻ります。
/// </summary>
/// <param name="sender">イベントをトリガーしたインスタンス。</param>
/// <param name="e">イベントが発生する条件を説明する
/// イベント データ。</param>
protected virtual void GoBack(object sender, RoutedEventArgs e)
{
    // ナビゲーション フレームを使用して前のページに戻ります
    if (this.Frame != null
        && this.Frame.CanGoBack) this.Frame.GoBack();
}

  
コードを見れば理解できると思いますが、Frameオブジェクトのナビゲーションメカニズムを使って直前のページに戻しているだけです。このことから理解できることは、独自に戻るボタンのイベントハンドラーをオーバーライドする場合は、最終的に基底クラスのGoBackを呼び出すようにすることで、GoBackの動作に変更があっても修正する箇所が基底クラスの一か所のみで良いということです。 次回は、ItemDetailPageの説明に入る予定です。


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

$
0
0

前回にGroupDetailPageの説明を行いました。今回は、最後のItemDetailPageの説明を行います。最初に、ItemDetailPageがナビゲーションを使って呼び出されることから、LoadStateメソッドを以下に示します。

protected override void LoadState(Object navigationParameter, 
                          Dictionary<String, Object> pageState)
{
    // 保存されたページの状態で、表示する最初のアイテムをオーバーライドすることを許可します
    if (pageState != null && pageState.ContainsKey("SelectedItem"))
    {
        navigationParameter = pageState["SelectedItem"];
    }

    // TODO: 問題のドメインでサンプル データを置き換えるのに適したデータ モデルを作成します
    var item = SampleDataSource.GetItem((String)navigationParameter);
    this.DefaultViewModel["Group"] = item.Group;
    this.DefaultViewModel["Items"] = item.Group.Items;
    this.flipView.SelectedItem = item;
}


navigationParameterに、SampleDataItem.UniqueIdが格納されていて、SampleDataSourceの静的メソッドであるGetItemメソッドを呼び出しています。このメソッドにより、目的のSampleDataItemのインスタンスを取得して、DefaultViewModelのGroupキーとItemsキー、FilvepViewのSelectedItemを設定しています。CollectionViewSourceの定義は、GroupDetailPage.xamlと同一になっており、レイアウトのルートとなるGridに対するDataContextやd:DataContextもGroupDetailPage.xamlと同-です。 異なるのは、詳細情報をFlipViewコントロールで行っていることです。
FlipView

FlipViewコントロールは、コレクションの要素を右端と左端の矢印によってナビゲーションすることができます。従って、FlipViewコントロールを使うのはコレクションの要素を列挙するような用途に向いているということになります。それでは、ItemDetailPage.xamlのFlipViewの定義を以下に示します。

<!--このページの残りは 1 つの大きな FlipView です。ここには、一度に
     1 つのアイテムの詳細が表示され、ユーザーは選択されたグループ内のすべてのアイテムを見ることが
    できます
--><FlipView
    x:Name="flipView"
    AutomationProperties.AutomationId="ItemsFlipView"
    AutomationProperties.Name="Item Details"
    TabIndex="1"
    Grid.RowSpan="2"
    ItemsSource="{Binding 
           Source={StaticResource itemsViewSource}}"><FlipView.ItemContainerStyle><Style TargetType="FlipViewItem"><Setter Property="Margin" Value="0,137,0,0"/></Style></FlipView.ItemContainerStyle><FlipView.ItemTemplate><DataTemplate><!--表示状態管理をサポートしているため、テンプレート化されたアイテムとして選択された UserControl
                読み込まれた/アンロードされたイベントが、ページからのビューステートの更新を明示的に定期受信します
            --><UserControl 
                  Loaded="StartLayoutUpdates" 
                  Unloaded="StopLayoutUpdates"><ScrollViewer x:Name="scrollViewer" 
                     Style='{StaticResource HorizontalScrollViewerStyle}'
                     Grid.Row='1'><!-- コンテンツは、必要な数の列をフローできます --><common:RichTextColumns x:Name='richTextColumns'
                            Margin='117,0,117,47'><RichTextBlock x:Name='richTextBlock'
                             Width='560' 
                             Style='{StaticResource ItemRichTextStyle}' 
                             IsTextSelectionEnabled='False'><Paragraph><Run 
                                   FontSize='26.667' 
                                   FontWeight='Light' 
                                   Text='{Binding Title}'/><LineBreak/><LineBreak/><Run 
                                   FontWeight='Normal' 
                                   Text='{Binding Subtitle}'/></Paragraph><Paragraph LineStackingStrategy='MaxHeight'><InlineUIContainer><Image x:Name='image' 
                                        MaxHeight='480' Margin='0,20,0,10' 
                                        Stretch='Uniform' 
                                        Source='{Binding Image}' 
                                        AutomationProperties.Name='{Binding Title}'/></InlineUIContainer></Paragraph><!-- 以下省略 --></common:RichTextColumns><VisualStateManager.VisualStateGroups><!-- 表示状態には、FlipView 内のアプリケーションのビューステートが反映されます --><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='richTextColumns' 
                                      Storyboard.TargetProperty='Margin'><DiscreteObjectKeyFrame KeyTime='0' 
                                                   Value='17,0,17,57'/></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName='scrollViewer' 
                                        Storyboard.TargetProperty='Style'><DiscreteObjectKeyFrame KeyTime='0' 
                                           Value='{StaticResource VerticalScrollViewerStyle}'/></ObjectAnimationUsingKeyFrames><!-- 以下省略 --></Storyboard></VisualState></VisualStateGroup></VisualStateManager.VisualStateGroups></ScrollViewer></UserControl></DataTemplate></FlipView.ItemTemplate></FlipView>

 
FlipViewのItemSource属性に、CollectionViewSourceである ItemViewSourceを指定しています(データバインディンの設定)。次に説明することは、FlipView.ItemTemplateの定義になります。

  • UserControl:ここでのポイントは、LoadedとUnloadedイベントにLayoutAwarePageのStartLayoutUpdates、StopLayoutUpdatesイベントハンドラーを指定していることです。既に説明したように、LayoutAwarePage.StartLayoutUpdatesイベントハンドラーはControlオブジェクトに対してVisualStateManagerを使ってビューの切り替えを行います。つまり、ItemTemplateに対してビューの切り替えを行いたいためにItemTemplateのルート要素としてUserControlを定義しているのです。
  • ScrollViewer:水平スクロールビューアー スタイルが指定されていることに注意してください。フルなどのビューにおいて、水平スクロールバーを表示してコンテンツをスクロールさせます。
  • common:RichTextColumns:これはカスタムコントロールで、Common\RichTextColumn.csで定義されています。イメージ的には、リッチテキストボックスに近いコントロールですが、異なるのは追加の列をColumnTemplateで生成できることでしょう。
  • VisualStateManager:UserControlに対するVisualStateManagerの定義で、ポートレイトとスナップ時の定義を行っています。細かな定義を読めば理解できますが、単純に説明すればMaginやWidth、Heightなどをビューによって設定しています。

  そしてItemDetailPageに対するVisualStateManagerの定義が行われています。この定義は、ポートレイトとスナップにおけるタイトルなどの設定で、これまでに説明してきたGroupedItemPage.xamlやGroupDetailPage.xamlと同じものとなります。

ItemDetailPage固有のロジックとしては、SaveSateメソッドがあります。

/// <summary>
/// アプリケーションが中断される場合、またはページがナビゲーション キャッシュから破棄される場合、
/// このページに関連付けられた状態を保存します。値は、
/// <see cref="SuspensionManager.SessionState"/> のシリアル化の要件に準拠する必要があります。
/// </summary>
/// <param name="pageState">シリアル化可能な状態で作成される空のディクショナリ。</param>
protected override void SaveState(Dictionary<String, Object> pageState)
{
    var selectedItem = (SampleDataItem)this.flipView.SelectedItem;
    pageState["SelectedItem"] = selectedItem.UniqueId;
}

     
SaveSateメソッドのコメントで、何を行うものかは理解できることでしょう。問題は、どのタイミングで呼び出されるのかという点になります。それでは、LayoutAwarePage.OnNavigatedFromメソッドを以下に示します。

/// <summary>
/// このページがフレームに表示されなくなるときに呼び出されます。
/// </summary>
/// <param name="e">このページにどのように到達したかを説明するイベント データ。Parameter 
/// プロパティは、表示するグループを示します。</param>
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    var frameState = SuspensionManager.SessionStateForFrame(this.Frame);
    var pageState = new Dictionary<String, Object>();
    this.SaveState(pageState);
    frameState[_pageKey] = pageState;
}


フレームが表示されなくなるタイミングでOnNavigateFromイベントハンドラーが呼びされます。このコードでSuspensionManagerのSuspensionStateforFrameメソッドが呼び出されていることに注意してください。簡単に説明するなら、SusupensionManagerを使ってこのページへナビゲートしてきた状態を保存するということです。この機能が定義されていて、App.xaml.csのOnSuspendingイベントハンドラーを見てみましょう。

private async void OnSuspending(object sender, SuspendingEventArgs e)
{
    var deferral = e.SuspendingOperation.GetDeferral();
    await SuspensionManager.SaveAsync();
    deferral.Complete();
}


OnSupendingイベントハンドラーで、「SuspensionManager.SaveAsync()」と記述して、Frameオブジェクトによる状態を保存しています。このメソッドで、pageState["SelectedItem"] に設定したナビゲーション パラメーターも一緒に保存されます。つまり、Windowsストアアプリにおける一時停止とFrameオブジェクトのイベントハンドラーは以下のような関係にあります。

  1. Frame.OnNavigateFromイベントハンドラーが発生する。
    Frameが表示されなくなる
  2. OnSuspendingイベントハンドラーが発生する。

このような動きをすることで、Windows ストアアプリケーション固有の動きである、「一時停止から強制終了」、次に起動したタイミングで「状態を復旧」(最後に表示していあたページへ復帰させる)を実現しています。また、SampleDataSourceの説明で行ったようにSampleDataGroupとSampleDataItemは循環参照を持っています。この循環参照持っていることで、LoadStateメソッドに記述されている「this.DefaultViewModel["Group"] = item.Group」が成り立つようになっています。
ここまで説明したなら、LayoutAwarePage.OnNavigatedToイベントハンドラーも解説すべきでしょう。

/// <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]);
    }
}

OnNavigatedToイベントハンドラーでは、ナビゲーションに発生するナビゲーションパラメーターなどを設定したり、状態をクリアーしたりしています。このようにページの状態を保持する仕組みを用意していることで、GoBackイベントハンドラーで自由に戻るというナビゲーションを実現しています。

このようにグリッドアプリケーション テンプレートは、単純なテンプレートではありません。むしろ、Windows ストアアプリケーション開発が容易になるように作り込まれたサンプルであると説明した方が適切でしょう。 これが、グリッドアプリケーション テンプレートの解説を記述しようと考えたきっかけです。 このエントリーは、後1回位を予定しています。次回は、カスタマイズする場合のポイントを取り上げる予定です。

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

$
0
0

少し時間があきましたが、今回はグリッドアプリケーションを使った場合にどのようにカスタマイズするかということを説明します。最初に、開発体験テンプレートをご存じな方も多いことと思います。このテンプレート集のNewReaderのReadme.txtにカスタマイズのポイントが記載されているので、その中から抜粋したものを以下に示します。

  • (0) アプリの名前
  • (2) ブランディング
  • (3) プライバシーポリシー
  • (4) package.appxmanifest

最初にブランディングを説明します。ブランディングに関係する事項として、以下のようなことを考慮する必要があります。

  • バックグラウンド
    背景色や画像など
  • アイコン デザイン
    ロゴ(150x150)、ワイドロゴ(310x150)、小さいロゴ(30x30)、バッジロゴ(24x24)、スプラッシュスクリーン(620x300)、ストアロゴ(50x50)

特にブランディングは重要で、作成したアプリが容易に識別できるようにするためにも必須となります。また、アプリ内の各種のページ設計などを含めて統一感を持ってデザインすることが重要となり、これらの統一がアプリの印象を利用者に認識してもらうのに役立ちます。
このブランディングを考える上で、最初に取り組むのが背景色の変更だと思います。Windows ストア アプリのテンプレートでは、テーマという機能が含まれておりデフォルトのテーマは「Dark」となっています。これをSDKサンプルのように白を基調としたテーマにするには、App.xamlに「RequestedTheme」属性を記述します。

<Application・・・・・
	RequestedTheme = "Light"
	・・・・・   >

 
アプリの名前を変更するのも忘れないようにしましょう。アプリの名前は、App.xamlに「AppName」というキーを持つリソースとして定義されています。

プライバシーポリシーは、ネットワーク通信を行うアプリでは必須となります。Windows 8 アプリの認定の要件 (Windows)の要件4.1.1に「ユーザーの IP アドレスなどの個人情報をアプリで収集または送信する場合は、」とあり、収集するかしないは別として通信を行う場合は通信インフラストラクチャーがIPアドレスを送信します。この理由から、プライバシーポリシーを用意することが必須となります。特に、Visual Studioで作成した新規プロジェクトではデフォルトで「インターネット」が有効になっているので、注意しましょう。用意するプライバシーポリシーは、以下の2ヶ所への明示が必須となります。

  • アプリ内でプライバシーポリシーを表示できるようにする。
    アプリで表示するページでも良いですし、Webサイトへのリンクでも構いません。
  • ストアのアプリ紹介ページからプライバシーポリシーを表示できるようにする。
    プライバシーポリシーを掲示しているWebサイトのリンクを用意する必要があります。

Package.appmanifestは、特に以下の点を正しく設定します。

  • アプリケーション UI タブ
    表示名:必ずデフォルトの名前から変更します。アプリケーションの名前で、タイルなどに表示されます。長い場合は、短い名前も適切に設定します。
    説明
    各種ロゴと背景色。特にスプラッシュスクリーンの背景色は、アプリ全体の背景色と統一感を持って設定します。
  • パッケージ化 タブ
    パッケージ表示名:必ずデフォルトの名前から変更します。
    パッケージ名:変更しても良いでしょうし、デフォルトのGUIDにしておいても構いません。

パッケージ名やパッケージ表示名は、意図的にデフォルトの値にしておくこともあります。これは、セキュリティ上の理由です。Windows ストアアプリは、インストール自体がパッケージ名単位で行われます。逆を言えば、パッケージ名が理解しやすいと、カジュアルハックがし易いとも言えます。カジュアルハックと言っても、パッケージのローカルストア(ユーザー プロファイルの中にあります)をWindows エクスプローラーなどで覗いたりという意味です。パッケージ名を変更することは、一見しただけでアプリとの関連付けを連想させないためだけに、アプリと関係の無いパッケージ名やパッケージ表示名にしておくという程度の効果しかありません。この理由は、タスクマネージャーなどを使うことで、パッケージ名を利用者が知ることができるからです。

ここまで説明してきたのは、NewReaderテンプレートのReadme.txtに基づいています。新規のグリッドアプリケーションを自分で作成した場合は、データソースの変更が最初に行うステップとなります。以下に記載するのは、必ずではありませんが、このような考え方でNewsReaderはデータソースをカスタマイズしたと理解して頂ければ結構です。

  • Sampleで始まる名前をNewsにリネーム
  • NewsDataComon、NewsDataItemクラスへプロパティを追加
  • NewsDataGroupクラスへ、TopHighlitedItemsプロパティとNumberOfToken、ArticleCountプロパティを追加
    グリッドアプリケーションのHubページは、TopItemsプロパティをデータバインドしています。TopItemsとは、Itemsプロパティのコレクションに対する操作に連動して最初の12件を保持するコレクションです。
    NewsDataGroupでは、NumberOfToken(デフォルト4)を設定することで、設定した件数のNewsDataItemを返すTopHighlitedItemsプロパティを用意しています。このプロパティが、Hubページにデータバインドしています。
    セマンティックズームのズームアウトビューにデータバインドするために、件数を返すArticleCountプロパティを用意しています。
  • NewsDataSourceクラスへ以下を追加
    GetGroupメソッドとGetItemsメソッドのバグへの対処( == 1 を >= 1)。
    IsNetworkAvailable(オフライン判定)、IsInitialized(データの読み込み済みかどうか)プロパティを追加。
    コンストラクターで、DesignMode.DesignModeEnabledの場合にサンプルデータの作成。
    LoadRemoteDataAsyncメソッドを追加して、このメソッドで実際のデータを読み込んでいます。
    TwitterやRSSフィードや、ローカルへの保存や読み込みなどを行うメソッドを追加。

簡単に言えば、自分が作成するアプリが必要とするデータに応じてデータモデルを変更するということです。また、ズームアウトビューに対応させるために、必要なプロパティの実装を行ったり、オフラインサポートに応じて必要な機能を実装したりする必要があるということです。もちろん、オフラインサポートが必須というわけではありません。が、アプリを使用するユーザーにとってモバイルシーンでは必須とも言えるでしょう。

データソースを変更すれば、そのデータの表示方法も変更させる必要があります。具体的には、データテンプレートを自分のデータ用にカスタマイズする作業となります。この目的で、NewsReaderテンプレートは以下のようにApp.xamlでデータテンプレートを定義しています。

  • DefaultGridItemTemplate
    GroupedItemPageのGridViewのItemTemplateです。グリッドアプリケーションのデフォルトは、Standard250x250ItemTemplateになっています。
  • ItemGridTemplate
    GrpupDetailPageのGridViewのItemTemplateです。グリッドアプリケーションのデフォルトは、Standard500x130ItemTemplateになっています。
  • ZoomedOutItem
    NewsReaderテンプレートのセマンティックズーム サポートで使用しているズームアウトビュー用のデータテンプレートです。

NewsReaderテンプレートでは、アプリバーなどを導入しており、そのために必要となるスタイルをApp.xamlにリソースとして定義しています。これ以外に、行うカスタマイズとしては、次に説明するようなものがあります。

設定コントラクト

Visual Sudioではテンプレートの用意がありませんので、NewsReaderテンプレートなどのApp.xaml.csから必要なコードを引用してください。特に、プライバシーポリシーの明示を行うには必須となる作業です。

検索コントラクトや共有ターゲットコントラクト

Visual Studioを使ってプロジェクトへ[追加]-[新しい項目]で表示されるウィザードを使って、検索コントラクト、あるいは共有ターゲットコントラクトを追加して雛形を作成します。追加した後に、以下の作業を行います。

  • 追加されたページが、LayoutAwarePageを継承していない場合は継承するように変更します。
  • ロジックを作成します。ロジックの参考例としては、開発体験テンプレートを参照してください。
  • 検索コントラクトや共有ターゲットコントラクトを追加すると、ウィザードは以下の作業を行っています。
    App.xaml.csにコントラクト用のアクティベーション コードを追加。
    該当ページ.xamlを追加。

15秒以内にアクティブ化を完了する

Windows ストアアプリでは、タイルをタップしてから起動が完了するまでに15秒を超えるとシステムがアプリを強制終了する仕組みを備えています。たとえばNewsReaderテンプレートなどは、BlogやTwitterをネットワークを使って読み込んでデータソースを作成しますから、容易に15秒という時間を超えてしまいます。仮に、15秒を超えていればシステムに強制終了させられるため、アプリが起動することはなくなってしまいます。この症状を避けるためにNewsReaderテンプレートが行っている方法は、以下のようなものです。

  • LoadStateメソッドを非同期とする

この対策を行っても複数のフィードを行うとビューが表示されるまでに、時間がかかることになってしまいます。この理由から、NewsReaderテンプレートでは、1)1画面に収まるデータの取得、2)残りのデータを取得の2段階に分類しており、後者を非同期に処理しています。つまり、ビューが表示された後も、データの読み込みを続けるので徐々にデータが増えていくという動作を行うことで、ユーザーがビューを操作できるようになる時間の短縮を行っています。
このような手法には、画一的なアプローチはなく、取り扱うアプリのコンセプトやデータによって最適と思われる方法を使っていく必要があります。そのためには、作成されたアプリのボトルネックが何処に存在するかを調査して、その結果をもとに解決策を考えていきます。ちなみに要件3.8では、「5秒以内に起動すること」とあり、起動時にかけられる制限は厳しくなっています。つまり、データの初期化を同期的に行うのではなく、非同期処理を適切に組み合わせる必要があるということになります。

5秒以内に一時停止を完了する

Windows ストアアプリでは、アプリがバックグラウンドに移行するとシステム側がシステムリソースとの関係で一時停止させたり、強制終了させたりする場合があります。この時の状態遷移は、「一時停止」-[復帰]、[一時停止]-[強制終了]の2種類となります。一時停止に伴い、Suspendイベントがアプリに通知されるため、Suspendイベントの処理は「5秒以内に完了」する必要があります。仮に、5秒を超えるとシステムがアプリを強制終了させます。
一般的に一時停止イベントで行う処理は、以下のようなものです。

  • アプリのセッションデータを保存します。
    セッションデータとは、動作している間の一時的なデータであり、強制終了後の起動で元の状態に戻すために必要とするデータです。

この動作は、強制終了された後にアプリを起動した時に、強制終了させられた状態にアプリを復帰させる場合は実装する必要があります。既に説明したようにグリッドアプリケーションは、SuspensionManagerによってFramのナビゲーション履歴を保存するようになっています。従って、この機能を有効にしている限りは、セッションデータの保存と復帰は必須となります。問題は、セッションデータの保存と復帰の実装コードを何処に記述するかという点になります。

  • App.xaml.cs
    SusupensionManagerが、保存や復帰を行っているので統一性が取れて簡単です。が、扱うデータが大きい場合は、アクティブ化や一時停止の時間制限が問題にならないかと検討しましょう。
  • LoadStateメソッド
    NewsReaderテンプレートで採用した方法です。LoadStateを非同期メソッドにすることで、アクティブ化の時間制限を回避することができます。
    問題になるのは、どのタイミングで保存するかということです。

NewsReaderテンプレートでは、RSSなどのフィードの読み込みが完了した時にデータを保存するようにしています。この手法の根底にある考え方は、データモデルに変化があったタイミングで保存するというものです。この当たりの考え方も統一的なものはありませんので、作成されるアプリに応じて考えるものとなります。ちなみに要件3.8では、「2秒以内に中断を実行できること」とあり、かなり厳しくなっています。要件3.8が厳しくなっている理由は、非力なPCのサポートを前提に置いていると考えると良いでしょう。つまり、サポートCPUを限定すれば問題がなくなることも少なくないということになります。

ユーザーコントロールを作成するには

C#やVB、そしてC++でxamlを使って開発される場合は、ユーザーコントロールを作成したい場合もあることでしょう。ユーザーコントロールを作成する場合に、注意するのはデータバインドとVisualStateManagerによるビューをどのように切り替えるかという点になります。最初に、簡単なXAMLの定義を以下に示します。

<UserControl
    x:Class="App4.MyUserControl1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App4"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    x:Name="self"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400"><Grid><TextBox x:Name="txtInput" 
                 Width="{Binding Width, ElementName=self}"
                 Height="{Binding Height, ElementName=self}"
                 Text="{Binding Data, ElementName=self, Mode=TwoWay}"
                 /></Grid></UserControl>

 
ユーザーコントロールにTextBoxを置いただけのシンプルなxamlです。このxaml定義で、注意する必要があるのはUserControlのName属性とデータバインドの記述方法の2種類になります。

  • x:Name="self"
    UserControlにName属性を付与することは、次のデータバインドを要素内で完結させるために必須となります。名前は、理解しやすいように「self」としていますが、何でも構いません。
  • {Binding Data, ElementName=self, Mode=TwoWay} 
    最初の「Data」が要素名を表し、ElementNamaeが自分自身(作成しているユーザーコントロール)を指します。つまり、MyUserControl1.Dataプロパティを指していることになります。最後のModeがバインディングを双方向にしています。これは、TextBoxコントロールが入力を受け取る性格から設定しています。実際には、一方向でも問題はありません。要は、作成されるコントロールが双方向バインディングをサポートするかどうかで判断すれば良いということです。

次にユーザーコントロールに実装したコードを以下に示します。

// 依存プロパティの定義
public static DependencyProperty DataProperty =
       DependencyProperty.Register("Data",
         typeof(string), typeof(MyUserControl1),
         new PropertyMetadata(string.Empty, 
             new PropertyChangedCallback(MyUserControl1.OnDataChanged)));
// プロパティ変更のイベントハンドラー
private static void OnDataChanged(DependencyObject obj,
                            DependencyPropertyChangedEventArgs args)
{
    var myUserControl1 = (MyUserControl1)obj;
    myUserControl1.Data = (string)args.NewValue;
}
// 依存プロパティの実装
public string Data
{
    get
    {
        return this.GetValue(DataProperty) as string;
    }
    set
    {
        this.SetValue(DataProperty, value);
    }
}

   
コードを見れば理解できると思いますが、DependencyPropertyを定義する必要があります。この定義で注意すべきことは、PropertyMetadataの定義だと思います。双方向バインディングを行う場合は、PropertyMetadataの第2引数にPropertyChangedCallbackを指定します。一方向バインディングであれば、第2引数はnullで構いません。この点が、データバインド可能なプロパティを実装する上での注意点となります。次に、このコントロールを使用するXAMLの定義を以下に示します。

<local:MyUserControl1 x:Name="myControl1" Data="{Binding Data, Mode=TwoWay}" 
                      Margin="95,127,0,0" Width="430" Height="65"/>


後は、myControl1のDataContextにデータソースを設定することで、データバインドは完了となります。 上記で説明したことは、データバインド可能なユーザーコントロールを作成する上での最低限の知識となります。次に、VisualStateManagerを定義する上で注意する点を以下に示します。

  • ユーザーコントロール側のXAMLにVisualStateManagerを記述する。
    記述することで、Visual Studioのデバイスタブでビューを切り替えてデザインの確認を行うことができます。
  • 利用する側のXAMLで、LoadedとUnloadedにLayoutAwarePageのイベントハンドラーを設定する。
    グリッドアプリケーションのVisualStateManagerを使ったビューの切り替えで説明したように、LayoutAwarePageのStartLayoutUpdatesメソッドとStopLayoutUpdateメソッドを使うことでビューの切り替えが行われるようになるためです。

上記に説明したのは、基本的な方法となります。もちろん、LayoutAwarePageを使用しない場合ではビューの切り替えを自分で考える必要があります。たとえば、ウィンドウのSize変更イベントなどを使って、ユーザーコントロール内で適切なビューに合わせて配置を変更するなどの方法も考えられます。この方法の欠点は、デザイン時のビューの確認をどうするかということです。デザイン時に確認したいとなれば、XAML内にVisualStateManagerを定義せざるを得ません。

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 第6版 第7章 WPF編

$
0
0

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

第7章 非同期性

この章では、.NET Framewrok 4.5(C# 5.0、Visual Basic 11)で追加された新しい非同期プログラミング(async、await)を説明しています。この説明の中で、MessageDialog クラスを取り扱っていますが、このクラスは WinRT のみで使用できるものになっています。WPF XAML では同等の機能はなく、MessageBox クラス (Windows Forms でも MessageBox クラス)があるだけになります。従って、7 章のタイトルにある非同期性を同じコードで説明するために、独自の MessageDialog クラスを用意して説明することにします。独自の MessageDialog クラスについては、説明の中で具体例な説明を盛り込んでいきます。

7.1(P245) スレッドとユーザー インターフェース

本節では、UI スレッドと時間のかかる処理の問題を取り上げ、.NET Framework 4.0 から採用されたタスク(System.Threading.Tasks.Task) クラスを使うことで非同期性と並行処理がサポート(TAP)されていることを説明していますので、基本概念の理解として熟読してください。.NET Framework 3.5 までは、自分でスレッド オブジェクトを使用するか、スレッド プールを使用して非同期プログラミング(APM、または EAP)をするしかありませんでした。

7.2(P246) MessageDialog の使用

本節では、WinRT の MessageDialog クラスを使って非同期メソッドの説明をしています。すでに説明したように、WPF には MessageDialog クラスがありませんので、HowToAsync1 プロジェクト用に MessageDialog クラスを作成しました。作成した MessageDialog クラスの MessageDialog.xaml を示します。

<Window x:Class="HowToAsync1.MessageDialog"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MessageDialog" Height="180" Width="370" 
        WindowStyle='ToolWindow' ShowInTaskbar='False' ><Grid Margin='10'><TextBlock x:Name='message' HorizontalAlignment='Center'
                   Text='メッセージ' /><StackPanel x:Name='body' Orientation='Horizontal' Margin='0, 30,0,0'></StackPanel></Grid></Window>

Window クラスを利用して、WindowStyle を「ToolWindow」(Close ボタンのみの表示)にし、ShowInTaskbar を「False」に設定しています。また、指定されたメッセージを表示するために TextBlock を配置し、指定されたコマンド ボタンを配置するための StackPanel を配置しています。今度は、MessageDialog.xaml.cs の抜粋を示します。

public partial class MessageDialog : Window
{
    const double WIDTH = 100;
    const double HEIGHT = 30;
    const double MARGIN = 5;
    object color;

    public MessageDialog()
    {
        InitializeComponent();
    }

    public MessageDialog(string title, string message, DialogButton[] buttons)
        : this()
    {
        this.Topmost = true;
        this.Title = title;
        this.message.Text = message;
        this.color = null;
        // ボタンのコマンド オブジェクト
        var command = new DelegateCommand( (parameter) =>
            {
                // 戻り値を準備する
                color = parameter;
                // ダイアログを閉じる
                this.Close();
            });
        // ボタンを追加します
        foreach (var b in buttons)
        {
            var button = new Button()
            {
                Content = b.Title,
                Width = WIDTH,
                Height = HEIGHT,
                Margin = new Thickness(MARGIN),
                Command = command,
                CommandParameter = b.Id
            };
            this.body.Children.Add(button);
        }
    }

    // 非同期メソッド
    public Task<Object> ShowAsync()
    {
        var dispatcherOperation = Dispatcher.InvokeAsync(() =>
        {
            var ret = this.ShowDialog();
            // 戻り値を返す
            return this.color;
        }, System.Windows.Threading.DispatcherPriority.Normal);
        dispatcherOperation.Completed += (s, args) => {
            // 戻る値を返すまで、Objectを延命させる
             };
        return dispatcherOperation.Task;
    }
}

class DelegateCommand : ICommand
{
    Action<object> execute;
    Func<object, bool> canExecute;
    // Event required by ICommand
    public event EventHandler CanExecuteChanged;

    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 = AlwaysCanExecute;
    }

    public bool CanExecute(object parameter)
    {
        return canExecute(parameter);
    }

    public void Execute(object parameter)
    {
        execute(parameter);
    }

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

コンストラクターに、3 つの引数を渡します。

  • title:メッセージ ダイアログに表示するタイトル文字列を指定します。
  • message:メッセージ ダイアログに表示するメッセージとなる文字列を指定します。
  • buttons:これからコードを示しますが、コマンドのタイトルと Id を指定した DialogButton の配列を指定します。これが、メッセージ ダイアログに表示するコマンドとなります。

この引数を受け取ってから、タイトルとメッセージを設定してから、StatckPanel に Button コントロールを追加します。Button コントロールで使用するコマンドのために、DelegateCommand クラスを用意しています。コマンドは、押されたボタンに対するオブジェクト(Id)をフィールドに設定するようにしています。そして、ShowAsync メソッドによって、MessageDialog ウィンドウをモーダルで表示して、戻り値である color フィールドを返すようにしています。今度は、DialogButton.cs を示します。

namespace HowToAsync1
{
    public class DialogButton
    {
        public DialogButton(string title, object color)
        {
            this.Title = title;
            this.Id = color;
        }

        public string Title { get; set; }
        public object Id { get; set; }
    }
}

作成した MessageDialog クラスにの使い方を示します。

var buttons = new DialogButton[]
{
    new DialogButton("Red", Colors.Red),
    new DialogButton("Green", Colors.Green),
    new DialogButton("Blue", Colors.Blue)
};

var m = new MessageDialog("Choose a color", "How To Async #1", buttons);
var messageTask = m.ShowAsync();

DialogButton 配列のインスタンスを作成してから、MessageButton クラスのインスタンスを作成した後に ShowAsync メソッドを呼び出すことで、「Task<object>」 が返ります。WinRT の MessageDialog.ShowAsync が返すのは「IAsyncOperarion<IUICommand>」型であり、この型は WinRT 固有になります。Task<T> も IAsyncOperation<T> と同様に、await で戻り値を待機することができますので、非同期メソッドとの使い方は同じとなります(作成した ShowAsync メソッドの実装方法方は本章の後半に説明がありますので、ここでは WinRT と同じように非同期メソッドとして使用できると考えていただければ、結構です)。

HowToAsync1 プロジェクトで、MessageDialog.ShowAsync メソッドの戻り値を受け取るには、戻り値に対する継続タスクを使用します。それでは、MainWindow.xaml.cs を示します。

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

    private void OnButtonClick(object sender, RoutedEventArgs e)
    {
        var buttons = new DialogButton[]
        {
            new DialogButton("Red", Colors.Red),
            new DialogButton("Green", Colors.Green),
            new DialogButton("Blue", Colors.Blue)
        };
        var m = new MessageDialog("Choose a color", "How To Async #1", buttons);
        var messageTask = m.ShowAsync();

        messageTask.ContinueWith((task) =>
            {
                var ret = task.Result;
                if (ret == null) return;    // 戻り値がnullの場合
                clr = (Color)ret;
                Dispatcher.BeginInvoke(
                    new Action(() => { OnDispatcherRunAsyncCallback(); }), 
                    null);
            });
    }

    void OnDispatcherRunAsyncCallback()
    {
        // Set the background brush
        contentGrid.Background = new SolidColorBrush(clr);
    }
}

ContinueWith メソッドにより継続タスクを登録しています。継続タスクで、Task オブジェクトの Result プロパティを使って戻り値を取得してから、Color 構造体へキャストしてから、Dispacher オブジェクトを使って Grid の Background プロパティに設定しています。それでは、実行結果を示します。
HowTowAsync1

 

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

<Window ... ><Grid x:Name="contentGrid"><Button Content="Show me a MessageDialog!"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Click="OnButtonClick" /></Grid></Window>

モーダルで MessageDialog クラスが表示されて、ボタンをクリックすると MessageDialog クラスが閉じて、ウィンドウの背景色がクリックしたボタンの色に設定されるようになります。書籍には、なぜ Dispatcher オブジェクトを使用するかが記述されています。この理由は、ContinueWith メソッドで登録された継続タスク(書籍で説明している Completed コールバックと同等であると理解してください)が、UI スレッドで動作しないことが理由となります。こうした考え方は、WPF XAML と WinRT XAML は同じですので、書籍の説明を熟読してください。

7.2(P252) コールバックでのラムダ関数の使用

本節では、WinRT の IAsyncOperation の Completed コールバックの代わりにラムダ関数を使用した方法を説明しています。このため、前節で作成した MessageDialog クラスを使用して、TaskAwaiter 構造体を使用した OnCompleted メソッドを使用して同等の処理にした、HowToAsync2 プロジェクトの MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{

    public MainWindow()
    {
        InitializeComponent();
    }

    private void OnButtonClick(object sender, RoutedEventArgs e)
    {
        var buttons = new DialogButton[]
        {
            new DialogButton("Red", Colors.Red),
            new DialogButton("Green", Colors.Green),
            new DialogButton("Blue", Colors.Blue)
        };

        var m = new MessageDialog("Choose a color", "How To Async #2", buttons);
        var messageTask = m.ShowAsync();

        var messageAwaiter = messageTask.GetAwaiter();
        messageAwaiter.OnCompleted(() =>
        {
            var ret = messageAwaiter.GetResult();
            if (ret == null) return;    // 戻り値がnullの場合
            Color clr = (Color)ret;
            contentGrid.Background = new SolidColorBrush(clr);
        });
    }
}

Task<object> 型である messageTask の GetAwaiter 拡張メソッドを呼び出すことで、TaskAwaiter 構造体のインスタンスを取得しています。そして、TaskAwiter の OnCompleted メソッドにラムダ関数を指定することで、Grid の Background を設定しています。前節や WinRT XAML と大きく違う点は、Dispatcher オブジェクトを使用していない点となります。TaskAwaiter の OnCompleted メソッドは、GetAwaiter メソッドを呼び出した UI スレッドで処理されるという特徴を持っています。ここまでの WPF XAML としての説明は、非同期メソッドの呼び出しを UI スレッドと異なる継続タスクとして表現する方法と TaskAwaiter 構造体を使ったタスクの完了イベントを使って表現する方法を示すことを意図しています。Dispatcher 自体の使い方は書籍と異なりますが、非同期メソッドを同じような用途で扱うことができると理解していただくことを目的にしています。

7.4(P253) await 演算子

本節では、.NET Framework 4.5(C# 5.0、Visual Basic 11)で追加された async/ await の利用法を説明しています。前節までで、非同期メソッドの基本となる利用方法を踏まえて、async/ await を使用するとコードがどのように変化するかという点に焦点を当てています。それでは、HowToAsync3 プロジェクトの MainWindow.xaml の抜粋を示します。

private async void OnButtonClick(object sender, RoutedEventArgs e)
{
    var buttons = new DialogButton[]
    {
        new DialogButton("Red", Colors.Red),
        new DialogButton("Green", Colors.Green),
        new DialogButton("Blue", Colors.Blue)
    };

    var m = new MessageDialog("Choose a color", "How To Async #3", buttons);
    var result = await m.ShowAsync();

    if (result == null) return;    // 戻り値がnullの場合
    Color clr = (Color)result;
    contentGrid.Background = new SolidColorBrush(clr);

}

await を使用するには、メソッド定義に「async」を付与します。そして、非同期メソッドの呼び出しに「await」を付与することで、同期的にコードを記述することができます。この機能により、Dispatcher オブジェクトを記述する必要がなくなりました。もちろん、機能的には前節までに説明した HowToAsync1 と HowTowAsync2 プロジェクトと同じになります。書籍では、どのような場合に async を使用できるかを説明していますので、書籍を熟読してください。
Visual Studio 2012/2013 のコード エディタは、非同期メソッド呼び出しに対して「await」を記述しないと警告の波線を表示します。この警告を表示させたくない場合は、非同期メソッドの戻り値を変数に代入してください。この方法が、前節までに使用しているコードになります。

7.5(P256) 非同期操作の取り消し

本節では、非同期メソッドの呼び出しをキャンセルする方法を説明しています。WinRT の非同期メソッドの大半は、呼び出しをキャンセルすることができるようになっています。この仕組みを使って、キャンセルする方法を具体例を使って説明しています。前節まででWPF XAML 用に作成した MessagDialog クラスは、キャンセルの仕組みを実装していませんので、ここではキャンセルできるようにした HowToCancelAsync プロジェクトの MessageDialog.xaml.cs を抜粋して示します。

public partial class MessageDialog : Window
{
    const double WIDTH = 100;
    const double HEIGHT = 30;
    const double MARGIN = 5;
    object color;
    DispatcherOperation<Object> dispatcherOperation;
    CancellationToken token;
    DispatcherTimer timer = null;

    public MessageDialog()
    {
        InitializeComponent();
    }

    public MessageDialog(string title, string message, DialogButton[] buttons)
        : this()
    {
        this.Topmost = true;
        this.Title = title;
        this.message.Text = message;
        this.color = null;

        // ボタンのコマンド オブジェクト
        var command = new DelegateCommand( (parameter) =>
            {
                // 戻り値を準備する
                color = parameter;
                // ダイアログを閉じる
                this.Close();
            });
        // ボタンを追加します
        foreach (var b in buttons)
        {
            var button = new Button()
            {
                Content = b.Title,
                Width = WIDTH,
                Height = HEIGHT,
                Margin = new Thickness(MARGIN),
                Command = command,
                CommandParameter = b.Id
            };
            this.body.Children.Add(button);
        }

        this.Unloaded += MessageDialog_Unloaded;

    }

    void MessageDialog_Unloaded(object sender, RoutedEventArgs e)
    {
        if (this.timer != null)
        {
            this.timer.Stop();
            this.timer.Tick -= timer_Tick;
        }
    }

    // 非同期メソッド
    public Task<Object> ShowAsync()
    {
        dispatcherOperation = Dispatcher.InvokeAsync(() =>
        {
            var ret = this.ShowDialog();
            // 戻り値を返す
            return this.color;
        }, System.Windows.Threading.DispatcherPriority.Normal);
        dispatcherOperation.Completed += (s, args) => {
            // 戻る値を返すまで Objectを延命させる
             };
        
        return dispatcherOperation.Task;
    }

    // キャンセル可能な非同期メソッド
    public Task<Object> ShowAsync(CancellationToken token)
    {
        // 操作のキャンセルを可能にします(1秒間隔で確認します)
        this.token = token;
        timer = new DispatcherTimer();
        timer.Interval = TimeSpan.FromSeconds(1);
        timer.Tick += timer_Tick;
        timer.Start();

        return ShowAsync();
    }

    void timer_Tick(object sender, EventArgs e)
    {
        token.ThrowIfCancellationRequested();
    }


    public void Cancell()
    {
        dispatcherOperation.Abort();
        this.Close();
    }
}

キャンセル可能な ShowAsync メソッド(CancellationToken を引数)を用意して、DispatcherTimer を使ってキャンセル可能かどうかを確認するようにしています。この MessageDialog クラスで、DispatcherTimer を使ってキャンセルを実現している理由は、Window オブジェクトをモーダルで表示しているためです。それでは、非同期メソッドをキャンセルする HowToCancelAsync プロジェクトの MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    CancellationTokenSource tokenSource;

    public MainWindow()
    {
        InitializeComponent();
    }

    private async void OnButtonClick(object sender, RoutedEventArgs e)
    {
        var buttons = new DialogButton[]
        {
            new DialogButton("Red", Colors.Red),
            new DialogButton("Green", Colors.Green),
            new DialogButton("Blue", Colors.Blue)
        };
        tokenSource = new CancellationTokenSource();

        var message = new MessageDialog("Choose a color", "How To Cancel Async", buttons);

        // Start a five-second timer
        var timer = new DispatcherTimer();
        timer.Interval = TimeSpan.FromSeconds(5);
        timer.Tick += OnTimerTick;
        timer.Start();

        var asyncOp = message.ShowAsync(tokenSource.Token);
        
        object result = null;
        try
        {
            result = await asyncOp;
        }
        catch (Exception)
        {
            // The exception in this case will be TaskCanceledException
            message.Close();
        }
        timer.Stop();

        if (result == null) return;    // 戻り値がnullの場合
        Color clr = (Color)result;
        contentGrid.Background = new SolidColorBrush(clr);
    }

    void OnTimerTick(object sender, EventArgs e)
    {
        tokenSource.Cancel();
    }
}

CancellationTokenSource のインスタンスを作成して、ShowAsync メソッドの引数に CancellationToken を渡しています。また、DispatcherTimer を利用して MessageDialog を表示してから 5 秒後に CancellationTokenSource を使って Cancel メソッドを呼び出すことで、MessageDialog クラスにキャンセルを通知しています。結果として、MessageDialog が TaskCanceledException という例外を通知しますので、MessageDialog の Close メソッド を呼び出して MessageDialog を閉じています。WinRT XAML では、MessageDialog クラスがキャンセルをサポートしているので、MessageDialog を閉じるような操作は必要ありません。ここで重要なことは、非同期メソッドをキャンセルする場合に、TaskCanceledException という例外が通知されるということです。

7.6(P258) ファイル I/O へのアプローチ

本節では、WinRT XAML では System.IO 名前空間の ファイル I/O 系の API が縮小されて、WinRT 専用としてファイル I/O 用の API が提供されているということを説明しています。WPF XAMLではというよりも、.NET Framework 4.5 では、System.IO 名前空間の API のいくつかに対して非同期メソッドの提供という機能拡張がされています。具体的には、非同期ファイル I/Oのドキュメントを参照してください。この理由から、次節以降では、WinRT の ファイル I/O を System.IO 名前空間に追加された非同期ファイル I/O に移植して説明をしていきます。この関係で、WinRT 固有の機能になることが多いことから、一般的に考えられる手法で読み替えていきます。これは、必ずこのようにして下さいというようなものではなく、このように置き換えられると考えられるだけのことです。自分の環境に置き換えて、適時読み替えて読み進めてください。

7.61.(P259) アプリケーション ローカル ストレージ

本節では、WinRT に固有なアプリケーション ローカル ストレージという概念を説明しています。デスクトップのアプリには、同様の概念はありません。一般的には、アプリを配置した場所であったり、ユーザーのドキュメント フォルダーなどが同様の目的で使用されます。但し、アプリを配置した場所が「Program Files」などの場合は、ファイル システムに対して書き換えなどを行うためには管理者権限が必要になることから、ユーザープロファイルなどに独自のフォルダーを作成するなどの処理が必要になりますので、ご注意ください。

7.6.2(P259) ファイル ピッカー

WinRT では、ユーザーがファイル システムからファイルを選択したりするための共通操作としてファイル ピッカー を用意しています。この ファイル ピッカーの基本的な考え方を説明しています。WPF XAML などのデスクトップ アプリは、Windows のコモン ダイアログを使用することでファイル システムからファイルを選択したりするための共通操作を行うことができます。WPF XAML では、Microsoft.Win32 名前空間に OpenFileDialog クラスと SaveFileDialog クラスを用意しています。Windows Forms では、System.Windows.Forms 名前空間で OpenFileDialog、SaveFileDialog、FolderBrowserDialog、FontDialog などを用意しています。つまり、WPF XAML では Windows Forms より少ないコモン ダイアログしか用意していません。この理由から不足するダイアログを使用するためには、Windows Forms のコモン ダイアログを使用するか、サードパーティー製のダイアログを使用するか、独自のコモン ダイアログを作成する必要があります。特に Windows Forms の ColorDialog などが返す型は、そのままでは WPF XAML で使用できないので、型を自分で変換する必要がある点に注意してください。型を自分で変換したとしても、スクラッチでコモン ダイアログを作成するよりは工数が少なくなるはずですので、用途によって最適なものを選択するようにしてください。

7.6.3(P260) バルクアクセス

本節で説明している概念は、WinRT 固有のものになります。従って、ファイル情報やフォルダー情報を扱う場合は、System.IO 名前空間の API を使用するようにしてください。

7.7(P261) ファイル ピッカーとファイル I/O

本節では、ここまでに説明したファイル ピッカーやファイル I/O 系の API の使い方を PrimitivePad プロジェクトという、メモ帳に似たアプリを使って説明してます。また、WPF XAML への基本的な考え方も説明してきていますので、PrimitivePad プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Style x:Key="buttonStyle" TargetType="ButtonBase"><Setter Property="HorizontalAlignment" Value="Center" /><Setter Property="Margin" Value="0 12" /></Style></Window.Resources><Grid Background="Black"><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="*" /></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><Button Content="Open..."
                Grid.Row="0"
                Grid.Column="0"
                Style='{StaticResource buttonStyle}'
                Click='OnFileOpenButtonClick' /><Button Content='Save As...'
                Grid.Row='0'
                Grid.Column='1'
                Style='{StaticResource buttonStyle}'
                Click='OnFileSaveAsButtonClick' /><ToggleButton x:Name='wrapButton'
                      Content='No Wrap'
                      Grid.Row='0'
                      Grid.Column='2'
                      Style='{StaticResource buttonStyle}'
                      Checked='OnWrapButtonChecked'
                      Unchecked='OnWrapButtonChecked' /><TextBox x:Name='txtbox'
                 Grid.Row='1'
                 Grid.Column='0'
                 Grid.ColumnSpan='3'
                 FontSize='24'
                 ScrollViewer.HorizontalScrollBarVisibility='Auto'
                 ScrollViewer.VerticalScrollBarVisibility='Auto'
                 AcceptsReturn='True' /></Grid></Window>

記述している XAML 自体は、すでに説明してきた変更だけで WPF XAML 用にしています。大きく異なるのは、各ボタンに対応するイベント ハンドラーの実装になります。最初に、 ファイルを開くボタンのイベント ハンドラーである OnFileOpenButtonClick を示します。

private async  void OnFileOpenButtonClick(object sender, RoutedEventArgs e)
{
    var dlg = new Microsoft.Win32.OpenFileDialog();
    dlg.Filter = "テキスト(*.txt)|*.txt";
    dlg.DefaultExt = ".txt";
    var dlgResult = dlg.ShowDialog();
    if (dlgResult != true) 
    {
        return;
    }

    using (var stream = File.OpenRead(dlg.FileName))
    {
        var textReader = (TextReader) new StreamReader(stream);
        var text = await textReader.ReadToEndAsync();
        txtbox.Text = text;
    }
}

FileOpenPicker を OpenFileDialog へ書き換えて、StrageFile などの WinRT API を System.IO の API へと置き換えています。また、非同期メソッドに対応するために、TextReader クラスに追加された ReadToEndAsync 非同期メソッド呼び出しを使用するようにしています。このように適切に書き換えることで、WinRT の ファイル I/O API を WPF XAML へ移植することができます。
今度は、 名前を付けて保存するボタンのイベント ハンドラーである OnFileSaveAsButtonClick を示します。

private async void OnFileSaveAsButtonClick(object sender, RoutedEventArgs e)
{
    var dlg = new Microsoft.Win32.SaveFileDialog();
    dlg.Filter = "テキスト(*.txt)|*.txt";
    dlg.DefaultExt = ".txt";
    var dlgResult = dlg.ShowDialog();
    if (dlgResult != true)
    {
        return;
    }

    using (var stream = File.OpenWrite(dlg.FileName))
    {
        var textWriter = (TextWriter)new StreamWriter(stream);
        await textWriter.WriteAsync(txtbox.Text);
        textWriter.Close();
    }
}

基本的な書き換え方法は、先程と同じです。FileSavePicker を SaveFileDialog へ書き換えて、StorageFile を System.IO の API へと置き換えています。ここでも、TextWriter クラスに追加された WriteAsync 非同期メソッド呼び出しを使用するようにしています。
残りは、Wrap ボタンのロジックになりますので、MainWindow.xaml.cs の抜粋を示します。

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

        Loaded += (sender, args) =>
        {
            //if (appData.Values.ContainsKey("TextWrapping"))
            //    txtbox.TextWrapping = (TextWrapping)appData.Values["TextWrapping"];
            txtbox.TextWrapping = Properties.Settings.Default.TextWrapping;
            wrapButton.IsChecked = txtbox.TextWrapping == TextWrapping.Wrap;
            wrapButton.Content = (bool)wrapButton.IsChecked ? "Wrap" : "No Wrap";

            //txtbox.Focus(FocusState.Programmatic);
            txtbox.Focus();
            
        };
    }

    ....

    private void OnWrapButtonChecked(object sender, RoutedEventArgs e)
    {
        txtbox.TextWrapping = (bool)wrapButton.IsChecked ? TextWrapping.Wrap :
                                                           TextWrapping.NoWrap;
        wrapButton.Content = (bool)wrapButton.IsChecked ? "Wrap" : "No Wrap";
        //appData.Values["TextWrapping"] = (int)txtbox.TextWrapping;
        Properties.Settings.Default.TextWrapping = (bool)wrapButton.IsChecked ? TextWrapping.Wrap : TextWrapping.NoWrap;
        Properties.Settings.Default.Save();

    }
}

残りの変更点は、ApplicationData という WinRT 固有の機能をアプリケーション設定ファイルを使用して読みだして、保存するように変更しています。こうすることで、TextWrpping の設定が保存されて、次に起動した時に読み込まれるようになります。それでは、実行結果を示します。
PrimitivePad

このように適切にコードを書き換えることで、WinRT XAML と同等のことが WPF XAMLでもできることがわかったことでしょう。ちなみにアプリケーション設定ファイルに対して保存した情報は、ユーザー プロファイルに保存して、次回以降の起動ではユーザー プロファイルに保存した情報が読み込まれようになりますので、実行ファイルと同じにフォルダーに保存したアプリケーション設定ファイルを直接書き換えていないことに注意してください。

7.8(P266) 例外処理

本節では、await が catch ブロックで使用できないことから、どのような例外処理が考えられるかを説明しています。ここで説明しているのは、1 つの考え方ですから、必要に応じて応用する必要があります。これらの考え方は、WinRT 固有のものではありませんので、async/await を使用する全てのケースに当てはまります。

7.9(P267) 非同期呼び出しの統合

本節では、ファイルを開くという非同期操作を含む処理を共通化するためにどうしたら良いかという観点で説明をしています。説明の中で、非同期メソッドを設計する方法に説明が及んでいます。ここでは、非同期メソッドを設計する上で考えるべき事項を説明します。最初に、非同期メソッドの戻り値は、次に示す型にする必要があります。

戻り値型説明
Task<T>await 演算子で実行を待機し、戻り値として T を返します。
Taskawait 演算子で実行を待機し、戻り値はありません。
voidawait 演算子は利用できない、非同期メソッドとなります。

非同期メソッドを設計する上で、待機できるようにするためには「Task<T>」か「Task」を戻り値として設計する必要があります。ですが、可能な限り「Task」を返すメソッドの設計を避けるようにしてください。この理由は、async / await を記述するとコンパイラーによって非同期メソッドを待機するための実装コードが自動生成されるのですが、コンパイラーの最適化によっては自分が記述した順序でコードが実行されるとは限らないからです。つまり、コンパイラーの最適化に左右されない非同期メソッドの呼び出しとは、await した次のステップで、戻り値をチェックしてから処理をすることになります。このことを意味する疑似コードを示します。

Task MyMethodAsync(...)
{
  // 何かの処理を行う
  return 戻り値;
}
...
async void MyMethodCall()
{
  var result = await MyMethodAsync(...);
  if (result)
  {
    // ここで継続処理を行う
  }
}

このように await 演算子で待機したメソッドの戻り値を、次のステップの if 文で処理しています。こうすることで、コンパイラーが最適化したとしても、「if (result) 」が必ず MyMethodAsync メソッドが結果を返してから実行されるようになります。このような考え方で、非同期メソッドを作成するのが望ましいと私は考えています。それでは、HowToAsync1 プロジェクト用に作成した MessageDialog クラスの非同期メソッドを再掲します。

// 非同期メソッド
public Task<Object> ShowAsync()
{
    var dispatcherOperation = Dispatcher.InvokeAsync(() =>
    {
        var ret = this.ShowDialog();
        // 戻り値を返す
        return this.color;
    }, System.Windows.Threading.DispatcherPriority.Normal);
    dispatcherOperation.Completed += (s, args) => {
        // 戻る値を返すまで、Objectを延命させる
         };
    return dispatcherOperation.Task;
}

この ShowAsync メソッドは、戻り値が「Task<Object>」になっており待機可能になっています。メソッドの中で行っていることを示します。

  • Dispatcher オブジェクトの InvokeAsync メソッドを呼び出す。
    InvokeAsync は、DispatcherOperartion<Object> 型を返す。
    Window クラスの ShowDialog メソッド(モーダル)を呼び出してから、color フィールドを返す。
  • DispatcherOperation の Completed イベントにハンドラーを登録する。
    この処理は、コメントに記述したように ShowAsync メソッドが戻り値を安全に返すためだけに使用。
  • ShowAsync メソッドの戻り値として、Task<Object> 型を返す。
    await で待機するロジックでは、Object だけが戻ります。

このようにして、非同期メソッドを設計しています。参考までに、ShowAsync メソッドを呼び出している箇所をILDASM による逆アセンブラである IL を使って示します。

.method private hidebysig instance void  OnButtonClick(object sender,
                                                       class [PresentationCore]System.Windows.RoutedEventArgs e) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = ( 01 00 2A 48 6F 77 54 6F 41 73 79 6E 63 33 2E 4D   // ..*HowToAsync3.M
                                                                                                                                     61 69 6E 57 69 6E 64 6F 77 2B 3C 4F 6E 42 75 74   // ainWindow+<OnBut
                                                                                                                                     74 6F 6E 43 6C 69 63 6B 3E 64 5F 5F 30 00 00 )    // tonClick>d__0..
  .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( 01 00 00 00 ) 
  // コード サイズ       64 (0x40)
  .maxstack  2
  .locals init ([0] valuetype HowToAsync3.MainWindow/'<OnButtonClick>d__0' V_0,
           [1] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder V_1)
  IL_0000:  ldloca.s   V_0
  IL_0002:  ldarg.0
  IL_0003:  stfld      class HowToAsync3.MainWindow HowToAsync3.MainWindow/'<OnButtonClick>d__0'::'<>4__this'
  IL_0008:  ldloca.s   V_0
  IL_000a:  ldarg.1
  IL_000b:  stfld      object HowToAsync3.MainWindow/'<OnButtonClick>d__0'::sender
  IL_0010:  ldloca.s   V_0
  IL_0012:  ldarg.2
  IL_0013:  stfld      class [PresentationCore]System.Windows.RoutedEventArgs HowToAsync3.MainWindow/'<OnButtonClick>d__0'::e
  IL_0018:  ldloca.s   V_0
  IL_001a:  call       valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Create()
  IL_001f:  stfld      valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder HowToAsync3.MainWindow/'<OnButtonClick>d__0'::'<>t__builder'
  IL_0024:  ldloca.s   V_0
  IL_0026:  ldc.i4.m1
  IL_0027:  stfld      int32 HowToAsync3.MainWindow/'<OnButtonClick>d__0'::'<>1__state'
  IL_002c:  ldloca.s   V_0
  IL_002e:  ldfld      valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder HowToAsync3.MainWindow/'<OnButtonClick>d__0'::'<>t__builder'
  IL_0033:  stloc.1
  IL_0034:  ldloca.s   V_1
  IL_0036:  ldloca.s   V_0
  IL_0038:  call       instance void [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Start<valuetype HowToAsync3.MainWindow/'<OnButtonClick>d__0'>(!!0&)
  IL_003d:  br.s       IL_003f
  IL_003f:  ret
} // end of method MainWindow::OnButtonClick

簡単に説明するのであれば、<OnButtonClick>d__0' をローカル変数に保存してから AsyncMethodBuilder の Create メソッドを呼び出しています。ILDASM で確認すれば、<OnButtonClick>d__0 構造体が含まれていることがわかります。これらが、コンパイラーによって await 演算子によって生成されたものになります。<OnButtonClick>d__0 の中に、非同期呼び出しと待機後の値を使った処理が含まれています。そして重要なことは、async 演算子を使用したメソッドには非同期メソッドの呼び出し後のコードが含まれていないことです。このように自分で記述したコードを元に、コンパイラーが新しいコードを生成するのが async/await であり、コード生成の過程で最適化も行われるということになります。繰り返しますが、非同期メソッドを設計する場合は、コードの実行順序を制御しやすいように必ず Task<T> 型で設計するようにしましょう。

7.10(P270) ファイル I/O の最適化

本節では、WinRT でファイル I/O を扱う上でどのように扱うのが良いかということを説明しています。WPF XAML というよりも、デスクトップ用の .NET Framework にどのように対応させるのかという観点で説明すると、テキスト ファイルをまとめて読み書きするメソッドである ReadLines や WriteLines メソッドが System.IO.File クラスに .NET Framework 4.0 から追加されていますので、行単位などで処理するよりも効率化できるとも言えます。また、UI との���係で考えるのであれば、.NET Framework 4.5 以降で導入された 非同期ファイル I/Oなどの使用も検討した方がよいでしょう。

7.11(P271) アプリケーションのライフサイクルの問題

本節では、WinRT XAML のアプリ プロセスが影響を受けるプロセス ライフサイクルを説明しています。具体的には、未起動、実行中、一時停止という状態のことです。このようなプロセス状態は、Windows ストア アプリ固有のものになります。よって、デスクトップで動作する WPF XAML などには関係しません。デスクトップで動作するアプリの状態は、今までと同じで実行中、未起動の 2 つだけになります。もちろん、プロセスの正常終了や異常終了の可能性もあります。ですが、Windows ストア アプリでは、アプリ内からアプリのプロセスを終了することは推奨されていませんので、デスクトップ上のアプリと違って終了操作をアプリ側で用意しないことになります。

書籍に記述していることは、Windows ストア アプリ固有の課題に対するものであり、WPF XAML の世界には関係しません。が、ユーザー体験の考え方によっては前回の終了状態を次の起動時に再現した方が望ましい場合もあります。このようなユーザー体験を考える上では、本節に記載されていることも参考になることでしょう。

本節で使用している QuickNotes プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid Background="Black"><TextBox x:Name="txtbox"
             AcceptsReturn="True"
             TextWrapping="Wrap" /></Grid>

この XAML は、組み込みのスタイルシートを除けば WinRT と同じになります。分離コードは、ファイル I/O 系の API が異なることから、書き換えています。それでは、MainWindow.xaml.cs の抜粋を示します。

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

        this.Loaded += MainWindow_Loaded;
        this.Closing += MainWindow_Closing;
    }

    async void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        var folder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
        using (var stream = File.Open(System.IO.Path.Combine(folder, "QuickNotes.txt"), FileMode.OpenOrCreate))
        {
            var textReader = (TextReader)new StreamReader(stream);
            txtbox.Text = await textReader.ReadToEndAsync();
            txtbox.SelectionStart = txtbox.Text.Length;
        }

        //txtbox.Focus(FocusState.Programmatic);
        txtbox.Focus();
    }

    async void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        var folder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
        using (var stream = File.Open(System.IO.Path.Combine(folder, "QuickNotes.txt"), FileMode.OpenOrCreate))
        {
            var textWriter = (TextWriter)new StreamWriter(stream);
            await textWriter.WriteAsync(txtbox.Text);
            textWriter.Close();
        }
    }
}

WinRT XAML と違って一時停止がないので、Window クラスの Closing イベントでデータを保存するようにしています。ユーザー 体験の観点では、タイマーなどで使って自動保存を用意しても良いかもしれません。どのように実装するかは、ユーザーが利用するシーンに合わせて意識することのない自然な方法を考えるのが良いでしょう。

7.12(P276) カスタム非同期メソッド

本節では、この記事ではすでに説明していますが、自分で非同期メソッドを設計する方法を説明しています。この記事では、WPF XAML に書籍の内容を適用する関係で、キャンセル可能な非同期メソッドの設計方法なども説明しました。考え方を書籍は説明していますので、この記事で非同期メソッドの作成方法を理解するのが難しいと感じた方は、書籍を熟読してください。

書籍では、非同期メソッドを作成する例として WordFreq プロジェクトを使用しています。それでは、WordFreq プロジェクトの MainWidnow.xaml.cs より GetWordFrequenciesAsync メソッドを示します。

Task<IOrderedEnumerable<KeyValuePair<string, int>>> GetWordFrequenciesAsync(
    Stream stream,
    CancellationToken cancellationToken,
    IProgress<double> progress)
{
    return Task.Run(async () =>
    {
        Dictionary<string, int> dictionary = new Dictionary<string, int>();

        using (StreamReader streamReader = new StreamReader(stream))
        {
            // Read the first line
            string line = await streamReader.ReadLineAsync();

            while (line != null)
            {
                cancellationToken.ThrowIfCancellationRequested();
                progress.Report(100.0 * stream.Position / stream.Length);

                string[] words = line.Split(' ', ',', '.', ';', ':');

                foreach (string word in words)
                {
                    string charWord = word.ToLower();

                    while (charWord.Length> 0 && !Char.IsLetter(charWord[0]))
                        charWord = charWord.Substring(1);

                    while (charWord.Length > 0 &&
                                    !Char.IsLetter(charWord[charWord.Length - 1]))
                        charWord = charWord.Substring(0, charWord.Length - 1);

                    if (charWord.Length == 0)
                        continue;

                    if (dictionary.ContainsKey(charWord))
                        dictionary[charWord] += 1;
                    else
                        dictionary.Add(charWord, 1);
                }
                line = await streamReader.ReadLineAsync();
            }
        }

        // Return the dictionary sorted by Value (the word count)
        return dictionary.OrderByDescending(i => i.Value);
    }, cancellationToken);
}

このメソッドのコード自体は、WinRT XAML と同じになります。今度は、MainWindow.xaml の抜粋を示します。

<Grid><Grid HorizontalAlignment="Center"><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="*" /></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><Button x:Name="startButton"
                Content="Start"
                Grid.Row="0" Grid.Column="0"
                HorizontalAlignment="Center"
                Margin="24 12"
                Click="OnStartButtonClick" /><Button x:Name="cancelButton"
                Content="Cancel"
                Grid.Row="0" Grid.Column="1"
                IsEnabled="false"
                HorizontalAlignment="Center"
                Margin="24 12"
                Click="OnCancelButtonClick" /><ProgressBar x:Name="progressBar"
                     Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
                     Height="10"
                     Margin="24" /><TextBlock x:Name="errorText"
                   Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"
                   FontSize="24"
                   TextWrapping="Wrap" /><ScrollViewer Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2"><StackPanel x:Name="stackPanel" /></ScrollViewer></Grid></Grid>

XAML 自体も今までと同じで組み込みスタイルを除けば、ProgressBar 要素に Height 属性を設定しただけになります。これは、WinRT XAML の ProgeressBar コントロールのデフォルトの高さとの違いによるものです。そして、MainWindow.xaml.cs の 基本的なコードも同じにすることができます。

public partial class MainWindow : Window
{
    // Project Gutenberg ebook of Herman Melville's "Moby-Dick" 
    Uri uri = new Uri("http://www.gutenberg.org/ebooks/2701.txt.utf-8");
    CancellationTokenSource cts;

    public MainWindow()
    {
        InitializeComponent();
    }

    private async void OnStartButtonClick(object sender, RoutedEventArgs e)
    {
        ...
    }

    private void OnCancelButtonClick(object sender, RoutedEventArgs e)
    {
        cts.Cancel();
    }

    void ProgressCallback(double progress)
    {
        progressBar.Value = progress;
    }

    Task<IOrderedEnumerable<KeyValuePair<string, int>>> GetWordFrequenciesAsync(
        Stream stream,
        CancellationToken cancellationToken,
        IProgress<double> progress)
    {
        ...
    }
}

ここまでのコードは、WinRT XAML のものと同じになります。それでは、OnStartButtonClick のイベント ハンドラーのコードを示します。

private async void OnStartButtonClick(object sender, RoutedEventArgs e)
{
    stackPanel.Children.Clear();
    progressBar.Value = 0;
    errorText.Text = "";
    startButton.IsEnabled = false;
    IOrderedEnumerable<KeyValuePair<string, int>> wordList = null;

    try
    {
        var client = new HttpClient();
        cancelButton.IsEnabled = true;
        cts = new CancellationTokenSource();
        var response = await client.GetAsync(uri, cts.Token);

        using (var stream = await response.Content.ReadAsStreamAsync())
        {
            //cancelButton.IsEnabled = true;
            //cts = new CancellationTokenSource();

            wordList = await GetWordFrequenciesAsync(stream, cts.Token,
                                            new Progress<double>(ProgressCallback));
            cancelButton.IsEnabled = false;

        }

    }
    catch (OperationCanceledException)
    {
        progressBar.Value = 0;
        cancelButton.IsEnabled = false;
        startButton.IsEnabled = true;
        return;
    }
    catch (Exception exc)
    {
        progressBar.Value = 0;
        cancelButton.IsEnabled = false;
        startButton.IsEnabled = true;
        errorText.Text = "Error: " + exc.Message;
        return;
    }

    // Transfer the list of word and counts to the StackPanel
    foreach (KeyValuePair<string, int> word in wordList)
    {
        if (word.Value > 1)
        {
            TextBlock txtblk = new TextBlock
            {
                FontSize = 24,
                Text = word.Key + " \x2014 " + word.Value.ToString()
            };
            stackPanel.Children.Add(txtblk);
        }

        await Task.Yield();
    }
    startButton.IsEnabled = true;
}

WinRT XAMLでは、RandomAccessStreamReference クラスを使用していました。このクラスは、WinRT 固有であることから、System.Net.Http.HttpClient クラスを使用するように変更しています。この変更に伴って、CancellationTokenSource のインスタンスを作成する位置を、HttpClient の GetAsync メソッドの前に移動しています。この理由は、GetAsync メソッドがキャンセル可能になっているためです。後は、HttpResponseMessage クラスより Stream を取り出すことで、以降の処理は WinRT XAML と同じになっています。これで実行をしてみると、WinRT XAML と同じ結果を得ることができますが、1 点だけ異なる挙動を示すものがあります。それが、ProgressBar コントロールになります。実は、ProgressBar コントロールが GetWordFrequenciesAsync メソッドからコールバックで呼び出されているにも関わらず、進捗状況を表すように描画がなされません。この点が、WinRT XAML との大きな違いになります。

この理由は、WinRT XAML の描画メカニズム自体が、非同期メソッドに対する最適化が行われていることに対して、WPF XAML 環境は非同期メソッドとの最適化が行われていないためです。この問題を解決するには、数か所に手を加える必要があります。それでは、OnStartButtonClick と ProgressCallback を示します。

private void OnStartButtonClick(object sender, RoutedEventArgs e)
{
    stackPanel.Children.Clear();
    progressBar.Value = 0;
    errorText.Text = "";
    startButton.IsEnabled = false;

    var startTask = new Task(() => StartTask());
    startTask.Start();
}

void ProgressCallback(double progress)
{
    // Backgroundに変更
    Dispatcher.InvokeAsync(() =>
    {
        progressBar.Value = progress;
    }, System.Windows.Threading.DispatcherPriority.Background);
    //progressBar.Value = progress;
}

OnStartButtonClick では、StartTask メソッド 呼び出しの Task を作成して、Start メソッドで実行するだけにしました(非同期メソッドの呼び出しのみ)。そして、ProgressCallback では、Dispacher オブジェクトを使った更新に変更しています。それでは、追加した Start メソッドを示します。

async void StartTask()
{
    IOrderedEnumerable<KeyValuePair<string, int>> wordList = null;

    try
    {
        var client = new HttpClient();
        await Dispatcher.InvokeAsync(() => 
        {
            cancelButton.IsEnabled = true;
        }, System.Windows.Threading.DispatcherPriority.Background);
        cts = new CancellationTokenSource();
        var response = await client.GetAsync(uri, cts.Token);

        using (var stream = await response.Content.ReadAsStreamAsync())
        {

            wordList = await GetWordFrequenciesAsync(stream, cts.Token,
                                            new Progress<double>(ProgressCallback));
            if (wordList != null || wordList.Count() == 0)
            {
                await Dispatcher.InvokeAsync(() =>
                {
                    cancelButton.IsEnabled = false;
                }, System.Windows.Threading.DispatcherPriority.Background);
            }

        }

    }
    catch (OperationCanceledException)
    {
        var t3 = Dispatcher.InvokeAsync(() =>
            {
                progressBar.Value = 0;
                cancelButton.IsEnabled = false;
                startButton.IsEnabled = true;

            }, System.Windows.Threading.DispatcherPriority.Background);
        return;
    }
    catch (Exception exc)
    {
        var t4 = Dispatcher.InvokeAsync(() =>
        {
            progressBar.Value = 0;
            cancelButton.IsEnabled = false;
            startButton.IsEnabled = true;
            errorText.Text = "Error: " + exc.Message;

        }, System.Windows.Threading.DispatcherPriority.Background);
        return;
    }

    // Transfer the list of word and counts to the StackPanel
    foreach (KeyValuePair<string, int> word in wordList)
    {
        if (word.Value > 1)
        {
            await Dispatcher.InvokeAsync(() =>
                {
                    TextBlock txtblk = new TextBlock
                    {
                        FontSize = 24,
                        Text = word.Key + " \x2014 " + word.Value.ToString()
                    };
                    stackPanel.Children.Add(txtblk);
                }, System.Windows.Threading.DispatcherPriority.Background);
        }
        await Task.Yield();
    }

    if (wordList != null)
    {
        await Dispatcher.InvokeAsync(() =>
        {
            startButton.IsEnabled = true;
        }, System.Windows.Threading.DispatcherPriority.Normal);
    }
}

追加した Start メソッドは、もともとの OnStartButtonClick イベント ハンドラーに記述しているコードとほとんどが同じです。異なるのは、Start メソッド自体が非同期メソッドであり、UI スレッドで実行されないことから、UI コントロールを操作する箇所を Dispacher オブジェクトを使用するように書き換えていることです。このようにすると、OnStartButtonClick イベント ハンドラーによって非同期メソッドが起動されるようになります。そうすると、非同期メソッドが UI スレッドとは異なるスレッド プールで実行を始めますので、Dispacher オブジェクトによって適切に UI コントロールの描画が行われるようになり、ProgressBar コントロールの描画の問題が解決します。それでは、実行結果を示します。
WordFreq

非同期メソッドの使用方法や設計方法などは、WinRT XAML と WPF XAML は同じになります。より正確に説明するのであれば、async/await は .NET Framework 4.5 以降の言語機能であり、WinRT などのランタイム側の機能ではないので同じなのは当たり前なのです。よって、書籍にかかれている内容は、WPF XAML にも通用するものになります。残念なのは、WPF が登場した時に async/await のような機能がなく、非同期プログラミングはプログラマーに任されていたことから、ランタイム レベルでの非同期プログラミングへの最適化が行われていないことです。この意味では、WinRT XAMLは、非同期プログラミングありきで登場しましたから、当然のこととして最適化が行われているのです。

このような違いを知っていれば、WPF XAML であっても async/await を使用することが問題になることはないでしょう。最適なユーザー体験を提供するためには、ユーザーの操作をブロックすることは最悪の手法になりますから、内容によっては積極的に非同期プログラミングを使うことになるでしょう。是非、ユーザーが望むような体験を作り込んでください。

Window のスタイルについて

本章では、非同期プログラミングを書籍に合わせて読み進めるために MessageDialog クラスを作成しました。実は、Windows Forms の Form クラスと WPF の Window クラスでは、設定できるスタイルに違いがあります。

Window クラスでスタイルを設定するには、WindowStyle プロパティに WindowStyle列挙を設定します。具体的な設定例を示します。

デフォルトが、SingleBorderWindow であり、MessageDialog クラスでは ToolWindow を設定していました。一方で、Windows Forms の Form クラスでは、ControlBox、MinimizeBox、MaximizeBox、FormBorderStyle プロパティなどを使用します。この中で、大きな違いは ウィンドウの閉じるボタンを非表示にする手段が、Window クラスにない点になります。これは、WPF XAML は ウィンドウの閉じるボタンを隠すという見せ方を推奨していないことを意味します。ユーザーにとって、Windows OS の操作に慣れていると仮定すると、閉じるボタンがあるという前提で操作をするのが普通のことになります。従って、閉じるボタンを表示しておくのが一般的であるということになります。どうしても、閉じるボタンを非表示にしたい場合だけは、Win32 API を使用することになります。
また、Windows Forms がサポートする MDI ウィンドウに関しては、標準でサポートされていません。従って、MDI を WPF XAML で実現する場合は、自分で作成するか、サードパーティー製のコントロールを使用することを検討してください。

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

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

$
0
0

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

第8章 アプリ バーとポップアップ

本章では、コマンドやメニューを表示するためのアプリ バーとポップアップ(コンテキスト メニューなど)を説明しています。アプリ バーは、Windows ストア アプリで導入された UI 要素で、上端、もしくは下端からのスワイプ操作によって表示できるコマンドを表示するための UX になります。この関係で、デスクトップで動作する WPF アプリケーションでは利用することはできます。WPF XAML でアプリ バーを使いたいとしたら、自分で作るか、サードパーティー製のコントロールを使用することになります。

8.1(P289) コンテキスト メニューの実装

本節では、WinRT XAML におけるコンテキスト メニューの作成方法を説明しています。Windows 8 では、PopupMenu クラスを使用しますが、補足で Windows 8.1 で追加された Flyout クラスと MenuFlyout クラスを使用する方法を説明しています。WPF XAML の場合は、ContextMenuクラスを使用することでコンテキスト メニューを実装することができます。それでは、SimpleContextMenu プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><TextBlock x:Name="textBlock"
               FontSize="24"
               HorizontalAlignment="Center"
               VerticalAlignment="Center"
               TextAlignment="Center"
               MouseLeftButtonDown="textBlock_MouseLeftButtonDown"><TextBlock.ContextMenu><ContextMenu x:Name="menuFlyout" Placement="Bottom"><MenuItem x:Name="menuLarger" Header="Larger Font" /><MenuItem x:Name="menuSmaller" Header="Smaller Font" /><Separator /><MenuItem x:Name="menuRed" Header="Red" /><MenuItem x:Name="menuGreen" Header="Green"  /><MenuItem x:Name="menuBlue" Header="Blue"  /></ContextMenu></TextBlock.ContextMenu>
        Simple Context Menu            <LineBreak /><LineBreak />
        (right-click or press-and-hold-and-release to invoke)</TextBlock></Grid>

WinRT XAML と違うのは、TextBlock 要素に添付プロパティとして ContextMenu 要素を指定している点になります。この意味では、補足に記載されている Flyout クラスを使用する方法と同じになります。また、WinRT XAML では、RightTapped イベント ハンドラーを使用していますが、WPF XAML では MouseLeftButtonDown イベント ハンドラーにしています。これは、意図的にマウスの左ボタン イベントにしています。マウスの右クリックに反応させるのであれば、MouseRightButtonDown イベント ハンドラーを使用します。それでは、コンテキスト メニューを開くコードとして MainWindow.xaml.cs の抜粋を示します。

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

        SettingMenuItemCommand();   // メニューコマンドの初期設定
    }
    private void SettingMenuItemCommand()
    {
        // フォント サイズ変更メニューのコマンド設定
        var sizeChangeCommand = new RelayCommand(arg =>
        {
            var size = (double)arg;
            textBlock.FontSize *= size;
        });
        menuLarger.Command = sizeChangeCommand;
        menuLarger.CommandParameter = 1.2;
        menuSmaller.Command = sizeChangeCommand;
        menuSmaller.CommandParameter = 1 / 1.2;
        // 色変更メニューのコマンド設定
        var colorChangedCommand = new RelayCommand(arg =>
        {
            var color = (Color)arg;
            textBlock.Foreground = new SolidColorBrush(color);
        });
        menuRed.Command = colorChangedCommand;
        menuRed.CommandParameter = Colors.Red;
        menuGreen.Command = colorChangedCommand;
        menuGreen.CommandParameter = Colors.Green;
        menuBlue.Command = colorChangedCommand;
        menuBlue.CommandParameter = Colors.Blue;
    }
    private void textBlock_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        menuFlyout.Placement = System.Windows.Controls.Primitives.PlacementMode.MousePoint;
        menuFlyout.IsOpen = true;
    }
}

class RelayCommand : ICommand
{
    private readonly Action<object> _execute;
    private readonly Func<bool> _canExecute;

    internal RelayCommand(Action<object> execute)
        : this(execute, null)
    {
    }

    internal RelayCommand(Action<object> execute, Func<bool> canExecute)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute();
    }

    public event EventHandler CanExecuteChanged;
    public void RaiseCanExecuteChanged()
    {
        var handler = CanExecuteChanged;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }
}

コンテキスト メニューにコマンドを設定するために、RelayCommand クラスを定義して、実行されるコマンドを定義してから設定しています。WinRT XAML では、OnFontSizeChanged、OnColorChanged メソッドを定義して UICommand のインスタンスを利用しています。UICommand クラスが WinRT XAML 固有のために、RelayCommand クラスを使用することで書き換えています。そして、ContextMenu を開くには IsOpen プロパティを設定します。ここで説明しないといけないのは、Windows が マウスの右クリックによってコンテキスト メニューを表示する共通操作を持っていることです。この機能によって、タッチ操作ではロングタッチによってマウスの右クリックと同じ操作が可能になります。つまり、ContextMenu クラスとは何のイベントを処理しなくても、マウスの右クリックに反応して表示がされるのです。このために、意図的に MouseLeftButtonDown イベントにして、ContextMenu をコードによって表示する例として示しています。それでは、実行結果を示します。
SimpleContextMenu

WPF XAML において ContextMenu クラスは、コンテキスト メニューで処理するコマンドと、どの要素に対して表示するかという点を設定することが中心になります。もちろん、書籍に記述されている内容なども考慮する必要があります。重要なことは、タッチ対応で考るのであればコンテキスト メニューには必ず ContextMenu クラスを使用するということです。次節で説明する Popup クラスを使用するよりも、表示が容易だからです。

8.2(P293) ポップアップ ダイアログ

本節では、Popup クラスの使い方を解説しています。Popup クラスは、WinRT XAML だけでなく、WPF XAML でもサポートされています。それでは、Popup クラスを使用してダイアログを表示する SimpleContextDialog プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><TextBlock x:Name="textBlock"
               FontSize="24"
               HorizontalAlignment="Center"
               VerticalAlignment="Center"
               TextAlignment="Center"
               MouseRightButtonDown="textBlock_MouseRightButtonDown"
               TouchDown="textBlock_TouchDown">
        Simple Context Dialog<LineBreak /><LineBreak />
        (right-click or press-hold-and-release to invoke</TextBlock></Grid>

XAML 自体は、ここまでに説明してきたものと同じになります(組み込みスタイルのみを変更)。ここでは、MouseRightButtonDown イベントと TouchDown イベントを処理するようにしています。理由は、既に説明していますので理解できることでしょう。それでは、MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
    private void textBlock_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
    {
        ShowPopup();
    }
    private void textBlock_TouchDown(object sender, TouchEventArgs e)
    {
        ShowPopup();
    }

    private void ShowPopup()
    {
        StackPanel stackPanel = new StackPanel();
        // Create two Button controls and add to StackPanel
        Button btn1 = new Button
        {
            Content = "Larger font",
            Tag = 1.2,
            HorizontalAlignment = HorizontalAlignment.Center,
            Margin = new Thickness(12)
        };
        btn1.Click += OnButtonClick;
        stackPanel.Children.Add(btn1);
        Button btn2 = new Button
        {
            Content = "Smaller font",
            Tag = 1 / 1.2,
            HorizontalAlignment = HorizontalAlignment.Center,
            Margin = new Thickness(12)
        };
        btn2.Click += OnButtonClick;
        stackPanel.Children.Add(btn2);
        // Create three RadioButton controls and add to StackPanel
        string[] names = { "Red", "Green", "Blue" };
        Color[] colors = { Colors.Red, Colors.Green, Colors.Blue };

        for (int i = 0; i < names.Length; i++)
        {
            RadioButton radioButton = new RadioButton
            {
                Content = names[i],
                Foreground = new SolidColorBrush(colors[i]),
                IsChecked = (textBlock.Foreground as SolidColorBrush).Color == colors[i],
                Margin = new Thickness(12)
            };
            radioButton.Checked += OnRadioButtonChecked;
            stackPanel.Children.Add(radioButton);
        }
        // Create a Border for the StackPanel
        Border border = new Border
        {
            Child = stackPanel,
            //Background = this.Resources["ApplicationPageBackgroundThemeBrush"] as SolidColorBrush,
            BorderBrush = new SolidColorBrush(Colors.Black),
            BorderThickness = new Thickness(1),
            Padding = new Thickness(24),
        };
        // Create the Popup object
        Popup popup = new Popup
        {
            Child = border,
            StaysOpen = false
            //, IsLightDismissEnabled = true
        };

        popup.PlacementTarget = textBlock;
        popup.Placement = PlacementMode.Center;
        // Adjust location based on content size
        border.Loaded += (loadedSender, loadedArgs) =>
        {
            btn1.Focus();
        };
        // Open the popup
        popup.IsOpen = true;
    }

    void OnButtonClick(object sender, RoutedEventArgs args)
    {
        textBlock.FontSize *= (double)(sender as Button).Tag;
    }
    void OnRadioButtonChecked(object sender, RoutedEventArgs args)
    {
        textBlock.Foreground = (sender as RadioButton).Foreground;
    }
}

WinRT XAML では、OnTextBlockRightTapped イベント ハンドラーに記述していました。WPF XAML では、MouseRightButtonDown と TouchDown イベント ハンドラーから呼び出す ShowPopup メソッドに記述しています。このメソッドに記述している内容を示します。

  • StackPanel を用意し、2 つの Button を追加
  • StatckPanel に 3 つの RadioButton を追加
  • Border を作成し、Child プロパティへ StatckPanel を設定
  • Popup を作成し、Child プロパティへ Border を設定
    WinRT XAML と違って、IsLightDismissEnabled プロパティはなく、StayOpen プロパティを使用します。
  • Popup クラスの表示位置の設定
    WinRT XAML と違うのは、PlacementTarget と Placement プロパティで行っている点になります。
  • Popup クラスの Loaded イベント ハンドラーの設定
    WinRT XAML と違って Focus メソッドに引数はありません。、

それでは、実行結果を示します。
SimpleContextDailog
自分でサンプルを動かして見ると理解できますが、Popup クラスの StayOpen プロパティを True に設定することで、マウスの右クリックで表示してから Popup へマウス カーソルを移動させないと Popup が閉じてしまいます。これが、StayOpen プロパティの挙動であり、タッチ対応としては難しい挙動になるということになります。この挙動の理由は、Popup クラスのコンテンツとして Border クラスを設定していることが理由になります。従って、Border クラスなどを使ってコードでコンテンツを作成するよりも、UserControl などを用意した方が良いということになります(8.8 XAML Cruncher では、UserControl を使用しています)。

8.3(P297) アプリ バー

本節から、Windows ストア アプリ固有の機能であるアプリ バーを説明しています。WPF XAML では提供されていないコントロールになりますので、利用するには自分で作成するか、サードパーティー製のコントロールを使用することになります。
ch08 appbar

画像の左側中央にあるボタンは、私が ControlTemplate(第 11 章で説明予定)をカスタマイズして作成したボタンになります。ボタンとして完成している訳ではありませんが、表示スタイルとしてカスタマイズが可能であることが理解できます。下側に表示しているアプリバーは、Developer Express社が販売しているコントロールになります。アプリ バーを WPF XAML で使用したい場合は、このように方法を考える必要があります。 もしくは、一般的な Windows アプリとしてメニューを活用することも考えられます。
本節では、UnconventinalAppBar プロジェクトを使用して、アプリ バーの基本的な使い方を説明しています。

8.4(P300) アプリ バーのボタンのスタイル

本節では、Windows 8 用のストア アプリ プロジェクトで使用できるアプリ バー用のボタン スタイルを LookAtAppBarButtonStyles プロジェクトを使って説明しています。Windows 8.1 では、AppBarButton クラスなどが追加されており、使い方が Windows 8 よりも簡単になっています。が、ボタン スタイルのバリエーションを学習するのに役立ちます。

8.5(P306) Segoe UI Symbol フォントの詳細

本節では、アプリ バーのボタンがどのように実現されているかを説明しています。アプリ バーのボタンに表示されている記号は、Segoe UI Symbol フォントに基づいており、このことを確認するために SegoeSymbols プロジェクトを使って説明しています。それでは、SegoeSymbols プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ...
        xmlns:local="clr-namespace:SegoeSymbols"
        ... ><Window.Resources><local:DoubleToStringHexByteConverter x:Key="hexByteConverter" /></Window.Resources><Grid><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="*" /><RowDefinition Height="Auto" /></Grid.RowDefinitions><!-- Styleを変更 --><TextBlock Name="titleText"
                   Grid.Row="0"
                   Text="Segoe UI Symbol"
                   HorizontalAlignment="Center"
                   FontSize="24"
                    /><Grid Name="characterGrid"
              Grid.Row="1"
              HorizontalAlignment="Center"
              VerticalAlignment="Center" /><Slider Grid.Row="2"
                Orientation="Horizontal"
                Margin="24 0"
                Minimum="0"
                Maximum="511"
                SmallChange="1"
                LargeChange="16"
                AutoToolTipPlacement="BottomRight"
                ValueChanged="OnSliderValueChanged" /></Grid></Window>

XAML 自体は、これまでに説明してきた内容を変更しているだけになります(組み込みスタイル、ThumbToolTipValueConverter など)。それでは、DoubleToStringHexByteConveter.cs を示します。

using System;
using System.Windows.Data;

namespace SegoeSymbols
{
    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;
        }
    }
}

コンバーターも説明してきたように、第 4 パラメータの型が異なるだけとなります。それでは、MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    const int CellSize = 36;
    const int LineLength = (CellSize + 1) * 16 + 18;
    FontFamily symbolFont = new FontFamily("Segoe UI Symbol");
    TextBlock[] txtblkColumnHeads = new TextBlock[16];
    TextBlock[,] txtblkCharacters = new TextBlock[16, 16];

    public MainWindow()
    {
        InitializeComponent();

        for (int row = 0; row < 34; row++)
        {
            RowDefinition rowdef = new RowDefinition();
            if (row == 0 || row % 2 == 1)
                rowdef.Height = GridLength.Auto;
            else
                rowdef.Height = new GridLength(CellSize, GridUnitType.Pixel);

            characterGrid.RowDefinitions.Add(rowdef);
            if (row != 0 && row % 2 == 0)
            {
                TextBlock txtblk = new TextBlock
                {
                    Text = (row / 2 - 1).ToString("X1"),
                    VerticalAlignment = VerticalAlignment.Center
                };
                Grid.SetRow(txtblk, row);
                Grid.SetColumn(txtblk, 0);
                characterGrid.Children.Add(txtblk);
            }

            if (row % 2 == 1)
            {
                Rectangle rectangle = new Rectangle
                {
                    Stroke = this.Foreground,
                    StrokeThickness = row == 1 || row == 33 ? 1.5 : 0.5,
                    Height = 1
                };
                Grid.SetRow(rectangle, row);
                Grid.SetColumn(rectangle, 0);
                Grid.SetColumnSpan(rectangle, 34);
                characterGrid.Children.Add(rectangle);
            }
        }

        for (int col = 0; col < 34; col++)
        {
            ColumnDefinition coldef = new ColumnDefinition();
            if (col == 0 || col % 2 == 1)
                coldef.Width = GridLength.Auto;
            else
                coldef.Width = new GridLength(CellSize);

            characterGrid.ColumnDefinitions.Add(coldef);
            if (col != 0 && col % 2 == 0)
            {
                TextBlock txtblk = new TextBlock
                {
                    Text = "00" + (col / 2 - 1).ToString("X1") + "_",
                    HorizontalAlignment = HorizontalAlignment.Center
                };
                Grid.SetRow(txtblk, 0);
                Grid.SetColumn(txtblk, col);
                characterGrid.Children.Add(txtblk);
                txtblkColumnHeads[col / 2 - 1] = txtblk;
            }

            if (col % 2 == 1)
            {
                Rectangle rectangle = new Rectangle
                {
                    Stroke = this.Foreground,
                    StrokeThickness = col == 1 || col == 33 ? 1.5 : 0.5,
                    Width = 1
                };
                Grid.SetRow(rectangle, 0);
                Grid.SetColumn(rectangle, col);
                Grid.SetRowSpan(rectangle, 34);
                characterGrid.Children.Add(rectangle);
            }
        }

        for (int col = 0; col < 16; col++)
            for (int row = 0; row < 16; row++)
            {
                TextBlock txtblk = new TextBlock
                {
                    Text = ((char)(16 * col + row)).ToString(),
                    FontFamily = symbolFont,
                    FontSize = 24,
                    HorizontalAlignment = HorizontalAlignment.Center,
                    VerticalAlignment = VerticalAlignment.Center
                };
                Grid.SetRow(txtblk, 2 * row + 2);
                Grid.SetColumn(txtblk, 2 * col + 2);
                characterGrid.Children.Add(txtblk);
                txtblkCharacters[col, row] = txtblk;
            }
    }

    private void OnSliderValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
        int baseCode = 256 * (int)e.NewValue;
        for (int col = 0; col < 16; col++)
        {
            txtblkColumnHeads[col].Text = (baseCode / 16 + col).ToString("X3") + "_";

            for (int row = 0; row < 16; row++)
            {
                int code = baseCode + 16 * col + row;
                string strChar = null;
                if (code <= 0x0FFFF)
                {
                    strChar = ((char)code).ToString();
                }
                else
                {
                    code -= 0x10000;
                    int lead = 0xD800 + code / 1024;
                    int trail = 0xDC00 + code % 1024;
                    strChar = ((char)lead).ToString() + (char)trail;
                }
                txtblkCharacters[col, row].Text = strChar;
            }
        }
    }
}

コードは、OnSliderValueChanged イベント ハンドラーの引数の型が異なる点を除ければ、WinRT XAML と同じになります。それでは、実行結果を示します。
SegoeSymbols

実行結果は、WinRT XAML と同じになっていることがわかります。このサンプルを WPF XAML として示したのは、Segoe UI Symbol フォントの存在を知っていれば、利用する機会があると考えたからです。

8.6(P313) アプリ バーのチェックボックスとラジオボタン

本節では、アプリ バーのボタンとして、チェックボックスとラジオボタンの使い方を説明しています。これは、第5章 コントロールとのやりとりで説明したボタン コントロールの 1 つのバリエーションでしかありません。Windows ストア アプリを開発する場合は、熟読をお願いします。

8.7(P317) メモ帳のアプリ バー

本節では、メモ帳アプリである AppBarPad プロジェクトを使って、必要となるファイル I/O 機能をアプリ バーに実装することを説明しています。すでに、ファイル I/O を WPF XAML で扱う方法も説明済みですので、興味があれば自分で実装してみてください。

8.8(P324) XamlCruncher

本節では、XAML をエディタで編集して、結果を表示するプログラムである XamlCruncher プロジェクトの作成方法を通じて、ファイル I/O などを含めて説明しています。XamlCruncher は、Applications = Code + Markupという書籍で WPF 用に使われたサンプルで、ソースを公開していませんが機能を追加した Xaml Cruncher 2.0、そして Silverlight 版もあります。これを WinRT へと移植したものになります。ここでは、XamlCruncher をもう一度、WPF XAML で動作するようにします。最初に、実行結果を示します。
XamlCruncher

アプリ バーのボタンをメニューへ移植し、設定用のポップアップなどはそのままの形にしています。そして、左側のエディタと右側の結果領域を分離するために SpliterContainer というユーザー コントロールを使用しています。それでは、XamlCruncher プロジェクトの SpliterContainer.xaml を示します。

<UserControl x:Class="XamlCruncher.SplitContainer"
             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"><Grid><!-- Default Orientation is Horizontal --><Grid.ColumnDefinitions><ColumnDefinition x:Name="coldef1" Width="*" MinWidth="100" /><ColumnDefinition Width="Auto" /><ColumnDefinition x:Name="coldef2" Width="*" MinWidth="100" /></Grid.ColumnDefinitions><!-- Alternative Orientation is Vertical --><Grid.RowDefinitions><RowDefinition x:Name="rowdef1" Height="*" /><RowDefinition Height="Auto" /><RowDefinition x:Name="rowdef2" Height="0" /></Grid.RowDefinitions><Grid Name="grid1"
              Grid.Row="0"
              Grid.Column="0" /><Thumb Name="thumb"
               Grid.Row="0"
               Grid.Column="1" 
               Width="12"
               DragStarted="OnThumbDragStarted"
               DragDelta="OnThumbDragDelta" /><Grid Name="grid2"
              Grid.Row="0"
              Grid.Column="2" /></Grid></UserControl>

このXAML は、WinRT XAML と同じになります。今度は、SplitContainer.xaml.cs の抜粋を示します。

public partial class SplitContainer : UserControl
{
  static SplitContainer()
  {
    Child1Property =
        DependencyProperty.Register("Child1",
            typeof(UIElement), typeof(SplitContainer),
            new PropertyMetadata(null, OnChildChanged));
    Child2Property =
        DependencyProperty.Register("Child2",
            typeof(UIElement), typeof(SplitContainer),
            new PropertyMetadata(null, OnChildChanged));
    OrientationProperty =
        DependencyProperty.Register("Orientation",
            typeof(Orientation), typeof(SplitContainer),
            new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));
    SwapChildrenProperty =
        DependencyProperty.Register("SwapChildren",
            typeof(bool), typeof(SplitContainer),
            new PropertyMetadata(false, OnSwapChildrenChanged));
    MinimumSizeProperty =
        DependencyProperty.Register("MinimumSize",
            typeof(double), typeof(SplitContainer),
            new PropertyMetadata(100.0, OnMinSizeChanged));
  }

  public static DependencyProperty Child1Property { private set; get; }
  public static DependencyProperty Child2Property { private set; get; }
  public static DependencyProperty OrientationProperty { private set; get; }
  public static DependencyProperty SwapChildrenProperty { private set; get; }
  public static DependencyProperty MinimumSizeProperty { private set; get; }

  public SplitContainer()
  {
    InitializeComponent();
  }

  public UIElement Child1
  {
    set { SetValue(Child1Property, value); }
    get { return (UIElement)GetValue(Child1Property); }
  }

  public UIElement Child2
  {
    set { SetValue(Child2Property, value); }
    get { return (UIElement)GetValue(Child2Property); }
  }

  public Orientation Orientation
  {
    set { SetValue(OrientationProperty, value); }
    get { return (Orientation)GetValue(OrientationProperty); }
  }

  public bool SwapChildren
  {
    set { SetValue(SwapChildrenProperty, value); }
    get { return (bool)GetValue(SwapChildrenProperty); }
  }

  public double MinimumSize
  {
    set { SetValue(MinimumSizeProperty, value); }
    get { return (double)GetValue(MinimumSizeProperty); }
  }

  // Property changed handlers
  static void OnChildChanged(DependencyObject obj,
                             DependencyPropertyChangedEventArgs args)
  {
    (obj as SplitContainer).OnChildChanged(args);
  }

  void OnChildChanged(DependencyPropertyChangedEventArgs args)
  {
    Grid targetGrid = (args.Property == Child1Property ^ this.SwapChildren) ? grid1 : grid2;
    targetGrid.Children.Clear();
    if (args.NewValue != null)
      targetGrid.Children.Add(args.NewValue as UIElement);
  }

  static void OnOrientationChanged(DependencyObject obj,
                                   DependencyPropertyChangedEventArgs args)
  {
    (obj as SplitContainer).OnOrientationChanged((Orientation)args.OldValue,
                                                 (Orientation)args.NewValue);
  }

  void OnOrientationChanged(Orientation oldOrientation, Orientation newOrientation)
  {
    // Shouldn't be necessary, but...
    if (newOrientation == oldOrientation)
      return;
    if (newOrientation == Orientation.Horizontal)
    {
      coldef1.Width = rowdef1.Height;
      coldef2.Width = rowdef2.Height;
      coldef1.MinWidth = this.MinimumSize;
      coldef2.MinWidth = this.MinimumSize;
      rowdef1.Height = new GridLength(1, GridUnitType.Star);
      rowdef2.Height = new GridLength(0);
      rowdef1.MinHeight = 0;
      rowdef2.MinHeight = 0;
      thumb.Width = 12;
      thumb.Height = Double.NaN;

      Grid.SetRow(thumb, 0);
      Grid.SetColumn(thumb, 1);
      Grid.SetRow(grid2, 0);
      Grid.SetColumn(grid2, 2);
    }
    else
    {
      rowdef1.Height = coldef1.Width;
      rowdef2.Height = coldef2.Width;
      rowdef1.MinHeight = this.MinimumSize;
      rowdef2.MinHeight = this.MinimumSize;
      coldef1.Width = new GridLength(1, GridUnitType.Star);
      coldef2.Width = new GridLength(0);
      coldef1.MinWidth = 0;
      coldef2.MinWidth = 0;
      thumb.Height = 12;
      thumb.Width = Double.NaN;

      Grid.SetRow(thumb, 1);
      Grid.SetColumn(thumb, 0);
      Grid.SetRow(grid2, 2);
      Grid.SetColumn(grid2, 0);
    }
  }

  static void OnSwapChildrenChanged(DependencyObject obj,
                                    DependencyPropertyChangedEventArgs args)
  {
    (obj as SplitContainer).OnSwapChildrenChanged((bool)args.OldValue,
                                                  (bool)args.NewValue);
  }

  void OnSwapChildrenChanged(bool oldOrientation, bool newOrientation)
  {
    grid1.Children.Clear();
    grid2.Children.Clear();
    grid1.Children.Add(newOrientation ? this.Child2 : this.Child1);
    grid2.Children.Add(newOrientation ? this.Child1 : this.Child2);
  }

  static void OnMinSizeChanged(DependencyObject obj,
                               DependencyPropertyChangedEventArgs args)
  {
    (obj as SplitContainer).OnMinSizeChanged((double)args.OldValue,
                                             (double)args.NewValue);
  }

  void OnMinSizeChanged(double oldValue, double newValue)
  {
    if (this.Orientation == Orientation.Horizontal)
    {
      coldef1.MinWidth = newValue;
      coldef2.MinWidth = newValue;
    }
    else
    {
      rowdef1.MinHeight = newValue;
      rowdef2.MinHeight = newValue;
    }
  }

  // Thumb event handlers
  void OnThumbDragStarted(object sender, DragStartedEventArgs args)
  {
    if (this.Orientation == Orientation.Horizontal)
    {
      coldef1.Width = new GridLength(coldef1.ActualWidth, GridUnitType.Star);
      coldef2.Width = new GridLength(coldef2.ActualWidth, GridUnitType.Star);
    }
    else
    {
      rowdef1.Height = new GridLength(rowdef1.ActualHeight, GridUnitType.Star);
      rowdef2.Height = new GridLength(rowdef2.ActualHeight, GridUnitType.Star);
    }
  }

  void OnThumbDragDelta(object sender, DragDeltaEventArgs args)
  {
    if (this.Orientation == Orientation.Horizontal)
    {
      double newWidth1 = Math.Max(0, coldef1.Width.Value + args.HorizontalChange);
      double newWidth2 = Math.Max(0, coldef2.Width.Value - args.HorizontalChange);
      coldef1.Width = new GridLength(newWidth1, GridUnitType.Star);
      coldef2.Width = new GridLength(newWidth2, GridUnitType.Star);
    }
    else
    {
      double newHeight1 = Math.Max(0, rowdef1.Height.Value + args.VerticalChange);
      double newHeight2 = Math.Max(0, rowdef2.Height.Value - args.VerticalChange);
      rowdef1.Height = new GridLength(newHeight1, GridUnitType.Star);
      rowdef2.Height = new GridLength(newHeight2, GridUnitType.Star);
    }
  }
}

コードも、WinRT XAML と同じになります。コードが同じですから、説明に関しては書籍を参照してください。Thumb コントロールを使うことで、スプリッターをドラッグして移動することができるようになっています。今度は、ルーラーを表示するための RulerContainer.xaml の抜粋を示します。

<UserControl ... ><Grid SizeChanged="OnGridSizeChanged"><Canvas Name="rulerCanvas" /><Grid Name="innerGrid"><Grid Name="gridLinesGrid" /><Border Name="border" /></Grid></Grid></UserControl>

この XAMLも、WinRT XAML と同じになります。今度は、RuleContainer.xaml.cs の抜粋を示します。

public partial class RulerContainer : UserControl
{
    const double RULER_WIDTH = 12;

    static RulerContainer()
    {
        ChildProperty =
            DependencyProperty.Register("Child",
                typeof(UIElement), typeof(RulerContainer),
                new PropertyMetadata(null, OnChildChanged));
        ShowRulerProperty =
            DependencyProperty.Register("ShowRuler",
                typeof(bool), typeof(RulerContainer),
                new PropertyMetadata(false, OnShowRulerChanged));
        ShowGridLinesProperty =
            DependencyProperty.Register("ShowGridLines",
                typeof(bool), typeof(RulerContainer),
                new PropertyMetadata(false, OnShowGridLinesChanged));
    }

    public static DependencyProperty ChildProperty { private set; get; }
    public static DependencyProperty ShowRulerProperty { private set; get; }
    public static DependencyProperty ShowGridLinesProperty { private set; get; }
    public RulerContainer()
    {
        InitializeComponent();
    }

    public UIElement Child
    {
        set { SetValue(ChildProperty, value); }
        get { return (UIElement)GetValue(ChildProperty); }
    }

    public bool ShowRuler
    {
        set { SetValue(ShowRulerProperty, value); }
        get { return (bool)GetValue(ShowRulerProperty); }
    }

    public bool ShowGridLines
    {
        set { SetValue(ShowGridLinesProperty, value); }
        get { return (bool)GetValue(ShowGridLinesProperty); }
    }

    // Property changed handlers
    static void OnChildChanged(DependencyObject obj,
                               DependencyPropertyChangedEventArgs args)
    {
        (obj as RulerContainer).border.Child = (UIElement)args.NewValue;
    }

    static void OnShowRulerChanged(DependencyObject obj,
                                   DependencyPropertyChangedEventArgs args)
    {
        (obj as RulerContainer).RedrawRuler();
    }

    static void OnShowGridLinesChanged(DependencyObject obj,
                                       DependencyPropertyChangedEventArgs args)
    {
        (obj as RulerContainer).RedrawGridLines();
    }

    void OnGridSizeChanged(object sender, SizeChangedEventArgs args)
    {
        RedrawRuler();
        RedrawGridLines();
    }

    void RedrawGridLines()
    {
        gridLinesGrid.Children.Clear();
        if (!this.ShowGridLines)
            return;
        // Vertical grid lines every 1/4"
        for (double x = 24; x < gridLinesGrid.ActualWidth; x += 24)
        {
            Line line = new Line
            {
                X1 = x,
                Y1 = 0,
                X2 = x,
                Y2 = gridLinesGrid.ActualHeight,
                Stroke = this.Foreground,
                StrokeThickness = x % 96 == 0 ? 1 : 0.5
            };
            gridLinesGrid.Children.Add(line);
        }
        // Horizontal grid lines every 1/4"
        for (double y = 24; y < gridLinesGrid.ActualHeight; y += 24)
        {
            Line line = new Line
            {
                X1 = 0,
                Y1 = y,
                X2 = gridLinesGrid.ActualWidth,
                Y2 = y,
                Stroke = this.Foreground,
                StrokeThickness = y % 96 == 0 ? 1 : 0.5
            };
            gridLinesGrid.Children.Add(line);
        }
    }

    void RedrawRuler()
    {
        rulerCanvas.Children.Clear();
        if (!this.ShowRuler)
        {
            innerGrid.Margin = new Thickness();
            return;
        }
        innerGrid.Margin = new Thickness(RULER_WIDTH, RULER_WIDTH, 0, 0);
        // Ruler across the top
        for (double x = 0; x < gridLinesGrid.ActualWidth - RULER_WIDTH; x += 12)
        {
            // Numbers every inch
            if (x > 0 && x % 96 == 0)
            {
                TextBlock txtblk = new TextBlock
                {
                    Text = (x / 96).ToString("F0"),
                    FontSize = RULER_WIDTH - 2
                };
                txtblk.Measure(new Size());
                Canvas.SetLeft(txtblk, RULER_WIDTH + x - txtblk.ActualWidth / 2);
                Canvas.SetTop(txtblk, 0);
                rulerCanvas.Children.Add(txtblk);
            }
            // Tick marks every 1/8"
            else
            {
                Line line = new Line
                {
                    X1 = RULER_WIDTH + x,
                    Y1 = x % 48 == 0 ? 2 : 4,
                    X2 = RULER_WIDTH + x,
                    Y2 = x % 48 == 0 ? RULER_WIDTH - 2 : RULER_WIDTH - 4,
                    Stroke = this.Foreground,
                    StrokeThickness = 1
                };
                rulerCanvas.Children.Add(line);
            }
        }
        // Heavy line underneath the tick marks
        Line topLine = new Line
        {
            X1 = RULER_WIDTH - 1,
            Y1 = RULER_WIDTH - 1,
            X2 = rulerCanvas.ActualWidth,
            Y2 = RULER_WIDTH - 1,
            Stroke = this.Foreground,
            StrokeThickness = 2
        };
        rulerCanvas.Children.Add(topLine);
        // Ruler down the left side
        for (double y = 0; y < gridLinesGrid.ActualHeight - RULER_WIDTH; y += 12)
        {
            // Numbers every inch
            if (y > 0 && y % 96 == 0)
            {
                TextBlock txtblk = new TextBlock
                {
                    Text = (y / 96).ToString("F0"),
                    FontSize = RULER_WIDTH - 2,
                };
                txtblk.Measure(new Size());
                Canvas.SetLeft(txtblk, 2);
                Canvas.SetTop(txtblk, RULER_WIDTH + y - txtblk.ActualHeight / 2);
                rulerCanvas.Children.Add(txtblk);
            }
            // Tick marks every 1/8"
            else
            {
                Line line = new Line
                {
                    X1 = y % 48 == 0 ? 2 : 4,
                    Y1 = RULER_WIDTH + y,
                    X2 = y % 48 == 0 ? RULER_WIDTH - 2 : RULER_WIDTH - 4,
                    Y2 = RULER_WIDTH + y,
                    Stroke = this.Foreground,
                    StrokeThickness = 1
                };
                rulerCanvas.Children.Add(line);
            }
        }
        Line leftLine = new Line
        {
            X1 = RULER_WIDTH - 1,
            Y1 = RULER_WIDTH - 1,
            X2 = RULER_WIDTH - 1,
            Y2 = rulerCanvas.ActualHeight,
            Stroke = this.Foreground,
            StrokeThickness = 2
        };
        rulerCanvas.Children.Add(leftLine);
    }
}

このコードも、WinRT XAMLと同じになります。コードが同じですから、説明に関しては書籍を参照してください。今度は、Tab キーに対応するための TabbableTextBox.cs を示します。

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

namespace XamlCruncher
{
    public class TabbableTextBox : TextBox
    {
        static TabbableTextBox()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(TabbableTextBox), new FrameworkPropertyMetadata(typeof(TextBox)));
            TabSpacesProperty =
                DependencyProperty.Register("TabSpaces",
                    typeof(int), typeof(TabbableTextBox),
                    new PropertyMetadata(4));

        }

        public static DependencyProperty TabSpacesProperty { private set; get; }
        public int TabSpaces
        {
            set { SetValue(TabSpacesProperty, value); }
            get { return (int)GetValue(TabSpacesProperty); }
        }

        public bool IsModified { set; get; }

        protected override void OnKeyDown(KeyEventArgs e)
        {
            this.IsModified = true;
            if (e.Key == Key.Tab)
            {
                int line, col;
                GetPositionFromIndex(this.SelectionStart, out line, out col);
                int insertCount = this.TabSpaces - col % this.TabSpaces;
                this.SelectedText = new string(' ', insertCount);
                this.SelectionStart += insertCount;
                this.SelectionLength = 0;
                e.Handled = true;
                return;
            }
            base.OnKeyDown(e);
        }

        // WPF XAML 用に移植
        public void GetPositionFromIndex(int index, out int line, out int col)
        {
            line = col = 0;
            int iChar = this.SelectionStart;
            int iLine = this.GetLineIndexFromCharacterIndex(iChar);

            // Check for error that may be a bug.
            if (iLine == -1)
            {
                line = col = -1;
                return;
            }
            int iCol = iChar - this.GetCharacterIndexFromLineIndex(iLine);
            if (this.SelectionLength > 0)
            {
                iChar += this.SelectionLength;
                iLine = this.GetLineIndexFromCharacterIndex(iChar);
                iCol = iChar - this.GetCharacterIndexFromLineIndex(iLine);
            }
            line = iLine;
            col = iCol;
        }
    }
}

このコードも、基本は WinRT XAML と同じになります(異なるのは、イベント ハンドラーの引数の型と GetPositionFromIndex メソッドになります)。コードが同じですから、説明に関しては書籍を参照してください。GetPositionFromIndex メソッドを変更した理由は、TextBox クラスがサポートする GetLineIndexFromCharacterIndex メソッドなどにあります。このメソッドなどが、WPF XAML ではサポートされており、行と位置を容易に検出することが可能だからです。一方で WinRT XAML の TextBox クラスは、このメソッドをサポートしていないことから、書籍では文字列の文字コードを調べて行と位置を検出しています。

8.9(P340) アプリケーションの設定とビュー モデル

本節では、XamlCruncher プロジェクトのアプリケーション設定をビュー モデルを使って実装することを説明しています。この関係で、ApplicationData という WinRT XAML 固有の機能を使用しています。ApplicationData などは、WPF XAML はサポートしていませんから、別の実装に置き換える必要があります。それでは、最初にエディタと結果をどのように配置するかを定義する列挙である EditOrientation.cs を示します。

namespace XamlCruncher
{
    public enum EditOrientation
    {
        Left, Top, Right, Bottom
    }
}

このコードも、WinRT XAMLと同じになります。コードが同じですから、説明に関しては書籍を参照してください。それでは、ビュー モデルである AppSettings.cs を示します。

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

namespace XamlCruncher
{
    public class AppSettings : INotifyPropertyChanged
    {
        // Application settings initial values
        EditOrientation editOrientation = EditOrientation.Left;
        Orientation orientation = Orientation.Horizontal;
        bool swapEditAndDisplay = false;
        bool autoParsing = true;
        bool showRuler = false;
        bool showGridLines = false;
        double fontSize = 18;
        int tabSpaces = 4;

        public event PropertyChangedEventHandler PropertyChanged;

        public AppSettings()
        {
            //ApplicationDataContainer appData = ApplicationData.Current.LocalSettings;
            var settings = Properties.Settings.Default;

            this.EditOrientation = (EditOrientation)settings.EditOrientation;
            this.AutoParsing = settings.AutoParsing;
            this.showRuler = settings.ShowRuler;
            this.ShowGridLines = settings.ShowGridLines;
            this.FontSize = settings.FontSize;
            this.TabSpaces = settings.TabSpaces;

        }

        public EditOrientation EditOrientation
        {
            set
            {
                if (SetProperty<EditOrientation>(ref editOrientation, value))
                {
                    switch (editOrientation)
                    {
                        case EditOrientation.Left:
                            this.Orientation = Orientation.Horizontal;
                            this.SwapEditAndDisplay = false;
                            break;

                        case EditOrientation.Top:
                            this.Orientation = Orientation.Vertical;
                            this.SwapEditAndDisplay = false;
                            break;

                        case EditOrientation.Right:
                            this.Orientation = Orientation.Horizontal;
                            this.SwapEditAndDisplay = true;
                            break;

                        case EditOrientation.Bottom:
                            this.Orientation = Orientation.Vertical;
                            this.SwapEditAndDisplay = true;
                            break;
                    }
                }
            }
            get { return editOrientation; }
        }

        public Orientation Orientation
        {
            protected set 
            {
                SetProperty<Orientation>(ref orientation, value);
            }
            get { return orientation; }
        }

        public bool SwapEditAndDisplay
        {
            protected set { SetProperty<bool>(ref swapEditAndDisplay, value); }
            get { return swapEditAndDisplay; }
        }

        public bool AutoParsing
        {
            set { SetProperty<bool>(ref autoParsing, value); }
            get { return autoParsing; }
        }

        public bool ShowRuler
        {
            set { SetProperty<bool>(ref showRuler, value); }
            get { return showRuler; }
        }

        public bool ShowGridLines
        {
            set { SetProperty<bool>(ref showGridLines, value); }
            get { return showGridLines; }
        }

        public double FontSize
        {
            set { SetProperty<double>(ref fontSize, value); }
            get { return fontSize; }
        }

        public int TabSpaces
        {
            set { SetProperty<int>(ref tabSpaces, value); }
            get { return tabSpaces; }
        }

        public void Save()
        {
            var settings = Properties.Settings.Default;
            settings.EditOrientation = (int)this.EditOrientation;
            settings.AutoParsing = this.AutoParsing;
            settings.ShowRuler = this.ShowRuler;
            settings.ShowGridLines = this.ShowGridLines;
            settings.FontSize = this.FontSize;
            settings.TabSpaces = this.TabSpaces;

            settings.Save();
            settings.Reload();
        }

        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 と同じですが、変更した内容を次に示します。

  • ApplicationData を アプリケーション設定ファイルへ変更。 

特にアプリケーション 設定ファイルを使用している点に、注意が必要です。オリジナルの WPF 版に XamlCruncher では XAML ドキュメントを使用しているからです。同じように移植することも可能ですが、ここでは WinRT XAML の ApplicationData クラスとの対比としてアプリケーション設定ファイルを使用しました。コードの説明は、書籍を参照してください。

8.10(P344) XamlCruncher のページ

本節では、XamlCruncher のメイン ページを説明しています。最初に、XamlCruncher プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ...
        xmlns:local="clr-namespace:XamlCruncher"
        ... ><Grid><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="*" /><RowDefinition Height="Auto" /></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions><Menu Grid.Row="0" Grid.ColumnSpan="2" ><MenuItem Header="_File"><MenuItem Header="_Open" Click="OnOpenAppBarButtonClick" /><MenuItem Header="Save _As" Click="OnSaveAsAppBarButtonClick" /><MenuItem Header="_Save" Click="OnSaveAppBarButtonClick" /><Separator/><MenuItem Header="A_dd" Click="OnAddAppBarButtonClick" /></MenuItem><MenuItem Header="_Refresh" Click="OnRefreshAppBarButtonClick" /><MenuItem Header="_Setting" Click="OnSettingsAppBarButtonClick" /></Menu><TextBlock x:Name="filenameText"
                   Grid.Row="1"
                   Grid.Column="0"
                   Grid.ColumnSpan="2"
                   FontSize="18"
                   TextTrimming="WordEllipsis" Margin="0,0,0,10" /><local:SplitContainer x:Name="splitContainer"  
                              Orientation="{Binding Orientation}"
                              SwapChildren="{Binding SwapEditAndDisplay}"
                              MinimumSize="200"
                              Grid.Row="2"
                              Grid.Column="0"
                              Grid.ColumnSpan="2"><local:SplitContainer.Child1 ><local:TabbableTextBox
                                       AcceptsReturn="True"
                                       FontSize="{Binding FontSize}"
                                       TabSpaces="{Binding TabSpaces}"
                                       TextChanged="OnEditBoxTextChanged"
                                       SelectionChanged="OnEditBoxSelectionChanged" /></local:SplitContainer.Child1><local:SplitContainer.Child2><local:RulerContainer
                                      ShowRuler="{Binding ShowRuler}"
                                      ShowGridLines="{Binding ShowGridLines}" /></local:SplitContainer.Child2></local:SplitContainer><TextBlock x:Name="statusText"
                   Text="OK"
                   Grid.Row="3"
                   Grid.Column="0"
                   FontSize="18"
                   TextWrapping="Wrap" /><TextBlock x:Name="lineColText"
                   Grid.Row="3"
                   Grid.Column="1"
                   FontSize="18" /></Grid></Window>

基本的なコードは同じですが、記述方法を変更している箇所があります。

  • filenameText の TextBlock に Margin プロパティを追加。
    実際のレイアウトから判断しました。
  • local:SplitContainer.Child2 要素に記述した local:RulerContainer 要素から、x:Name 属性を削除しました。
    この点も WinRT XAML と異なる点で、x:Name 属性を設定するとコンパイル エラーになります。この問題により、x:Name 属性を削除しました。もっとも、SplitContainer の Child1 と Child2 プロパティは、1つの要素しか設定できませんので、SplitContainer の x:Name 属性へアクセスできますので、問題はありません。この問題は、オリジナルのサンプルにもコード内にコメントが記述されていることから、実行時に同じ問題が発生しています。WinRT XAML と WPF XAML の違いは、コンパイラーがエラーを検出するかという点だけで、実行時の振る舞いとしては同じになります。
  • アップリ バーを Grid 上で 専用の Row を使って、メニュー コントロールで配置。
  • 組み込みのスタイルを変更。

今度は、書籍に合わせて MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    ...
    AppSettings appSettings;
    //StorageFile loadedStorageFile;
    string loadedStorageFile;
    ...
    public MainWindow()
    {
        InitializeComponent();

        // Set brushes
        textBlockBrush = new SolidColorBrush(Colors.Black);
        textBoxBrush = new SolidColorBrush(Colors.Black);
        errorBrush = new SolidColorBrush(Colors.Red);

        // Why aren't these set in the generated C# files?
        editBox = splitContainer.Child1 as TabbableTextBox;
        //editBox = splitContainer.Child1 as CustomControl1;
        resultContainer = splitContainer.Child2 as RulerContainer;

        // Set a fixed-pitch font for the TextBox
        //Language language = new Language(Windows.Globalization.Language.CurrentInputMethodLanguageTag);
        //LanguageFontGroup languageFontGroup = new LanguageFontGroup(language.LanguageTag);
        //LanguageFont languageFont = languageFontGroup.FixedWidthTextFont;
        //editBox.FontFamily = new FontFamily(languageFont.FontFamily);

        Loaded += OnLoaded;

        Closing += MainWindow_Closing;

    }

    async void OnLoaded(object sender, RoutedEventArgs args)
    {
        // Load AppSettings and set to DataContext
        appSettings = new AppSettings();
        this.DataContext = appSettings;
        appSettings.AutoParsing = true;

        // Load any file that may have been saved
        //StorageFolder localFolder = ApplicationData.Current.LocalFolder;
        //StorageFile storageFile = await localFolder.CreateFileAsync("XamlCruncher.xaml",
        //                                        CreationCollisionOption.OpenIfExists);
        var documents = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
        
        using (var stream = File.Open(System.IO.Path.Combine(documents, fileName), FileMode.OpenOrCreate))
        {
            var reader = (TextReader)new StreamReader(stream);
            editBox.Text = await reader.ReadToEndAsync();
        }

        if (editBox.Text.Length == 0)
            SetDefaultXamlFile();

        // Other initialization
        ParseText();
        //editBox.Focus(FocusState.Programmatic);
        editBox.Focus();
        DisplayLineAndColumn();

        //AppDomain.CurrentDomain.UnhandledException += (excSender, excArgs) =>
        //{
            //SetErrorText(excArgs.Message);
            //excArgs.Handled = true;
        //};
    }

    void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        if (appSettings != null)
            appSettings.Save();
    }

    void SetDefaultXamlFile()
    {
        editBox.Text =
            "<StackPanel xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\r\n" +
            "      xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\r\n" +
            "      Orientation=\"Vertical\">\r\n\r\n" +
            "    <TextBlock Text=\"Hello, Windows 8!\"\r\n" +
            "               FontSize=\"48\" />\r\n\r\n" +
            "</StackPanel>";
        //editBox.Text =
        //    "<Page xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\r\n" +
        //    "      xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\">\r\n\r\n" +
        //    "    <TextBlock Text=\"Hello, Windows 8!\"\r\n" +
        //    "               FontSize=\"48\" />\r\n\r\n" +
        //    "</Page>";

        editBox.IsModified = false;
        loadedStorageFile = "";
        filenameText.Text = "";
    }

    ...

    private void OnEditBoxSelectionChanged(object sender, RoutedEventArgs e)
    {
        DisplayLineAndColumn();
    }

    void DisplayLineAndColumn()
    {
        int line, col;
        editBox.GetPositionFromIndex(editBox.SelectionStart, out line, out col);
        lineColText.Text = String.Format("Line {0} Col {1}", line + 1, col + 1);

        if (editBox.SelectionLength > 0)
        {
            editBox.GetPositionFromIndex(editBox.SelectionStart + editBox.SelectionLength - 1,
                                         out line, out col);
            lineColText.Text += String.Format(" - Line {0} Col {1}", line + 1, col + 1);
        }
    }

    ...

}

基本的なコードは同じですが、WPF XAML へ移植するにあたって変更している箇所があります。

  • loadedStorageFile フィールドを文字列型へ変更。
  • コンストラクター
    TextBox の固定幅フォント設定を削除。これは、Language クラスなどが、WinRT 固有であり、WPF XAML と異なることが理由です。
    Suspending は WinRT 固有のため、Closing イベントへ変更しました。
  • OnLoaded イベント ハンドラー
    非同期メソッドより、同期メソッドに変更しました。ファイル I/O を同期で使用しているためです。
    StorageFile を ドキュメント フォルダーに対するアクセスへ変更しました。
    editBox の SetFocus メソッドの引数を削除しました。WPF XAML では、引数を使用しないためです。
    Application.Current.UnhandledException を削除しました。アプリケーション レベルの例外処理を不要と判断したためです。
  • Windows_Closing イベント ハンドラー
    テキスト コンテンツの保存を削除しました。これは、ドキュメント フォルダーを使用することから、無条件に保存するのは良くないと判断したためです。
  • SetDefaultXamlFile メソッド
    非同期メソッドから同期メソッドに変更しました。WinRT XAML でも、非同期にしている意味はあまりないと私は判断しています。
    editBox.Text に設定している XAML 文字列を Page より StackPanel に変更しました。WPF XAML では、Page が Pageナビゲーションを使用する場合のみにしか使用できないためです。

コードの意味自体は同じですので、書籍を参照してください。

8.11(P349) XAML の解析

WPF XAML 版では、XamlReader クラスの Parse メソッドを使用すること以外は、WinRT XAML と同じになります。それでは、MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    ...
    public MainWindow()
    {
        ...
        // Set brushes
        textBlockBrush = new SolidColorBrush(Colors.Black);
        textBoxBrush = new SolidColorBrush(Colors.Black);
        errorBrush = new SolidColorBrush(Colors.Red);
        ...
    }
    ...
    private void OnRefreshAppBarButtonClick(object sender, RoutedEventArgs e)
    {
        ParseText();
    }

    private void OnEditBoxTextChanged(object sender, TextChangedEventArgs e)
    {
        if (appSettings.AutoParsing)
            ParseText();
    }

    void ParseText()
    {
        object result = null;

        try
        {
            //result = XamlReader.Load(editBox.Text);
            result = XamlReader.Parse(editBox.Text);
        }
        catch (Exception exc)
        {
            SetErrorText(exc.Message);
            return;
        }
        if (result == null)
        {
            SetErrorText("Null result");
        }
        else if (!(result is UIElement))
        {
            SetErrorText("Result is " + result.GetType().Name);
        }
        else
        {
            resultContainer.Child = result as UIElement;
            SetOkText();
            return;
        }
    }

    void SetErrorText(string text)
    {
        SetStatusText(text, errorBrush, errorBrush);
    }

    void SetOkText()
    {
        SetStatusText("OK", textBlockBrush, textBoxBrush);
    }

    void SetStatusText(string text, Brush statusBrush, Brush editBrush)
    {
        statusText.Text = text;
        statusText.Foreground = statusBrush;
        editBox.Foreground = editBrush;
    }
}

書籍では、XamlReder.Load メソッド後に例外が発生する場合の説明と、その対処して UnhandledException イベントのイベント ハンドラーの説明を行っています。今回の WPF XAML 版としては、このコードを削除しています。もし、同じような例外が発生する場合は、Dispatcher オブジェクトの UnhandledException イベントを使用して例外を処理することになります。この点は、WinRT XAML との違いになります。

8.12(P351) XAML ファイルの保存とロード

編集した XAML の保存とロードを行う機能を XAML Cruncher は持っています。この機能を WPF XAML へ移植するには、ファイル処理を System.IO 名前空間のクラスへ書き換える必要があります。それでは、MainWindow.xaml.cs の抜粋を示します。

private void OnSaveAppBarButtonClick(object sender, RoutedEventArgs e)
{
    if (!string.IsNullOrEmpty(loadedStorageFile))
    {
        SaveXamlToFile(loadedStorageFile);
    }
    else
    {
        string storageFile = GetFileFromSavePicker();
        if (!string.IsNullOrEmpty(storageFile))
        {
            SaveXamlToFile(storageFile);
        }
    }
}

private void OnSaveAsAppBarButtonClick(object sender, RoutedEventArgs e)
{
    string storageFile = GetFileFromSavePicker();
    if (string.IsNullOrEmpty(storageFile))
        return;
    SaveXamlToFile(storageFile);
}

string GetFileFromSavePicker()
{
    //FileSavePicker picker = new FileSavePicker();
    var picker = new Microsoft.Win32.SaveFileDialog();
    picker.DefaultExt = ".xaml";
    picker.Filter = "XAML(*.xaml)|*.xaml";
    picker.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
    string name = fileName;
    if (!string.IsNullOrEmpty(loadedStorageFile))
        name = System.IO.Path.GetFileName(loadedStorageFile);
    picker.FileName = name;
    var pickerResult = picker.ShowDialog();
    if (pickerResult == true)
        return picker.FileName;
    return "";
}

void SaveXamlToFile(string storageFile)
{
    loadedStorageFile = storageFile;
    string exception = null;
    try
    {
        File.WriteAllText(storageFile, editBox.Text);
    }
    catch (Exception exc)
    {
        exception = exc.Message;
    }
    if (exception != null)
    {
        string message = String.Format("Could not save file {0}: {1}",
                                       System.IO.Path.GetFileName(storageFile), exception);
        MessageBox.Show(message, "XAML Cruncher");
    }
    else
    {
        editBox.IsModified = false;
        filenameText.Text = storageFile;
    }
}

WinRT XAML と違って、メソッド定義から「async」キーワードを取り除いている点に注意してください。これは、FileSavePicker クラスが WinRT XAML 固有であり、SaveFileDialog クラスへ置き換えて、 FileIO クラスを File クラスへ置き換えたことが理由です。今度は、ファイルを開くなどのコードを MainWindow.xaml.cs より抜粋します。

private async void OnAddAppBarButtonClick(object sender, RoutedEventArgs e)
{
    await CheckIfOkToTrashFile(SetDefaultXamlFile);
}

private async void OnOpenAppBarButtonClick(object sender, RoutedEventArgs e)
{
    await CheckIfOkToTrashFile(LoadFileFromOpenPicker);
}

async Task CheckIfOkToTrashFile(Action commandAction)
{
    if (!editBox.IsModified)
    {
        commandAction();
        return;
    }
    string message =
        String.Format("Do you want to save changes to {0}?",
            string.IsNullOrEmpty(loadedStorageFile) ? "(untitled)" : System.IO.Path.GetFileName(loadedStorageFile));
    var buttons = new DialogButton[]
    {
        new DialogButton("Save", "save", true),
        new DialogButton("Don't Save", "dont"),
        new DialogButton("Cancel", "cancel", false, true)
    };

    MessageDialog msgdlg = new MessageDialog(message, "XAML Cruncher", buttons);    object command = await msgdlg.ShowAsync();
    if ((string)command == "cancel" || command == null)
        return;
    if ((string)command == "dont")
    {
        commandAction();
        return;
    }
    if (string.IsNullOrEmpty(loadedStorageFile))
    {
        string storageFile = GetFileFromSavePicker();
        if (string.IsNullOrEmpty(storageFile))
            return;
        loadedStorageFile = storageFile;
    }
    SaveXamlToFile(loadedStorageFile);
    commandAction();
}

void LoadFileFromOpenPicker()
{
    //FileOpenPicker picker = new FileOpenPicker();
    var picker = new Microsoft.Win32.OpenFileDialog();
    picker.Filter = "XAML(*.xaml)|*.xaml";
    picker.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
    string name = fileName;
    if (!string.IsNullOrEmpty(loadedStorageFile))
        name = System.IO.Path.GetFileName(loadedStorageFile);
    picker.FileName = name;
    var pickerResult = picker.ShowDialog();
    if (pickerResult == true)
    {
        string exception = null;
        string file = picker.FileName;
        try
        {
            editBox.Text = File.ReadAllText(file);
        }
        catch (Exception exc)
        {
            exception = exc.Message;
        }
        if (exception != null)
        {
            string message = String.Format("Could not load file {0}: {1}",
                                           System.IO.Path.GetFileName(file), exception);
            MessageBox.Show(message, "XAML Cruncher");
        }
        else
        {
            editBox.IsModified = false;
            loadedStorageFile = file;
            filenameText.Text = file;
        }
    }
}

今度は、FileOpenPicker クラスを OpenFileDialog クラスに書き換えて、FileIO クラスを File クラスに書き換えています。そして、CheckIfOkToTrashFile メソッドでは、引数を Action デリゲートのみして、第7章 非同期性で使用したのと同じで自分で用意した MessageDialog クラスで WinRT の MessageDialog クラスを置き換えています。これらの点を除けば、WinRT XAML と変わりはありません。

8.13(P355) 設定ダイアログ

WPF XAML では、アプリ バーをメニュー コントロールに置き換えました。この意味では、設定もメニューで表現することができます。Xaml Cruncher 2.0は、メニューを使用して設定を行っています。ここでは、WPF XAML の学習を目的としていることから、WinRT XAML と同じでダイアログとして作成します。最初に表示の向きをを指定するための、EditOrientationRadioButton.cs を示します。

using System.Windows.Controls;

namespace XamlCruncher
{
    public class EditOrientationRadioButton : RadioButton
    {
        public EditOrientation EditOrientationTag { set; get; }
    }
}

このコードは、WinRT XAML と同じになります。それでは、SettingsDialog.xaml の抜粋を示します。

<UserControl ...
             xmlns:local="clr-namespace:XamlCruncher"
             ... ><UserControl.Resources><Style x:Key="DialogCaptionTextStyle"
               TargetType="TextBlock"><Setter Property="FontSize" Value="14.67" /><Setter Property="FontWeight" Value="Light" /><Setter Property="Foreground" Value="Black" /><Setter Property="Margin" Value="7 0 0 0" /></Style><local:ShowHideConverter x:Key="showHide" /></UserControl.Resources><Border Background="Black"
            BorderBrush="Black"
            BorderThickness="1"><StackPanel Margin="24" Background="White"><TextBlock Text="XamlCruncher settings"
                       FontSize="24"
                       Background="Black"
                       Foreground="White"
                       Margin="0 0 0 12" /><!-- Auto parsing --><ToggleButton Content="Automatic parsing"
                          Width="150"
                          IsChecked="{Binding AutoParsing, Mode=TwoWay}" /><!--<ToggleSwitch Header="Automatic parsing"
                          IsOn="{Binding AutoParsing, Mode=TwoWay}" />--><!-- Orientation --><TextBlock Text="Orientation"
                       Style='{StaticResource DialogCaptionTextStyle}' /><Grid Name='orientationRadioButtonGrid'
                  Margin='7 0 0 0'><Grid.RowDefinitions><RowDefinition Height='Auto' /><RowDefinition Height='Auto' /></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width='Auto' /><ColumnDefinition Width='Auto' /></Grid.ColumnDefinitions><Grid.Resources><Style TargetType='Border'><Setter Property='BorderBrush' 
                                Value='Black' /><Setter Property='BorderThickness' Value='1' /><Setter Property='Padding' Value='3' /></Style><Style TargetType='TextBlock'><Setter Property='TextAlignment' Value='Center' /><Setter Property='Foreground' Value='Black' /></Style><Style TargetType='local:EditOrientationRadioButton'><Setter Property='Margin' Value='0 6 12 6' /></Style></Grid.Resources><local:EditOrientationRadioButton Grid.Row='0' Grid.Column='0'
                                                  EditOrientationTag='Left'
                             Checked='OnOrientationRadioButtonChecked'><StackPanel Orientation='Horizontal'><Border><TextBlock Text='edit' /></Border><Border><TextBlock Text='display' /></Border></StackPanel></local:EditOrientationRadioButton><local:EditOrientationRadioButton Grid.Row='0' Grid.Column='1'
                                                  EditOrientationTag='Bottom'
                             Checked='OnOrientationRadioButtonChecked'><StackPanel><Border><TextBlock Text='display' /></Border><Border><TextBlock Text='edit' /></Border></StackPanel></local:EditOrientationRadioButton><local:EditOrientationRadioButton Grid.Row='1' Grid.Column='0'
                                                  EditOrientationTag='Top'
                             Checked='OnOrientationRadioButtonChecked'><StackPanel><Border><TextBlock Text='edit' /></Border><Border><TextBlock Text='display' /></Border></StackPanel></local:EditOrientationRadioButton><local:EditOrientationRadioButton Grid.Row='1' Grid.Column='1'
                                                  EditOrientationTag='Right'
                             Checked='OnOrientationRadioButtonChecked'><StackPanel Orientation='Horizontal'><Border><TextBlock Text='display' /></Border><Border><TextBlock Text='edit' /></Border></StackPanel></local:EditOrientationRadioButton></Grid><!-- Ruler --><StackPanel Orientation='Horizontal'><TextBlock Text='Ruler' Width='70'/><ToggleButton x:Name='rulerButton' 
                              Content='{Binding ShowRuler, Converter={StaticResource showHide},FallbackValue=Show}'
                              Width='150'
                              IsChecked='{Binding ShowRuler, Mode=TwoWay}' /></StackPanel><!-- Grid lines --><StackPanel Orientation='Horizontal'><TextBlock Text='Grid lines' Width='70' /><ToggleButton x:Name='gridLinesButton'
                              Content='{Binding ShowGridLines, Converter={StaticResource showHide},FallbackValue=Show}'
                              Width='150'
                              IsChecked='{Binding ShowGridLines, Mode=TwoWay}' /></StackPanel><!-- Font size --><TextBlock Text='Font size'
                        /><Slider Value='{Binding FontSize, Mode=TwoWay}'
                    AutoToolTipPlacement='TopLeft'
                    Minimum='10'
                    Maximum='48'
                    Margin='7 0 0 0' /><!-- Tab spaces --><TextBlock Text='Tab spaces'
                        /><Slider Value='{Binding TabSpaces, Mode=TwoWay}'
                    AutoToolTipPlacement='TopLeft'
                    Minimum='1'
                    Maximum='12'
                    Margin='7 0 0 0' /><Button Content='Close' Width='60'
                    Margin='0,10,0,10'
                    HorizontalAlignment='Center'
                    Click='close_Click'/></StackPanel></Border></UserControl>

組み込みスタイルや ToggleSwitch を除けば、WinRT XAML と同じになります。ToggleSwitch を ToggleButton へ変更し、ShowHideConverter コンバーターを用意することで、表示文字列を切り替えるようにしています。また、閉じる用のボタンを追加しています。そして、SettingsDialog.xaml で重要なことは、データ バインディングを活用している点になります。それでは、SettingsDialog.xaml.cs を示します。

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

namespace XamlCruncher
{
    public partial class SettingsDialog : UserControl
    {
        public SettingsDialog()
        {
            InitializeComponent();

            this.Loaded += SettingsDialog_Loaded;
        }

        void SettingsDialog_Loaded(object sender, RoutedEventArgs e)
        {
            AppSettings appSettings = DataContext as AppSettings;

            if (appSettings != null)
            {
                foreach (UIElement child in orientationRadioButtonGrid.Children)
                {
                    EditOrientationRadioButton radioButton = child as EditOrientationRadioButton;
                    radioButton.IsChecked =
                        appSettings.EditOrientation == radioButton.EditOrientationTag;
                }
            }
        }

        private void OnOrientationRadioButtonChecked(object sender, RoutedEventArgs e)
        {
            AppSettings appSettings = DataContext as AppSettings;
            EditOrientationRadioButton radioButton = sender as EditOrientationRadioButton;
            if (appSettings != null)
            {
                appSettings.EditOrientation = radioButton.EditOrientationTag;
            }
        }

        private void close_Click(object sender, RoutedEventArgs e)
        {
            var popup = this.Parent as Popup;
            popup.IsOpen = false;
        }
    }
}

WinRT XAML と基本的なコードも同じになります。Load イベントで、DataConmtext に AppSettings クラスをインスタンスを設定することで、XAML に記述したデータ バインドが有効になります。今度は、設定ダイアログを表示するコードを、MainWindow.xaml.cs より抜粋して示します。

public partial class MainWindow : Window
{
    ...
    private void OnSettingsAppBarButtonClick(object sender, RoutedEventArgs e)
    {
        SettingsDialog settingsDialog = new SettingsDialog();
        settingsDialog.DataContext = appSettings;

        var popup = new Popup();
        popup.Child = settingsDialog;
        popup.PlacementTarget = (UIElement)sender;
        popup.StaysOpen = false;

        popup.IsOpen = true;
    }
    ...
}

WinRT XAML と比較すると、Flyout クラスに近い記述になっています。なぜなら、PlacementTarget プロパティによって表示位置を制御しているからです。コードの詳細は説明していませんので、掲載したコードを自分で理解するか、書籍を熟読してください。

8.14(P361) WinRT XAML を超えて

本節では、WinRT 上で XAML Cruncher などが抱える問題点を解説しています。特に追加のアセンブリを読み込むことなどが、WinRT XAML では制限事項として存在しています。一方で、WPF XAML は デスクトップ用の .NET Framework の世界ですから、アセンブリを動的に追加したりすることもできます(Xaml Cruncher 2.0は、アセンブリの読み込みができます)。そこで、読み込み済みのアセンブリを使った、カスタム コントロールの表示方法を提示しています。
ch08 XamlCruncher

このような方法を書籍では説明していますので、WPF XAML にも応用できますので、書籍を参照してください。第8章で利用した手法は、第1章から第7章までに解説してきたものばかりです。WinRT XAML にしか存在しないコントロールを同等機能のコントロールに置き換えるか、サードパーティー製のコントロールを使用するか、独自のカスタム コントロールを作成するかということになります。後は、ファイル I/O などの大きな違いを書き換えるだけで、WPF XAML でも動作させることができるのです。

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

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

$
0
0

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

第9章 アニメーション

本章では、アニメーションを解説しています。ここまでにも、何回かアニメーションがサンプルコードにも含まれていました。WPF XAML と WinRT XAMLでは、アニメーションの基本的な考え方は同じになります。しかし、大きな違いもあります。それは、WPF XAML のアニメーションは型に厳しく(専用のタイムラインの使用が強制される)、WinRT XAML のアニメーションは型にたいして緩い(別の表現では柔軟とも言えます)という特徴があります。これは、Silverlight に代表される XAML 系の UI フレームワークに共通する特徴にもなっています。この理由は、フレームワークが提供するランタイムをコンパクトにまとめていることと関係があります。一方で、WPF XAML のようなフルスタックのフレームワークは、型に対する厳密性が強制されることになります。また、アニメーションは、XAML 系の UI 技術に共通するもので、Windows Forms には含まれない技術になります。このため、ユーザー エクスぺリエンス(UX) の観点からは、利用者であるユーザーが操作方法に気付くために使用したり、タッチ操作に対する反応として利用したりします。ユーザーの操作体験を当たり前のように感じさせるための手段として、アニメーションの利用を考える必要があると私は考えます。

9.1(P363) Windows.UI.Xaml.Media.Animation 名前空間

本節では、CompositionTarget.Rendering イベントを題材に WinRT に組み込まれているアニメーションの概要を説明しています。WPF XAML では、System.Windows.Media.Animation名前空間で組み込みのアニメーションが定義されています。考え方は同じですが、WinRT XAML と WPF XAML で異なるのは、組み込みのアニメーション ライブラリが WPF XAML には用意されていない点になります。

9.2(P364) アニメーションの基礎

本節では、アニメーションの基礎を説明するために DoubleAnimation というタイムライン を用いて説明しています。最初に、SimpleAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Storyboard x:Key="storyboard"><DoubleAnimation Storyboard.TargetName="txtblk"
                             Storyboard.TargetProperty="FontSize"
                             From="1" To="144" Duration="0:0:3" /></Storyboard></Window.Resources><Grid><Grid.RowDefinitions><RowDefinition Height="*" /><RowDefinition Height="*" /></Grid.RowDefinitions><TextBlock x:Name="txtblk"
                   Text="Animated Text"
                   Grid.Row="0"
                   FontSize="48"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center" /><Button Content="Trigger!"
                Grid.Row="1"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Click="OnButtonClick" /></Grid></Window>

基本的なコードは、WinRT XAML と同じですが、違いは DoubleAnimation から EnbaleDependentAnimation プロパティを削除していることと組み込みスタイルを変更した点になります。このプロパティは、WinRT 固有のものであり、アニメーションを UI スレッドを使用するかどうかを指定するものになります。一方で、WPF XAML のアニメーションは UI スレッドで実行されるという特徴があります。それでは、このアニメーションを起動する MainWindow.xaml.cs を示します。

using System.Windows;
using System.Windows.Media.Animation;

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

        private void OnButtonClick(object sender, RoutedEventArgs e)
        {
            (this.Resources["storyboard"] as Storyboard).Begin();

        }
    }
}

リソースから DoubleAnimation を取得して、Begin メソッドでアニメーションを実行します。もちろん、コードは名前空間を除けば WinRT XAML と同じになります。
SimpleAnimation

DoubleAnimation クラスとは、オブジェクトの Double 型のプロパティを From から To に対して指定した時間(Duration) で変化させるためのタイムラインになります。 書籍では、AutoReverse、RepeatBehavior などの様々な設定によって、アニメーションがどのように変化するかを解説しています。今度は、SimpleAnimation プロジェクトと同等のアニメーションをコードで実現する SimpleAnimationCode プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Style TargetType="Button"><Setter Property="Content" Value="Trigger!" /><Setter Property="FontSize" Value="48" /><Setter Property="HorizontalAlignment" Value="Center" /><Setter Property="VerticalAlignment" Value="Center" /><Setter Property="Margin" Value="12" /></Style></Window.Resources><Grid><Grid HorizontalAlignment="Center"
              VerticalAlignment="Center"><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="Auto" /><ColumnDefinition Width="Auto" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions><Button Grid.Row="0" Grid.Column="0" Click="OnButtonClick" /><Button Grid.Row="0" Grid.Column="1" Click="OnButtonClick" /><Button Grid.Row="0" Grid.Column="2" Click="OnButtonClick" /><Button Grid.Row="1" Grid.Column="0" Click="OnButtonClick" /><Button Grid.Row="1" Grid.Column="1" Click="OnButtonClick" /><Button Grid.Row="1" Grid.Column="2" Click="OnButtonClick" /><Button Grid.Row="2" Grid.Column="0" Click="OnButtonClick" /><Button Grid.Row="2" Grid.Column="1" Click="OnButtonClick" /><Button Grid.Row="2" Grid.Column="2" Click="OnButtonClick" /></Grid></Grid></Window>

コードは、組み込みスタイル以外は WinRT XAML と同じになります。アニメーションを実現する OnButtonClick イベント ハンドラーを MainWindow.xaml.cs より抜粋します。

private void OnButtonClick(object sender, RoutedEventArgs e)
{
    DoubleAnimation anima = new DoubleAnimation
    {
        To = 96,
        Duration = new Duration(new TimeSpan(0, 0, 1)),
        AutoReverse = true,
        RepeatBehavior = new RepeatBehavior(3)
    };
    Storyboard.SetTarget(anima, sender as Button);
    Storyboard.SetTargetProperty(anima, new PropertyPath(Button.FontSizeProperty));

    Storyboard storyboard = new Storyboard();
    storyboard.Children.Add(anima);
    storyboard.Begin();

}

コードは、既に説明したように EnableDependentAnimation プロパティを削除しています。また、SetTargetProperty メソッドの第2引数の指定方法が、WinRT XAML が文字列なのに対して、WPF XAML では PropertyPath オブジェクトのインスタンスになる点も異なります。実行結果は、もちろん同じになります。
SimpleAnimationCode

もちろん、書籍にはコードの解説もありますので熟読をお願いします。

9.4(P375) その他の DoubleAnimation

本節では、前節までが FontSize プロパティをアニメーションしていたので、その他の Double  型 を持つプロパティを使ったアニメーションを説明しています。それでは、ElipseBlobAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Storyboard x:Key="storyboard"
                    RepeatBehavior="Forever"
                    AutoReverse="True"><DoubleAnimation Storyboard.TargetName="ellipse"
                             Storyboard.TargetProperty="Width"
                             From="100" To="600" Duration="0:0:1" /><DoubleAnimation Storyboard.TargetName="ellipse"
                             Storyboard.TargetProperty="Height"
                             From="600" To="100" Duration="0:0:1" /></Storyboard></Window.Resources><Grid><Ellipse x:Name="ellipse"><Ellipse.Fill><LinearGradientBrush><GradientStop Offset="0" Color="Pink" /><GradientStop Offset="1" Color="LightBlue" /></LinearGradientBrush></Ellipse.Fill></Ellipse></Grid></Window>

もちろん、EnableDependentAnimation プロパティを除けば WinRT XAML と同じになります。このアニメーションは、AutoReverse プロパティを指定しているので永遠に実行されるようになります。それでは、MainWindow.xaml.cs を示します。

using System.Windows;
using System.Windows.Media.Animation;

namespace EllipseBlobAnimation
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Loaded += (sender, args) =>
            {
                (this.Resources["storyboard"] as Storyboard).Begin();
            };
        }
    }
}

実行結果も同じになります。
EllipseBlobAnimation

今度は、Shape オブジェクトの StrokeThickness プロパティをアニメーション化する AnimateStrokeThickness プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Storyboard x:Key="storyboard"><DoubleAnimation Storyboard.TargetName="ellipse"
                             Storyboard.TargetProperty="StrokeThickness"
                             From="1" To="100" Duration="0:0:4"
                             AutoReverse="True"
                             RepeatBehavior="Forever" /></Storyboard></Window.Resources><Grid><Ellipse x:Name="ellipse"
                 Stroke="Red"
                 StrokeDashCap="Round"
                 StrokeDashArray="0 2" /></Grid></Window>

もちろん、EnableDependentAnimation プロパティ以外は同じになります。このアニメーションは、StrokeDashArray プロパティが設定されていることから点線のようなものになります。
AnimateStrokeThickness

今度は、StrokeDashOffset プロパティを使用する AnimateDashOffset プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Storyboard x:Key="storyboard"><DoubleAnimation Storyboard.TargetName="path"
                             Storyboard.TargetProperty="StrokeDashOffset"
                             From="0" To="1.5" Duration="0:0:1"
                             RepeatBehavior="Forever" /></Storyboard></Window.Resources><Grid><Viewbox><Path x:Name="path"
                  Margin="12"
                  Stroke="Black"
                  StrokeThickness="24"
                  StrokeDashArray="0 1.5"
                  StrokeDashCap="Round"
                  Data="M 100   0
                        C  45   0,   0  45, 0 100
                        S  45 200, 100 200
                        S 200 150, 250 100
                        S 345   0, 400   0
                        S 500  45, 500 100
                        S 455 200, 400 200
                        S 300 150, 250 100
                        S 155   0, 100   0" /></Viewbox></Grid></Window>

このアニメーションも EnableDependentAnimation プロパティを除けば、同じになります。実行結果を示します。
AnimateDashOffset

書籍では、Path の定義などに関する説明がありますので、Path クラスを活用するのであれば熟読をお願いします。今度は、Opacity プロパティを使用する CheshireCat プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Storyboard x:Key="storyboard"><DoubleAnimation Storyboard.TargetName="image2"
                             Storyboard.TargetProperty="Opacity"
                             From="0" To="1" Duration="0:0:2"
                             AutoReverse="True"
                             RepeatBehavior="Forever" /></Storyboard></Window.Resources><Grid><Viewbox><Grid><Image Source="Images/alice23a.gif"
                       Width="640" /><TextBlock FontFamily="Century Schoolbook"
                           FontSize="24"
                           Foreground="Black"
                           TextWrapping="Wrap"
                           TextAlignment="Justify"
                           Width="320"
                           Margin="0 0 24 60"
                           HorizontalAlignment="Right"
                           VerticalAlignment="Bottom">&#x2003;&#x2003;“All right,” said the Cat; and this 
                    time it vanished quite slowly, beginning with the end 
                    of the tail, and ending with the grin, which
                    remained some time after the rest of it had gone.<LineBreak /><LineBreak />&#x2003;&#x2003;“Well! I’ve often seen a cat without a 
                    grin,” thought Alice; “but a grin without a cat! It’s 
                    the most curious thing I ever saw in all my life!”</TextBlock><Image x:Name="image2"
                       Source="Images/alice24a.gif"
                       Stretch="None"
                       VerticalAlignment="Top"><Image.Clip><RectangleGeometry Rect="320 70 320 240" /></Image.Clip></Image></Grid></Viewbox></Grid></Window>

このコードも EnableDpendentAnimation プロパティを除けば、同じになります。それでは、実行結果を示します。
CheshireCat

猫の画像の Opacity がアニメーションによって変化しますが、スクリーン ショットでは判別しにくいので、ご自分で実行してみてください。

9.5(P382) 添付プロパティのアニメーション

本節では、アニメーションを使ってオブジェクトを移動することを説明しています。このために、Canvas オブジェクトの添付プロパティを使用しています。それでは、AttachedPropertyAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Storyboard x:Key="storyboard"><DoubleAnimation Storyboard.TargetName="ellipse"
                             Storyboard.TargetProperty="(Canvas.Left)"
                             From="0" Duration="0:0:2.51"
                             AutoReverse="True"
                             RepeatBehavior="Forever" /><DoubleAnimation Storyboard.TargetName="ellipse"
                             Storyboard.TargetProperty="(Canvas.Top)"
                             From="0" Duration="0:0:1.01"
                             AutoReverse="True"
                             RepeatBehavior="Forever" /></Storyboard></Window.Resources><Grid><Canvas SizeChanged="OnCanvasSizeChanged"
                Margin="0 0 48 48"><Ellipse x:Name="ellipse"
                     Width="48"
                     Height="48"
                     Fill="Red" /></Canvas></Grid></Window>

コードは、組み込みスタイルを除けば同じになります。書籍にも説明がありますが、Storybord の TargetProperty に括弧付の特殊な構文を使用しています。今度は、MainWindow.xaml.cs を示します。

using System;
using System.Windows;
using System.Windows.Media.Animation;

namespace AttachedPropertyAnimation
{
    public partial class MainWindow : Window
    {
        Storyboard storyboard;
        TimeSpan time;

        public MainWindow()
        {
            InitializeComponent();
            Loaded += (sender, args) =>
            {
                (this.Resources["storyboard"] as Storyboard).Begin();
            };
        }

        private void OnCanvasSizeChanged(object sender, SizeChangedEventArgs e)
        {
            bool isTime = false;
            Storyboard storyboard = this.Resources["storyboard"] as Storyboard;
            try
            {
                time = storyboard.GetCurrentTime();
                isTime = true;
            }
            catch { }

            storyboard.Stop();
            // Canvas.Left animation
            DoubleAnimation anima = storyboard.Children[0] as DoubleAnimation;
            anima.To = e.NewSize.Width;
            // Canvas.Top animation
            anima = storyboard.Children[1] as DoubleAnimation;
            anima.To = e.NewSize.Height;
            storyboard.Begin();
            if (isTime)
                storyboard.Seek(time);
        }
    }
}

コードでは、OnCanvasSizeChanged イベント ハンドラを大きく変更しています。変更した内容を次に示します。

  • isTime 変数を追加して、ストリーボードの経過時間を取得する。
  • アニメーションを停止(Stop)してから、To プロパティを設定する。
  • アニメーションを開始(Begin)してから、ストーリーボードの経過時間を進める(Seek)。

WinRT XAML では、ストーリーボードを停止することなく、To プロパティを変更していますが、WPF XAML は同じように実行中に To プロパティを変更することはできません。厳密には、変更してもアニメーションの実行に反映されないのです。このために、アニメーションを停止してから To プロパティを変更してから アニメーションを開始して、経過時間だけアニメーションを進めるという手法を取っています。
AttachedPropertyAnimation

スクリーン ショットではわかりませんが、赤い円がウィンドウに向かって動いて行き、跳ね返るように動いていきます。WinRT XAML では、実行中にウィンドウ サイズを変更することを前提にしていることから、To プロパティを実行中にアニメーションに反映させるという改良が WPF XAML の仕組みに対して行われていると理解すれば良いでしょう。一方で、WPF XAML では ウィンドウ サイズなどが変更された場合は、アニメーションの動きもプログラマが調整する必要があるということになります。これは、単純にアニメーションを開始するまえにアニメーションするプロパティの開始値(From) と 終了値(To) が決まっていないければならないからです。このサンプルで説明したように、WPF XAML では工夫さえすれば、動的にアニメーションで変化させる値を変更することも可能にする手段はあるということになります。無いのであれば、無いなりに工夫しましょうということです。

9.6(P385) イージング関数

本節では、前節までに説明した DubleAnimation タイムラインが直線的なアニメーションを実現するのに対して、曲線的に変化させるイージング関数を説明しています。そして、イージング関数を説明するために AnimationEaseGrapher プロジェクトを使用して、提供される様々なイージング関数を体験できるようにしています。それでは、AnimationEaseGrapher  プロジェクト の MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Storyboard x:Key="storyboard"
                    FillBehavior="Stop"><DoubleAnimation Storyboard.TargetName="redBall"
                             Storyboard.TargetProperty="(Canvas.Left)"
                             From="-6" To="994" Duration="0:0:3" /><DoubleAnimation x:Name="anima2"
                             Storyboard.TargetName="redBall"
                             Storyboard.TargetProperty="(Canvas.Top)"
                             From="-6" To="494" Duration="0:0:3" /></Storyboard></Window.Resources><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="Auto" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><!-- Control panel --><Grid Grid.Column="0"
              VerticalAlignment="Center"><Grid.RowDefinitions><RowDefinition Height="*" /><RowDefinition Height="*" /><RowDefinition Height="*" /></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="Auto" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions><!-- Easing function (populated by code) --><StackPanel x:Name="easingFunctionStackPanel"
                        Grid.Row="0"
                        Grid.RowSpan="3"
                        Grid.Column="0"
                        VerticalAlignment="Center"><RadioButton Content="None"
                             Margin="6"
                             Checked="OnEasingFunctionRadioButtonChecked" /></StackPanel><!-- Easing mode --><StackPanel x:Name="easingModeStackPanel"
                        Grid.Row="0"
                        Grid.Column="1"
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center"><RadioButton Content="Ease In"
                             Margin="6"
                             Checked="OnEasingModeRadioButtonChecked"><RadioButton.Tag><EasingMode>EaseIn</EasingMode></RadioButton.Tag></RadioButton><RadioButton Content="Ease Out"
                            Margin="6"
                            Checked="OnEasingModeRadioButtonChecked"><RadioButton.Tag><EasingMode>EaseOut</EasingMode></RadioButton.Tag></RadioButton><RadioButton Content="Ease In/Out"
                             Margin="6"
                             Checked="OnEasingModeRadioButtonChecked"><RadioButton.Tag><EasingMode>EaseInOut</EasingMode></RadioButton.Tag></RadioButton></StackPanel><!-- Easing properties (populated by code) --><StackPanel x:Name="propertiesStackPanel"
                        Grid.Row="1"
                        Grid.Column="1"
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center" /><!-- Demo button --><Button Grid.Row="2"
                    Grid.Column="1"
                    Content="Demo!"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Click="OnDemoButtonClick" /></Grid><!-- Graph using arbitrary coordinates and scaled to window --><Viewbox Grid.Column="1"><Grid Width="1000"
                  Height="500"
                  Margin="0 250 0 250"><!-- Rectangle outline --><Polygon Points="0 0, 1000 0, 1000 500, 0 500"
                         Stroke="Black"
                         StrokeThickness="3" /><Canvas><!-- Linear transfer --><Polyline Points="0 0, 1000 500"
                              Stroke="Black"
                              StrokeThickness="1"
                              StrokeDashArray="3 3" /><!-- Points set by code based on easing function --><Polyline x:Name="polyline"
                              Stroke="Blue"
                              StrokeThickness="3" /><!-- Animated ball --><Ellipse x:Name="redBall"
                             Width="12"
                             Height="12"
                             Fill="Red" /></Canvas></Grid></Viewbox></Grid></Window>

コード自体は、組み込みスタイルを除けば WinRT XAML と同じになります。分離コードで、イージング関数をリフレクションを使って RadioButton として追加をしていますので、MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    EasingFunctionBase easingFunction;

    public MainWindow()
    {
        InitializeComponent();

        Loaded += OnMainPageLoaded;
    }

    void OnMainPageLoaded(object sender, RoutedEventArgs args)
    {
        Type baseType = typeof(EasingFunctionBase);
        TypeInfo baseTypeInfo = baseType.GetTypeInfo();
        Assembly assembly = baseTypeInfo.Assembly;
        // Enumerate through all Windows Runtime types
        foreach (Type type in assembly.ExportedTypes)
        {
            TypeInfo typeInfo = type.GetTypeInfo();
            // Create RadioButton for each easing function
            if (typeInfo.IsPublic &&
                baseTypeInfo.IsAssignableFrom(typeInfo) &&
                type != baseType)
            {
                RadioButton radioButton = new RadioButton
                {
                    Content = type.Name,
                    Tag = type,
                    Margin = new Thickness(6),
                };
                radioButton.Checked += OnEasingFunctionRadioButtonChecked;
                easingFunctionStackPanel.Children.Add(radioButton);
            }
        }
        // Check the first RadioButton in the StackPanel (the one labeled "None")
        (easingFunctionStackPanel.Children[0] as RadioButton).IsChecked = true;
    }

    private void OnEasingFunctionRadioButtonChecked(object sender, RoutedEventArgs e)
    {
        RadioButton radioButton = sender as RadioButton;
        Type type = radioButton.Tag as Type;
        easingFunction = null;
        propertiesStackPanel.Children.Clear();
        // type is only null for "None" button
        if (type != null)
        {
            TypeInfo typeInfo = type.GetTypeInfo();
            // Find a parameterless constructor and instantiate the easing function
            foreach (ConstructorInfo constructorInfo in typeInfo.DeclaredConstructors)
            {
                if (constructorInfo.IsPublic && constructorInfo.GetParameters().Length == 0)
                {
                    easingFunction = constructorInfo.Invoke(null) as EasingFunctionBase;
                    break;
                }
            }
            // Enumerate the easing function properties
            foreach (PropertyInfo property in typeInfo.DeclaredProperties)
            {
                // We can only deal with properties of type int and double
                if (property.PropertyType != typeof(int) &&
                    property.PropertyType != typeof(double))
                {
                    continue;
                }
                // Create a TextBlock for the property name
                TextBlock txtblk = new TextBlock
                {
                    Text = property.Name + ":"
                };
                propertiesStackPanel.Children.Add(txtblk);
                // Create a Slider for the property value
                Slider slider = new Slider
                {
                    Width = 144,
                    Minimum = 0,
                    Maximum = 10,
                    Tag = property
                };
                if (property.PropertyType == typeof(int))
                {
                    //slider.StepFrequency = 1;
                    slider.SmallChange = 1;
                    slider.Value = (int)property.GetValue(easingFunction);
                }
                else
                {
                    //slider.StepFrequency = 0.1;
                    slider.SmallChange = 0.1;
                    slider.Value = (double)property.GetValue(easingFunction);
                }
                // Define the Slider event handler right here
                slider.ValueChanged += (sliderSender, sliderArgs) =>
                {
                    Slider sliderChanging = sliderSender as Slider;
                    PropertyInfo propertyInfo = sliderChanging.Tag as PropertyInfo;
                    if (property.PropertyType == typeof(int))
                        property.SetValue(easingFunction, (int)sliderArgs.NewValue);
                    else
                        property.SetValue(easingFunction, (double)sliderArgs.NewValue);

                    DrawNewGraph();
                };
                propertiesStackPanel.Children.Add(slider);
            }
        }
        // Initialize EasingMode radio buttons
        foreach (UIElement child in easingModeStackPanel.Children)
        {
            RadioButton easingModeRadioButton = child as RadioButton;
            easingModeRadioButton.IsEnabled = easingFunction != null;
            easingModeRadioButton.IsChecked =
                easingFunction != null &&
                easingFunction.EasingMode == (EasingMode)easingModeRadioButton.Tag;
        }
        DrawNewGraph();
    }

    private void OnEasingModeRadioButtonChecked(object sender, RoutedEventArgs e)
    {
        RadioButton radioButton = sender as RadioButton;
        easingFunction.EasingMode = (EasingMode)radioButton.Tag;
        DrawNewGraph();
    }

    private void OnDemoButtonClick(object sender, RoutedEventArgs e)
    {
        // Set the selected easing function and start the animation
        Storyboard storyboard = this.Resources["storyboard"] as Storyboard;
        (storyboard.Children[1] as DoubleAnimation).EasingFunction = easingFunction;
        storyboard.Begin();
    }

    void DrawNewGraph()
    {
        polyline.Points.Clear();
        if (easingFunction == null)
        {
            polyline.Points.Add(new Point(0, 0));
            polyline.Points.Add(new Point(1000, 500));
            return;
        }
        for (decimal t = 0; t <= 1; t += 0.01m)
        {
            double x = (double)(1000 * t);
            double y = 500 * easingFunction.Ease((double)t);
            polyline.Points.Add(new Point(x, y));
        }
    }
}

コードで WinRT XAML と異なるのは、OnEasingFunctionRadioButtonChecked イベント ハンドラー内の Slider オブジェクトに対する StepFrequency プロパティを SmallChange プロパティに変更した点になります。それでは、実行結果を示します。
AnimationEaseGrapher

イージング関数とコードそのものの説明は書籍で行われていますので、書籍を熟読してください。今度は、円周上をぐるぐる回るオブジェクトを実現するために、CircleAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Storyboard x:Key="storyboard" SpeedRatio="3"><DoubleAnimation Storyboard.TargetName="ball"
                             Storyboard.TargetProperty="(Canvas.Left)"
                             From="-350" To="350" Duration="0:0:2"
                             AutoReverse="True"
                             RepeatBehavior="Forever"><DoubleAnimation.EasingFunction><SineEase EasingMode="EaseInOut" /></DoubleAnimation.EasingFunction></DoubleAnimation><DoubleAnimation Storyboard.TargetName="ball"
                             Storyboard.TargetProperty="(Canvas.Top)"
                             BeginTime="0:0:1"
                             From="-350" To="350" Duration="0:0:2"
                             AutoReverse="True"
                             RepeatBehavior="Forever"><DoubleAnimation.EasingFunction><SineEase EasingMode="EaseInOut" /></DoubleAnimation.EasingFunction></DoubleAnimation></Storyboard></Window.Resources><Grid><Canvas HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Margin="0 0 48 48"><Ellipse x:Name="ball"
                     Width="48"
                     Height="48"
                     Fill="Red" /></Canvas></Grid></Window>

コードは、組み込みスタイルを除けば同じになります。実行結果を示します。
CircleAnimationpng

スクリーン ショットでは区別できませんが、赤い円が円周上をアニメーションで動きます。これもイージング関数によって実現していますから、説明は書籍を熟読してください。

9.7(P395) XAML アニメーション

本節では、前節までのアニメーションが Loaded イベント ハンドラーによって開始していたことに対して、自動的にアニメーションを開始させる仕組みである Triggers プロパティを説明しています。この動きを示すために ForeverColorAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><Grid.Background><SolidColorBrush x:Name="gridBrush" /></Grid.Background><TextBlock Text="Color Animation"
                   FontFamily="Times New Roman"
                   FontSize="96"
                   FontWeight="Bold"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"><TextBlock.Foreground><SolidColorBrush x:Name="txtblkBrush" /></TextBlock.Foreground></TextBlock></Grid><Window.Triggers><EventTrigger RoutedEvent="Loaded"><BeginStoryboard><Storyboard RepeatBehavior="Forever"
                            AutoReverse="True"><ColorAnimation Storyboard.TargetName="gridBrush"
                                    Storyboard.TargetProperty="Color"
                                    From="Black" To="White" Duration="0:0:2" /><ColorAnimation Storyboard.TargetName="txtblkBrush"
                                    Storyboard.TargetProperty="Color"
                                    From="White" To="Black" Duration="0:0:2" /></Storyboard></BeginStoryboard></EventTrigger></Window.Triggers></Window>

このコードで違うところは、EnventTrigger 要素の RoutedEvent 属性の設定になります。WinRT XAMLでは、EventTrigger 要素は Loaded イベントにしか使用することができないので、記述自体を省略することが求められます。これに対して WPF XAML では、他のイベントでも使用できるようになっていることから、明示的に Loaded イベントを指定しています。次に、ColorAnimation タイムラインが記述されていることにも注意をしてください。Brush オブジェクトの Color プロパティをアニメーションする場合には、当たり前ですが型が Double ではないため DoubleAnimation タイムラインを使用できないことから、Color プロパティ専用のタイムラインである ColorAnimation を使用します。それでは、実行結果を示します。
ForeverColorAnimation

今度は図形を変化させる SquaringTheCircle プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><Canvas HorizontalAlignment="Center"
                VerticalAlignment="Center"><Path Fill="Gray"
                  Stroke="Black" 
                  StrokeThickness="3" ><Path.Data><PathGeometry><PathFigure x:Name="bezier1" IsClosed="True"><BezierSegment x:Name="bezier2" /><BezierSegment x:Name="bezier3" /><BezierSegment x:Name="bezier4" /><BezierSegment x:Name="bezier5" /></PathFigure></PathGeometry></Path.Data><Path.Triggers><EventTrigger RoutedEvent="Loaded"><BeginStoryboard><Storyboard RepeatBehavior="Forever"><PointAnimation Storyboard.TargetName="bezier1"
                                                Storyboard.TargetProperty="StartPoint"
                                                From="0 200" To="0 250"
                                                AutoReverse="True" /><PointAnimation Storyboard.TargetName="bezier2"
                                                Storyboard.TargetProperty="Point1"
                                                From="110 200" To="125 125"
                                                AutoReverse="True" /><PointAnimation Storyboard.TargetName="bezier2"
                                                Storyboard.TargetProperty="Point2"
                                                From="200 110" To="125 125"
                                                AutoReverse="True" /><PointAnimation Storyboard.TargetName="bezier2"
                                                Storyboard.TargetProperty="Point3"
                                                From="200 0" To="250 0"
                                                AutoReverse="True" /><PointAnimation Storyboard.TargetName="bezier3"
                                                Storyboard.TargetProperty="Point1"
                                                From="200 -110" To="125 -125"
                                                AutoReverse="True" /><PointAnimation Storyboard.TargetName="bezier3"
                                                Storyboard.TargetProperty="Point2"
                                                From="110 -200" To="125 -125"
                                                AutoReverse="True" /><PointAnimation Storyboard.TargetName="bezier3"
                                                Storyboard.TargetProperty="Point3"
                                                From="0 -200" To="0 -250"
                                                AutoReverse="True" /><PointAnimation Storyboard.TargetName="bezier4"
                                                Storyboard.TargetProperty="Point1"
                                                From="-110 -200" To="-125 -125"
                                                AutoReverse="True" /><PointAnimation Storyboard.TargetName="bezier4"
                                                Storyboard.TargetProperty="Point2"
                                                From="-200 -110" To="-125 -125"
                                                AutoReverse="True" /><PointAnimation Storyboard.TargetName="bezier4"
                                                Storyboard.TargetProperty="Point3"
                                                From="-200 0" To="-250 0"
                                                AutoReverse="True" /><PointAnimation Storyboard.TargetName="bezier5"
                                                Storyboard.TargetProperty="Point1"
                                                From="-200 110" To="-125 125"
                                                AutoReverse="True" /><PointAnimation Storyboard.TargetName="bezier5"
                                                Storyboard.TargetProperty="Point2"
                                                From="-110 200" To="-125 125"
                                                AutoReverse="True" /><PointAnimation Storyboard.TargetName="bezier5"
                                                Storyboard.TargetProperty="Point3"
                                                From="0 200" To="0 250"
                                                AutoReverse="True" /></Storyboard></BeginStoryboard></EventTrigger></Path.Triggers></Path></Canvas></Grid></Window>

このコードが WinRT XAML と異なるのは、組み込みスタイル、Storyboard の EnableDependentAnimation プロパティ、EventTrigger の RoutedEvent プロパティになります。また、PointAnimation タイムラインが使用されていることも注意してください。PointAnimation は、Point プロパティ(System.Windows.Point) をアニメーションさせるための専用のタイムラインとなります。それでは、実行結果を示します。
SquaringTheCircle

9.8(P400) カスタムクラスのアニメーション

本節では、カスタム コントロールとして PieSlice を定義して扇形にアニメーションさせる方法を説明しています。それでは、AnimatedPieSlice プロジェクトの PieSlice.cs を示します。

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;

namespace AnimatedPieSlice
{
    // Path が seald クラスの為 に Shape に変更
    public class PieSlice : Shape
    {
        PathFigure pathFigure;
        LineSegment lineSegment;
        ArcSegment arcSegment;

        static PieSlice()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(Path), new FrameworkPropertyMetadata(typeof(Path)));
            CenterProperty = DependencyProperty.Register("Center",
                typeof(Point), typeof(PieSlice),
                new PropertyMetadata(new Point(100, 100), OnPropertyChanged));

            RadiusProperty = DependencyProperty.Register("Radius",
                typeof(double), typeof(PieSlice),
                new PropertyMetadata(100.0, OnPropertyChanged));

            StartAngleProperty = DependencyProperty.Register("StartAngle",
                typeof(double), typeof(PieSlice),
                new PropertyMetadata(0.0, OnPropertyChanged));

            SweepAngleProperty = DependencyProperty.Register("SweepAngle",
                typeof(double), typeof(PieSlice),
                new PropertyMetadata(90.0, OnPropertyChanged));
            // DataProperty を追加
            DataProperty = DependencyProperty.Register("Data",
                typeof(Geometry), typeof(PieSlice),
                new PropertyMetadata(null));
        }

        public PieSlice()
        {
            pathFigure = new PathFigure { IsClosed = true };
            lineSegment = new LineSegment();
            arcSegment = new ArcSegment { SweepDirection = SweepDirection.Clockwise };
            pathFigure.Segments.Add(lineSegment);
            pathFigure.Segments.Add(arcSegment);

            PathGeometry pathGeometry = new PathGeometry();
            pathGeometry.Figures.Add(pathFigure);

            this.Data = pathGeometry;

            UpdateValues();
        }

        public static DependencyProperty CenterProperty { private set; get; }

        public static DependencyProperty RadiusProperty { private set; get; }

        public static DependencyProperty StartAngleProperty { private set; get; }

        public static DependencyProperty SweepAngleProperty { private set; get; }
        // DataProperty を追加
        public static DependencyProperty DataProperty { set; get; }

        public Point Center
        {
            set { SetValue(CenterProperty, value); }
            get { return (Point)GetValue(CenterProperty); }
        }

        public double Radius
        {
            set { SetValue(RadiusProperty, value); }
            get { return (double)GetValue(RadiusProperty); }
        }

        public double StartAngle
        {
            set { SetValue(StartAngleProperty, value); }
            get { return (double)GetValue(StartAngleProperty); }
        }

        public double SweepAngle
        {
            set { SetValue(SweepAngleProperty, value); }
            get { return (double)GetValue(SweepAngleProperty); }
        }

        static void OnPropertyChanged(DependencyObject obj,
                                      DependencyPropertyChangedEventArgs args)
        {
            (obj as PieSlice).UpdateValues();
        }

        void UpdateValues()
        {
            pathFigure.StartPoint = this.Center;

            double x = this.Center.X + this.Radius * Math.Sin(Math.PI * this.StartAngle / 180);
            double y = this.Center.Y - this.Radius * Math.Cos(Math.PI * this.StartAngle / 180);
            lineSegment.Point = new Point(x, y);

            x = this.Center.X + this.Radius * Math.Sin(Math.PI * (this.StartAngle +
                                                                  this.SweepAngle) / 180);

            y = this.Center.Y - this.Radius * Math.Cos(Math.PI * (this.StartAngle +
                                                                  this.SweepAngle) / 180);
            arcSegment.Point = new Point(x, y);
            arcSegment.IsLargeArc = this.SweepAngle >= 180;

            arcSegment.Size = new Size(this.Radius, this.Radius);
        }

        // Data プロパティと DefiningGeometry プロパティを追加
        public Geometry Data
        {
            get { return (Geometry)GetValue(DataProperty); }
            set { SetValue(DataProperty, value); }
        }
        protected override Geometry DefiningGeometry
        {
            get 
            {
                Geometry data = this.Data;
                if (data == null)
                {
                    data = Geometry.Empty;
                }
                return data;
            }
        }
    }
}

コードは、WinRT XAML と大きく異なっている箇所がありますので、次に示します。

  • 継承元のクラスを Path より Shape に変更。
    WinRT XAML の Path クラスは継承可能ですが、WPF XAML の Path クラスは継承できないためです。
    WPF XAML では、Shape クラスの派生型が継承できない(Seald)クラスになっています。
  • DefaultStyleKeyProperty.OverrideMetadata メソッドを追加し、Path クラスのスタイルを適用。
    作成したクラスを WinRT XAML と同じように Path を継承したスタイルに合わせるためです。
  • 依存関係プロパティとして Data プロパティを追加。
  • DefiningGeometry プロパティをオーバーライド

DefaultStyleKeyProperty、Data 依存関係プロパティ などは、WinRT XAML と同じように Path クラスを継承したのと同じようにするために追加しています(DefaultStyleKeyProperty は、追加しなくても同じ動作をすることを確認していますが、念のために追加しています)。この PieSlice カスタム コントロールを使用する MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><local:PieSlice x:Name="pieSlice"
                        Center="400 400"
                        Radius="200"
                        Stroke="Red"
                        StrokeThickness="3"
                        Fill="Yellow" /></Grid><Window.Triggers><EventTrigger RoutedEvent="Loaded"><BeginStoryboard><Storyboard><DoubleAnimation Storyboard.TargetName="pieSlice"
                                     Storyboard.TargetProperty="SweepAngle"
                                     From="1" To="359" Duration="0:0:3"
                                     AutoReverse="True"
                                     RepeatBehavior="Forever" /></Storyboard></BeginStoryboard></EventTrigger></Window.Triggers></Window>

XAML は、組み込みのスタイルと EventTrigger 要素の RoutedEvent 属性を除けば同じになります。もちろん、実行結果も同じになります。
AnimatedPieSlice

作成した PieSlice クラスに関する説明は、書籍を参照してください。

9.9(P404) キーフレーム アニメーション

本節では、複数のアニメーションをタイムラインによって組み合わせるキーフレーム アニメーションを説明しています。この目的で円を画面上で動かす SimpleKeyFrameAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><Path Fill="Blue"><Path.Data><EllipseGeometry x:Name="ellipse"
                                 RadiusX="24"
                                 RadiusY="24" /></Path.Data></Path></Grid><Window.Triggers><EventTrigger RoutedEvent="Loaded"><BeginStoryboard><Storyboard><PointAnimationUsingKeyFrames Storyboard.TargetName="ellipse"
                                                  Storyboard.TargetProperty="Center"
                                                  RepeatBehavior="Forever"><DiscretePointKeyFrame KeyTime="0:0:0" Value="100 100" /><LinearPointKeyFrame KeyTime="0:0:2" Value="700 700" /><LinearPointKeyFrame KeyTime="0:0:2.1" Value="700 100" /><LinearPointKeyFrame KeyTime="0:0:4.1" Value="100 700" /><LinearPointKeyFrame KeyTime="0:0:4.2" Value="100 100" /></PointAnimationUsingKeyFrames></Storyboard></BeginStoryboard></EventTrigger></Window.Triggers></Window>

XAML自体は、組み込みスタイルと EventTrigger を除けば同じになります。このアニメーションのポイントは、PointAnimationUsingKeyFrames を使って キーフレームのコレクションに従ってアニメーションを行っているところにあります。キーフレームの説明は書籍にありますので、ここでは基本的な動きだけを次に示します。

  • 中心座標が (100,100) が開始位置となります。
  • 2 秒後に中心座標を (700,700) へ移動します。
    右側、ななめ下へ
  • 2.1 秒後に中心座標を (700,100) へ移動します。
    x座標は同じで、開始位置へ(つまり、上へ)
  • 4.1 秒後に中心座標を (100,700) へ移動します。
    x座標を開始位置にし、y座標を下へ(つまり、ななめ左下へ)
  • 4.2 秒後に中心座標を (100,100) へ移動します。
    開始位置へ(つまり、上へ)
  • Forever によって繰り返します。

念のため、実行結果を示します。
SimpleKeyFrameAnimation

もちろん、スクリーン ショットではアニメーションの動きを確認できませんので、自分で動かしてみてください。今度は、背景色をアニメーション化する RainbowAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><Grid.Background><SolidColorBrush x:Name="brush" /></Grid.Background></Grid><Window.Triggers><EventTrigger RoutedEvent="Loaded"><BeginStoryboard><Storyboard RepeatBehavior="Forever"><ColorAnimationUsingKeyFrames Storyboard.TargetName="brush"
                                                  Storyboard.TargetProperty="Color"><DiscreteColorKeyFrame KeyTime="0:0:0" Value="#FF0000" /><LinearColorKeyFrame KeyTime="0:0:1" Value="#FFFF00" /><LinearColorKeyFrame KeyTime="0:0:2" Value="#00FF00" /><LinearColorKeyFrame KeyTime="0:0:3" Value="#00FFFF" /><LinearColorKeyFrame KeyTime="0:0:4" Value="#0000FF" /><LinearColorKeyFrame KeyTime="0:0:5" Value="#FF00FF" /><LinearColorKeyFrame KeyTime="0:0:6" Value="#FF0000" /></ColorAnimationUsingKeyFrames></Storyboard></BeginStoryboard></EventTrigger></Window.Triggers></Window>

このXAMLは、WinRT XAML と基本的に同じもの(EventTrigger 以外)になりますので、実行すれば Grid の背景色が7色に変化するようになります。
RainbowAnimation

今度は、Grid の背景色をグラデーションを使って回転させる GradientBrushPointAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><Grid.Background><LinearGradientBrush x:Name="gradientBrush"><GradientStop Offset="0" Color="Red" /><GradientStop Offset="1" Color="Blue" /></LinearGradientBrush></Grid.Background></Grid><Window.Triggers><EventTrigger RoutedEvent="Loaded"><BeginStoryboard><Storyboard RepeatBehavior="Forever"><PointAnimationUsingKeyFrames Storyboard.TargetName="gradientBrush"
                                                  Storyboard.TargetProperty="StartPoint"><LinearPointKeyFrame KeyTime="0:0:0" Value="0 0" /><LinearPointKeyFrame KeyTime="0:0:1" Value="1 0" /><LinearPointKeyFrame KeyTime="0:0:2" Value="1 1" /><LinearPointKeyFrame KeyTime="0:0:3" Value="0 1" /><LinearPointKeyFrame KeyTime="0:0:4" Value="0 0" /></PointAnimationUsingKeyFrames><PointAnimationUsingKeyFrames Storyboard.TargetName="gradientBrush"
                                                  Storyboard.TargetProperty="EndPoint"><LinearPointKeyFrame KeyTime="0:0:0" Value="1 1" /><LinearPointKeyFrame KeyTime="0:0:1" Value="0 1" /><LinearPointKeyFrame KeyTime="0:0:2" Value="0 0" /><LinearPointKeyFrame KeyTime="0:0:3" Value="1 0" /><LinearPointKeyFrame KeyTime="0:0:4" Value="1 1" /></PointAnimationUsingKeyFrames></Storyboard></BeginStoryboard></EventTrigger></Window.Triggers></Window>

この XAML も、基本的には WinRT XAML と同じもの(EventTrigger 以外)になります。キーフレーム アニメーションの定義を読めば、グラデーションの開始位置(StartPoint)と終了位置(EndPoint) を移動させることが理解できるでしょう。それでは、実行結果を示します。
GradientBrushPointAnimation

9.10(P408) オブジェクト アニメーション

本節では、WinRT XAML で良く使うと考えられるオブジェクト アニメーションを実現する ObjectKeyFrame の派生クラスの説明をしています。このキーフレームの使い方は、WPF XAML とはかなり異なっています。最初に FastNotFluid プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid Background="Gray"><Canvas SizeChanged="OnCanvasSizeChanged"
                Margin="0 0 96 96"><Ellipse x:Name="ellipse"
                     Width="96"
                     Height="96" Fill="Black" /></Canvas></Grid><Window.Triggers><EventTrigger RoutedEvent="Loaded"><BeginStoryboard><Storyboard x:Name="animation"><DoubleAnimation x:Name="horzAnima"
                                     Storyboard.TargetName="ellipse"
                                     Storyboard.TargetProperty="(Canvas.Left)"
                                     From="0" Duration="0:0:2.51"
                                     AutoReverse="True"
                                     RepeatBehavior="Forever" /><DoubleAnimation x:Name="vertAnima"
                                     Storyboard.TargetName="ellipse"
                                     Storyboard.TargetProperty="(Canvas.Top)"
                                     From="0" Duration="0:0:1.01"
                                     AutoReverse="True"
                                     RepeatBehavior="Forever" /><ObjectAnimationUsingKeyFrames
                                        Storyboard.TargetName="ellipse"
                                        Storyboard.TargetProperty="(UIElement.Visibility)"
                                        RepeatBehavior="Forever"><DiscreteObjectKeyFrame KeyTime="0:0:0" Value="{x:Static Visibility.Visible}" /><DiscreteObjectKeyFrame KeyTime="0:0:0.2" Value="{x:Static Visibility.Collapsed}" /><DiscreteObjectKeyFrame KeyTime="0:0:0.25" Value="{x:Static Visibility.Visible}" /><DiscreteObjectKeyFrame KeyTime="0:0:0.3" Value="{x:Static Visibility.Collapsed}" /><DiscreteObjectKeyFrame KeyTime="0:0:0.45" Value="{x:Static Visibility.Visible}" /></ObjectAnimationUsingKeyFrames><ColorAnimationUsingKeyFrames
                                        Storyboard.TargetName="ellipse"
                                        Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
                                        RepeatBehavior="Forever"><EasingColorKeyFrame KeyTime="0:0:0" 
                            Value="Black" /><EasingColorKeyFrame KeyTime="0:0:0.2" 
                            Value="White" /><EasingColorKeyFrame KeyTime="0:0:0.4" 
                            Value="Black" /><EasingColorKeyFrame KeyTime="0:0:0.6" 
                            Value="Black" /></ColorAnimationUsingKeyFrames></Storyboard></BeginStoryboard></EventTrigger></Window.Triggers></Window>

この XAML は、EventTrigger 要素の RoutedEvent 属性以外にも違いがありますから、キーフレーム アニメーションの説明を示します。

  • DoubleAnimation の horzAnima は、同じで Canvas.Left 添付プロパティを指定しています。
  • DoubleAnimation の vertAnima は、同じで Canvas.Top 添付プロパティを指定しています。
  • ObjectAnimationUsingKeyFrames は、TargetProperty 添付プロパティの指定方法が異なります。
    具体的には、「(UIElement.Visibility)」 を指定しています。一方で、WinRT XAML は文字列で Visibility を指定しています。
    WPF XAML では、継承したプロパティに関しては継承元のクラスを指定して記述しなければなりません(型に対する厳密性)。
  • ColorAnimationUsingKeyFrames は、ObjectAnimationUsingKeyFrames を置き換えています。
    また、TargetProperty 添付プロパティの指定方法が異なります。具体的には、「(Shape.Fill).(SolidColorBrush.Color)」を指定しています。
    ColorAnimationUsingKeyFrames に変更したことで、EasingColorKeyFrame に変更しています。

すでに説明���ていますが、WinRT XAML の ObjectAnimationUsingKeyFrame は様々な用途に使えるの便利なキーフレーム になります。一方で WPF XAML は、型に対する厳密性が要求されるようになっているためことから専用のキーフレームを指定する必要と TargetProperty 添付プロパティには指定するプロパティを実装する派生元のクラスを使った記述を行う必要があります。これが、Fill プロパティを指定する場合に行っている「(Shape.Fill).(SolidColorBrush.Color)」の意味になります。この構文は、Shape クラスで実装する Fill プロパティ(Brush 型)を SolidColorBrush 型の Color プロパティ を指定しています。今度は、アニメーションの上限値を設定している、MainWindow.xaml.cs の OnCanvasSizeChanged イベント ハンドラーを示します。

private void OnCanvasSizeChanged(object sender, SizeChangedEventArgs e)
{
    bool isError = false;
    TimeSpan time = new TimeSpan();
    try
    {
        time = animation.GetCurrentTime();
        isError = false;
    }
    catch
    {
        isError = true;
    }
    animation.Stop();

    horzAnima.To = e.NewSize.Width;
    vertAnima.To = e.NewSize.Height;

    animation.Begin();
    if (!isError)
        animation.Seek(time);
}

WinRT XAML のコードとは、かなり異なります。すでに説明していますが、To プロパティを動的に変化させても動作中のアニメーションに反映しないためになります。それでは、実行結果を示します。
FastNotFluid

ここでは、WinRT XAML と WPF XAML のアニメーションの違いを簡単に説明します。最初に、良く使われる基本的なアニメーションを示します。

プロパティ型From/ToWPFWinRT
DoubleDoubleAnimationOO
ColorColorAnimationOO
PointPointAnimationOO
RectRectAnimationOX
SizeSizeAnimationOX
Int32Int32AnimationOX

今度は、キーフレーム アニメーションを抜粋して示します。

プロパティ型From/ToWPFWinRT
ObjectObjectAnimationUsingKeyFramesOO
ColorColorAnimationUsingKeyFramesOO
PointPointAnimationUsingKeyFramesOO
DoubleDoubleAnimationUsingKeyFramesOO
BooleanBooleaAnimationUsingKeyFramesOX
StringStringAnimationUsingKeyFramesOX

このように、WPF XAML では型に応じたアニメーションやキーフレームの型が豊富に用意されています。一方で、WinRT XAML は WPF XAML の一部分だけが提供されています(もちろん、WinRT XAML だけに含まれるアニメーションもあります)。このために、WinRT XAML では ObjectAnimationUsingKeyFrames が何度も活躍することになります。そして、TargetProperty 添付プロパティに対する記述方法は、WinRT XAML が簡略記法を許可しているだけになります。アニメーションさせるプロパティによっては、簡略記法が使えないので括弧を使った派生元の型(クラス)を指定してプロパティを指定したことが、私には何度もあります。つまり、WinRT XAML でも括弧を使用した派生元の型を指定する記述は有効なので、アニメーションで例外が発生するような場合は書き直してみてください(Blend for Visual Studio でアニメーションの設定を行うと、省略記法ではなく括弧を利用した派生元の型を指定する形式の XAML を生成してくれます)。アニメーションに指定するプロパティや、タイムラインとキーフレームにこのような違いがあるので、本記事の冒頭で WPF XAML は型を厳密に指定しなければならないと説明しました。一方で、Silverlight から始まる XAML 系の UI フレームワークの経験から良く使われるであろうアニメーションに的を絞って、コンパクトに仕上げたのが WinRT XAML などの UI フレームワークの特徴だと考えることができます。

9.11(P411) 定義済みのアニメーションと遷移

本節では、WinRT XAML で用意されている組み込みのアニメーション ライブラリを説明しています。WPF XAML では、ほんの一部(Popup など)を除いて、組み込みのアニメーション ライブラリは用意されていません。これは、WPF が登場した時代にタッチ操作などを含めてアニメーションの一般的な用途が決まっていなかったことが原因だと考えられます。Windows 8 のストア アプリの設計では、ページ遷移を含めて一般化したアニメーション ライブラリが定義されたので、WinRT XAML でも提供することで、全てのユーザーに統一化したアニメーションを提供できるようになりました。ここでは、一部のアニメーションを定義した precnfiguredAnimations プロジェクトの Mainwindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Style TargetType="Button"><Setter Property="Margin" Value="0 6" /></Style><Storyboard x:Key="fadeIn"
                    Storyboard.TargetName="button"
                    Storyboard.TargetProperty="Opacity"><DoubleAnimation From="0.0" To="1.0" Duration="0:0:4" /></Storyboard><Storyboard x:Key="fadeOut"
                    Storyboard.TargetName="button"
                    Storyboard.TargetProperty="Opacity"><DoubleAnimation From="1.0" To="0.0" Duration="0:0:4" /></Storyboard><Storyboard x:Key="popIn"
                    Storyboard.TargetName="button"
                    Storyboard.TargetProperty="Opacity"><DoubleAnimation From="0.0" To="1.0" Duration="0:0:0.5" /></Storyboard><Storyboard x:Key="popOut"
                    Storyboard.TargetName="button"
                    Storyboard.TargetProperty="Opacity"><DoubleAnimation From="1.0" To="0.0" Duration="0" /></Storyboard></Window.Resources><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="Auto" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><StackPanel x:Name="animationTriggersStackPanel"
                    Grid.Column="0"
                    VerticalAlignment="Center"><Button Content="Fade In"
                    Tag="fadeIn" 
                    Click="OnButtonClick" /><Button Content="Fade Out"
                    Tag="fadeOut" 
                    Click="OnButtonClick" /><Button Content="Pop In"
                    Tag="popIn" 
                    Click="OnButtonClick" /><Button Content="Pop Out"
                    Tag="popOut" 
                    Click="OnButtonClick" /><Button Content="Reposition"
                    Tag="reposition" 
                    Click="OnButtonClick" /><Button Content="Pointer Up"
                    Tag="pointerUp" 
                    Click="OnButtonClick" /><Button Content="Pointer Down"
                    Tag="pointerDown" 
                    Click="OnButtonClick" /><Button Content="Swipe Back"
                    Tag="swipeBack" 
                    Click="OnButtonClick" /><Button Content="Swipe Hint"
                    Tag="swipeHint" 
                    Click="OnButtonClick" /><Button Content="Drag Item"
                    Tag="dragItem" 
                    Click="OnButtonClick" /><Button Content="Drop Target Item"
                    Tag="dropTargetItem" 
                    Click="OnButtonClick" /><Button Content="Drag Over"
                    Tag="dragOver" 
                    Click="OnButtonClick" /></StackPanel><!-- Animation target --><Button x:Name="button"
                Grid.Column="1"
                Content="Big Button"
                FontSize="48"
                HorizontalAlignment="Center"
                VerticalAlignment="Center" /></Grid></Window>

XAML を読めば、違いは明らかなことでしょう。Fade Out から Pop Out のみのアニメーションを定義しています。定義した時間も、これが正しいというものではなく、そのようなアニメーションに感じるというものになります。つまり、WPF XAML では、全てのアニメーションを自分で定義しなければならないのです。WPF XAML で用意されている組み込みのアニメーションは、Popup クラスに用意された PopupAnimation プロパティに指定できる PopupAnimation 列挙などだけになります。WinRT XAML に用意されている組み込みアニメーションを確認する場合は、書籍を熟読するのとサンプルを自分で動かしてみてください。

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

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

$
0
0

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

WPF を利用する上で、有益な資料を八巻さんがアップデートして公開してくれました。「Windowsフォームに対するWPFの真の優位性とは」という記事から、ダウンロードできますので、参考にしてください。私が、「XAML とは何か」で記述したディスプレイ ドライバー のことなども踏まえている資料になります。

第10章 座標変換

本章では、アニメーションなどで使用する座標変換を説明しています。WPF XAML も WinRT XAML と同じように座標変換を利用可能になっています。座標変換は、Windows Forms などでは高度なグラフィックスを描画する場合などに必要とされる技術でしたから、Direct X などのグラフィックス技術を扱わない技術者にとっても馴染みがないものになることでしょう。

10.1(P417) 概要

本節では、座標変換が座標 (x,y) を新しい座標 (x',y') に変換するものであり、WinRT XAML では RenderTransform、RenderTransformOrigin、Projection という 3つのプロパティでサポートされていることを説明しています。WPF XAML に置き換えると、Projection プロパティのみが適用できないものになります。なぜなら、WPF XAML が 3D オブジェクトをサポートしているためです。Projection は、Silverlight で導入されて、WinRT XAML でも採用された疑似的な 3D を扱う座標変換になります。最初に、RenderTransform の派生クラスである RotateTransform を使用する SimpleRotate プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><Image Source="http://www.charlespetzold.com/pw6/PetzoldJersey.jpg"
           Stretch="None"
           HorizontalAlignment="Right"
           VerticalAlignment="Bottom"><Image.RenderTransform><RotateTransform Angle="135" /></Image.RenderTransform></Image></Grid></Window>

この XAML は、組み込みスタイルを除けば WinRT XAML と同じになります。Angle 属性で指定されているように、135度に回転させることになります。
SimpleRotate

書籍に記載されていますが、RotateTransform は 左上角を基準に回転させる座標変換を行います。書籍では、Transform 派生クラスを簡単に説明してから、Projection を使った 3D エフェクトの説明で SimpleProjection プロジェクトを使った説明になります。すでに説明したように、WPF XAML は Projection をサポートしていまん。サポートしていませんから、Projection と同じような効果を得ようとすれば、3D オブジェクトを使用することになります。たとえば、Projection と同じような効果を得る記事として「Enter The Planerator - Dead-simple 3D in WPF, with a stupid name」があり、この記事に添付されている PlaneratorSolution を使って Visual Studio 2013 の WPF プロジェクトで実行した結果を示します。このプロジェクトは、サンプルに含めていませんので、試したいかたは自分で作成してみてください。
PlaneratorSolution

また、WPF で 3D オブジェクトの扱い方を学習される場合は、「WPF 3Dプログラミング―誰でも簡単に3Dゲームやツールが作れる最新技術!」や「3D Programming for Windows」なども参考になることでしょう。「3D Programming for Windows」は、プログラミング Windows の著者であるペゾルドの著作になります。

逆説的に説明すれば、WinRT XAML では 3D グラフィックスをサポートしていないので、DirectX を使用しないと 3D グラフィックスを描画できないと言えます(この目的では、Visual C++/CX を使用するか、SharpDxを使用することもできます)。

10.2(P420) 手動回転と自動回転

本節では、TranslateTransform を使って何ができるかを説明しています。その 1つとして、RotateTransform の Angle プロパティにバインディングする RotateTheText プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><Border BorderBrush="Black"
                BorderThickness="1"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"><Grid><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /></Grid.RowDefinitions><Slider Name="slider"
                        Grid.Row="0"
                        Minimum="0"
                        Maximum="360" /><TextBlock Name="txtblk"
                           Text="Rotate Text with Slider"
                           Grid.Row="1"
                           FontSize="48"><TextBlock.RenderTransform><RotateTransform Angle="{Binding ElementName=slider, Path=Value}" /></TextBlock.RenderTransform></TextBlock></Grid></Border></Grid></Window>

この XAML も、組み込みのスタイルを除けば WinRT XAML と同じになります。Angle プロパティへ、スライダーをバインディングしており、角度をスライダーで指定するので値を 0度から 360度に指定しています。それでは、実行結果を示します。
RotateTheText

書籍では、回転が左上角を中心点に行われることから、これを画面の中央にする方法を説明しています。そして、中心座標を周りを回転するアニメーションにした RotateAroundCenter プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><TextBlock x:Name="txtblk"
                   Text="Rotated Text"
                   FontSize="48"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Top"><TextBlock.RenderTransform><RotateTransform x:Name="rotate" /></TextBlock.RenderTransform></TextBlock></Grid><Window.Triggers><EventTrigger RoutedEvent="Loaded"><BeginStoryboard><Storyboard RepeatBehavior="Forever"><DoubleAnimation Storyboard.TargetName="rotate"
                                     Storyboard.TargetProperty="Angle"
                                     From="0" To="360" Duration="0:0:2" /></Storyboard></BeginStoryboard></EventTrigger></Window.Triggers></Window>

この XAML は、組み込みスタイルを除けば、WinRT XAML と同じになります。そして、中心座標を指定するために RotateTransform の CenterX と CenterY プロパティをコードで設定していますので、MainWindow.xaml.cs の抜粋を示します。

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

        Loaded += (sender, args) =>
        {
            rotate.CenterX = txtblk.ActualWidth / 2;
        };

        SizeChanged += (sender, args) =>
        {
            rotate.CenterY = args.NewSize.Height / 2;
        };
    }
}

コードは、WinRT XAML と同じで、Loaded イベントで CenterX を設定し、SizeChanged イベントで CenterY を設定しています。もちろん、実行結果も同じになります。
RotateAroundCenter

つまり、TextBlock は Window の中心座標を中心として回転するようになっています。

10.3(P426) ビジュアル フィードバック

本節では、ユーザーが操作した結果に対する反応を返すアニメーションを説明しています。Windows ストア アプリでは、ユーザーの操作に対してフィードバックを返すことが推奨され、ビジュアル フィードバックのガイドラインにまとめられています。フィードバックを返す例として、JiggleButtonDemo プロジェクトのカスタム コントロールである JiggleButton.xaml を示します。

<Button x:Class="JiggleButtonDemo.JiggleButton"
        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"
        RenderTransformOrigin="0.5 0.5"
        Click="OnJiggleButtonClick"><Button.Resources><Storyboard x:Key="jiggleAnimation"><DoubleAnimation Storyboard.TargetName="rotate"
                             Storyboard.TargetProperty="Angle"
                             From="0" To="10" Duration="0:0:0.33"
                             AutoReverse="True"><DoubleAnimation.EasingFunction><ElasticEase EasingMode="EaseIn" /></DoubleAnimation.EasingFunction></DoubleAnimation></Storyboard></Button.Resources><Button.RenderTransform><RotateTransform x:Name="rotate" /></Button.RenderTransform></Button>

この XAML は、名前空間とデザイン用の属性(d: プレフィックス) という違いがありますが、その他のコードは WinRT XAML と同じになっています。そして、Click イベントによって RotateTransform を使ったアニメーションを開始しますので、JiggleButton.xaml.cs を示します。

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

namespace JiggleButtonDemo
{
    public partial class JiggleButton : Button
    {
        public JiggleButton()
        {
            InitializeComponent();
        }

        private void OnJiggleButtonClick(object sender, RoutedEventArgs e)
        {
            (this.Resources["jiggleAnimation"] as Storyboard).Begin();
        }
    }
}

名前空間を除けば、WinRT XAML と同じになります。このコードで、ボタンをクリックするとボタンが 10度の回転を行うことが理解できます。それでは、このカスタム コントロールを使用する MainWindow.xaml の抜粋を示します。

<Window ...
        xmlns:local="clr-namespace:JiggleButtonDemo"
        ... ><Grid><local:JiggleButton Content="JiggleButton Demo"
                            FontSize="24"
                            HorizontalAlignment="Center"
                            VerticalAlignment="Center" /></Grid></Window>

もちろん、これまでに説明してきたように組み込みスタイルを除けば WinRT XAML と同じになりますから、実行結果も同じになります。
JiggleButtonDemo

書籍では、Button 派生クラスに対する注意事項を説明していますので、書籍も熟読してください。

10.4(P428) 平行移動

本節では、平行移動を行う TranslateTransform の説明をしています。最初にテキストに対してエンボス加工により「浮き出した状態」に加工し、エングレープ加工にっより「彫り込まれた状態」にする TextEffexts プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Style TargetType="TextBlock"><Setter Property="FontFamily" Value="Times New Roman" /><Setter Property="FontSize" Value="192" /><Setter Property="HorizontalAlignment" Value="Center" /><Setter Property="VerticalAlignment" Value="Center" /></Style></Window.Resources><Grid Background="Gray"><Grid.RowDefinitions><RowDefinition Height="*" /><RowDefinition Height="*" /><RowDefinition Height="*" /></Grid.RowDefinitions><TextBlock Text="EMBOSS"
                   Foreground="White"
                   Grid.Row="0" /><TextBlock Text="EMBOSS"
                   Grid.Row="0"
                   Foreground="Gray" ><TextBlock.RenderTransform><TranslateTransform X="-2" Y="-2" /></TextBlock.RenderTransform></TextBlock><TextBlock Text="ENGRAVE"
                   Foreground="White"
                   Grid.Row="1" /><TextBlock Text="ENGRAVE"
                   Grid.Row="1"
                   Foreground="Gray" ><TextBlock.RenderTransform><TranslateTransform X="2" Y="2" /></TextBlock.RenderTransform></TextBlock><TextBlock Text="Drop Shadow"
                   Grid.Row="2"
                   Foreground="Black"><TextBlock.RenderTransform><TranslateTransform X="6" Y="6" /></TextBlock.RenderTransform></TextBlock><TextBlock Text="Drop Shadow"
                   Foreground="White"
                   Grid.Row="2" /></Grid></Window>

このXAMLは、組み込みのスタイルを除けば同じになります。それでは、実行結果を示します。
TextEffects

今度は、1 ピクセルずつずらして視覚的な奥行きを表現する DepthText プロジェクトの MainWindow.xaml の抜粋を示します(MainWindow.xaml は Grid を定義しているだけになります)。

public partial class MainWindow : Window
{
    const int COUNT = 48;   // ~1/2 inch

    public MainWindow()
    {
        InitializeComponent();

        Grid grid = this.Content as Grid;
        Brush foreground = new SolidColorBrush(Colors.Black);
        Brush grayBrush = new SolidColorBrush(Colors.Gray);

        for (int i = 0; i < COUNT; i++)
        {
            bool firstOrLast = i == 0 || i == COUNT - 1;
            TextBlock txtblk = new TextBlock
            {
                Text = "DEPTH",
                FontSize = 192,
                FontWeight = FontWeights.Bold,
                HorizontalAlignment = HorizontalAlignment.Center,
                VerticalAlignment = VerticalAlignment.Center,
                RenderTransform = new TranslateTransform
                {
                    X = COUNT - i - 1,
                    Y = i - COUNT + 1,
                },
                Foreground = firstOrLast ? foreground : grayBrush
            };
            grid.Children.Add(txtblk);
        }
    }
}

このコードは、Forground 変数に設定する Brush を変更しただけで、WinRT XAML と同じになります。それでは、実行結果を示します。
DepthText

TranslateTransform の説明は書籍に記述されていますので、書籍の熟読をお願いします。

10.5(P432) TransformGroup

本節では、複数の座標変換を扱う TransformGroup の説明をしています。その説明として、RotateTransform の中心座標を指定する手法の 1つとして ImageRotate プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><Image Source="http://www.charlespetzold.com/pw6/PetzoldJersey.jpg"
               Stretch="None"
               HorizontalAlignment="Center"
               VerticalAlignment="Center"><Image.RenderTransform><TransformGroup><TranslateTransform X="-160" Y="-200" /><RotateTransform x:Name="rotate" /><TranslateTransform X="160" Y="200" /></TransformGroup></Image.RenderTransform></Image></Grid><Window.Triggers><EventTrigger RoutedEvent="Loaded"><BeginStoryboard><Storyboard RepeatBehavior="Forever"><DoubleAnimation Storyboard.TargetName="rotate"
                                     Storyboard.TargetProperty="Angle"
                                     From="0" To="360" Duration="0:0:3"><DoubleAnimation.EasingFunction><ElasticEase EasingMode="EaseInOut" /></DoubleAnimation.EasingFunction></DoubleAnimation></Storyboard></BeginStoryboard></EventTrigger></Window.Triggers></Window>

この XAML は、組み込みのスタイルと EventTrigger を除けば WinRT XAML と同じになります。それでは、実行結果を示します。
ImageRotate

書籍では、このプロセスを分解して説明するために RorationCenterDemo プロジェクトを使用しています。それでは、RotationCenterDemo プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Style TargetType="TextBlock"><Setter Property="Text" Value="Rotate around Center" /><Setter Property="FontSize" Value="48" /><Setter Property="HorizontalAlignment" Value="Center" /><Setter Property="VerticalAlignment" Value="Center" /></Style></Window.Resources><Grid><TextBlock Name="txtblk"
                   Foreground="#D0D0D0" /><TextBlock Foreground="#A0A0A0"><TextBlock.RenderTransform><TranslateTransform x:Name="translateBack1" /></TextBlock.RenderTransform></TextBlock><TextBlock Foreground="#707070"><TextBlock.RenderTransform><TransformGroup><TranslateTransform x:Name="translateBack2" /><RotateTransform Angle="45" /></TransformGroup></TextBlock.RenderTransform></TextBlock><TextBlock Foreground="Black"><TextBlock.RenderTransform><TransformGroup><TranslateTransform x:Name="translateBack3" /><RotateTransform Angle="45" /><TranslateTransform x:Name="translate" /></TransformGroup></TextBlock.RenderTransform></TextBlock></Grid></Window>

この XAML は、組み込みのスタイルを除けば WinRT XAML と同じになります。今度は、TranslateTransform の X と Y プロパティを設定している、MainWindow.xaml.cs の抜粋を示します。

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

        Loaded += (sender, args) =>
        {
            translateBack1.X =
            translateBack2.X =
            translateBack3.X = -(translate.X = txtblk.ActualWidth / 2);
            translateBack1.Y =
            translateBack2.Y =
            translateBack3.Y = -(translate.Y = txtblk.ActualHeight / 2);
        };
    }
}

もちろん、コードは WinRT XAML と同じになります。それでは、実行結果を示します。
RotationCenterDemo

この座標変換を理解できれば、ImageRotate プロジェクトの座標変換も理解できることでしょう。

今度は、座標変換とアニメーションを組み合わせてプロペラを動かす Propeller プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><Polygon Points="40   0,  60  0, 53 47, 
                        100  40, 100 60, 53 53, 
                         60 100, 40 100, 47 53, 
                          0  60,  0  40, 47 47"
                 Stroke="Black"
                 Fill="SteelBlue"
                 HorizontalAlignment="Center"
                 VerticalAlignment="Center"
                 RenderTransformOrigin="0.5 0.5"><Polygon.RenderTransform><TransformGroup><RotateTransform x:Name="rotate1" /><TranslateTransform X="300" /><RotateTransform x:Name="rotate2" /></TransformGroup></Polygon.RenderTransform></Polygon></Grid><Window.Triggers><EventTrigger RoutedEvent="Loaded"><BeginStoryboard><Storyboard><DoubleAnimation Storyboard.TargetName="rotate1"
                                     Storyboard.TargetProperty="Angle"
                                     From="0" To="360" Duration="0:0:0.5"
                                     RepeatBehavior="Forever" /><DoubleAnimation Storyboard.TargetName="rotate2"
                                     Storyboard.TargetProperty="Angle"
                                     From="0" To="360" Duration="0:0:6"
                                     RepeatBehavior="Forever" /></Storyboard></BeginStoryboard></EventTrigger></Window.Triggers></Window>

この XAML も、組み込みのスタイルを除けば WinRT XAML と同じになります。それでは、実行結果を示します。
Propeller

このアニメーションは、最初の DoubleAnimation によってプロペラを回転させて、次の DoubleAnimation によって半径300論理ピクセルの円を回転させるものになっています。なぜ、このようになるのかについては、書籍を熟読してください。

10.6(P438) 拡大縮小

本節では、拡大と縮小を行う ScaleTransform を説明しています。最初の例題として、第2章の「2.8 ViewBox による拡大と縮小」と同じことを行う、OppositelyScaledText プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><TextBlock Text="Scaled Text"
                   FontSize="144"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   RenderTransformOrigin="0.5 0.5"><TextBlock.RenderTransform><ScaleTransform x:Name="scale" /></TextBlock.RenderTransform></TextBlock></Grid><Window.Triggers><EventTrigger RoutedEvent="Loaded"><BeginStoryboard><Storyboard><DoubleAnimation Storyboard.TargetName="scale"
                                     Storyboard.TargetProperty="ScaleX"
                                     BeginTime="0:0:2"
                                     From="1" To="0.01" Duration="0:0:2"
                                     AutoReverse="True"
                                     RepeatBehavior="Forever" /><DoubleAnimation Storyboard.TargetName="scale"
                                     Storyboard.TargetProperty="ScaleY"
                                     From="10" To="0.1" Duration="0:0:2"
                                     AutoReverse="True"
                                     RepeatBehavior="Forever" /></Storyboard></BeginStoryboard></EventTrigger></Window.Triggers></Window>

この XAML は、組み込みのスタイルを除けば WinRT XAML と同じになります。それでは、実行結果を示します。
OppositelyScaledText

ScaleTransform の動作については、書籍に説明がありますので熟読をお願いします。今度は反射効果を実現するために ReflectedFadeOutImage プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><Grid.RowDefinitions><RowDefinition Height="*" /><RowDefinition Height="*" /></Grid.RowDefinitions><Image Source="http://www.charlespetzold.com/pw6/PetzoldJersey.jpg"
               HorizontalAlignment="Center" /><Grid RenderTransformOrigin="0 1"
              HorizontalAlignment="Center"><Grid.RenderTransform><ScaleTransform ScaleY="-1" /></Grid.RenderTransform><Image Source="http://www.charlespetzold.com/pw6/PetzoldJersey.jpg" /><Rectangle><Rectangle.Fill><LinearGradientBrush StartPoint="0 0" EndPoint="0 1" ><GradientStop Offset="0" 
                            Color="Black" /><GradientStop Offset="1" Color="Transparent" /></LinearGradientBrush></Rectangle.Fill></Rectangle></Grid></Grid></Window>

このコードは、組み込みのスタイルを除けば WinRT XAML と同じになります。それでは、実行結果を示します。
ReflectedFadeOutImage

反射の Image 要素の Source 属性には、同じ値を指定していますが、望ましいのはバインディング記述を使うことでしょう。書籍でも少し話題として記述されていますが、WPF XAML であれば VisualBrush を使うことで同じ効果を簡単に得ることができます。

<Image Source="http://www.charlespetzold.com/pw6/PetzoldJersey.jpg"
       x:Name="ReflectedVisual" HorizontalAlignment="Center" /><Rectangle Height="{Binding ElementName=ReflectedVisual, Path=ActualHeight}"
           Width="{Binding ElementName=ReflectedVisual, Path=ActualWidth}"
           Grid.Row="1"
           HorizontalAlignment="Center"><Rectangle.Fill><VisualBrush Opacity="0.75" Stretch="None"
                     Visual="{Binding ElementName=ReflectedVisual}"><VisualBrush.RelativeTransform><TransformGroup><ScaleTransform ScaleX="1" ScaleY="-1" /><TranslateTransform  Y="1" /></TransformGroup></VisualBrush.RelativeTransform></VisualBrush></Rectangle.Fill><Rectangle.OpacityMask><LinearGradientBrush StartPoint="0 0" EndPoint="0 1" ><GradientStop Offset="0" 
                    Color="Black" /><GradientStop Offset="1" Color="Transparent" /></LinearGradientBrush></Rectangle.OpacityMask></Rectangle>

このコードは、Rectangleの塗りつぶしに VisualBrush を使い、RelativeTransform に ScaleTransform と TranslateTransform を用いることで実現しています。WinRT XAML では、RelativeTransform には 1 つの Transform オブジェクトしか指定できないので、書籍で説明した手法となっています。この手法が書籍と異なるのは、反射した画像の下側の色が白色に薄くなることです。これは、書籍が背景がダークのテーマを利用しているためになります。今回の WPF XAML のように背景色が白であれば、Color に 「Black」ではなく「White」を指定するのが正常な考え方になります。

10.7(P441) アナログ時計の作成

本節では、ここまでに説明してきた座標変換とアニメーションを使ってアナログ時計を作成する手法を説明しています。どのような過程を経て作成されたかについては、書籍を熟読してください。それでは、AnalogClock プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Style TargetType="Path"><Setter Property="Stroke" Value="Black" /><Setter Property="StrokeThickness" Value="2" /><Setter Property="StrokeStartLineCap" Value="Round" /><Setter Property="StrokeEndLineCap" Value="Round" /><Setter Property="StrokeLineJoin" Value="Round" /><Setter Property="StrokeDashCap" Value="Round" /><Setter Property="Fill" Value="Blue" /></Style></Window.Resources><Grid><Viewbox><!-- Grid containing all graphics based on (0, 0) origin, 100-pixel radius --><Grid Width="200" Height="200"><!-- Transform for entire clock --><Grid.RenderTransform><TranslateTransform X="100" Y="100" /></Grid.RenderTransform><!-- Small tick marks --><Path Fill="{x:Null}"
                      StrokeThickness="3"
                      StrokeDashArray="0 3.14159"><Path.Data><EllipseGeometry RadiusX="90" RadiusY="90" /></Path.Data></Path><!-- Large tick marks --><Path Fill="{x:Null}"
                      StrokeThickness="6"
                      StrokeDashArray="0 7.854"><Path.Data><EllipseGeometry RadiusX="90" RadiusY="90" /></Path.Data></Path><!-- Hour hand pointing straight up --><Path Data="M 0 -60 C 0 -30, 20 -30, 5 -20 L 5 0
                                    C 5 7.5, -5 7.5, -5 0 L -5 -20
                                    C -20 -30, 0 -30, 0 -60"><Path.RenderTransform><RotateTransform x:Name="rotateHour" /></Path.RenderTransform></Path><!-- Minute hand pointing straight up --><Path Data="M 0 -80 C 0 -75, 0 -70, 2.5 -60 L 2.5 0
                                    C 2.5 5, -2.5 5, -2.5 0 L -2.55 -60
                                    C 0 -70, 0 -75, 0 -80"><Path.RenderTransform><RotateTransform x:Name="rotateMinute" /></Path.RenderTransform></Path><!-- Second hand pointing straight up --><Path Data="M 0 10 L 0 -80"><Path.RenderTransform><RotateTransform x:Name="rotateSecond" /></Path.RenderTransform></Path></Grid></Viewbox></Grid></Window>

この XAML は、組み込みのスタイルを除けば WinRT XAML と同じになります。そして、分離コードで時計回りの角度を計算していますので、MainWindow.xaml.cs の抜粋を示します。

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

        CompositionTarget.Rendering += OnCompositionTargetRendering;
    }

    void OnCompositionTargetRendering(object sender, object args)
    {
        DateTime dt = DateTime.Now;
        rotateSecond.Angle = 6 * (dt.Second + dt.Millisecond / 1000.0);
        rotateMinute.Angle = 6 * dt.Minute + rotateSecond.Angle / 60;
        rotateHour.Angle = 30 * (dt.Hour % 12) + rotateMinute.Angle / 12;
    }
}

コードは、WinRT XAML と同じになります。それでは、実行結果を示します。
AnalogClock

書籍では、秒針の動きをどのようにするかということについての説明がありますので、興味があれば熟読をお願いします。

10.8(P447) 傾斜

本節では、傾斜(Skew)と呼ばれる座標変換を説明しています。この具体例として、SkewTransform の AngleX と AngleY プロパティを変化させる SlewPlusSkew プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><TextBlock Text="SKEW"
                   FontSize="288"
                   FontWeight="Bold"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   RenderTransformOrigin="0.5 0.5"><TextBlock.RenderTransform><SkewTransform x:Name="skew" /></TextBlock.RenderTransform></TextBlock></Grid><Window.Triggers><EventTrigger RoutedEvent="Loaded"><BeginStoryboard><Storyboard SpeedRatio="0.5" RepeatBehavior="Forever"><DoubleAnimationUsingKeyFrames Storyboard.TargetName="skew"
                                                   Storyboard.TargetProperty="AngleX"><!-- Back and forth for 4 seconds --><DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0" /><LinearDoubleKeyFrame KeyTime="0:0:1" Value="90" /><LinearDoubleKeyFrame KeyTime="0:0:2" Value="0" /><LinearDoubleKeyFrame KeyTime="0:0:3" Value="-90" /><LinearDoubleKeyFrame KeyTime="0:0:4" Value="0" /><!-- Do nothing for 4 seconds --><DiscreteDoubleKeyFrame KeyTime="0:0:8" Value="0" /><!-- Back and forth for 4 seconds --><LinearDoubleKeyFrame KeyTime="0:0:9" Value="90" /><LinearDoubleKeyFrame KeyTime="0:0:10" Value="0" /><LinearDoubleKeyFrame KeyTime="0:0:11" Value="-90" /><LinearDoubleKeyFrame KeyTime="0:0:12" Value="0" /></DoubleAnimationUsingKeyFrames><DoubleAnimationUsingKeyFrames Storyboard.TargetName="skew"
                                                   Storyboard.TargetProperty="AngleY"><!-- Do nothing for 4 seconds --><DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0" /><DiscreteDoubleKeyFrame KeyTime="0:0:4" Value="0" /><!-- Back and forth for 4 seconds --><LinearDoubleKeyFrame KeyTime="0:0:5" Value="-90" /><LinearDoubleKeyFrame KeyTime="0:0:6" Value="0" /><LinearDoubleKeyFrame KeyTime="0:0:7" Value="90" /><LinearDoubleKeyFrame KeyTime="0:0:8" Value="0" /><!-- Back and forth for 4 seconds --><LinearDoubleKeyFrame KeyTime="0:0:9" Value="-90" /><LinearDoubleKeyFrame KeyTime="0:0:10" Value="0" /><LinearDoubleKeyFrame KeyTime="0:0:11" Value="90" /><LinearDoubleKeyFrame KeyTime="0:0:12" Value="0" /></DoubleAnimationUsingKeyFrames></Storyboard></BeginStoryboard></EventTrigger></Window.Triggers></Window>

この XAML は、これまでに説明してきたように組み込みのスタイルと EventTrigger を除けば WinRT XAML と同じになります。それでは、実行結果を示します。
SkewPlusSkew

10.9(P450) 派手な登場

本節では、ページが表示される時に要素を登場させるアニメーションの実現方法を説明しています。この目的のために書籍では概要を説明して、SkewSlideInText プロジェクト を使用します。それでは、MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><TextBlock Text="Hello!"
                   FontSize="192"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   RenderTransformOrigin="0.5 1"><TextBlock.RenderTransform><TransformGroup><SkewTransform x:Name="skew" /><TranslateTransform x:Name="translate" /></TransformGroup></TextBlock.RenderTransform></TextBlock></Grid><Window.Triggers><EventTrigger RoutedEvent="Loaded"><BeginStoryboard><Storyboard><DoubleAnimation Storyboard.TargetName="translate"
                                     Storyboard.TargetProperty="X"
                                     From="-1000" Duration="0:0:1" /><DoubleAnimationUsingKeyFrames
                                     Storyboard.TargetName="skew"
                                     Storyboard.TargetProperty="AngleX"><DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="15" /><LinearDoubleKeyFrame KeyTime="0:0:1" Value="30" /><EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="0"><EasingDoubleKeyFrame.EasingFunction><ElasticEase /></EasingDoubleKeyFrame.EasingFunction></EasingDoubleKeyFrame></DoubleAnimationUsingKeyFrames></Storyboard></BeginStoryboard></EventTrigger></Window.Triggers></Window>

この XAML は、組み込みのスタイルと EventTrigger を除けば WinRT XAML と同じになります。ストリーボードを見れば理解できますが、左側(-1000)から右側へ平行移動した後に SkewTransform とイージング関数によって跳ね返るような効果を与えています。それでは、実行結果を示します。
SkewSlideInText

書籍では、この平行移動の詳細な説明がありますので熟読をお願いします。

10.10(P451) 座標変換と数学

本節では、座標変換を行う Transform オブジェクトと数学の関係を説明しています。その中で、ScaleTransform、TranslateTransform、RotateTransform がどのような式で表現できるかと Matrix 構造体を解説しています。そして、何種類かの回転を行う RenderTransform を説明するために CommonMatrixTransforms プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Style TargetType="TextBlock"><Setter Property="FontSize" Value="24" /><Setter Property="RenderTransformOrigin" Value="0 0.5" /></Style></Window.Resources><Grid><!-- Move origin to center --><Canvas HorizontalAlignment="Center"
                VerticalAlignment="Center"><TextBlock Text="  RenderTransform='1 0 0 1 0 0'" 
                       RenderTransform="1 0 0 1 0 0" /><TextBlock Text="  RenderTransform='.7 .7 -.7 .7 0 0'"
                       RenderTransform=".7 .7 -.7 .7 0 0" /><TextBlock Text="  RenderTransform='0 1 -1 0 0 0'"
                       RenderTransform="0 1 -1 0 0 0" /><TextBlock Text="  RenderTransform='-.7 .7 -.7 -.7 0 0"
                       RenderTransform="-.7 .7 -.7 -.7 0 0" /><TextBlock Text="  RenderTransform='-1 0 0 -1 0 0'" 
                       RenderTransform="-1 0 0 -1 0 0" /><TextBlock Text="  RenderTransform='-.7 -.7 .7 -.7 0 0'"
                       RenderTransform="-.7 -.7 .7 -.7 0 0" /><TextBlock Text="  RenderTransform='0 -1 1 0 0 0'"
                       RenderTransform="0 -1 1 0 0 0" /><TextBlock Text="  RenderTransform='.7 -.7 .7 .7 0 0"
                       RenderTransform=".7 -.7 .7 .7 0 0" /></Canvas></Grid></Window>

この XAML は、組み込みのスタイルを除けば WinRT XAML と同じになります。それでは、実行結果を示します。
CommonMatrixTransforms

書籍では、コードで操作することに関して説明がありますので、興味があれば熟読をお願いします。

10.11(P460) 複合変換

本節では、座標変換を組み合わせる場合の注意事項などを説明しています。この説明として、CompositeTransform を利用しています。CompositeTransform は WinRT XAML 固有であり、WPF XAML では TransformGroup を用いることになります。それでは、TitledShadow プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Style TargetType="TextBlock"><Setter Property="Text" Value="quirky" /><Setter Property="FontFamily" Value="Times New Roman" /><Setter Property="FontSize" Value="192" /><Setter Property="HorizontalAlignment" Value="Center" /><Setter Property="VerticalAlignment" Value="Center" /></Style></Window.Resources><Grid><!-- Shadow TextBlock --><TextBlock Foreground="Gray"
                   RenderTransformOrigin="0 1"><TextBlock.RenderTransform><TransformGroup><ScaleTransform ScaleY="1.5" /><SkewTransform AngleX="-60" /></TransformGroup></TextBlock.RenderTransform></TextBlock><!-- TextBlock with all styled properties --><TextBlock /></Grid></Window>

この XAML は、組み込みのスタイルと説明した CompositeTransform を除けば WinRT XAML と同じになります。CompositeTransform を TransformGroup に置き換えていますから、ScaleTransform と SkewTransform を組み合わせています。この理由は、CompositeTransform で ScaleY と SkewX プロパティを組み合わせていたからです。それでは、実行結果を示します。
TiltedShadow

書籍では、この変換の効果をデセンダーのない状態にするにはどうしたら良いかを説明しています。興味があれば、熟読をお願いします。

10.12(P463) ジオメトリの変換

本節では、Geometry クラスに対して変換を適用することを説明しています。ここで説明している内容は、そのまま WPF XAML に適用できますので、書籍の熟読をお願いします。

10.13(P464) ブラシの変換

本節では、Brush クラスに対して変換を適用することを説明しています。この説明のために第3章の RaibowEight プロジェクトを用いて、RaibowEightTransform プロジェクトの Main Window.xaml の抜粋を示します。

<Window ... ><Grid><Viewbox><Path StrokeThickness="50"
                  Margin="0 25 0 0"><Path.Data><PathGeometry><PathFigure StartPoint="110 0"><ArcSegment Size="90 90" Point="110 180" 
                                        SweepDirection="Clockwise" /><ArcSegment Size="110 110" Point="110 400" 
                                        SweepDirection="Counterclockwise" /><ArcSegment Size="110 110" Point="110 180" 
                                        SweepDirection="Counterclockwise" /><ArcSegment Size="90 90" Point="110 0" 
                                        SweepDirection="Clockwise" /></PathFigure></PathGeometry></Path.Data><Path.Stroke><LinearGradientBrush StartPoint="0 0" EndPoint="1 1"
                                         SpreadMethod="Repeat"><LinearGradientBrush.RelativeTransform><TranslateTransform x:Name="translate" /></LinearGradientBrush.RelativeTransform><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" /></LinearGradientBrush></Path.Stroke></Path></Viewbox></Grid><Window.Triggers><EventTrigger RoutedEvent="Loaded"><BeginStoryboard><Storyboard><DoubleAnimation Storyboard.TargetName="translate"
                                     Storyboard.TargetProperty="Y"
                                     From="0" To="-1.36" Duration="0:0:10"
                                     RepeatBehavior="Forever" /></Storyboard></BeginStoryboard></EventTrigger></Window.Triggers></Window>

この XAML は、組み込みのスタイルと EventTrigger、EnableDependentAnimation を除けば WinRT XAML と同じになります。それでは、実行結果を示します。
RainbowEightTransform

書籍では、Path 要素やグラデーションなどのトリックとも言える方法を解説しています。実際に動かしてみると虹色がアニメーションで動いているのが分かりますから、なぜ?という疑問を持たれたなら、書籍を熟読してください。

10.14(P469) 要素の配置先の取得

本節では、前節の RainbowEightTransform プロジェクトの説明に関係して Matrix の計算結果を取得することを説明しています。この説明を使って、CompoiteTransform のプロパティをアニメーション化するサンプルとして、WheresMyElement プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid x:Name="contentGrid"><TextBlock x:Name="txtblk"
                   Text="Tap to Find"
                   FontSize="96"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   RenderTransformOrigin="0.5 0.5"><TextBlock.RenderTransform><TransformGroup><TranslateTransform x:Name="translateTransform" /><RotateTransform x:Name="rotateTransform" /><ScaleTransform x:Name="sclaeTransform" /><SkewTransform x:Name="skewTransform" /></TransformGroup><!--<CompositeTransform x:Name="transform" />--></TextBlock.RenderTransform></TextBlock><Polygon x:Name="polygon" Stroke="Blue" /><Path x:Name="path" Stroke="Red" /></Grid><Window.Triggers><EventTrigger RoutedEvent="Loaded"><BeginStoryboard><Storyboard x:Name="storyboard"><DoubleAnimation Storyboard.TargetName="translateTransform"
                                     Storyboard.TargetProperty="X"
                                     From="-300" To="300" Duration="0:0:2.11"
                                     AutoReverse="True" RepeatBehavior="Forever" /><DoubleAnimation Storyboard.TargetName="translateTransform"
                                     Storyboard.TargetProperty="Y"
                                     From="-300" To="300" Duration="0:0:2.23"
                                     AutoReverse="True" RepeatBehavior="Forever" /><DoubleAnimation Storyboard.TargetName="rotateTransform"
                                     Storyboard.TargetProperty="Angle"
                                     From="0" To="360" Duration="0:0:2.51"
                                     AutoReverse="True" RepeatBehavior="Forever" /><DoubleAnimation Storyboard.TargetName="sclaeTransform"
                                     Storyboard.TargetProperty="ScaleX"
                                     From="1" To="2" Duration="0:0:2.77"
                                     AutoReverse="True" RepeatBehavior="Forever" /><DoubleAnimation Storyboard.TargetName="sclaeTransform"
                                     Storyboard.TargetProperty="ScaleY"
                                     From="1" To="2" Duration="0:0:3.07"
                                     AutoReverse="True" RepeatBehavior="Forever" /><DoubleAnimation Storyboard.TargetName="skewTransform"
                                     Storyboard.TargetProperty="AngleX"
                                     From="-30" To="30" Duration="0:0:3.31"
                                     AutoReverse="True" RepeatBehavior="Forever" /><DoubleAnimation Storyboard.TargetName="skewTransform"
                                     Storyboard.TargetProperty="AngleY"
                                     From="-30" To="30" Duration="0:0:3.53"
                                     AutoReverse="True" RepeatBehavior="Forever" /></Storyboard></BeginStoryboard></EventTrigger></Window.Triggers></Window>

この XAML は、組み込みのスタイルと EventTrigger、CompositeTransform、CompositeTransform に伴う DoubleAnimation の変更を除けば WinRT XAML と同じになります。すでに説明したように CompositeTransformは、TransformGroup へ置き換えて、TranslateTransform、RotateTransform、ScaleTransform、SkewTransform を組み合わせています。この変更に伴って、DoubleAnimation の TagetName 添付プロパティを変更しています。今度は、MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    bool storyboardPaused = false;

    public MainWindow()
    {
        InitializeComponent();
    }

    protected override void OnTouchDown(TouchEventArgs e)
    {
        if (storyboardPaused)
        {
            storyboard.Resume(this);
            storyboardPaused = false;
            return;
        }
        GeneralTransform xform = txtblk.TransformToVisual(contentGrid);
        // Draw blue polygon around element
        polygon.Points.Clear();
        polygon.Points.Add(xform.Transform(new Point(0, 0)));
        polygon.Points.Add(xform.Transform(new Point(txtblk.ActualWidth, 0)));
        polygon.Points.Add(xform.Transform(new Point(txtblk.ActualWidth, txtblk.ActualHeight)));
        polygon.Points.Add(xform.Transform(new Point(0, txtblk.ActualHeight)));
        // Draw red bounding box
        path.Data = new RectangleGeometry
        {
            Rect = xform.TransformBounds(new Rect(new Point(0, 0), txtblk.DesiredSize))
        };
        storyboard.Pause(this);
        storyboardPaused = true;
        base.OnTouchEnter(e);
    }
}

このコードは、OnTapped イベントを書き換えただけになります。また、GeneralTransform の TransformPoint メソッドを Transform メソッドに変更しています。これは、TransformPoint メソッドが WinRT 固有だからです。これ以外は、WinRT XAML と同じになります。記述しているコードを読めば理解できると思いますが、タッチした時に TextBlock の周囲を Polygon で囲むことです。それでは、実行結果を示します。
WheresMyElement

書籍には、TransformVisual メソッドなどの説明がありますので熟読をお願いします。

10.15(P472) 投影変換

本節では、3 次元グラフィックス変換をサポートするための Matrix3D などを説明しています。WPF XAML は 3D グラフィックスをサポートしていますから、Matrix3D クラスを System.Windows.Media.Media3D 名前空間で定義しています。そして、説明として使用しているのが、ThreeDeepSpinningText プロジェクトと TapToFlip プロジェクト がありますが、両方とも Projection を使用しています。10.1 で説明したように、WPF XAML は Projection という疑似 3D 効果をサポートしていませんので、この節のサンプルは興味があれば 3D グラフィックスを使って、自分で移植を試みてください。そして、できれば結果を公開してください。

10.16(P480) Matirx3D の拡張

本節では、Matrix3D 構造体を使って様々な座標変換を説明しています。この説明のために、NonAffineStretch プロジェクトを Projection と Matrix3DProjection を使って説明しています。WPF XAML は 3D オブジェクトをサポートしますから、TranslateTransform3D などの様々な変換も提供しています。その代わり、Projection のような疑似 3D 効果をサポートしていません。つまり、本節も前節と同じで、興味があれば自分で移植を試みてください。

座標変換に関しては、細かな違いを除けば 3D オブジェクトに対するサポートが WinRT XAML と WPF XAML では異なります。大事なことは、3D オブジェトを含めてユーザー エクスぺリエンスを高めるために統一的に扱えるようになっていることです。WPF XAML で 3D オブジェクトをバリバリに使って下さいと言うつもりはありません。むしろ、3D オブジェクトをバリバリに使いたいのであれば、WPF よりも Direct3D を使った方が良いでしょう。WinRT XAML は、Silverlight の経験を生かして WPF XAML の 3D オブジェクト サポートではなく疑似 3D 効果のサポートだけにしているのも、ユーザー エクスぺリエンスを高めるために効果的だと判断したからでしょう。つまり、アニメーションや座標変換もユーザー エクスぺリエンスを高めるために使用するのが大切なことなのです。
ここまで説明してきた違いを意識しながら( 3D 効果を除く)、第10章を読むことで WPF にも書籍の内容を応用することができるようになることでしょう。

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

$
0
0

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

第11章 3つのテンプレート

本章では、タイトルにあるように 3種類のテンプレート(コントロール テンプレート、データ テンプレート、アイテム テンプレート)を説明しています。データ テンプレートは、データバインドを行う場合に活用されるもので、ContentControl を継承するクラスで使用されます。コントロール テンプレートは、コントロールの外観を再定義するために使用するもので、マウスなどの動きに応じてコントロールの外観を変化させるアニメーションの定義なども含まれています。最後のアイテム テンプレートは、コレクションをデータ バインドした時に個々のデータに適用されるテンプレートになります。たとえば、リスト ボックスなどのアイテムに適用されるのがアイテム テンプレートです。書籍にも記述がありますが、これらのテンプレートを作成したりカスタマイズするのであれば、Blend for Visual Studio の操作に慣れておくことをお勧めします。その理由は、Blend ではテンプレートをビジュアルに編集することが容易だからです。テンプレートの編集では、Visual Studio のデザイナーは Blend よりも非力になります。

11.1(P494) ボタンのデータ

本節では、Button コントロールの Content プロパティを使ってどのように外観を定義できるかを具体例を使って説明しています。最初に Image オブジェクトを使い、次に Ellipse と LinearGradientBrush を組み合わせてから、テンプレートをリソースに定義することを説明しています。ここまでの説明をしてから、Button に対するスタイル定義を説明するために、SharedStyleWithDataTemplate プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><Style TargetType="Button"><Setter Property="HorizontalAlignment" Value="Center" /><Setter Property="VerticalAlignment" Value="Center" /><Setter Property="ContentTemplate"><Setter.Value><DataTemplate><Ellipse Width="144"
                                 Height="192"
                                 Fill="{Binding}" /></DataTemplate></Setter.Value></Setter></Style></Window.Resources><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><Button Grid.Column="0"><SolidColorBrush Color="Green" /></Button><Button Grid.Column="1"><LinearGradientBrush><GradientStop Offset="0" Color="Blue" /><GradientStop Offset="1" Color="Red" /></LinearGradientBrush></Button><Button Grid.Column="2"><ImageBrush ImageSource="http://www.charlespetzold.com/pw6/PetzoldJersey.jpg" /></Button></Grid></Window>

この XAML は、組み込みのスタイルを除けば WinRT XAML と同じになります。それでは、実行結果を示します。
SharedStyleWithDataTemplate

今度は、時計を表示するボタンの外観を作成するために ClockButton プロジェクトの Clock.cs を示します。

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

namespace ClockButton
{
    public class Clock : INotifyPropertyChanged
    {
        bool isEnabled;
        int hour, minute, second;
        int hourAngle, minuteAngle, secondAngle;

        public event PropertyChangedEventHandler PropertyChanged;

        public bool IsEnabled
        {
            set
            {
                if (SetProperty<bool>(ref isEnabled, value, "IsEnabled"))
                {
                    if (isEnabled)
                        CompositionTarget.Rendering += OnCompositionTargetRendering;
                    else
                        CompositionTarget.Rendering -= OnCompositionTargetRendering;
                }
            }
            get
            {
                return isEnabled;
            }
        }

        public int Hour
        {
            set { SetProperty<int>(ref hour, value); }
            get { return hour; }
        }

        public int Minute
        {
            set { SetProperty<int>(ref minute, value); }
            get { return minute; }
        }

        public int Second
        {
            set { SetProperty<int>(ref second, value); }
            get { return second; }
        }

        public int HourAngle
        {
            set { SetProperty<int>(ref hourAngle, value); }
            get { return hourAngle; }
        }

        public int MinuteAngle
        {
            set { SetProperty<int>(ref minuteAngle, value); }
            get { return minuteAngle; }
        }

        public int SecondAngle
        {
            set { SetProperty<int>(ref secondAngle, value); }
            get { return secondAngle; }
        }

        void OnCompositionTargetRendering(object sender, object args)
        {
            DateTime dateTime = DateTime.Now;
            this.Hour = dateTime.Hour;
            this.Minute = dateTime.Minute;
            this.Second = dateTime.Second;
            this.HourAngle = 30 * dateTime.Hour + dateTime.Minute / 2;
            this.MinuteAngle = 6 * dateTime.Minute + dateTime.Second / 10;
            this.SecondAngle = 6 * dateTime.Second + dateTime.Millisecond / 166;        }

        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 virtual void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

このコードは、WinRT XAML と同じになります。そして、作成した Clock クラスをデータソースとして使用する ClockButton プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><Button HorizontalAlignment="Center"
            VerticalAlignment="Center"><local:Clock IsEnabled="True" /><Button.ContentTemplate><DataTemplate><Grid Width="144" Height="144"><Grid.Resources><Style TargetType="Polyline"><Setter Property="Stroke"
                                    Value="Black" /></Style></Grid.Resources><Polyline Points="72 80, 72 24"
                              StrokeThickness="6"><Polyline.RenderTransform><RotateTransform Angle="{Binding HourAngle}"
                                             CenterX="72"
                                             CenterY="72" /></Polyline.RenderTransform></Polyline><Polyline Points="72 88, 72 12"
                              StrokeThickness="3"><Polyline.RenderTransform><RotateTransform Angle="{Binding MinuteAngle}"
                                             CenterX="72"
                                             CenterY="72" /></Polyline.RenderTransform></Polyline><Polyline Points="72 88, 72 6"
                              StrokeThickness="1"><Polyline.RenderTransform><RotateTransform Angle="{Binding SecondAngle}"
                                             CenterX="72"
                                             CenterY="72" /></Polyline.RenderTransform></Polyline></Grid></DataTemplate></Button.ContentTemplate></Button></Grid>

この XAML は、組み込みのスタイルを除けば WinRT XAML と同じになります。それでは、実行結果を示します。
ClockButton

書籍には、ボタンのしての動き(たとえば、クリックした場合)などの説明がありますし、外観をどのように変更したかなどの説明もあります。それでも、書籍と併せてここまでを読むだけでも、Button コントロールでも様々な外観にテンプレートを使うことでカスタマイズができることを理解できることでしょう。このカスタマイズ性は、XAML 系 UI 技術の特徴でもあり、Windows Forms には無いものになります。

11.2(P504) 意志決定

本節では、XAML の記述だけでは条件分岐などができないことから、11.1 の ClockButton プロジェクトの Clock クラスを利用して拡張することで条件によって外観を変更することを説明しています。それでは、ConditionalClockButton クラスの TwelveHourColck.cs を示します。

namespace ConditionalClockButton
{
    public class TwelveHourClock : ClockButton.Clock
    {
        // Initialize for Hour value of 0
        int hour12 = 12;
        bool isAm = true;
        bool isPm = false;

        public int Hour12
        {
            set { SetProperty<int>(ref hour12, value); }
            get { return hour12; }
        }

        public bool IsAm
        {
            set { SetProperty<bool>(ref isAm, value); }
            get { return isAm; }
        }

        public bool IsPm
        {
            set { SetProperty<bool>(ref isPm, value); }
            get { return isPm; }
        }

        protected override void OnPropertyChanged(string propertyName)
        {
            if (propertyName == "Hour")
            {
                this.Hour12 = (this.Hour - 1) % 12 + 1;
                this.IsAm = this.Hour < 12;
                this.IsPm = !this.IsAm;
            }
            base.OnPropertyChanged(propertyName);
        }
    }
}

このコードは、WinRT XAML と同じになります。次に表示を切り替えるために、BooleanToVisibilityConverter.cs を示します。

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

namespace ConditionalClockButton
{
    public sealed class BooleanToVisibilityConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return (bool)value ? Visibility.Visible : Visibility.Collapsed;
        }

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

コードは、名前空間とConvert メソッドと ConvertBack メソッドの第4パラメーターを除けば WinRT XAML と同じになります。すでに説明していますが、Windows ストア アプリのプロジェクトと違うので WPF XAML では、サンプルになるようなコンバーターのコードはプロジェクトに含まれていません。それでは、ConditionalClockButton プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><Button HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="24"><local:TwelveHourClock IsEnabled="True" /><Button.ContentTemplate><DataTemplate><StackPanel Orientation="Horizontal"><StackPanel.Resources><local:BooleanToVisibilityConverter x:Key="booleanToVisibility" /></StackPanel.Resources><TextBlock Text="It's after " /><TextBlock Text="{Binding Hour12}" /><TextBlock Text=" o'clock" /><TextBlock Text=" in the morning!"
                               Visibility="{Binding IsAm, 
                                                    Converter={StaticResource booleanToVisibility}}" /><TextBlock Text=" in the afternoon!"
                               Visibility="{Binding IsPm, 
                                                    Converter={StaticResource booleanToVisibility}}" /></StackPanel></DataTemplate></Button.ContentTemplate></Button></Grid>

このコードは、組み込みのスタイルを除けば WinRT XAML と同じになります。書籍に説明がありますが、今までと異なるのはリソースの定義が StackPanle に定義されていることになります。これは、リソースを扱った章で説明していますが、リソースは様々な要素に定義できることの一例でしかありません。このように定義することで、StackPanel の中だけで使用できるリソースとなります。つまり、リソースのスコープを定義したわけです。それでは、実行結果を示します。
ConditionalClockButton

11.3(P508) コレクション コントロールと DataTemplate の本来の使用方法

本節では、DataTemplate を使用すべき場面を説明しています。そのために、ItemControl を使ってコレクションがどのように表示されるかを説明しています。ここまでの説明が済んでから、DataTemplate の具体例を説明します。そして、DataTemplate の使い方を説明するために ColorItems プロジェクトの MainWindow.xaml の抜粋を示します。

<Grid><ScrollViewer><ItemsControl x:Name="itemsControl"
                      FontSize="24"><ItemsControl.ItemTemplate><DataTemplate><Grid Width="240"
                          Margin="0 12"><Grid.ColumnDefinitions><ColumnDefinition Width="144" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /></Grid.RowDefinitions><Rectangle Grid.Column="0"
                                   Grid.Row="0"
                                   Grid.RowSpan="4"
                                   Margin="12 0"><Rectangle.Fill><SolidColorBrush Color="{Binding}" /></Rectangle.Fill></Rectangle><StackPanel Grid.Column="1"
                                    Grid.Row="0"
                                    Orientation="Horizontal"><TextBlock Text="A = " /><TextBlock Text="{Binding A}" /></StackPanel><StackPanel Grid.Column="1"
                                    Grid.Row="1"
                                    Orientation="Horizontal"><TextBlock Text="R = " /><TextBlock Text="{Binding R}" /></StackPanel><StackPanel Grid.Column="1"
                                    Grid.Row="2"
                                    Orientation="Horizontal"><TextBlock Text="G = " /><TextBlock Text="{Binding G}" /></StackPanel><StackPanel Grid.Column="1"
                                    Grid.Row="3"
                                    Orientation="Horizontal"><TextBlock Text="B = " /><TextBlock Text="{Binding B}" /></StackPanel></Grid></DataTemplate></ItemsControl.ItemTemplate></ItemsControl></ScrollViewer></Grid>

この XAML は、組み込みのスタイルを除けば 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);
            itemsControl.Items.Add(clr);
        }
    }
}

このコードは、WinRT XAML と同じになります。Colors 列挙をリフレクションによって、ItemControls の Items コレクションに追加しています。それでは、実行結果を示します。
ColorItems

実行結果には、色の名前が含まれていないことから、これを解決するために Petzold.ProgrammingWindows6.Chapter11 プロジェクトの NamedColor.cs を示します。

using System.Collections.Generic;
using System.Reflection;
using System.Windows.Media;

namespace Petzold.ProgrammingWindows6.Chapter11
{
    public class NamedColor
    {
        static NamedColor()
        {
            List<NamedColor> colorList = new List<NamedColor>();
            IEnumerable<PropertyInfo> properties = typeof(Colors).GetTypeInfo().DeclaredProperties;

            foreach (PropertyInfo property in properties)
            {
                NamedColor namedColor = new NamedColor
                {
                    Name = property.Name,
                    Color = (Color)property.GetValue(null)
                };
                colorList.Add(namedColor);
            }
            All = colorList;
        }

        public static IEnumerable<NamedColor> All { private set; get; }

        public string Name { private set; get; }

        public Color Color { private set; get; }
    }
}

このコードは、WinRT XAML と同じになります。そして、INotifyPropertyChanged インタフェースを実装していない理由なども書籍では説明しています。この理由は、単純でデータが動的に変化しないためです。そして、次に色の成分値を 16進数に変換するコンバーターである ByteToHexStringConverter.cs を示します。

using System;
using System.Windows.Data;

namespace Petzold.ProgrammingWindows6.Chapter11
{
    public class ByteToHexStringConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return ((byte)value).ToString("X2");
        }

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

このコードは、名前空間とConvert メソッドと ConvertBack メソッドの第4パラメーターを除けば WinRT XAML と同じになります。それでは、これらのクラスを使用する ColorItemsSource プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ...
        xmlns:ch11="clr-namespace:Petzold.ProgrammingWindows6.Chapter11;assembly=Petzold.ProgrammingWindows6.Chapter11"
        ... ><Window.Resources><ch11:ByteToHexStringConverter x:Key="byteToHexString" /></Window.Resources><Grid><ScrollViewer><ItemsControl x:Name="itemsControl"><ItemsControl.ItemTemplate><DataTemplate><Border BorderBrush="Black"
                                BorderThickness="1"
                                Width="336"
                                Margin="6"><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="Auto" /><ColumnDefinition Width="*"/></Grid.ColumnDefinitions><Rectangle Grid.Column="0"
                                           Height="72"
                                           Width="72"
                                           Margin="6"><Rectangle.Fill><SolidColorBrush Color="{Binding Color}" /></Rectangle.Fill></Rectangle><StackPanel Grid.Column="1"
                                            VerticalAlignment="Center"><TextBlock FontSize="24"
                                               Text="{Binding Name}" /><ContentControl FontSize="18"><StackPanel Orientation="Horizontal"><TextBlock Text="{Binding Color.A,
                                                                      Converter={StaticResource byteToHexString}}" /><TextBlock Text="-" /><TextBlock Text="{Binding Color.R,
                                                                      Converter={StaticResource byteToHexString}}" /><TextBlock Text="-" /><TextBlock Text="{Binding Color.G,
                                                                      Converter={StaticResource byteToHexString}}" /><TextBlock Text="-" /><TextBlock Text="{Binding Color.B,
                                                                      Converter={StaticResource byteToHexString}}" /></StackPanel></ContentControl></StackPanel></Grid></Border></DataTemplate></ItemsControl.ItemTemplate></ItemsControl></ScrollViewer></Grid></Window>

この XAML は、名前空間「xmlns:ch11」の定義方法と組み込みのスタイルを除けば WinRT XAML と同じになります。それでは、データ ソースを設定する MainWindow.xam.cs の抜粋を示します。

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

        itemsControl.ItemsSource = NamedColor.All;
    }
}

このコードは、WinRT XAML と同じになります。それでは、実行結果を示します。
ColorItemsSource

今度は、コードを使用しないでデータ バインディングを行う ColorItemsSourceWithBinding プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ...
        xmlns:ch11="clr-namespace:Petzold.ProgrammingWindows6.Chapter11;assembly=Petzold.ProgrammingWindows6.Chapter11"
        ... ><Window.Resources><ch11:NamedColor x:Key="namedColor" /><ch11:ByteToHexStringConverter x:Key="byteToHexString" /></Window.Resources><Grid><ScrollViewer><ItemsControl ItemsSource="{Binding Source={StaticResource namedColor},
                                                Path=All}"><ItemsControl.ItemTemplate><DataTemplate><Border BorderBrush="Black"
                                BorderThickness="1"
                                Width="336"
                                Margin="6"><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="Auto" /><ColumnDefinition Width="*"/></Grid.ColumnDefinitions><Rectangle Grid.Column="0"
                                           Height="72"
                                           Width="72"
                                           Margin="6"><Rectangle.Fill><SolidColorBrush Color="{Binding Color}" /></Rectangle.Fill></Rectangle><StackPanel Grid.Column="1"
                                            VerticalAlignment="Center"><TextBlock FontSize="24"
                                               Text="{Binding Name}" /><ContentControl FontSize="18"><StackPanel Orientation="Horizontal"><TextBlock Text="{Binding Color.A,
                                                                      Converter={StaticResource byteToHexString}}" /><TextBlock Text="-" /><TextBlock Text="{Binding Color.R,
                                                                      Converter={StaticResource byteToHexString}}" /><TextBlock Text="-" /><TextBlock Text="{Binding Color.G,
                                                                      Converter={StaticResource byteToHexString}}" /><TextBlock Text="-" /><TextBlock Text="{Binding Color.B,
                                                                      Converter={StaticResource byteToHexString}}" /></StackPanel></ContentControl></StackPanel></Grid></Border></DataTemplate></ItemsControl.ItemTemplate></ItemsControl></ScrollViewer></Grid></Window>

この XAML は、名前空間の記述と組み込みのスタイルを除けば WinRT XAML と同じになります。もちろん、実行結果も ColorItemsSource プロジェクトと同じになります。「コレクションとデータ バインディングとテンプレートの組み合わせです。WinRT プログラミングの本質はそこにあります」(書籍より引用)と書籍に記述されていますが、これは XAML 系 UI 技術に共通する特徴ですから、WPF XAML にも当てはまります。また、書籍ではDataTemplate の定義内容などを説明していますので、書籍の熟読をお願いします。

11.4(P520) コレクションとインタフェース

本節では、コレクションをバインディングするために使用されている IEnumerable<T> インタフェースや IDictionary<TKey, TValue> インタフェース、INotifyPropertyChanged インタフェース、INotifyCollectionChanged インタフェースと WinRT の関係を説明しています。WinRT と .NET Framework のコレクションは、透過的に使えるようにプロジェクションという仕組みが用意されています。そして、データ バインディングを考える上で、本節の説明は WPF XAML にも当てはまりますので書籍の熟読をお願いします。

11.5(P522) タップと選択

本節では、DataTemplate で表現されるデータを選択するためにタップ イベントでどのように処理するかを説明しています。そして、選択したアイテムを使う SimpleListBox プロジェクトの Mainwindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><ch11:NamedColor x:Key="namedColor" /></Window.Resources><Grid><ListBox Name="lstbox"
                 ItemsSource="{Binding Source={StaticResource namedColor},
                                       Path=All}"
                 DisplayMemberPath="Name"
                 Width="288"
                 HorizontalAlignment="Center" /><Grid.Background><SolidColorBrush Color="{Binding ElementName=lstbox,
                                             Path=SelectedItem.Color}" /></Grid.Background></Grid></Window>

この XAML は、WinRT XAML と同じになります。Grid の Background にリストボックスで選択した色をバインディングしていることが、XAML から理解できることでしょう。それでは、実行結果を示します。
SimpleListBox

書籍では、SelectedValuePath プロパティなどの使い方を説明してから、ListBox の ItemTemplate に DataTemplate を使用する説明をしています。それでは、ListBoxWithItemTemplate プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><ch11:NamedColor x:Key="namedColor" /></Window.Resources><Grid><ListBox Name="lstbox"
                 ItemsSource="{Binding Source={StaticResource namedColor},
                                       Path=All}"
                 Width="380"><ListBox.ItemTemplate><DataTemplate><Border BorderBrush="{Binding RelativeSource={RelativeSource TemplatedParent},
                                                  Path=Foreground}"
                            BorderThickness="1"
                            Width="336"
                            Margin="6"
                            Loaded="OnItemLoaded"><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="Auto" /><ColumnDefinition Width="*"/></Grid.ColumnDefinitions><Rectangle Grid.Column="0"
                                       Height="72"
                                       Width="72"
                                       Margin="6"><Rectangle.Fill><SolidColorBrush Color="{Binding Color}" /></Rectangle.Fill></Rectangle><TextBlock Grid.Column="1"
                                       FontSize="24"
                                       Text="{Binding Name}"
                                       VerticalAlignment="Center" /></Grid></Border></DataTemplate></ListBox.ItemTemplate></ListBox><Grid.Background><SolidColorBrush Color="{Binding ElementName=lstbox,
                                             Path=SelectedItem.Color}" /></Grid.Background></Grid></Window>

この XAML は、WinRT XAML と同じになります。それでは、実行結果を示します。
ListBoxWithItemTemplate

書籍では、特殊なデータ バインディング記述として「TemplatedParent」を説明しています。もちろん、組み込みのスタイルを除けば WPF XAML にも共通します。このことは、XAML そのものを大きく変更していないことでも明らかですから、詳しい説明は書籍を参照してください。

11.6(P527) 見えるパネルと見えないパネル

本節では、ListBoxWithItemTemplate プロジェクトの Border に対する Loaded イベント ハンドラーを使って UI の仮想化を説明しています。仮想化を説明してから、ItemPanelTemplate の説明になります。それでは、ItemPanleTemplate を使用する HorizontalListBox プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><ch11:NamedColor x:Key="namedColor" /></Window.Resources><Grid><ListBox Name="lstbox"
                 ItemsSource="{Binding Source={StaticResource namedColor},
                                       Path=All}"
                 Height="120"
                 ScrollViewer.HorizontalScrollBarVisibility="Auto"
                 ScrollViewer.VerticalScrollBarVisibility="Disabled"><ListBox.ItemTemplate><DataTemplate><Border BorderBrush="{Binding RelativeSource={RelativeSource TemplatedParent},
                                                  Path=Foreground}"
                            BorderThickness="1"
                            Width="336"
                            Margin="6"
                            Loaded="OnItemLoaded"><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="Auto" /><ColumnDefinition Width="*"/></Grid.ColumnDefinitions><Rectangle Grid.Column="0"
                                       Height="72"
                                       Width="72"
                                       Margin="6"><Rectangle.Fill><SolidColorBrush Color="{Binding Color}" /></Rectangle.Fill></Rectangle><TextBlock Grid.Column="1"
                                       FontSize="24"
                                       Text="{Binding Name}"
                                       VerticalAlignment="Center" /></Grid></Border></DataTemplate></ListBox.ItemTemplate><ListBox.ItemsPanel><ItemsPanelTemplate><VirtualizingStackPanel Orientation="Horizontal" /></ItemsPanelTemplate></ListBox.ItemsPanel></ListBox><Grid.Background><SolidColorBrush Color="{Binding ElementName=lstbox,
                                             Path=SelectedItem.Color}" /></Grid.Background></Grid></Window>

この XAML は、ScrollViewer の HorizontalScrollMode と VerticalScrollMode プロパティを除けば WinRT XAML と同じになります。WinRT XAML の HorizontalScrollMode と VerticalScrollMode プロパティは、ScrollMode 列挙を指定するものであり、WPF XAML では Panning プロパティに相当するものになります。Panning プロパティとは、タッチ操作におけるスクロールを制御するためのプロパティですから、タッチ対応にするのであれば指定した方が良いでしょう。Panning プロパティを指定しなかった場合は、デフォルトでパン ジェスチャが有効になっています。そして、ItemsPanelTemplate に VirtualizingStackPanel を指定して 水平方向(Horizontal) に指定していますから、アイテムが縦方向ではなく横(水平)方向に表現されることとなります。それでは、実行結果を示します。
HorizontalListBox

書籍には、ItemsPanelTemplate の説明がありますので熟読をお願いします。UI の仮想化は、Windows Forms になく XAML 系 の UI 技術の特徴でもあり、高速化に寄与しています。WinRT XAML では、UI の仮想化だけではなくデータの仮想化もサポートされていますので、興味があれば自分で調べることをお勧めします。

11.7(P532) カスタム パネル

本節では、WrapGrid や VariableSizedWrapGrid パネルがうまく使えないことを出発点にして、独自のカスタム パネルを定義するために必要な事項を説明しています。そして、WPF XAML に含まれている UniformGrid パネルを独自に定義することに説明が移ります。しかし、この記事は WPF XAML なので、UniformGrid が標準提供されていますから、WinRT XAML のように UniformGrid を独自に定義する必要はありません。もし、カスタム パネルを自分で作成したい場合は書籍や「Applications = Code + Markup」など参考にするのと WPF Toolkitなどの具体例なども参考にしてください。それでは、UniformGrid パネルを使用する AllColorsItemsControl プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><ch11:NamedColor x:Key="namedColor" /><ch11:ColorToContrastColorConverter x:Key="colorConverter" /></Window.Resources><Grid><ItemsControl ItemsSource="{Binding Source={StaticResource namedColor},
                                            Path=All}"><ItemsControl.ItemTemplate><DataTemplate><Border BorderBrush="{Binding RelativeSource={RelativeSource TemplatedParent},
                                                  Path=Foreground}"
                            BorderThickness="2"
                            Margin="2"><Border.Background><SolidColorBrush Color="{Binding Color}" /></Border.Background><Viewbox><TextBlock Text="{Binding Name}"
                                       HorizontalAlignment="Center"
                                       VerticalAlignment="Center"><TextBlock.Foreground><SolidColorBrush Color="{Binding Color,
                                                Converter={StaticResource colorConverter}}" /></TextBlock.Foreground></TextBlock></Viewbox></Border></DataTemplate></ItemsControl.ItemTemplate><ItemsControl.ItemsPanel><ItemsPanelTemplate><UniformGrid /></ItemsPanelTemplate></ItemsControl.ItemsPanel></ItemsControl></Grid></Window>

この XAML は、組み込みスタイルと「ch11:UniformGrid」を除けば WinRT XAML と同じになります。次にモノクロ諧調を計算する Petzold.ProgrammingWindows6.Chapter11 プロジェクトの ColorToContrastColorConverter.cs を示します。

using System;
using System.Windows.Data;
using System.Windows.Media;

namespace Petzold.ProgrammingWindows6.Chapter11
{
    public class ColorToContrastColorConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            Color clr = (Color)value;
            double grayShade = 0.30 * clr.R + 0.59 * clr.G + 0.11 * clr.B;
            return grayShade > 128 ? Colors.Black : Colors.White;
        }

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

この コードは、Convert と ConvertBack メソッドの第4引数を除けば WinRT XAML と同じになります。それでは、実行結果を示します。
AllColorsItemsControl

実は、Petzold.ProgrammingWindows6.Chapter11 プロジェクトに定義されている UniformGrid も WPF 向けに移植してあります(UniformGrid2 クラス)。でも、ウィンドウ サイズをリサイズした場合の表示がうまく行っていません。多分、MeasureOverride メソッドなどで適切なサイズを計算できていないのが原因だと思われます。書籍を WPF XAML に置き換えての説明という観点では標準の UniformGrid で目的を達成できていますので、興味がある方が UniformGrid2 クラスを完成させて頂ければと思います。完成したならば、どこかに記事を掲載してください。そうすることで、WPF を学ぶ人にとって有益なものになることでしょう。

今度は、Border 要素と TextBlock 要素にサイズ指定した ListBoxWithUniformGrid プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><ch11:NamedColor x:Key="namedColor" /><ch11:ColorToContrastColorConverter x:Key="colorConverter" /></Window.Resources><Grid><ListBox ItemsSource="{Binding Source={StaticResource namedColor},
                                       Path=All}"><ListBox.ItemTemplate><DataTemplate><Border BorderBrush="{Binding RelativeSource={RelativeSource TemplatedParent},
                                                  Path=Foreground}"
                            Width="288"
                            Height="72"
                            BorderThickness="3"
                            Margin="3"><Border.Background><SolidColorBrush Color="{Binding Color}" /></Border.Background><TextBlock Text="{Binding Name}"
                                       FontSize="24"
                                       HorizontalAlignment="Center"
                                       VerticalAlignment="Center"><TextBlock.Foreground><SolidColorBrush Color="{Binding Color,
                                                Converter={StaticResource colorConverter}}" /></TextBlock.Foreground></TextBlock></Border></DataTemplate></ListBox.ItemTemplate><ListBox.ItemsPanel><ItemsPanelTemplate><UniformGrid /></ItemsPanelTemplate></ListBox.ItemsPanel></ListBox></Grid></Window>

この XAML は、AllCorlorsItemsControl と同じ箇所だけを WPF XAML 用に変更しています。Border に Width と Height を指定した結果は、WinRT XAML と同じで正しく表示されています。そでは、実行結果を示します。
ListBoxWithUniformGrid

ListBoxWithUniformGrid プロジェクト で、UniformGrid を Petzold.ProgrammingWindows6.Chapter11 プロジェクトに定義されている UniformGrid2 に置き換える場合は、Rows か Columns プロパティを明示的に指定することで正常に動作します。つまり、書籍にも記述されていますが利用可能な幅と子要素の最大幅に基づいて、列の数を割り出すロジックなどにおいて WinRT XAML との違いがあることになります。これらの問題を解消すれば、WPF XAML 用のカスタム パネルである UniformGrid2 を完成させることができることでしょう。書籍では、スクロールの切り替え方なども説明していますので、書籍の熟読をお願いします。

11.8(P546) アイテム テンプレートによる棒グラフ

本節では、表題の通りアイテム テンプレートを使って棒グラフを描画するアイテム テンプレートを説明しています。それでは、RgbBarChart プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><ch11:NamedColor x:Key="namedColor" /><ch11:ByteToHexStringConverter x:Key="byteToHex" /></Window.Resources><Grid><ItemsControl ItemsSource="{Binding Source={StaticResource namedColor},
                                            Path=All}"><ItemsControl.ItemTemplate><DataTemplate><StackPanel Name="stackPanel"
                                Height="765"
                                RenderTransformOrigin="0.5 0.5"
                                Margin="1 0" ><StackPanel.RenderTransform><ScaleTransform ScaleY="-1" /></StackPanel.RenderTransform><Rectangle Fill="Red" 
                                   Height="{Binding Color.R}" /><Rectangle Fill="Green"
                                   Height="{Binding Color.G}" /><Rectangle Fill="Blue"
                                   Height="{Binding Color.B}" /><ToolTipService.ToolTip><ToolTip x:Name="tooltip"
                                     PlacementTarget="{Binding ElementName=stackPanel}"><Grid><StackPanel ><TextBlock Text="{Binding Name}"
                                                   HorizontalAlignment="Center" /><StackPanel DataContext="{Binding Color}" 
                                                    Orientation="Horizontal"
                                                    HorizontalAlignment="Center"><TextBlock Text="R=" /><TextBlock Text="{Binding R}" /><TextBlock Text=" G=" /><TextBlock Text="{Binding G}" /><TextBlock Text=" B=" /><TextBlock Text="{Binding B}" /></StackPanel></StackPanel></Grid></ToolTip></ToolTipService.ToolTip></StackPanel></DataTemplate></ItemsControl.ItemTemplate><ItemsControl.ItemsPanel><ItemsPanelTemplate><UniformGrid Rows="1" /></ItemsPanelTemplate></ItemsControl.ItemsPanel></ItemsControl></Grid></Window>

この XAML は、組み込みのスタイルと Grid の DataContext、StackPanel の DataContext を除けば WinRT XAML と同じになります。この DataContext の指定方法は、WinRT XAML と WPF XAML で異なる動作をするものになっています。それでは、実行結果を示します。
RgbBarChart

結論から言えば、WPF XAML のアイテム テンプレートではバインディングされたアイテムが ToolTip であっても自動的に反映されるために「Grid DataContext="{Binding ElementName=tooltip, Path=PlacementTarget}"」を記述してはいけません。記述してしまうと、記述に従って StackPanel へバインディングされるために何も表示されなくなります。一方で WinRT XAML では、Tooltip は表示を行う時にバインディングが解決されるような動きをしてい���す。つまり、「Grid DataContext="{Binding ElementName=tooltip, Path=PlacementTarget}"」の記述によって ToolTip を表示する時に StackPanel の DataContext へとデータソースが解決されます。この動きによって、期待通りに ToolTip が表示されるようになります。この動きの違いは、WPF XAML はフルスタックのフレームワークであり ToolTip を含めて統一的にデータソースが解決されるからであり、WinRT XAML では必要がないものは必要とされるまで解決しない(実行速度を優先)という考え方に基づいたサブセットと考えると理解し易いかも知れません。この RdbBarChart プロジェクトの WPF XAML 版の欠点は、グラフの大きさが固定されているのでウィンドウのリサイズに対応できないことです。この点については、グラフの高さを計算するロジックにウィンドウ サイズを加味し、サイズ変更イベントの処理を組み合わせれば解決することができますので、自分で挑戦してみてください。もちろん、書籍にあるように RenderTransform プロパティで調整するのでも構いません。

11.9(P548) FlipView コントロール

本節では、WinRT XAML に導入された FlipView コントロールを使ってアイテム テンプレートを使用する方法を説明しています。従って、WPF XAML ではアイテム テンプレートの説明を別のコントロールに流用することはできますが、WinRT の FlipView のようなコントロールを使いたいとすると、自分で作成するか、サードパーティー製のコントロールを使用することになります。たとえば、DevExpressの製品などになります。

11.10(P551) 基本的なコントロール テンプレート

本節では、DataTemplate、ItemsPanelTemplate に続く 3つ目のテンプレートとして ControlTemplate の説明をしています。ControlTemplate の説明として、Style に始まり、ControlTemplate へと続き、TemplateBinding を説明し、ContentPresenter の説明へと続きます。ほとんどの説明が、WPF XAML にも利用できますので、書籍の熟読をお願いします。

11.11(P562) ビジュアル状態マネージャー

本節では、コントロールがユーザー操作に反応するフィードバックを定義するビジュアル状態マネージャー(VisualStateManager)の説明をしています。この説明のために Button コントロールを使用して、7つのビジュアル状態を説明し、2つのグループに分類されることを説明しています。

  • CommonStates
    Normal、MouseOrver、Pressed、Disabled
  • FocusStates (WinRT では、FocusedStates)
    Forcused、Unforcused

WinRT XAML と WPF XAML では、ビジュアル状態の状態名が少し異なっています。具体例を次に示します。

WinRTWPFグループ
PointOrverMouseOrverCommonStates
PointerFocused無しFocusStates

この点に注意すれば、WinRT XAML のサンプルや WPF XAML のサンプルを相互に書き換えることができます。書籍では、これらのビジュアル状態の定義と ContentPresenter の使い方を説明してから、カスタム ボタンを定義した全体像を示します。それでは、CustomButtonTemplate プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Window.Resources><ControlTemplate x:Key="buttonTemplate" TargetType="Button"><Grid><VisualStateManager.VisualStateGroups><VisualStateGroup x:Name="CommonStates"><VisualState x:Name="Normal" /><VisualState x:Name="MouseOver" /><VisualState x:Name="Pressed"><Storyboard><ColorAnimationUsingKeyFrames Storyboard.TargetName="border"
                                                               Storyboard.TargetProperty="(Background).(SolidColorBrush.Color)"><EasingColorKeyFrame KeyTime="0" Value="LightGray" /></ColorAnimationUsingKeyFrames><!-- border に TextBlock.Foreground を追加する必要がある
                                     ContentPresenter に Foreground がないため--><ColorAnimationUsingKeyFrames Storyboard.TargetName="border"
                                                               Storyboard.TargetProperty="(TextBlock.Foreground).(SolidColorBrush.Color)"><EasingColorKeyFrame KeyTime="0" Value="Black" /></ColorAnimationUsingKeyFrames></Storyboard></VisualState><VisualState x:Name="Disabled"><Storyboard><ObjectAnimationUsingKeyFrames Storyboard.TargetName="disabledRect"
        							Storyboard.TargetProperty="(UIElement.Visibility)"><DiscreteObjectKeyFrame KeyTime="0"
        								Value="{x:Static Visibility.Visible}" /></ObjectAnimationUsingKeyFrames></Storyboard></VisualState></VisualStateGroup><VisualStateGroup x:Name="FocusStates"><VisualState x:Name="Unfocused"/><VisualState x:Name="Focused"><Storyboard><DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="focusRectangle"><EasingDoubleKeyFrame KeyTime="0" Value="1"/></DoubleAnimationUsingKeyFrames></Storyboard></VisualState></VisualStateGroup></VisualStateManager.VisualStateGroups><Border x:Name="border"
        			Background="{TemplateBinding Background}"
        			BorderBrush="{TemplateBinding BorderBrush}"
        			BorderThickness="{TemplateBinding BorderThickness}"
                            TextBlock.Foreground="{TemplateBinding Foreground}"
        			CornerRadius="12"><Grid x:Name="grid"><ContentPresenter x:Name="contentPresenter"
        					Content="{TemplateBinding Content}"
        					ContentTemplate="{TemplateBinding ContentTemplate}"
        					Margin="{TemplateBinding Padding}"
        					HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
        					VerticalAlignment="{TemplateBinding VerticalContentAlignment}" /><Rectangle x:Name="focusRectangle"
        					Stroke="{TemplateBinding Foreground}"
        					Opacity="0"
        					StrokeThickness="1"
        					StrokeDashArray="2 2"
        					Margin="4"
        					RadiusX="12"
        					RadiusY="12" /></Grid></Border><Rectangle x:Name="disabledRect"
        			Visibility="Collapsed"
        			Fill="Black"
        			Opacity="0.5" /></Grid></ControlTemplate><Style x:Key="buttonStyle" TargetType="Button"><Setter Property="Background" Value="White" /><Setter Property="Foreground" Value="Blue" /><Setter Property="BorderBrush" Value="Red" /><Setter Property="BorderThickness" Value="3" /><Setter Property="FontSize" Value="24" /><Setter Property="Padding" Value="12" /><Setter Property="Template" Value="{StaticResource buttonTemplate}" /></Style></Window.Resources><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><Button Content="Disable center button"
                Grid.Column="0"
                Style='{StaticResource buttonStyle}'
                Click='OnButton1Click'
                HorizontalAlignment='Center'
                VerticalAlignment='Center' /><Button x:Name='centerButton'
                Content='Center button'
                Grid.Column='1'
                Style='{StaticResource buttonStyle}'
                FontSize='48'
                Background='DarkGray'
                Foreground='Red'
                HorizontalAlignment='Center'
                VerticalAlignment='Center' /><Button Content='Enable center button'
                Grid.Column='2'
                Style='{StaticResource buttonStyle}'
                Click='OnButton3Click'
                HorizontalAlignment='Center'
                VerticalAlignment='Center' /></Grid></Window>

この XAML は、既に説明したビジュアル状態の他にもアニメーションなどの数か所を変更しています。変更箇所を次に示します。

  • CommonStates の PointOver を MouseOrver に変更。
  • CommonStates の Pressed の Storybord 内のアニメーションを変更。
    ObjectAnimationUsingKeyFrame を ColorAnimationUsingKeyFrames へ変更。
    TargetProperty 添付プロパティを「(Background).(SolidColorBrush.Color)」表記へ変更。
    TargetProperty 添付プロパティを「(TextBlock.Foreground).(SolidColorBrush.Color)」表記へ変更(WPF XAML の ContentPresenter は Foreground プロパティを持たないため)。
    DiscreteObjectKeyFrame を EasingColorKeyFrame へ変更。
  • CommonStates の Disabled の Storyboard 内のアニメーションを変更。
    TargetProperty 添付プロパティを「(UIElement.Visibility)」 へ変更。
    DiscreteObjectKeyFrame の Value を「{x:Static Visibility.Visible}」へ変更。
  • ForcusedStates を ForcusStates に変更。
  • ForcusStates の Forcused の storyboard 内のアニメーションを変更。
    DoubleAnimation を DoubleAnimationUsingKeyFrames に変更。
    TargetProperty 添付プロパティを「(UIElement.Opacity)」表記に変更。
  • Border へ 「TextBlock.Foreground="{TemplateBinding Foreground}"」 要素を追加。
    WPF XAML の ContentPresenter が Foreground プロパティを持たないため。
  • 組み込みスタイルを変更

変更箇所を見れば、ビジュアル状態の差異による変更とアニメーションの変更であることを理解できることでしょう。ContentPresenter クラスに関しては、Foreground プロパティを WinRT XAML が持っており、WPF XAML が持たないことによる変更を加えています。この理由が、Border 要素に TextBlock.Forground 添付プロパティを追加した理由であり、Button の Content にテキストを設定した時に使われるのが TextBlock であることを利用して添付プロパティにしています。ボタンをクリックした時のイベント ハンドラーを MainWindow.xaml.cs より抜粋して示します。

private void OnButton1Click(object sender, RoutedEventArgs e)
{
    centerButton.IsEnabled = false;
}

private void OnButton3Click(object sender, RoutedEventArgs e)
{
    centerButton.IsEnabled = true;
}

このコードは、WinRT XAML と同じになります。それでは、実行結果を示します。
CustomButtonTemplate

スクリーン ショットを見れば、フォーカスを取得した場合に点線がボタン内に表示されていることに気が付くことでしょう。これが、ビジュアル状態として定義した Focused のアニメーションによる効果になります。このようなコントロール テンプレートは、理解できるまでは複雑に思えますから、書籍を参考にしながら色々と試して学習することをお勧めします。でも、全ての人がコントロール テンプレートを定義する必要があるかと言えば、カスタム コントロールを作成するなどの一部の人に限られることでしょう。でも、コントロール テンプレートの仕組みを知っていることは、とても大切なことです。

11.12(P571) generic.xaml の使用

本節では、Windows ストア アプリ プロジェクトに含まれるスタイル シートを説明しています。Visual Studio 2012 のプロジェクトでは、StandardStyle.xaml が含まれており、Visual Studio 2013 のプロジェクトには StandardStyle.xaml は含まれていません。StandrdStyle.xaml は generic.xaml から作成されており、Visual Studio 2013 のプロジェクトをビルド時に generic.xaml が組み込まれるようになっています。これらのスタイル シートが、どのような役目を持っているかを説明しています。

WPF XAML では、カスタム コントロールを作成する場合に generic.xaml というスタイル シートが必要になります。Visual Studio でカスタム コントロール テンプレートを使��て新しいアイテムを追加すれば、プロジェクト内に Themes フォルダが作成されて、その中に Generic.xaml というスタイル シートが作成されます。従って、カスタム コントロールを作成する場合に必要になるスタイル シートであると理解していただければ結構です。

11.13(P571) テンプレートのパーツ

本節では、コントロール テンプレートがどのような構造になっているかという観点でコントロールが含む部品である構成要素をパーツとして説明しています。その過程で、OnApplyTemplate メソッドや GetTemplateChild メソッドなども説明しています。OnApplyTemplate メソッドは public メソッドですが、GetTemplateChild メソッドが protected メソッドである点に注意してください。つまり、GetTemplateChild メソッドでコントロールのパーツへアクセスするには、対象のコントロールを継承したクラスを作成して、作成したクラス内で GetTemplateChild メソッドを使用しなければならないからです。

書籍では、具体例として Slider コントロールに HorizontalTemplate と VerticalTemplate という名前を付けてください。という説明があり、Slider のコントロール テンプレートをカスタマイズした BareBonesSilder プロジェクトの説明になります。ここで知っておかないといけない事は、HorizontalTemplate と VerticalTemplate という名前をどこから入手したかということです。最初に思いつくのが、ドキュメントに記述されているのではないかということです。事実として、ドキュメントに記述されているものもあります。

WPF XAML の Slider には、HorizontalTemplate と VerticalTemplate という名前は定義されていませんので、BoreBonesSlider プロジェクトと同じ方法を使ったテンプレートの定義方法は使えません。この理由は、テンプレートのパーツの構成が異なるからです。プログラミング Windows の著者であるペゾルドは、BoreBonesSlider プロジェクトの着想を MSDN マガジンの「テンプレートを使用した WPF コントロールのカスタマイズ」という記事にあることが、書籍の中の注記に記述されています。この記事の中に、BareBonesProgressBar サンプルがあります。このサンプルは、プログレス バーのテンプレートをカスタマイズするというものですが、このサンプルを Visual Studio 2013 へ移植したものの実行結果を示します。
BareBonesProgressBar

このサンプルのコードを理解することで、どのようにカスタマイズができるかを理解できることでしょう。

書籍では、BareBonesSlider プロジェクトに手を加えることでスライダーをバネにした SpringLoaderSlider の説明をしています。このサンプルも、MSDN マガジンの「テンプレートを使用した WPF コントロールのカスタマイズ」という記事に原点があります。従って、この記事の SpringLoadedScrollBar サンプルを Visual Studio 2013 へ移植したものの実行結果を示します(このサンプルは、ProgressBar をカスタマイズしています)。
SpringLoadedScrollBar

書籍のサンプルと違う点は、スライダーの向きを縦方向にするテンプレートが含まれていない点になりますが、スライダー をバネ状にカスタマイズするという観点では同じものになりますから、コントロールのカスタマイズという観点では有益なものになることでしょう。

書籍では、SpeedMeterProgressBar プロジェクトを使ってスライダーと速度計(プログレス バー)を使うサンプルの説明をしています。このサンプルも、MSDN マガジンの「テンプレートを使用した WPF コントロールのカスタマイズ」という記事に原点があります。従って、この記事の SpeedMeterProgressBar サンプルを Visual Studio 2013 へ移植したものの実行結果を示します。
SpeedometerProgressBar

書籍にも記述がありますが、MSDN マガジンの「テンプレートを使用した WPF コントロールのカスタマイズ」という記事を原点に WinRT XAML へ移植しているので、もとの記事を読むことが役立つことでしょう。

この節の最初で説明しましたが、標準コントロールのスタイルやテンプレートを学習するには、どうしたら良いでしょうか。ドキュメントを探すのも 1つの手法ですが、全てのコントロールに対してスタイルやドキュメントが記述されているわけではありません。このような場合に行うとしたら、Blend for Visual Studio や Visual Studio を使ったテンプレートの編集機能を使用することをお勧めします。テンプレート編集をビジュアルに行うことができるので、ここでは Blend を使ったテンプレート編集の方法を説明します。最初に、Blend でプロジェクトを開きます。次にテンプレートを編集したいコントロールを選択して、コンテキスト メニューから[テンプレートの編集]-[コピーして編集]を選択します。この方法は、WinRT XAML も WPF XAML でも同じです。
ch11 blend1
そうすると、ダイアログが表示されますので、名前と定義先を指定して「OK」ボタンをクリックします。定義先とは、作成するスタイルを定義するリソースの場所になります。アプリケーションとは、App.xaml を意味し、このドキュメントが現在編集している Window や Page になります。
ch11 blend2

スタイル リソースが作成されると、スタイルの編集画面に切り替わります。
ch11 blend3

編集画面では、大きくは次に示す 2つの作業を行います。

  • 状態パネル
    ビジュアル状態を定義するために使用します。ビジュアル状態の名前を知らなくても、状態パネルを使って指定して、アニメーションの自動記録などを使うことでタイムラインを自動生成することができます。
  • オブジェクトとタイムライン
    ドキュメント ツリーを開いていくことで、目的となるオブジェクトを編集したりすることができます。
    ch11 blend4

このサンプルでは、書籍と同じように Slider コントロール(WPF XAML)のテンプレートを編集しています。参考までに作成された、スタイルとテンプレートを示します。

<Window.Resources><SolidColorBrush x:Key="SliderThumb.Static.Foreground" Color="#FFE5E5E5"/><SolidColorBrush x:Key="SliderThumb.MouseOver.Background" Color="#FFDCECFC"/><SolidColorBrush x:Key="SliderThumb.MouseOver.Border" Color="#FF7Eb4EA"/><SolidColorBrush x:Key="SliderThumb.Pressed.Background" Color="#FFDAECFC"/><SolidColorBrush x:Key="SliderThumb.Pressed.Border" Color="#FF569DE5"/><SolidColorBrush x:Key="SliderThumb.Disabled.Background" Color="#FFF0F0F0"/><SolidColorBrush x:Key="SliderThumb.Disabled.Border" Color="#FFD9D9D9"/><SolidColorBrush x:Key="SliderThumb.Static.Background" Color="#FFF0F0F0"/><SolidColorBrush x:Key="SliderThumb.Static.Border" Color="#FFACACAC"/><ControlTemplate x:Key="SliderThumbHorizontalTop" TargetType="{x:Type Thumb}"><Grid HorizontalAlignment="Center" UseLayoutRounding="True" VerticalAlignment="Center"><Path x:Name="grip" Data="M 0,6 C0,6 5.5,0 5.5,0 5.5,0 11,6 11,6 11,6 11,18 11,18 11,18 0,18 0,18 0,18 0,6 0,6 z" Fill="{StaticResource SliderThumb.Static.Background}" Stretch="Fill" SnapsToDevicePixels="True" Stroke="{StaticResource SliderThumb.Static.Border}" StrokeThickness="1" UseLayoutRounding="True" VerticalAlignment="Center"/></Grid><ControlTemplate.Triggers><Trigger Property="IsMouseOver" Value="true"><Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Background}"/><Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Border}"/></Trigger><Trigger Property="IsDragging" Value="true"><Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Background}"/><Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Border}"/></Trigger><Trigger Property="IsEnabled" Value="false"><Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Background}"/><Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Border}"/></Trigger></ControlTemplate.Triggers></ControlTemplate><ControlTemplate x:Key="SliderThumbHorizontalBottom" TargetType="{x:Type Thumb}"><Grid HorizontalAlignment="Center" UseLayoutRounding="True" VerticalAlignment="Center"><Path x:Name="grip" Data="M 0,12 C0,12 5.5,18 5.5,18 5.5,18 11,12 11,12 11,12 11,0 11,0 11,0 0,0 0,0 0,0 0,12 0,12 z" Fill="{StaticResource SliderThumb.Static.Background}" Stretch="Fill" SnapsToDevicePixels="True" Stroke="{StaticResource SliderThumb.Static.Border}" StrokeThickness="1" UseLayoutRounding="True" VerticalAlignment="Center"/></Grid><ControlTemplate.Triggers><Trigger Property="IsMouseOver" Value="true"><Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Background}"/><Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Border}"/></Trigger><Trigger Property="IsDragging" Value="true"><Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Background}"/><Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Border}"/></Trigger><Trigger Property="IsEnabled" Value="false"><Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Background}"/><Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Border}"/></Trigger></ControlTemplate.Triggers></ControlTemplate><SolidColorBrush x:Key="SliderThumb.Track.Border" Color="#FFD6D6D6"/><SolidColorBrush x:Key="SliderThumb.Track.Background" Color="#FFE7EAEA"/><Style x:Key="RepeatButtonTransparent" TargetType="{x:Type RepeatButton}"><Setter Property="OverridesDefaultStyle" Value="true"/><Setter Property="Background" Value="Transparent"/><Setter Property="Focusable" Value="false"/><Setter Property="IsTabStop" Value="false"/><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="{x:Type RepeatButton}"><Rectangle Fill="{TemplateBinding Background}" Height="{TemplateBinding Height}" Width="{TemplateBinding Width}"/></ControlTemplate></Setter.Value></Setter></Style><ControlTemplate x:Key="SliderThumbHorizontalDefault" TargetType="{x:Type Thumb}"><Grid HorizontalAlignment="Center" UseLayoutRounding="True" VerticalAlignment="Center"><Path x:Name="grip" Data="M 0,0 C0,0 11,0 11,0 11,0 11,18 11,18 11,18 0,18 0,18 0,18 0,0 0,0 z" Fill="{StaticResource SliderThumb.Static.Background}" Stretch="Fill" SnapsToDevicePixels="True" Stroke="{StaticResource SliderThumb.Static.Border}" StrokeThickness="1" UseLayoutRounding="True" VerticalAlignment="Center"/></Grid><ControlTemplate.Triggers><Trigger Property="IsMouseOver" Value="true"><Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Background}"/><Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Border}"/></Trigger><Trigger Property="IsDragging" Value="true"><Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Background}"/><Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Border}"/></Trigger><Trigger Property="IsEnabled" Value="false"><Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Background}"/><Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Border}"/></Trigger></ControlTemplate.Triggers></ControlTemplate><ControlTemplate x:Key="SliderHorizontal" TargetType="{x:Type Slider}"><Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True"><Grid><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto" MinHeight="{TemplateBinding MinHeight}"/><RowDefinition Height="Auto"/></Grid.RowDefinitions><TickBar x:Name="TopTick" Fill="{TemplateBinding Foreground}" Height="4" Margin="0,0,0,2" Placement="Top" Grid.Row="0" Visibility="Collapsed"/><TickBar x:Name="BottomTick" Fill="{TemplateBinding Foreground}" Height="4" Margin="0,2,0,0" Placement="Bottom" Grid.Row="2" Visibility="Collapsed"/><Border x:Name="TrackBackground" BorderBrush="{StaticResource SliderThumb.Track.Border}" BorderThickness="1" Background="{StaticResource SliderThumb.Track.Background}" Height="4.0" Margin="5,0" Grid.Row="1" VerticalAlignment="center"><Canvas Margin="-6,-1"><Rectangle x:Name="PART_SelectionRange" Fill="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" Height="4.0" Visibility="Hidden"/></Canvas></Border><Track x:Name="PART_Track" Grid.Row="1"><Track.DecreaseRepeatButton><RepeatButton Command="{x:Static Slider.DecreaseLarge}" Style='{StaticResource RepeatButtonTransparent}'/></Track.DecreaseRepeatButton><Track.IncreaseRepeatButton><RepeatButton Command='{x:Static Slider.IncreaseLarge}' Style='{StaticResource RepeatButtonTransparent}'/></Track.IncreaseRepeatButton><Track.Thumb><Thumb x:Name='Thumb' Focusable='False' Height='18' OverridesDefaultStyle='True' Template='{StaticResource SliderThumbHorizontalDefault}' VerticalAlignment='Center' Width='11'/></Track.Thumb></Track></Grid></Border><ControlTemplate.Triggers><Trigger Property='TickPlacement' Value='TopLeft'><Setter Property='Visibility' TargetName='TopTick' Value='Visible'/><Setter Property='Template' TargetName='Thumb' Value='{StaticResource SliderThumbHorizontalTop}'/><Setter Property='Margin' TargetName='TrackBackground' Value='5,2,5,0'/></Trigger><Trigger Property='TickPlacement' Value='BottomRight'><Setter Property='Visibility' TargetName='BottomTick' Value='Visible'/><Setter Property='Template' TargetName='Thumb' Value='{StaticResource SliderThumbHorizontalBottom}'/><Setter Property='Margin' TargetName='TrackBackground' Value='5,0,5,2'/></Trigger><Trigger Property='TickPlacement' Value='Both'><Setter Property='Visibility' TargetName='TopTick' Value='Visible'/><Setter Property='Visibility' TargetName='BottomTick' Value='Visible'/></Trigger><Trigger Property='IsSelectionRangeEnabled' Value='true'><Setter Property='Visibility' TargetName='PART_SelectionRange' Value='Visible'/></Trigger><Trigger Property='IsKeyboardFocused' Value='true'><Setter Property='Foreground' TargetName='Thumb' Value='Blue'/></Trigger></ControlTemplate.Triggers></ControlTemplate><ControlTemplate x:Key='SliderThumbVerticalLeft' TargetType='{x:Type Thumb}'><Grid HorizontalAlignment='Center' UseLayoutRounding='True' VerticalAlignment='Center'><Path x:Name='grip' Data='M 6,11 C6,11 0,5.5 0,5.5 0,5.5 6,0 6,0 6,0 18,0 18,0 18,0 18,11 18,11 18,11 6,11 6,11 z' Fill='{StaticResource SliderThumb.Static.Background}' Stretch='Fill' Stroke='{StaticResource SliderThumb.Static.Border}'/></Grid><ControlTemplate.Triggers><Trigger Property='IsMouseOver' Value='true'><Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.MouseOver.Background}'/><Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.MouseOver.Border}'/></Trigger><Trigger Property='IsDragging' Value='true'><Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.Pressed.Background}'/><Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.Pressed.Border}'/></Trigger><Trigger Property='IsEnabled' Value='false'><Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.Disabled.Background}'/><Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.Disabled.Border}'/></Trigger></ControlTemplate.Triggers></ControlTemplate><ControlTemplate x:Key='SliderThumbVerticalRight' TargetType='{x:Type Thumb}'><Grid HorizontalAlignment='Center' UseLayoutRounding='True' VerticalAlignment='Center'><Path x:Name='grip' Data='M 12,11 C12,11 18,5.5 18,5.5 18,5.5 12,0 12,0 12,0 0,0 0,0 0,0 0,11 0,11 0,11 12,11 12,11 z' Fill='{StaticResource SliderThumb.Static.Background}' Stretch='Fill' Stroke='{StaticResource SliderThumb.Static.Border}'/></Grid><ControlTemplate.Triggers><Trigger Property='IsMouseOver' Value='true'><Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.MouseOver.Background}'/><Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.MouseOver.Border}'/></Trigger><Trigger Property='IsDragging' Value='true'><Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.Pressed.Background}'/><Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.Pressed.Border}'/></Trigger><Trigger Property='IsEnabled' Value='false'><Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.Disabled.Background}'/><Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.Disabled.Border}'/></Trigger></ControlTemplate.Triggers></ControlTemplate><ControlTemplate x:Key='SliderThumbVerticalDefault' TargetType='{x:Type Thumb}'><Grid HorizontalAlignment='Center' UseLayoutRounding='True' VerticalAlignment='Center'><Path x:Name='grip' Data='M0.5,0.5 L18.5,0.5 18.5,11.5 0.5,11.5z' Fill='{StaticResource SliderThumb.Static.Background}' Stretch='Fill' Stroke='{StaticResource SliderThumb.Static.Border}'/></Grid><ControlTemplate.Triggers><Trigger Property='IsMouseOver' Value='true'><Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.MouseOver.Background}'/><Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.MouseOver.Border}'/></Trigger><Trigger Property='IsDragging' Value='true'><Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.Pressed.Background}'/><Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.Pressed.Border}'/></Trigger><Trigger Property='IsEnabled' Value='false'><Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.Disabled.Background}'/><Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.Disabled.Border}'/></Trigger></ControlTemplate.Triggers></ControlTemplate><ControlTemplate x:Key='SliderVertical' TargetType='{x:Type Slider}'><Border x:Name='border' BorderBrush='{TemplateBinding BorderBrush}' BorderThickness='{TemplateBinding BorderThickness}' Background='{TemplateBinding Background}' SnapsToDevicePixels='True'><Grid><Grid.ColumnDefinitions><ColumnDefinition Width='Auto'/><ColumnDefinition MinWidth='{TemplateBinding MinWidth}' Width='Auto'/><ColumnDefinition Width='Auto'/></Grid.ColumnDefinitions><TickBar x:Name='TopTick' Grid.Column='0' Fill='{TemplateBinding Foreground}' Margin='0,0,2,0' Placement='Left' Visibility='Collapsed' Width='4'/><TickBar x:Name='BottomTick' Grid.Column='2' Fill='{TemplateBinding Foreground}' Margin='2,0,0,0' Placement='Right' Visibility='Collapsed' Width='4'/><Border x:Name='TrackBackground' BorderBrush='{StaticResource SliderThumb.Track.Border}' BorderThickness='1' Background='{StaticResource SliderThumb.Track.Background}' Grid.Column='1' HorizontalAlignment='center' Margin='0,5' Width='4.0'><Canvas Margin='-1,-6'><Rectangle x:Name='PART_SelectionRange' Fill='{DynamicResource {x:Static SystemColors.HighlightBrushKey}}' Visibility='Hidden' Width='4.0'/></Canvas></Border><Track x:Name='PART_Track' Grid.Column='1'><Track.DecreaseRepeatButton><RepeatButton Command='{x:Static Slider.DecreaseLarge}' Style='{StaticResource RepeatButtonTransparent}'/></Track.DecreaseRepeatButton><Track.IncreaseRepeatButton><RepeatButton Command='{x:Static Slider.IncreaseLarge}' Style='{StaticResource RepeatButtonTransparent}'/></Track.IncreaseRepeatButton><Track.Thumb><Thumb x:Name='Thumb' Focusable='False' Height='11' OverridesDefaultStyle='True' Template='{StaticResource SliderThumbVerticalDefault}' VerticalAlignment='Top' Width='18'/></Track.Thumb></Track></Grid></Border><ControlTemplate.Triggers><Trigger Property='TickPlacement' Value='TopLeft'><Setter Property='Visibility' TargetName='TopTick' Value='Visible'/><Setter Property='Template' TargetName='Thumb' Value='{StaticResource SliderThumbVerticalLeft}'/><Setter Property='Margin' TargetName='TrackBackground' Value='2,5,0,5'/></Trigger><Trigger Property='TickPlacement' Value='BottomRight'><Setter Property='Visibility' TargetName='BottomTick' Value='Visible'/><Setter Property='Template' TargetName='Thumb' Value='{StaticResource SliderThumbVerticalRight}'/><Setter Property='Margin' TargetName='TrackBackground' Value='0,5,2,5'/></Trigger><Trigger Property='TickPlacement' Value='Both'><Setter Property='Visibility' TargetName='TopTick' Value='Visible'/><Setter Property='Visibility' TargetName='BottomTick' Value='Visible'/></Trigger><Trigger Property='IsSelectionRangeEnabled' Value='true'><Setter Property='Visibility' TargetName='PART_SelectionRange' Value='Visible'/></Trigger><Trigger Property='IsKeyboardFocused' Value='true'><Setter Property='Foreground' TargetName='Thumb' Value='Blue'/></Trigger></ControlTemplate.Triggers></ControlTemplate><Style x:Key='SliderStyle1' TargetType='{x:Type Slider}'><Setter Property='Stylus.IsPressAndHoldEnabled' Value='false'/><Setter Property='Background' Value='Transparent'/><Setter Property='BorderBrush' Value='Transparent'/><Setter Property='Foreground' Value='{StaticResource SliderThumb.Static.Foreground}'/><Setter Property='Template' Value='{StaticResource SliderHorizontal}'/><Style.Triggers><Trigger Property='Orientation' Value='Vertical'><Setter Property='Template' Value='{StaticResource SliderVertical}'/></Trigger></Style.Triggers></Style></Window.Resources>

このスタイルとコントロール テンプレートが膨大なことを気にしないですください。内容は多いですが、Blend を使うことでビジュアルに編集することが可能だからです。このように、ドキュメントに記述がなくてもツールを使うことで標準コントロールのスタイルとテンプレートの編集を行うことができるようになっています。これも、XAML 系 UI 技術の特徴になっています。

11.14(P580) カスタム コントロール

本節では、カスタム コントロールに適用するスタイルやテンプレートが Themes フォルダーの Generic.xaml というスタイル シートに記述されなければならないことを説明しています。このことを説明するために Petzold.ProgrammingWindows6.Chapter11 プロジェクトにカスタム コントロールを作るという説明になっています。それでは、Petzold.ProgrammingWindows6.Chapter11 プロジェクトの NewToggle.cs を示します。

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

namespace Petzold.ProgrammingWindows6.Chapter11
{
    public class NewToggle : ContentControl
    {
        public event EventHandler IsCheckedChanged;
        Button uncheckButton, checkButton;

        static NewToggle()
        {
            CheckedContentProperty = DependencyProperty.Register("CheckedContent",
                typeof(object),
                typeof(NewToggle),
                new PropertyMetadata(null));
            IsCheckedProperty = DependencyProperty.Register("IsChecked",
                typeof(bool),
                typeof(NewToggle),
                new PropertyMetadata(false, OnIsCheckedChanged));
        }

        public NewToggle()
        {
            this.DefaultStyleKey = typeof(NewToggle);
        }

        public static DependencyProperty CheckedContentProperty { private set; get; }

        public static DependencyProperty IsCheckedProperty { private set; get; }
        public object CheckedContent
        {
            set { SetValue(CheckedContentProperty, value); }
            get { return GetValue(CheckedContentProperty); }
        }

        public bool IsChecked
        {
            set { SetValue(IsCheckedProperty, value); }
            get { return (bool)GetValue(IsCheckedProperty); }
        }

        // protected をpublic
        public override void OnApplyTemplate()
        {
            if (uncheckButton != null)
                uncheckButton.Click -= OnButtonClick;
            if (checkButton != null)
                checkButton.Click -= OnButtonClick;
            uncheckButton = GetTemplateChild("UncheckButton") as Button;
            checkButton = GetTemplateChild("CheckButton") as Button;
            if (uncheckButton != null)
                uncheckButton.Click += OnButtonClick;
            if (checkButton != null)
                checkButton.Click += OnButtonClick;
            base.OnApplyTemplate();
        }

        void OnButtonClick(object sender, RoutedEventArgs args)
        {
            this.IsChecked = sender == checkButton;
        }

        static void OnIsCheckedChanged(DependencyObject obj,
                                     DependencyPropertyChangedEventArgs args)
        {
            (obj as NewToggle).OnIsCheckedChanged(EventArgs.Empty);
        }

        protected virtual void OnIsCheckedChanged(EventArgs args)
        {
            VisualStateManager.GoToState(this,
                                         this.IsChecked ? "Checked" : "Unchecked",
                                         true);
            if (IsCheckedChanged != null)
                IsCheckedChanged(this, args);
        }
    }
}

このコードは、OnApplyTemplate メソッドのアクセシビリティを protected から public へ変更しただけになります。これは、WinRT XAML と WPF XAML で OnApplyTemplate メソッドのアクセシビリティが異なることが理由です。それでは、Themes フォルダーの Generic.xaml を示します。

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Petzold.ProgrammingWindows6.Chapter11"><Style TargetType="local:NewToggle"><Setter Property="BorderBrush" Value="Black" /><Setter Property="BorderThickness" Value="1" /><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="local:NewToggle"><Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"><VisualStateManager.VisualStateGroups><VisualStateGroup x:Name="CheckStates"><VisualState x:Name="Unchecked" /><VisualState x:Name="Checked"><Storyboard><ThicknessAnimationUsingKeyFrames 
                                                Storyboard.TargetName="UncheckButton"
                                                Storyboard.TargetProperty="(Border.BorderThickness)"><DiscreteThicknessKeyFrame KeyTime="0"
                                                                       Value="0" /></ThicknessAnimationUsingKeyFrames><ThicknessAnimationUsingKeyFrames 
                                                Storyboard.TargetName="CheckButton"
                                                Storyboard.TargetProperty="(Border.BorderThickness)"><DiscreteThicknessKeyFrame KeyTime="0"
                                                                    Value="8" />                                        </ThicknessAnimationUsingKeyFrames></Storyboard></VisualState></VisualStateGroup></VisualStateManager.VisualStateGroups><UniformGrid Rows="1"><Button Name="UncheckButton"
                                    Content="{TemplateBinding Content}"
                                    ContentTemplate="{TemplateBinding ContentTemplate}"
                                    FontSize="{TemplateBinding FontSize}"
                                    BorderBrush="Red"
                                    BorderThickness="8"
                                    HorizontalAlignment="Stretch" /><!-- Content="{TemplateBinding CheckedContent}" --><Button Name="CheckButton"
                                    Content="{Binding RelativeSource={RelativeSource AncestorType=local:NewToggle}, Path=CheckedContent}"
                                    ContentTemplate="{TemplateBinding ContentTemplate}"
                                    FontSize="{TemplateBinding FontSize}"
                                    BorderBrush="Green"
                                    BorderThickness="0"
                                    HorizontalAlignment="Stretch" /></UniformGrid></Border></ControlTemplate></Setter.Value></Setter></Style></ResourceDictionary>

このスタイル シートは、次に示す点以外は WinRT XAML と同じになります。

  • 組み込みスタイルを変更。
  • ObjectAnimationUsingKeyFrame を ThicknessAnimationUsingKeyFrames に変更。
    TargetProperty 添付プロパティを 「(Border.BorderThickness)」に変更。
  • local:UniformGrid を UniformGrid に変更。
  • Button の Content を「{Binding RelativeSource={RelativeSource AncestorType=local:NewToggle}, Path=CheckedContent}」に変更。
    WinRT XAML では、作成しているカスタム コントロールの依存関係プロパティを記述できます(CheckedContent)。が、WPF XAML は、このような省略記法が許可されていないので、RelativeSource を使った正式な記述になります。

今度は、NewToggle コントロールを使用する NewToggleDemo プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... 
        xmlns:ch11="clr-namespace:Petzold.ProgrammingWindows6.Chapter11;assembly=Petzold.ProgrammingWindows6.Chapter11"
        ... ><Window.Resources><Style TargetType="ch11:NewToggle"><Setter Property="HorizontalAlignment" Value="Center" /><Setter Property="VerticalAlignment" Value="Center" /></Style></Window.Resources><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><ch11:NewToggle Content="Don't do it!"
                        CheckedContent="Let's go for it!"
                        Grid.Column="0" Margin="20"
                        FontSize="24" /><ch11:NewToggle Grid.Column="1" Margin="20"><ch11:NewToggle.Content><Image Source="Images/MunchScream.jpg" /></ch11:NewToggle.Content><ch11:NewToggle.CheckedContent><Image Source="Images/BotticelliVenus.jpg" /></ch11:NewToggle.CheckedContent></ch11:NewToggle></Grid></Window>

この XAML は、組み込みスタイルを除けば WinRT XAML と同じになります。それでは、実行結果を示します。
NewToggleDemo

ここまでの説明で、コントロールのスタイルとテンプレートというものが、かなりの部分で WinRT XAML と WPF XAML で共通であることが理解できたことでしょう。標準コントロールが持つスタイルの定義そのものは、WinRT XAML と WPF XAML で同じものありますが、Slider コントロールのように異なる考え方で作成されているものもあります。この問題は、提供されるコントロール自体が想定されている利用方法自体が違うことに起因していると考えることができます。この記事で説明した Blend を使用したテンプレートの編集機能を使えば、利用するコントロールのテンプレート編集は容易になることでしょう。考え方は、書籍に記述されていますので、熟読をお願いします。この節の最後としては、ペゾルドの「テンプレートを使用した WPF コントロールのカスタマイズ」という記事に記載されていますが、コントロールのスタイルをダンプするツールが「Applications = Code + Markup」という書籍のサンプルに含まれていますから、このサンプルである DumpControlTemplate を Visual Studio 2013 へ移植したものをサンプルに含めていますので、自分で試してみてください。DumpControlTemplate の実行結果を示します。
DumpControlTemplate

11.15(P586) テンプレートとアイテム コンテナー

本節では、コレクションをバインディングするコントロールで Selector( WinRT XAML では SelectorItem)を継承するコントロールが持つアイテム コンテナーを説明しています。提供するクラス階層などは、WinRT XAML と WPF XAML で異なりますが、考え方は同じなので置き換えて考えることで書籍の説明は両方に当てはまります。それでは、アイテム コンテナー スタイルを指定する CustomListBoxItemStyle プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ...
        xmlns:ch11="clr-namespace:Petzold.ProgrammingWindows6.Chapter11;assembly=Petzold.ProgrammingWindows6.Chapter11"
        ... ><Window.Resources><ch11:NamedColor x:Key="namedColor" /></Window.Resources><Grid><ListBox Name="lstbox"
                 ItemsSource="{Binding Source={StaticResource namedColor},
                                       Path=All}"
                 Width="380"><ListBox.ItemTemplate><DataTemplate>
                    ...</DataTemplate></ListBox.ItemTemplate><ListBox.ItemContainerStyle><Style TargetType="ListBoxItem"><Setter Property="Background" Value="Transparent" /><!--<Setter Property="TabNavigation" Value="True" />--><Setter Property="Padding" Value="8,10" /><Setter Property="HorizontalContentAlignment" Value="Left" /><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="ListBoxItem"><Border Background="{TemplateBinding Background}"
                                        BorderBrush="{TemplateBinding BorderBrush}"
                                        BorderThickness="{TemplateBinding BorderThickness}"><VisualStateManager.VisualStateGroups><VisualStateGroup x:Name="SelectionStates"><VisualState x:Name="Unselected"><Storyboard><ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
                                                                                   Storyboard.TargetProperty="(TextElement.FontStyle)"><DiscreteObjectKeyFrame KeyTime="0"><DiscreteObjectKeyFrame.Value><FontStyle>Normal</FontStyle></DiscreteObjectKeyFrame.Value></DiscreteObjectKeyFrame></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
                                                                                   Storyboard.TargetProperty="(TextElement.FontWeight)"><DiscreteObjectKeyFrame KeyTime="0"><DiscreteObjectKeyFrame.Value><FontWeight>Normal</FontWeight></DiscreteObjectKeyFrame.Value></DiscreteObjectKeyFrame></ObjectAnimationUsingKeyFrames></Storyboard></VisualState><VisualState x:Name="Selected" /><VisualState x:Name="SelectedUnfocused" /></VisualStateGroup></VisualStateManager.VisualStateGroups><Grid Background="Transparent"><ContentPresenter x:Name="ContentPresenter"
                                                          TextElement.FontStyle='Italic'
                                                          TextElement.FontWeight='Bold'
                                                          Content='{TemplateBinding Content}'
                                                          ContentTemplate='{TemplateBinding ContentTemplate}'
                                                          HorizontalAlignment='{TemplateBinding HorizontalContentAlignment}'
                                                          VerticalAlignment='{TemplateBinding VerticalContentAlignment}'
                                                          Margin='{TemplateBinding Padding}' /></Grid></Border></ControlTemplate></Setter.Value></Setter></Style></ListBox.ItemContainerStyle></ListBox><Grid.Background><SolidColorBrush Color='{Binding ElementName=lstbox,
                                             Path=SelectedItem.Color}' /></Grid.Background></Grid></Window>

この XAML は、次に示す点を除けば WinRT XAML と同じになります。

  • ListBox.ItemContainerStyle で TabNavigation プロパティを削除。
    WinRT XAML 固有のためです。
  • ビジュアル状態、SelectedDisabled、SelectedPointOrver、SelectedPressed を削除。
    WPF XAML に定義されていないためです。
  • ビジュアル状態 Unselected の Storyboard のアニメーションを変更。
    ObjectAnimationUsingKeyFrame の TargetProperty 添付プロパティ を「(TextElement.FontStyle)」と「(TextElement.FontWeight)」に変更。
    DiscreteObjectKeyFrame の Value 属性をプロパティ構文に変更(FontStyles、FontWeights 列挙で値を指定するため)。
  • ContentPresenter に TextElement.FontStyle属性とTextElement.FontWeight 属性を追加。
    WPF XAML の ContentPresenter が FontStyke と FontWeight プロパティをサポートしないためです。
    逆に WinRT XAML の ContentPresenter がサポートしているだけとも言えます。

記述は書籍と同じように省略していますが、DataTemplate は WinRT XAML と同じになります。もちろん、実行結果も同じになります。
CustomListBoxItemStyle

書籍と「テンプレートを使用した WPF コントロールのカスタマイズ」という記事を熟読することで、3 つのテンプレートをより理解することができることでしょう。書籍にも記述されていますが、WPF XAML は XAML UI 技術のフルセットなどので WPF を正しく理解していれば、サブセットである WinRT XAML への応用や移植がし易くなりますので、特にテンプレートを正しく理解するようにしてください。

ここまで説明してきた違いを意識しながら、第11章を読むことで WPF にも書籍の内容を応用することができるようになることでしょう。もちろん、この記事だけで説明していることも理解して頂ければ、WinRT XAML と WPF XAML に役立つことでしょう。


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

$
0
0

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

第12章 ページとナビゲーション

本章では、WinRT XAML におけるページとナビゲーションを説明しています。この説明は、XAML 系の UI 技術が提供するナビゲーション メカニズムを採用するのであれば、WPF XAML にも当てはまることが多くなります。もちろん、違いも存在します。最初に、説明するのはどのようなナビゲーションを使用するかというころです。

  • WinRT XAML では、Frame クラスを使ったナビゲーションを採用した方が良いでしょう。
    この理由は、ページの進むや戻るという実装が容易になるからです。
  • WPF XAML では、Windows Forms と同じようにウィンドウを基準にするのであれば、プログラマーが自由に設計します。
    WinRT XAML と同じように Frame クラスを使ったナビゲーションを利用することもできますが、この場合は Window クラスではなく NavigationWindow クラスを使用するという制約があります。

Visual Studio 2013 が提供するプロジェクト テンプレートには、次に示す 4 種類があります。

  • WPF アプリケーション
    Window クラスを継承する MainWindow.xaml が含まれます。
  • WPF ブラウザー アプリケーション
    Page クラスを継承する Page1.xaml が含まれます。
  • WPF ユーザー コントロール ライブラリ
    ユーザー コントロールのクラス ライブラリ(DLL)を作成するプロジェクトです。
  • WPF カスタム コントロール ライブラリ
    カスタム コントロールのクラス ライブラリ(DLL)を作成するプロジェクトです。

ナビゲーションを使用する WPF アプリケーションの場合は、WPF アプリケーションで作成されるファイル(MainWindow.xaml と MainWindow.xaml.cs)の Window クラスを NavigationWindow クラスへ書き換える必要があります。この理由は、ナビゲーション可能なページは Page クラスを継承したものであり、Page クラスを表示できるウィンドウが NavigationWindow という制約があるためです。
WPF ブラウザー アプリケーション(XBAP)は、ブラウザー内で実行する WPF アプリケーションで、ブラウザーが Page クラスを表示するウィンドウの役割を持つものになります。詳しく知りたい場合は、ナビゲーションの概要ドキュメントをお読みください。

ここまで説明すると Windows ストア アプリの Page クラスを表示するためにウィンドウがあるのかないのかという点が、疑問になることでしょう。Windows ストア アプリの場合は、内部的に CoreWindowというウィンドウが作成されて、この CoreWindow内でナビゲーション メカニズムがサポートされるという仕組みになっています。

※Windows ストア アプリでは、HTML/JavaScript でもアプリを開発することができます。このアプリも内部的には CoreWindowを使用しています。

12.1 画面解像度の問題

本節では、DPI 設定によって解像度がどのようになるかを説明しています。基本的な考え方は WPF XAML でも同じになりますが、WinRT と WPF ではサンプルの WhatRes プロジェクトに対するアプローチを変更しなければなりません。最初に、WhatRes プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... ><Grid><TextBlock x:Name="textBlock"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   FontSize="24" /></Grid></Window>

この XAML は、Page を Window に変更したのと組み込みのスタイルを除けば WinRT と同じになります。WhatRes プロジェクトが WPF XAML に対応させるための大きな変更は、WinRT 固有の機能である DisplayPropeties クラスなどを変更することです。それでは、 MainWindow.xaml.cs の抜粋を示します。

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

        this.SizeChanged += OnMainPageSizeChanged;
        this.Loaded += (s, args) => 
        {
            UpdateDisplay(); 
        };
    }

    void OnMainPageSizeChanged(object sender, SizeChangedEventArgs e)
    {
        UpdateDisplay();
    }

    void UpdateDisplay()
    {
        var primaryMonitor = System.Windows.Forms.Screen.PrimaryScreen;
        var dpi = GetDPI();
        textBlock.Text =
            String.Format("Window size = {0} x {1}\r\n" +
                          "Screen size = {2} x {3}\r\n" +
                          "DPI         = {4}\r\n" +
                          "Bit / Pixel = {5}\r\n",
                          this.ActualWidth, this.ActualHeight,
                          primaryMonitor.Bounds.Width,
                          primaryMonitor.Bounds.Height,
                          dpi.X,
                          primaryMonitor.BitsPerPixel);
    }

    Point GetDPI()
    {
        var result = new Point();
        WindowInteropHelper whnd = new WindowInteropHelper(Application.Current.MainWindow);
        System.Drawing.Graphics graphics = System.Drawing.Graphics.FromHwnd(whnd.Handle);
        result.X = graphics.DpiX;
        result.Y = graphics.DpiY; 
        return result;
    }
}

この コードの WinRT XAML に対する変更点を次に示します。

  • コンストラクタから、DisplayProperties クラスの使用を削除。
  • GetDpi メソッドの追加。
    System.Drawing.Graphics.FromHwnd メソッドによって、System.Drawing.Graphics オブジェクトを取得。
    System.Drawing.Graphics オブジェクトを使って DPI を Point 構造体で返す。
  • UpdateDisplay メソッドのコードを変更。
    System.Windows.Forms.Screen.PrimaryScreen プロパティを使って、画面サイズを取得。
    出力内容を変更。
  • Windows Forms 用のアセンブリの参照設定を追加。
    System.Windows.Forms と System.Drawing。

それでは、実行結果を示します。
WhatRes

表示されている値を変更していますが、Windows Forms のアセンブリを使用することで DPI などを表示することができました。

ここでは、デスクトップ アプリにおける DPI 設定を少しだけ説明します。詳しい説明は、Writing DPI-Aware Desktop and Win32 Applicationsを参照してください。最初に DPI と スケーリングの関係を示します。

DPIスケーリング
96100%
120125%
144150%
192200%

Windows 8/8.1 以降で DPI 設定を変更するには、ディスプレイのプロパティを使用します。最初に、ディスプレイごとに DPI が設定できる場合を示します。
ch12 display1

ディスプレイのプロパティに「小さくする から 大きくする というスライダー」があるのが、分かります。このスライダーを変更しても、サインオフしなおさないとプログラムが取得できる DPI の値は変更されない点に注意が必要です。今度が、「すべてのディスプレイで同じ拡大率を使用する」がチェックされている場合を示します。
ch12 display2

この設定では、スケーリングに応じてラジオボタンが表示されています。そして、スケーリングを変更するとサインオフのしなおしを促します。つまり、プログラムが API によって取得できる DPI の値を適切に取得するには、DPI 変更後のサインオフが必須となります。一方で Windows シミュレータはどうでしょうか。私が試した限りでは、DPI の値を変更しているのではなく、Windows ストア アプリに対してだけ疑似的に DPI 変更をシミュレーションしているように思えます。つまり、DPI 変更に対応するプログラムのテストにおいては、Windows シミュレータを使うことはできないという制約があるということになります。

書籍には、DPI がスケーリングされることでどのような状況が起きるかについて詳しく説明しています。もし、高 DPI 対応などを考えている場合は、熟読をお願いします。

12.2(P597) スケーリングの問題

本節では、WinRT XAML におけるスケーリングの注意点を説明しています。結論から言えば、Windows ストア アプリではスケーリングに応じて自動的に画像ファイルなどのリソースに対して適切なサイズが選択されるようになっています。アプリ側では、スケーリングに応じた画像を用意するだけになっています。画像ファイル名が「name.拡張子」だとすれば、次のような命名規則なっています。

スケーリングファイル名サブフォルダー
100%name-scale-100.拡張子scale-100\name.拡張子
140%name-scale-140.拡張子scale-140\name.拡張子
180%name-scale-180.拡張子scale-180\name.拡張子

どちらの命名規則を使ったとしても、プログラムからは「name.拡張子」でアクセスできるようになっています。

WPF XAML においては、フォントなどは自動的にスケーリングされますが、画像などをスケーリングに応じて自動的に選択する機能はありません。このため、スケーリングに応じて画像などを入れ替える場合は、DPI などから表示する画像を選択するロジックの開発が必要となります。DPI を取得する��ジックは、12.1 の WhatRes サンプルで提示しています。このような事情で、書籍のサンプルである AutoImageSelection プロジェクトを移植していません。移植するにしても、必要な情報をすべて説明していますので、自分でチャレンジしてみてください。

12.3(P601) スナップ ビュー

本節では、Windows ストア アプリに固有となるビューであるスナップ(Windows 8.1 ではウィンドウ化モード)を説明しています。このようなビューの状態を判定できるのは、WinRT 固有の機能であり、WPF XAML には無い機能になります。従って、WhatSnap プロジェクトも WPF XAML へ移植していません。WhatSnap プロジェクトを移植しようとすれば、表示されている画面サイズに応じて処理を行うということになります。これは、デスクトップ アプリから考えるとウィンドウのリサイズへ対応するということでしかありません。ウィンドウ リサイズへ対応で、ある閾値によって適切なレイアウトに構成し直すことでスナップ ビューのようなビューへの対応を行うことができると言えるでしょう。

12.4(P607) 画面の向き

本節では、画面の向きに応じて制御する方法を説明しています。この中で使用している CurrentOrientation プロパティなどを含めて WinRT 固有の機能になりました。従って、WPF XAML で異なる手法を利用しないと、画面の向きを判断することはできません。この理由から、NativeUp プロジェクトも移植していません。それでは、WPF XAML で画面の向きを判断する手法の幾つかを示します。

  • 画面サイズの縦横を比較して判断する。
  • システム イベントの DisplaySettingsChangedイベントで処理する(もしくは、WM_DISPLAYCHANGED メッセージを ウィンドウ プロシージャで処理します)。

これらの説明が、「Detecting Screen Orientation and Screen Rotation in Tablet PC Applications」という ドキュメントに記載されていますので、参考にして対応すれば良いことになります。

12.2 スケーリングの問題から本節までを振り返ってみれば、Windows ストア アプリに対してはスケーリング、画面サイズの変更、画面の向きの変更に対して自動的に対応する仕組みが用意されているということです。WPF XAML などのデスクトップ アプリの世界では、これらの対応はプログラマに任されているということになります。従って、スケーリング、画面サイズの変更、画面の向きの変更に対して対応するかどうかを、実装者が判断しなければなりません。もちろん、Windows 8 対応のタブレット PCなどの導入が決まっている場合は、対応させた方が良いでしょう。その代わり、新しいレイアウトや対応コードなどの追加のコストが掛かりますが、利用するユーザーが好きなスタイルでタブレット PC を使えるようになるというメリットも生まれます。

12.5(P610) 簡単なナビゲーション

本節では、WinRT XAML が提供するナビゲーション フレームワークを説明しています。本章の冒頭で簡単に説明しましたが、WPF XAML でも基本的な考え方は同じになりますが、WinRT XAML との違いもあります。WPF XAML では、Page クラスを使用したナビゲーションには、NavigationWindow クラスか XBAP(XAML ブラウザー アプリケーション)である必要があります。そして、最初にページを指定するには、NavigationWindow クラスの Source プロパティにページを定義した XAML のファイル名を指定します(もしくは、Navigate メソッドを使用します)。Source プロパティでナビゲーション先のページを指定するのは、WPF XAML 固有であり、WinRT XAML にはありません。今度は、Navigate メソッドのシグネチャを示します。

  • this.Frame.Navigate(pageType):WinRT XAML
  • NavigationService.Navigate(pageObject):WPF XAML

WinRT XAML では、引数にページの型オブジェクトを指定し、WPF XAML ではページ オブジェクトのインスタンスを指定します。また、Navigate メソッドを提供するのも、WinRT であれば Frame オブジェクトであり、WPF XAML では NavigationService クラスという違いもあります。それでは、SimpleNavigation プロジェクトの MainWindow.xaml の抜粋を示します。

<NavigationWindow x:Class="SimplePageNavigation.MainWindow"
                  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                  Title="SimplePageNavigation" Height="350" Width="525" WindowState="Maximized"
                  Source="FirstPage.xaml"></NavigationWindow>

この XAML は、WPF XAML に固有な NavigationWindow を使用して最初のページとして「FirstPage.xaml」を指定しています。そして、FirstPage.xaml が 書籍の MainPage.xaml に相当しますので、抜粋を示します。

<Page ... ><Grid><StackPanel><TextBlock Text="Main Page"
                       FontSize="48"
                       HorizontalAlignment="Center"
                       Margin="48" /><TextBox x:Name="txtbox"
                     Width="320"
                     FontSize="20"
                     HorizontalAlignment="Center"
                     Margin="48" /><Button Content="Go to Second Page"
                    HorizontalAlignment="Center"
                    Margin="48"
                    Click="OnGotoButtonClick" /><Button x:Name="forwardButton" 
                    Content="Go Forward"
                    HorizontalAlignment="Center"
                    Margin="48"
                    Click="OnForwardButtonClick" /><Button Name="backButton" 
                    Content="Go Back"
                    HorizontalAlignment="Center"
                    Margin="48"
                    Click="OnBackButtonClick" /></StackPanel></Grid></Page>

この XAML は、組み込みのスタイルを除けば WinRT XAML の MainPage.xaml と同じになります。それでは、FirstPage.xaml.cs の抜粋を示します。

public partial class FirstPage : Page
{
    public FirstPage()
    {
        InitializeComponent();

        this.Loaded += (s, args) =>
            {
                forwardButton.IsEnabled = this.NavigationService.CanGoForward;
                backButton.IsEnabled = this.NavigationService.CanGoBack;
            };
    }

    private void OnGotoButtonClick(object sender, RoutedEventArgs e)
    {
        this.NavigationService.Navigate(new Uri("SecondPage.xaml", UriKind.RelativeOrAbsolute));
    }

    private void OnForwardButtonClick(object sender, RoutedEventArgs e)
    {
        this.NavigationService.GoForward();
    }

    private void OnBackButtonClick(object sender, RoutedEventArgs e)
    {
        this.NavigationService.GoBack();
    }
}

このコードと WinRT XAML の MainPage.xaml.cs との違いを示します。

  • OnNavigatedTo メソッドは WinRT XAML 固有。
    Loaded イベント ハンドラーに置き換え。
  • Navigate メソッドを NavigationService の静的メソッドに置き換え。
    ここでは、Uri 表記を使用。
  • Frame クラスを NavigationService クラスへ置き換え。

SecondPage.xaml は、組み込みスタイルを除けば WinRT XAML と同じなので、SecondPage.xaml.cs の OnGotoButtonClick イベント ハンドラーを示します。

private void OnGotoButtonClick(object sender, RoutedEventArgs e)
{
    var frame = new Frame();
    this.NavigationService.Navigate(new FirstPage());
}

このコードの変更点は、FirstPage.xaml.cs で説明したことと同じであり、今度は Navigate メソッドにページのインスタンスを指定している点が、FirstPage.xaml.cs と異なる箇所になります。これは、Navigate メソッドのオーバーロードを説明するために、このようにしています。それでは、実行結果を示します。
SimplePageNavigation1

画面の上にあるナビゲーション バーと「Go Forward」ボタン、「Go Back」ボタンが無効になっています。「Go To Second Page」ボタンをクリックした結果を示します。
SimplePageNavigation2

今度は、画面の上にあるナビゲーション バーと「Go Back」ボタンが有効になりました。これは、ページ ナビゲーション履歴が格納されたことが理由であり、このボタンの有効化と無効化を設定するために Loaded イベント ハンドラーを指定しています。

this.Loaded += (s, args) =>
    {
        forwardButton.IsEnabled = this.NavigationService.CanGoForward;
        backButton.IsEnabled = this.NavigationService.CanGoBack;
    };

書籍では、このコードをデータ バインドで説明しています。もちろん、NavigationService をデータ バインドすると、正常に動かないこともありますので、WPF では標準のナビゲーション バー(画面の上)を利用するのが良いでしょう。今度が、Navigate メソッドを呼び出した結果として発生するイベント シーケンスを説明します。最初に、WinRT XAML の Navigate メソッドを示します。
ch12 navigate(WinRT)

WinRT XAML では、ナビゲート先のページのインスタンスが作成されてから、OnNavigatedTo が呼び出されて、呼び出し元のページで OnNavigatedFrom メソッドが呼び出されてます。この OnNavigatedTo と OnNavigatedFrom メソッドは、WinRT XAML に固有なメソッドです。今度は、WPF XAML の Navigate メソッドを示します。
ch12 navigate(WPF)

WPF XAML では、ナビゲート先のインスタンスが作成されてから(Uri を指定した場合)、ナビゲート先の Load イベントが発生し、呼び出し元のページで Navigated イベントが発生します。この Navigated イベントに渡される引数は、ナビゲート先のページになります。WPF XAML では、ナビゲートが行われる時に呼び出される Navigated イベント は、ナビゲート元のページで発生するようになっています。この点は、WinRT XAML と大きく違う点になります。これは、WPF XAML でナビゲーション システムを提供してから、フィードバックなどから使い易くするために改良された結果だと考えられます。必ずしも正確では無いかもしれませんが、Navigate メソッドをもう少し説明すると、引数にはページ インスタンスか、Uri を指定することができます。つまり、ページ インスタンスが作成された時点では、必ずしも NavigationService が設定されていなことを意味しています。この理由で、Load イベントで this.NavigationService.CanGoForward のように記述していました。これらは、WPF というフレームワークが自動的に NavigationService と Page クラスを関連付ける癖のようなものなので、このように考えて頂ければ結構です。

12.6(P616) バック スタック

本節では、ナビゲーションがなぜ進むや戻るを実現できるのかを説明しています。バック スタックは、Windows 8.1 では容易なアクセスが可能になるような改良がおこなわれています。WPF XAML では、RemoveBackEntry メソッドで最新の履歴の削除、AddBackEntry メソッドによる独自の戻るエントリーの追加のみができるようになっています。どのようなエントリーを追加するかは、開発者が CustomContentClass 抽象クラスを継承した実装クラスを作成しなければなりません。

ナビゲーション履歴に関しては、WPF よりも WinRT XAML の方��使いやすくなっています。これは、WinRT XAML はナビゲーションが必須であることからナビゲーション システムの使い難さを始めとして様々な改良が行われたと考えることができます。

12.7(P619) ナビゲーション イベントとページの復元

本節は、次節に共通しますが Windows ストア アプリのプロセス状態に基づいてページ状態を保存するための手法を説明しています。Windows ストア アプリでは、プロセスが休止し、システム リソースが不足すればプロセスが終了します。プロセスが終了した後に、ユーザーが同じアプリを起動すれば、同じ状態をユーザーに提示することがアプリに求められます。つまり、最後に表示されていたページの状態を復元することが求められます。このための手法を説明しています。

WPF XAML では、プロセスが強制的に終了させられるようなことはありません。ユーザーとの対話操作の中で、終了操作などがあるだけになります。つまり、ページの復元なども必要がなければ何も考慮しないということになります。書籍の説明は、考え方としては WPF XAML でも応用できますので、状態を保存する手法として学習するのであれば、書籍を参照してください。

12.8(P623) アプリケーション状態の保存と復元

本節では、前節で説明したWindows ストア アプリのプロセス状態に基づいてページ状態を保存するための手法を説明しています。

従って、前節と同じで WPF XAML では気にする必要がありませんから、考え方を応用するという感覚での学習をお願いします。

12.9(P628) ナビゲーション アクセラレーターとマウス ボタン

本節では、アクセラレータとマウス ボタンによるナビゲーションを説明しています。WPF XAML では、OnKeyDown イベントや OnMouseLeftButton、OnMouseRightDown イベント などを組み合わせて実現します。つまり、キーボード操作やマウス操作によりナビゲーションを実装する場合に、実装するということです。実装する場合に、書籍に記載されている内容を参考にすることができます。

12.10(P631) データの受け渡し

本節では、ページ間でデータを受け渡す方法を説明しています。考え方は、WPF XAML にも利用できますが、異なる箇所もありますので、この点は順を追って説明します。それでは、DataPassingAndReturning プロジェクトの MainWindow.xaml の抜粋を示します。

<NavigationWindow ...
                  Source="MainPage.xaml"
                  ShowsNavigationUI="True"
                  ... ></NavigationWindow>

この XAML は、WPF XAML に固有の NavigationWindow の定義です。ここでは、意図的に ShowsNavigation プロパティを設定しています。このプロパティは、ウィンドウの上部に表示されるナビゲーション バーの表示を抑制する時に「False」を設定します。それでは、DialogPage.xaml の抜粋を示します。

<Page ... ><Grid><StackPanel><TextBlock Text="Color Dialog"
                       FontSize="48"
                       HorizontalAlignment="Center"
                       Margin="48" /><StackPanel x:Name="radioStack"
                        HorizontalAlignment="Center" 
                        Margin="48"><RadioButton Content="Red" Margin="12"><RadioButton.Tag><Color>Red</Color></RadioButton.Tag></RadioButton><RadioButton Content="Green" Margin="12"><RadioButton.Tag><Color>Green</Color></RadioButton.Tag></RadioButton><RadioButton Content="Blue" Margin="12"><RadioButton.Tag><Color>Blue</Color></RadioButton.Tag></RadioButton></StackPanel><Button Content="Finished"
                    HorizontalAlignment="Center"
                    Margin="48"
                    Click="OnReturnButtonClick" /></StackPanel></Grid></Page>

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

<Page ... ><Grid x:Name="contentGrid"><StackPanel><TextBlock Text="Main Page"
                       FontSize="48"
                       HorizontalAlignment="Center"
                       Margin="48" /><StackPanel x:Name="radioStack"
                        HorizontalAlignment="Center" 
                        Margin="48"><RadioButton Content="Red" Margin="12"><RadioButton.Tag><Color>Red</Color></RadioButton.Tag></RadioButton><RadioButton Content="Green" Margin="12"
                             IsChecked="True"><RadioButton.Tag><Color>Green</Color></RadioButton.Tag></RadioButton><RadioButton Content="Blue" Margin="12"><RadioButton.Tag><Color>Blue</Color></RadioButton.Tag></RadioButton></StackPanel><Button Content="Get Color"
                    HorizontalAlignment="Center"
                    Margin="48"
                    Click="OnGotoButtonClick" /></StackPanel></Grid></Page>

この XAML は、組み込みスタイルを除けば WinRT XAML と同じになります。今度は、ページ間で受け渡すデータである PassData.cs を示します。

using System.Windows.Media;

namespace DataPassingAndReturning
{
    public class PassData
    {
        public Color InitializeColor { set; get; }
    }
}

このコードは、名前空間を除けば WinRT XAML と同じになります。今度は、DialogPage から MainPage へ返されるデータである ReturnData.cs を示します。

using System.Windows.Media;

namespace DataPassingAndReturning
{
    public class ReturnData
    {
        public Color ReturnColor { set; get; }
    }
}

このコードも、名前空間を除けば WinRT XAML と同じになります。

今度は、MainPage から DialogPage へデータを受け渡す OnGotoButtonClick イベント ハンドラー を MainPage.xaml.cs より抜粋して示します。

private void OnGotoButtonClick(object sender, RoutedEventArgs e)
{
    // Create PassData object
    PassData passData = new PassData();
    // Set the InitializeColor property from the RadioButton controls
    foreach (UIElement child in radioStack.Children)
        if ((child as RadioButton).IsChecked.Value)
            passData.InitializeColor = (Color)(child as RadioButton).Tag;
    // Pass that object to Navigate
    this.NavigationService.Navigate(new DialogPage(), passData);
}

このコードは、Frame を NavigationService に置き換えただけとなります。Navigate メソッドの第2引数に渡すデータである PassData クラスのインスタンスを指定しているのも同じになります。今度は、DialogPage がデータを受け取るためのイベントである Navigated を MainPage.xaml より抜粋して示します。

void OnNavigated(object sender, NavigationEventArgs e)
{
    if (e.Content.GetType().Equals(typeof(DialogPage)))
    {
        var dialogPage = e.Content as DialogPage;
        dialogPage.Completed += OnDialogPageCompleted;

        PassData passData = e.ExtraData as PassData;
        // ナビゲーション履歴を使用する場合(ShowsNavigationUI)
        // GoToボタンが押されていないため、PassDataがnull
        if (passData == null)
        {
            passData = new PassData();
            foreach (UIElement child in radioStack.Children)
                if ((child as RadioButton).IsChecked.Value)
                    passData.InitializeColor = (Color)(child as RadioButton).Tag;
        }
        var dialogStack = dialogPage.radioStack;
        foreach (UIElement child in dialogStack.Children)
            if ((Color)(child as RadioButton).Tag == passData.InitializeColor)
                (child as RadioButton).IsChecked = true;
    }
}

このコードは、WinRT XAML とは考え方が異なっています。すでに説明したように、WPF XAML には NavigatedTo や NavigatedFrom が無く、Navigated イベントがあるだけです。Dialog ページが表示される前に、設定されているイベント ハンドラーが MainPage であることから、MainPage の Navigated イベントになっています。コードの意味を次に示します。

  • NavigationEventArgs の Content プロパティの型が  DialogPage 型かどうかを確認して処理します。
  • DialogPage の Completed イベント ハンドラーを設定(説明は、後で行います)。
  • NavigationEventArgs の ExtraData プロパティをキャストして、PassData を取り出す。
    WinRT XAML では、NavigationEventArgs の Parameter プロパティです。
  • PassData が null の場合を処理。
    この処理は、ナビゲーション バーの進むを使う時のためで、MainPage より PassData を作成しています。
    WinRT XAML でも GoFoward メソッドのみで進むを使うのであれば必要になります。
  • foreach を同じにするために DialogPage の radioStack を取得。

今度は、DialogPage.xaml から MainPage へ戻るための OnReturnButtonClick イベント ハンドラーを DialogPage.xaml.cs より抜粋して示します。

private void OnReturnButtonClick(object sender, RoutedEventArgs e)
{
    this.NavigationService.GoBack();
}

このコードは、Frame を NavigationService に書き換えただけになります。書籍と同じで、GoBack メソッドにはデータを引き渡すような機能はないので、DialogPage クラスに Completed というイベント ハンドラーを設定しますので、DialogPage.xaml.cs より抜粋して示します。

public partial class DialogPage : Page
{
    public event EventHandler<ReturnData> Completed;

    public DialogPage()
    {
        InitializeComponent();

        this.Loaded += (s, e) =>
            {
                this.NavigationService.Navigated += OnNavigated;
            };
        this.Unloaded += (s, e) =>
            {
                if (this.NavigationService == null) return;
                this.NavigationService.Navigated -= OnNavigated;
            };
    }

    void OnNavigated(object sender, NavigationEventArgs e)
    {
        if (e.Content.GetType().Equals(typeof(MainPage)))
        {
            if (Completed != null)
            {
                // Create ReturnData object
                ReturnData returnData = new ReturnData();
                // Set the ReturnColor property from the RadioButton controls
                foreach (UIElement child in radioStack.Children)
                    if ((child as RadioButton).IsChecked.Value)
                        returnData.ReturnColor = (Color)(child as RadioButton).Tag;
                // Fire the Completed event
                Completed(this, returnData);
            }
        }
    }

    ...
}

このコードは、イベント ハンドラーの定義が同じだけになります。そして、すでに説明したように NavigatedTo や NavigatedFrom が WPF XAML にないことから Navigated イベント ハンドラーを Loaded イベント で設定しています。OnNavigated イベント ハンドラーでは、MainPage.xaml.cs で説明したのと同じで NavigationEventArgs の Content プロパティの型が MainPage 型であることを確認してから、ReturnData を設定して Completd イベントを呼び出しています。もちろん、ReturnData の作成から Complete イベント呼び出しまでは、WinRT XAML と同じであることは言うまでもありません。それでは、MainPage.xaml.cs の OnDialogPageCompleted を抜粋して示します。

private void OnDialogPageCompleted(object sender, ReturnData e)
{
    // Set background from returned color
    contentGrid.Background = new SolidColorBrush(e.ReturnColor);
    // Set RadioButton for returned color
    foreach (UIElement child in radioStack.Children)
        if ((Color)(child as RadioButton).Tag == e.ReturnColor)
            (child as RadioButton).IsChecked = true;
    (sender as DialogPage).Completed -= OnDialogPageCompleted;
}

このコードは、WinRT XAML と同じになります。それも当然のことで、自分で定義したイベントなので使い方に違いが無くて当たり前なのです。それでは、実行結果を示します。
DataPassingAndReturning1DataPassingAndReturning2DataPassingAndReturning3

WPF XAML でナビゲーション フレームワークを使うと、ウィザード形式のダイアログ パターンを簡単に実現することができます。もちろん、戻るや進むというナビゲーション バーが不要であれば、ShowsNavigationUI プロパティを「False」にする必要があります。また、ナビゲーションを使用した場合は、ナビゲーションが完了するとナビゲーション元のページ インスタンスが破棄されることになりますから、戻るなどを行えば新しくページのインスタンスが作成されることになります。このページ インスタンスの動き自体は、WPF XAML も WinRT XAML と同じになります。ページ インスタンスを破棄しない場合は、WinRT XAML では NavigationCacheModeプロパティに Disabled 以外の値を設定して、キャッシュします。WPF XAML の場合は、KeepAliveプロパティに True を設定することでインスタンスをキャッシュするようになります。もちろん、ページをキャッシュすれば同じインスタンスになりますから、データの受け渡しや Navigated イベント などに注意をしなけいといけませんので、ご注意ください。

12.11(P637) Visual Studio の標準テンプレート

本節では、Windows ストア アプリ向けに提供されているプロジェクト テンプレートを説明しています。Windows ストア アプリのグリッド アプリケーションにようにカスタマイズされたプロジェクト テンプレートは、WPF XAML にはありません。しかし、LayoutAwarePage クラスや BindableBase クラスの考え方が役に立つ場合があります。書籍では、基本的な考え方のみを説明しています。このため詳細に知りたい場合は、私が公開した記事であるVisual Studio 2012 のグリッド アプリケーションVisual Studio 2013 のグリッドアプリケーションを参照してください。

12.12(P644) ビューモデル とコレクション

本節から、今までに説明した知識を活用してアプリを作っていく場合の様々な課題を説明しています。この課題として、テキサス州エルパソ公立図書館で公開されているエルパソ高校の記念卒業アルバムのデータを活用した ElPasoHeighSchool プロジェクトを使用しています。それでは、ElPasoHighSchool プロジェクトの Student.cs を示します。

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

namespace ElPasoHighSchool
{
    public class Student : INotifyPropertyChanged
    {
        string fullName, firstName, middleName, lastName, sex, photoFilename;
        double gradePointAverage;

        public event PropertyChangedEventHandler PropertyChanged;

        public string FullName
        {
            set { SetProperty<string>(ref fullName, value); }
            get { return fullName; }
        }

        public string FirstName
        {
            set { SetProperty<string>(ref firstName, value); }
            get { return firstName; }
        }

        public string MiddleName
        {
            set { SetProperty<string>(ref middleName, value); }
            get { return middleName; }
        }

        public string LastName
        {
            set { SetProperty<string>(ref lastName, value); }
            get { return lastName; }
        }

        public string Sex
        {
            set { SetProperty<string>(ref sex, value); }
            get { return sex; }
        }

        public string PhotoFilename
        {
            set { SetProperty<string>(ref photoFilename, value); }
            get { return photoFilename; }
        }

        public double GradePointAverage
        {
            set { SetProperty<double>(ref gradePointAverage, value); }
            get { return gradePointAverage; }
        }

        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 XAML と同じになります。今までに説明してきたように、ビューモデル、正確には INotifyPropertyChanged インタフェースを継承したクラスには WinRT XAML と WPF XAML の違いがないのです。今度は、Student クラスのコレクションを ObservableCollection で定義した StudentBody.cs を示します。

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace ElPasoHighSchool
{
    public class StudentBody : INotifyPropertyChanged
    {
        string school;
        ObservableCollection<Student> students = new ObservableCollection<Student>();

        public event PropertyChangedEventHandler PropertyChanged;

        public string School
        {
            set { SetProperty<string>(ref school, value); }
            get { return school; }
        }

        public ObservableCollection<Student> Students
        {
            set { SetProperty<ObservableCollection<Student>>(ref students, value); }
            get { return students; }
        }

        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 XAML と同じになります。ビュー モデルを使用するときは、コレクションとして ObservableCollection を活用すると便利です。この理由は、ObservableCollection が INotifyCollectionChanged インタフェース と INotifyPropertyChanged インタフェースを実装しているからです。書籍では、使用するデータの XML を提示しています。内容を確認するには、こちらを参照してください。今度は、データである XML から StudentBody クラスを作成する StudentBodyPresenter.cs を示します。

using System;
using System.ComponentModel;
using System.IO;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Xml.Serialization;

namespace ElPasoHighSchool
{
    public class StudentBodyPresenter : INotifyPropertyChanged
    {
        StudentBody studentBody;
        Random rand = new Random();
        //Window currentWindow = Application.Current.MainWindow;
        System.Threading.Timer timer;   // DispatcherTimer を変更

        public event PropertyChangedEventHandler PropertyChanged;

        public StudentBodyPresenter()
        {
            // Download XML file
            HttpClient httpClient = new HttpClient();
            Task<string> task =
                httpClient.GetStringAsync("http://www.charlespetzold.com/Students/students.xml");
            task.ContinueWith(GetStringCompleted);
        }

        void GetStringCompleted(Task<string> task)
        {
            if (task.Exception == null && !task.IsCanceled)
            {
                string xml = task.Result;

                // Deserialize XML
                StringReader reader = new StringReader(xml);
                XmlSerializer serializer = new XmlSerializer(typeof(StudentBody));
                this.StudentBody = serializer.Deserialize(reader) as StudentBody;

                // Set a timer for random changes
                timer = new System.Threading.Timer(OnTimerCallback, null, 100, 100);
            }
        }

        public StudentBody StudentBody
        {
            set { SetProperty<StudentBody>(ref studentBody, value); }
            get { return studentBody; }
        }

        // Mimic changing grade point averages
        void OnTimerCallback(object state)
        {
            System.Diagnostics.Trace.WriteLine("timercallback");
            int index = rand.Next(studentBody.Students.Count);
            Student student = this.StudentBody.Students[index];
            double factor = 1 + (rand.NextDouble() - 0.5) / 5;
            student.GradePointAverage =
                Math.Max(0.0, Math.Min(5.0, (int)(100 * factor * student.GradePointAverage) / 100.0));
        }

        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));
        }
    }
}

このコードは、名前空間の違いに伴って少しだけ WPF XAML 用に変更しています。

  • DispatcherTimer を System.Threading.Timer クラスに変更しています。
    この変更に伴って、Window クラスが不要になっています。

この変更は、WinRT XAML と WPF XAML の大きな違いになります。私が色々と試した限りにおいては、ElPasoHighSchool プロジェクトで DispacherTimer を使用すると OnTimerCallback メソッドが正常に動作しなかったことから、System.Threading.Timer クラスに置き換えています。System.Threading.Timer クラスは、WinRT XAML で使用することはできません。これは、WinRT XAML が明示的なスレッドの使用を許可していないことが理由であり、WinRT XAML では DispacherTimer クラスのみが使用可能なタイマーとなっているからです。

書籍では、作成した ELPasoHighSchool プロジェクトのビューモデルを使った様々なデータ バインディングを説明していきます。そして、基本的な考え方が理解できたところで、データ テンプレートを使用する DisplayHightSchoolStudents プロジェクトの MainPage.xaml の抜粋を示します。

<Page ... 
      xmlns:local="clr-namespace:DisplayHighSchoolStudents"
      xmlns:common="clr-namespace:DisplayHighSchoolStudents.Common"
      xmlns:elpaso="clr-namespace:ElPasoHighSchool;assembly=ElPasoHighSchool"
      ... ><Page.Resources><!-- デザイナーはビジュアル編集ができません(Visual Studio 2012でも同じです)
             理由は、StudentBodyPresenterが内部で非同期メソッドを使用して、task.ContinueWithでデータソースを初期化するからです --><elpaso:StudentBodyPresenter x:Key="presenter" /><DataTemplate x:Key="studentTemplate"><StackPanel HorizontalAlignment="Center"><Image Source="{Binding PhotoFilename}"
                               Width="240" /><TextBlock Text="{Binding Sex}"
                                   HorizontalAlignment="Center"
                                   Margin="10" /><StackPanel Orientation="Horizontal"
                                    HorizontalAlignment="Center"
                                    Margin="10"><TextBlock Text="GPA = " /><TextBlock Text="{Binding GradePointAverage}" /></StackPanel></StackPanel></DataTemplate></Page.Resources><Grid DataContext="{Binding Source={StaticResource presenter},
          Path=StudentBody}"><Grid.RowDefinitions><RowDefinition Height="140" /><RowDefinition Height="*" /></Grid.RowDefinitions><Grid Grid.Row="0"><Grid.ColumnDefinitions><ColumnDefinition Width="120" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions><Button x:Name="btnDeatil"
                    Content="Detail"
                    Grid.Column="0"
                    FontSize="24"
                    IsEnabled="False"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Click="OnBtnDetailClicked"/><TextBlock Name="pageTitle"
                       Text="{Binding School}" 
                       Grid.Column="1"
                       FontSize="48"
                       VerticalAlignment="Bottom" Margin="0,0,30,40" /></Grid><DataGrid x:Name="dataGrid"
                  Grid.Row="1"
                  ItemsSource="{Binding Students}"
                  Padding="116 0 40 46"
                  RowDetailsTemplate="{StaticResource studentTemplate}"
                  SelectionMode="Single"
                  SelectionChanged="OnDataGridSelectionChanged"></DataGrid></Grid></Page>

この XAML は、名前空間と組み込みスタイル以外にも変更していますので、次に示します。

  • DataTemplate の studentTemplate の内容を詳細表示用に変更。
    Student の一覧表示は、DataGrid 標準で対応。
  • タイトル欄に、タイトルと詳細表示ボタンを追加。
    DataGrid の詳細表示機能を確認するためです。
  • GridView、ListView、VisualStateManager を DataGrid に置き換え。
    DataGrid の列の自動生成機能を使用しています(データソースから自動生成します)
    GridView は WinRT XAML 固有のためです。
    ListView と VisualStateManager は、Windows ストア アプリのビュー切り替え対応のためです。

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

public partial class MainPage : Page
{
    public MainPage()
    {
        InitializeComponent();

        this.Loaded += (sender, e) =>
            {
                this.NavigationService.Navigated += OnNavigated;
            };
    }

    private void OnNavigated(object sender, NavigationEventArgs e)
    {
        // DataContext の設定
        if (e.Content.GetType().Equals(typeof(StudentPage)))
        {
            if (e.ExtraData != null)
            {
                ((e.Content) as StudentPage).DataContext = e.ExtraData;
            }
        }
    }

    private void OnDataGridSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (dataGrid.SelectedItem != null)
        {
            btnDeatil.IsEnabled = true;
            var selected = dataGrid.SelectedItem as ElPasoHighSchool.Student;
            if (selected == null || string.IsNullOrEmpty(selected.FirstName))
                btnDeatil.IsEnabled = false;
        }
        else
            btnDeatil.IsEnabled = false;
    }

    private void OnBtnDetailClicked(object sender, RoutedEventArgs e)
    {
        var data = dataGrid.SelectedItem as ElPasoHighSchool.Student;
        if (data != null)
            this.NavigationService.Navigate(new StudentPage(), data);
    }
}

この コードは、色々な面で手を加えています。

  • Navigated イベントで、StudentPage の DataContext を設定。
  • OnDataGridSelectionChanged イベントでの処理。
    詳細ボタンの有効化と無効化の切り替え。
  • 詳細ボタンによる StudentPage へのナビゲート。

デザイン自体も大きく変更していますが、DisplayHighSchoolStudents の実行結果を示します。
DisplayHighSchoolStudents1

DataGrid の行を選択すると、RowDetailsTemplate を使って詳細が表示されます。
DisplayHighSchoolStudents2

詳細ボタンをクリックすると、StudentPage へナビゲートします。
DisplayHighSchoolStudents3

デザイン自体は、大きく変更になっていますが、基本的な機能に変更はありません。ここまでで示していない、MainWindow.xaml の抜粋を示します。

<NavigationWindow x:Class="DisplayHighSchoolStudents.MainWindow"
                  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                  Source="MainPage.xaml"
                  Title="DisplayHighSchoolStudents" Height="350" Width="525" WindowState="Maximized"></NavigationWindow>

この XAML は、WPF XAML に固有のものになります。今度は、StudentPage.xaml の抜粋を示します。

<Page ... ><Grid><Grid.RowDefinitions><RowDefinition Height="140" /><RowDefinition Height="*" /></Grid.RowDefinitions><Grid Grid.Row="0"><Grid.ColumnDefinitions><ColumnDefinition Width="120" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><TextBlock Name="pageTitle"
                       Text="{Binding FullName}" 
                       Grid.Column="1" 
                       FontSize="48"
                       VerticalAlignment="Bottom" Margin="0,0,30,40"/></Grid><StackPanel Grid.Row="1"
                    HorizontalAlignment="Center"><Image Source="{Binding PhotoFilename}"
                   Width="240" /><TextBlock Text="{Binding Sex}"
                       HorizontalAlignment="Center"
                       Margin="10" /><StackPanel Orientation="Horizontal"
                        HorizontalAlignment="Center"
                        Margin="10"><TextBlock Text="GPA = " /><TextBlock Text="{Binding GradePointAverage}" /></StackPanel></StackPanel></Grid></Page>

この XAML は、組み込みのスタイルと VisualStateManager 、戻るボタンを除けば WinRT XAML と同じになります。戻るボタンについては、標準のナビゲーション バーを使用することにして削除しました。今度は、SutudentPage.xaml.cs は、WinRT XAML と違ってビューの切り替えが不要なことから、追加したコードはありません。

ここまでの説明と書籍を照らし合わせれば、大きな違いは MainPage のデザインに集約されることでしょう。これは、WinRT XAML がタッチを前提にしたタイル上のデザインになっているのに対して、WPF XAML では従来通りの表形式のデザインになっているためです。WPF XAML で使用した DataGrid もデータテンプレートを使って表示をカスタマイズすることもできますが、より柔軟性の高いカスタマイズを行うのであれば、サードパーティー製のコントロールを使った方が良いでしょう。

12.13(P667) アイテムのグループ化

本節では、GridView や ListView コントロールを使用する時に利用できるデータ ソースのグループ化を説明しています。GridView は、WinRT XAML に固有のコントロールであることから、WPF XAML としてはコレクション データを使った標準のグループ化について説明します。データ ソースのグループ化を定義するには、CollectionViewSource を使用しますので、GroupBySex プロジェクトの MainPage.xaml の定義を抜粋します。

<Page ... 
      xmlns:elpaso="clr-namespace:ElPasoHighSchool;assembly=ElPasoHighSchool"
      ... ><Page.Resources><elpaso:StudentBodyPresenter x:Key="presenter" /><CollectionViewSource x:Key="collectionView"
                              Source="{Binding Source={StaticResource presenter},
                                               Path=StudentBody.Students}" ><CollectionViewSource.GroupDescriptions><PropertyGroupDescription PropertyName="Sex" /></CollectionViewSource.GroupDescriptions></CollectionViewSource></Page.Resources>

    ...
</Page>

この XAML は、書籍の WinRT XAML のアプローチとは違っています。

  • StudentGroup クラスを定義しないで、StudentBodyPresenter を使用しています。
  • CollectionViewSource 定義では、GroupDescriptions プロパティを使用しています。
    IsSourceGrouped と ItemsPath プロパティは WinRT XAML 固有です。
    WPF XAML では、GroupDescriptions プロパティを使用します。

今度は、MainPage.xaml の表示を定義している箇所を抜粋します。

<Grid><DataGrid ItemsSource="{Binding Source={StaticResource collectionView} }"
              Padding="116 0 40 46"
              SelectionMode="Single"><DataGrid.GroupStyle><GroupStyle><GroupStyle.ContainerStyle><Style TargetType="{x:Type GroupItem}"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="{x:Type GroupItem}"><Expander IsExpanded="True"><Expander.Header><StackPanel Orientation="Horizontal"><TextBlock Text="{Binding Path=Name}" /><TextBlock Text="{Binding Path=ItemCount}" Margin="10,0"/></StackPanel></Expander.Header><Expander.Content><ItemsPresenter /></Expander.Content></Expander></ControlTemplate></Setter.Value></Setter></Style></GroupStyle.ContainerStyle></GroupStyle></DataGrid.GroupStyle></DataGrid></Grid>

この XAML の Grid 以降の定義は、組み込みのスタイル以外にも違いがあります。それは、すでに説明したように GridView を WPF XAML がサポートしていないからです。

  • GridView を DataGrid へ置き換え。
  • DataGrid.GroupStyle でグループの表示方法を定義しています。

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

実行結果を見れば理解できますが、GroupStyle で定義した Expander コントロールによってデータを折り畳んだりすることができます。GroupStyle を定義した理由は、グループ名と件数を一行で表示したかったからです。コードとして示していませんが、MainWindow.xaml で NavigationWindow を定義しています。

コレクション データのグループ化に関するアプローチは、WinRT XAML と WPF XAML には明確な違いがあります。どちらが良いというものでは、ありません。むしろ、データを表示するアプローチが違うことから、データの表現の 1つとしてのアプローチの違いだと言えるでしょう。GroupDescriptions というアプローチを有効活用するためのビュー モデルを作成するという手法を、採用することもあるでしょう。または、サードパーティー製のコントロールに応じたビュー モデルを作成するという手法も考えられることでしょう。これらのアプローチは、どれかが正解というようなものではありません。作成するプログラムに応じて、最適な手法を選択すれば良いというだけになります。書籍とここまでの記事を読んできたのであれば、ビュー モデル やデータ テンプレート、データ バインディングをすでに理解していますから、理解した内容を応用して、自分の課題に応じて応用すれば良いだけになります。

最後に、GridView ですが、正確には WPF XAML に存在しないのではありません。GridView コントロールという単独のコントロールが、WPF XAML に存在しないだけで、ListViewコントロールの表示状態の 1つとして GridViewが提供されています。

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

HttpClient で 認証プロキシ サーバーを使用するには

$
0
0

.NET Framework 4.5 以降や Windows Phone、Windows ストア アプリで、http 通信に HttpClient クラスを使用することが多いと思います。HttpClient クラスを使用する場合に問題になるのが、プロキシ サーバーの有無でしょう。基本的な考え方としては、インターネット エクスプローラーに設定されているプロキシ設定を HttpClient は透過的に使用します。

例外的にインターネット エクスプローラーのプロキシ情報が、空になって HttpClient による通信が失敗するケースが報告されています。この事象に対する技術情報が公開されており、この技術情報によれば PC を再起動することで回復すると記述されています。

今度は、認証情報を必要とするプロキシ サーバーを使用する場合はどうしたら良いかという問題です。HttpClientクラスのドキュメントによれば、シンプルな解決方法としては HttpClientHandlerを使用する方法になります。何故かと言えば、HttpClientHandler クラスが Proxy プロパティを持っているからです。ですから、HttpClientHandlerのドキュメントにあるように HttpClient のコンストラクタに引数として渡す前に、プロキシを設定します。このような仕組みになっていると、Azure Mobile Service のように独自のクラス MobileServiceClientのように内部で HttpClient クラスを使用している場合にプロキシを設定するにはどうしたら良いでしょうかかという問題が起きます。このような時に役立つ記事として「HttpClientを認証プロキシに対応させる」や「HttpClientを簡単に認証プロキシに対応させることができるライブラリ、RuntimeProxyをつくりました」がありました。この記事に掲載されているコードを示します。

//Handlerに設定
//ProxyはOSで設定したプロキシサーバーURLとポートが取得可能
//認証情報は取得できなかったのでPickerによって取得
HttpClientHandler handler = new HttpClientHandler();
handler.Proxy = WebRequest.DefaultWebProxy;
handler.Proxy.Credentials = new NetworkCredential(UserName, Password);
// 以降はどのようにHttpClientでコネクションしても接続できます。
HttpClient client = new HttpClient();
var s = await client.GetStringAsync("http://garicchi.com");

このコードを読んでいて、あれと思うことがありました。というのは、HttpClient クラスは内部的にインターネット エクスプローラーのプロキシ設定を利用するという事を考えたからです。たとえば、このコードが「new HttpClient(handler)」となっていたら、このような疑問を持ちませんでした。要は、HttpClientHandler を HttpClient クラスに設定していないのに動くという疑問です。この疑問を解決するヒントは、「handler.Proxy = WebRequest.DefaultWebProxy」の記述になります。この記述は、デフォルト(インターネット エクスプローラー)のプロキシ設定を取得するということです。ということは、次のコードでも良いのではないかと考えられます。

// デフォルト プロキシ情報を取得して、資格情報を設定する
var proxy = WebRequest.DefaultWebProxy;
proxy.Credentials = new NetworkCredential(UserName, Password);

私にはこのコードをテストする環境がないので、結果は不明ですが、デフォルト プロキシを設定するという意味において同じではないかと考えられます。テストできる環境を持ちの方がいらっしましたら、テストした結果をお知らせいただければと思います。

プログラミング Windows 第6版 下巻に向けて

$
0
0

プログラミング Windows 第6版」という書籍を使用して、WPF の学習を補助するという目的で、上巻を 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章 ページとナビゲーション

そして、第1章の記事に書いた目標は、「下巻は自分で WPF に置き換えて学習ができるようになることです」ということでした。今回は、下巻に自分で取り組む場合の簡単な概要を示します。

  1. 第14章 ビットマップ
    WritableBitmap クラスを使用して、ビットマップ オブジェクトを作成する方法を説明しています。Windows 8.1 の話題では、RenderTargetBitmap を説明しています。
    WPF XAML でも WritableBitmapクラスを同様に使用することができます。そして、WritableBitmap クラスは、Renderメソッドをサポートしていますから、Visual を継承する UIElement や FrameworkElement というビジュアルをビットマップ化することができます。WinRT XAML では、Render メソッドがサポートされていなかったために、Windows 8.1 より RenderTargetBitmap クラスが追加されたのです。つまり、WPF XAML で出来ていた機能が WinRT XAML で削除された、また追加されたということになります。この章は、自分でビットマップ オブジェクトを作成する場合に役立ちます。
  2. 第15章 ネイティブ機能へのアクセス
    この章では、Win32 API などへのアクセス方法を説明しています。というのは、WinRT XAML の環境では厳しく制限されている分野になるからです。
    WPF XAML は、デスクトップ アプリですから Windows Forms と同じように Win32 API や COM にアクセスすることができます。注意点としては、ウィンドウ ハンドル を使用する Win32 API の使用には制限があるという点になります。この点は、XAML とは何かでも解説しました。
  3. 第16章 リッチテキスト
    この章では、WinRT XAML の RichTextBlock と RitchEditBox コントロールを説明しています。
    WPF XAML では、RitchTextBoxコントロールに置き換えると同じ機能を実現することができます。リッチ テキストを学習するには、最適な章になっています。
  4. 第17章 共有と印刷
    この章では、WinRT XAML における印刷と Windows 8 以降の機能である共有機能を説明しています。
    WPF XAML では、System.Printing 名前空間などに印刷に必要な機能がありますので、これに置き換えて学習する必要があります。もちろん、共有機能はありません。印刷については、「印刷および印刷システムの管理」に説明があります。
  5. 第18章 センサーとGPS
    この章では、WinRT XAML におけるセンサーや位置情報の使い方を説明しています。
    WPF XAML では、位置情報は  System.Device.Location名前空間で用意されており、センサー API に関しては COMとして用意されています。もちろん、Windows 7 までの OS では位置情報を検出するデバイスやプロバイダーが標準で含まれていない点に注意が必要です。しかし、センサーや位置情報を扱う基本的な考え方は、WinRT XAML と同じですから、本章は WPF XAML にも役立つことでしょう。
  6. 第19章 ペン入力
    この章では、WinRT XAML におけるペン入力を説明しています。
    WPF XAML では、「デジタル インク」で説明していますが、書籍の説明も考え方が同じですから役立つことでしょう。

第14章から第19章までの内容を考えれば、上巻で XAML に関する基本的な考え方が学習できていれば、上記に記載した点に注意しながら読み進めれば、ほとんどの内容が WPF XAML にも使えることが理解できることでしょう。重要なことは、XAML という UI 技術に関する基本的な知識を身に付けることにあります。そうすれば、後は .NET Framework のライブラリや C#/VB といった言語知識を使うことで応用することができることでしょう。

WPF に限りませんが、XAML 系 UI 技術の特徴として説明していない概念として、ビジュアル ツリー(VisualTree)があります。ビジュアル ツリーとは、XAML などで定義されたオブジェクトのメモリ構造であるオブジェクト グラフのことです。XAML は、XML で記述しますから、DOM(ドキュメント オブジェクト モデル)と説明した方が理解し易いかも知れません。つまり、ビジュアル ツリーを検索することで目的のオブジェクト(コントロールなど)を実行時に取得することができます。たとえば、第12章でコントロール テンプレートを説明していますが、コントロール テンプレートの各要素に名前を付与したとしても、プログラム内から名前で呼び出すことはできません。コントロール テンプレートには、次に示す特徴があります。

  1. テンプレート定義内の名前には、テンプレート オブジェクトからしかアクセスできません。
    定義時点では、テンプレート オブジェクトであってコントロールには適用されていません。
  2. コントロールがインスタンス化されてから、テンプレートが適用されます。
    FindName メソッド(FrameworkElementで定義)を使って、名前でテンプレート内の要素へアクセスできません。
    コントロールにテンプレートが適用されると、コントロールの子要素としてテンプレートで定義されたオブジェクトがビジュアル ツリーに追加されます。
  3. ビジュアル ツリーを使って、コントロールの子要素としてテンプレートで定義されたオブジェクトへアクセスができます。

ビジュアル ツリーへアクセスするには、VisualTreeHelperクラスを使用します。VisualTreeHelperクラスを使いやすくするための拡張メソッドなどが、WPF Toolkitでは VisualTreeExtensions クラスとして定義されています。必要に応じて VisualTreeHelperクラスを使用すれば、より柔軟にオブジェクトへアクセスできるようになることでしょう。

今度は、第11章 3つのテンプレートの中で MSDN マガジンの記事である「テンプレートを使用した WPF コントロールのカスタマイズ」より引用した SpringLoadedScrollBar サンプルでは実行結果だけを示しました。

このサンプルと書籍の SplingLoadedSlider サンプルでは、大きな違いとして Orientaion プロパティの Vertical のテンプレートが無い点があります。これは、WinRT XAML の Slider コントロール テンプレートにおいて、横向きのテンプレートは HorizontalTemplate であり、縦向きのテンプレートは VerticalTemplate という名前を持っており、同じ名前でテンプレートを上書きするという手法で実現しています。WPF XAML の ScrollBar コントロール テンプレートの場合は、横向きや縦向きのテンプレートを名前では区別していないので、WinRT XAML とは別のアプローチを使用しないと、同じようなテンプレートを作成することができません。縦向きのテンプレートも作成したので、その XAML の抜粋を示します。

<Window ... ><Window.Resources><Style x:Key="springScroll" TargetType="{x:Type ScrollBar}"><Style.Resources><!-- Make all the RepeatButtons non-focusable --><Style TargetType="{x:Type RepeatButton}"><Setter Property="Focusable" Value="False" /><Setter Property="IsTabStop" Value="False" /></Style><!-- Define a template for the buttons on each end --><ControlTemplate x:Key="templateArrow"
                                 TargetType="{x:Type RepeatButton}">
                    ...</ControlTemplate><!-- Define a template for the central buttons --><ControlTemplate x:Key="templateSpring" 
                                 TargetType="{x:Type RepeatButton}">
                    ...</ControlTemplate></Style.Resources><Setter Property="Width" Value="50" /><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="{x:Type ScrollBar}"><Grid><Grid.RowDefinitions><RowDefinition Height="50" /><RowDefinition Height="1*" /><RowDefinition Height="50" /></Grid.RowDefinitions><!-- Line-top button on left side --><RepeatButton Grid.Row="0" 
                                          Command="ScrollBar.LineLeftCommand"
                                          Foreground="{TemplateBinding Foreground}"
                                          Template="{StaticResource templateArrow}"
                                          LayoutTransform="0 1 1 0 0 0" /><!-- Named track occupies most of the ScrollBar --><Track Grid.Row="1" Name="PART_Track" IsDirectionReversed="True" Orientation="Vertical" ><Track.DecreaseRepeatButton><RepeatButton Command="ScrollBar.PageUpCommand" Width="Auto"
                                                  Foreground="{TemplateBinding Foreground}"
                                                  Template="{StaticResource templateSpring}"
                                                  LayoutTransform="0 -1 1 0 0 0"/></Track.DecreaseRepeatButton><Track.IncreaseRepeatButton><RepeatButton Command="ScrollBar.PageDownCommand" Width="Auto"
                                                  Foreground="{TemplateBinding Foreground}"
                                                  Template="{StaticResource templateSpring}"
                                                  LayoutTransform="0 -1 1 0 0 0"/></Track.IncreaseRepeatButton><Track.Thumb><Thumb Background="{TemplateBinding Foreground}" /></Track.Thumb></Track><!-- Line-bottom button on right side --><RepeatButton Grid.Row="2" 
                                          Command="ScrollBar.LineRightCommand"
                                          Foreground="{TemplateBinding Foreground}"
                                          Template="{StaticResource templateArrow}" 
                                          LayoutTransform="0 -1 1 0 0 0" /></Grid></ControlTemplate></Setter.Value></Setter><Style.Triggers><Trigger Property="Orientation" Value="Horizontal"><Setter Property="Width" Value="Auto" /><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="{x:Type ScrollBar}">
                                ... <!-- サンプルの定義と同じ --></ControlTemplate></Setter.Value></Setter></Trigger></Style.Triggers></Style></Window.Resources><Grid><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="*" /></Grid.RowDefinitions><ScrollBar Style='{StaticResource springScroll}'
                   Grid.Row='0'
                   Orientation='Horizontal' Margin='48' 
                   Minimum='-50' Maximum='50' SmallChange='1' LargeChange='10'
                   Foreground='SteelBlue' Height='50' /><ScrollBar Style='{StaticResource springScroll}'
                   Grid.Row='1' 
                   Orientation='Vertical' Margin='48' HorizontalAlignment='Left'
                   Minimum='0' Maximum='50' SmallChange='1' LargeChange='10'
                   Foreground='HotPink' Height='Auto' /></Grid></Window>

この XAML のポイントを、次に示します。

  • ControlTenmplate 単独ではなく、Style として定義。
  • Template プロパティに縦向きの ControlTemplate を定義。
    要素を LayoutTransform で座標変換(縦向きになるように)。
    Track 要素に IsDirectionReversed 属性と Orientation 属性を指定。
  • Style.Triggers プロパティで、Triger 要素を定義して、Orientation プロパティが Horizontal の時のテンプレートを定義。
    このようにした理由は、ScrollBar コントロール テンプレートが、Style.Triggers で Horizontal を定義していたので同じにしました。

それでは、実行結果を示します。
SpringLoadedScrollBar2

書籍と同じように、縦向きのコントロール テンプレートが定義できていることを確認することができます。書籍の SplingLoadedSlider サンプルでは、スライダーのバネの左側と右側で色を変えていますが、示した XAML を理解できれば、同じように色を変更することもできることでしょう。このように Style の Triggers プロパティを使用すれば、コントロールのプロパティに応じたコントロール テンプレートに切り替えることもできるようになります。この機能は、WinRT XAML の Style クラスにはありません。つまり、Triggers プロパティを WinRT XAML はサポートしていないのです。ですから、WinRT XAML の Slider コントロールでは、縦向きと横向きのテンプレートに名前を付けて、プロパティによって切り替えるようになっているとも言えます。これなども、WPF XAML と WinRT XAML のアプローチの違いであると考えることができます。この一連の記事の最初に説明しましたが、WPF XAML はフルセットの高機能型であり、WinRT XAML は Silverlight などのサブセットのフィードバックなどから、よりコンパクトでありながら、使いやすさの面で改良されたものであることから、このようなアプローチの違いがあるのです。このような違いは、ドキュメントやコントロール テンプレートを自分で調べれば、すぐに見つけられることでしょう。そのために必要な情報は、書籍と今回の一連の記事の中で説明してきているからです。

それでは、プログラミング Windows 第6版 を使った WPF の説明は、今回で終了となります。是非、WPF や Windows ストア アプリを作ってみてください。

追記
WPF がリリースされたのは、Windows Vista に含まれる .NET Framework 3.0 になります。その後に、Windows 7 に含まれる .NET Framework 3.5 で機能強化が行われています。また、このタイミングで .NET Framework SDK と Windows SDK の統合が行われています。たとえば、LocBaml ツールのサンプルに記載されているサンプルが Windows SDK に含まれています。これ以外のサンプルも、Windows SDK に含まれています。WPF などのサンプルが含まれた Windows SDK は、Microsoft Windows SDK for Windows 7 and .NET Framework 3.5 SP1となります。Microsoft Windows SDK for Windows 7 and .NET Framework 4には、.NET Framework のサンプルが含まれていないのでご注意ください。以前の Windows SDKをダウンロードするには、Windows SDK Archiveのページを参照してください。
また、センサーなどを使用する場合は Window API Code Packの使用をご検討ください。

WPF のロードマップ

$
0
0
本記事は、マイクロソフト本社の .NET Blogの記事を勝手に翻訳したものです。
元記事は、The Roadmap for WPF 2014/11/12 AM 7:37

私のオレオレ翻訳なので、翻訳の変更などはこのBlogのコメント欄にフィードバックをください。正確な情報は、オリジナルのBlogを参照するようにしてください。

 

2006年に WPF (.NET Framework 3.0) を紹介した時の反応は、とても素晴らしいものでした。なぜなら、企業、ISV、マイクロソフトのパートナーは、お客様向けのミッション クリティカル アプリケーションと素晴らしいバーティカル ソリューションを構築するビジネスの中心となる技術として選択してくれたからです。また、現在へ繋がる推進力にもなりました( Visual Studio 2013 で過去60日で開発された新規プロジェクトの 10% が WPF プロジェクトです。WPF は、データ中心のビジネス アプリを構築する情熱的で活力のあるコミュニティを持っています。最近の事例としては、私たちのパートナーである InterKnowlogyが開発した新しい WPF アプリケーションがあります。このアプリは、CNN のプロデューサーがオンエア中の投票におけるデータの構成と検証、中間結果のアップロードを行うために使用しています。投票データは、CNN のMagic Wallに表示されています。Magic Wallの開発には、マイクロソフトの Bing Pulseチームが協力しています。この記事では、WPF プラットフォームのロードマップを取り扱います。ロードマップには、次期 Visual Studio リリースのツールにおける機能強化と投資領域の優先順位が含まれています。

プラットフォームに対する投資領域

今年(2014年)の //build カンファレンスで行ったユーザー調査における示唆や数か月に渡る様々なマーケットで活躍する多くの開発者に対するインタビューを基にして、WPF をより素晴らしいプラットフォームにするための投資領域の優先順を以下に記載します。

パフォーマンス:WPF が、大規模で高パフォーマンスなアプリ(たとえば、Visual Studio や Blend)で使われていることから、よりパフォーマンスを向上したプラットフォームにして欲しいという要望がカスタマー フィードバックとして届いています。具体的には、幾つかの重要なシナリオにおいて、たとえばアプリの起動時間や ItemsControl に対するスクローリングと仮想化のパフォーマンスの最適化を前進させることです。

DirectX との相互運用性:主となるシナリオは、WPF アプリケーションと最新の DirectX をシームレスに相互運用させることです。

最新ハードウェアのサポート:タッチや高DPIのディスプレイのような技術は、今日の様々なデバイスで使われています。新しいハードウェアをサポートするためには、既存の WPF アプリが新しいハードウェアを持つ最新型のデスクトップ PCに対応することは重要なことです。

ツール:私たちは、.NET や WinRT のような新しいプラットフォームの登場と合わせて対応させる場合において、ツールも一緒に進化させていきます。この約束は、この記事のツール領域に対する投資を反映しています。

幾つかの投資領域は、特定の OS バージョンなどに依存するリスクを持っています。このような場合は、OS が持つ機能を絞り込むか、その機能を使用する必要があります。

現在の進捗状況

最初に、共通する質問であるサポートを説明します:WPF は、.NET Framework の構成要素です。 .NET Framework は、独立した製品ではなく オペレーティング システムを構成するコンポーネントとして定義されています。この理由から、サポートはWindows オペレーティング システムのサポート ライフサイクルに依存しています。現行の推奨バージョンである Windows 8.1 上の .NET Framework 4.5.2 の延長サポートは、2023年まで提供されます。私たちは、WPF カスタマーに影響する様々な報告されたバグやセキュリティ問題に対する修正を行い続けます。

WPF の品質に対する改善

我々は、WPF の改善を止めずに更に発展させて行きます。たとえば、次期 Visual Studio のリリース や .NET Framework 4.6 があります。

次に示すのは、.NET Framework 4.6 で提供した最新の修正内容です。

  • System.Windows.Input.Cursor におけるマルチ イメージ カーソル ファイル
  • 透明なチャイルド ウィンドウ(Tranparent child windows)
    (元の Blogのコメントを読んでいくと、Window スタイルにおける透明スタイルのサポートのことであり、Window オブジェクトの AreTransparent プロパティのことではありませんので、ご注意ください -荒井による注ー)
  • ダブル タップ ジェスチャの性能向上
  • TextBox コントロールにおけるダブル タップによるテキスト選択
  • ComboBox コントロールにおけるスタイラス 入力の信頼性向上

私たちは、あなたのフィードバックを求めています!

私たちは、将来のリリースのために connect で投票の多かった報告されたバグや信頼性問題を調査しています。

タイトル投票数
タッチ イベントが遅れる29
リボン ウィンドウ:ボーダーが細過ぎる18
マイクロソフト カメラ コーデックパックがインストールされている場合に、BitmapFrame.Create で TIFF ファイルを扱うと 予約メモリの 300MB が使用される12

ツールに対する改善

WPF 向けのツールは、ユーザー調査やカスタマー インタビューにおいて上位に位置する要望事項です。これは、XAML ツール カテゴリにおける上位5つの中の3つであり、WPF サポートに寄せられる要望を反映したものです。

ビジュアルな診断機能:調査の第1位と XAML ツールに寄せられたアイディアの第2位は、WPF アプリ向けの UI デバッガーが必要だというものです。我々は、WPF アプリ向けの完全なデバッグ ツール スイート(デバッグ中に、ライブ ビジュアル ツリーに対する検査機能やプロパティの変更を可能にします)を構築していることをアナウンスすることを非常に喜んでいます。このツールは、デバッグ中の変更をソース コードへ反映することを可能にします。


タイムライン ツール:繰り返し寄せられる要望としてユーザー調査の第4位が、WPF 向けのパフォーマンス診断ツールです。私たちは、WPF アプリ向けの新しい診断ツール(アプリの起動時間の遅さ、低速なフレーム レートなどの共通的なパフォーマンス問題に対するトラブルシュートを可能にします)を開発している最中です。既存のメモリ使用率CPU 使用率のツールとマージして、より素晴らしい WPF アプリを開発できるようにするために Visual Studio に組み込むツールセットを提供します。

Blend の改善点:Blend for Visual Studio 2015 は、素晴らしいユーザー インターフェースを持つ XAML アプリを作成するための最良の選択肢になるように再デザインされています。Blend は、Visual Studio とのワークフローを改善し、ツールセットとして見栄えの一貫性を持ちます。さらに、新しい Blend は、Visual Studio(WPF を含んでいます!) と同じ技術をベースにしています。これには、以前の Blend の欠点を改善し、より良いソリューション エクスプローラーとソース コントロール サポートを含んでいます。もっと重要なことは、Blend に XAML インテリセンス、基本的なデバッガー機能が存在しているということです。この改善のための重要なことの1つは、非同期のソリューション ロードのサポートが行われたということです。大規模な WPF ソリューション向けに、この機能が既に提供されています。また、WPF に対する イン プレースのテンプレート編集と XAML エディタにおいて「定義をここに表示(Peek)」するなどの機能を含めて洗練した体験を提供します。

 荒井による注記:Blend for Visual Studio 2015 Previewの詳細については、「Blend for Visual Studio 2015 Preview」の記事を参照してください。XAML インテリセンスの詳細と合わせて、スケッチ フローが提供されなくなることが記載されています。

もっとフィードバックをください

私たちは、Visual Studio 2015 におけるツールの改善と WPF プラットフォームのロードマップについて、皆さんが考えていることを知ることに関心があります。どうか、この記事に対するフィードバックや、e-mailConnectユーザー ボイスを使って私たちにご連絡ください。

 

注意事項

この記事は、オレオレ翻訳ですので、フィードバックは「The Roadmap for WPF」のコメント欄か、e-mailConnectユーザー ボイスを使用してください。オレオレ翻訳に関するフィードバックについては、この記事のコメント欄にお願いします。

Blend for Visual Studio 2015 プレビュー

$
0
0

マイクロソフト本社の「Blend for Visual Stduio 2015 Preview」の記事を参考にしながら、プレビューで何ができるようになっているかを説明します。最初に、Blend を起動したスクリーン ショットを掲載します。
Blend1

この画像から理解できることは、Blend のルック & フィールが Visual Studio のダークテーマになっており、Visual Studio の IDE と非常に似通っているということです。似通っているだけでなく、多くの機能が Visual Studio と同じになっています。このことを、Visual Studio のユーザー インターフェースとの一貫性(Consistent user interface)と呼んでいます。

  • Visual Studio Online へのサインイン(ユーザー設定情報の保存など)
  • ソリューション エクスプローラー
  • XAML のビジュアル エディタと XAML のコードエディタ
  • など

次に示すのは、XAML エディタの改善機能になります。
Blend2

コンテキスト メニューだけでなく、XAML エディタにおけるインテリセンスのサポートも追加されています。コンテキスト メニューの「定義をここに表示」機能は、まだ使えませんが、「定義へ移動」を呼び出すとオブジェクト ブラウザーが表示されます。

Blend3

オブジェクト ブラウザーも、新しく追加された機能です。今度は、コード エディタを示します。
Blend4

Visual Studio と同じようにコンテキスト メニューを表示しています。「定義をここに表示」を行うと、Visual Studio と同じ動作をします。
Blend5

現在のプレビューでは、ブレーク ポイントは、コンテキスト メニュー、あるいはデバッグ メニューから設定するようになっていますが、インテリセンスを始めとしてかなり Visual Studio のエディタに近づいていることが理解できます。次に、デバッグ時のスクリーン ショットを示します。
Blend6

ローカル変数、イミディエイト ウィンドウ、さらにイミディエイト ウィンドウにおけるインテリセンスなど、Visual Studio と同じようなデバッグ機能がを持つことが理解できます。 Blend for Visual Studio 2013 までは、XAML のデザイン ツールという側面がありましたが、Visual Studio 2015 プレビューでは IDE としての機能が向上していることを試すことができます。もちろん、プレビューですから、まだ出来ないことも多くあります。たとえば、プロジェクト プロパティでターゲット プロファイル(フレームワークのバージョンなど)を変更できますが、ビルド時は指定したプロファイルに切り替わらないようです(.NET 4.5 から .NET 4.5.3に変更しても、エラーになるコードがありました)。

紹介した以外に、NuGetサポートやチーム エクスプローラーなども提供されます。続々と機能強化される一方で、廃止される機能もあります。スケッチ フローは Blend for Visual Studio に搭載されなくなります。スケッチ フローを使ったデザイン イテレーションを実施するには、Visual Studio 2013 を使用するようにしてください。

最後に、.NET Framework 4.6 で提供される透明なチャイルド ウィンドウを説明します。.NET Framework 4.6では、System.Windows.Interop.HwndSourceParameters構造体に、UsePerPixelTransparencyプロパティが追加されます。このプロパティを使用することで、透明なチャイルド ウィンドウ スタイル(GDI) を実現することができます。

private void button_Click(object sender, RoutedEventArgs e)
{
    IntPtr parentWindowHandle = new WindowInteropHelper(this).Handle;
    HwndSourceParameters windowParams = new HwndSourceParameters("SemiTransparentChildWindow");
    windowParams.ParentWindow = parentWindowHandle;

    //int values of WS_CHLID, WS_CLIPCHILDREN, and WS_VISIBLE
    int styleParams = 0x40000000 | 0x02000000 | 0x10000000;
    windowParams.WindowStyle = styleParams;
    windowParams.UsesPerPixelTransparency = true;
    windowParams.PositionX = 100;
    windowParams.PositionY = 100;
    HwndSource hwndsSrc = new HwndSource(windowParams);

    Ellipse ellipse = new Ellipse();
    ellipse.Width = 100;
    ellipse.Height = 100;
    ellipse.Fill = Brushes.Green;
    ellipse.Opacity = 0.1;
    hwndsSrc.RootVisual = ellipse;
}


そして、アプリケーション マニフェストに次の記述を行います。

<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"><application><!--アプリケーションが Windows 8 で動作するように設計されている場合は、次の supportedOS ノードのコメントを解除します—><supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"></supportedOS></application></compatibility>


アプリケーション マニフェストを記述しないと例外が発生します。実行結果は、次のようになります。
ChildWindows

 

図形である Ellipse が半透明になっていることがわかります。これが、半透明のチャイルド ウィンドウ サポートになります。Visual Studio 2015 プレビューのターゲット フレームワークには、.NET Framework 4.6 がありませんので「.NET Framework 4.5.3」を指定しています。

この機会に、ぜひ Visual Studio 2015 プレビューを使用して、フィードバックをしてください。

Viewing all 163 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>