Partilhar via


"Tutorial: Cria um painel personalizado

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

APIs importantes: Painel, 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 quiser mais informações sobre estes conceitos de layout e como podem aplicar-se ao seu cenário de layout em particular, veja a 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 da sua aplicação é renderizada. Você pode definir painéis personalizados para layout XAML derivando uma classe personalizada da classe Panel . Você fornece comportamento ao seu painel ao sobrescrever os métodos ArrangeOverride e MeasureOverride, fornecendo lógica que mede e organiza os elementos filhos. Este exemplo deriva do Painel. Quando começas a partir do Painel, os métodos ArrangeOverride e MeasureOverride não têm um comportamento inicial. 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

Tendo isso em mente, o BoxPanel que aparece aqui é para um cenário particular. 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 quiseres saber mais sobre o cenário primeiro, salta para "O cenário para BoxPanel" e depois volta ao código.

Comece por derivar do Painel

Comece por derivar uma classe personalizada a partir de Panel. Provavelmente, a forma mais fácil de fazer isto é definir um ficheiro de código separado para esta classe, usando as opções do menu de contexto Add | New Item | Class para um projeto a partir do Solution Explorer no Microsoft Visual Studio. Nomeie a classe (e o ficheiro BoxPanel).

O ficheiro modelo de uma classe não começa com muitas declarações 'using' porque não é especificamente para aplicações Windows. Portanto, em primeiro lugar, adiciona instruções using. O ficheiro modelo também começa com algumas instruções using que provavelmente não são necessárias e que podem ser eliminadas. Aqui está uma lista sugerida de instruções de uso que podem resolver tipos que precisará para o código típico de 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 podes resolver Painel, torna-o a classe base de BoxPanel. Além disso, torna BoxPanel público:

public class BoxPanel : Panel
{
}

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

Depois de fazeres isto, o ficheiro de código completo fica assim (removendo comentários sobre o uso, agora que sabes porque 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 MeasureOverride é a loop através de cada elemento em Panel.Children. Chame sempre o método Measure em cada um destes 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. Por isso, antes de poderes fazer 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 , obtém o valor AvailableSize . Esse é o tamanho que o elemento pai do painel usou quando chamou Measure, que foi o gatilho para este MeasureOverride ser inicialmente invocado. Assim, uma lógica típica é conceber um esquema em que cada elemento filho divide o espaço do tamanho total disponível do painel. Depois, passa o tamanho de cada divisão para a Medida de cada elemento filho.

A forma como BoxPanel divide o tamanho é bastante simples: divide o seu espaço em várias caixas que são largamente 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 ela é descartada e o painel se torna um retângulo em vez de quadrado em termos de sua relação linha: coluna. Para mais informações sobre como esta lógica foi chegada, avance para "O cenário para o BoxPanel".

Então, o que faz a medida aprovada? Define um valor para a propriedade DesiredSize de somente leitura em cada elemento onde Measure foi chamado. Ter um valor DesiredSize é possivelmente importante quando se chega à fase de arranjo, porque o DesiredSize comunica qual deve ou pode ser o tamanho ao organizar e na fase de renderização final. Mesmo que não uses o DesiredSize na tua própria lógica, o sistema ainda precisa dele.

É possível que este painel seja usado quando o componente de altura do availableSize é 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-no passando um chamado Tamanho para a Medida para filhos onde Tamanho.Altura é infinito. Isso é legal. Quando Medida é chamada, a lógica é que o Tamanho Desejado é definido como o mínimo destes: o que foi passado para Medida, ou o tamanho natural desse elemento a partir de fatores como Altura e Largura explicitamente definidas.

Observação

A lógica interna do StackPanel também apresenta este comportamento: o StackPanel passa um valor de dimensão infinita para Measure nos filhos, indicando que não existe restrição nos filhos na dimensão de orientação. O StackPanel normalmente dimensiona-se dinamicamente, para acomodar todas as crianças numa pilha que cresce nessa dimensão.

No entanto, o painel em si não pode devolver um Tamanho com valor infinito do MeasureOverride; Isso cria 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 LimitUnboundedSize auxiliar que foi referenciada no código anterior, que depois utiliza essa altura máxima da célula para atribuir uma altura finita ao painel antes de devolver, assim como assegurar que cellheight é um número finito antes de iniciar a passagem de organização:

// 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. Chame sempre o método Arrange em cada um destes elementos.

Note como não há tantos cálculos como no MeasureOverride; Isso é típico. O tamanho dos filhos já é conhecido pela própria lógica MeasureOverride do painel, ou pelo valor DesiredSize de cada filho durante o processo de medição. 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). Assim, 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 (a anchorPoint) para cada elemento que este painel contém. Esse Ponto, juntamente com o Tamanho já conhecido pela medida, são usados como os dois componentes que constroem um Reto. Rect é o tipo de entrada para Arrange.

