Fronteiras da interface do usuário

Eventos de manipulação multitoque no WPF

Charles Petzold

Baixar o código de exemplo

Charles PetzoldNos últimos anos, o multitoque progrediu de uma proposta futurista de filmes de ficção científica para uma forma comum de interface do usuário. As telas multitoque agora são o padrão em novos modelos de smartphones e Tablet PCs. O multitoque provavelmente também se tornará onipresente em computadores de espaços públicos, como quiosques ou no computador de mesa pioneiro Microsoft Surface.

A única incerteza real é a popularidade do multitoque no computador desktop convencional. Talvez o maior impedimento seja a fatiga conhecida como “braço de gorila” associada à movimentação dos dedos em telas verticais por longos períodos de tempo. A minha esperança é que o poder do multitoque realmente provoque uma reformulação da tela da área de trabalho. Posso prever um computador desktop com uma tela que lembra a configuração de uma prancheta de desenho, talvez tão grande quanto ela.

Porém, isso é o futuro (talvez). No presente, os desenvolvedores têm novas APIs para dominar. O suporte ao multitoque no Windows 7 foi filtrado e incorporado a diversas áreas do Microsoft .NET Framework com interfaces baixa e alta.

Organizando o suporte a multitoque

Se você considerar a complexidade de expressão possível com o uso de vários dedos em uma tela, talvez possa avaliar por que ninguém parece saber ainda qual é a interface de programação “correta” para multitoque. Isso vai levar algum tempo. Enquanto isso, você tem várias opções.

O Windows Presentation Foundation (WPF) 4.0 possui duas interfaces multitoque disponíveis para programas executados no Windows 7. Para usos especializados de multitoque, os programadores desejarão explorar a interface de baixo nível, que consiste em vários eventos roteados definidos pelo UIElement nomeado TouchDown, TouchMove, TouchUp, TouchEnter, TouchLeave, com versões de visualização dos eventos abaixo, mover e acima. Obviamente, eles são modelados de acordo com os eventos do mouse, exceto que a propriedade ID do inteiro é necessária para controlar os vários dedos na tela. O Microsoft Surface foi criado no WPF 3.5, mas dá suporte a uma interface Contact, mais extensiva e de baixo nível, que diferencia tipos e formas da entrada de toque.

O assunto desta coluna é o suporte de alto nível ao multitoque no WPF 4.0, que consiste em um conjunto de eventos cujos nomes começam com a palavra Manipulation. Esses eventos Manipulation executam vários trabalhos essenciais de multitoque:

  • consolidar a interação de dois dedos em uma única ação
  • resolver o movimento de um ou dois dedos em transformações
  • implementar a inércia quando os dedos saem da tela

Um subconjunto dos eventos Manipulation é listado na documentação do Silverlight 4, porém ele é um pouco enganoso. Os eventos ainda não têm o suporte do próprio Silverlight, mas têm suporte em aplicativos do Silverlight escritos para o Windows Phone 7. Os eventos Manipulation são listados na Figura 1.

Figura 1 Os eventos Manipulation no Windows Presentation Foundation 4.0

Evento Com suporte do Windows Phone 7?
ManipulationStarting Não
ManipulationStarted Sim
ManipulationDelta Sim
ManipulationInertiaStarted Não
ManipulationBoundaryFeedback Não
ManipulationCompleted Sim

 

Os aplicativos do Silverlight 4 baseados na Web continuarão a usar o evento Touch.FrameReported que abordei no artigo “Estilo de dedo: Explorando o suporte a multitoque no Silverlight” na edição de março de 2010 da MSDN Magazine.

Juntamente com os próprios eventos Manipulation, a classe UIElement no WPF também dá suporte a métodos substituíveis como On­ManipulationStarting, correspondente aos eventos Manipulation. No Silverlight para Windows Phone 7, esses métodos substituíveis são definidos pela classe Control.

Um exemplo de multitoque

Talvez o aplicativo multitoque arquetípico seja um visualizador fotográfico que permite mover fotos em uma superfície, ampliá-las ou reduzi-las com dois dedos, além de girá-las. Essas operações às vezes são chamadas de panorâmica, zoom e girar, e correspondem às transformações gráficas padrão de translação, colocação em escala e rotação.

