.NET: WPF のいろいろ

あれをやるには、のメモ。

ボタン、パディング、マージン、イベント

<Button Padding="2" Margin="2,0,0,0" Click="Button_Click">参照...</Button>

ボタンに表示する内容は、Content 属性に書くか、Button 要素の内容として書く。

Padding はボタンの枠線と内容との間。Margin はボタンの外の余白。数値 1 つだと上下左右に同じ値が適用され、4 つの数字をカンマ区切りで書くと順に左、上、右、下の値となる。

この例では Click イベント発生時にイベントハンドラの Button_Click メソッドを呼ぶよう指定しているが、イベントハンドラXAML ファイルには書けないので、コードビハインドを使う。つまり、XAML とセットになっている C# ファイルの方に書く。

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

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("Hello WPF!");
        }
    }
}

ラベル

<Label Content="再生回数:"/>

ラベルに表示する内容は、Content 属性に書くか、Button 要素の内容として書く。

テキストボックス

<TextBox Text="鈴木太郎" MinWidth="200"/>

テキストボックスの入力値は Text 属性。表示幅は、完全に固定するなら Width 属性でいいんだけど、初期状態ではこの幅だけどウィンドウを広げたらテキストボックスも広がってほしい、ということであれば MinWidth 属性が使える。

グループボックス

<GroupBox Header="詳細設定" Margin="10" Padding="5">
    <!-- ここに中身を書く -->
</GroupBox>

見出しと枠線のついた箱。Header に見出しを書く。

テキストブロック

<TextBlock>Hello</TextBlock>

テキストを表示する。ラベルと何が違うのかというと、テキストブロックは HTML ライクなリッチテキストを表示することができるようだ。ラベルと違って、表示幅に合わせて折り返しも行われる。Label と TextBlock では派生元の親クラスが異なるそうで、それに伴って様々な違いがあるという。

ステータスバー

<StatusBar>
    <TextBlock>待機中</TextBlock>
</StatusBar>

中に何かを入れて使う。子要素として StatusBarItem を複数置いて、その中に具体的な内容を入れたり、その間に Separator 要素を置いて区切り線を表示したりすることもできるようだ。

スタックパネル

<StackPanel Orientation="Horizontal">
    <!-- ここに中身を書く -->
</StackPanel>

レイアウトコンテナの一種。つまり、これ自体が具体的に何か表示されるコンポーネントなわけではなくて、中に入れた要素をうまく並べてくれる。StackPanel の場合は、中に入れた要素を順に並べる。デフォルトでは縦に並べていくが、Orientation 属性に Horizontal を指定すると横に並ぶ。

グリッド

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Button />
    <Button Grid.Column="1"/>
    <Button Grid.Row="1" Grid.ColumnSpan="2"/>
</Grid>

レイアウトコンテナの一種。縦何マス、横何マスという具合に格子状に区切って内容を表示する。

最初に Grid.RowDefinitionsGrid.ColumnDefinitions で各行の高さ・各列の幅を指定できる。Auto を指定すると内容に応じて設定され、* を指定すると残りをすべて使用する。HTML の table 要素のように 2* とか 3* といった指定も可能で、残りの領域を指定した比率で配分することができる。

中に配置される子要素に対しては、Grid.ColumnGrid.Row 属性を使って格子のどのマスに置くかを指定できるほか、Grid.RowSpanGrid.ColumnSpan で複数のマスにまたがって配置させることができる。このあたりも HTML の table 要素と似ている。

タブ

<TabControl Margin="10" HorizontalContentAlignment="Stretch">
    <TabItem Header="概要">
    </TabItem>
    <TabItem Header="詳細">
    </TabItem>
</TabControl>

TabControl の中に TabItem を配置して、複数のタブを切り替えられるようにする。

ひとつ使いづらいことに、TabControl の横幅が、アクティブな TabItem の内容に依存してしまう。つまり、横幅が広いコンポーネントを含むタブと、そうでないタブがあったとすると、切り替えるたびに TabControl 自体の横幅が伸びたり縮んだりする。対策をググってみたところ、以下のようにするとある程度緩和されるようだ。

