Partilhar via


BoxPanel, um exemplo de painel personalizado

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

APIs importantes: Panel, ArrangeOverrideMeasureOverride

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 ser aplicados ao seu cenário de layout específico, consulte visão geral dos painéis personalizados XAML.

Um painel é um objeto que fornece um comportamento de layout para os elementos filhos que contém, quando o sistema de layout XAML é executado e a interface de utilizador da aplicação é renderizada. Você pode definir painéis personalizados para layout XAML derivando uma classe personalizada da classe Panel . Você fornece comportamento para o seu painel substituindo os métodos ArrangeOverride e MeasureOverride, fornecendo lógica que mede e organiza os elementos filho. Este exemplo deriva de Panel. Ao começar a partir de Panel, os métodos ArrangeOverride e MeasureOverride não possuem um comportamento inicial definido. O seu código está a fornecer a porta de entrada através da qual os elementos filho se tornam conhecidos pelo sistema de layout XAML e são renderizados na interface do utilizador. Portanto, é muito importante que seu código contabilize todos os elementos filho e siga os padrões esperados pelo sistema de layout.

Seu cenário de layout

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

Um cenário de layout é expresso através de:

  • O que o painel fará quando tiver elementos filho
  • Quando o painel tem restrições no 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 renderizado da interface do usuário dos filhos

Com isso em mente, o BoxPanel mostrado aqui é para um cenário específico. No interesse de manter o código em primeiro lugar 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, pule para "O cenário para BoxPanel" e, em seguida, volte para o código.

Comece por partir do Painel

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

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

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 Panel, torne-o a classe base do BoxPanel. Além disso, tornar BoxPanel público:

public class BoxPanel : Panel
{
}

No nível da classe, defina alguns int e valores de duplo 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, cellheightmaxcellheight, aspectratio.

Depois de fazer isso, o arquivo de código completo terá esta aparência (removendo comentários sobre o uso, 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;
}

A partir de agora, vamos mostrar uma definição de membro de cada vez, seja uma substituição de método ou algo de 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 de é o loop através de cada elemento em Panel.Children. Utilize sempre o método Measure em cada um desses elementos. Measure tem um parâmetro do tipo Size. O que você está passando aqui é o tamanho que seu painel está se comprometendo a ter disponível para esse elemento filho específico. Assim, antes de poderes realizar o loop e começar a chamar Measure, precisas de saber quanto espaço cada célula pode dedicar. A partir do próprio método MeasureOverride, tem o valor de availableSize. O tamanho é este que o pai do painel usou quando chamou Measure, o que funcionou como o gatilho para chamar inicialmente este MeasureOverride. Portanto, uma lógica típica é conceber um esquema pelo qual cada elemento filho divide o espaço geral do painel availableSize. 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 um número de caixas que é amplamente controlado 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 ela é descartada e o painel se torna um retângulo em vez de quadrado em termos de sua relação linha: coluna. Para obter mais informações sobre como essa lógica foi alcançada, pule para "O cenário para BoxPanel".

Então, o que faz a medida aprovada? Ele define um valor para a propriedade somente leitura DesiredSize em cada elemento onde Measure foi chamado. Ter um valor de DesiredSize é possivelmente importante quando chegar à fase de organização, porque o DesiredSize indica qual pode ou deve ser o tamanho ao organizar e na renderização final. Mesmo que você não use DesiredSize em sua própria lógica, o sistema ainda precisa dele.

É possível que este painel seja usado quando o componente height de availableSize estiver ilimitado. Se isso for verdade, o painel não tem uma altura conhecida para dividir. Neste caso, a lógica para o passe da medida informa cada criança que ainda não tem uma altura limitada. Faz isso ao passar um Tamanho para a chamada de Medida para crianças onde Tamanho.Altura é infinito. Isso é legal. Quando de Medida é chamado, a lógica é que o DesiredSize é definido como o mínimo destes: o que foi passado para Medida, ou o tamanho natural desse elemento a partir de fatores como de Altura explicitamente definidos ede Largura .

Observação

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

