42
</Label></Border>
43
TwoWay
であるため、TextプロパティにはGetter
とSetter
の両方を用意します)。public class MainWindowViewModel : ViewModel.ViewModelBase {
public object LabelText => "TestLabel";
string text;
public string Text {
get { return text; } set {
if (text != value) { text = value;
}
base.RaisePropertyChanged("Text");
} } }
9 標準で提供されている UI コントロール
WPF
では、以下のように様々なUI
コントロールが標準で用意されています。Grid、StackPanel、WrapPanel、DockPanel、Canvas、
Button、Label、ComboBox、TextBox、TextBlock、CheckBox、RadioButton、
ScrollViewer、
ViewBox、
Menu、MenuItem、ContextMenu、
Expander、
Slider、
ListView、DataGrid、
ContentControl、
ItemsControl、Popup、
Border、
UserControl、Window
上記から幾つかピックアップして、利用例を紹介します。
ここで、入門ノートの範囲からは外れるのですが、WPFの描画が遅く、パフォーマンスを
44
意識する必要がある場合、「レイアウトの選択」や「UIの仮想化」を考慮することがあるか も知れません。リンクを
2
つ紹介しておきます。WPF
アプリケーションのパフォーマンスの最適化https://msdn.microsoft.com/ja-jp/library/aa970683(v=vs.110).aspx
第
3
回 WPFアプリケーション・チューニングhttp://www.atmarkit.co.jp/fdotnet/chushin/vsperf_03/vsperf_03_02.html
(1) Grid
Grid
について押さえておくべきことは次の2
つだと思います。1)
行と列を定義して、何行何列目に配置するといった指定が可能である。2)
行や列の指定をしないと、Grid内のUI
コントロールは重ねて描画される。これらについて例示します。
1)
例えば、3行4
列を定義し、各行にボタンを配置する例を挙げます。<!-- MainWindow.xaml -->
<Window x:Class="WpfSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="2.*" />
<RowDefinition Height="3.*" />
<RowDefinition Height="5.*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1.*" />
<ColumnDefinition Width="3.*" />
<ColumnDefinition Width="2.*" />
<ColumnDefinition Width="4.*" />
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="2" Content="(1,3)" />
<Button Grid.Row="1" Grid.Column="3" Content="(2,4)" />
<Button Grid.Row="1" Grid.Column="1" Content="(2,2)" />
<Button Grid.Row="2" Grid.Column="0" Content="(3,1)" />
</Grid>
</Window>
45
ここで、図を見ると分かるように、「a.*」の指定は比率を意味しています。
一方、ピクセル指定もできます。
2)
行や列を定義せずにGrid
にButton
を2
つ配置してみます。<!-- MainWindow.xaml -->
<Window x:Class="WpfSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button Width="100" Height="30" Content="TEST1" />
<Button Width="100" Height="30" Content="TEST2" />
</Grid>
</Window>
実行結果は次のようになります。
Content
がTEST1
のボタンは、ContentがTEST2
のボタンに上書きされ、イベントも拾 えない状態になります。また、VisualTree
は上から順に描画されていることも分かります。46
このままでは使えませんが、例えば、HorizontalAlignmentや
VerticalAlignment
を指定し て使えば、重ねて描画はむしろ記述量が少なくなり便利なことがあります。<Grid>
<Button Width="100" Height="30" Content="TEST1" HorizontalAlignment="Left" />
<Button Width="100" Height="30" Content="TEST2" HorizontalAlignment="Right" />
</Grid>
(2) Canvas
MSDN
の次の記事パフォーマンスの最適化:レイアウトとデザイン
https://msdn.microsoft.com/ja-jp/library/bb613542(v=vs.110).aspx
にもあるように、 5 つのパネル(Grid、StackPanel、WrapPanel、DockPanel、Canvas)
の中で最も描画効率がよいのが
Canvas
です。Canvas
では、座標指定してUI
コントロールを配置します。他のパネルでは様々なレイアウトロジックが実行されて位置が決まり、慣れない内は表示したい場所に表示できない ことがあるかも知れません。Canvasの場合は、そんなことはなくシンプルです。
<Canvas>
<Button Canvas.Left="10" Canvas.Top="20" Width="100" Height="30" Content="(10,20)" />
<Button Canvas.Left="200" Canvas.Top="50" Width="100" Height="30" Content="(200,50)" />
<Button Canvas.Left="70" Canvas.Top="150" Width="100" Height="30" Content="(70,150)" />
</Canvas>
ちなみに、C#コード上で
button
インスタンスのLeft
の値を設定するには、次のように書 きます。Canvas.SetLeft(button, 100d);
47 (3) Label
Label
は目に見えない伸縮性のアメーバのように使えて、拾えなかったイベントを拾う、といったことができます。例えば、次のように
Label
を配置したとします。<Grid>
<Canvas>
<Button Canvas.Left="10" Canvas.Top="20" Width="100" Height="30"
Content="(10,20)" />
<Button Canvas.Left="200" Canvas.Top="50" Width="100" Height="30"
Content="(200,50)" Click="Button_Click" />
<Button Canvas.Left="70" Canvas.Top="150" Width="100" Height="30"
Content="(70,150)" />
</Canvas>
<Label MouseLeftButtonDown="Label_MouseLeftButtonDown" />
</Grid>
このように記述すると、Canvasが描画された後に
Label
が描画され、かつ、Gridで行と列 を定義していないので、Grid 全体をLabel
が覆うことになり、ボタンのクリックイベント が呼ばれなくなります(Label_MouseLeftButtonDownハンドラが呼ばれます)。一方、次のように
Label
をCanvas
より前に宣言したとします。<Grid>
<Label MouseLeftButtonDown="Label_MouseLeftButtonDown" />
<Canvas>
<Button Canvas.Left="10" Canvas.Top="20" Width="100" Height="30"
Content="(10,20)" />
<Button Canvas.Left="200" Canvas.Top="50" Width="100" Height="30"
Content="(200,50)" Click="Button_Click" />
<Button Canvas.Left="70" Canvas.Top="150" Width="100" Height="30"
Content="(70,150)" />
</Canvas>
</Grid>
すると、ボタンをクリックすればボタンのクリックイベントが呼ばれ、余白をクリックすれ
48
ば
Label_MouseLeftButtonDown
ハンドラが呼ばれます。(4) ComboBox
「Key: Value」のコレクションを並べる例を挙げます。
STEP 1/3:KeyValue
クラスを作成します。namespace WpfSample.Models {
public class KeyValue {
public string Key { get; set; } public string Value { get; set; }
public KeyValue(string key, string value) {
this.Key = key;
this.Value = value;
}
public override string ToString() => $"{Key}: {Value}";
} }
STEP 2/3:Xaml
でComboBox
のItemsSource
依存関係プロパティにComboItems
をパス で指定します。<ComboBox ItemsSource="{Binding Path=ComboItems}" Width="150" Height="25" />
STEP 3/3:VM
でComboItems
を定義します。// using WpfSample.Models;
public IEnumerable<KeyValue> ComboItems =>
new[] {
new KeyValue(key:"k1", value:"v1"), new KeyValue(key:"k2", value:"v2"), };
49 (5) ScrollViewer
基本的な使い方は、
HorizontalScrollBarVisibility
とVerticalScrollBarVisibility
の各依存関 係プロパティに値をセットして次のように使います。<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
Width="300" Height="200">
<StackPanel>
<StackPanel.Resources>
<Style TargetType="{x:Type Button}">
<Style.Setters>
<Setter Property="Width" Value="100" />
<Setter Property="Height" Value="30" />
</Style.Setters>
</Style>
</StackPanel.Resources>
<Button Content="{Binding Path=ButtonContents[0], Mode=OneWay}" />
<Button Content="{Binding Path=ButtonContents[1], Mode=OneWay}" />
<Button Content="{Binding Path=ButtonContents[2], Mode=OneWay}" />
<Button Content="{Binding Path=ButtonContents[3], Mode=OneWay}" />
<Button Content="{Binding Path=ButtonContents[4], Mode=OneWay}" />
<Button Content="{Binding Path=ButtonContents[5], Mode=OneWay}" />
<Button Content="{Binding Path=ButtonContents[6], Mode=OneWay}" />
<Button Content="{Binding Path=ButtonContents[7], Mode=OneWay}" />
</StackPanel>
</ScrollViewer>
ちなみに、VMでの
ButtonContents
の定義は次の通りです。public string[] ButtonContents =>
new[] { "0", "1", "2", "3", "4", "5", "6", "7", };
50
もう少し細かい制御をしたい場合は、System.Windows.Controls.ScrollViewerは
ScrollInfo
プロパティをprotected internal
で持っているため、ScrollViewerを継承して カスタムのScrollViewer
を作成するといいと思います。(6) DataGrid
System.Data.DataTable
をItemsSource
に次のように設定して使えます。Xaml側は例え ば次の通りです。<DataGrid ItemsSource="{Binding Path=DataGridSource}" />
VM
に定義するプロパティは、例えば次の通りです。DataTable dt;
public DataTable DataGridSource {
get { return dt; } set {
if (dt != value) { dt = value;
base.RaisePropertyChanged("DataGridSource");
} } }
System.Data.DataTable
の使い方はWindows
フォームと同様です。例えば、ビューのビハ インドで次のようにデータを作成し、VMのDataGridSource
プロパティに設定します。var dt = new DataTable("TestTable");
dt.Columns.AddRange(new[] {
new DataColumn(columnName:"Key"), new DataColumn(columnName:"Value"), new DataColumn(columnName:"Date"), });
dt.Rows.Add("k1", "v1", "2017/3/12");
dt.Rows.Add("k2", "v2", "2017/3/12");
dt.Rows.Add("k3", "v3", "2017/3/12");
51
System.Windows.Controls.DataGrid
には、CellStyle やColumnHeaderStyle
の各依存関係 プロパティなどがあり、見た目のカスタマイズを行う方法が提供されています。(7) ContentControl
System.Windows.Controls.ContentControl
は、object 型のContent
依存関係プロパティ を持っています。例えば、Content
にパネルを設定することにし、マウス左ボタン押下でパ ネルA
(StackPanel)、マウス右ボタン押下でパネルB
(WrapPanel)を設定する例を挙げま す。Xaml
は次の通りです。<ContentControl Content="{Binding Path=CustomContent}" />
VM
は次の通りです。object customContent;
public object CustomContent {
get { return customContent; } set {
if (customContent != value) { customContent = value;
base.RaisePropertyChanged("CustomContent");
} } }
ビューのビハインドのハンドラは次の通りです。
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) {
base.OnMouseLeftButtonDown(e);
var vm = this.DataContext as MainWindowViewModel;
var stackPanel = new StackPanel();
var textBlock = new TextBlock { Text = "SampleText", Width = 100d, Height = 30d };
var button = new Button { Content = "Content", Width = 100d, Height = 30d };
stackPanel.Children.Add(textBlock);
stackPanel.Children.Add(button);
vm.CustomContent = stackPanel;
}
protected override void OnMouseRightButtonDown(MouseButtonEventArgs e) {
base.OnMouseRightButtonDown(e);
var vm = this.DataContext as MainWindowViewModel;
52
var wrapPanel = new WrapPanel();var buttons = new Button[20];
for (var i = 0; i < buttons.Length; ++i) {
buttons[i] = new Button { Content = i, Width = 100d, Height = 30d };
wrapPanel.Children.Add(buttons[i]);
}
vm.CustomContent = wrapPanel;
}
10 カスタムコントロール
WPF
の大きな特徴の1
つは、標準で提供されているUIコントロールを組み合わせて、カスタムでコントロールを柔軟に作成することができることです。
カスタムで
UI
コントロールを作成するには、次の3
つの方法があります。(1) UI
コントロールのTemplate
プロパティにControlTemplate
を実装します。(2) System.Windows.Controls.UserControl
を継承して作成します (UIは主にXaml
で 記述します)。(3) System.Windows.ContentControl
を継承したクラスを継承して作成します (UIは主に
C#コードで記述します)。
(1)
の例例えば、
Label
をタグ付きのボタンとして扱うようにControlTemplate
を定義することが できます。RelativeSourceでTemplateParent
を指定していることにご注意ください。<Label Tag="タグ" Content="TEST">
<Label.Style>
<Style TargetType="{x:Type Label}">
<Style.Setters>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Tag,
RelativeSource={RelativeSource TemplatedParent}}"
Height="25" Margin="0 0 8 0" />
53
<Button Content="{Binding Path=Content,
RelativeSource={RelativeSource TemplatedParent}}"
Width="100" Height="25" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</Label.Style>
</Label>
他にも、上下
2
段にテキストを表示するLabel、文字数に応じて文字サイズを自動でスケー
リングするTextBox
くらいのカスタマイズならControlTemplate
の枠組みで実現できます。どうやって実現するかに興味がある方は、実現方法を考えてみて下さい。
ControlTemplate
をより実際的に使う状況があります。それは、WPF標準で提供されている
ComboBox
やMenu
などのUI
コントロールをカスタマイズする場合です。例えば、ComboBox
のデフォルトのスタイルとテンプレートは、MSDNの次のサイトで公開されています。
ComboBox
のスタイルとテンプレートhttps://msdn.microsoft.com/ja-jp/library/ms752094(v=vs.110).aspx
このコードを見ると、ComboBox は幾つもの
ControlTemplate
によって実現されているこ とがよく分かります。また、StoryBoard によるアニメーションがどこで使われているかと いうことや、ComboBox
のドロップダウンが、System.Windows.Controls.Primitives.Popup に よ っ て 構 成 さ れ て い る こ と な ど も 分 か り ま す 。 こ の コ ー ド を ダ ウ ン ロ ー ド し 、ResourceDictionary
としてComboBox.xaml
のように作成したファイルにコピーして、ControlTemplate
をカスタマイズすると、既存のComboBox
を変えることができます。ちなみに、StoryBoard によるアニメーションを削除するだけでパフォーマンスの改善が 可能です。
(2)
の例例えば、カスタムのステータスバーを
UserControl
で作成する例を挙げます。部品として 切り出すことで他のビューで利用することができ、VisualTree
がメンテしやすくなります。STEP 1/3:VisualStudio
で、[追加]→[新しい項目]→[ユーザーコントロール (WPF)] を 選択し、CustomStatusBar.xamlファイルを作成し、次のように記載します。54
<!-- CustomStatusBar.xaml -->
<UserControl x:Class="WpfSample.CustomStatusBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="24">
<DockPanel LastChildFill="True">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left" Margin="4 0 0 0">
<TextBlock Text="ステータス:" />
<TextBlock Text="{Binding Path=StatusText}" />
</StackPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right" Margin="0 0 4 0">
<TextBlock Text="メッセージ:" />
<TextBlock Text="{Binding Path=MessageText}" />
</StackPanel>
<Label />
</DockPanel>
</UserControl>
STEP 2/3:MainWindow.xaml
に、作成したCustomStatusBar.xaml
を埋め込みます。ここで、特に明示しなければ、CustomStatusBarの
DataContext
は、MainWindowのDataContext
を継承することに注意しておきます。<!-- MainWindow.xaml -->
<Window x:Class="WpfSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfs="clr-namespace:WpfSample"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<wpfs:MainWindowViewModel />
</Window.DataContext>
<DockPanel LastChildFill="True">
<wpfs:CustomStatusBar DockPanel.Dock="Bottom" Background="LightGray" />
<Label />
</DockPanel>
</Window>