次の方法で共有


"チュートリアル: カスタム パネルを作成する

カスタム Panel クラスのコードを記述し、ArrangeOverride メソッドと MeasureOverride メソッドを実装し、Children プロパティを使用する方法について説明します。

重要な API: PanelArrangeOverrideMeasureOverride

このコード例はカスタム パネルの実装を示していますが、さまざまなレイアウト シナリオに合わせてパネルをカスタマイズする方法に影響するレイアウトの概念について説明することに多くの時間を費やしていません。 これらのレイアウトの概念と、それらが特定のレイアウト シナリオにどのように適用されるかについて詳しくは、「 XAML カスタム パネルの概要」をご覧ください。

パネルは、XAML レイアウト システムが実行され、アプリ UI がレンダリングされるときに、そのパネルに含まれる子要素のレイアウト動作を提供するオブジェクトです。 PANEL クラスからカスタム クラスを派生させることで、XAML レイアウトのカスタム パネル を定義できます。 パネルの動作を提供するには、 ArrangeOverride メソッドと MeasureOverride メソッドをオーバーライドし、子要素を測定および配置するロジックを提供します。 この例は Panel から派生しています。 Panel から開始する場合、ArrangeOverride メソッドと MeasureOverride メソッドには開始動作はありません。 コードは、子要素が XAML レイアウト システムに認識され、UI にレンダリングされるゲートウェイを提供しています。 そのため、コードがすべての子要素を考慮し、レイアウト システムが期待するパターンに従うのが本当に重要です。

あなたのレイアウトシナリオ

カスタム パネルを定義するときは、レイアウト シナリオを定義します。

レイアウト シナリオは、次の方法で表されます。

  • パネルに子要素がある場合の処理
  • パネルに独自のスペースに制約がある場合
  • パネルのロジックによって、最終的に子の UI レイアウトがレンダリングされるすべての測定、配置、位置、およびサイズを決定する方法

そのことを念頭に置いて、次に示す BoxPanel は特定のシナリオを対象にしています。 この例で最も重要なコードを保持するために、シナリオについてはまだ詳しく説明しません。代わりに、必要な手順とコーディング パターンに集中します。 最初にシナリオの詳細を知りたい場合 は、「 BoxPanelのシナリオ」に進んでから、コードに戻ります。

最初に Panel から派生する

最初に Panel からカスタム クラスを派生 します。 おそらくこれを実行する最も簡単な方法は、このクラスのために別のコードファイルを定義することです。Microsoft Visual Studio の Add | New Item | Class コンテキストメニューオプションを使用して、Solution Explorer のプロジェクトから操作します。 クラス (およびファイル) BoxPanelに名前を付けます。

クラスのテンプレートファイルは、Windowsアプリ専用ではないため、多くの using ステートメントで始まることはありません。 まず、using ステートメントを追加します。 テンプレート ファイルは、おそらく不要なusing ステートメントがいくつか含まれており、削除できます。 一般的なカスタム パネル コードに必要な型を解決できる using ステートメントの一覧を次に示します。

using System;
using System.Collections.Generic; // if you need to cast IEnumerable for iteration, or define your own collection properties
using Windows.Foundation; // Point, Size, and Rect
using Windows.UI.Xaml; // DependencyObject, UIElement, and FrameworkElement
using Windows.UI.Xaml.Controls; // Panel
using Windows.UI.Xaml.Media; // if you need Brushes or other utilities

これで Panel を解決できるようになったので、それを BoxPanel の基底クラスにします。 また、 BoxPanel パブリックにします。

public class BoxPanel : Panel
{
}

クラス レベルで、いくつかのロジック関数で共有される int 値と double 値を定義しますが、パブリック API として公開する必要はありません。 この例では、 maxrcrowcountcolcountcellwidthcellheightmaxcellheightaspectratioという名前です。

これを完了すると、完全なコード ファイルは次のようになります ( 使用に関するコメントを削除すると、その理由がわかります)。

using System;
using System.Collections.Generic;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

public class BoxPanel : Panel 
{
    int maxrc, rowcount, colcount;
    double cellwidth, cellheight, maxcellheight, aspectratio;
}

