*.NETプログラミング研究 第69号 [#aef76f38]

**.NET Tips [#l3fd7d51]

今回は、.NET Framework 2.0から追加されたWindowsアプリケーションのコントロール、FlowLayoutPanelとTableLayoutPanelコントロールについて説明します。これらのコントロールはコンテナとして働き、複数のコントロールを整列させたい時に使用します。

***FlowLayoutPanelコントロール [#yd1425ee]

FlowLayoutPanelコントロールについて、MSDNでは次のように説明されています。

「FlowLayoutPanel コントロールは、その内容を水平または垂直のフロー方向に整列させます。内容は次の行または次の列に折り返すことができます。また、折り返さずにクリップすることもできます。」

-[[FlowLayoutPanel コントロール (Windows フォーム)>http://msdn2.microsoft.com/ja-jp/library/zah8ywcc.aspx]]

言葉で説明するより、実際に使ってみるのが一番早いというわけで、次のURLにFlowLayoutPanelを使ったサンプル(FlowLayoutPanel1.exe)を置いておきます。(ソースコードも置いておきます。)実行するには、.NET Framework 2.0がインストールされている必要があります。(VB.NETのコードはありません。また、これらのファイルは、1ヶ月ほどで削除します。)

-[[サンプル「FlowLayoutPanel1.exe」>http://dobon.net/vb/dotnet/control/files/FlowLayoutPanel1.exe]]
-[[ソース>http://dobon.net/vb/dotnet/control/files/FlowLayoutPanel1.zip]]

このサンプルは、フォームにFlowLayoutPanelコントロールを配置し、DockプロパティをFillとし、Buttonコントロールを15個配置したものです。フォームの大きさを変更させると、Buttonコントロールが右端で折り返されて整列することが確認できるでしょう。

FlowLayoutPanelコントロールには、重要なプロパティとして、FlowDirectionとWrapContentsがあります。FlowDirectionプロパティは、整列の方向を指定するために使用します。また、WrapContentsプロパティは、FlowLayoutPanelに配置したコントロールを折り返して整列させるかを指定するために使用します。

サンプル「FlowLayoutPanel1.exe」では、FlowDirectionとWrapContentsプロパティを試すため、メニューを使ってFlowLayoutPanelコントロールのFlowDirectionとWrapContentsプロパティの値を簡単に変更できるようになっています。実際にこれらの値を変更させるとどのように変化するのかを試してみてください。

***AnchorとDockプロパティ [#w9057de1]

FlowLayoutPanelコントロールに配置された子コントロールは、AnchorとDockプロパティに、通常とは少し異なる特別な役割が与えられています。これに関しては、MSDNで説明されています。

-[[方法 : FlowLayoutPanel コントロールで子コントロールを固定およびドッキングする>http://msdn2.microsoft.com/ja-jp/library/ms171633.aspx]]

これを実際に確認するためのサンプルが次に紹介する「FlowLayoutPanel2.exe」です。

-[[サンプル「FlowLayoutPanel2.exe」>http://dobon.net/vb/dotnet/control/files/FlowLayoutPanel2.exe]]
-[[ソース>http://dobon.net/vb/dotnet/control/files/FlowLayoutPanel2.zip]]

このサンプルでは、様々な大きさのButtonコントロールがFlowLayoutPanelコントロールに配置されています。さらに、Buttonコントロールを右クリックすることにより、コンテキストメニューが表示され、ButtonのAnchorとDockプロパティを簡単に変更できるようになっています。

例えば、FlowLayoutPanelのFlowDirectionプロパティがLeftToRightの時、ButtonのAnchorをTopにすると、Buttonはその列の一番上に配置されるようになります。AnchorをBottomにすると、逆に一番下に配置されます。TopとBottom両方を指定すると、列の幅いっぱいに広がります。Noneでは中央に配置されます。LeftとRightは、この場合は、関係ありません。

Dockプロパティも同様に、Topで一番上、Bottomで一番下、Fillで幅いっぱいに配置されるようになります。

***FlowBreakプロパティ [#z8ef084e]

さらにこのサンプルでは、FlowLayoutPanelコントロールに配置されたコントロールのFlowBreakプロパティを変更することもできます。FlowBreakプロパティをTrueとすることにより、そのコントロールの次で強制的に折り返して整列されるようになります。

FlowBreakプロパティはVS 2005のデザイナのプロパティウィンドウで変更することができますが、実際のコントロールのクラスにFlowBreakというプロパティが存在するわけではありません。実際にはFlowLayoutPanelコントロールのSetFlowBreakメソッドを使って指定します。例えば、FlowLayoutPanelコントロール「FlowLayoutPanel1」に配置されたButtonコントロール「Button1」のFlowBreakプロパティをTrueにするには、次のようにします。

#code(vbnet){{
FlowLayoutPanel1.SetFlowBreak(Button1, True)
}}

#code(csharp){{
FlowLayoutPanel1.SetFlowBreak(Button1, true);
}}

また、Button1のFlowBreakの状態を調べるには、FlowLayoutPanel.GetFlowBreakメソッドを使います。

***コントロールの順番を変更する [#z34ef817]

FlowLayoutPanelに配置されるコントロールの順番は、FlowLayoutPanel.Controlsコレクションの順番で決まるようです。よって、コントロールの順番を変更するには、変更するコントロール以降のコントロールをすべて削除してから、追加しなおさなければならないようです。

サンプル「FlowLayoutPanel2.exe」で、コントロールを先頭に移動する例を紹介しています。以下にその抜粋を示します。currentButtonというコントロールを移動させています。

#code(vbnet){{
'移動先の位置
Dim newIndex As Integer = 0

Dim conts As New List(Of Control)

FlowLayoutPanel1.SuspendLayout()

'flowLayoutPanel1内のコントロールを記憶&削除
Dim i As Integer
For i = FlowLayoutPanel1.Controls.Count - 1 To newIndex Step -1
    If Not FlowLayoutPanel1.Controls(i).Equals(currentButton) Then
        conts.Insert(0, FlowLayoutPanel1.Controls(i))
    End If
    FlowLayoutPanel1.Controls.RemoveAt(i)
Next i

'移動するコントロールがまだあるときは、削除
If FlowLayoutPanel1.Controls.IndexOf(currentButton) > -1 Then
    FlowLayoutPanel1.Controls.Remove(currentButton)
End If
'移動するコントロールを追加
FlowLayoutPanel1.Controls.Add(currentButton)
'残りのコントロールを追加
FlowLayoutPanel1.Controls.AddRange(conts.ToArray())

FlowLayoutPanel1.ResumeLayout()
}}

#code(csharp){{
//移動先の位置
int newIndex = 0;

List<Control> conts = new List<Control>();

flowLayoutPanel1.SuspendLayout();

//flowLayoutPanel1内のコントロールを記憶&削除
for (int i = flowLayoutPanel1.Controls.Count - 1; i >= newIndex; i--)
{
    if (!flowLayoutPanel1.Controls[i].Equals(currentButton))
        conts.Insert(0, flowLayoutPanel1.Controls[i]);
    flowLayoutPanel1.Controls.RemoveAt(i);
}

//移動するコントロールがまだあるときは、削除
if (flowLayoutPanel1.Controls.IndexOf(currentButton) > -1)
    flowLayoutPanel1.Controls.Remove(currentButton);

//移動するコントロールを追加
flowLayoutPanel1.Controls.Add(currentButton);
//残りのコントロールを追加
flowLayoutPanel1.Controls.AddRange(conts.ToArray());

flowLayoutPanel1.ResumeLayout();
}}

***TableLayoutPanelコントロール [#ca1c4628]

TableLayoutPanelコントロールは、まるでHTMLのTABLEタグのようなコントロールと言えば分かりやすいでしょうか。MSDNでは、次のように説明されています。

TableLayoutPanel コントロールは、実行時に比例的なサイズ変更機能を提供するため、フォームのサイズ変更に合わせてレイアウトを滑らかに変更できます。このため、TableLayoutPanel コントロールは、データ入力フォームやアプリケーションのローカライズなどに適しています。

-[[TableLayoutPanel コントロール (Windows フォーム)>http://msdn2.microsoft.com/ja-jp/library/3a1tbfwd.aspx]]

MSDNの「TableLayoutPanel コントロールの推奨される手順」では、TableLayoutPanelコントロールをどのようなケースで使用し、どのようなケースでは使用すべきではないかが説明されています。

-[[TableLayoutPanel コントロールの推奨される手順>http://msdn2.microsoft.com/ja-jp/library/ms171689.aspx]]

これによると、親フォームのサイズ変更や、ローカリゼーションに伴いコントロールに表示するテキストの長さが変更されるようなケースでTableLayoutPanelコントロールを使うのが有効であるということです。逆にTableLayoutPanelコントロールのDockプロパティをFillにする、TableLayoutPanelに別のTableLayoutPanelを配置して入れ子にする、TableLayoutPanelを配置したフォームを継承したフォームでさらにコントロールを追加する、列がレベルとテキストの2つしかない単純なフォームで使用するなどといったことは推奨されていません。このような推奨事項を守る限り、TableLayoutPanelコントロールの使い道はごく限られそうです。(巷に出回っているTableLayoutPanelコントロールのサンプルは、これらの推奨事項を無視しているものがほとんどですので、注意が必要です。)

TableLayoutPanelコントロールの使い方は、MSDNの「TableLayoutPanel コントロール (Windows フォーム)」などに詳しいです。しかしほとんどがVisual Studioのフォームデザイナを使った場合の説明であり、コードによる説明はほとんどありません。そこでここでは、実行時に行うにはどうするかを中心に説明します。

なお、TableLayoutPanelコントロールの実際の挙動を確かめるためのサンプル(TableLayoutPanel1.exe)も用意しました。

-[[サンプル「TableLayoutPanel1.exe」>http://dobon.net/vb/dotnet/control/files/TableLayoutPanel1.exe]]
-[[ソース>http://dobon.net/vb/dotnet/control/files/TableLayoutPanel1.zip]]

***TableLayoutPanelコントロールにコントロールを追加する [#v02de5be]

MSDNでは、フォームデザイナを使った方法が次のページなどで説明されています。

-[[チュートリアル : TableLayoutPanel を使用した Windows フォーム上のコントロールの配置>http://msdn2.microsoft.com/ja-jp/library/w4yc3e8c.aspx]]
-[[チュートリアル : データ入力用のサイズ変更可能な Windows フォームの作成>http://msdn2.microsoft.com/ja-jp/library/991eahec.aspx]]

TableLayoutPanelコントロール内にコントロールを配置するには、Controls.Addメソッドを使います。このメソッドではコントロールを挿入するセルの位置を指定することができます。

-[[TableLayoutControlCollection.Add メソッド>http://msdn2.microsoft.com/ja-jp/library/system.windows.forms.tablelayoutcontrolcollection.add.aspx]]

TableLayoutPanelコントロールは一つのセルに一つのコントロールしか入れることができません(セルにPanelを配置し、そこに複数のコントロールを配置することはできます)。よって指定した挿入位置にすでにコントロールがあるときは、それ以降のコントロールが押し出されるように次のセルに移動します。

Controls.Addでは挿入位置を指定しなくても大丈夫です。挿入位置を指定しなかった場合は、右上の空いているセルから順番に配置されます。((-1, -1)を指定したことになります。)

すべてのセルがすでに埋まっている時に新しいコントロールを追加した時の動作は、TableLayoutPanel.GrowStyleプロパティにより異なります。GrowStyleプロパティがAddRowsのとき(デフォルト)は行を拡張し、AddColumnsのときは列を拡張します。FixedSizeのときは、例外ArgumentExceptionがスローされます。

次の例では、insertRowとinsertColumnで指定された位置にButtonコントロールを追加しています。挿入位置にコントロールがあるか調べ、無いときのみ挿入しています。

#code(vbnet){{
'挿入位置
Dim insertRow As Integer = 0
Dim insertColumn As Integer = 0

If TableLayoutPanel1.GetControlFromPosition( _
    insertColumn, insertRow) Is Nothing Then
    Dim newButton As New Button()
    newButton.Text = "button"
    TableLayoutPanel1.Controls.Add( _
        newButton, insertColumn, insertRow)
End If
}}

#code(csharp){{
//挿入位置
int insertRow = 0;
int insertColumn = 0;

if (tableLayoutPanel1.GetControlFromPosition(
    insertColumn, insertRow) == null)
{
    Button newButton = new Button();
    newButton.Text = "button";
    tableLayoutPanel1.Controls.Add(newButton, insertColumn, insertRow);
}
}}

サンプル「TableLayoutPanel1.exe」でも、メニューから簡単にButtonコントロールの追加や、TableLayoutPanel.GrowStyleプロパティの変更を行うことができますので、お試しください。

***子コントロールの位置の取得(GetCellPositionとGetPositionFromControlの違い) [#wc56c5ee]

TableLayoutPanelに配置されたコントロールが占有しているセルの位置を取得する方法としてTableLayoutPanelクラスには、GetColumn、GetRow、GetCellPosition、GetPositionFromControlといったメソッドが用意されています。この内、GetColumn、GetRowとGetCellPositionは同じですが、これらとGetPositionFromControlは似ているようで全く異なります。

例えば、列数が十分にあるTableLayoutPanel「tableLayoutPanel1」に次のように2つのボタンを配置したとします。

#code(vbnet){{
tableLayoutPanel1.Controls.Add(button1, 0, 0)
tableLayoutPanel1.Controls.Add(button2, 1, 0)
}}

#code(csharp){{
tableLayoutPanel1.Controls.Add(button1, 0, 0);
tableLayoutPanel1.Controls.Add(button2, 1, 0);
}}

このときは、button1に対してGetCellPositionとGetPositionFromControlが返す値は同じ(0,0)になります。また、button2に対しても両者とも(1,0)となります。

さて、さらにbutton3を次のように追加すると、どうなるでしょうか?

#code(vbnet){{
tableLayoutPanel1.Controls.Add(button3, 0, 0)
}}

#code(csharp){{
tableLayoutPanel1.Controls.Add(button3, 0, 0);
}}

button3が(0,0)に入ることにより、button1は(1,0)に、button2は(2,0)に追い出されます。このとき、button1とbutton2に対してGetPositionFromControlが返す値は(1,0)と(2,0)になりますが、GetCellPositionが返す値は(0,0)と(1,0)のままです。button3に対してGetCellPositionとGetPositionFromControlが返す値は(0,0)ですので、GetCellPositionが(0,0)を返すコントロールが2つ存在することになります。

つまり、GetPositionFromControlは現在実際にコントロールが占有しているセルの位置を返し、GetCellPositionはControls.Add(あるいはTableLayoutPanel.SetCellPositionメソッド)で指定された値を返すということになります。位置を指定しないでControls.Addを呼び出したコントロールに対しては、GetCellPositionは(-1,-1)を返します。

VS2005のフォームデザイナのみでコントロールを配置した場合はGetCellPositionとGetPositionFromControlは一致すると考えてよさそうですが、実行時に配置したコントロールがある場合は、そうならない可能性があることを知っておくべきでしょう。

MSDNには、

「GetPositionFromControl メソッドは、位置が LayoutEngine によって決定される場合でも、control の実際の現在位置を返します。」

とありますが、これはこういう意味だったのです。

-[[TableLayoutPanel.GetPositionFromControl メソッド >http://msdn2.microsoft.com/ja-jp/library/system.windows.forms.tablelayoutpanel.getpositionfromcontrol.aspx]]

***Controls.Addで指定した位置に配置できない?! [#afddf87b]

上で紹介した例に、次のようにしてもう一つbuttonを追加してみましょう。一体どこに追加されるでしょうか?

#code(vbnet){{
tableLayoutPanel1.Controls.Add(button4, 1, 0)
}}

#code(csharp){{
tableLayoutPanel1.Controls.Add(button4, 1, 0);
}}

ちょっと意外ですが、button4は(2,0)の位置、つまりbutton1とbutton2の間に挿入されます。つまり、GetCellPositionが(0,0)であるbutton1が優先されるようなのです。

それでは、間違いなく指定した位置のセルにコントロールを配置するにはどうすればいいのかということになると、あまりいい方法が思いつきません。一つの方法として、次のように実際に配置されている位置をGetCellPositionが返す位置に強制的に設定してしまってから、コントロールを追加するやり方が考えられます。

#code(vbnet){{
Dim c As Control
For Each c In TableLayoutPanel1.Controls
    TableLayoutPanel1.SetCellPosition( _
        c, TableLayoutPanel1.GetPositionFromControl(c))
Next c
}}

#code(csharp){{
foreach (Control c in tableLayoutPanel1.Controls)
{
    tableLayoutPanel1.SetCellPosition(
        c,
        tableLayoutPanel1.GetPositionFromControl(c));
}
}}

***ColumnCountとRowCountは実際のTableLayoutPanelの列数と行数を表すものではない [#s76fdc66]

これもまた分かりにくい話ですが、TableLayoutPanelのColumnCountやRowCountプロパティは、実際の列数や行数と一致するとは限りません。実行時にコントロールをTableLayoutPanelに追加して実際の列数や行数が拡張された場合でも、ColumnCountやRowCountの値は変更されませんので、実際の列数、行数と違いが出てしまいます。つまり、これらのプロパティはTableLayoutPanelのGrowStyleプロパティがFixedSizeの場合のみ実際の列数や行数と必ず一致し、GrowStyleがAddColumnsまたはAddRowsの場合は、RowCountまたはColumnCountだけが実際の列数や行数と必ず一致するということになります。

-[[TableLayoutPanel.ColumnCount プロパティ>http://msdn2.microsoft.com/ja-jp/library/system.windows.forms.tablelayoutpanel.columncount.aspx]]

それでは実際の列数と行数を取得するにはどのようにすればよいかというと、...これがよく分かりません。TableLayoutPanelに配置されているすべてのコントロールの実際の位置をGetPositionFromControlで取得して、その最大値とColumnCountまたはRowCountの大きい方が実際の列数または行数であるとする以外の良い方法が今のところ見つかりません。

今回、予想外に記事が長くなってしまいましたので、きりが良くないのですが、続きは次回とさせていただきます。

//これより下は編集しないでください
#pageinfo(,2010-03-19 (金) 02:14:13,DOBON!,2010-03-19 (金) 02:19:27,DOBON!)
[ トップ ]   [ 新規 | 子ページ作成 | 一覧 | 単語検索 | 最終更新 | ヘルプ ]