Compartilhar via


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ê define o comportamento do painel substituindo o MeasureOverride e ArrangeOverride, fornecendo a lógica para medir e organizar os elementos filhos.

A classe base do Painel

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 do Painel , pois pode ser difícil contornar a lógica de layout existente de um painel que já tem 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.

No Painel , seu painel personalizado herda essas APIs:

  • A propriedade Crianças.
  • Oem segundo plano , propriedades 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 painel de base não substitui esses, mas painéis práticos como Grid têm substituições implementadas como código nativo, 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 aos valores dessas propriedades em suas substituições de layout, mas elas não são virtuais, portanto, você normalmente não as substitui.

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 de Panel usam a propriedade Children como o local para armazenar seus elementos filho contidos em uma coleção. Children é designado 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ê essa marcação, Children é conhecido por ser a propriedade de conteúdo XAML para todos os tipos derivados de Panel, portanto, o analisador adicionará os dois elementos Botão ao valor UIElementCollection da propriedade Children. A propriedade de conteúdo XAML facilita uma relação pai-filho simplificada na marcação XAML na definição de uma interface de 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 coleção que mantém o valor da propriedade Children é a classe UIElementCollection. UIElementCollection é uma coleção fortemente tipada que usa UIElement como seu tipo de item imposto. UIElement é um tipo base herdado por centenas de tipos práticos de elementos de interface do usuário, por isso a imposição de tipos aqui é intencionalmente flexível. Mas isso impõe que você não possa ter um Brush como um filho direto de um Painel , e isso geralmente significa que apenas os elementos que devem estar visíveis na UI e participar do layout serão encontrados como elementos filhos em um painel .

Normalmente, um painel personalizado aceita qualquer elemento UIElement filho por uma definição XAML, simplesmente usando as características da propriedade Children as-is. Como um cenário avançado, você pode dar suporte à verificação adicional de tipos dos elementos filho ao iterar sobre a coleção em suas sobreposições de layout.

Além de fazer loop pela coleção 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 iterar por 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 pai invoca o layout em seus filhos, isso propaga uma chamada para métodos de layout para cada elemento de interface do usuário possível que deveria fazer parte de um layout. No layout XAML, há dois estágios: medir e organizar.

Você não obtém nenhum comportamento de método de layout embutido para MeasureOverride e ArrangeOverride da classe base Panel. Os itens em Crianças não serão renderizados automaticamente como parte da árvore visual XAML. Cabe a você tornar os itens conhecidos pelo processo de layout invocando métodos de layout em cada um dos itens que você encontra em Filhos por meio de uma passagem de layout dentro de suas implementações MeasureOverride e ArrangeOverride.

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

Durante a passagem de medição, a lógica de layout consulta cada elemento filho pelo tamanho desejado, chamando o método Measure nesse elemento filho. Chamar o método Measure estabelece o valor para a propriedade DesiredSize. O valor retornado MeasureOverride é o tamanho desejado para o próprio painel.

Durante a etapa de organização, as posições e os tamanhos dos elementos filho são determinados no espaço bidimensional e a composição do layout é preparada para renderização. Seu código deve chamar Organizar em cada elemento filho em filhos para que o sistema de layout detecte que o elemento pertence ao layout. A chamada Organizar é um precursor para a composição e renderização; informa o sistema de layout sobre a localização do elemento 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 no processo de layout é que os elementos sem filhos (geralmente o elemento mais profundamente inserido na interface do usuário) são os que podem finalizar as medidas primeiro. Eles não têm nenhuma dependência de elementos filho que influenciam o tamanho desejado. Eles podem ter seus próprios tamanhos desejados, e essas são sugestões de tamanho até que o layout realmente aconteça. 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 do candidato deve caber na janela atual do aplicativo ou outras partes da interface do usuário serão cortadas. Os painéis geralmente são o local onde a lógica de recorte é determinada. A lógica do painel pode determinar qual tamanho está disponível de dentro da implementação do MeasureOverride e pode ter que empurrar as restrições de tamanho para as crianças e dividir espaço entre as crianças para que tudo se ajuste da melhor maneira 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 de painel ficará bom se o design geral da interface do usuário incluir mais elementos filho do que os que podem caber 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 retornado usado pelo sistema de layout como o DesiredSize inicial para o próprio painel, quando o método Measure é chamado no painel por seu 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 de MeasureOverride devem passar por filhose 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 de que forma esse espaço é dividido entre elementos ou dimensionado para um elemento filho específico.

Aqui está um esqueleto muito básico 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 a aprovação da medida, a DesiredSize poderá indicar esse tamanho natural, se o availableSize que você passou para Measure for menor. Se o tamanho natural for maior que availableSize você passou para Measure, o DesiredSize será restringido a availableSize. É assim que a implementação interna do Measurese comporta e suas substituições de layout devem levar esse comportamento em conta.

Alguns elementos não têm um tamanho natural porque têm valores de Automático para de Altura ede Largura . Esses elementos usam o tamanho total disponível , pois é isso que um valor Auto representa: ajustar o elemento para o tamanho máximo disponível, que o pai de layout imediato comunica ao chamar 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 passar um tamanho para Measure que tem pelo menos uma das dimensões infinitas, para indicar que o painel pode tentar ajustar-se às medidas do seu conteúdo. Cada elemento filho que está sendo medido define seu valor DesiredSize usando seu tamanho natural. Em seguida, durante a passagem de arranjo, o painel geralmente 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 ruim.

