Estilos e modelos no WPF
Bruno Sonnino e Roberto Sonnino
Introdução
O WPF traz ao desenvolvedor muitas novidades na maneira de criar programas: já vimos a separação entre o código e a interface, com os arquivos XAML e a flexibilidade de posicionamento e alinhamento automático dos objetos, com os elementos de layout. Neste artigo veremos como personalizar os controles e manter uma interface padronizada, utilizando estilos e modelos (styles e templates).
Nesta página
Introdução a estilos
Introdução a recursos
Estilos
Triggers
Modelos de controles
Conclusões
Links Interessantes
Introdução a estilos
Quando montamos uma aplicação, uma das maiores dificuldades é manter um visual integrado, mantendo a apresentação entre as diversas telas do programa. Este problema é minimizado com a introdução do conceito de estilos do WPF. Um estilo funciona de maneira semelhante aos estilos do Word ou de páginas Web com CSS: você pode criar um conjunto de regras para personalizar os controles, aplicando-as a vários controles simultaneamente. A Figura 1 mostra uma tela com botões estilizados:
Figura 1 - Botões quadrados, com fundo vermelho, rotacionados
Esta imagem foi obtida aplicando-se um mesmo estilo a todos os botões. Os estilos podem ser aplicados individualmente ou a um tipo de controle, como os botões.
Antes de mostrar como são criados e aplicados os estilos, devemos falar um pouco sobre o conceito de recursos (resources).
Introdução a recursos
O conceito de recursos é muito amplo e se aplica a diversas características do WPF. Podemos definí-los, a grosso modo, como trechos de XAML que podem ser acessados e reutilizados em toda a interface.
Os recursos podem ser definidos de duas maneiras: recursos associados a uma dada página ou recursos inseridos em um arquivo XAML especial, chamado de dicionário de recursos externo (resource dictionary), que pode ser importado pelas páginas que compõem a aplicação.
Diversos tipos de informações podem ser armazenados como recursos: textos, gráficos, imagens, estilos ou mesmo objetos. Neste artigo nos concentraremos no uso de recursos para estilos ou modelos.
Para definir um recurso, usamos a propriedade Resources do elemento raiz. Esta propriedade contém a coleção de recursos da cena em questão. Por exemplo, o seguinte recurso contém um ImageBrush que pode ser usado em diversas partes da interface:
<Grid.Resources> <ImageBrush x:Key="IB_Egito" ImageSource="c:\windows\egito.bmp" /> </Grid.Resources>
Este recurso pode ser usado, por exemplo, para preencher o fundo de um botão, bastando usar o seguinte código:
<Button Background="{StaticResource IB_Egito}"/>
O recurso acessado é colocado entre chaves ("{" e "}") e especificado como StaticResource. O nome do recurso é dado pela sua propriedade x:Key. Podemos ter dois tipos de recursos: estáticos, que não mudam durante a execução; ou dinâmicos, que podem ser alterados durante a execução ou que provém do sistema operacional, como a cor do fundo padrão dos botões.
Estilos
Estilos definem a aparência dos objetos a que são aplicados. Podemos definir os estilos para um controle individual ou defini-lo como recurso e aplicá-lo a vários controles. A definição de estilos é feita de duas possíveis maneiras:
Você pode usar um estilo inline, ou seja, inserido no próprio botão:
<Button x:Name="Botao1"> <Button.Style> <Style> <Setter Property="Button.FontSize" Value="32" /> <Setter Property="Button.FontWeight" Value="Bold" /> </Style> </Button.Style> </Button>
Você pode usar um estilo como um recurso (resource) da página e dar-lhe um nome, referenciando o nome do estilo no botão:
<Grid ...>
<Grid.Resources>
<Style x:Key="Estilo_Botao_Negrito"
TargetType="{x:Type Button}">
<Setter Property="FontSize" Value="32" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
</Grid.Resources>
...
<Button Style="{StaticResource Estilo_Botao_Negrito}" />
...
</Grid>
Na definição de estilos como recursos, o atributo TargetType é opcional, mas seu uso possibilita definir propriedades específicas do controle em questão. Por isso, se um dado estilo for usado em apenas um tipo de controle, recomenda-se o uso do TargetType.
A definição como recurso é muito mais flexível e versátil, pois possibilita que um mesmo estilo seja aplicado a diversos controles, padronizando a interface:
<Grid ...>
<Grid.Resources>
<Style x:Key="Estilo1">
<Setter Property="TextElement.FontSize" Value="32" />
<Setter Property="Button.IsCancel" Value="False" />
</Style>
</Grid.Resources>
...
<!-- O Button tem uma propriedade IsCancel e
também é derivado de TextElement,
logo muda ambas as propriedades -->
<Button Style="{StaticResource Estilo1}" />
...
<!-- O TextBlock não tem uma propriedade IsCancel,
logo apenas muda a fonte -->
<TextBlock Style="{StaticResource Estilo1}" />
...
</Grid>
Os estilos também podem ser herdados de outros estilos, utilizado a propredade BasedOn:
<Grid ...>
<Grid.Resources>
<Style x:Key="Estilo_Negrito" TargetType="{x:Type Button}">
<Setter Property="FontSize" Value="32" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style x:Key="Estilo_Negrito_Vermelho"
TargetType="{x:Type Button}"
BasedOn="{StaticResource Estilo_Negrito}">
<Setter Property="Foreground" Value="Red" />
</Style>
</Grid.Resources>
...
<!-- Este botão terá o texto negrito, tamanho 32 e vermelho -->
<Button Style="{StaticResource Estilo_Negrito_Vermelho}"
Content="Botão Vermelho"/> ...
</Grid>
Por fim, os estilos também podem ser aplicados a toda uma classe de controles, bastando omitir o nome do estilo e usar a propriedade TargetType:
<Grid ...>
<Grid.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="FontSize" Value="32" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
</Grid.Resources>
...
<!-- Este botão terá o texto negrito, tamanho 32 -->
<Button />
...
<!-- Este também -->
<Button />
...
</Grid>
Neste caso, todos os botões incluídos nesta janela terão o mesmo formato, definido no estilo. Se quisermos que algum dos botões tenha um estilo diferente deste, devemos criar um estilo com nome e usá-lo no controle. Você pode ainda definir os estilos como recursos de toda a aplicação, padronizando ainda mais. Para isso, numa aplicação compilada, criamos o estilo no arquivo Application.xaml:
<!-- Application.xaml -->
<Application ...>
<Application.Resources>
<!-- Todos os botões da aplicação terão este estilo -->
<Style TargetType="{x:Type Button}">
<Setter Property="FontSize" Value="32" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
</Application.Resources>
...
</Application>
Triggers
Uma das funcionalidades mais poderosas dos estilos no WPF é o uso de triggers (gatilhos). Os triggers são disparados por condições (ex. Valores de propriedades) e permitem alterar os estilos dos objetos. Há diversos tipos de triggers, mas neste artigo focaremos em dois tipos básicos: os Triggers de propriedade e os MultiTriggers.
Os triggers de propriedade têm como condição o valor de uma propriedade do objeto:
<Style TargetType="{x:Type Button}">
...
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="LightBlue" />
</Trigger>
<Trigger Property="IsFocused" Value="True">
<Setter Property="Background" Value="Yellow" />
</Trigger>
</Style.Triggers>
<Setter Property="FontWeight" Value="Bold" />
</Style>
O estilo apresentado acima tem as seguintes consequências:
-
O botão estará sempre em negrito
-
Se o mouse estiver sobre o botão, ele terá fundo azul-claro
-
Se o botão tiver o foco, ele terá fundo amarelo
Além disso, como o estilo não foi nomeado, ele será aplicado a todos os botões no escopo do estilo.
Os MultiTriggers são triggers que têm como condição valores de várias propriedades, simultaneamente:
<Style TargetType="{x:Type Button}">
...
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsEnabled" Value="True" />
</MultiTrigger.Conditions>
<Setter Property="Background" Value="LightBlue" />
</MultiTrigger>
</Style.Triggers>
</Style>
Este estilo faz com que o botão fique azul-claro apenas se ele estiver sob o mouse e se ele estiver habilitado, simultaneamente. Caso ocorra apenas uma das condições, o fundo não se altera.
Modelos de controles
Os estilos são bastante poderosos quando se quer alterar alguma propriedade dos controles. No entanto, eles não permitem alterar completamente o visual dos controles, como por exemplo o formato ou posicionamento. Para isso, existem os modelos.
No WPF, os controles são faceless, ou seja, não têm um visual definido. O que eles têm é um modelo padrão definido pelo sistema operacional. No entanto, você pode criar seus próprios modelos para usar em sua aplicação, permitindo um visual totalmente personalizado. Vejamos um exemplo de como isso pode ser feito:
<Button x:Name="Botao1"> <Button.Template> <ControlTemplate> <Ellipse Fill="Red"/> </ControlTemplate> </Button.Template> </Button>
O trecho de código acima faz com que o botão em questão fique com o formato de uma elipse, mantendo as propriedades e comportamento atuais. Os modelos, assim como os estilos, podem ser definidos como recursos para maior flexibilidade:
<Grid ...>
<Grid.Resources>
<ControlTemplate TargetType="{x:Type Button}">
<Ellipse Fill="Red"/>
</ControlTemplate>
</Grid.Resources>
...
<!-- Este botão terá a forma de uma elipse vermelha -->
<Button />
...
</Grid>
Para conseguir um poder ainda maior, podemos também definir os modelos dentro de estilos, inclusive usando triggers:
<Grid ...>
<Grid.Resources>
<Style TargetType="{x:Type Button}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter>
<Setter.Property>
Template
</Setter.Property>
<Setter.Value>
<ControlTemplate
TargetType="{x:Type Button}">
<Ellipse Fill="Red"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style> </Grid.Resources>
...
<!-- Quando o mouse estiver sobre este botão
ele terá a forma de uma elipse vermelha -->
<Button />
...
</Grid>
Os modelos apresentados acima são completamente funcionais, no entanto eles não estão ligados às propriedades do botão nem mostram o conteúdo (Content) do botão. Para resolver este problema, devemos usar a ligação de modelos (Template Binding).
A ligação de modelos funciona da seguinte maneira: dentro de um modelo, liga-se uma propriedade à propriedade do controle que está sendo "modelado". Vejamos um exemplo:
<Button x:Name="Botao1" Background="Yellow">
<Button.Template>
<ControlTemplate>
<Ellipse Fill="Red"/>
</ControlTemplate>
</Button.Template>
</Button>
<Button x:Name="Botao2" Background="Yellow">
<Button.Template>
<ControlTemplate>
<Ellipse Fill="{TemplateBinding Property=Background}"/>
</ControlTemplate>
</Button.Template>
</Button>
No exemplo acima, o Botao1 ficará vermelho e o Botao2 ficará amarelo, devido ao fato de que o modelo do Botao2 usa a propriedade Background como ligação para definir o Fill da elipse. Em Botao1, embora a propriedade BackGround seja declarada, ela não é usada e é sobreposta pela cor do template.
Para resolver o problema do conteúdo do botão, deve-se usar no modelo o elemento ContentPresenter, que deve ser ligado ao conteúdo do controle (ContentControl.Content):
<Grid ...>
<Grid.Resources>
<ControlTemplate x:Key="Eliptico">
<Grid>
<Ellipse Fill="Yellow"/>
<ContentPresenter Content="{TemplateBinding
Property=ContentControl.Content}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
</Grid.Resources>
<!-- Este botão terá a forma de uma elipse amarela,
com o conteúdo correto -->
<Button Template="{StaticResource Eliptico}" Width="300" Height="100"
Content="Botão com modelo" FontWeight="Bold" FontSize="24" /> ...
</Grid>
Figura 3 - Botão com template
Para facilitar, você pode definir o TargetType do modelo. Fazendo isso, você evita a necessidade de ligar o conteúdo do ContentPresenter, pois esta ligação é feita implicitamente:
<Grid ...>
<Grid.Resources>
<ControlTemplate x:Key="Eliptico" TargetType="{x:Type Button}">
<Grid>
<Ellipse Fill="Yellow"/>
<ContentPresenter
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
</Grid.Resources>
...
<!-- Este botão terá a forma de uma elipse amarela,
com o conteúdo correto -->
<Button Content="Botão com modelo"/>
...
</Grid>
O ideal é que, ao usar modelos, você utilize ao máximo as ligações de modelos para que os controles sejam fáceis de personalizar através de suas propriedades, mas mantendo a padronização.
Conclusões
O WPF tem características de personalização de controles e interfaces bastante poderosa, permitindo alterar desde a cor do fundo até o visual completo e o formato de um controle. Tudo isto é feito sem a necessidade de código, permitindo que o Designer se encarregue da criação e padronização do visual, independentemente do Desenvolvedor.
Mostramos também um primeiro passo do conteúdo do próximo artigo, que é Ligação a Dados (Data Binding), através da ligação das propriedades de um modelo com o controle modelado. No próximo artigo, veremos outros tipos de ligações, inclusive a dados externos ou ao código do programa. Até lá!
Links Interessantes
http://windowssdk.msdn.microsoft.com/en-us/library/ms752351.aspx - Artigo introdutório da MSDN Library sobre estilos e modelos
http://windowssdk.msdn.microsoft.com/en-us/library/ms747308.aspx - Artigo da MSDN Library sobre modelos
http://www.microsoft.com/belux/msdn/nl/community/columns/gillcleeren/wpf_stylesandtriggers.mspx - Artigo da MSDN Belgica e Luxemburgo sobre estilos e triggers