レイアウト

このトピックでは、Windows Presentation Foundation (WPF) レイアウト システムについて説明します。 WPF でユーザー インターフェイスを作成するには、レイアウトの計算が発生するタイミングと方法を理解することが非常に重要です。

このトピックは、次のセクションで構成されています。

要素の境界ボックス

WPF でレイアウトについて考えるときは、すべての要素を囲む境界ボックスを理解することが重要です。 レイアウト システムによって使用される各 FrameworkElement は、レイアウトに挿入される四角形と考えることができます。 LayoutInformation クラスは、要素のレイアウト割り当て (スロット) の境界を返します。 四角形のサイズは、使用可能な画面スペース、サイズの制約、レイアウトに固有のプロパティ (余白やパディングなど)、親 Panel 要素の個々の動作を計算することによって決まります。 このデータを処理することで、レイアウト システムは特定の Panel のすべての子の位置を計算できるようになります。 Border などの親要素で定義されているサイジング特性が、その子に影響することを覚えておくことが重要です。

次の図は、シンプルなレイアウトを示しています。

Screenshot that shows a typical grid, no bounding box superimposed.

このレイアウトは、次の XAML を使用して実現できます。

<Grid Name="myGrid" Background="LightSteelBlue" Height="150">
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="250"/>
  </Grid.ColumnDefinitions>
  <Grid.RowDefinitions>
    <RowDefinition />
    <RowDefinition />
    <RowDefinition />
  </Grid.RowDefinitions>
  <TextBlock Name="txt1" Margin="5" FontSize="16" FontFamily="Verdana" Grid.Column="0" Grid.Row="0">Hello World!</TextBlock>
  <Button Click="getLayoutSlot1" Width="125" Height="25" Grid.Column="0" Grid.Row="1">Show Bounding Box</Button>
  <TextBlock Name="txt2" Grid.Column="1" Grid.Row="2"/>
</Grid>

1 つの TextBlock 要素が Grid 内でホストされています。 最初の列の左上隅だけにテキストが入力されているのに対し、TextBlock に割り当てられた領域は実際にはそれよりずっと大きくなっています。 GetLayoutSlot メソッドを使用すると、任意の FrameworkElement の境界ボックスを取得できます。 次の図は、TextBlock 要素の境界ボックスを示しています。

Screenshot that shows that the TextBlock bounding box is now visible.

黄色の四角形で示されているように、TextBlock 要素には、その外観よりもはるかに大きい領域が割り当てられています。 Grid にさらに要素を追加すると、追加される要素の種類とサイズによって、この割り当てが縮小または拡大される場合があります。

TextBlock のレイアウト スロットは、GetLayoutSlot メソッドを使用して Path に変換されます。 この方法は、要素の境界ボックスの表示に役立ちます。

private void getLayoutSlot1(object sender, System.Windows.RoutedEventArgs e)
{
    RectangleGeometry myRectangleGeometry = new RectangleGeometry();
    myRectangleGeometry.Rect = LayoutInformation.GetLayoutSlot(txt1);
    Path myPath = new Path();
    myPath.Data = myRectangleGeometry;
    myPath.Stroke = Brushes.LightGoldenrodYellow;
    myPath.StrokeThickness = 5;
    Grid.SetColumn(myPath, 0);
    Grid.SetRow(myPath, 0);
    myGrid.Children.Add(myPath);
    txt2.Text = "LayoutSlot is equal to " + LayoutInformation.GetLayoutSlot(txt1).ToString();
}
Private Sub getLayoutSlot1(ByVal sender As Object, ByVal e As RoutedEventArgs)
    Dim myRectangleGeometry As New RectangleGeometry
    myRectangleGeometry.Rect = LayoutInformation.GetLayoutSlot(txt1)
    Dim myPath As New Path
    myPath.Data = myRectangleGeometry
    myPath.Stroke = Brushes.LightGoldenrodYellow
    myPath.StrokeThickness = 5
    Grid.SetColumn(myPath, 0)
    Grid.SetRow(myPath, 0)
    myGrid.Children.Add(myPath)
    txt2.Text = "LayoutSlot is equal to " + LayoutInformation.GetLayoutSlot(txt1).ToString()
End Sub

レイアウト システム

簡単に言うと、レイアウトは、要素のサイズ測定、配置、および描画を繰り返す再帰的なシステムです。 より具体的には、レイアウトは Panel 要素の Children コレクションのメンバーを測定および配置するプロセスを記述します。 レイアウトは集中的なプロセスです。 Children コレクションが大きいほど、より多くの計算を行う必要があります。 コレクションを所有する Panel 要素で定義されたレイアウト動作に基づく複雑さが加わる場合もあります。 Canvas などの比較的単純な Panel は、Grid などのより複雑な Panel よりも大幅にパフォーマンスが向上する可能性があります。

