WPFでプログラムしてみよう(2)


私が勉強で使っているテキストはこちらです。

Windows Presentation Foundation プログラミング入門

Windows Presentation Foundation プログラミング入門


今回は勉強するための環境作成と3章の「パネルとレイアウト」についてまとめます。

分類 私の環境
OS Windows Vista SP1
Framework .NET Framework3.5
editor gvim 7.1

テスト環境作成

いつもIDEを使って開発をしていたので、今回のようにIDE無しの環境でプログラムを作るのが面倒に感じられます。ちょっと手抜きをするために、ソースのテンプレートを準備してそれをコピーするバッチファイルを作成しました。


今回、開発をするにあたってフォルダ階層はこのようにしました。

d:\pg\WPF>tree /F
フォルダ パスの一覧
ボリューム シリアル番号は 0006F240 2862:
D:.
│  c.bat
│
├─exe
│
├─source
│
└─template
        template.cs


で、テンプレートファイルをコピーするバッチファイルを作成しました。ファイル(D:\pg\WPF\c.bat)の中はこんな感じです。

@ECHO OFF

CLS

:SELECTPROCESS
ECHO テンプレートファイルを作成しますか?
SET /PMKFILE=[Y:作る/N:作らない/D:一覧表示]

DEL .\*.*~ > NUL
MOVE *.exe .\exe > NUL
MOVE *.cs .\source > NUL

IF /I "%MKFILE%" EQU "D" GOTO SOURCEVIEW
IF /I "%MKFILE%" EQU "Y" GOTO MAKEFILE
IF /I "%MKFILE%" EQU "N" GOTO FINISH

:SOURCEVIEW
	CLS
	ECHO ● ソースファイル一覧
	DIR /ON "%~dp0source\*.cs" | FIND /I ".cs"
	GOTO SELECTPROCESS


:MAKEFILE
CLS
  SET NEWFILENAME=sample.cs
  ECHO ファイル名を入力してください[止める場合は.を入力]
  SET /PNEWFILENAME=[初期値:%NEWFILENAME%]?

  IF /I "%NEWFILENAME%" EQU "." GOTO FINISH

  COPY /Y "%~dp0template\template.cs" .\%NEWFILENAME%
  VI.EXE "%~dp0%NEWFILENAME%"


:FINISH
CLS

チョーシンプル。Simple is Bestです。
で。テンプレートファイル(D:\pg\WPF\template\template.cs)はこんな感じです。

using System;
using System.Windows;
using System.Windows.Controls;

class Test {
        [STAThread]
        public static void Main(){


                Window wnd = new Window();


                Application app = new Application();
                app.Run(wnd);
        }
}


今後、パターンが増えてきたらもう少しいいテンプレートを考案します。
というわけで開発環境の作成は終わり。

第3章 パネルとレイアウト

キャンバス

画面上にボタンなどを配置するためにCanvasクラスを利用します。

    1. 画面上の位置を指定する場合(左) → Canvas.SetLeft(UIElement,int)
    2. 画面上の位置を指定する場合(上) → Canvas.SetTop(UIElement,int)
    3. 重ね合わせ順を指定する場合   → Canvas.SetZIndex(UIElement,int)


では画面上にボタンを重ね合わせて表示して、押されたボタンが最上位に移動するようなプログラムを作成してみます。

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;


class Test {
	[STAThread]
	public static void Main(){
		Button button1 = new Button();
		button1.Content = "button1";
		button1.FontSize = 50;
		Canvas.SetLeft(button1,10);
		Canvas.SetTop(button1,10);
		Canvas.SetZIndex(button1,0);


		Button button2 = new Button();
		button2.Content = "BUTTON2";
		button2.FontSize = 50;
		Canvas.SetLeft(button2,100);
		Canvas.SetTop(button2,30);
		Canvas.SetZIndex(button2,2);


		Button button3 = new Button();
		button3.Content = "ボタン3";
		button3.FontSize = 50;
		Canvas.SetLeft(button3,200);
		Canvas.SetTop(button3,50);
		Canvas.SetZIndex(button2,2);

		button1.Click += (sender, e) => {
			Canvas.SetZIndex(button1,2);
			Canvas.SetZIndex(button2,1);
			Canvas.SetZIndex(button3,0);
		};

		button2.Click += (sender, e) => {
			Canvas.SetZIndex(button2,2);
			Canvas.SetZIndex(button3,1);
			Canvas.SetZIndex(button1,0);
		};

		button3.Click += (sender, e) => {
			Canvas.SetZIndex(button3,2);
			Canvas.SetZIndex(button1,1);
			Canvas.SetZIndex(button2,0);
		};

		Canvas cnvs1 = new Canvas();
		cnvs1.Children.Add(button1);
		cnvs1.Children.Add(button2);
		cnvs1.Children.Add(button3);

		Window wnd = new Window();
		wnd.Content = cnvs1;

		Application app = new Application();
		app.Run(wnd);
	}

}


