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 (Extensible Application Markup Language) está em execução e a interface do usuário do aplicativo é renderizada.

APIs importantes: Panel, ArrangeOverride, MeasureOverride

Você pode definir painéis personalizados para layout XAML derivando uma classe personalizada da classe painel . Você fornece o comportamento para o seu painel, substituindo MeasureOverride e ArrangeOverride, fornecendo uma lógica que mede e organiza os elementos filhos.

A classe base Panel

Para definir uma classe de painel personalizada, você pode derivar da classe Panel diretamente ou derivar de uma das classes de painel práticas que não são lacradas, como Grid ou StackPanel. É mais fácil derivar de Panel, porque pode ser difícil contornar a lógica de layout existente de um painel que já tem o comportamento de layout. Além disso, um painel com comportamento pode ter propriedades existentes que não são relevantes para os recursos de layout do painel.

Do Panel, seu painel personalizado herda as seguintes APIs:

  • A propriedade Children.
  • As propriedades Background, ChildrenTransitions e IsItemsHost, e os identificadores de propriedade de dependência. Nenhuma dessas propriedades é virtual, portanto, você normalmente não as sobrepõe ou substitui. Normalmente, você não precisa dessas propriedades para cenários de painel personalizados, nem mesmo para valores de leitura.
  • Os métodos de substituição de layout MeasureOverride e ArrangeOverride. Elas foram originalmente definidas pelo FrameworkElement. A classe de base Panel não os substitui, mas os painéis práticos como Grid tem implementações de substituição que são implementadas como código nativo e são executadas pelo sistema. Implementar novas ou complementares versões para ArrangeOverride e MeasureOverride constitui a maior parte do esforço necessário para definir um painel personalizado.
  • Todas as outras APIs do FrameworkElement, UIElement e DependencyObject, como Altura, Visibilidade e assim por diante. Às vezes, você faz referência a valores dessas propriedades nas substituições de layout, mas eles não são virtuais, de modo que, normalmente, não são sobrepostos ou substituídos.

Este foco aqui é descrever os conceitos de layout XAML, para que você possa considerar todas as possibilidades de como um painel personalizado pode e deve se comportar no layout. Se você preferir entrar e ver um exemplo de implementação de painel personalizado, consulte BoxPanel, um exemplo de painel personalizado.

A propriedade Children

A propriedade Children é relevante para um painel personalizado, porque todas as classes derivadas do Panel usam a propriedade Children como o lugar para armazenar seus elementos filho contidos em uma coleção. Children é designada como a propriedade de conteúdo XAML para a classe Panel, e todas as classes derivadas de Panel podem herdar o comportamento da propriedade de conteúdo XAML. Se uma propriedade for designada como a propriedade de conteúdo XAML, isso significa que a marcação XAML poderá omitir um elemento de propriedade ao especificar essa propriedade na marcação e os valores serão definidos como filhos de marcação imediatos (o "conteúdo"). Por exemplo, se você derivar uma classe chamada CustomPanel do Painel que não define nenhum novo comportamento, você ainda poderá usar esta marcação:

<local:CustomPanel>
  <Button Name="button1"/>
  <Button Name="button2"/>
</local:CustomPanel>

Quando um analisador XAML lê esta marcação, Children passa a ser conhecido por ser a propriedade de conteúdo XAML para todos os tipos derivados de Panel, de modo que o analisador adicionará os dois elementos Button ao valor de UIElementCollection da propriedade Children. A propriedade de conteúdo XAML facilita um relacionamento pai-filho simplificado na marcação XAML para uma definição de interface do usuário. Para obter mais informações sobre propriedades de conteúdo XAML e como as propriedades de coleção são preenchidas quando XAML é analisado, consulte o guia de sintaxe XAML.

