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