UIElement がその位置を変更するたびに、レイアウト システムによる新しいパスがトリガーされる可能性があります。 したがって、不要な呼び出しはアプリケーション パフォーマンスの低下につながる可能性があるため、レイアウト システムを呼び出す可能性があるイベントを理解することが重要です。 以下に、レイアウト システムが呼び出されたときに発生するプロセスについて説明します。

  1. UIElement は、最初にそのコア プロパティを測定して、レイアウト プロセスを開始します。

  2. FrameworkElement で定義されているサイズ設定プロパティ (WidthHeightMargin など) が評価されます。

  3. Dock の方向や積み重ねの Orientation などの Panel 固有のロジックが適用されます。

  4. すべての子が測定された後にコンテンツが配置されます。

  5. Children コレクションが画面に描画されます。

  6. コレクションにさらに Children が追加された場合、LayoutTransform が適用された場合、または UpdateLayout メソッドが呼び出された場合には、プロセスがもう一度呼び出されます。

このプロセスとその呼び出し方法は、次のセクションで詳細に定義します。

子の測定と配置

レイアウト システムは、Children コレクションのメンバーごとに、測定パスと配置パスの 2 つのパスを実行します。 それぞれの子 Panel は、独自の MeasureOverride メソッドと ArrangeOverride メソッドを提供して、独自の特定のレイアウト動作を実行します。

測定パスの実行中に、Children コレクションの各メンバーが評価されます。 プロセスは Measure メソッドの呼び出しで開始します。 このメソッドは、親 Panel 要素の実装内で呼び出されます。レイアウトで発生させるために明示的に呼び出す必要はありません。

最初に、ClipVisibility などの UIElement のネイティブ サイズのプロパティが評価されます。 これにより、MeasureCore に渡される constraintSize という名前の値が生成されます。

次に、FrameworkElement で定義されているフレームワーク プロパティが処理されます。これは constraintSize の値に影響します。 一般に、これらのプロパティは、基になる UIElement のサイズ変更特性 (HeightWidthMarginStyle など) を記述します。 これらの各プロパティは、要素を表示するために必要な領域を変更できます。 MeasureOverride は、constraintSize でパラメーターとして呼び出されます。

注意

HeightWidth および ActualHeightActualWidth のプロパティには違いがあります。 たとえば、ActualHeight プロパティは、他の高さ入力とレイアウト システムに基づいて計算された値です。 この値は、実際のレンダリング パスに基づいて、レイアウト システム自体で設定されるため、入力の変更の基準である Height などのプロパティの設定値よりもわずかに遅れる場合があります。

ActualHeight は計算された値であるため、レイアウト システムによるさまざまな操作の結果として、その値に対して複数の変更や増分変更が報告される可能性があることにご注意ください。 レイアウト システムが、子要素に必要な測定スペース、親要素による制約などを計算している場合があります。

測定パスの最終的な目標は、子が MeasureCore の呼び出し中に発生する、その DesiredSize を決定することです。 DesiredSize 値は、コンテンツの配置パスの実行中に使用するため、Measure によって格納されます。

配置パスは Arrange メソッドの呼び出しで開始します。 配置パスの実行中に、親 Panel 要素が子のバインドを表す四角形を生成します。 この値は処理のために ArrangeCore メソッドに渡されます。

ArrangeCore メソッドは、子の DesiredSize を評価し、要素のレンダリングされるサイズに影響する可能性のある追加の余白をすべて評価します。 ArrangeCorearrangeSize を生成します。これはPanelArrangeOverride メソッドにパラメーターとして渡されます。 ArrangeOverride は子の finalSize を生成します。 最後に、ArrangeCore メソッドは、余白や配置などのオフセット プロパティの最終評価を行い、レイアウト スロット内に子を配置します。 子は割り当てられた領域全体を埋める必要はありません (そうしない場合もよくあります)。 その後、コントロールが親 Panel に返され、レイアウト プロセスが完了します。

パネル要素とカスタム レイアウトの動作

WPF には、Panel から派生した要素のグループが含まれています。 これらの Panel 要素により、多数の複雑なレイアウトが可能になります。 たとえば、スタッキング要素は、StackPanel 要素を使用して簡単に実現できますが、より複雑で流動性の高いレイアウトは、Canvas を使用することで可能になります。

次の表に、使用可能なレイアウト Panel 要素をまとめています。