O tipo de conjunto que mantém o valor da propriedade Children é a classe UIElementCollection. UIElementCollection é um conjunto fortemente tipado que usa UIElement como seu tipo de item forçado. UIElement é um tipo de base herdado por centenas de tipos de elementos práticos de interface do usuário, por isso a aplicação de tipo aqui é deliberadamente solta. Mas isso reforça que não seria possível ter um Brush como um filho direto de um Panel, e isso geralmente significa que apenas os elementos que se esperam que sejam visíveis na interface do usuário e participem no layout serão identificados como elementos filho em um Panel.

Normalmente, um painel personalizado aceita qualquer elemento filho UIElement por uma definição XAML apenas usando as características da propriedade Children no estado em que se encontra. Como um cenário avançado, seria possível suportar ainda mais a verificação de tipo de elementos filho, ao iterar sobre o conjunto nas substituições de layout.

Além de loop através do conjunto Children nas substituições, a lógica do painel também pode ser influenciada por Children.Count. Você pode ter uma lógica que está alocando espaço pelo menos parcialmente com base no número de itens, em vez dos tamanhos desejados e das outras características de itens individuais.

Substituindo os métodos de layout

O modelo básico para os métodos de substituição de layout (MeasureOverride e ArrangeOverride) é que eles devem percorrer através de todos os filhos e chamar o método de layout específico de cada elemento filho. O primeiro ciclo de layout é iniciado quando o sistema de layout XAML define o visual para a janela raiz. Como cada um dos pais invoca layout nos filhos, isso propaga uma chamada aos métodos de layout a todos os elementos de interface do usuário possível que são supostamente parte de um layout. No layout XAML, há dois estágios: medir e organizar.

Você não tem nenhum comportamento de método de layout interno para MeasureOverride e ArrangeOverride da classe de base Panel. Os itens em Crianças não serão renderizados automaticamente como parte da árvore visual XAML. Você decide se quer tornar os itens conhecidos para o processo de layout, invocando os métodos de layout em cada um dos itens encontrados no Children através de um cálculo de layout dentro das implementações MeasureOverride e ArrangeOverride.

Não há nenhuma razão para chamar implementações de base nas substituições de layout, a menos que você tenha sua própria herança. Os métodos nativos para o comportamento de layout (se existirem) são executados de qualquer forma, e não chamar a implementação base das substituições não impedirá que o comportamento natural aconteça.

Durante o cálculo de medida, a lógica de layout consulta cada elemento filho para o tamanho desejado, chamando o método Measure no elemento filho. Chamar o método Measure estabelece o valor para a propriedade DesiredSize. O valor de retorno MeasureOverride é o tamanho desejado para o próprio painel.

Durante o cálculo de organização, as posições e os tamanhos de elementos filhos são determinados no espaço x-y e a composição de layout é preparada para renderização. O código deve chamar o Arrange em cada elemento filho em Children, de modo que o sistema de layout detecte que o elemento pertence ao layout. A chamada Arrange é um precursor da composição e renderização, que informa o sistema de layout para onde esse elemento vai e quando a composição é enviada para renderização.

Muitas propriedades e valores contribuem para como a lógica de layout funcionará em runtime. Uma maneira de pensar sobre o processo de layout é que os elementos sem filhos (geralmente o elemento mais profundamente aninhado na interface do usuário) são os únicos que podem finalizar as medições primeiro. Eles não têm nenhuma dependência de elementos filho que influenciem no tamanho desejado. Eles podem ter seus próprios tamanhos desejados, e esses tamanhos são sugestões até que o layout realmente seja feito. Em seguida, o processo de medição continua subindo a árvore visual até que o elemento raiz tenha suas medidas e todas as medições possam ser concluídas.

O layout candidato deve caber dentro da janela do aplicativo atual. Caso contrário, serão cortadas partes da interface do usuário. Os painéis são frequentemente o lugar onde a lógica de corte é determinada. A lógica do painel pode determinar qual o tamanho disponível de dentro da implementação MeasureOverride e pode ter que aplicar as restrições de tamanho nos filhos e dividir o espaço entre os filhos, para que tudo se encaixe da melhor forma possível. O resultado do layout é idealmente algo que usa várias propriedades de todas as partes do layout, mas ainda se encaixa dentro da janela do aplicativo. Isso requer uma boa implementação para a lógica de layout dos painéis e também um design de interface do usuário criterioso por parte de qualquer código de aplicativo que crie uma interface do usuário usando esse painel. Nenhum design do painel ficará bem se o design geral da interface do usuário incluir mais elementos filhos do que possivelmente caberiam no aplicativo.