ここからは、メソッドのオーバーライドや依存関係プロパティなどのサポートなど、一度に 1 つのメンバー定義を示します。 これらは、上記のスケルトンに任意の順序で追加できます。

MeasureOverride

protected override Size MeasureOverride(Size availableSize)
{
    // Determine the square that can contain this number of items.
    maxrc = (int)Math.Ceiling(Math.Sqrt(Children.Count));
    // Get an aspect ratio from availableSize, decides whether to trim row or column.
    aspectratio = availableSize.Width / availableSize.Height;

    // Now trim this square down to a rect, many times an entire row or column can be omitted.
    if (aspectratio > 1)
    {
        rowcount = maxrc;
        colcount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc;
    } 
    else 
    {
        rowcount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc;
        colcount = maxrc;
    }

    // Now that we have a column count, divide available horizontal, that's our cell width.
    cellwidth = (int)Math.Floor(availableSize.Width / colcount);
    // Next get a cell height, same logic of dividing available vertical by rowcount.
    cellheight = Double.IsInfinity(availableSize.Height) ? Double.PositiveInfinity : availableSize.Height / rowcount;
           
    foreach (UIElement child in Children)
    {
        child.Measure(new Size(cellwidth, cellheight));
        maxcellheight = (child.DesiredSize.Height > maxcellheight) ? child.DesiredSize.Height : maxcellheight;
    }
    return LimitUnboundedSize(availableSize);
}

MeasureOverride 実装の必要なパターンは、Panel.Children 内の各要素を介したloopです。 これらの各要素で常に Measure メソッドを呼び出します。 Measure には Size 型のパラメーターを持っています。 ここで渡しているのは、パネルがその特定の子要素で使用できるようにコミットしているサイズです。 そのため、loopを実行し、Measure の呼び出しを開始する前に、各セルが割り当てることができる領域を把握しておく必要があります。 MeasureOverride メソッド自体には availableSize 値があります。 これは、パネルの親が Measure を呼び出したときに使用したサイズです。これは、最初に呼び出されるこの MeasureOverride のトリガーでした。 したがって、一般的なロジックは、各子要素がパネルの全体的な availableSize の領域を分割するスキームを考案することです。 次に、サイズの各除算を各子要素の Measure に渡します。

サイズ BoxPanel 分割する方法は非常に簡単です。スペースは、項目の数によって主に制御される多数のボックスに分割されます。 ボックスのサイズは、行と列の数と使用可能なサイズに基づいて調整されます。 正方形の一部の行や列が必要ない場合、それを省略することがあります。その結果、パネルは行と列の比率が正方形ではなく長方形になります。 このロジックがどのように到着したかの詳細については、「 BoxPanel のシナリオ」に進んでください。

では、その法案は何をするのでしょうか? 各要素で Measure が呼び出された場合、読み取り専用の DesiredSize プロパティの値が設定されます。 DesiredSize は配置パスに到達した後に DesiredSize 値を持つことが重要になる可能性があります。これは、DesiredSize が配置時と最終レンダリング時に可能なサイズと必要なサイズを伝えるためです。 独自のロジックで DesiredSize を使用しない場合でも、システムには引き続き必要です。

availableSize の高さコンポーネントが無制限の場合は、このパネルを使用できます。 それが本当であれば、パネルには分割するための既知の高さがありません。 この場合、測定パスの論理は、制約のある高さがまだないことを各子に知らせます。 子に対して Size.Height が無限である場合の Measure 呼び出しに Size を渡すことによってそれを行います。 これは有効です。 Measure が呼び出されると、DesiredSize がこれらの最小値として設定されます。Measure に渡されたもの、または明示的に設定された HeightWidth などの要素の自然なサイズです。

StackPanel の内部ロジックにも、この動作があります。StackPanel は、子の Measure に無限ディメンション値を渡し、向きディメンションの子に制約がないことを示します。 StackPanel は、通常、そのディメンションで拡張されるスタック内のすべての子を収容するために、動的にサイズを設定します。