Por vezes, os painéis precisam de recortar o seu conteúdo. Se o fizerem, o tamanho recortado é o tamanho presente no DesiredSize, porque a lógica da Medida define como o mínimo do que foi passado para a Medida, ou outros fatores naturais de tamanho. Por isso, normalmente não precisas de verificar especificamente o clipping durante Arrange; o clipping acontece apenas com base na passagem do DesiredSize para cada chamada Arrange.

Nem sempre precisas de uma contagem ao passar pelo loop se toda a informação necessária para definir a posição de renderização for conhecida por outros meios. Por exemplo, na lógica de layout Canvas, a posição na coleção Children não importa. Toda a informação necessária para posicionar cada elemento numa Canvas é obtida através da leitura dos valores de Canvas.Left e Canvas.Top dos filhos como parte da lógica de arranjo. A lógica BoxPanel precisa, por acaso, de uma contagem para comparar com a colcount para saber quando começar uma nova linha e ajustar o valor de y.

É típico que a entrada finalSize e a Size que devolves de uma implementação ArrangeOverride sejam as mesmas. Para mais informações sobre o motivo, veja a secção "ArrangeOverride" na 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 relação de aspeto do painel seja "retrato". Por isso, vamos adicionar uma propriedade de dependência opcional que o utilizador 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 a utilização Orientation impacta a lógica de medida em MeasureOverride. Na verdade, tudo o que faz é alterar como rowcount e colcount são derivados de maxrc e da razão de aspeto verdadeira, e existem diferenças de tamanho correspondentes para cada célula por causa disso. Quando Orientation está Vertical (por defeito), inverte o valor da verdadeira proporção de aspeto antes de a utilizar na contagem de linhas e colunas no layout de retângulo em 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 é um painel onde um dos principais determinantes de como distribuir o espaço é saber o número de itens filhos e distribuir o espaço disponível conhecido para o painel. Os painéis são formas inatamente retangulares. Muitos painéis dividem esse espaço retangular em outros retângulos; é isso que Grid faz para suas células. No caso de Grid, o tamanho das células é definido pelos valores ColumnDefinition e RowDefinition , e os elementos declaram exatamente a célula para onde entram com as propriedades Grid.Row e Grid.Column anexadas. Obter um bom layout de uma Grelha normalmente requer saber o número de elementos filhos antecipadamente, para que haja células suficientes e cada elemento filho defina as suas propriedades anexas para caber 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 estiver a usar data binding para backing collections/business objects, receber essas atualizações e atualizar a interface é feito automaticamente, por isso essa é frequentemente a técnica preferida (ver data binding em detalhe).

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. Uma alteração no número de itens filhos não representa um problema para BoxPanel porque usa a contagem dos itens filhos nos cálculos e ajusta tanto os elementos filhos existentes como os novos num novo layout para que todos se ajustem.

Um cenário avançado para estender BoxPanel mais (não mostrado aqui) poderia tanto acomodar crianças dinâmicas como usar o Tamanho Desejado de uma criança como um fator mais forte para o dimensionamento das 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; Está a usar uma técnica mais simples para dividir o espaço. BoxPanel's técnica é determinar o menor quadrado perfeito que é maior do que a contagem de filhos. 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