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


この記事では、「プログラミング 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 と異なるボタンを示します。

WinRT WPF 説明
HyperLinkButton HyperLink WPF には 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 にも書籍の内容を応用することができるようになることでしょう。

ch05.zip

Comments (0)

Skip to main content