Windows Formではコントロール自身が保有している子コントロールのコレクションをプロパティとして持っている。これを利用して、多数コントロールに対する処理をループや再帰処理で済ませることが出来る。
例えば、
・画面状態A : 画面内の全てのTextBoxが入力不能
・画面状態B : 画面内の全てのTextBoxが入力可能
という状態遷移を実現するのに、画面上の全TextBoxのreadonlyプロパティをひとつひとつ手動で変更するのは骨が折れる。大体、こういう単純作業をしないためのPGではなかったか。要するに面倒くさいのは嫌なので、子要素のコレクションを利用して適当にフィルタリングしつつループを回してプロパティを変更…ということをしたりする(個人的には「全コントロールを舐める」処理とか呼んでる)。まぁ、可読性やパフォーマンスの面を考えると賛否両論あるかもしれないが。
さて、WPFではどうだろう。ざっと調べた感じ汎用の子コントロールコレクションのようなプロパティは見当たらない(機能上、特定の子要素を持つようなコントロールは除く)。しかし、一度でも書いたことがある人はわかるだろうが、XAMLはどう見ても親-子の依存関係が明確なツリー構造をしており、何らかのアクセス方法があってもおかしくない。
んで、いくつか記事やフォーラムを覗いてみるとLogicalTreeHelperとVisualTreeHelperというヘルパークラスが用意されており、それがまさしくそうらしいことがわかった。なお、一番参考になったのは、かずきのBlogさんの記事。VisualTreeとLogicalTreeの違いはその記事を見ていただければわかると思うが、「全コントロールを舐める」にはLogicalTreeのほうが向いていると思う。勿論VisualTreeにアクセスする必要のある処理もあるだろうけど。以下、パクって参考にしてテストコードを書いてみた。
public class WpfUtil { /// <summary> /// targetの論理ツリー上の子要素全てに対してactionを実行します。 /// actionはtarget自身にも作用する。再帰処理なのでスタックフレームに注意。 /// </summary> /// <param name="target">ルートとするオブジェクト</param> /// <param name="action">実行するメソッドのデリゲート</param> public static void OperateLogicalChildren(DependencyObject target, Action<DependencyObject> action) { action(target); foreach(var child in LogicalTreeHelper.GetChildren(target)) { if (child is DependencyObject) { OperateLogicalChildren((DependencyObject)child, action); } } } /// <summary>targetの論理ツリー内での階層を返す</summary> public static int GetDepthInLogicalTree(DependencyObject target) { DependencyObject parent = LogicalTreeHelper.GetParent(target); int depth = 0; while (parent != null) { depth++; parent = LogicalTreeHelper.GetParent(parent); } return depth; } }
典型的な使い方としてはこんなもんだと思う。共通関数っぽく作ってみたので(悪い癖?)、具体的処理内容はActionデリゲートにしてある。ちなみにあんまり使いやすくない(前処理とか後処理がほしくなったり、そもそもデリゲートに引数追加したくなったり)。まぁこのへんはおいおい改良するとして、ラムダ式の練習がてら適当なサンプルWPFのGridコントロール以下の論理ツリーを出力するプログラムを書いてみた。
<Window x:Class="WpfTreeTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="176" Width="197"> <Grid Name="Container" Margin="10"> <Grid.RowDefinitions> <RowDefinition Height="auto" /> <RowDefinition Height="auto" /> <RowDefinition Height="auto" /> <RowDefinition Height="auto" /> <RowDefinition Height="auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="80" /> <ColumnDefinition Width="100*" /> </Grid.ColumnDefinitions> <Label Grid.Row="0" Grid.Column="0">ラベル1</Label> <Border Grid.Row="0" Grid.Column="1" BorderThickness="3" VerticalAlignment="Top" CornerRadius="5"> <Border.BorderBrush>#F00F</Border.BorderBrush> <TextBlock Name="Title"> <Bold>あいうえお</Bold> </TextBlock> </Border> <Label Grid.Row="1" Grid.Column="0">ラベル2</Label> <Button Grid.Row="1" Grid.Column="1" Name="OutputButton" Click="OutputButton_Click"> <Button.Content>出力</Button.Content> </Button> <Label Grid.Row="2" Grid.Column="0">ラベル3</Label> <ComboBox Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Stretch" Grid.ColumnSpan="2"> <ComboBoxItem>アイテム1</ComboBoxItem> <ComboBoxItem>アイテム2</ComboBoxItem> <ComboBoxItem>アイテム3</ComboBoxItem> </ComboBox> <Label Grid.Row="3" Grid.Column="0">ラベル4</Label> <CheckBox Name="CheckA" Grid.Row="3" Grid.Column="1" VerticalAlignment="Center">チェックA</CheckBox> <Label Grid.Row="4" Grid.Column="0">ラベル5</Label> <CheckBox Name="CheckB" Grid.Row="4" Grid.Column="1" VerticalAlignment="Center">チェックB</CheckBox> </Grid> </Window>
この適当なXAMLコードで記述されたGridに対して
/// <summary> /// target以下の論理ツリー上の子要素をコンソール出力。階層分インデントする。 /// 要素がControlでNameを持っていた場合は併せて出力。 /// </summary> public static void PrintLogicalChildren(DependencyObject target) { WpfUtil.OperateLogicalChildren(target, t => { String contName = ""; Control cont = t as Control; if (cont != null && cont.Name.Length > 0) { contName = " [" + cont.Name + "]"; } int depth = WpfUtil.GetDepthInLogicalTree(t); //デバッグの都合を考えて一つずつ出力する Console.WriteLine(new string(' ', depth*2) + t.GetType().ToString() + contName); } ); }
このメソッドをかますと以下の出力が得られる。
System.Windows.Controls.Grid System.Windows.Controls.ColumnDefinition System.Windows.Controls.ColumnDefinition System.Windows.Controls.RowDefinition System.Windows.Controls.RowDefinition System.Windows.Controls.RowDefinition System.Windows.Controls.RowDefinition System.Windows.Controls.RowDefinition System.Windows.Controls.Label System.Windows.Controls.Border System.Windows.Controls.TextBlock System.Windows.Documents.Bold System.Windows.Documents.Run System.Windows.Controls.Label System.Windows.Controls.Button [OutputButton] System.Windows.Controls.Label System.Windows.Controls.ComboBox System.Windows.Controls.ComboBoxItem System.Windows.Controls.ComboBoxItem System.Windows.Controls.ComboBoxItem System.Windows.Controls.Label System.Windows.Controls.CheckBox [CheckA] System.Windows.Controls.Label System.Windows.Controls.CheckBox [CheckB]
まぁ、こんなもんだろう。コンソール出力じゃなくて各要素に対する処理を書けばいい感じ。
なお、VisualTreeのほうも書いてはみたもののカオスになるので記事にはしないでおく。メソッドの書き方としてはほとんど変わらない(VisualTreeHelper.GetChildrenメソッドが存在しないくらい程度)。