Uma grande parte do que faz o sistema de layout funcionar é que qualquer elemento baseado em FrameworkElement já tem alguns de seus próprios comportamentos inerentes ao agir como um filho em um contêiner. Por exemplo, há várias APIs do FrameworkElement que informam o comportamento do layout ou são necessárias para que o layout funcione. Elas incluem:

MeasureOverride

O método MeasureOverride tem um valor de retorno que é usado pelo sistema de layout como o ponto de partida DesiredSize para o próprio painel, quando o método Measure é chamado no painel pelo pai no layout. As escolhas lógicas dentro do método são tão importantes quanto o que ele retorna, e a lógica geralmente influencia qual valor é retornado.

Todas as implementações MeasureOverride devem fazer um loop através de Children, e chamar o método Measure em cada elemento filho. Chamar o método Measure estabelece o valor para a propriedade DesiredSize. Isso pode informar quanto espaço o próprio painel precisa, bem como a forma que o espaço é dividido entre os elementos ou dimensionados para um elemento filho específico.

Consulte a seguir a estrutura básica de um método MeasureOverride:

protected override Size MeasureOverride(Size availableSize)
{
    Size returnSize; //TODO might return availableSize, might do something else
     
    //loop through each Child, call Measure on each
    foreach (UIElement child in Children)
    {
        child.Measure(new Size()); // TODO determine how much space the panel allots for this child, that's what you pass to Measure
        Size childDesiredSize = child.DesiredSize; //TODO determine how the returned Size is influenced by each child's DesiredSize
        //TODO, logic if passed-in Size and net DesiredSize are different, does that matter?
    }
    return returnSize;
}

Os elementos geralmente têm um tamanho natural quando estão prontos para layout. Após o cálculo de medida, DesiredSize pode indicar o tamanho natural, se availableSize que você calculou para Measure for menor. Se o tamanho natural for maior que availableSize que você calculou para Measure, DesiredSize será restrito a availableSize. É assim que a implementação interna de Measure se comporta, e as substituições de layout devem levar em conta esse comportamento.

Alguns elementos não têm tamanho natural, porque eles têm valores Auto para Height e Width. Depois, esses elementos usarão o availableSize completo, porque é isso que um valor Auto representa: dimensionar o elemento para o tamanho máximo disponível, que o pai de layout de imediato comunica chamando Measure com availableSize. Na prática, há sempre alguma medida para a qual uma interface do usuário é dimensionada (mesmo que essa seja a janela de nível superior). Eventualmente, a passagem da medida resolve todos os valores de Auto para restrições pai e todos os elementos de valor Auto obtêm medidas reais (que você pode obter verificando ActualWidth e ActualHeight, após a conclusão do processo de layout).

É permitido calcular um tamanho para Measure que tenha ao menos uma dimensão infinita, para indicar que o painel pode tentar se dimensionar para caber todo o seu conteúdo. Cada elemento filho que está sendo medido define seu valor DesiredSize usando seu tamanho natural. Em seguida, durante o cálculo de organização, o painel normalmente organiza usando esse tamanho.

Elementos de texto como TextBlock têm uma LarguraFactual e uma AlturaFactual calculadas com base em sua cadeia de texto e propriedades de texto, mesmo que nenhum valor de Altura ou de Largura seja definido, e essas dimensões devem ser respeitadas pela lógica do seu painel. Cortar texto é uma experiência de interface do usuário especificamente ruim.

