BoxPanel, 예제 사용자 지정 패널

사용자 지정 패널 클래스에 대한 코드를 작성하고, ArrangeOverrideMeasureOverride 메서드를 구현하고, Children 속성을 사용하는 방법을 알아봅니다.

중요 API: Panel, ArrangeOverride,MeasureOverride

예제 코드는 사용자 지정 패널 구현을 보여 주지만 다양한 레이아웃 시나리오에 맞게 패널을 사용자 지정하는 방법에 영향을 주는 레이아웃 개념을 설명하는 데 많은 시간을 할애하지는 않습니다. 이러한 레이아웃 개념 및 특정 레이아웃 시나리오에 적용할 수 있는 방법에 대한 자세한 내용은 XAML 사용자 지정 패널 개요를 참조하세요.

패널은 XAML 레이아웃 시스템이 실행되고 앱 UI가 렌더링될 때 포함된 자식 요소에 대한 레이아웃 동작을 제공하는 개체입니다. Panel 클래스에서 사용자 지정 클래스를 파생시켜 XAML 레이아웃에 대한 사용자 지정 패널을 정의할 수 있습니다. ArrangeOverrideMeasureOverride 메서드를 재정의하고 자식 요소를 측정하고 정렬하는 논리를 제공하여 패널에 대한 동작을 제공합니다. 이 예제는 Panel에서 파생됩니다. Panel에서 시작하면 ArrangeOverrideMeasureOverride 메서드에 시작 동작이 없습니다. 코드는 자식 요소가 XAML 레이아웃 시스템에 알려지고 UI에서 렌더링되는 게이트웨이를 제공합니다. 따라서 코드가 모든 자식 요소를 고려하고 레이아웃 시스템에서 예상하는 패턴을 따르는 것이 매우 중요합니다.

레이아웃 시나리오

사용자 지정 패널을 정의할 때 레이아웃 시나리오를 정의합니다.

레이아웃 시나리오는 다음을 통해 표현됩니다.

  • 패널에 자식 요소가 있을 때 수행할 작업
  • 패널에 자체 공간에 제약 조건이 있는 경우
  • 패널의 로직이 모든 측정, 배치, 위치 및 크기를 결정하여 결국 렌더링된 자식 UI 레이아웃을 생성하는 방법

이 점을 염두에 두고 여기에 표시된 BoxPanel은 특정 시나리오에 대한 것입니다. 이 예제에서는 코드에 중점을 두기 위해 시나리오를 자세히 설명하지 않고 필요한 단계와 코딩 패턴에 집중합니다. 시나리오에 대해 먼저 자세히 알아보려면 "BoxPanel"에 대한 시나리오"로 건너뛰고 코드로 돌아갑니다.

패널에서 파생하여 시작

먼저 Panel에서 사용자 지정 클래스를 파생합니다. 이 작업을 수행하는 가장 쉬운 방법은 Microsoft Visual Studio의 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
{
}

클래스 수준에서 여러 논리 함수에서 공유되지만 공용 API로 노출될 필요가 없는 일부 intdouble을 정의합니다. 이 예제에서는 이름이 maxrc, rowcount, colcount, cellwidth, cellheight, maxcellheight, aspectratio로 지정됩니다.

이 작업을 완료한 후 전체 코드 파일은 다음과 같이 표시됩니다(을 사용하여 에 대한 주석을 제거했으니 이제 그 이유를 알 수 있습니다).

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;
}

이제부터는 메서드 재정의나 종속성 속성과 같은 지원요소 등 한 번에 하나의 멤버 정의를 보여드리겠습니다. 위의 골격 순서에 관계없이 이를 추가할 수 있습니다.

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의 각 요소를 통한 루프입니다. 이러한 각 요소에서 항상 Measure 메서드를 호출합니다. Measure에는 Size 형식의 매개 변수가 있습니다. 여기서 전달하는 것은 패널이 특정 자식 요소에 사용할 수 있도록 커밋하는 크기입니다. 따라서 루프를 수행하고 Measure 호출을 시작하기 전에 각 셀이 얼마나 많은 공간을 사용할 수 있는지 알아야 합니다. MeasureOverride 메서드 자체에서 availableSize 값이 있습니다. 패널의 부모가 Measure를 호출할 때 사용한 크기입니다. 이는 처음에 호출되는 이 MeasureOverride의 트리거였습니다. 따라서 일반적인 논리는 각 자식 요소가 패널의 전체 availableSize 공간을 나누는 체계를 고안하는 것입니다. 그런 다음 각 자식 요소의 Measure값에 각 크기 나누기를 전달합니다.

