Last Updated 2011/09/21
コントロールのレイアウトは、WPF アプリケーションを構築する上で、もっとも重要といっていいと思います。しかし、従来の Windows フォームアプリケーションのフォームのデザイン方法とは根本的に異なります。
私はフォームをデザインするとき、フォームデザイナを使いません。操作が冗長すぎると考えるからです。そこで、コードを直接編集しています。結果が即座に反映されるので、面倒と感じたことはありません。また、特に必要でない限り、Grid コントロールを使いません。
FrameworkElementクラスの Margin プロパティおよび Control クラスの Padding プロパティは、コントロールをレイアウトするときの重要な要素です。それぞれのプロパティの意味を下図に示します。
図の Margin と Padding はそれぞれ Control に属するプロパティです。
Margin プロパティは、Control の境界線と Container との距離で、Padding プロパティは Control 境界線と内部の Content との距離です。ともに、Thickness 型で設定します。つまり、各辺との距離は一定の値でもいいし、それぞれ異なる値を設定することもできます。
Margin プロパティと Padding プロパティとがなぜ重要かというと、コントロールによってデフォルトの値が設定されているため、それが常に適切とはいえないからです。たとえば、Label コントロールの Margin プロパティのデフォルトは 0 で、Padding プロパティ値は 5 です。
下図の上は StackPanel コントロールに 2 つの Label コントロールを水平に配置したところですが、上の Margin プロパティはデフォルトの 0 のまま、下は四辺とも 4 に設定しました。Label1 と Label2 との間隔は 8 になりますが、これを 4 にしたければ Label1 の右辺の Margin を 0 にすればいいです。
ちなみに、Thickness 型は左辺、上辺、右辺、下辺の順番で値を設定できます。
下図は、Label2 の Padding プロパティを 0 にしたところです。Label2 の高さはそのままにしましたが、高さを低くしてもテキストは正常に表示できるはずです。
オーナー描画のオーナーはアプリケーション開発者をさします。したがって、オーナー描画とは標準の描画とは異なり、開発者自身が明示的に描画をすることです。Windows フォームアプリケーションにおいて、たとえば色を選択する ComboBox コントロールを作る場合、DrawItem イベントを利用します。しかし、WPF アプリケーションにはオーナー描画という概念自体がありません。ないからといって、できないという意味ではありません。構築方法が異なるだけです。
ComboBox コントロールのリスト項目である ComboBoxItem クラスは ContentControl クラスを継承します。つまり、項目の中に別のコンテンツ(要素)を含めることができます。そこで、StackPanel コントロールをリスト項目のコンテンツとし、StackPanek コントロールの中に Border コントロールと TextBlock コントロールとを組み込みます。Border コントロールの内部を選択すべき色で塗りつぶし、TextBlock コントロールに色名を表示します。
Window1.xaml には ComboBox コントロールを配置し、 Height プロパティとMaxDropDownHeight プロパティを設定してください。また、Window1 の Loaded イベントハンドラ(window1_Loaded)を指定します。
<Window x:Class="ComboBoxTest.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:src="clr-namespace:ComboBoxTest" Title="Window1" Height="300" Width="300" Loaded="window1_Loaded" > <ComboBox Name="comboBox1" Height ="25" MaxDropDownHeight="200"/>
Window1.xaml.cs に以下のコードを書きます。
private void window1_Loaded(object sender, RoutedEventArgs e) { this.SetupComboBox(); if (comboBox1.Items.Count > 0) comboBox1.SelectedIndex = 0; } //---------------------------------------------------------------------------- private void SetupComboBox() { Type type = typeof(System.Windows.Media.Brushes); BindingFlags flags = BindingFlags.Static | BindingFlags.Public; // リフレクションを使って Brushes クラスのすべてのプロパティを取得する PropertyInfo[] infos = type.GetProperties(flags); foreach (PropertyInfo info in infos) { SolidColorBrush brush = info.GetValue(null, null) as SolidColorBrush; ComboBoxItem item = new ComboBoxItem(); StackPanel panel = new StackPanel(); panel.Orientation = Orientation.Horizontal; Border border = new Border(); border.Background = brush; border.CornerRadius = new CornerRadius(3); border.Width = 30; border.Height = 12; border.BorderBrush = Brushes.Black; border.BorderThickness = new Thickness(1); TextBlock block = new TextBlock(); block.Text = info.Name; block.Width = comboBox1.Width - border.Width - 20; block.Margin = new Thickness(10, 0, 0, 0); panel.Children.Add(border); panel.Children.Add(block); item.Content = panel; comboBox1.Items.Add(item); } }
アクセスキーについては、「メニューとステータスバー」-「アクセスキー」の項で触れましたので、ここでは Label コントロールに関係するトピックを取り上げます。
TextBox コントロールのようにアクセスキーを設定することができないコントロールの場合、Label コントロールにアクセスキーを設定し、その Target プロパティに TextBox コントロールを指定するとアクセスキーによるフォーカスの移動が可能となります。
<TextBox Name="textBox1" Width="50"/> <Label Target="{Binding ElementName=textBox1}">ファイル名(_F)</Label>
コントロールのレイアウトに関係するコントロールをリストアップします。ScrollViewer コントロールを除いて、そのほかはすべて Panel クラスから派生するクラスです。Panel クラスはそのほかのパネルコントロールの基本クラスですが、System.Windows.Forms.Panel クラスとはまったく性格が異なります。なお、ここでは VirtualizingPanel、VirtualizingStackPanel、TabPanel、ToolBarOverflowPanel の各コントロールについての解説はしません。
フォーム上にコントロールを配置するときの手順は、Windows フォームアプリケーションの場合とはまったく異なります。そこで、まず WPF アプリケーション的レイアウトの基本について説明します。
.Net Framework による Windows フォームアプリケーションを含めて、伝統的な Windows アプリケーションはコントロールを絶対的な座標値で位置とサイズを設定します。したがって、アプリケーションを実行する環境(たとえば、ディスプレイの解像度)によってアプリケーションの外観がデザイン時とは異なる場合があります。WPF アプリケーションはこれを避ける、つまり、どんな環境でも外観がくずれないようにすることがもとめられます。そのために、コントロールの位置とサイズを固定化しないようにします。たとえば、コントロールの Width プロパティと Height プロパティには "Auto" という設定が可能で、これはコントロールの幅と高さは WPF におまかせという意味になります。
ただし、Canvas コントロールだけは例外で、このコントロールは絶対的な座標値で設定することは可能です。しかし、ベクターグラフィックスの場合にだけ使うことが推奨されています。
WPF の各コントロールには Left プロパティと Top プロパティがないことに気付くはずです。前の項で説明したとおり、WPF ではコントロールを絶対的な座標値で位置決めすることはしたくてもできないということです。
WPF の各コントロールには Width プロパティと Height プロパティのほかに、ActualWidth プロパティ と ActualHeight プロパティがあります。"actual" は「実際の」という意味ですから、それぞれ 実際の Width プロパティ、実際の Height プロパティという意味になります。
コントロールの Width プロパティ と Height プロパティを設定すると、WPF のレイアウトエンジンはコントロールを実際に配置し、その結果を ActualWidth プロパティ と ActualHeight プロパティに格納します。つまり、あるコントロールの幅を Width プロパティで設定したとしてもそのコントロールの実際の幅が設定どおりになるかどうかは分かりません。したがって、実際のコントロールの幅が知りたければ ActualWidth プロパティを調べなければなりません。
これについてはすでに説明したとおりです。パネルコントロールを使ってコントロールのおおまかな位置決めをしたあと、微調整は Margin プロパティと Padding プロパティを利用します。
UIElement クラスに ClipToBounds プロパティがあります。デフォルトは false ですが、true に設定すると、コントロールの境界線で子要素をクリッピングします。つまり、親要素の表示領域を子要素がはみ出る場合、子要素のはみ出た部分を描画しません。
<Canvas VerticalAlignment="Center" HorizontalAlignment="Center" Width="100" Height="80" Background="LightBlue" ClipToBounds="true"> <Border Canvas.Left="40" Canvas.Top="-10" Width="80" Height="40" BorderThickness="2" BorderBrush="Red" /> </Canvas>
Windows フォームアプリケーションにおける ListBox コントロールは Windows 標準のコントロールを使っていますが、WPF アプリケーションの ListBox コントロールは WPF コントロールを組み合わせて作ったものです。WPF アプリケーションで ListBox コントロールを自作することはそれほど難しい作業ではありません。つまり、ScrollView コントロール内に StackPanel コントロールを配置し、その中に TextBlock コントロールを積み上げるだけです。
ScrollViewer コントロールは Panel クラスを継承しませんが、コントロールのレイアウトと関連していますので、あえて取り上げました。
WPF のコントロールには Anchor プロパティがありません。Anchor プロパティがあれば、下図のようにコントロールを配置するとき、フォームの幅を変化させると右端にあるボタンコントロールは常にフォームの右端を維持する効果を与えることができます。これと同じ効果は、DockPanel コントロールを利用すると実現可能です。以下のコードは、テキストボックスの幅をフォームの幅に合わせて伸縮します。
<DockPanel Margin="10"> <StackPanel Orientation="Vertical" DockPanel.Dock="Left"> <TextBlock Height="30" VerticalAlignment="Center">入力ファイル名</TextBlock> <TextBlock Height="30" VerticalAlignment="Center">フォルダ名</TextBlock> <TextBlock Height="30" VerticalAlignment="Center">カラー</TextBlock> </StackPanel> <StackPanel Orientation="Vertical" DockPanel.Dock="Right" Margin="4,0,4,0"> <ctrl:CommonDialogButton x:Name="btnFile" Width="26" Height="26" Click="btnFile_Click" Margin="0,2,0,2" DockPanel.Dock="Right"> <Image Source="images/openHS.png" Width="16" Height="16" /> </ctrl:CommonDialogButton> <ctrl:CommonDialogButton x:Name="btnFolder" Width="26" Height="26" Click="btnFolder_Click" Margin="0,2,0,2" DockPanel.Dock="Right"> <Image Source="images/NewFolderHS.png" Width="16" Height="16" /> </ctrl:CommonDialogButton> <ctrl:CommonDialogButton x:Name="btnColor" Width="26" Height="26" Click="btnColor_Click" Margin="0,2,0,2" DockPanel.Dock="Right"> <Image Source="images/DisplayInColorHS.png" Width="16" Height="16" /> </ctrl:CommonDialogButton> </StackPanel> <StackPanel Orientation="Vertical"> <TextBox Name="txtFileName" Height="25" Margin="4,2,4,2"></TextBox> <TextBox Name="txtFolderName" Height="25" Margin="4,2,4,2"></TextBox> <TextBox Name="txtColor" Height="25" Margin="4,2,4,2"></TextBox> </StackPanel> </DockPanel>
3 つの TextBlock は StackPanel に入れていますが、こうしておくとテキストの位置がそろいます。同じ理由で Button と TextBox もそうしました。
私は Grid コントロールを使うことを好みません。自由度が高いという点では便利なコントロールではありますが、私は避けています。現に、このサイトで公開するサンプルプロジェクトでも Grid コントロールは使っていません。その理由はコードを読むのがひどく面倒の一言です。また、Grid コントロールにこだわらなくても手があるという理由もあります。もちろん、コントロールを不規則に配置する場合はやむを得ず使う場合はあります。
フォームのビジュアルなデザインが可能というメリットはありますが、Visual Studio の動作が不安定で、そんなに便利なのかなと思わないでもありません。私はフォームにコントロールを配置するとき、XAML コードを直接編集しています。
カスタムコントロールのページで紹介するサンプルプロジェクトは、Anchor プロパティと同じ効果を得られるテクニックを含んでいますので、興味のある人はダウンロードしてください。
−以上−