Obviamente, um programa de exibição de fotos precisa manter a coleção de fotos, permitir que novas fotos sejam adicionadas e que fotos sejam removidas, e sempre é bacana exibir as fotos em uma pequena moldura gráfica, mas vou ignorar tudo isso e me concentrar apenas na interação multitoque. Fiquei surpreso em ver como tudo fica fácil com os eventos Manipulation, e acho que você também ficará.

Todo o código-fonte dessa coluna está em uma única solução que pode ser baixada, chamada WpfManipulationSamples. O primeiro projeto é SimpleManipulationDemo e o arquivo MainWindow.xaml é mostrado na Figura 2.

Figura 2 O arquivo XAML de SimpleManipulationDemo

<Window x:Class="SimpleManipulationDemo.MainWindow"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  Title="Simple Manipulation Demo">

  <Window.Resources>
    <Style TargetType="Image">
      <Setter Property="Stretch" Value="None" />
      <Setter Property="HorizontalAlignment" Value="Left" />
      <Setter Property="VerticalAlignment" Value="Top" />
    </Style>
  </Window.Resources>

  <Grid>
    <Image Source="Images/112-1283_IMG.JPG"  
      IsManipulationEnabled="True"
      RenderTransform="0.5 0 0 0.5 100 100" />

    <Image Source="Images/139-3926_IMG.JPG"
      IsManipulationEnabled="True"
      RenderTransform="0.5 0 0 0.5 200 200" />
        
    <Image Source="Images/IMG_0972.JPG"
      IsManipulationEnabled="True"
      RenderTransform="0.5 0 0 0.5 300 300" />
        
    <Image Source="Images/IMG_4675.JPG"
      IsManipulationEnabled="True"
      RenderTransform="0.5 0 0 0.5 400 400" />
  </Grid>
  </Window>

Em primeiro lugar, observe a configuração em todos os três elementos Image:

IsManipulationEnabled="True"

Essa propriedade é false por padrão. Você deve defini-la como true para qualquer elemento no qual deseja obter entrada de multitoque e gerar eventos Manipulation.

Os eventos Manipulation são eventos roteados do WPF, ou seja, eles são propagados para cima na árvore visual. Nesse programa, nem a Grid nem a MainWindow têm a propriedade IsManipulationEnabled definida como true, mas você ainda pode anexar manipuladores para os eventos Manipulation aos elementos Grid e MainWindow, ou substituir os métodos OnManipulation na classe MainWindow.

Observe também que cada um dos elementos Image possui seu Render­Transform definido como uma cadeia de seis números:

RenderTransform="0.5 0 0 0.5 100 100"

Esse é um atalho que define a propriedade RenderTransform como um objeto MatrixTransform inicializado. Nesse caso específico, o objeto Matrix definido como o MatrixTransform é inicializado para executar uma escala de 0,5 (fazendo com que as fotos tenham metade do seu tamanho real) e uma translação de 100 unidades para a direita e para baixo. O arquivo de code-behind da janela acessa e modifica esse MatrixTransform.

O arquivo MainWindow.xaml.cs completo é mostrado na Figura 3 e substitui apenas dois métodos, OnManipulationStarting e OnManipulationDelta. Esses métodos processam as manipulações geradas pelos elementos Image.

Figura 3 O arquivo de code-behind de SimpleManipulationDemo

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace SimpleManipulationDemo {
  public partial class MainWindow : Window {
    public MainWindow() {
      InitializeComponent();
    }

    protected override void OnManipulationStarting(
      ManipulationStartingEventArgs args) {

      args.ManipulationContainer = this;

      // Adjust Z-order
      FrameworkElement element = 
        args.Source as FrameworkElement;
      Panel pnl = element.Parent as Panel;

      for (int i = 0; i < pnl.Children.Count; i++)
        Panel.SetZIndex(pnl.Children[i],
          pnl.Children[i] == 
          element ? pnl.Children.Count : i);

      args.Handled = true;
      base.OnManipulationStarting(args);
    }

    protected override void OnManipulationDelta(
      ManipulationDeltaEventArgs args) {

      UIElement element = args.Source as UIElement;
      MatrixTransform xform = 
        element.RenderTransform as MatrixTransform;
      Matrix matrix = xform.Matrix;
      ManipulationDelta delta = args.DeltaManipulation;
      Point center = args.ManipulationOrigin;

      matrix.ScaleAt(
        delta.Scale.X, delta.Scale.Y, center.X, center.Y);
      matrix.RotateAt(
        delta.Rotation, center.X, center.Y);
      matrix.Translate(
        delta.Translation.X, delta.Translation.Y);
      xform.Matrix = matrix;

      args.Handled = true;
      base.OnManipulationDelta(args);
    }
  }
  }