SetLeft,SetTop,SetZIndexは前述のアタッチプロパティを介して各オブジェクトの位置/表示順を指定しています。
これをコンパイルして実行してみます。

    • まず起動するとボタンが3つ重なり合って表示
    • で、隠れているボタンを押してみると...
    • 押されたボタンが最前面になりました


イベントハンドラがそのまま書けるのはとても便利でいいと思います。短い処理で汎用性がなければかなり重宝しそうです。


一応こんな感じでCanpasを利用してオブジェクトを配置出来るのですが、実際にはあまり使わない方法だとか。なんじゃそりゃ...。

スタックパネル

キャンバスの次はスタックパネル。名前からしてスタックのようにしてオブジェクトを管理するパネルでしょうか。
ひとまずスタックパネルでボタンを管理するサンプルを書いてみました。

using System;
using System.Windows;
using System.Windows.Controls;

class Test {
	[STAThread]
	public static void Main(){
		StackPanel stkpnl1 = new StackPanel();
		stkpnl1.Orientation = Orientation.Horizontal;

		string[] texts = {"ほげほげ1", "もげもげ", "ふがふが3", "フー4", "バー5"};

		foreach (string text in texts){
			Button btn  = new Button();
			btn.FontSize = 30;
			btn.Content = text;

			btn.Click += (sender, e) => {
				stkpnl1.Children.Remove(sender as Button);
			};

			stkpnl1.Children.Add(btn);
		}

		Window wnd = new Window();
		wnd.Content = stkpnl1;


		Application app = new Application();
		app.Run(wnd);
	}
}


押したボタンから消えていきますが、よく考えたらもっとスタックらしいプログラムを書けばよかった...。
ま、いいや。とりあえずこんなことも出来るよってことで。

ドックパネル

ドックパネルは自身の子要素を上下左右に配置するためのパネルです。

using System;
using System.Windows;
using System.Windows.Controls;

class Test {
	[STAThread]
	public static void Main(){
		DockPanel dkPanel1 = new DockPanel();

		Button[] btns   = new Button[5];
		string[] texts  = {"Top", "Bottom", "Left", "Right", "Center"};
		Dock[] DockType = {Dock.Top, Dock.Bottom, Dock.Left, Dock.Right}; 

		for (int i = 0 ;  i < btns.Length ; i++){
			btns[i]          = new Button();
			btns[i].Content  = texts[i];
			btns[i].FontSize =  30;
			if (i == 4) {
				btns[i].Click += (sender, e) => {
					dkPanel1.LastChildFill = !dkPanel1.LastChildFill;
				};
			} else {
				DockPanel.SetDock(btns[i], DockType[i]);
			}
			dkPanel1.Children.Add(btns[i]);
		}

		Window wnd = new Window();
		wnd.Content = dkPanel1;

		Application app = new Application();
		app.Run(wnd);
	}
}


ここで出てきているDockPanel.SetDock()はアタッチプロパティを利用しています。少しずつ見慣れてきたとはいえ、やはりなじめない書き方です。


とりあえず動かして見ます。

    • 実行するとボタンが表示されて...
    • 押すと中央部分の大きさが変わります


これは中央部分のボタン(Centerとテキストが書かれたボタン)のクリックイベントにDockPanelのLastChildFillプロパティをtrue/falseを切り替えるようにしたためです。このプロパティ次第で最後に追加されたCenter用のボタンが空き部分を作るのかどうか挙動が変わります。


このレイアウトは結構使いそうなので(テキストにもよく使うと書いてあります)覚えておいた方がよさそうです。

グリッド

表のようなレイアウトを実現するのがグリッドです。
グリッドで表を表現するためには列と行を設定する必要があります。


大まかな流れは以下のとおりです

    1. Gridクラスを作成する(grdと命名)
    2. grdに追加したい列数分、ColumnDefinitionsにColumnDefinitionクラスのインスタンスを追加する
    3. grdに追加したい行数分、RowDefinitionsにRowDefinitionクラスのインスタンスを追加する
    4. 表示するためのWindowクラスを作成する
    5. 上記で作成したWindowクラスのContentプロパティにgrdを設定
    6. Windowクラスのインスタンスを表示
using System;
using System.Windows;
using System.Windows.Controls;

class Test {
	[STAThread]
	public static void Main(){
		string[,] texts = {
			{"1-1","2-1","3-1"},
			{"1-2","2-2","3-2"},
			{"1-3","2-3","3-3"}
		};

		Grid grdPnl = new Grid();
		
		for (int i = 0 ; i < texts.GetLength(0) ; i++){
			grdPnl.ColumnDefinitions.Add(new ColumnDefinition());
			grdPnl.RowDefinitions.Add(new RowDefinition());
			for (int j = 0 ; j < texts.GetLength(1) ; j++){
				Button btn = new Button();
				btn.Content = texts[i,j];

				btn.Click += (sender, e) => {
					MessageBox.Show((sender as Button).Content.ToString());
				};

				Grid.SetRow(btn,i);
				Grid.SetColumn(btn,j);

				grdPnl.Children.Add(btn);
			}
		}

		Window wnd  = new Window();
		wnd.Content = grdPnl;


		Application app = new Application();
		app.Run(wnd);
	}
}


