Compartilhar via


"Tutorial: Criar um painel personalizado

Aprenda a escrever código para uma classe de Painel personalizada, implementando métodos ArrangeOverride e MeasureOverride e usando a propriedade Children .

APIs importantes: Panel, ArrangeOverride, MeasureOverride

O código de exemplo mostra uma implementação de painel personalizado, mas não dedicamos muito tempo explicando os conceitos de layout que influenciam como você pode personalizar um painel para diferentes cenários de layout. Se você quiser mais informações sobre esses conceitos de layout e como eles podem se aplicar ao seu cenário de layout específico, confira a visão geral dos painéis personalizados XAML.

Um painel é um objeto que fornece um comportamento de layout para elementos filho que ele contém, quando o sistema de layout XAML é executado e a interface do usuário do aplicativo é renderizada. Você pode definir painéis personalizados para layout XAML derivando uma classe personalizada da classe painel . Você fornece comportamento para o painel substituindo os métodos ArrangeOverride e MeasureOverride , fornecendo a lógica que mede e organiza os elementos filho. Este exemplo deriva do Painel. Quando você começa no Painel, os métodos ArrangeOverride e MeasureOverride não têm um comportamento inicial. Seu código está fornecendo o ponto de entrada pelo qual os elementos filho se tornam conhecidos pelo sistema de layout XAML e são renderizados na interface do usuário. Portanto, é muito importante que seu código contemple todos os elementos filho e siga os padrões que o sistema de layout espera.

Seu cenário de layout

Ao definir um painel personalizado, você está definindo um cenário de layout.

Um cenário de layout é expresso por meio de:

  • O que o painel fará quando tiver elementos-filho
  • Quando o painel tem restrições em seu próprio espaço
  • Como a lógica do painel determina todas as medidas, posicionamento, posições e dimensionamentos que eventualmente resultam em um layout de interface do usuário renderizado de filhos

Com isso em mente, o BoxPanel mostrado aqui é para um cenário específico. No interesse de manter o código acima de tudo neste exemplo, ainda não explicaremos o cenário em detalhes e, em vez disso, nos concentraremos nas etapas necessárias e nos padrões de codificação. Se você quiser saber mais sobre o cenário primeiro, vá para "O cenário para BoxPanel" e volte para o código.

Comece derivando do Painel

Comece derivando uma classe personalizada do Painel. Provavelmente, a maneira mais fácil de fazer isso é definir um arquivo de código separado para essa classe, usando as opções de menu de contexto Add | New Item | Class de um projeto do Solution Explorer no Microsoft Visual Studio. Nomeie a classe (e o arquivo) BoxPanel.

O arquivo de modelo de uma classe não começa com muitas instruções de uso porque não é especificamente para aplicativos do Windows. Então, primeiro, adicione declarações using. O arquivo de modelo também começa com algumas instruções using que você provavelmente não precisa e podem ser excluídas. Aqui está uma lista sugerida de uso de instruções que podem resolver os tipos necessários para o código de painel personalizado típico:

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

Agora que você pode resolver o Painel, torne-o a classe base de BoxPanel. Além disso, torne BoxPanel público:

public class BoxPanel : Panel
{
}

No nível da classe, defina alguns valores int e duplos que serão compartilhados por várias de suas funções lógicas, mas que não precisarão ser expostos como API pública. No exemplo, eles são nomeados: maxrc, , rowcount, colcount, cellwidth, , cellheight, , maxcellheight. aspectratio

Depois de fazer isso, o arquivo de código completo terá esta aparência (removendo comentários sobre como usar, agora que você sabe por que os temos):

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

Daqui em diante, passaremos a mostrar uma definição de membro por vez, seja uma substituição de método ou algo que ofereça suporte, como uma propriedade de dependência. Você pode adicioná-los ao esqueleto acima em qualquer ordem.

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

O padrão necessário de uma implementação de MeasureOverride é fazer um loop por meio de cada elemento em Panel.Children. Sempre chame o método Measure em cada um desses elementos. A medida tem um parâmetro do tipo Tamanho. O que você está passando aqui é o tamanho que seu painel está se comprometendo a ter disponível para esse elemento filho específico. Portanto, antes de fazer o loop e começar a chamar Measure, você precisa saber quanto espaço cada célula pode dedicar. No próprio método MeasureOverride , você tem o valor availableSize . Esse é o tamanho usado pelo elemento pai do painel quando ele chamou Measure, que foi o gatilho para esse MeasureOverride ser chamado inicialmente. Portanto, uma lógica típica é criar um esquema pelo qual cada elemento filho divide o espaço do AvailableSize geral do painel. Em seguida, você passa cada divisão de tamanho para Medida de cada elemento filho.