BoxPanel가 크기를 나누는 방법은 매우 간단합니다. 공간을 여러 개의 상자로 나누고 항목의 개수에 따라 크기를 조절합니다. 상자는 행 및 열 수와 사용 가능한 크기에 따라 크기가 조정됩니다. 때로는 정사각형의 행 또는 열 중 하나가 필요하지 않으므로 삭제되어 행 : 열 비율 측면에서 패널이 정사각형이 아닌 직사각형이 됩니다. 이 논리가 어떻게 도착했는지에 대한 자세한 내용은 "BoxPanel 시나리오"로 건너뜁니다.

그렇다면 측정값이 통과되면 어떻게 될까요? Measure가 호출된 각 요소에서 읽기 전용 DesiredSize 속성의 값을 설정합니다. 정렬 패스에 도착하면 DesiredSize 값이 중요할 수 있습니다. DesiredSize는 정렬할 때와 최종 렌더링에서 크기를 조정할 수 있는 또는 조정해야 하는 크기를 알려줍니다. 자체 논리에서 DesiredSize를 사용하지 않더라도 시스템에는 여전히 필요합니다.

availableSize의 높이 구성 요소가 무제한인 경우 이 패널을 사용할 수 있습니다. 이 경우 패널을 나눌 수 있는 알려진 높이가 없습니다. 이 경우 측정값 패스의 로직은 각 자식에게 아직 제한된 높이가 없음을 알립니다. Size.Height가 무한인 자식에 대한 Measure 호출에 Size를 전달하여 수행합니다. 그것은 합법적입니다. Measure가 호출될 때 논리는 DesiredSize가 다음의 최소값으로 설정된다는 것입니다. 즉, Measure에 전달된 값 또는 명시적으로 설정된 HeightWidth와 같은 요소의 자연 크기입니다.

참고 항목

StackPanel의 내부 논리에도 다음과 같은 동작이 있습니다. StackPanell은 자식의 Measure에 무한 차원 값을 전달하여 방향 차원의 자식에 대한 제약이 없음을 나타냅니다. StackPanel 은 일반적으로 동적으로 크기를 조정하여 해당 크기로 커지는 스택의 모든 자식을 수용합니다.

그러나 패널 자체는 MeasureOverride에서 무한 값을 가진 Size를 반환할 수 없습니다. 레이아웃 중에 예외를 throw합니다. 따라서 논리의 일부는 자식이 요청하는 최대 높이를 확인하고 패널의 자체 크기 제약 조건에서 아직 가져오지 않는 경우 해당 높이를 셀 높이로 사용하는 것입니다. 다음은 이전 코드에서 참조된 도우미 함수 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의 각 요소를 통한 루프입니다. 이러한 각 요소에서 항상 Arrange 메서드를 호출합니다.

MeasureOverride만큼 계산이 많지 않은지 확인합니다. 이것이 일반적입니다. 자식의 크기는 패널의 자체 MeasureOverride 논리 또는 측정 패스 중에 설정된 각 자식 집합의 DesiredSize 값에서 이미 알려져 있습니다. 그러나 각 자식이 표시되는 패널 내의 위치를 결정해야 합니다. 일반적인 패널에서 각 자식은 다른 위치에서 렌더링되어야 합니다. 겹치는 요소를 만드는 패널은 일반적인 시나리오에는 바람직하지 않습니다(의도한 시나리오인 경우 의도적으로 겹치는 패널을 만드는 것은 문제될 것은 없습니다).

이 패널은 행과 열의 개념에 따라 정렬됩니다. 행과 열의 수는 이미 계산되었습니다(측정에 필요). 이제 행과 열의 모양과 각 셀의 알려진 크기가 이 패널에 포함된 각 요소에 대한 렌더링 위치( anchorPoint)를 정의하는 논리에 기여합니다. 해당 지점은 측정값에서 이미 알려진 Size와 함께 Rect를 생성하는 두 구성 요소로 사용됩니다. Rect정렬에 대한 입력 형식입니다.

패널에서 콘텐츠를 잘라야 하는 경우도 있습니다. 이렇게 하면 Measure 논리가 이를 Measure에 전달된 최소값 또는 기타 자연 크기 요소로 설정하기 때문에 잘린 크기는 DesiredSize에 있는 크기입니다. 따라서 일반적으로 Arrange 중에 클리핑을 위해 특별히 검사 필요가 없습니다. 클리핑은 각 Arrange 호출에 DesiredSize를 전달한 후에만 수행됩니다.

렌더링 위치를 정의하는 데 필요한 모든 정보가 다른 수단으로 알려진 경우 루프를 진행하는 동안 항상 개수가 필요하지는 않습니다. 예를 들어 Canvas 레이아웃 논리에서 Children 컬렉션의 위치는 중요하지 않습니다. Canvas에서 각 요소를 배치하는 데 필요한 모든 정보는 Canvas.Left를 읽고 정렬 논리의 일부로 자식 값을 Canvas.Top 값을 읽음으로써 알 수 있습니다. BoxPanel 논리는 새 행을 시작하고 y값을 오프셋해야 하는 시기를 알 수 있도록 colcount와 비교할 개수가 필요합니다.