Mesmo que a implementação não utilize as medidas de tamanho desejadas, é melhor chamar o método Measure em cada elemento filho, porque há comportamentos internos e nativos que são acionados pela chamada de Measure. Para que um elemento participe do layout, cada elemento filho deve ter Measure chamada nele durante o cálculo de medição e o método Arrange chamado nele durante o cálculo de organização. Chamar esses métodos configura indicadores internos no objeto e preenche valores (como a propriedade DesiredSize) necessários para a lógica de layout do sistema ao criar a árvore visual e renderizar a interface do usuário.

O valor de retorno MeasureOverride baseia-se na lógica do painel interpretando o DesiredSize ou outras considerações de tamanho para cada um dos elementos filho em Children quando Measure é chamado neles. O que fazer com valores DesiredSize dos filhos e o modo como o valor de retorno MeasureOverride deve usá-los depende da interpretação da própria lógica. Normalmente, você não adiciona os valores sem modificação, porque a entrada de MeasureOverride muitas vezes é um tamanho fixo disponível que está sendo sugerido pelo pai do painel. Se esse tamanho for excedido, o próprio painel poderá ser cortado. Normalmente, você compararia o tamanho total de crianças com o tamanho disponível do painel e faria ajustes, se necessário.

Dicas e diretrizes

  • Idealmente, um painel personalizado deve ser adequado para ser o primeiro verdadeiro visual em uma composição de interface do usuário, talvez em um nível imediatamente abaixo de Page, UserControl ou outro elemento que seja a raiz da página XAML. Em implementações MeasureOverride, a entrada Size não é geralmente retornada sem examinar os valores. Se o retorno Size tiver um valor Infinity nele, isso poderá acionar exceções em tempo de execução de lógica do layout. Um valor Infinito pode vir da janela principal do aplicativo, que é rolável e, portanto, não tem uma altura máxima. Outro conteúdo rolável pode ter o mesmo comportamento.
  • Outro erro comum nas implementações de MeasureOverride é retornar um novo Tamanho padrão (os valores de altura e largura são 0). Você pode começar com esse valor, que pode até ser o valor correto se o painel determinar que nenhum dos filhos deve ser renderizado. Mas um Size padrão fará com que seu painel não seja dimensionado corretamente pelo host. Ele não solicita espaço na interface do usuário e, portanto, não obtém espaço e não é renderizado. Se não fosse o caso, o código do painel poderia funcionar bem, mas o painel ou seu conteúdo ainda não será exibido se ele estiver sendo composto com altura zero e largura zero.
  • Dentro das substituições, evite a tentação de lançar elementos filho para FrameworkElement e use propriedades que são calculadas como um resultado de layout, especialmente ActualWidth e ActualHeight. Para a maioria dos cenários comuns, é possível basear a lógica no valor do DesiredSize do filho e você não precisará de nenhuma das propriedades Height ou Width relacionadas de um elemento filho. Para casos especializados, em que você conhece o tipo de elemento e tem informações adicionais, por exemplo, o tamanho natural de um arquivo de imagem, você pode usar as informações especializadas do elemento porque não é um valor que está sendo alterado ativamente por sistemas de layout. Incluir propriedades calculadas de layout como parte da lógica de layout aumenta substancialmente o risco de definir um loop de layout não intencional. Esses loops causam uma condição em que um layout válido não pode ser criado e o sistema pode gerar um LayoutCycleException se o loop não for recuperável.
  • Os painéis normalmente dividem o espaço disponível entre vários elementos filhos, embora a divisão exata do espaço varie. Por exemplo, Grid implementa a lógica de layout que usa seus valores RowDefinition e ColumnDefinition para dividir o espaço nas células Grid, suportando os valores de dimensionamento de estrela e pixel. Se eles forem valores de pixel, o tamanho disponível para cada filho já é conhecido, então isso é o que será passado como tamanho de entrada para um estilo de grade Measure.
  • Os painéis podem introduzir um espaço reservado para preenchimento entre itens. Se você fizer isso, assegure-se de expor as medições como uma propriedade que é diferente deMargin ou de qualquer propriedade Padding.
  • Os elementos podem ter valores para suas propriedades ActualWidth e ActualHeight com base em uma passagem de layout anterior. Se os valores forem alterados, o código de interface do usuário do aplicativo poderá colocar manipuladores para LayoutUpdated em elementos se houver lógica especial a ser executada, mas a lógica de painel normalmente não precisa verificar se há alterações com a manipulação de eventos. O sistema de layout já está fazendo as determinações ao executar novamente o layout porque uma propriedade relevante de layout mudou o valor foi alterado e um MeasureOverride ou ArrangeOverride de painel é chamado automaticamente nas circunstâncias adequadas.