ただし、パネル自体は MeasureOverride から無限の値を持つ Size を返すことができません。それを行うと、レイアウト中に例外がスローされます。 そのため、ロジックの一部は、子が要求する最大の高さを調べるためであり、パネルの独自のサイズ制約から取得されていない場合は、その高さをセルの高さとして使用します。 前のコードで参照されたヘルパー関数 LimitUnboundedSize を次に示します。このヘルパー関数は、その最大セルの高さを受け取り、それを使用してパネルに返される有限の高さを与え、配置パスが開始される前に cellheight が有限の数値であることを保証します。

// This method limits the panel height when no limit is imposed by the panel's parent.
// That can happen to height if the panel is close to the root of main app window.
// In this case, base the height of a cell on the max height from desired size
// and base the height of the panel on that number times the #rows.
Size LimitUnboundedSize(Size input)
{
    if (Double.IsInfinity(input.Height))
    {
        input.Height = maxcellheight * colcount;
        cellheight = maxcellheight;
    }
    return input;
}

ArrangeOverride メソッド

protected override Size ArrangeOverride(Size finalSize)
{
     int count = 1;
     double x, y;
     foreach (UIElement child in Children)
     {
          x = (count - 1) % colcount * cellwidth;
          y = ((int)(count - 1) / colcount) * cellheight;
          Point anchorPoint = new Point(x, y);
          child.Arrange(new Rect(anchorPoint, child.DesiredSize));
          count++;
     }
     return finalSize;
}

ArrangeOverride 実装の必要なパターンは、Panel.Children の各要素を介したloopです。 これらの各要素で必ず Arrange メソッドを呼び出します。

MeasureOverride ほど多くの計算がない点に注意してください。これは一般的です。 子のサイズは、パネル独自のMeasureOverride ロジックや、測定パス中に設定される各子要素のDesiredSize 値から既に知られています。 ただし、各子が表示されるパネル内の場所を決定する必要があります。 一般的なパネルでは、各子は異なる位置にレンダリングする必要があります。 重複する要素を作成するパネルは、一般的なシナリオでは望ましくありません (ただし、実際に意図したシナリオである場合は、意図的な重複を持つパネルを作成することは問題ではありません)。

このパネルは、行と列の概念によって配置されます。 行と列の数は既に計算されています (測定に必要でした)。 したがって、行と列の形状と各セルの既知のサイズは、このパネルに含まれる各要素のレンダリング位置 ( anchorPoint) を定義するロジックに寄与します。 そのPointは、メジャーから既に認識されているSizeと共に、Rectを構成する2つのコンポーネントとして使用されます。 Rect、Arrange の入力の種類です。

パネルのコンテンツをクリップする必要がある場合があります。 その場合、クリップされたサイズは DesiredSize に存在するサイズであり、これは計測ロジックによって Measure に渡された値の最小値またはその他の自然なサイズの要素として設定されます。 したがって、通常、Arrange中にクリッピングを特にチェックする必要はありません。クリッピングは、DesiredSizeを各Arrange呼び出しに渡すことで自動的に行われます。

レンダリング位置を定義するために必要なすべての情報が他の方法で認識されている場合は、loopを通過するときに必ずしもカウントが必要であるとは限りません。 たとえば、 Canvas レイアウト ロジックでは、 Children コレクション内の位置は関係ありません。 Canvas 内の各要素を整列させるために必要なすべての情報は、整列ロジックの一部として子要素の Canvas.LeftCanvas.Top の値を読み込むことによって認識されます。 BoxPanelロジックでは、新しい行を開始して y 値をオフセットするタイミングがわかるように、colcount と比較するカウントが必要になります。

通常、ArrangeOverride 実装から返される入力 finalSizeSize は同じです。 理由の詳細については、XAML カスタム パネルの概要の「ArrangeOverride」セクションを参照してください。

絞り込み: 行数と列数を制御する

このパネルは、今と同じようにコンパイルして使用できます。 ただし、もう 1 つの絞り込みを追加します。 先ほど示したコードでは、ロジックによって、縦横比が最も長い側に追加の行または列が配置されます。 ただし、セルの図形をより詳細に制御するためには、パネルの縦横比が "縦" であっても、3x4 ではなく 4x3 のセルセットを選択することが望ましい場合があります。そのため、パネルの利用者がその動作を制御するために設定可能な、任意の依存関係プロパティを追加します。 非常に基本的な依存関係プロパティの定義を次に示します。