<TabControl Margin="10" HorizontalContentAlignment="Stretch" Grid.IsSharedSizeScope="True">
    <TabItem Header="概要">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition SharedSizeGroup="TabItemGroup"/>
            </Grid.ColumnDefinitions>
            <GroupBox Header="ファイルの概要" Margin="10" Padding="5">
            </GroupBox>
        </Grid>
    </TabItem>
    <TabItem Header="詳細">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition SharedSizeGroup="TabItemGroup"/>
            </Grid.ColumnDefinitions>
            <GroupBox Header="ファイルの詳細" Margin="10" Padding="5">
            </GroupBox>
        </Grid>
    </TabItem>
</TabControl>

ポイントは、TabControl に付与した Grid.IsSharedSizeScope="True" と、各タブに Grid を配置した上で列幅を SharedSizeGroup="TabItemGroup" と指定しているところ。TabItemGroup という文字列はなんでもいいんだけど、各タブの GridSharedSizeGroup に同じ文字列を指定すると、列幅が共有されて揃うようになるので、タブを切り替えても TabControl の幅が伸び縮みしなくなる、という仕組みなのだそうだ。

ただし、TabControl は画面に表示された直後の時点では 2 つ目以降のタブについてレイアウト計算をしていないので、本来の望みである「すべてのタブの中で一番横幅が大きいものに、他のタブも合わせてほしい」は実現されていなくて、「最初のタブの横幅が、2 つ目以降のタブに共有される」ということになるようだ。

データバインディング

WPF では、オブジェクトのプロパティと UI 要素のプロパティを結び付けて、片方を変更すると他方にも自動で反映させることができる。

といっても Plain Old なオブジェクトには対応していなくて、バインディングに対応したオブジェクトである必要がある。作り方はいくつかあるそうだけど、例えばこんな感じ。

namespace HelloWpf
{
    class HelloConfig : DependencyObject
    {
        public static readonly DependencyProperty MessageProperty =
            DependencyProperty.Register("Message", typeof(string),
                typeof(HelloConfig), new PropertyMetadata(""));

        public string Message
        {
            get { return (string)GetValue(MessageProperty); }
            set { SetValue(MessageProperty, value); }
        }
    }
}

これを XAML に結び付けるには、1) コンポーネントDataContext プロパティに、バインドするオブジェクトを指定する、2) コンポーネントのプロパティに {Binding XXX} のようにオブジェクト側のプロパティ名を指定する、という 2 つのステップが必要になる。

この 1) と 2) は同じコンポーネントでやらなくてもよいので、あるタブの表示項目が、それぞれバインド先オブジェクトのプロパティに対応する、というような場合、TabItemDataContext プロパティにオブジェクトを設定しておけば楽だ。

<Window x:Class="HelloWpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:HelloWpf"
        Title="MainWindow">
    <TabControl>
        <TabItem x:Name="FileTab" Header="ファイル">
            <TabItem.DataContext>
                <local:HelloConfig/>
            </TabItem.DataContext>
            <StackPanel>
                <TextBox Text="{Binding Message}"/>
            </StackPanel>
        </TabItem>
    </TabControl>
</Window>

この例では、DataContext プロパティに設定するオブジェクトを XAML 内でインスタンス化しているが、コードビハインドを使って、C# 側のコンストラクタで設定するのでもいいらしい。

で、結び付けられたのはいいとしてですよ。XAML 側でインスタンス化したオブジェクトを C# コード中で参照するにはどうしたらいいんだろうか。とりあえず、XAML 側で TabItemx:Name="FileTab" と書いておいたので、C# から参照して DataContext プロパティを見ればいいのかな。

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

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            HelloConfig config = (HelloConfig)FileTab.DataContext;
            MessageBox.Show(config.Message);
        }
    }
}

一応これで、テキストボックスの内容がメッセージボックスに出せるようになった。