Como BoxPanel divide o tamanho é bastante simples: ele divide seu espaço em várias caixas que são amplamente controladas pelo número de itens. As caixas são dimensionadas com base na contagem de linhas e colunas e no tamanho disponível. Às vezes, uma linha ou coluna de um quadrado não é necessária, então é descartada e o painel se torna um retângulo ao invés de um quadrado em termos de sua relação linha/coluna. Para obter mais informações sobre como essa lógica foi desenvolvida, avance para "O cenário para BoxPanel".

Então, o que a aprovação da medida faz? Ele define um valor para a propriedade somente leitura DesiredSize em cada elemento onde Measure foi chamado. Ter um valor DesiredSize é possivelmente importante quando você chega à etapa de organização, pois o DesiredSize indica qual deve ser o tamanho durante a organização e na renderização final. Mesmo que você não use DesiredSize em sua própria lógica, o sistema ainda precisará dele.

É possível que esse painel seja usado quando o componente de altura do availableSize não estiver limitado. Se isso for verdade, o painel não tem uma altura conhecida para dividir. Nesse caso, a lógica da aprovação da medida informa a cada filho que ela ainda não tem uma altura limitada. Ele faz isso passando um tamanho para a chamada medida para crianças em que Size.Height é infinito. Isso é legal. Quando Measure é chamado, a lógica é que o DesiredSize é definido como o menor entre: o valor passado para Measure ou o tamanho natural do elemento, proveniente de fatores como Altura e Largura explicitamente definidos.

Observação

A lógica interna do StackPanel também tem esse comportamento: StackPanel passa um valor de dimensão infinita para Measure on children, indicando que não há nenhuma restrição em crianças na dimensão de orientação. O StackPanel normalmente se dimensiona dinamicamente, para acomodar todos os filhos em uma pilha que cresce nessa dimensão.

No entanto, o próprio painel não pode retornar um Tamanho com um valor infinito de MeasureOverride; que gera uma exceção durante o layout. Portanto, parte da lógica é descobrir a altura máxima que qualquer filho solicita e usar essa altura como a altura da célula, caso isso ainda não venha das restrições de tamanho do próprio painel. Aqui está a função LimitUnboundedSize auxiliar que foi referenciada no código anterior, que primeiro toma essa altura máxima da célula e a usa para definir uma altura finita para o painel retornar. Além disso, assegura que cellheight seja um número finito antes que a passagem de arranjo seja iniciada.

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

O padrão necessário de uma implementação ArrangeOverride é percorrer cada elemento em Panel.Children. Sempre chame o método Arrange em cada um desses elementos.

Observe como não há tantos cálculos como no MeasureOverride; isso é típico. O tamanho das crianças já é conhecido da própria lógica MeasureOverride do painel ou do valor DesiredSize de cada criança definido durante a passagem de medição. No entanto, ainda precisamos decidir o local dentro do painel em que cada filho será exibido. Em um painel típico, cada filho deve renderizar em uma posição diferente. Um painel que cria elementos sobrepostos não é desejável para cenários típicos (embora não esteja fora de questão criar painéis que tenham sobreposições propositais, se esse for realmente o cenário pretendido).

Este painel é organizado pelo conceito de linhas e colunas. O número de linhas e colunas já foi calculado (era necessário para medição). Portanto, agora a forma das linhas e colunas mais os tamanhos conhecidos de cada célula contribuem para a lógica de definir uma posição de renderização (o anchorPoint) para cada elemento que este painel contém. Esse ponto, junto com o Tamanho já conhecido por medição, são usados como os dois componentes que constroem um Rect. Rect é o tipo de entrada para Arrange.

Às vezes, os painéis precisam recortar seu conteúdo. Se isso acontecer, o tamanho recortado será o tamanho que está presente no DesiredSize, pois a lógica Measure o define como o mínimo entre o valor passado para Measure ou outros fatores de tamanho naturais. Portanto, normalmente, você não precisa verificar especificamente se há recorte durante Arrange; o recorte ocorre simplesmente com base na passagem do DesiredSize através de cada chamada Arrange.

Nem sempre você precisa de uma contagem durante o loop caso todas as informações necessárias para definir a posição de renderização forem conhecidas por outros meios. Por exemplo, na lógica de layout Canvas, a posição na coleção Children não importa. Todas as informações necessárias para posicionar cada elemento em uma Tela são conhecidas lendo Canvas.Left e Canvas.Top valores de filhos como parte da lógica de organização. Acontece BoxPanel que a lógica precisa de uma contagem para comparar com o colcount, para saber quando iniciar uma nova linha e ajustar o valor y.