// Property
public Orientation Orientation
{
    get { return (Orientation)GetValue(OrientationProperty); }
    set { SetValue(OrientationProperty, value); }
}

// Dependency Property Registration
public static readonly DependencyProperty OrientationProperty =
        DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(BoxPanel), new PropertyMetadata(null, OnOrientationChanged));

// Changed callback so we invalidate our layout when the property changes.
private static void OnOrientationChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
    if (dependencyObject is BoxPanel panel)
    {
        panel.InvalidateMeasure();
    }
}

次に、Orientationを使用することによってMeasureOverrideの測定ロジックに及ぼす影響を説明します。 実際に行っているのは、 rowcountcolcountmaxrc と真の縦横比から導き出される方法を変更することです。そのため、各セルに対応するサイズの違いがあります。 Orientation[縦] (既定値) の場合、"ポートレート" 四角形レイアウトの行数と列数を決定する前に、実際の縦横比の値がまず反転されます。

// Get an aspect ratio from availableSize, decides whether to trim row or column.
aspectratio = availableSize.Width / availableSize.Height;

// Transpose aspect ratio based on Orientation property.
if (Orientation == Orientation.Vertical) { aspectratio = 1 / aspectratio; }

BoxPanel のシナリオ

BoxPanelの特定のシナリオは、スペースを分割する方法の主な決定要因の 1 つは、子項目の数を把握し、パネルの既知の使用可能な領域を分割することです。 パネルは、本質的に四角形です。 多くのパネルは、その四角形のスペースをさらに四角形に分割することによって動作します。これは、 Grid がセルに対して行う処理です。 Grid の場合、セルのサイズは ColumnDefinition 値と RowDefinition 値によって設定され、要素は Grid.Row および Grid.Column 添付プロパティを使用して、セルが入る正確なセルを宣言します。 グリッドから適切なレイアウトを取得するには、通常、十分なセルがあり、各子要素が独自のセルに収まるように添付プロパティを設定するために、事前に子要素の数を把握しておく必要があります。

しかし、子の数が動的な場合はどうでしょうか。 それは確かに可能です。アプリ コードは、UI を更新する価値があるほど重要であると考えられる動的な実行時条件に応じて、コレクションに項目を追加できます。 データ バインディングを使用してコレクション/ビジネス オブジェクトをバッキングする場合、このような更新の取得と UI の更新は自動的に処理されるため、多くの場合、推奨される手法です (詳細な データ バインディングを参照)。

ただし、すべてのアプリ シナリオがデータ バインディングに役立つわけではありません。 場合によっては、実行時に新しい UI 要素を作成して表示する必要があります。 BoxPanel は、このシナリオ用です。 子項目の数が変わっても、BoxPanel はそれを計算に取り入れ、既存の子要素と新しい子要素をすべて収めるために新しいレイアウトに調整するので、問題はありません。

BoxPanelをさらに拡張するための高度なシナリオ (ここでは示されていません) では、動的な子に対応し、子の DesiredSize を個々のセルのサイズ設定のより強力な要素として使用できます。 このシナリオでは、さまざまな行または列のサイズ、またはグリッド以外の図形を使用して、"無駄な" 領域を減らすことができます。 これには、さまざまなサイズと縦横比の複数の四角形を、美学と最小サイズの両方を含む四角形に収まる方法の戦略が必要です。 BoxPanel それはしません。スペースを分割するためのより簡単な手法を使用しています。 BoxPanelの技法は、子カウントより大きい最小平方数を決定することです。 たとえば、9 個の項目は 3 x 3 の正方形に収まります。 10 個のアイテムには 4 x 4 の正方形が必要です。 ただし、多くの場合、開始四角形の 1 つの行または列を削除しながら項目を収めてスペースを節約できます。 count=10 の例では、4x3 または 3x4 の四角形に収まります。

パネルが 10 個の項目に対して 5x2 を選択しないのはなぜか疑問に思うかもしれません。これは項目番号にきちんと合うためです。 ただし、実際には、パネルは特に強い向きの縦横比を持つことはほとんどない四角形として設計されています。 最小二乗法は、サイズ設定ロジックを典型的なレイアウトの形状に適合させ、セルの形状が不自然な縦横比にならないようにする方法です。

リファレンス

概念