パネル名 説明
Canvas Canvas 領域に対する座標により子要素を明示的に配置できる領域を定義します。
DockPanel 子要素を互いに水平方向または垂直方向に整列する領域を定義します。
Grid 行と列で構成される柔軟性のあるグリッド領域を定義します。
StackPanel 子要素を水平方向または垂直方向の単一行に整列します。
VirtualizingPanel 子のデータ コレクションを仮想化する Panel 要素のフレームワークを提供します。 これは抽象クラスです。
WrapPanel 左から右へ順に子要素を配置し、ボックスの端で改行してコンテンツを次の行へ送ります。 後続の配置は、Orientation プロパティの値に応じて、上から下または右から左に向かって行われます。

定義済みの Panel 要素を使用してできないレイアウトを必要とするアプリケーションに対しては、Panel から継承し、MeasureOverride メソッドと ArrangeOverride メソッドをオーバーライドすることで、カスタム レイアウト動作を実現できます。

レイアウトのパフォーマンスの考慮事項

レイアウトは再帰的なプロセスです。 Children コレクション内の各子要素は、レイアウト システムの呼び出しごとに処理されます。 そのため、必要ない場合はレイアウト システムをトリガーしないようにしてください。 次の考慮事項は、パフォーマンスを向上させるのに役立ちます。

  • どのプロパティ値の変更がレイアウト システムによる再帰的な更新を強制するかに注意してください。

    レイアウト システムを初期化できる値が設定されている依存関係プロパティは、パブリック フラグでマークされます。 AffectsMeasureAffectsArrange は、どのプロパティ値の変更がレイアウト システムによる再帰的な更新を強制するかに関する役立つヒントを提供します。 一般に、要素の境界ボックスのサイズに影響を与えるプロパティでは、AffectsMeasure フラグを true に設定する必要があります。 依存関係プロパティの詳細については、「依存関係プロパティの概要」を参照してください。

  • 可能であれば、LayoutTransform の代わりに RenderTransform を使用します。

    LayoutTransform はユーザー インターフェイス (UI) のコンテンツに影響を与える、非常に便利な方法となる場合があります。 ただし、変換の効果を他の要素の位置に影響させる必要がない場合は、代わりに RenderTransform を使用することをお勧めします。これは RenderTransform がレイアウト システムを呼び出さないためです。 LayoutTransform はその変換を適用し、影響を受ける要素の新しい位置を考慮するため、再帰的なレイアウトの更新を強制します。

  • UpdateLayout の不要な呼び出しを回避します。

    UpdateLayout メソッドは、再帰的なレイアウトの更新を強制しますが、多くの場合、必要ありません。 完全な更新が必要であると確信できる場合を除き、このメソッドの呼び出しはレイアウト システムに任せます。

  • 大規模な Children コレクションを使用する場合は、通常の StackPanel の代わりに VirtualizingStackPanel を使用することを検討してください。

    子コレクションを仮想化することで、VirtualizingStackPanel は、現在、親のビューポート内にあるメモリ内オブジェクトのみを保持します。 その結果、ほとんどのシナリオでパフォーマンスが大幅に向上します。

サブピクセル レンダリングとレイアウトの丸め処理

WPF グラフィックス システムでは、デバイスに依存しない単位を使用して、解像度とデバイスの独立性を可能にします。 デバイスに依存しない各ピクセルは、システムのドット/インチ (dpi) 設定に応じて自動的にスケーリングされます。 これにより、異なる dpi 設定に対して WPF アプリケーションに適切なスケーリングを提供し、アプリケーションを自動的に dpi 対応にします。

ただし、この dpi 非依存は、アンチエイリアシングのため、不規則なエッジ レンダリングが作成される場合があります。 これらの成果物 (通常はぼやけたエッジや半透明のエッジが見られます) は、エッジの場所がデバイス ピクセルの間ではなく、デバイス ピクセルの中間にある場合に発生します。 レイアウト システムは、レイアウトの丸め処理でこれを調整する方法を提供します。 レイアウトの丸め処理では、レイアウト システムがレイアウト パスの実行中に任意の整数以外のピクセル値を丸めます。

レイアウトの丸め処理は既定で無効になっています。 レイアウトの丸め処理を有効にするには、任意の FrameworkElementUseLayoutRounding プロパティを true に設定します。 これは依存関係プロパティであるため、値はビジュアル ツリー内のすべての子に反映されます。 UI 全体のレイアウトの丸め処理を有効にするには、ルートコンテナーで UseLayoutRoundingtrue に設定します。 例については、「UseLayoutRounding」を参照してください。

次の内容

要素の測定方法と配置方法を理解することが、レイアウトを理解する最初のステップです。 使用可能な Panel 要素の詳細については、「パネルの概要」を参照してください。 レイアウトに影響を与えるさまざまな配置プロパティをより理解するには、「配置、余白、パディングの概要」を参照してください。 軽量のアプリケーションにまとめて配置する準備ができたら、「チュートリアル:初めての WPF デスクトップ アプリケーション」を参照してください。

関連項目