ArrangeOverride

O método ArrangeOverride tem um valor de retorno Size que é usado pelo sistema de layout ao renderizar o próprio painel, quando o método Arrange é chamado no painel pelo pai no layout. É comum que a entrada finalSize e o ArrangeOverride que retornaram Size sejam os mesmos. Se não forem, isso significa que o painel está tentando fazer um tamanho diferente do tamanho que os outros participantes do layout afirmam estar disponível. O tamanho final foi baseado na execução prévia da etapa de medição do layout através do seu código de painel, por isso retornar um tamanho diferente não é típico: isso significa que você está deliberadamente ignorando a lógica de medição.

Não retornam um Size com um componente Infinity. Tentar utilizar tal Size gera uma exceção de layout interno.

Todas as implementações de ArrangeOverride devem fazer um loop por meio de Children, e chamar o método Arrange em cada elemento filho. Assim como a Medida, Arrange não tem um valor retornado. Ao contrário de Measure, nenhuma propriedade calculada é definida como resultado (no entanto, o elemento em questão normalmente dispara um evento LayoutUpdated ).

Aqui está um esqueleto muito básico de um método ArrangeOverride:

protected override Size ArrangeOverride(Size finalSize)
{
    //loop through each Child, call Arrange on each
    foreach (UIElement child in Children)
    {
        Point anchorPoint = new Point(); //TODO more logic for topleft corner placement in your panel
       // for this child, and based on finalSize or other internal state of your panel
        child.Arrange(new Rect(anchorPoint, child.DesiredSize)); //OR, set a different Size 
    }
    return finalSize; //OR, return a different Size, but that's rare
}

O cálculo de organização de layout pode acontecer sem ser precedido por um cálculo de medição. No entanto, isso só acontece quando o sistema de layout determinou que nenhuma propriedade foi alterada, o que teria afetado as medidas anteriores. Por exemplo, se um alinhamento for alterado, não será necessário medir novamente esse elemento específico porque seu DesiredSize não mudará quando sua opção de alinhamento for alterada. Por outro lado, se ActualHeight alterar qualquer elemento em um layout, será necessário um novo cálculo de medição. O sistema de layout detecta automaticamente mudanças de medição verdadeiras e invoca o cálculo de medição novamente, e em seguida, executa outro cálculo de organização.

A entrada para Arrange toma um valor Rect. A maneira mais comum de construir este Rect é usar o construtor que tem uma entrada Point e uma entrada Size. O Point é o ponto onde o canto superior esquerdo da caixa delimitadora para o elemento deve ser colocado. O Tamanho são as dimensões usadas para renderizar esse elemento específico. Você muitas vezes usa o DesiredSize para esse elemento como valor Size, porque estabelece o DesiredSize para todos os elementos envolvidos no layout foi a finalidade do cálculo de medição de layout. (O cálculo de medição determina todo o dimensionamento dos elementos de forma iterativa, de modo que o sistema de layout possa otimizar como os elementos são colocados uma vez que inicie o cálculo de organização.)

O que normalmente varia entre implementações de ArrangeOverride é a lógica pela qual o painel determina o componente Point de como ele organiza a cada filho. Um painel de posicionamento absoluto, como o Canvas, usa a informação de posicionamento explícito que ele toma de cada elemento através de valores de Canvas.Left e Canvas.Top. Um painel de divisão de espaço, como Grid , teria operações matemáticas que dividiam o espaço disponível em células e cada célula teria um valor x-y para onde seu conteúdo deve ser colocado e organizado. Um painel adaptável, como o StackPanel , pode estar se expandindo para ajustar o conteúdo em sua dimensão de orientação.

