Lançado o starter kit para criação de readers em WPF

Se você ainda não conhece o New York Times Reader vale a pena conhecê-lo, pois é um ótimo exemplo de aplicação WPF.

Se você gostou mesmo dele e quer desenvolver algo semelhante, chegou a hora: a Microsoft lançou o reader toolkit, com o nome de Syndicated Client Experiences Starter Kit para que qualquer um possa desenvolver seus readers. Além diss, ela disponibilizou um reader muito interessante do MSDN Magazine com código fonte. Vale a pena dar uma olhada!

Zoom no canvas em WPF

Muitas vezes, queremos fazer um zoom em componentes WPF. Como todo o desenho é vetorial, não há perda de resolução neste processo.

Uma maneira de fazer o zoom é usar uma transformação, do tipo ScaleTransform. O ScaleTransform aumenta ou diminui o tamanho do componente, segundo suas propriedades ScaleX e ScaleY. Por exemplo, se quisermos aumentar ou diminuir o zoom no canvas do post anterior, podemos fazer o seguinte: no arquivo xaml, criamos um DockPanel para armazenar os botões e o canvas:

  <DockPanel>
    <StackPanel Orientation="Horizontal" Height="40" DockPanel.Dock="Top">
      <Button x:Name="maisZoom" Click="maisZoom_Click" Margin="5" Content="Mais zoom"/>
      <Button x:Name="menosZoom" Click="menosZoom_Click" Margin="5" Content="Menos zoom"/>
      <TextBlock x:Name="textZoom" Text="Zoom: 100%" VerticalAlignment="Center" Margin="5"/>
    </StackPanel>
    <Canvas....>
  </Canvas>
  </DockPanel>

Na parte superior do DockPanel colocamos um StackPanel. Nesse StackPanel colocamos dois botões e um textblock que irá mostrar o zoom atual. 

Em seguida, criamos uma transformação (LayoutTransform) para o canvas, inicializando a escala para 1:

      <Canvas...>
      <Canvas.LayoutTransform>
        <ScaleTransform x:Name="canvasZoom" ScaleX="1" ScaleY="1" />
      </Canvas.LayoutTransform>
 

 Finalmente, colocamos o código para o evento Click dos botões:

        private void maisZoom_Click(object sender, RoutedEventArgs e)
        {
            canvasZoom.ScaleX += 0.1;
            canvasZoom.ScaleY += 0.1;
            textZoom.Text = String.Format("Zoom: {0}%", canvasZoom.ScaleX * 100);
        }

        private void menosZoom_Click(object sender, RoutedEventArgs e)
        {
            canvasZoom.ScaleX -= 0.1;
            canvasZoom.ScaleY -= 0.1;
            textZoom.Text = String.Format("Zoom: {0}%", canvasZoom.ScaleX * 100);
        }
 

Executando o programa, vemos que os botões aumentam ou diminuem o zoom do canvas, sem nenhuma perda de resolução.

Quando aumentamos o zoom, o Canvas aumenta de tamanho, mas não há nenhuma indicação disso: devemos aumentar nossa janela para ver o restante da imagem. Podemos usar barras de rolagem para ver o restante da imagem quando o zoom é maior que 100%. Para isso, colocamos o Canvas dentro de um ScrollViewer:

     <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" >
      <Canvas ...>
         ....     
      </Canvas>
    </ScrollViewer>

O ScrollViewer apresenta barras de rolagem que são mostradas quando o tamanho de seu conteúdo é maior que seu tamanho. Assim, se dermos um zoom no Canvas que faça que ele fique maior que a área útil do ScrollViewer, as barras de rolagem aparecerão.

O projeto completo pode ser baixado aqui 

 

Movendo Shapes com o mouse em WPF

Em WPF, uma Shape (retângulos, elipses, linhas,..) é um FrameworkElement e recebe os eventos de mouse e teclado, como qualquer outro FrameworkElement (botões, por exemplo). Isto é muito diferente de WinForms, onde as formas desenhadas usando GDI+ são apenas desenhos na tela e não recebem nenhum evento. 

Podemos usar isso para movimentar nossas shapes com o mouse usando os eventos PreviewMouseLeftButtonDown, PreviewMouseLeftButtonUp e PreviewMouseMove.

