Publicado em: 10 de janeiro de 2007
Por Bruno Sonnino and Roberto Sonnino
Nesta página
Introdução
Canetas e preenchimentos
Primitivas de desenho
Caminhos (Paths)
Transformações
Conclusões
Introdução
O WPF permite criar facilmente aplicações visualmente atraentes com recursos que exploram tanto o software como o hardware gráfico de sua máquina. Os gráficos em WPF podem ser vetoriais, isto é, não sofrem a influência da resolução ou tamanho de tela: ao aumentar ou diminuir a janela, eles são redimensionados e não perdem qualidade. Além do simples desenho de elementos, que podem estar dispostos inclusive dentro de outros elementos, podemos personalizar os visuais, aplicando transformações que renovam a aparência.
Neste artigo iremos mostrar os conceitos principais da criação e utilização de gráficos com WPF.
Canetas e preenchimentos
Quando trabalhamos com gráficos, dois objetos muito importantes são as canetas e preenchimentos. Isto é semelhante ao .net 2.0, onde criamos uma caneta ou um preenchimento antes de desenhar algo, utilizando a classe Graphics. No WPF, este modelo foi aprofundado e está integrado de maneira mais transparente e fácil de usar.
Os elementos visuais WPF, ao serem desenhados, em geral, têm um atributo Stroke que indica a cor da caneta que desenha a sua borda. Para o preenchimento, em geral, utiliza-se o atributo Fill ou Background, que recebe um objeto do tipo Brush. O código abaixo mostra uma elipse com borda azul e preenchimento amarelo.
<Ellipse Stroke="Blue" Fill="Yellow"/>
Embora estejamos apenas especificando uma cor, o WPF tem objetos Brush pré-definidos, que usam as cores nomeadas. Quando dizemos Fill="Yellow", na verdade estamos dizendo ao WPF para usar um pincel com preenchimento sólido de cor amarela. Poderíamos escrever o mesmo elemento da seguinte maneira:
<Ellipse StrokeThickness="1">
<Ellipse.Fill>
<SolidColorBrush Color="Yellow" />
</Ellipse.Fill>
<Ellipse.Stroke>
<SolidColorBrush Color="Blue" />
</Ellipse.Stroke>
<Ellipse.StrokeDashArray>
<DoubleCollection>1 0</DoubleCollection>
</Ellipse.StrokeDashArray>
</Ellipse>
Como podemos ver, o que está acontecendo por trás do desenho de uma caneta é uma série de propriedades que formam o estilo da linha, como o Stroke, o StrokeDashArray, StrokeThickness, entre outros. Já a propriedade Fill é composta de um SolidColorBrush com cor amarela.
Os diversos tipos de pincéis que podem ser utilizados são:
-
SolidColorBrush - é o tipo mais simples de pincel, basta definir a cor de preenchimento:
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Page.Background>
<SolidColorBrush Color="Yellow" />
</Page.Background>
</Page>
-
LinearGradientBrush - preenche com um gradiente linear. Este gradiente pode ser tão simples quanto um gradiente de duas cores horizontal até gradientes complexos, com inúmeras cores em posição diagonal. O código a seguir transforma sua página num arco-íris:
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Page.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<LinearGradientBrush.GradientStops>
<GradientStop Color="Red" Offset="0" />
<GradientStop Color="Orange" Offset="0.167" />
<GradientStop Color="Yellow" Offset="0.333" />
<GradientStop Color="Green" Offset="0.5" />
<GradientStop Color="Blue" Offset="0.667" />
<GradientStop Color="Indigo" Offset="0.833" />
<GradientStop Color="Violet" Offset="1" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Page.Background>
</Page>
Figura 1 - Fundo gradiente com cores do arco-íris
-
RadialGradientBrush - preenche com gradiente radial, a partir de um ponto especificado. Como no gradiente linear, podemos usar tanto duas cores como múltiplos pontos de "parada".
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Page.Background>
<RadialGradientBrush>
<RadialGradientBrush.GradientStops>
<GradientStop Color="Red" Offset="0" />
<GradientStop Color="White" Offset="1" />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</Page.Background>
</Page>
-
ImageBrush - preenche o desenho com uma imagem, que pode ser posicionada de diversas maneiras (lado a lado, esticada na horizontal ou vertical ou mesmo centralizada)
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Page.Background>
<ImageBrush ImageSource=
"http://www.microsoft.com/israel/business/club/images/vista_logo.jpg"
Viewport="0,0,80,80" ViewportUnits="Absolute" TileMode="Tile"
Stretch="None">
</ImageBrush>
</Page.Background>
</Page>
-
DrawingBrush - este tipo de Brush é composto do desenho de primitivas, como por exemplo, linhas e elipses
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Page.Background>
<DrawingBrush TileMode="FlipXY" ViewportUnits="Absolute"
Viewport="0 0 25 25">
<DrawingBrush.Drawing>
<GeometryDrawing>
<GeometryDrawing.Geometry>
<PathGeometry Figures="M 0 0 L 25 25" />
</GeometryDrawing.Geometry>
<GeometryDrawing.Pen>
<Pen Brush="Black" Thickness="1" />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</Page.Background>
</Page>
-
VisualBrush - este é um Brush que mostra um elemento visual WPF. O preenchimento é feito apenas com o conteúdo visual do elemento, seu comportamento não é transportado para ele. Assim, se preenchermos com um botão, ao clicar sobre o botão desenhado não acontecerá nada.
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Page.Background>
<VisualBrush TileMode="FlipX" Stretch="None" Viewport="0,0,100,40"
ViewportUnits="Absolute">
<VisualBrush.Visual>
<Button Width="100" Height="40" Content="Não me clique" />
</VisualBrush.Visual>
</VisualBrush>
</Page.Background>
</Page>
Figura 2 - VisualBrush repetido mostrando botões
Primitivas de desenho
O WPF permite o desenho de diversos tipos de primitivas, derivadas da classe abstrata Shape. A linha externa da primitiva é dada pelo atributo Stroke e o preenchimento é feito pelo atributo Fill. Podemos desenhar as seguintes primitivas:
-
Linhas - são desenhadas definindo-se os pontos inicial (X1,Y1) e final (X2,Y2). A maneira de desenhar a linha pode ser modificada usando-se as propriedades StrokeThickness, que muda a espessura da linha, StrokeDashArray, um vetor de doubles que indica o tamanho das áreas pintadas intercalada com o tamanho das áreas brancas e StrokeStartLineCap e StrokeEndLineCap, que muda a forma dos pontos inicial e final. O seguinte código desenha uma linha com espessura 10, pontilhada e com final em forma de triângulo:
<Line X1="100" Y1="200" X2="600" Y2="200" Stroke="Navy"
StrokeThickness="10" StrokeDashArray="5 2"
StrokeEndLineCap="Triangle" />
-
Retângulos - são definidos pela sua altura e largura, podem ter seus cantos arredondados usando as propriedades RadiusX e RadiusY:
<Rectangle Width="300" Height="200" Stroke="Navy"
StrokeThickness="10" StrokeDashArray="2 1" Fill="Yellow" RadiusX="20"
RadiusY="20"/>
-
Elipses - sua definição é semelhante aos retângulos, mas não contam com as propriedades RadisuX e RadiusY:
<Ellipse Width="300" Height="200" Stroke="Navy"
StrokeThickness="10" StrokeDashArray="2 1" Fill="Yellow" />
-
Polígonos e Polilinhas - são compostos de múltiplas linhas, e podem ter preenchimentos. Os pontos destas primitivas são dados pela propriedade Points. Ela é uma matriz de pontos em seqüência, com as coordenadas X e Y intercaladas. A diferença entre um polígono e uma polilinha é que, enquanto o polígono é sempre fechado (o último ponto é ligado automaticamente ao primeiro), a polilinha não tem o fechamento automático. Por exemplo, a diferença entre o polígono e a polilinha desenhados no código a seguir é a linha que fecha o triângulo:
<Polygon Points="200 50 300 300 100 300" Stroke="Navy"
StrokeThickness="10" StrokeDashArray="2 1" Fill="Yellow" />
<Polyline Points="200 50 300 300 100 300" Stroke="Navy"
StrokeThickness="10" StrokeDashArray="2 1" Fill="Yellow" />
Figura 3 - Polígono e Polilinha
Caminhos (Paths)
Além das primitivas simples, o WPF disponibiliza também primitivas mais complexas, derivadas das outras primitivas simples, chamadas de Path. Um Path tem a propriedade Data, que contém uma série de objetos Geometry, que determinam a maneira que o caminho será desenhado. Estes objetos são agrupados num GeometryGroup, como em:
<Path Stroke="Blue" Fill="Yellow">
<Path.Data>
<GeometryGroup>
<EllipseGeometry Center="200,200" RadiusX="100" RadiusY="100"/>
<EllipseGeometry Center="160,150" RadiusX="10" RadiusY="10"/>
<EllipseGeometry Center="240,150" RadiusX="10" RadiusY="10"/>
<EllipseGeometry Center="200,200" RadiusX="10" RadiusY="10"/>
<EllipseGeometry Center="200,250" RadiusX="50" RadiusY="10"/>
</GeometryGroup>
</Path.Data>
</Path>
Assim como temos Paths compostos de geometrias de primitivas simples, podemos ter geometrias compostas de segmentos retos e curvos, usando a PathGeometry. A PathGeometry é formada por PathFigures, que são conjuntos de segmentos do tipo Segment, como o LineSegment, o ArcSegment, o PolyLineSegment, ou BezierSegment. Veja um exemplo abaixo, que desenha um hexágono:
<Path HorizontalAlignment="Center" VerticalAlignment="Center" Stroke="Black" Fill="White">
<Path.Data>
<PathGeometry>
<PathFigure StartPoint="100 0" IsClosed="True">
<PolyLineSegment
Points="300,0 400,173 300,346 100,346 0,173"/>
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
Podemos também usar segmentos de arcos ou mesmo curvas bézier ou quadráticas avançadas. Para desenhar um arco simples, usamos um ponto que determina um arco a partir do último ponto de desenho. Definem-se dois valores que serão os arcos da elipse de onde o arco será extraído pela propriedade Size (Obs.: se estes arcos forem iguais, teremos arcos de circunferência.). Vejamos um exemplo:
<Path HorizontalAlignment="Center" VerticalAlignment="Center" Stroke="Black" StrokeThickness="10" Fill="White">
<Path.Data>
<PathGeometry>
<PathFigure StartPoint="80 50" IsClosed="True">
<ArcSegment Point="80,150" Size="100 80"
IsLargeArc="True"/>
<LineSegment Point="180,50" />
<ArcSegment Point="180,150" Size="100 80" IsLargeArc="True"
SweepDirection="Clockwise"/>
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
Figura 4 - As possibilidades com WPF são infinitas...
Note que nos segmentos de arcos usamos as propriedades IsLargeArc e SweepDirection para definir como o arco será desenhado, sendo o maior ou menor arco, e o sentido de desenho, em relação aos ponteiros do relógio.
Para desenhar uma curva de Bézier, basta usar o BezierSegment e definir o ponto final e os dois pontos de controle, usando as propriedades Point1, Point2 e Point3 (este último indica o ponto final):
<Path HorizontalAlignment="Center" VerticalAlignment="Center" Stroke="Black" Fill="White">
<Path.Data>
<PathGeometry>
<PathFigure StartPoint="100 100" >
<BezierSegment Point1="350 0" Point2="50 0"
Point3="300 100" />
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
Na maioria das vezes, esta sintaxe é muito longa e trabalhosa para desenhos complexos. O WPF tem uma sintaxe mais reduzida para PathGeometry que funciona analogamente à sintaxe completa. Para usar esta sintaxe reduzida, usamos a propriedade Data do Path e preenchemos com uma String especial, que define os tipos de segmentos e os pontos necessários. Os comando são compostos de uma letra seguida dos valores das coordenadas dos pontos necessários. Se for usada uma letra maiúscula, os valores são absolutos, e se for usada uma letra minúscula, os valores são relativos aos anteriores, isto é, representam a distância entre o novo ponto e o ponto anterior. Os comandos mais comuns são:
|
Comando
|
Descrição
|
Equivalente
|
|
M x y
| Movimenta para o ponto (x,y) | StartPoint="x y" |
|
L x y
| Linha até o ponto (x,y) | <LineSegment Point="x y"/> |
|
H x
| Linha horizontal até o ponto (x, y0) | <LineSegment Point="x y0"/> |
|
V y
| Linha vertical até o ponto (x0, y) | <LineSegment Point="x0 y"/> |
|
A xr yr a i j x y
| Arco de uma elipse com arcos xr e yr, até o ponto (x,y). O valor de a é o ângulo para rotacionar, i indica se é o arco maior (IsLargeArc=1) e j indica o sentido de desenho (Clockwise=1) | <ArcSegment Point="x y" Size="xr yr" IsLargeArc="i" SweepDirection="j" RotationAngle="a" /> |
|
C x1 y1 x2 y2 x3 y3
| Bézier cúbica com ponto final (x3,y3) e pontos de controle (x1,y1) e (x2,y2) | <BezierSegment Point1="x1 y1" Point2="x2 y2" Point3="x3 y3" /> |
| Q x1 y1 x2 y2 | Bézier quadrática com ponto final (x2,y2) e ponto de controle (x1,y1) | <QuadraticBezierSegment Point1="x1 y1" Point2="x2 y2" /> |
| Z | Fechar figura | IsClosed="true" |
Para repetir um comando, não é necessário repetir a letra.
Usando esta sintaxe, podemos redesenhar nossas figuras assim:
<Path HorizontalAlignment="Center" VerticalAlignment="Center" Stroke="Black" Fill="White"
Data="M 100 0 L 300,0 400,173 300,346 100,346 0,173 Z" />
<Path HorizontalAlignment="Center" VerticalAlignment="Center" Stroke="Black" StrokeThickness="10" Fill="White"
Data="M 80,50 A 100,80 0 1 0 80,150 L 180,50 A 100,80 0 1 1 180,150 Z"/>
<Path HorizontalAlignment="Center" VerticalAlignment="Center" Stroke="Black" Fill="White"
Data="M 100,100 C 350,0 50,0 300,100"/>
Note como a sintaxe ficou muito mais limpa e concisa, enquanto resultam nas mesmas figuras.
Transformações
Quando queremos alterar propriedades de um objeto visual, a maneira mais prática é utilizarmos as transformações. As transformações são conjuntos de instruções que alteram características visuais do elemento, sendo bastante poderosas, pois podem trabalhar com valores relativos e absolutos.
Em geral, as transformações são aplicadas utilizando-se a propriedade RenderTransform ou LayoutTransform de um elemento visual:
<Button Width="100" Height="50" >
Botão torto
<Button.RenderTransform>
<RotateTransform Angle="-30" />
</Button.RenderTransform>
</Button>
A diferença entre o RenderTransform e o LayoutTransform é que o primeiro ocorre depois da aplicação do layout (posicionamento, margens, tamanho, etc.), não o afetando e o segundo ocorre antes. Veja um exemplo que ilustra esta diferença:
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid ShowGridLines="True">
<Grid.RowDefinitions>
<RowDefinition Height="80"/>
<RowDefinition Height="80"/>
<RowDefinition Height="80"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Width="100" Height="50" Grid.Row="0">
Layout
<Button.LayoutTransform>
<RotateTransform Angle="-45" />
</Button.LayoutTransform>
</Button>
<Button Width="100" Height="50" Grid.Row="2">
Render
<Button.RenderTransform>
<RotateTransform Angle="-45" />
</Button.RenderTransform>
</Button>
</Grid>
</Page>
Veja a imagem abaixo:
Figura 5 - Diferença entre LayoutTransform e RenderTransform
Perceba que o primeiro botão, com LayoutTransform, foi afetado pela altura da linha e foi cortado pois este teve seu layout aplicado após a tranformação. O segundo passou as linhas da Grid, pois este teve a transformação efetuada após a criação do layout.
Vejamos alguns tipos mais comuns de transformações:
-
TranslateTransform - Move o objeto horizontalmente e verticalmente. Define duas propriedades, X e Y, que dizem quanto o objeto deve ser movido em X e Y:
<TranslateTransform X="-50" Y="50" />
-
ScaleTransform - Altera o tamanho do objeto, mantendo ou não a proporção. Define duas propriedades principais ScaleX e ScaleY que alteram o tamanho nas dimensões X e Y, e duas propriedades CenterX e CenterY que definem o centro do novo objeto aumentado ou diminuído. Você também pode inverter (refletir) o objeto dando valores de ScaleX e ScaleY negativos.
<Button Width="100" Height="50" >
Botão escalado
<Button.RenderTransform>
<ScaleTransform ScaleX="-2" ScaleY="3" CenterX="50" CenterY="25"
/>
</Button.RenderTransform>
</Button>
-
SkewTransform - Altera a forma do objeto, mudando os ângulos do retângulo em volta dele para um paralelogramo. Define duas propriedades principais AngleX e AngleY que alteram o ângulo nas dimensões X e Y, e duas propriedades CenterX e CenterY que definem o centro do novo objeto deformado.
<Button Width="200" Height="100" >
Botao deformado
<Button.RenderTransform>
<SkewTransform AngleX="-20" AngleY="0" CenterX="50"
CenterY="25" />
</Button.RenderTransform>
</Button>
Figura 6 -Botão transformado com SkewTransform
-
RotateTransform - Gira o objeto. Define uma propriedade principal Angle que diz de quanto será a rotação do objeto, e duas propriedades CenterX e CenterY que definem o centro do novo objeto rotacionado.
<Button Width="100" Height="50" >
Botão rotacionado
<Button.RenderTransform>
<RotateTransform Angle="-30" CenterX="50" CenterY="25" />
</Button.RenderTransform>
</Button>
Muitas vezes, desejamos fazer uma transformação em torno de uma origem relativa ao objeto (ex. O centro do objeto). Para isso utilizamos a propriedade RenderTransformOrigin do objeto transformado que determina um ponto relativo ao tamanho do objeto:
<Button Width="100" Height="50" RenderTransformOrigin="0.5 0.5">
Botao rotacionado
<Button.RenderTransform >
<RotateTransform Angle="-30" />
</Button.RenderTransform>
</Button>
Perceba que esta mudança só se aplica a objetos sendo transformados com RenderTransform pois a alteração da origem no LayoutTransform não altera o resultado, pois o posicionamento não será afetado.
Quando precisamos aplicar mais de uma transformação, podemos usar o objeto TransformGroup, que contém todas as transformações necessárias:
<Button Width="100" Height="50" RenderTransformOrigin="0.5 0.5">
Botao transformado
<Button.RenderTransform>
<TransformGroup>
<TranslateTransform X="30" Y="-20" />
<ScaleTransform ScaleX="-2" ScaleY="3" CenterX="50"
CenterY="25" />
<SkewTransform AngleX="-20" AngleY="45" CenterX="50"
CenterY="25" />
<RotateTransform Angle="-30" CenterX="50" CenterY="25" />
</TransformGroup>
</Button.RenderTransform>
</Button>
Conclusões
O WPF permite muita flexibilidade no desenho, tanto na quantidade de elementos para desenho como na variedade de maneira que eles podem ser desenhados e transformados. Os gráficos são independentes de resolução e utilizam a capacidade do hardware.
No próximo artigo, daremos movimento ao WPF mostrando como fazer animações. Até lá!