WPFにおけるGUI構築講座 -座標ベタ書きから脱却しよう-

# 概要
　Visual Studioでは、「GUI上で」「簡単に」ウィンドウの上にオブジェクトを配置して開発できます。
　ですが、そのせいで「**テキトーに配置して作るとマズい**」という事実に初学者はなかなか気づきません<sup>[[要出典](https://ja.wikipedia.org/wiki/Wikipedia:「要出典」をクリックされた方へ)]</sup>。
　今回はWPFを例に、ダメなGUI設計の例と、それをどうすれば改善できるかを示していきます。

※今回紹介する範囲内では、メニューなどを利用して**GUIの見た目を大きく変える方向の修正は行いません。**

# 今回改修するGUI

![image](https://qiita-image-store.s3.amazonaws.com/0/29802/492d999a-7ada-6495-5229-0d782602bc56.png)

```xml
<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="170" Width="300">
	<Grid>
		<TextBox x:Name="AddLeftTextBox" HorizontalAlignment="Left" Height="23" Margin="14,18,0,0" TextWrapping="Wrap" Text="1" VerticalAlignment="Top" Width="120"/>
		<TextBox x:Name="AddRightTextBox" HorizontalAlignment="Left" Height="23" Margin="156,18,0,0" TextWrapping="Wrap" Text="2" VerticalAlignment="Top" Width="120"/>
		<TextBlock HorizontalAlignment="Left" Margin="139,19,0,0" TextWrapping="Wrap" Text="＋" VerticalAlignment="Top" RenderTransformOrigin="-9.667,-5.618"/>
		<TextBlock HorizontalAlignment="Left" Margin="139,67,0,0" TextWrapping="Wrap" Text="＝" VerticalAlignment="Top"/>
		<TextBox x:Name="AnswerTextBox" HorizontalAlignment="Left" Height="23" Margin="156,60,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="120"/>
		<Button x:Name="CalcButton" Content="計算" HorizontalAlignment="Left" Margin="108,100,0,0" VerticalAlignment="Top" Width="75" RenderTransformOrigin="0.502,1.6"/>
	</Grid>
</Window>
```

　……いかにも「XAMLデザイナーからポン付けしました」と言わんばかりのXAMLですが、(レイアウト)コンテナを知らなかった頃はこれでも良かったのです。なにせ、テキストボックスを動かすと**こんな赤い線がガイドしてくれる**のですよ？　Visual Studioが気を利かせてくれていると思うじゃないですか。それが罠だと気づかずに……。

![image](https://qiita-image-store.s3.amazonaws.com/0/29802/2d51f6a8-a5dd-5885-b90a-8920b925f237.png)

　このようにポン付けしたコードでまず問題になるのは、「オブジェクトを増やしていくと位置合わせが面倒だ」ということです。
　まず、**赤い線は同じ端には揃えられるが中央揃えには使えない**ため、中央揃えしたい場合はちまちま(`Window`直下の`Grid`に対する)`Margin`を弄る必要があります。
　また、オブジェクトをコピペして利用する場合も、`Margin`の値が同じだと同じ位置になるため、またちまちま数値を弄る必要があります。1つ2つならともかく、5個や6個となるとかなり面倒臭いですし、動的にオブジェクトを増やしたい際には「置き場所を調整するため数値を計算して……」といったことまで考えなければなりません。
　更に、ポン付けするとオブジェクトに`Width`や`Height`が全部入力されているため、例えば「この列にあるオブジェクトの横幅を全部10pxだけ縮めたい」と思った際は**地獄のWidth修正作業が待っています(※)。**
　とにかく、ポン付けのGUIはメンテしづらいのが問題なのです。

※ここだけ見ると、「マウスで選択して伸縮すれば」操作できますが、後述するコンテナを使えばもっと楽ができます

# コンテナとオブジェクトの関係について
　先のようなポン付けを改善するため、まずは**コンテナ**についてざっくり学びましょう。
　先のXAMLにあった<Grid>もコンテナの一つで、本来なら**領域を表形式に分割して配置する**機能を持ちます。
　ところがXAMLデザイナーの場合、WinForms時代よろしくポン付けできることを優先した結果、**とりあえずGridの上にMargin・Width・Heightでガチガチに位置決めしたオブジェクトを配置する**といった雑な処理をしてしまいます。これではせっかくのコンテナの性能が生かされません。

## Grid

　領域を分割して、分割後の領域にオブジェクトやコンテナを置くことができます。
　下図のように、分割する際は幅(px単位)や比率を指定することができます。

![image](https://qiita-image-store.s3.amazonaws.com/0/29802/27d88550-72e7-c30a-46f1-af9d233b3f27.png)

```xml
<Grid>
	<Grid.RowDefinitions>
		<RowDefinition Height="50" />
		<RowDefinition Height="100" />
	</Grid.RowDefinitions>
	<Grid.ColumnDefinitions>
		<ColumnDefinition Width="1*" />
		<ColumnDefinition Width="2*" />
		<ColumnDefinition Width="3*" />
	</Grid.ColumnDefinitions>
</Grid>
```

　また本来、オブジェクトの`Margin`はオブジェクトの周囲のコンテナとの距離を指定するものですので、このように区切っておくことにより、**WidthやHeightを指定しなくとも自動でサイズを計算して広がってくれます**。
　更に、`Grid.ColumnSpan`や`Grid.RowSpan`を設定すれば縦横2マス以上に跨って配置できますし、`HorizontalAlignment`や`VerticalAlignment`を使えば(区切られた範囲内で)中央揃えや右寄せ左寄せなどが簡単に設定できます。オブジェクトが端に寄りすぎて困る場合は、それこそ`Margin`を設定すればいいのです。
(`Margin`や`HorizonalAlignment`についての説明画像→[WPF 個人用メモ.1 - Qiita](http://qiita.com/Temarin/items/5c02d3597a699aa38d9e))

![image](https://qiita-image-store.s3.amazonaws.com/0/29802/00e91802-4823-60e7-f60f-90947746bb5c.png)

```xml
<Grid>
	<Grid.RowDefinitions>
		<RowDefinition Height="50" />
		<RowDefinition Height="*" />
	</Grid.RowDefinitions>
	<Grid.ColumnDefinitions>
		<ColumnDefinition Width="1*" />
		<ColumnDefinition Width="2*" />
		<ColumnDefinition Width="3*" />
	</Grid.ColumnDefinitions>
	<TextBlock Text="LongTextBlock" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" FontSize="30"
                   Margin="5,5,5,5" HorizontalAlignment="Center" VerticalAlignment="Center"/>
	<Button Content="Click!" Grid.Row="1" Grid.Column="1" Margin="10, 10, 10, 10"/>
</Grid>
```

## StackPanel・WrapPanel
　どちらも、領域にオブジェクトやコンテナを、縦か横に並べることができます。
　デフォルトでは縦に並びますが、`Orientation="Horizontal"`と設定すれば横に並びます。

![image](https://qiita-image-store.s3.amazonaws.com/0/29802/23f70c90-306b-89b0-d6d2-412832a50985.png)

```xml
<StackPanel>
	<Button Content="Button1"/>
	<Button Content="Button2"/>
	<Button Content="Button3"/>
	<Button Content="Button4"/>
	<Button Content="Button5"/>
</StackPanel>
```

![image](https://qiita-image-store.s3.amazonaws.com/0/29802/645699f1-81c7-4611-685c-4b9fffc91a9f.png)

```xml
<StackPanel Orientation="Horizontal">
	<Button Content="Button1"/>
	<Button Content="Button2"/>
	<Button Content="Button3"/>
	<Button Content="Button4"/>
	<Button Content="Button5"/>
</StackPanel>
```

　この2種類のPanelの違いは、**縦 or 横に大量に並べた際に折り返すか折り返さないか**です。
　名前の通り`WrapPanel`は折り返す方、`StackPanel`は折り返さない方となります。

![image](https://qiita-image-store.s3.amazonaws.com/0/29802/5ac67de8-e7ac-9ff4-3353-fdb47c581d13.png)

```xml
<Grid>
	<Grid.ColumnDefinitions>
		<ColumnDefinition/>
		<ColumnDefinition/>
	</Grid.ColumnDefinitions>
	<StackPanel Grid.Column="0">
		<Button Content="Button1" Width="60"/>
		<Button Content="Button2" Width="60"/>
		<Button Content="Button3" Width="60"/>
		<Button Content="Button4" Width="60"/>
		<Button Content="Button5" Width="60"/>
		<Button Content="Button6" Width="60"/>
		<Button Content="Button7" Width="60"/>
		<Button Content="Button8" Width="60"/>
		<Button Content="Button9" Width="60"/>
		<Button Content="Button10" Width="60"/>
	</StackPanel>
	<WrapPanel Grid.Column="1">
		<Button Content="Button1" Width="60"/>
		<Button Content="Button2" Width="60"/>
		<Button Content="Button3" Width="60"/>
		<Button Content="Button4" Width="60"/>
		<Button Content="Button5" Width="60"/>
		<Button Content="Button6" Width="60"/>
		<Button Content="Button7" Width="60"/>
		<Button Content="Button8" Width="60"/>
		<Button Content="Button9" Width="60"/>
		<Button Content="Button10" Width="60"/>
	</WrapPanel>
</Grid>
```

　ここで、**「折り返さないのならスクロールさせたい」**といった願望が出てくるかと思いますが、ちゃんとそれ用のコンテナとして`ScrollViewer`が用意されています。これで覆うことにより、スクロールバーが出現してスクロール可能になります。

![image](https://qiita-image-store.s3.amazonaws.com/0/29802/fe234d70-96a7-aab4-7368-3dc1a86c0b07.png)

```xml
<Grid>
	<Grid.ColumnDefinitions>
		<ColumnDefinition/>
		<ColumnDefinition/>
	</Grid.ColumnDefinitions>
	<ScrollViewer Grid.Column="0">
		<StackPanel>
			<Button Content="Button1" Width="60"/>
			<Button Content="Button2" Width="60"/>
			<Button Content="Button3" Width="60"/>
			<Button Content="Button4" Width="60"/>
			<Button Content="Button5" Width="60"/>
			<Button Content="Button6" Width="60"/>
			<Button Content="Button7" Width="60"/>
			<Button Content="Button8" Width="60"/>
			<Button Content="Button9" Width="60"/>
			<Button Content="Button10" Width="60"/>
		</StackPanel>
	</ScrollViewer>
	<WrapPanel Grid.Column="1">
		<Button Content="Button1" Width="60"/>
		<Button Content="Button2" Width="60"/>
		<Button Content="Button3" Width="60"/>
		<Button Content="Button4" Width="60"/>
		<Button Content="Button5" Width="60"/>
		<Button Content="Button6" Width="60"/>
		<Button Content="Button7" Width="60"/>
		<Button Content="Button8" Width="60"/>
		<Button Content="Button9" Width="60"/>
		<Button Content="Button10" Width="60"/>
	</WrapPanel>
</Grid>
```

## Canvas

　先ほど説明した`Grid`や`StackPanel`などは、オブジェクトを置く位置をピクセル単位ではなく、相対的な位置取りで決定します。
　なので楽ができるという話をしていたのですが、場合によってはピクセル単位で決めたいこともあります。その場合に使うコンテナが`Canvas`です。

![image](https://qiita-image-store.s3.amazonaws.com/0/29802/b6cce330-7794-8fd9-b7db-25f62f74bc14.png)

```xml
<Canvas>
	<Button Canvas.Left="10" Canvas.Top="10" Content="A" Width="40" Height="40"/>
	<Button Canvas.Left="116" Canvas.Top="19" Content="B" Width="25" Height="101"/>
	<Button Canvas.Left="10" Canvas.Top="61" Content="C" Width="78" Height="40"/>
	<Button Canvas.Left="157" Canvas.Top="49" Content="D" Width="111" Height="40"/>
</Canvas>
```

## DockPanelなど
　その他のコンテナについては、次の記事が分かりやすいでしょう。
　　[連載：WPF入門：第7回　WPF UI要素の基礎とレイアウト用のパネルを学ぼう (2_2) - ＠IT](http://www.atmarkit.co.jp/ait/articles/1011/30/news116_2.html)

# コンテナを利用して改修してみる

　では、冒頭で挙げたGUIを例に、コンテナでどう修正されるかを見ていきます。
　`Canvas`で並べていけないわけではないのですが、オブジェクトの縦方向や横方向の位置を揃えたいので`Grid`でまずは区切ります。

![image](https://qiita-image-store.s3.amazonaws.com/0/29802/6ee48081-5345-5ebe-7ed9-e4c73e065576.png)

```xml
<Grid Margin="10,10,10,10">
	<Grid.RowDefinitions>
		<RowDefinition />
		<RowDefinition />
		<RowDefinition />
	</Grid.RowDefinitions>
	<Grid.ColumnDefinitions>
		<ColumnDefinition Width="4*" />
		<ColumnDefinition Width="1*" />
		<ColumnDefinition Width="4*" />
	</Grid.ColumnDefinitions>
</Grid>
```

次に、Gridの各マスにオブジェクトを配置します。画面下の計算ボタンは中央に配置したいので、`Grid`の下段3マスをぶち抜くような設定にします。

![image](https://qiita-image-store.s3.amazonaws.com/0/29802/4318830a-27d0-bb01-2859-fc95889c8d1f.png)

```xml
<Grid Margin="10,10,10,10">
	<Grid.RowDefinitions>
		<RowDefinition />
		<RowDefinition />
		<RowDefinition />
	</Grid.RowDefinitions>
	<Grid.ColumnDefinitions>
		<ColumnDefinition Width="4*" />
		<ColumnDefinition Width="1*" />
		<ColumnDefinition Width="4*" />
	</Grid.ColumnDefinitions>
	<TextBox Grid.Row="0" Grid.Column="0" x:Name="AddLeftTextBox" Text="1" />
	<TextBlock Grid.Row="0" Grid.Column="1" Text="＋" />
	<TextBox Grid.Row="0" Grid.Column="2" x:Name="AddRightTextBox" Text="2" />
	<TextBlock Grid.Row="1" Grid.Column="1" Text="＝" />
	<TextBox Grid.Row="1" Grid.Column="2" x:Name="AnswerTextBox" />
	<Button Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" x:Name="CalcButton" Content="計算" />
</Grid>
```

　ただ、これだと見栄えが良くありません。`Grid`の項で説明したように、`Grid`に配置したオブジェクトやコンテナは、`Width`や`Height`を設定しないと目一杯にまで広がってしまいます。また、`TextBox`オブジェクトは[デフォルトだと複数行の入力が行なえません](https://code.msdn.microsoft.com/windowsdesktop/XAML-WPF-bcac5a0f)ので、1行入力なのに縦に広いと誤解を招きそうです。
　そこで、各オブジェクトに対して次のように属性を設定します。
- `TextBox`は`Height`と`Margin`を設定することで、最低限のマージンと高さを持った上で横に広がるようにした
- `TextBlock`は`VerticalAlignment`と`HorizontalAlignment`を設定することで、中央に寄せるようにした
- `Button`は`Width`と`Height`を設定することで適切なサイズにした
 - 周囲の`Grid`との間隔との方を大切にするなら、`Margin`だけ設定してもいい

![image](https://qiita-image-store.s3.amazonaws.com/0/29802/29a6c553-e3bd-4df0-c387-9c5b4915fa90.png)

```xml
<Grid Margin="10,10,10,10">
	<Grid.RowDefinitions>
		<RowDefinition />
		<RowDefinition />
		<RowDefinition />
	</Grid.RowDefinitions>
	<Grid.ColumnDefinitions>
		<ColumnDefinition Width="4*" />
		<ColumnDefinition Width="1*" />
		<ColumnDefinition Width="4*" />
	</Grid.ColumnDefinitions>
	<TextBox Grid.Row="0" Grid.Column="0" x:Name="AddLeftTextBox" Text="1" Height="20" Margin="10,10,10,10"/>
	<TextBlock Grid.Row="0" Grid.Column="1" Text="＋" VerticalAlignment="Center" HorizontalAlignment="Center"/>
	<TextBox Grid.Row="0" Grid.Column="2" x:Name="AddRightTextBox" Text="2" Height="20" Margin="10,10,10,10"/>
	<TextBlock Grid.Row="1" Grid.Column="1" Text="＝" VerticalAlignment="Center" HorizontalAlignment="Center"/>
	<TextBox Grid.Row="1" Grid.Column="2" x:Name="AnswerTextBox" Height="20" Margin="10,10,10,10"/>
	<Button Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" x:Name="CalcButton" Content="計算" Width="80" Height="20"/>
</Grid>
```

　ここまで調整した上で、冒頭のGUIと見比べてみましょう。外見はほぼ変化していませんが、

![image](https://qiita-image-store.s3.amazonaws.com/0/29802/492d999a-7ada-6495-5229-0d782602bc56.png)

![image](https://qiita-image-store.s3.amazonaws.com/0/29802/1f6dea5f-78bd-ce77-b7b2-0280ba995082.png)

中身は大違いだと分かります。後者の方が全体的にスッキリしているはずです。

```xml
<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="170" Width="300">
	<Grid>
		<TextBox x:Name="AddLeftTextBox" HorizontalAlignment="Left" Height="23" Margin="14,18,0,0" TextWrapping="Wrap" Text="1" VerticalAlignment="Top" Width="120"/>
		<TextBox x:Name="AddRightTextBox" HorizontalAlignment="Left" Height="23" Margin="156,18,0,0" TextWrapping="Wrap" Text="2" VerticalAlignment="Top" Width="120"/>
		<TextBlock HorizontalAlignment="Left" Margin="139,19,0,0" TextWrapping="Wrap" Text="＋" VerticalAlignment="Top" RenderTransformOrigin="-9.667,-5.618"/>
		<TextBlock HorizontalAlignment="Left" Margin="139,67,0,0" TextWrapping="Wrap" Text="＝" VerticalAlignment="Top"/>
		<TextBox x:Name="AnswerTextBox" HorizontalAlignment="Left" Height="23" Margin="156,60,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="120"/>
		<Button x:Name="CalcButton" Content="計算" HorizontalAlignment="Left" Margin="108,100,0,0" VerticalAlignment="Top" Width="75" RenderTransformOrigin="0.502,1.6"/>
	</Grid>
</Window>
```

```xml
<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="170" Width="300">
	<Grid Margin="10,10,10,10">
		<Grid.RowDefinitions>
			<RowDefinition />
			<RowDefinition />
			<RowDefinition />
		</Grid.RowDefinitions>
		<Grid.ColumnDefinitions>
			<ColumnDefinition Width="4*" />
			<ColumnDefinition Width="1*" />
			<ColumnDefinition Width="4*" />
		</Grid.ColumnDefinitions>
		<TextBox Grid.Row="0" Grid.Column="0" x:Name="AddLeftTextBox" Text="1" Height="20" Margin="10,10,10,10"/>
		<TextBlock Grid.Row="0" Grid.Column="1" Text="＋" VerticalAlignment="Center" HorizontalAlignment="Center"/>
		<TextBox Grid.Row="0" Grid.Column="2" x:Name="AddRightTextBox" Text="2" Height="20" Margin="10,10,10,10"/>
		<TextBlock Grid.Row="1" Grid.Column="1" Text="＝" VerticalAlignment="Center" HorizontalAlignment="Center"/>
		<TextBox Grid.Row="1" Grid.Column="2" x:Name="AnswerTextBox" Height="20" Margin="10,10,10,10"/>
		<Button Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" x:Name="CalcButton" Content="計算" Width="80" Height="20"/>
	</Grid>
</Window>
```

# まとめ
　以上のように、コンテナを理解して構築すると、スッキリとしたXAMLに仕上がることが分かりました。
　ちなみに先ほどのGUIの例ですが、唐突にウィンドウサイズを400x250にすると次のように変化します。
　「左上からの位置」を維持するポン付けと、「コンテナから導かれる位置関係」から再計算される手打ちとの差は明らかでしょう。

![image](https://qiita-image-store.s3.amazonaws.com/0/29802/e3bd917f-7dd3-e721-fdbc-02a58d432081.png)

![image](https://qiita-image-store.s3.amazonaws.com/0/29802/814198ac-0560-a031-50b2-f8aeb00be2a1.png)