Desenhando gráficos com WPF
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> 
     
    
    					
    Cc518036.Des_graficos_WPF_Fig01(pt-br,MSDN.10).jpg

    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>
    
    				
    Cc518036.Des_graficos_WPF_Fig02(pt-br,MSDN.10).jpg

    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" />
    
    				
    Cc518036.Des_graficos_WPF_Fig03(pt-br,MSDN.10).jpg

    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>

			
Cc518036.Des_graficos_WPF_Fig04(pt-br,MSDN.10).jpg

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 y2Bézier quadrática com ponto final (x2,y2) e ponto de controle (x1,y1)<QuadraticBezierSegment Point1="x1 y1" Point2="x2 y2" />
ZFechar figuraIsClosed="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:

Cc518036.Des_graficos_WPF_Fig05(pt-br,MSDN.10).jpg

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>
    
    				
    Cc518036.Des_graficos_WPF_Fig06(pt-br,MSDN.10).jpg

    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á!

Page view tracker