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

この記事では、「プログラミング 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="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="https://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=\"https://schemas.microsoft.com/winfx/2006/xaml/presentation\"\r\n" +
            "      xmlns:x=\"https://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=\"https://schemas.microsoft.com/winfx/2006/xaml/presentation\"\r\n" +
        //    "      xmlns:x=\"https://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 にも書籍の内容を応用することができるようになることでしょう。

ch08.zip