일반적으로 입력 finalSizeArrangeOverride 구현에서 반환하는 Size는 동일합니다. 그 이유에 대한 자세한 내용은 XAML 사용자 지정 패널 개요의 "ArrangeOverride" 색션을 참고하세요.

구체화: 행 및 열 개수 제어

이 패널을 지금 그대로 컴파일하여 사용할 수 있습니다. 그러나 한 가지 더 구체화를 추가하겠습니다. 방금 표시된 코드에서 논리는 가로 세로 비율에서 가장 긴 쪽에 추가 행 또는 열을 배치합니다. 그러나 셀 셰이프를 보다 효율적으로 제어하려면 패널의 가로 세로 비율이 "세로"인 경우에도 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에 대한 특정 시나리오는 공간을 나누는 방법에 대한 기본 결정자 중 하나가 자식 항목의 수를 알고 패널에 대해 알려진 사용 가능한 공간을 나누는 패널이라는 것을 알 수 있습니다. 패널은 본질적으로 직사각형 모양입니다. 많은 패널이 해당 사각형 공간을 추가 사각형으로 분할하여 작동합니다. 즉, Grid가 셀에 대해 수행하는 기능입니다. Grid의 경우 셀 크기는 ColumnDefinitionRowDefinition값으로 설정되며 요소는 Grid.RowGrid.Column 연결 속성을 사용하여 정확한 셀을 선언합니다. Grid에서 좋은 레이아웃을 가져오려면 일반적으로 충분한 셀이 있고 각 자식 요소가 연결된 속성을 자체 셀에 맞게 설정하도록 자식 요소의 수를 미리 알고 있어야 합니다.

그러나 자식 수가 유동적이라면 어떻게 해야 할까요? 앱 코드에서 UI를 업데이트할 가치가 있을 만큼 중요하다고 판단되는 동적 런타임 조건에 따라 컬렉션에 항목을 추가할 수 있습니다. 데이터 바인딩을 사용하여 컬렉션/비즈니스 개체를 백업하는 경우 이러한 업데이트를 가져오고 UI를 업데이트하면 자동으로 처리되므로 기본 설정 기술인 경우가 많습니다(데이터 바인딩 심층 참조).

그러나 모든 앱 시나리오가 데이터 바인딩에만 적용되는 것은 아닙니다. 경우에 따라 런타임에 새 UI 요소를 만들고 표시하도록 해야 합니다. BoxPanel은 이 시나리오에 적합합니다. BoxPanel은 항목의 수가 변경되면 계산에서 자식 개수를 사용하고 기존 요소와 새 자식 요소를 모두 새 레이아웃으로 조정하므로 문제가 되지 않습니다.

BoxPanel을 더 확장하기 위한 고급 시나리오(여기에 표시되지 않음)는 동적 자식을 수용하고 자식의 DesiredSize를 개별 셀의 크기 조정을 위한 더 강력한 요인으로 사용할 수 있습니다. 이 시나리오에서는 다양한 행 또는 열 크기 또는 그리드가 아닌 모양을 사용하여 '낭비되는' 공간을 줄일 수 있습니다. 이를 위해서는 다양한 크기와 가로 세로 비율의 여러 직사각형이 모두 모두 미적 감각과 최소 크기를 위해 하나의 직사각형 안에 들어갈 수 있는 전략이 필요합니다. BoxPanel은 이 작업을 수행하지 않고 공간을 나누는 더 간단한 기법을 사용합니다. BoxPanel의 기법은 자식 개수보다 큰 최소 정사각형 수를 결정하는 것입니다. 예를 들어 9개 항목은 3x3 정사각형에 맞습니다. 10개 항목에는 4x4 정사각형이 필요합니다. 그러나 공간을 절약하기 위해 시작 사각형의 한 행 또는 열을 제거하면서 항목에 맞출 수 있는 경우가 많습니다. count=10 예제에서 4x3 또는 3x4 사각형에 맞습니다.

10개 품목에 대해 5x2를 선택하지 않는 이유가 궁금할 수 있는데, 품목 번호에 깔끔하게 맞기 때문입니다. 그러나 실제로는 패널의 크기가 직사각형으로 되어 있어 가로 세로 비율이 강한 경우는 거의 없습니다. 최소 제곱 기술은 일반적인 레이아웃 모양에서 잘 작동하도록 크기 조정 논리를 편향시키고 셀 모양이 이상한 가로세로 비율을 갖는 경우 크기 조정을 권장하지 않는 방법입니다.

참조

개념