Inicialmente, colocamos algumas shapes no Canvas:

    <Canvas x:Name="canvas1" PreviewMouseLeftButtonDown="canvas1_PreviewMouseDown"
            PreviewMouseMove="canvas1_PreviewMouseMove"
            PreviewMouseLeftButtonUp="canvas1_PreviewMouseUp">
      <Rectangle Canvas.Top="10" Canvas.Left="10" Width="20" Height="40" Fill="Fuchsia" />
      <Ellipse Canvas.Top="25" Canvas.Left="50" Width="40" Height="40" Fill="DarkSeaGreen" />
      <Line Canvas.Top="120" Canvas.Left="50" X1="10" Y1="10" X2="50" Y2="50" Stroke="Navy" StrokeThickness="3"/>
    <Polygon Canvas.Top="50" Canvas.Left="120" Points="30,20 80,24 80,54 30,20" Fill="Red"/>
  </Canvas>
 

Como podemos ver, colocamos os manipuladores de eventos no Canvas. Devido ao recurso de Bubbling e Tunneling, os eventos são propagados por toda a árvore de elementos e, quando clicamos em qualquer uma das shapes, o Canvas recebe o evento também. Assim, podemos processar os eventos em um único local, não nos preocupando de atribuir os manipuladores para cada shape do desenho.

 No código fonte, definimos alguns campos auxiliares:


        Point start; // Ponto base para a movimentação
        int currentZ = 0; // Z-Index atual
        bool isDragging = false; // Está movendo?
        Shape movedElement; // Elemento sendo movido

O manipulador para o evento PreviewMouseLeftButtonDown é:


        private void canvas1_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            // Verifica se clicamos numa Shape
            if (e.Source is Shape)
            {
                // Pega posição atual do mouse
                start = e.GetPosition(canvas1);
                // Inicializa variáveis e configura opacidade da shape para 0.5
                isDragging = true;
                movedElement = (Shape)e.Source;
                ((Shape)e.Source).Opacity = 0.5;
                canvas1.CaptureMouse();
                e.Handled = true;
            }
        }


Aqui, inicializamos a variável start com a posição que o mouse foi clicado, configuramos isDragging para true, deixamos o elemento semi-transparente e capturamos o mouse. O manipulador para o evento PreviewMouseMove é:


        private void canvas1_PreviewMouseMove(object sender, MouseEventArgs e)
        {
            if (isDragging)
            {
                Point Pt = e.GetPosition(canvas1);
                // Pega posição atual da shape
                double CurrentLeft = (double)movedElement.GetValue(Canvas.LeftProperty);
                double CurrentTop = (double)movedElement.GetValue(Canvas.TopProperty);

                // Calcula nova posição
                double newLeft = CurrentLeft + Pt.X - start.X;
                double newTop = CurrentTop + Pt.Y - start.Y;

                // Reposiciona elemento
                movedElement.SetValue(Canvas.LeftProperty, newLeft);
                movedElement.SetValue(Canvas.TopProperty, newTop);

                start = Pt;
                e.Handled = true;
            }
        }

Aqui verificamos se estamos arrastando um elemento. Se estivermos, pegamos a nova posição do mouse, recalculamos a posição da Shape e movemos. Como a posição é dada por attached properties, devemos usar SetValue e GetValue para obter e alterar as propriedades Canvas.Left e Canvas.Top. Finalmente, fechamos o movimento no evento PreviewMouseLeftButtonUp:


        private void canvas1_PreviewMouseUp(object sender, MouseButtonEventArgs e)
        {
            // Restaura valores
            movedElement.Opacity = 1;
            movedElement.SetValue(Canvas.ZIndexProperty, ++currentZ);
            isDragging = false;
            canvas1.ReleaseMouseCapture();
        }

Aqui apenas restauramos os valores. Uma coisa que deve ser notada é que estamos alterando a propriedade Canvas.ZIndex. Isto é devido ao fato que queremos que os elementos que movemos fiquem sempre por cima dos outros. Assim, usamos a variável currentZ para guardar o ZIndex, incrementando-o a cada movimento, de maneira que a cada vez que movemos um elemento, ele tenha um ZIndex maior que o anterior.

Desta maneira, podemos mover os elementos dentro de uma janela WPF. Note que este código pode ser estendido a qualquer FrameworkElement, não sendo apenas para Shapes.