Há ainda influências de posicionamento adicionais nos elementos no layout, além do que você controla diretamente e calcula para Arrange. Essas implementações vêm da implementação nativa interna de Arrange, que é comum a todos os tipos derivados de FrameworkElement e são complementadas por alguns outros tipos, como elementos de texto. Por exemplo, os elementos podem ter margem e alinhamento, e alguns podem ter preenchimento. Essas propriedades geralmente interagem. Para obter mais informações, consulte Alinhamento, margem e preenchimento.

Painéis e controles

Evite colocar a funcionalidade em um painel personalizado que, em vez disso, deve ser criado como um controle personalizado. A função de um painel é apresentar qualquer conteúdo de elemento filho que exista dentro dele, como uma função de layout que acontece automaticamente. O painel pode adicionar decorações ao conteúdo (semelhante à forma como uma Borda adiciona a borda ao redor do elemento que ele apresenta) ou executar outros ajustes relacionados ao layout, como preenchimento. Mas isso é o mais longe que você deve ir ao estender a saída de árvore visual para além de relatórios e uso de informações dos filhos.

Se houver qualquer interação acessível ao usuário, você deverá escrever um controle personalizado, não um painel. Por exemplo, um painel não deve adicionar visores de rolagem ao conteúdo que apresenta, mesmo que o objetivo seja impedir o recorte, pois as barras de rolagem, os polegares e assim por diante são partes de controle interativas. (O conteúdo pode ter barras de rolagem, afinal, mas você deve deixar isso para a lógica do filho. Não force isso adicionando rolagem como uma operação de layout.) Você pode criar um controle e também escrever um painel personalizado que desempenha um papel importante na árvore visual desse controle, quando se trata de apresentar conteúdo nesse controle. Mas o controle e o painel devem ser objetos de código distintos.

Um dos motivos pelos quais a distinção entre controle e painel é importante é devido à Automação da Interface do Usuário da Microsoft e à acessibilidade. Os painéis fornecem um comportamento de layout visual, não um comportamento lógico. A aparência visual de um elemento da interface do usuário geralmente não é um aspecto importante em cenários de acessibilidade. Acessibilidade é sobre expor as partes de um aplicativo que são logicamente importantes para entender uma interface do usuário. Quando a interação é necessária, os controles devem expor as possibilidades de interação à infraestrutura de Automação da Interface do Usuário. Para obter mais informações, consulte Pares de automação personalizados.

Outra API de layout

Há outras APIs que fazem parte do sistema de layout, mas não são declaradas pelo Painel. Você pode usá-los em uma implementação de painel ou em um controle personalizado que usa painéis.

  • UpdateLayout, InvalidateMeasure e InvalidateArrange são métodos que iniciam uma passagem de layout. InvalidateArrange pode não disparar um cálculo de medição, mas os outros dois fazem isso. Nunca chame esses métodos de dentro de um método de substituição de layout, pois é quase certo que eles vão causar um loop de layout. O código de controle normalmente também não precisa chamá-los. A maioria dos aspectos do layout é ativada automaticamente pela detecção de alterações nas propriedades de layout definidas pela estrutura, como Largura e assim por diante.
  • LayoutUpdated é um evento que é acionado quando algum aspecto do layout do elemento é alterado. Isso não é específico para painéis; o evento é definido por FrameworkElement.
  • SizeChanged é um evento que é acionado somente depois que os cálculos do layout são finalizados e indicam que ActualHeight ou ActualWidth foram alterados como resultado. Esse é outro evento FrameworkElement. Há casos em que LayoutUpdated dispara, mas SizeChanged não dispara. Por exemplo, o conteúdo interno pode ser reorganizado, mas o tamanho do elemento não foi alterado.

Referência

Conceitos