Mesmo que sua implementação não use as medidas de tamanho desejadas, é melhor chamar o método Measure em cada elemento filho, pois há comportamentos internos e nativos que são ativados ao ser chamado Measure. Para que um elemento participe do layout, cada elemento filho deve ter o método Measure chamado durante a passagem de medição e o método Arrange chamado durante a passagem 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 retornado MeasureOverride baseia-se na lógica do painel que interpreta o DesiredSize ou outras considerações de tamanho para cada um dos elementos filho em Children quando Measure é chamado sobre eles. Como lidar com os valores DesiredSize provenientes dos elementos filhos e como o valor de retorno de MeasureOverride deve utilizá-los depende da interpretação de sua própria lógica. Normalmente, você não soma os valores sem modificação, pois a entrada do MeasureOverride geralmente é um tamanho fixo disponível que está sendo sugerido pelo elemento pai do painel. Se você exceder esse tamanho, o painel em si poderá ser recortado. 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 elemento visual em uma composição de UI, talvez em um nível imediatamente em Page, UserControl ou outro elemento que seja a raiz da página XAML. Em implementações de MeasureOverride, não retorne rotineiramente a entrada Tamanho sem examinar os valores. Se o retorno Tamanho tiver um valor Infinity nele, isso pode gerar exceções na lógica de layout durante o tempo de execução. 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 e pode até ser o valor correto se o painel determinar que nenhum dentre os filhos deve ser renderizado. Porém, um tamanho padrão resulta no painel não ser 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. Todo o código do painel pode estar funcionando bem, mas você ainda não verá o seu painel nem seu conteúdo se ele estiver com altura e largura zero.
  • Dentro das substituições, evite a tentação de converter os elementos filho para FrameworkElement e usar propriedades calculadas como resultado do layout, particularmente ActualWidth e ActualHeight. Para a maioria dos cenários comuns, você pode basear a lógica no valor de DesiredSize da criança e não precisará de nenhuma das propriedades relacionadas à altura ou à largura 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 por 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 seu espaço disponível entre vários elementos filhos, embora a forma exata como o espaço é dividido possa variar. 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, dando suporte a valores de tamanho de estrela e pixel. Se forem valores de pixel, o tamanho disponível para cada filho já é conhecido, de modo que é o que é passado como tamanho de entrada para um measurede estilo de grade.
  • Os próprios painéis podem introduzir um espaço reservado para acolchoamento entre os itens. Se você fizer isso, garanta expor as medidas como uma propriedade distinta de Margin ou qualquer propriedade de 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 da interface do usuário do aplicativo poderá colocar manipuladores para LayoutUpdated em elementos se houver uma lógica especial a ser executada, mas a lógica do painel normalmente não precisará verificar por alterações utilizando o tratamento de eventos. O sistema de layout já está fazendo as determinações de quando reexecutar o layout porque uma propriedade relevante ao layout mudou de valor, e o MeasureOverride ou ArrangeOverride de um painel são invocados automaticamente nas circunstâncias apropriadas.

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 por seu elemento pai durante o layout. É típico que a entrada finalSize e o ArrangeOverride retornado tenham o mesmo Size. Se não estiverem, isso significa que o painel está tentando ajustar-se para um tamanho diferente do que os outros participantes na disposição 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 retorne um tamanho com um componente Infinito. Tentar usar um tamanho gera uma exceção do layout interno.

Todas as implementações de ArrangeOverride devem percorrer Childrene chamar o método Arrange para 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
}

A aprovação da organização do layout pode ocorrer sem ser precedida por uma aprovação de medida. 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 mudar em qualquer elemento em um layout, uma nova passagem de medição será necessária. O sistema de layout detecta automaticamente as mudanças reais de medida e invoca a passagem de medida novamente e, em seguida, executa outra passagem de arranjo.

A entrada de dados para Organizar utiliza um valor Rect. A maneira mais comum de construir este Rect é usar o construtor que possui uma entrada Point e uma entrada Size. O Point é o ponto em que o canto superior esquerdo da caixa delimitadora do elemento deve ser colocado. O Tamanho são as dimensões usadas para renderizar esse elemento específico. Geralmente, você usa o DesiredSize para esse elemento como este valor de Size, porque estabelecer o DesiredSize para todos os elementos envolvidos no layout foi o objetivo da etapa de medição do layout. (O passo de medição determina o dimensionamento total dos elementos de forma iterativa, permitindo que o sistema de layout otimize a disposição dos elementos ao chegar ao passo de organização.)

O que normalmente varia entre implementações de ArrangeOverride é a lógica pela qual o painel determina o componente ponto de de como ele organiza cada filho. Um painel de posicionamento absoluto, como Canvas usa as informações de posicionamento explícitas que obtém de cada elemento por meio de valores 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.

Ainda há influências de posicionamento adicionais nos elementos no layout, além do que você controla diretamente e passa para Arranjar. 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 espaçamento interno. 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 parte de um 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 da árvore visual além de relatar e usar informações das crianças.

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 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 uma passagem 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 eles quase certamente causarão um loop de layout. Normalmente, o código de controle não precisa invocá-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 passes de layout são finalizados e indica que ActualHeight ou ActualWidth foram alterados como resultado. Este é outro evento de FrameworkElement . Há casos em que LayoutUpdated é acionado, mas SizeChanged não. Por exemplo, o conteúdo interno pode ser reorganizado, mas o tamanho do elemento não foi alterado.

Referência

Conceitos