No entanto, o painel em si não pode retornar um Tamanho com um valor infinito de MeasureOverride; isso lança uma exceção durante o layout. Assim, parte da lógica é descobrir a altura máxima que qualquer criança solicita e usar essa altura como a altura da célula, caso isso não seja proveniente das próprias restrições de tamanho do painel. Aqui está a função auxiliar LimitUnboundedSize que foi referenciada no código anterior, que então pega essa altura máxima da célula e a usa para dar ao painel uma altura finita para retornar, além de garantir que cellheight seja um número finito antes que a passagem de organização 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 é o loop através de 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 em MeasureOverride; isso é típico. O tamanho das crianças já é conhecido pela própria lógica de MeasureOverride do painel ou pelo valor de DesiredSize de cada criança definido durante a aprovação da medida. No entanto, ainda precisamos decidir o local dentro do painel onde cada criança aparecerá. Em um painel típico, cada filho deve ser apresentado 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 organiza-se pelo conceito de linhas e colunas. O número de linhas e colunas já estava calculado (era necessário para a medição). Portanto, agora a forma das linhas e colunas mais os tamanhos conhecidos de cada célula contribuem para a lógica de definição de uma posição de renderização (a anchorPoint) para cada elemento que este painel contém. Esse Ponto, juntamente com o Tamanho já conhecido a partir da medida, são usados como os dois componentes que formam um Retângulo. Rect é o tipo de entrada para Arranjar.

Por vezes, os painéis precisam de recortar o seu conteúdo. Se o fizerem, o tamanho cortado é o tamanho que está presente em DesiredSize, porque a lógica Measure define-o como o mínimo do que foi passado para Measureou outros fatores naturais de tamanho. Portanto, normalmente não é necessário verificar especificamente se há clipping durante Organizar; o recorte só acontece com base na passagem do DesiredSize para cada chamada Organizar.

Você nem sempre precisa de uma contagem ao passar pelo loop se 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 um Canvas são conhecidas lendo Canvas.Left e Canvas.Top valores das crianças como parte da lógica de arranjo. A lógica BoxPanel precisa de uma contagem para comparar com colcount, de forma a determinar quando começar uma nova linha e ajustar o valor de y.

É típico que a entrada finalSize e o Size retornados de uma implementação de ArrangeOverride sejam os mesmos. Para saber mais sobre o motivo, veja a seção "ArrangeOverride" da visão geral dos painéis personalizados XAML.

Um refinamento: controlar a contagem de linhas versus colunas

Você pode compilar e usar este painel exatamente como ele é agora. No entanto, adicionaremos mais um refinamento. No código mostrado, a lógica coloca a linha ou coluna extra no lado que é mais longo na proporção. Mas para um maior controlo sobre as formas das células, pode ser desejável escolher um conjunto de células 4x3 em vez de 3x4, mesmo que a própria proporção do 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 de 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 realidade, tudo o que faz é mudar como rowcount e colcount são derivadas de maxrc e da proporção verdadeira, e há diferenças de tamanho correspondentes para cada célula devido a isso. Quando Orientation é Vertical (padrão), ele inverte o valor da relação de aspeto verdadeira antes de a utilizar nas contagens de linhas e colunas no nosso layout de retângulo com orientação "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 particular para BoxPanel é que se trata de um painel onde um dos principais determinantes para dividir o espaço é saber o número de itens filho e distribuir o espaço disponível de forma conhecida para o painel. Os painéis são formas inatamente retangulares. Muitos painéis operam dividindo esse espaço retangular em retângulos adicionais; é isso que Grid faz para as suas células. No caso de Grid, o tamanho das células é definido pelos valores de ColumnDefinition e RowDefinition, e os elementos declaram a célula exata em que entram com as propriedades anexadas de Grid.Row e Grid.Column. Obter um bom layout a partir de uma Grade geralmente requer saber o número de elementos filhos de antemão, de modo a que haja células suficientes e cada elemento filho defina as suas propriedades associadas de modo a encaixar na sua própria célula.

Mas e se o número de filhos for dinâmico? Isso é certamente possível; o código do seu 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 valer a pena atualizar sua interface do usuário. Se você estiver usando a vinculaçã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, portanto, essa geralmente é a técnica preferida (consulte Vinculação de dados em profundidade).

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

Um cenário avançado para estender mais (não mostrado aqui) poderia acomodar crianças dinâmicas e usar o 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 vários tamanhos e proporções podem caber em um retângulo de contenção, tanto para estética quanto para tamanho menor. BoxPanel não faz isso; é usar uma técnica mais simples para dividir o espaço. A técnica do BoxPanelé determinar o menor número quadrado perfeito que é maior do que a contagem de crianças. Por exemplo, 9 itens caberiam em um quadrado 3x3. 10 itens requerem um quadrado 4x4. No entanto, muitas vezes você pode ajustar itens enquanto ainda remove uma linha ou coluna do quadrado inicial, para economizar espaço. No exemplo count=10, que se encaixa em um retângulo 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 fortemente orientada. A técnica dos mínimos quadrados é uma maneira de ajustar a lógica de dimensionamento para funcionar bem com formas de layout típicas e não favorecer tamanhos em que as formas das células adquiram proporções estranhas.

Referência

Conceitos