Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
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 o comportamento para o seu painel ao substituir os métodos ArrangeOverride e MeasureOverride fornecendo lógica que mede e organiza os elementos filhos. 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 fornece o gateway pelo qual os elementos filhos passam a ser reconhecidos 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 possuir elementos filhos
- Quando o painel tem restrições em seu próprio espaço
- Como a lógica do painel determina todas as medidas, colocações, posições e dimensionamentos que acabam resultando 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 Gerenciador de Soluções 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. Aí vai uma lista sugerida de instruções using que podem resolver tipos de que você precisará para o código do 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 Microsoft.UI.Xaml; // DependencyObject, UIElement, and FrameworkElement
using Microsoft.UI.Xaml.Controls; // Panel
using Microsoft.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 Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
public class BoxPanel : Panel
{
int maxrc, rowcount, colcount;
double cellwidth, cellheight, maxcellheight, aspectratio;
}
A partir de agora, estaremos mostrando uma definição de membro por vez, seja ele uma substituição de método ou algo que forneç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 está sendo passado aqui é o tamanho que o seu painel está se comprometendo a ter para aquele elemento filho em particular. 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 que pai do painel usou quando chamou o Measure, que era o gatilho para este MeasureOverride ser chamado. 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 o Measure de cada elemento filho.
É simples como o BoxPanel divide o tamanho: ele divide o espaço em um número de caixas controlado, em grande parte, 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 o cálculo da medida faz? Ele define um valor para a propriedade apenas de leitura DesiredSize em cada elemento onde o Measure foi chamado. Ter um valor DesiredSize é provavelmente importante quando você organizar o cálculo, porque o DesiredSize comunica qual tamanho deve ser ou pode ser durante a organização e a 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 for verdade, o painel não precisa ter uma altura conhecida para ser dividida. Nesse caso, a lógica para o cálculo da medida informa cada filho que ele ainda não tem uma altura ilimitada. Ele o faz passando Size para a chamada Measure para os filhos cujo 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 nos filhos, indicando que não há nenhuma restrição em filhos na dimensão de orientação. StackPanel costuma dimensionar a si próprio para acomodar todos os filhos em uma pilha que cresce naquela 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. Então, parte da lógica é descobrir a altura máxima que qualquer filho solicitar e usar tal altura como a altura da célula no caso de isso não vir nas próprias restrições de tamanho do painel. Aqui está a função auxiliar LimitUnboundedSize que foi referenciada no código anterior, que pega a altura máxima da célula e a usa para dar ao painel uma altura finita em retorno, além de assegurar que cellheight seja um número finito antes de o cálculo de organização ser iniciado:
// 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 * rowcount;
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 dos filhos já é conhecido pela própria lógica MeasureOverride do painel ou pelo valor DesiredSize de cada filho definido durante o cálculo da medida. 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, painéis precisam recortar seu conteúdo. Se o fizeram, o tamanho recortado é o tamanho presente em DesiredSize, porque a lógica Measure o define como o mínimo do que foi passado para Measure ou outros fatores de tamanho natural. Então não é comum precisar verificar o recorte durante Arrange; o recorte acontece com base no DesiredSize ser passado em 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 em um Canvas é conhecida pela leitura dos valores Canvas.Left e Canvas.Top de filhos como parte da lógica de organização. Acontece que a lógica do BoxPanel precisa de uma contagem para comparar o colcount para que se saiba quando começar uma nova linha e deslocar 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 a contagem de linhas x colunas
Você pode compilar e usar este painel da mesma forma que está agora. No entanto, adicionaremos mais um refinamento. No código que acabamos de mostrar, a lógica coloca a linha extra ou a coluna do lado que é mais longo na taxa de 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. O que isso de fato está fazendo é mudando como rowcount e colcount derivam de maxrc e a taxa de proporção verdadeira, e há diferenças de tamanho correspondentes para cada célula por causa disso. Quando Orientation é 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 específico para BoxPanel é que ele é um painel em que um dos principais determinantes da forma de dividir o espaço é conhecer o número de itens filhos e dividir o espaço conhecido disponível do painel. Os painéis têm naturalmente formas 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 layout bom de um Grid costuma exigir o conhecimento do número de elementos filhos antes, para que haja células suficientes e cada elemento filho define suas propriedades anexadas para caber em 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 vinculação de dados para dar suporte a coleções/objetos de negócios, o recebimento dessas atualizações e a atualização da interface do usuário serão feitos automaticamente, e muitas vezes essa é a técnica preferida de dados (veja Vinculação de dados em detalhes).
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. Um mudança no número de itens filhos não é problema para o BoxPanel porque ele está usando a contagem de filhos nos cálculos, e ajusta tanto os elementos filhos novos como os existentes em um novo layout, portanto todos eles se encaixem.
Uma situação antecipada para estender mais o BoxPanel (não mostrado aqui) poderia ser tanto acomodar filhos dinâmicos quanto usar o DesiredSize de um filho 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. A técnica do BoxPanel é determinar o menor número quadrado que é maior que a contagem de filhos. Por exemplo, 9 itens caberiam em um quadrado 3x3. 10 itens exigem 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 onde as formas de células obtêm proporções estranhas.
Tópicos relacionados
Referência
Conceitos
Windows developer