É comum que o finalSize de entrada e o tamanho retornado de uma implementação ArrangeOverride sejam os mesmos. Para obter mais informações sobre o motivo, consulte a seção "ArrangeOverride" da visão geral dos painéis personalizados XAML.

Um refinamento: controlando o número de linhas versus colunas

Você pode compilar e usar este painel da mesma forma que está agora. No entanto, adicionaremos mais um refinamento. No código mostrado, a lógica coloca a linha ou coluna extra no lado mais longo na proporção. Mas, para um maior controle sobre as formas das células, pode ser desejável escolher um conjunto de células 4x3 em vez de 3x4, mesmo que a proporção do próprio painel seja "retrato". Portanto, adicionaremos uma propriedade de dependência opcional que o consumidor do painel pode definir para controlar esse comportamento. Aqui está a definição da propriedade de dependência, que é muito básica:

// 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();
    }
}

E abaixo está como o uso Orientation afeta a lógica de medida em MeasureOverride. Na verdade, tudo o que está sendo feito é alterar como rowcount e colcount são derivados de maxrc e da razão de aspecto verdadeira, e há diferenças correspondentes de tamanho para cada célula por causa disso. Quando Orientation for Vertical (padrão), ele inverte o valor da taxa de proporção verdadeira antes de usá-la para contagens de linhas e colunas para nosso layout de retângulo "retrato".

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

O cenário para BoxPanel

O cenário BoxPanel específico é que é um painel em que um dos principais determinantes de como dividir espaço é conhecer o número de itens filho e dividir o espaço disponível conhecido para o painel. Os painéis são intrinsecamente formatos retangulares. Muitos painéis operam dividindo esse espaço retangular em retângulos adicionais; é o que Grid faz com suas células. No caso de Grid, o tamanho das células é definido pelos valores ColumnDefinition e RowDefinition e os elementos declaram a célula exata em que entram com as propriedades anexadas Grid.Row e Grid.Column . Obter um bom layout de uma Grade geralmente requer saber o número de elementos filho antecipadamente, para que haja células suficientes e cada elemento filho defina suas propriedades associadas para se ajustar à sua própria célula.

Mas e se o número de filhos for dinâmico? Isso é certamente possível; O código do aplicativo pode adicionar itens a coleções, em resposta a qualquer condição dinâmica de tempo de execução que você considere importante o suficiente para que valha a pena atualizar sua interface do usuário. Se você estiver usando a associação de dados para fazer backup de coleções/objetos de negócios, obter essas atualizações e atualizar a interface do usuário será tratado automaticamente, de modo que geralmente é a técnica preferida (consulte a associação de dados detalhadamente).

Mas nem todos os cenários de aplicativo se prestam à associação de dados. Às vezes, você precisa criar novos elementos de interface do usuário em runtime e torná-los visíveis. BoxPanel é para este cenário. Uma alteração no número de itens filhos não é um problema para BoxPanel porque está usando a contagem de filhos nos cálculos e ajusta tanto os elementos filhos existentes quanto os novos em um novo layout para que todos se ajustem.

Um cenário avançado para estender BoxPanel ainda mais (não mostrado aqui) poderia acomodar crianças dinâmicas e usar DesiredSize de uma criança como um fator mais forte para o dimensionamento de células individuais. Esse cenário pode usar tamanhos de linha ou coluna variáveis ou formas que não sejam de grade para que haja menos espaço "desperdiçado". Isso requer uma estratégia de como vários retângulos de diferentes tamanhos e proporções podem se encaixar em um retângulo contido, de modo estético e ocupando o menor espaço possível. BoxPanel não faz isso; está usando uma técnica mais simples para dividir espaço. BoxPanelA técnica é determinar o número mínimo quadrado maior que a contagem de filhos. Por exemplo, 9 itens caberiam em um quadrado 3x3. 10 itens necessitam de um quadrado 4x4. No entanto, geralmente você pode ajustar itens enquanto ainda remove uma linha ou coluna do quadrado inicial, para economizar espaço. No exemplo em que count=10, isso se encaixa em um retângulo de 4x3 ou 3x4.

Você pode se perguntar por que o painel não escolheria 5x2 para 10 itens, porque isso se encaixa perfeitamente no número do item. No entanto, na prática, os painéis são dimensionados como retângulos que raramente têm uma proporção bem definida. A técnica de quadrados mínimos é uma maneira de influenciar a lógica de dimensionamento para funcionar bem com formas de layout típicas e não incentivar o dimensionamento em que as formas de célula obtêm proporções ímpares.

Referência

Conceitos