オープンソースかつクロスプラットフォームのGUIライブラリEto.Formsを用いて,GUIプログラムを作成してみよう.この演習をすすめる前には環境構築2を完了させておこう.
Form
クラス
いわゆるウィンドウを表すクラス.
Eto.Formsのテンプレートからデフォルト設定で生成されるコードは,以下のように,
- Formの派生クラスを定義するコードと,
- そのフォームからなるGUIアプリケーションを実行するコード(バックエンド毎.デフォルトだとMac,Wpf,GTKの3つ)
の二つの部分からなっている.
// PROJECTNAME/MainForm.cs
// PROJECTNAMEの部分は dotnet new etoapp を実行したディレクトリ名.
//(-oや-nで陽に指定しなければ)下記のコードが含まれているプロジェクトの名前でもある.
using System;
using Eto.Forms;
using Eto.Drawing;
namespace PROJECTNAME
{
// partialはMainFormの定義を分割可能にする.ここでは不要
// publicは必要.コンパイル単位が異なるため.
public partial class MainForm : Form
{
public MainForm()
{
// 作成される Form にいろいろなGUI部品を追加
}
}
}
// PROJECTNAME.BACKEND/Program.cs
// BACKENDの部分は(dotnet new etoappの際に陽に指定しなければ)MacかWpfかGTK
using System;
using Eto.Forms;
namespace PROJECTNAME.BACKEND
{
class Program
{
[STAThread] // 属性.COMのスレッドモデルをsingle-threaded apartmentに指定する…らしい.
// この public も必須ではない
public static void Main(string[] args)
{
// Macの場合
new Application(Eto.Platforms.Mac64).Run(new MainForm());
// Wpfの場合
new Application(Eto.Platforms.Wpf).Run(new MainForm());
// GTKの場合
new Application(Eto.Platforms.Gtk).Run(new MainForm());
}
}
}
本演習では後者はの部分は変更せずに,前者のMainForm.cs
を拡張することで進めていく.すなわち,作成するForm
に様々なGUI部品(コントロールと呼ばれる)を追加することになる.
その際は以下のプロパティを適切に設定することが有用であろう.
public string Title |
ウィンドウのタイトルを表すプロパティ |
public Control Content |
ウィンドウの中身となるGUI部品 |
public Size ClientSize |
中身のサイズ(ボーダーやタイトルバーを除いたサイズ) |
またテンプレート生成されたコードでは,以下のプロパティが用いられている.
public MenuBar Menu |
メニューバーを表す(Macだとウィンドウの中じゃなくて上の部分) |
public ToolBar ToolBar |
ツールバーを表す |
では,まずはTitle
,Content
,ClientSize
のみを使うように,MainForm.cs
で定義されているMainForm
クラスのコンストラクタを以下のように変更してみよう.
public MainForm()
{
= "Hello World";
Title = new Size(200, 200);
ClientSize
// オブジェクト初期化子を利用
= new Label() { Text = "Hello, World!" };
Content }
以下のようなウィンドウが表示されたことだろう(以下はMacにおける例)
ところで,Macにおいては左上の「閉じる」ボタンを押してもアプリケーションが終了しないことに気付いたかもしれない.これはいささか不便である.幸いなことに空のMenuBarをMenuに代入すると Mac 環境ではいろいろなアイテムを Eto.Forms が追加してくれる.
public MainForm()
{
= "Hello World";
Title = new Size(200, 200);
ClientSize
= new Label() { Text = "Hello, World!" };
Content
// 「空」のメニューバーを追加する.
// すると,Mac環境においてはMenuプロパティのsetterがいろいろなアイテムを追加してくれる.
= new MenuBar();
Menu }
これで,Command + Q等の標準的な操作でアプリケーションを終了することができるようになる(WpfやGtkだと「閉じる」ボタンで,終了する).
オブジェクト初期化子があれば,引数なしのコンストラクタの括弧は省略できる.たとえば,上のContent =
の行は
= new Label { Text = "Hello, World!" }; Content
とも書ける.
コントロール(GUI部品)
さて,上記にあった Label
は単に与えられたテキストを表示するというコントロール(GUI部品)である.たとえば,よく使うコントロールは Label
を含め以下のようなものであろう.
コントロール(を表すクラス) | 説明 |
---|---|
Label |
テキストを画面に表示する |
Button |
その名の通りボタン |
TextBox |
一行のテキストを入力できる欄 |
TextArea |
複数行のテキストを入力できる欄.スクロールバー付き |
Eto.Formsでは他にも様々なコントロールが提供されている.たとえば以下など.
コントロール(を表すクラス) | 説明 |
---|---|
CheckBox |
ラベル付きのチェックボックス |
RadioButton |
ラジオボタン(一個 on にすると他が off になるボタン) |
DropDown |
ドロップダウンリスト |
ImageView |
画像の表示 |
Slider |
「つまみ」を縦か横に動かして値を決める部品 |
コンテナ(コントロールを保持するもの)
コントロールを複数個(1個のみのときもある)保持するコントロールはコンテナと呼ばれる.
スクロールを可能にするScrollable
,タブでコントロールを切り替えられるTabControl
,二つのコントロールを区切り線で分けるSplitter
や次項で説明するレイアウトがある.また,Form
もコントロールを一個保持するようなコンテナの一つである.
レイアウト
レイアウトはコンテナの一種であり,コントロールを適切な位置に並べてくれる. Eto.Formsの提供する代表的なレイアウトに以下のようなものがある.
StackLayout |
横または縦の一方向にコントロールを並べるレイアウト |
TableLayout |
表のセルのようにコントロールを配置するレイアウト |
DynamicLayout |
縦と横を切り替えつつコントロールを並べるレイアウト |
ここでは,StackLayout
とTableLayout
について紹介する.
StackLayout
の例
public MainForm()
{
= "StackLayout Example";
Title = new Size(200, 200);
ClientSize = new MenuBar();
Menu
= new StackLayout();
StackLayout stackLayout
// stackLayout自体と「中身」との間のスペース
.Padding = 10;
stackLayout
// stackLayoutの各コンポーネント間のスペース
.Spacing = 5;
stackLayout
// コンポーネントの配置の方向を指定(Orientation.Horizontalなら横).
// デフォルトが縦なので,この文は実は不要
.Orientation = Orientation.Vertical;
stackLayout
// 横方向の配置位置.HorizontalAlignment.Stretchは左も右もいっぱいまで伸ばす.
.HorizontalContentAlignment = HorizontalAlignment.Stretch;
stackLayout
.Items.Add(new Label { Text = "Label.日本語も書けるよ!" });
stackLayout.Items.Add(new Button { Text = "Button" });
stackLayout// 縦に伸べるTextArea.伸びないようにするには二つ目の引数をfalseにする.
.Items.Add(new StackLayoutItem(new TextArea { Text = "1\n22\n333" }, true));
stackLayout.Items.Add(new TextBox { PlaceholderText = "なにか入力" });
stackLayout
= stackLayout;
Content }
上記の実行例(Macの場合)
上のプログラムでは下記のプロパティを使った.
プロパティ名 | 大雑把な説明 |
---|---|
Padding |
StackLayout自体とその中身の間のスペース |
Spacing |
StackLayoutに含まれる各コントロール間のスペース(int 型) |
Orientation |
StackLayoutの方向.Orientation.Vertical かOrientation.Horizontal か. |
|
|
|
上の縦版. |
Items |
保持するコントロールのコレクション.読み出し専用(代入できないという意味) |
正確にはItems
はStackLayoutItem
を保持するコレクションである.C#では暗黙の型変換をユーザが定義することができ,各コントロールの基底クラスであるControl
からStackLayoutItem
への型変換が定義されているため,コントロールを直接Items
へAdd
メソッドを使って追加することができる.また,StackLayoutItem
のコンストラクタStackLayoutItem(Control, bool)
を使うことにより,そのコントロールがOrientation
に沿って伸長するかどうかを指定できる.
Note
上記はオブジェクト初期化子とコレクション初期化子(new List<int> {1,2,3,4}
の{1,2,3,4}
の部分)を使うことで,以下のようにより簡潔に記述できる.
public MainForm()
{
= "StackLayout Example";
Title = new Size(200, 200);
ClientSize = new MenuBar();
Menu
// オブジェクト初期化子を使った方法
= new StackLayout
Content {
= 10,
Padding = 5,
Spacing = Orientation.Vertical,
Orientation = HorizontalAlignment.Stretch,
HorizontalContentAlignment // Items は read-only プロパティだが,オブジェクト初期化子中に
// Items = {a, b, c, d} と書くことで Items.Add(a), Items.Add(b), Items.Add(c), Items.Add(d)
// をオブジェクト生成後に呼ぶことができる
= {
Items new Label { Text = "Label. 日本語も書けるよ!" },
new Button { Text = "Button" },
new StackLayoutItem ( new TextArea { Text = "1\n22\n333" }, true ),
new TextBox { PlaceholderText = "なにか入力" }
}
};
}
練習問題
上記のプログラムを少し改変してみて挙動の変化を確認してみよう.たとえば,以下等をしてみるとよい.
Orientation
を変えてみる- 紹介されたが上の例では使われなかったプロパティを使ってみる
Padding
やSpacing
を変えてみる- 同じコントロールを複数追加してみる
StackLayoutItem
コンストラクタの第二引数にfalseを渡してみる- あるいは,コントロール
c
を追加する際にAdd(c)
をAdd(new StackLayoutItem (c, true)
としてみる
TableLayout
の例
TableLayout
は Eto.Forms の提供するレイアウトのうち基本となるものであり(StackLayout
やDynamicLayout
も内部でTableLayoutを使用している),表組みのようなレイアウトを実現する.
public MainForm()
{
= "TableLayout Example";
Title = new Size(400, 400);
ClientSize = new MenuBar();
Menu
= new TableLayout();
TableLayout tableLayout // 横と縦のパディングを指定(4引数バージョンもある)
.Padding = new Padding(10, 5);
tableLayout// tableLayoutのSpacingは横と縦の両方を指定
.Spacing = new Size(10, 5);
tableLayout.Rows.Add(
tableLayoutnew TableRow(new Label { Text = "Apple" },
// 横方向に伸長するセル
new TableCell(new TextBox { }, true),
new Label { Text = "Banana" }));
.Rows.Add(
tableLayoutnew TableRow(new Label { Text = "Cabbage" },
new TextArea { },
// TableLayout.AutoSized は中の要素が伸長しないセルを作る(このセルは伸長する行に置かれている)
.AutoSized(new DropDown { Items = { "Item 1", "Item 2", "Item 3" } }))
TableLayout// この行は伸長する
{ ScaleHeight = true });
.Rows.Add(
tableLayoutnew TableRow(new Label { Text = "Tuna" },
// null は伸長する何もないセル
null,
new Label { Text = "Bonito" }));
// 以下をアンコメントしたらどうなる?
// tableLayout.Rows.Add(null); // 伸長する何もない行の追加
= tableLayout;
Content }
上記の実行例(Macの場合)
上のプログラムでは下記のTableLayout
のプロパティを使った.
プロパティ名 | 大雑把な説明 |
---|---|
Padding |
TableLayout自体とその中身の間のスペース |
Spacing |
TableLayoutに含まれる各コントロール間のスペース(Size 型) |
Rows |
各要素がTableRowであるコレクション(読み出し専用) |
また,TableRowの以下のコンストラクタおよびプロパティを使用した.
メンバ名 | 大雑把な説明 |
---|---|
TableRow(TableCell c1, ..., TableCell cn) |
セル c1 , ... , cn を含んだ行を作るコンストラクタ |
ScaleHeight |
行が伸長可能かどうか |
StackLayoutItem
のときと同様にControl
からTableCell
への暗黙の型変換が定義されている.そのため,それを利用することで,Control
型のnew Label { Text = "Apple" }
等をそのままTableRow
コンストラクタの引数に渡すことができる.セルの横幅を伸長可能にしたい場合は上記のようにコンストラクタTableCell(Control, bool)
を使用する.
TableLayout.AutoSized
というstatic メソッドは伸長するセルの中に伸長したくないコントロールを配置するのに使う.
練習問題
上記のプログラムを少し改変してみて挙動の変化を確認してみよう.たとえば,以下等をしてみるとよい.
- 「以下をアンコメントしたらどうなる?」に従ってみる
TableLayout.AutoSized
を使わないでみる.あるいはTextArea
を追加する際にAutoSized
を使ってみるPadding
やSpacing
を変えてみる- 同じコントロールを複数追加してみる
StackLayout
のときのように,オブジェクト初期化子をできるだけ活用する形にプログラムを変更してみる
レイアウトを組み合せる
レイアウトを組み合わせることで,複雑なレイアウトを実現することができる.
たとえば
(Button1) (Button2) (Button3) [TextBox ]
+----------------------------------------------------------+
| TextArea |
| |
| |
| |
| |
+----------------------------------------------------------+
Label
という配置は,StackLayoutを組み合わせることにより実現可能である.
public MainForm()
{
= "Combining Layout Example";
Title = new Size(600, 400);
ClientSize = new MenuBar();
Menu
= new StackLayout();
StackLayout headerPart .Orientation = Orientation.Horizontal;
headerPart.Spacing = 5;
headerPart.Items.Add(new Button { Text = "戻る" });
headerPart.Items.Add(new Button { Text = "進む" });
headerPart.Items.Add(new Button { Text = "再読込" });
headerPart.Items.Add(new StackLayoutItem(new TextBox { PlaceholderText = "アドレス" }, true));
headerPart
= new StackLayout();
StackLayout mainPart .Spacing = 5;
mainPart.Padding = 5;
mainPart.HorizontalContentAlignment = HorizontalAlignment.Stretch;
mainPart.Items.Add(headerPart);
mainPart.Items.Add(new StackLayoutItem(new TextArea { Text = "本文用テキストエリア" }, true));
mainPart.Items.Add(new Label { Text = "ステータスバー" });
mainPart
= mainPart;
Content }
上記の実行例(Macの場合)
ボタンを押したときの動作
さて,これまではGUI部品を配置するだけで,GUI部品がなにか面白いことをすることはなかった.ここではボタンを押したときの動作を実装してみよう.
まずは非常に単純なアプリケーションを作ってみる.このアプリケーションの画面には一つのボタンと一つのラベルがあるのみである.ボタンを押すと,ラベルにボタンを押した回数が表示されるというものである.各コントロールの配置は適当でよい.
コントロールの配置だけを考えれば以下のようなプログラム(MainForm
コンストラクタのみ抜粋)が書けるだろう.
public MainForm()
{
= "Button Example";
Title = new Size(200, 100);
ClientSize = new MenuBar();
Menu
= new Button { Text = "Count" };
Button countButton = new Label { Text = "0" };
Label countLabel
= new StackLayout();
StackLayout stackLayout .Padding = 5;
stackLayout.HorizontalContentAlignment = HorizontalAlignment.Stretch;
stackLayout.Items.Add(new StackLayoutItem(countLabel, true));
stackLayout.Items.Add(countButton);
stackLayout
= stackLayout;
Content }
上記の実行例(Macの場合)
イベントハンドラの登録
ボタンbutton
を押したときに,なにかをさせたい場合は以下のようにbutton.Click
イベントにハンドラを登録すればよい.
// クリックされたときに YOUR_METHOD(イベント発生させたオブジェクト,EventArgs型のオブジェクト) が呼び出される
.Click += YOUR_METHOD; button
C#ではクラスやオブジェクトはイベント(のハンドラを格納するもの)を持つことができる.実際にClick
は
// EventHandlerについては https://docs.microsoft.com/dotnet/api/system.eventhandler-1 参照
public event EventHandler<EventArgs> Click;
というButton
の(インスタンス)メンバとして宣言されている.
イベントはフィールドやプロパティとは異なり,その当該のクラスからしか発生させる(イベントハンドラを呼び出す)ことができない(なので,派生クラスからイベントを発生させたい場合は,基底クラスのほうでイベント発生をラップする protected ないし public なメソッドを用意する必要がある).ただし,イベントはそのクラスの外でも購読(イベントハンドラを登録する)ことができる.上記の+=
はbutton.Click
というイベントを購読する(あるいは"button.Click"というイベントに対するイベントハンドラを登録する)ための構文である.また,イベントの購読解除も行うことができ,対応する構文は-=
である.
さて,上記のYOUR_METHOD
の部分はstatic メソッドでもそうでない通常のメソッド(インスタンスメソッド)でもよいが,今回の目的でもっとも簡潔なのは以下に紹介するラムダ式を使う方法である.
匿名関数(ラムダ式)
匿名関数を作成するため式(ラムダ式)の基本的な構文は以下である.
() => { ... }
たとえば,上記のボタンの例だと
int cnt = 0;
// sとeは無名関数のパラメータ.型はオプショナル
.Click += (s, e) =>
countButton{
// あたり前に感じるかもしれないが,無名関数の中身の部分では,
// その外側で定義された変数にもアクセスできる.
++;
cnt.Text = cnt.ToString();
countLabel};
のように書く.MainForm
コンストラクタ全体は以下のようになる.
public MainForm()
{
= "Button Example";
Title = new Size(200, 100);
ClientSize = new MenuBar();
Menu
= new Button { Text = "Count" };
Button countButton = new Label { Text = "0" };
Label countLabel
int cnt = 0;
.Click += (s, e) =>
countButton{
++;
cnt.Text = cnt.ToString();
countLabel};
= new StackLayout();
StackLayout stackLayout .Padding = 5;
stackLayout.HorizontalContentAlignment = HorizontalAlignment.Stretch;
stackLayout.Items.Add(new StackLayoutItem(countLabel, true));
stackLayout.Items.Add(countButton);
stackLayout
= stackLayout;
Content }
Note
インスタンスメソッドを使った場合は,たとえば,MainForm
のインスタンスを用いる場合は
public class MainForm : Form
{
private int cnt = 0;
private Label countLabel;
private void countUp(object sender, EventArgs e)
{
++;
cnt.Text = cnt.ToString();
countLabel}
public MainForm()
{
= "Button Example";
Title = new Size(200, 100);
ClientSize = new MenuBar();
Menu
= new Button { Text = "Count" };
Button countButton = new Label { Text = "0" };
countLabel
.Click += this.countUp;
countButton
= new StackLayout();
StackLayout stackLayout .Padding = 5;
stackLayout.HorizontalContentAlignment = HorizontalAlignment.Stretch;
stackLayout.Items.Add(new StackLayoutItem(countLabel, true));
stackLayout.Items.Add(countButton);
stackLayout
= stackLayout;
Content }
}
となり,あるいはカウントやラベルの情報を格納するためだけのクラスを作る場合は
public class MainForm : Form
{
private class countUpClass
{
private int count;
private Label label;
public countUpClass(int c, Label l)
{
= c; label = l;
count }
public void countUp(object sender, EventArgs e)
{
++;
count.Text = count.ToString();
label}
}
public MainForm()
{
= "Button Example";
Title = new Size(200, 100);
ClientSize = new MenuBar();
Menu
= new Button { Text = "Count" };
Button countButton = new Label { Text = "0" };
Label countLabel
.Click += new countUpClass(0, countLabel).countUp;
countButton
= new StackLayout();
StackLayout stackLayout .Padding = 5;
stackLayout.HorizontalContentAlignment = HorizontalAlignment.Stretch;
stackLayout.Items.Add(new StackLayoutItem(countLabel, true));
stackLayout.Items.Add(countButton);
stackLayout
= stackLayout;
Content }
}
のようになる.