Noções básicas de manipulação

Uma manipulação é definida como um ou mais dedos tocando um determinado elemento. Uma manipulação completa começa com o evento Manipulation­Starting (seguido logo após por ManipulationStarted) e termina com ManipulationCompleted. Entre eles, pode haver muitos eventos ManipulationDelta.

Cada um dos eventos Manipulation é acompanhado pelo seu próprio conjunto de argumentos de evento encapsulados em uma classe que tem o nome do evento com EventArgs anexado, como ManipulationStartingEventArgs e ManipulationDeltaEventArgs. Essas classes derivam do familiar InputEventArgs que, por sua vez, deriva de RoutedEvent­Args. As classes incluem as propriedades Source e OriginalSource, indicando o local de origem do evento.

No programa SimpleManipulationDemo, Source e Original­Source serão ambos definidos como o elemento Image que gera os eventos Manipulation. Apenas um elemento com sua propriedade IsManipulation­Enabled definida como true será exibido como as propriedades Source e OriginalSource nesses eventos Manipulation.

Além disso, cada uma das classes de argumento de evento associadas aos eventos Manipulation inclui uma propriedade chamada Manipulation­Container. Esse é o elemento no qual a manipulação multitoque está ocorrendo. Todas as coordenadas nos eventos Manipulation são relativas a esse contêiner.

Por padrão, a propriedade ManipulationContainer é definida como o mesmo elemento das propriedades Source e OriginalSource — o elemento que está sendo manipulado — mas provavelmente não é isso que você quer. Em geral, não é conveniente que o contêiner de manipulação seja o mesmo do elemento que está sendo manipulado, porque interações complicadas envolvem a movimentação, o dimensionamento e a rotação dinâmicos do mesmo elemento que está relatando informações de toque. Em vez disso, é preferível que o contêiner de manipulação seja um parente do elemento manipulado, ou talvez um elemento que esteja mais acima na árvore visual.

Na maioria dos eventos Manipulation, a propriedade ManipulationContainer é somente obtenção. A exceção é o primeiro evento Manipulation que um elemento recebe. Em ManipulationStarting você tem a oportunidade de alterar o ManipulationContainer para algo mais adequado. No projeto SimpleManipulationDemo, esse trabalho é uma única linha de código:

args.ManipulationContainer = this;

Em todos os eventos subsequentes, o ManipulationContainer será o elemento MainWindow em vez do elemento Image, e todas as coordenadas serão relativas a essa janela. Isso funciona bem, pois o Grid que contém os elementos Image também é alinhado com a janela.

O resto do método OnManipulationStarting é dedicado a trazer para frente o elemento Image que foi tocado redefinindo as propriedades anexadas de Panel.ZIndex de todos os elementos Image da Grid. Essa é uma maneira simples de manipular o ZIndex, mas provavelmente não é a melhor, pois cria alterações repentinas.

ManipulationDelta e DeltaManipulation

O único outro evento manipulado por SimpleManpulationDemo é ManipulationDelta. A classe ManipulationDeltaEventArgs define duas propriedades do tipo ManipulationDelta (sim, o evento e a classe têm o mesmo nome). Essas propriedades são DeltaManipulation e CumulativeManipulation. Como os nomes sugerem, DeltaManipulation reflete a manipulação que ocorreu desde o último evento ManipulationDelta, e CumulativeManipulation é a manipulação completa que teve início com o evento ManipulationStarting.

ManipulationDelta tem quatro propriedades:

  • Translation do tipo Vector
  • Scale do tipo Vector
  • Expansion do tipo Vector
  • Rotation do tipo double

A estrutura Vector define duas propriedades chamadas X e Y do tipo double. Uma das diferenças mais significativas com o suporte a Manipulation no Silverlight para Windows Phone 7 é a ausência das propriedades Expansion e Rotation.