実行すると3x3のボタンがある画面が表示されます。


ボタンを押すと押したボタンのテキストをメッセージボックスに表示します。


ちなみにここで使用しているMessageBoxクラスは通常のWindowsフォームで使用している名前空間(System.Windows.Forms)ではなく、System.WindowsのMessageBoxクラスです。


次にグリッドの線を変更してみます。
グリッドに線を引くのはGridクラスのShowGridLinesプロパティを使用します。上のソースを少し手直しして再利用します。

using System;
using System.Windows;
using System.Windows.Controls;

class Test {
	[STAThread]
	public static void Main(){
		string[,] texts = {
			{"1-1","2-1","3-1"},
			{"1-2","2-2","3-2"},
			{"1-3","2-3","3-3"}
		};

		Grid grdPnl = new Grid();
		Window wnd  = new Window();
		
		for (int i = 0 ; i < texts.GetLength(0) ; i++){
			grdPnl.ColumnDefinitions.Add(new ColumnDefinition());
			grdPnl.RowDefinitions.Add(new RowDefinition());
			for (int j = 0 ; j < texts.GetLength(1) ; j++){
				Button btn = new Button();
				btn.Content = texts[i,j];

				btn.Click += (sender, e) => {
					grdPnl.ShowGridLines = !grdPnl.ShowGridLines ; 
					wnd.Title = (sender as Button).Content.ToString();
				};

				Grid.SetRow(btn,i);
				Grid.SetColumn(btn,j);

				grdPnl.Children.Add(btn);
			}
		}

		wnd.Content = grdPnl;

		Application app = new Application();
		app.Run(wnd);
	}
}


これを実行してみます。
初期表示される画面は修正前のソースで作ったプログラムと一緒です。



で、何かボタンを押してみるとボタンとボタンの間に点線が表示されます。


さらに今回はMessageBoxではなく、WindowのTitleを利用してどのボタンが押されたのかをわかるようにしてみました。


グリッドのまとめとして複数グリッドにまたがったボタンを作って最後にします。

using System;
using System.Windows;
using System.Windows.Controls;

class Test {
	[STAThread]
	public static void Main(){
		Window wnd  = new Window();
		Grid grdPnl = new Grid();
		grdPnl.ShowGridLines = true;
		
		for (int i = 0 ; i < 3 ; i++){
			grdPnl.ColumnDefinitions.Add(new ColumnDefinition());
			grdPnl.RowDefinitions.Add(new RowDefinition());
		}

		Button btn1 = new Button();

		btn1.Content = "1-1...3-1";
		Grid.SetRow(btn1,0);
		Grid.SetColumn(btn1,0);
		Grid.SetColumnSpan(btn1,3);

		btn1.Click += (sender, e) => {
			wnd.Title = (sender as Button).Content.ToString();
			grdPnl.ShowGridLines = !grdPnl.ShowGridLines ; 
		};

		Button btn2 = new Button();

		btn2.Content = "2-2...2-3";
		Grid.SetRow(btn2,1);
		Grid.SetColumn(btn2,1);
		Grid.SetRowSpan(btn2,2);

		btn2.Click += (sender, e) => {
			wnd.Title = (sender as Button).Content.ToString();
			grdPnl.ShowGridLines = !grdPnl.ShowGridLines ; 
		};

		grdPnl.Children.Add(btn1);
		grdPnl.Children.Add(btn2);

		wnd.Content = grdPnl;


		Application app = new Application();
		app.Run(wnd);
	}
}


実行するとたしかに複数グリッドにまたがったボタンのある画面が表示されます。


「2-1...2-3」ボタンを押すとグリッドが消えてタイトルが変わります。


今度は「1-1...3-1」ボタンを押すとグリッドがまた出てきてタイトルが変わります。


複数のグリッドを超えてボタンが設置出来るのは非常に面白いと感じます。



以上で3章はおしまい。
実際に作ってみてやっとWindowsフォームとの違いが何となく感じられるようになってきた感じがします。積み木を作っているようなそんな印象を受けました。
テキストと全く同じものを作っても得るものが少ないと思い、イベントハンドラの追加やMessageBoxを使用してみましたが、このあたりの作りやすさはさすがだと思います。特にイベントハンドラに匿名メソッドを割り当てられるのは便利だと感じています。
多用する機会はなさそうですが、あって悪い機能ではないしそのうち好きになりそうです。


次は4章の描画オブジェクトについてまとめます。