다음을 통해 공유


BoxPanel, 예제 사용자 지정 패널

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

중요 API: 패널, ArrangeOverride,MeasureOverride

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

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

레이아웃 시나리오

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

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

  • 패널에 자식 요소가 있을 때 수행할 작업
  • 패널에 자체 공간에 제약 조건이 있는 경우
  • 패널의 논리가 최종적으로 자식의 렌더링된 UI 레이아웃을 초래하는 모든 측정값, 배치, 위치 및 크기 조정을 결정하는 방법

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

패널 파생하여 시작

Panel에서 사용자 지정 클래스를 생성합니다. 이 작업을 수행하는 가장 쉬운 방법은 Microsoft Visual Studio의 솔루션 탐색기 프로젝트에 대한 | | 클래스 상황에 맞는 메뉴 옵션을 사용하여 이 클래스에 대한 별도의 코드 파일을 정의하는 것입니다. 클래스와 파일의 이름을 BoxPanel으로 지정해주세요.

클래스 템플릿 파일은 Windows 앱용이 아니기 때문에 많은 문으로 시작하지 않습니다. 먼저 문을 사용하여 을 추가하십시오. 템플릿 파일은 아마도 필요하지 않을 몇 가지 , 문으로 시작하여 삭제할 수 있습니다. 다음은 일반적인 사용자 지정 패널 코드에 필요한 유형을 해결할 수 있는 문을 사용한 의 목록입니다.

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

이제 패널을(를) 해결할 수 있으므로, BoxPanel의 기본 클래스로 지정합니다. 또한 BoxPanel을 공개로 설정하십시오.

public class BoxPanel : Panel
{
}

클래스 수준에서 일부 int 정의하고 여러 논리 함수에서 공유되지만 공용 API로 노출될 필요는 없는 이중 값을 . 예제에서는 이름이 다음과 같이 지정됩니다: 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;
}

앞으로는 메서드 재정의나 종속성 속성과 같은 지원적인 요소일 경우를 포함하여 멤버 정의를 한 번에 하나씩 보여 드리겠습니다. 위의 골격에 어떤 순서로든 추가할 수 있습니다.

측정 재정의

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

BoxPanel 크기를 나누는 방법은 매우 간단합니다. 공간을 항목 수에 의해 주로 제어되는 여러 상자로 나눕니다. 상자는 행 및 열 수와 사용 가능한 크기에 따라 크기가 조정됩니다. 경우에 따라 정사각형의 일부 행이나 열이 필요 없어서 제거되면, 패널은 행 대 열 비율 측면에서 정사각형이 아닌 직사각형이 됩니다. 이 논리가 도출된 방식을 알고 싶다면 "BoxPanel에 대한 시나리오"로 건너뛰십시오.로 이동하십시오.

그렇다면 그 조치를 통과시키면 어떤 결과가 있을까요? Measure가 호출된 각 요소에서 읽기 전용 DesiredSize 속성의 값을 설정합니다. 정렬 단계에 들어가면 DesiredSize 값을 갖는 것이 중요할 수 있습니다. 이는 DesiredSize가 정렬 시와 최종 렌더링에서 크기가 어떻게 될 것인지를 전달하기 때문입니다. 자체 논리에서 DesiredSize 를 사용하지 않더라도 시스템에는 여전히 필요합니다.

availableSize의 높이 구성 요소가 바인딩되지 않은 경우 이 패널을 사용할 수 있습니다. 이 경우 패널에 나눌 알려진 높이가 없습니다. 이 경우, 측정 과정에서의 논리는 각 자식에게 아직 제한된 높이가 없음을 알립니다. 자식에 대한 측정 호출시 크기을 제공합니다. 여기서 Size.Height은 무한입니다. 그것은 합법적입니다. Measure 호출되면, 로직은 DesiredSizeMeasure에 전달된 값과 명시적으로 설정된 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;
}

배열재정의

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)를 정의하는 논리에 기여합니다. 해당 지점은 측정값에서 이미 알려진 크기와 함께 Rect을 구성하는 두 가지 요소로 사용됩니다. Rect정렬의 입력 형식입니다.

패널이 콘텐츠를 잘라야 하는 경우가 있습니다. 그들이 그렇게 할 경우, 클리핑된 크기는 DesiredSize에 있는 크기입니다. 이는 Measure에 전달된 값의 최소값 또는 다른 자연 크기 요인으로 설정하기 때문입니다. Measure 논리는 이를 최소값으로 설정합니다. 따라서 일반적으로 정렬동안에는 클리핑을 구체적으로 확인할 필요가 없습니다. 클리핑은 단지 DesiredSize가 각 정렬 호출에 전달됨에 따라 자동으로 발생합니다.

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

일반적으로, 입력 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측정 논리에 영향을 미치는 방법입니다. rowcount 및 실제 가로 세로 비율에서 colcountmaxrc이 파생되는 방식을 변경하는 것이며, 이로 인해 각 셀에 크기 차이가 있습니다. 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는 계산에서 자식 개수를 사용하며, 기존 자식 요소와 새로운 자식 요소를 모두 새로운 레이아웃에 맞게 조정하여 모두 잘 맞도록 합니다.

BoxPanel를 더 확장하는 고급 시나리오(여기서는 보여주지 않음)는 동적 자식을 수용할 수 있으며, 자식의 DesiredSize를 개별 셀 크기 조정에 더 효과적으로 반영할 수 있습니다. 이 시나리오에서는 "낭비되는" 공간이 적도록 다양한 행 또는 열 크기 또는 눈금이 아닌 셰이프를 사용할 수 있습니다. 이를 위해서는 다양한 크기와 가로 세로 비율의 여러 직사각형이 모두 심미적 및 가장 작은 크기의 포함 사각형에 모두 맞을 수 있는 방법에 대한 전략이 필요합니다. BoxPanel 이 작업을 수행하지 않습니다. 공간을 나누는 더 간단한 기술을 사용합니다. BoxPanel기술은 자식 개수보다 큰 최소 제곱 수를 결정하는 것입니다. 예를 들어 9개 항목은 3x3 정사각형에 맞습니다. 10개 항목에는 4x4 정사각형이 필요합니다. 그러나 시작 사각형의 한 행 또는 열을 제거하면서도 물건을 그 안에 맞출 수 있는 경우가 많아 공간을 절약할 수 있습니다. count=10 예제에서 그것은 4x3 또는 3x4 사각형에 맞습니다.

패널이 왜 항목 수에 딱 들어맞는 5x2 대신 다른 것을 선택하지 않았는지 궁금할 수 있습니다. 패널은 실제로 뚜렷한 가로 세로 비율이 거의 없는 사각형으로 크기가 결정됩니다. 최소 제곱 기술은 일반적인 레이아웃 셰이프에서 잘 작동하도록 크기 조정 논리를 편향시키고 셀 셰이프가 홀수 가로 세로 비율을 얻는 크기 조정을 권장하지 않는 방법입니다.

참조

개념