A propriedade Translation indica movimento (ou uma panorâmica) nas direções horizontal e vertical. Um único dedo em um elemento pode gerar alterações na translação, mas esta também pode fazer parte de outras manipulações.

As propriedades Scale e Expansion indicam uma alteração no tamanho (um zoom), que sempre requer dois dedos. Scale é multiplicadora e Expansion é aditiva. Use Scale para definir uma transformação de escala; use Expansion para aumentar ou diminuir as propriedades Width e Height de um elemento por unidades independentes de dispositivo.

No WPF 4.0, os valores X e Y do vetor Scale são sempre iguais. Os eventos Manipulation não fornecem informações suficientes para dimensionar um elemento anisotropicamente (ou seja, de formas diferentes nas direções horizontal e vertical).

Por padrão, Rotation também requer dois dedos, embora você vá ver mais adiante como habilitar a rotação com um dedo. Em qualquer evento ManipulationDelta em particular, todas as quatro propriedades podem ser definidas. Um par de dedos pode estar ampliando um elemento e, ao mesmo tempo, girando-o e movendo-o para outro local.

A colocação em escala e a rotação sempre são relativas a um determinado ponto central. Esse centro também é fornecido em ManipulationDeltaEvent­Args na propriedade chamada ManipulationOrigin do tipo Point. Essa origem é relativa ao ManipulationContainer definido no evento ManipulationStarting.

Seu trabalho no evento ManipulationDelta é modificar a propriedade Render­Transform do objeto manipulado de acordo com os valores delta na seguinte ordem: primeiro, colocação em escala, depois rotação e, finalmente, translação (na realidade, como os fatores de colocação em escala horizontal e vertical são idênticos, você pode alternar a ordem das transformações de colocação em escala e de rotação, e ainda obter o mesmo resultado).

O método OnManipulationDelta na Figura 3 mostra uma abordagem padrão. O objeto Matrix é obtido do conjunto MatrixTransform no elemento Image manipulado. Ele é modificado por meio de chamadas para ScaleAt e RotateAt (ambos relativos à ManipulationOrigin) e Translate. Matrix é uma estrutura e não uma classe, então é necessário concluir substituindo o valor antigo em MatrixTransform pelo novo.

É possível variar um pouco esse código. Como mostrado, ele é dimensionado ao redor de um centro com esta instrução:

matrix.ScaleAt(delta.Scale.X, delta.Scale.Y, center.X, center.Y);

Ela equivale à translação para o negativo do ponto central, colocação em escala e, em seguida, translação de volta:

matrix.Translate(-center.X, -center.Y);
matrix.Scale(delta.Scale.X, delta.Scale.Y);
matrix.Translate(center.X, center.Y);

Da mesma forma, o método RotateAt pode ser substituído por:

matrix.Translate(-center.X, -center.Y);
matrix.Rotate(delta.Rotation);
matrix.Translate(center.X, center.Y);

As duas chamadas Translate adjacentes agora cancelam uma a outra, então a composição é:

matrix.Translate(-center.X, -center.Y);
matrix.Scale(delta.Scale.X, delta.Scale.Y);
matrix.Rotate(delta.Rotation);
matrix.Translate(center.X, center.Y);

Provavelmente ela é um pouco mais eficiente.

A Figura 4 mostra o programa SimpleManipulationDemo em ação.

Figure 4 The SimpleManipulationDemo Program

Figura 4 O programa SimpleManipulationDemo

Habilitando o contêiner?

Um dos recursos interessantes do programa SimpleManpulationDemo é que você pode manipular simultaneamente dois elementos Image, ou até mesmo mais, se tiver o suporte de hardware e um número suficiente de dedos. Cada elemento Image gera o seu próprio evento ManipulationStarting e a sua própria série de eventos Manipulation­Delta. O código efetivamente diferencia os vários elementos Image pela propriedade Source dos argumentos do evento.

Por esse motivo, é importante não definir nos campos nenhuma informação de estado que sugira que apenas um elemento pode ser manipulado de cada vez.

A manipulação simultânea de vários elementos é possível porque cada um dos elementos Image possui sua própria propriedade IsManipulationEnabled definida como true. Cada um deles pode gerar uma única série de eventos Manipulation.

Ao abordar esses eventos Manipulation pela primeira vez, você pode investigar a configuração de IsManpulationEnabled como true apenas na classe MainWindow ou em outro elemento que funcione como um contêiner. Isso é possível, porém um pouco mais forçado na prática, além de não ser tão poderoso. A única vantagem real é que não é necessário definir a propriedade ManipulationContainer no evento ManipulationStarting. A confusão vem mais tarde, quando você deve determinar qual elemento está sendo manipulado pelo teste de clique nos elementos filho usando a propriedade ManipulationOrigin no evento ManipulatedStarted.

Seria então necessário armazenar o elemento que está sendo manipulado como um campo para uso em futuros eventos ManipulationDelta. Nesse caso, é seguro armazenar informações de estado em campos, pois você só poderá manipular um elemento do contêiner de cada vez.

O modo de manipulação

Como você viu, uma das propriedades fundamentais a serem definidas durante o evento ManipulationStarting é ManipulationContainer. Outras duas propriedades são úteis para personalizar a manipulação em particular.

É possível limitar os tipos de manipulação que você pode executar inicializando a propriedade Mode com um membro da enumeração Manipulation­Modes. Por exemplo, se você estiver usando a manipulação unicamente para rolar horizontalmente, talvez queira limitar os eventos somente à translação horizontal. O programa ManipulationModesDemo permite definir o modo dinamicamente exibindo uma lista de elementos RadioButton listando as opções, como mostra a Figura 5.

Figure 5 The ManipulationModeDemo Display

Figura 5 A exibição ManipulationModeDemo

É claro que o RadioButton é um dos muitos controles no WPF 4.0 que respondem diretamente ao toque.

A rotação com um só dedo

Por padrão, são necessários dois dedos para girar um objeto. No entanto, se uma foto real estiver sobre uma mesa real, você poderá colocar o dedo no canto e girá-la em um círculo. A rotação não está ocorrendo exatamente no centro do objeto.

É possível fazer isso com os eventos Manipulation definindo a propriedade Pivot de ManipulationStartingEventArgs. Por padrão, a propriedade Pivot é nula; você habilita a rotação com um dedo definindo a propriedade para um objeto ManipulationPivot. A principal propriedade de
ManipulationPivot é Center, que você pode considerar calcular como o centro do elemento que está sendo manipulado:

Point center = new Point(element.ActualWidth / 2, 
                         element.ActualHeight / 2);

Mas esse ponto central deve ser relativo ao contêiner de manipulação, que no programa que venho mostrando a você é o elemento que está manipulando os eventos. A translação desse ponto central do elemento que está sendo manipulado para o contêiner é fácil:

center = element.TranslatePoint(center, this);

Outra pequena informação também precisa ser definida. Se tudo que você está especificando é um ponto central, surge um problema quando você coloca o dedo bem no centro do elemento: um pequeno movimento fará com que o elemento gire sem parar! Por esse motivo, ManipulationPivot também tem uma propriedade Radius. A rotação não ocorrerá se o dedo estiver dentro do número de unidades de Radius do ponto Center. O programa ManipulationPivotDemo define esse raio como meia polegada:

args.Pivot = new ManipulationPivot(center, 48);

Agora um único dedo pode executar uma combinação de rotação e translação.

Além das noções básicas

O que você viu aqui são noções básicas de como usar os eventos Manipulation do WPF 4.0. É claro que existem algumas variações nessas técnicas, que mostrarei em futuras colunas, bem como o poder da inércia da manipulação.

Talvez você também queira dar uma olhada no Kit de ferramentas do Surface para Windows Touch, que fornece controles otimizados para toque para os seus aplicativos. O controle ScatterView em particular elimina a necessidade de usar os eventos Manipulation diretamente para itens básicos como manipular fotos. Ele tem alguns efeitos e comportamentos atrativos que irão garantir que o seu aplicativo se comporte da mesma maneira que outros aplicativos de toque.

Charles Petzoldé um editor colaborador da MSDN Magazine há muito tempo. Atualmente ele está escrevendo o livro “Programming Windows Phone 7”, o qual será publicado ainda em 2010, como um livro eletrônico que pode ser baixado gratuitamente. Uma edição de demonstração está disponível em seu site, charlespetzold.com.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Doug Kramer, Robert Levy e Anson Tsao