Share via


Las fronteras de las UI

Eventos de manipulación multitoque en WPF

Charles Petzold

Descargar el ejemplo de código

Charles PetzoldSólo durante los últimos años, el multitoque ha progresado de ser un accesorio futurista de películas de ciencia ficción a un medio convencional de interfaz de usuario. Las pantallas multitoque ahora son estándar en los modelos nuevos de smartphones y Tablet PC. Es probable que el multitoque también comience a estar presente en los equipos de espacios públicos, como quioscos o equipos de mesa, una innovación de Microsoft Surface.

Lo único realmente incierto es la popularidad del multitoque en el equipo de escritorio convencional. Quizás el mayor impedimento sea el cansancio conocido como "brazo de gorila" asociado con mover los dedos sobre pantallas verticales durante largos períodos. Mi esperanza personal es que la potencia del multitoque provoque en realidad un rediseño de la pantalla de los equipos de escritorio. Puedo prever un equipo de escritorio con una pantalla que se asemeje a la configuración de una mesa de dibujo, y quizás del mismo tamaño.

Pero ése es el futuro (quizás). Por ahora, los desarrolladores tienen nuevas API que dominar. La compatibilidad de multitoque en Windows 7 se ha filtrado y adaptado a diversas áreas de Microsoft .NET Framework con interfaces tanto inferiores como superiores.

Organización de la compatibilidad de multitoque

Si considera la complejidad de la expresión que es posible al usar varios dedos en una pantalla, quizás pueda apreciar por qué al parecer todavía nadie conoce completamente la interfaz de programación "correcta" para el multitoque. Esto demorará un tiempo. Mientras tanto, cuenta con varias opciones.

Windows Presentation Foundation (WPF) 4.0 tiene dos interfaces de multitoque disponibles para programas que se ejecutan en Windows 7. Para usos especializados de multitoque, los programadores desearán explorar la interfaz de nivel inferior que consta de diversos eventos enrutados definidos por el UIElement llamados TouchDown, TouchMove, TouchUp, TouchEnter, TouchLeave, con versiones de vista previa de los eventos down, move y up. Obviamente estos se modelan a partir de los eventos del mouse, excepto en que se necesita una propiedad de identificador entero para hacer seguimiento de los diversos dedos en la pantalla. Microsoft Surface está creado sobre WPF 3.5, pero es compatible con una interfaz de contacto de nivel inferior más extensiva que distingue tipos y formas de entrada táctil.

El tema de esta columna es la compatibilidad multitoque de nivel superior en WPF 4.0, que consta de una colección de eventos cuyos nombres comienzan con la palabra Manipulation (Manipulación). Estos eventos de manipulación realizan varios trabajos cruciales de multitoque para:

  • consolidar la interacción de dos dedos en una sola acción;
  • resolver el movimiento de uno o dos dedos en las transformaciones;
  • implementar la inercia cuando los dedos dejan de tocar la pantalla.

Un subconjunto de eventos de manipulación aparece en la documentación de Silverlight 4, pero puede ser un poco engañoso. Los eventos todavía no son compatibles con Silverlight, pero sí son compatibles con aplicaciones de Silverlight escritas para Windows Phone 7. Los eventos de manipulación aparecen en la figura 1.

Figura 1 Los eventos de manipulación en Windows Presentation Foundation 4.0

Evento ¿Compatible con Windows Phone 7?
ManipulationStarting No
ManipulationStarted
ManipulationDelta
ManipulationInertiaStarted No
ManipulationBoundaryFeedback No
ManipulationCompleted

 

Las aplicaciones de Silverlight 4 basadas en web seguirán usando el evento Touch.FrameReported que analicé en el artículo “Estilo de un toque: Exploración de compatibilidad multitoque en Silverlight” en el número de marzo de 2010 de MSDN Magazine.

Junto con los eventos de manipulación mismos, la clase UIElement en WPF también admite métodos reemplazables como On­ManipulationStarting que corresponden a los eventos de manipulación. En Silverlight para Windows Phone 7, es la clase Control la que define estos métodos reemplazables.

Un ejemplo de multitoque

Quizás la aplicación multitoque arquetípica sea un visor de fotografía que le permite mover fotos en una superficie, agrandándolas o achicándolas con un par de dedos, y girarlas. A veces, estas operaciones se denominan panorámica, zoom y rotación, y corresponden a las transformaciones gráficas estándar de traslación, escala y rotación.

Obviamente, un programa para la visualización de fotografías necesita mantener la colección de fotos, permitir agregar fotos nuevas y eliminar otras, y siempre es bueno mostrar las fotos en un pequeño marco gráfico, pero voy a ignorar todo eso y sólo me centraré en la interacción multitoque. Me sorprendió lo fácil que resulta con los eventos de manipulación y creo que también se sorprenderá.

Todo el código fuente de esta columna se encuentra en una sola solución descargable llamada WpfManipulationSamples. El primer proyecto es SimpleManipulationDemo y el archivo MainWindow.xaml aparece en la figura 2.

Figura 2 El archivo XAML para 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>

Primero observe la configuración en los tres elementos de imagen:

IsManipulationEnabled="True"

Esta propiedad está definida en false de manera predeterminada. Debe definirla en true para cualquier elemento en que desee obtener entrada de multitoque y generar eventos de manipulación.

Los eventos de manipulación son eventos enrutados de WPF, lo que significa que los eventos ascienden en el árbol visual. En este programa, ni la cuadrícula ni la MainWindow tienen la propiedad IsManipulationEnabled definida en true, pero todavía puede conectar controladores para los eventos de manipulación a los elementos de cuadrícula y MainWindow, o reemplazar los métodos OnManipulation en la clase MainWindow.

Observe también que cada uno de los elementos de la imagen tiene su Render­Transform definido en una cadena de seis números:

RenderTransform="0.5 0 0 0.5 100 100"

Éste es un acceso directo que define la propiedad RenderTransform en un objeto MatrixTransform inicializado. En este caso en particular, el objeto Matrix definido en MatrixTransform se inicializa para realizar una escala de 0,5 (lo que hace que las fotografías tengan la mitad de su tamaño real) y una traslación de 100 unidades a la derecha y abajo. El archivo de código subyacente para la ventana obtiene acceso a esta MatrixTransform y la modifica.

El archivo MainWindow.xaml.cs completo aparece en la figura 3, y sólo reemplaza dos métodos, OnManipulationStarting y OnManipulationDelta. Estos métodos procesan las manipulaciones generadas por los elementos de la imagen.

Figura 3 El archivo de código subyacente para 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);
    }
  }
  }

Aspectos básicos de la manipulación

Una manipulación se define como uno o más dedos tocando un elemento en particular. Una manipulación completa comienza con el evento Manipulation­Starting (seguido muy de cerca por ManipulationStarted) y finaliza con ManipulationCompleted. Entremedio, pueden existir muchos eventos ManipulationDelta.

Cada uno de los eventos de manipulación está acompañado de su propio conjunto de argumentos de eventos encapsulados en una clase denominada a partir del evento con el EventArgs anexado, como ManipulationStartingEventArgs y ManipulationDeltaEventArgs. Estas clases derivan del InputEventArgs familiar, el que a su vez deriva de RoutedEvent­Args. Estas clases incluyen las propiedades Source y OriginalSource que indican dónde se originó el evento.

En el programa SimpleManipulationDemo, tanto Source como Original­Source se definirán en el elemento de imagen que genera los eventos de manipulación. Sólo un elemento que tenga su propiedad IsManipulation­Enabled definida en true aparecerá como las propiedades Source y OriginalSource en estos eventos de manipulación.

Además, cada una de las clases de argumento de evento asociadas con los eventos de manipulación incluye una propiedad llamada Manipulation­Container. Éste es el elemento dentro del cual se produce la manipulación multitoque. Todas las coordenadas en los eventos de manipulación se relacionan con este contenedor.

De manera predeterminada, la propiedad ManipulationContainer se define en el mismo elemento que las propiedades Source y OriginalSource (es decir, el elemento que se manipula, pero probablemente no es lo que desea. En general, no desea que el contenedor de manipulación sea el mismo que el elemento que se manipula, debido a que hay interacciones complicadas relativas a mover, escalar y girar dinámicamente el mismo elemento que proporciona información táctil. En lugar de eso, desea que el contenedor de manipulación sea un principal del elemento manipulado, o quizás un elemento que vaya más allá del árbol visual.

En la mayoría de los eventos de manipulación, la propiedad ManipulationContainer es get-only. La excepción es el primerísimo evento de manipulación que recibe un elemento. En ManipulationStarting tiene la oportunidad de cambiar ManipulationContainer para que haga algo más adecuado. En el proyecto SimpleManipulationDemo, este trabajo es una línea de código única:

args.ManipulationContainer = this;

En todos los eventos subsiguientes, ManipulationContainer será el elemento MainWindow en lugar del elemento de imagen, y todas las coordenadas se relacionarán con esa ventana. Esto funciona bien, porque la cuadrícula que contiene los elementos de imagen también está alineada con la ventana.

El resto del método OnManipulationStarting se dedica a traer el elemento de imagen tocado al primer plano al restablecer las propiedades adjuntas de Panel.ZIndex de todos los elementos de imagen de la cuadrícula. Es una manera sencilla de manejar ZIndex, pero probablemente no sea la mejor puesto que crea cambios repentinos.

ManipulationDelta y DeltaManipulation

El único otro evento del que se ocupa SimpleManpulationDemo es ManipulationDelta. La clase ManipulationDeltaEventArgs define dos propiedades de tipo ManipulationDelta. (Sí, el evento y la clase tienen el mismo nombre). Estas propiedades son DeltaManipulation y CumulativeManipulation. Tal como lo sugiere el nombre, DeltaManipulation refleja la manipulación que se produce desde el último evento ManipulationDelta, y CumulativeManipulation es la manipulación completa que comenzó con el evento ManipulationStarting.

ManipulationDelta tiene cuatro propiedades:

  • Translación de tipo Vector
  • Escala de tipo Vector
  • Expansión de tipo Vector
  • Rotación de tipo doble

La estructura Vector define dos propiedades llamadas X e Y de tipo doble. Una de las diferencias más importantes con la compatibilidad de la manipulación en Silverlight para Windows Phone 7 es la ausencia de las propiedades Expansión y Rotación.

La propiedad Traslación indica el movimiento (o una panorámica) en dirección horizontal y vertical. Un solo dedo sobre un elemento puede generar cambios en la traslación, pero ésta también puede ser parte de otras manipulaciones.

Las propiedades Escala y Expansión indican un cambio de tamaño (un zoom), que siempre requiere dos dedos. Escala es multiplicativa y Expansión es aditiva. Use Escala para definir una transformación de escala; use Expansión para aumentar o disminuir las propiedades Ancho y Altura de un elemento por unidades independientes del dispositivo.

En WPF 4.0, los valores X e Y del vector Escala siempre son los mismos. Los eventos de manipulación no brindan la información suficiente como para escalar un elemento anisotrópicamente (es decir, de manera distinta en dirección horizontal y vertical).

De manera predeterminada, Rotación también requiere dos dedos, a pesar de que verá después cómo habilitar la rotación con un solo dedo. En cualquier evento de ManipulationDelta en particular, se deben definir las cuatro propiedades. Un par de dedos pueden agrandar un elemento y, al mismo tiempo, girarlo y moverlo a otra ubicación.

La escala y la rotación siempre son relativas a un punto central en particular. Este centro también se proporciona en ManipulationDeltaEvent­Args en la propiedad llamada ManipulationOrigin de tipo Punto. Este origen se relaciona con el ManipulationContainer definido en el evento ManipulationStarting.

Su trabajo en el evento ManipulationDelta es modificar la propiedad Render­Transform del objeto manipulado según los datos delta en el siguiente orden: primero la escala, luego la rotación y, finalmente, la traslación. (En realidad, como los factores de escala horizontal y vertical son idénticos, puede cambiar el orden de las transformaciones de escala y giro y seguir obteniendo el mismo resultado).

El método OnManipulationDelta en la figura 3 muestra un enfoque estándar. El objeto Matrix se obtiene de MatrixTransform que se define en el elemento de imagen manipulado. Se modifica a través de llamadas a ScaleAt y RotateAt (ambos relativos a ManipulationOrigin) y Translate. Más que una clase, la matriz es una estructura, por lo que debe finalizar reemplazando el valor anterior en MatrixTransform con el nuevo.

Es posible variar un poco este código. Tal como se muestra, se escala alrededor de un centro con esta instrucción:

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

Esto es equivalente a la traslación al negativo del punto central, escalar y luego volver a realizar la traslación:

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

Asimismo, el método RotateAt se puede reemplazar con esto:

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

Las dos llamadas a Traslación adyacentes ahora se cancelan entre sí, por lo que el compuesto es:

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

Es probablemente un poco más eficiente.

La figura 4 muestra el programa SimpleManipulationDemo en acción.

Figure 4 The SimpleManipulationDemo Program

Figura 4 El programa SimpleManipulationDemo

¿Habilitación del contenedor?

Una de las características interesantes del programa SimpleManipulationDemo es que puede manipular simultáneamente dos elementos de imagen, o incluso más si tiene la compatibilidad de hardware y un número suficiente de dedos. Cada elemento de imagen genera su propio evento ManipulationStarting y su propia serie de eventos Manipulation­Delta. El código distingue de manera efectiva entre los diversos elementos de imagen por la propiedad Source de los argumentos del evento.

Por esta razón, es importante no establecer ninguna información de estado en los campos que implique que sólo se puede manipular un elemento a la vez.

La manipulación simultánea de varios elementos es posible porque cada uno de los elementos de imagen tiene su propia propiedad IsManipulationEnabled definida en true. Cada uno de ellos puede generar una serie única de eventos de manipulación.

Cuando se abordan por primera vez estos eventos de manipulación, en vez de eso es posible investigar la definición de IsManpulationEnabled en true sólo en la clase MainWindow o en otro elemento que sirva como contenedor. Esto es posible, pero de algún modo tiene menos fluidez en la práctica y no es tan poderoso. La única ventaja real es que no necesita definir la propiedad ManipulationContainer en el evento ManipulationStarting. El desorden viene después, cuando debe determinar el elemento que se manipula en las pruebas de acceso en los elementos secundarios mediante el uso de la propiedad ManipulationOrigin en el evento ManipulatedStarted.

Entonces debería almacenar el elemento que se manipula como un campo para usarlo en futuros eventos de ManipulationDelta. En este caso, es seguro almacenar información de estado en campos, porque sólo podrá manipular un elemento en el contenedor a la vez.

El modo Manipulación

Tal como vio, una de las propiedades fundamentales que hay que definir durante el evento ManipulationStarting es ManipulationContainer. Hay otro par de propiedades que son útiles para personalizar la manipulación particular.

Puede limitar los tipos de manipulación que puede realizar si inicializa la propiedad Mode con un miembro de la enumeración de Manipulation­Modes. Por ejemplo, si estuviese usando la manipulación únicamente para desplazarse de manera horizontal, es posible que quisiera limitar los eventos sólo a la traslación horizontal. El programa ManipulationModesDemo le permite definir el modo de forma dinámica mostrando una lista de elementos de RadioButton que enumera las opciones, tal como se muestra en la figura 5.

Figure 5 The ManipulationModeDemo Display

Figura 5 La visualización de ManipulationModeDemo

Por supuesto, RadioButton es uno de los diversos controles en WPF 4.0 que responde directamente al toque.

La rotación con un solo dedo

De manera predeterminada, necesita dos dedos para hacer girar un objeto. Sin embargo, si una foto real se encuentra en un escritorio real, puede ponerle el dedo en la punta y hacerla girar en círculos. La rotación se produce aproximadamente alrededor del centro del objeto.

Puede hacer esto con los eventos de manipulación si define la propiedad Pivot de ManipulationStartingEventArgs. De manera predeterminada, la propiedad Pivot es nula: habilite la rotación con un dedo definiendo esta propiedad en un objeto ManipulationPivot. La propiedad clave de
ManipulationPivot es Center, lo que puede considerar calcular como el centro del elemento que se manipula:

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

Pero este punto central debe ser relativo al contenedor de manipulación el que, en los programas que le he mostrado, es el elemento que controla los eventos. Realizar la traslación de ese punto central desde el elemento que se manipula al contenedor es fácil:

center = element.TranslatePoint(center, this);

También es necesario establecer otro fragmento de información. Si todo lo que especifica es un punto central, surge un problema cuando pone el dedo justo al centro del elemento: sólo un pequeño movimiento hará que el elemento se ponga a girar sin parar. Por esta razón, ManipulationPivot también tiene una propiedad Radius. No se producirá ninguna rotación si el dedo se encuentra dentro de las unidades de radio del punto central. El programa ManipulationPivotDemo define este radio como media pulgada:

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

Ahora un solo dedo puede realizar una combinación de rotación y traslación.

Más allá de lo básico

Lo que ha visto aquí son los aspectos básicos del uso de los eventos de manipulación de WPF 4.0. Por supuesto, hay algunas variaciones en estas técnicas que le mostraré en futuras columnas, así como también el poder de la inercia de manipulación.

Es posible que también desee echarle un vistazo al Kit de herramientas de superficie para la tecnología táctil de Windows, que brinda controles optimizados para toque para las aplicaciones. El control ScatterView en particular elimina la necesidad de usar los eventos de manipulación directamente para cosas básicas, como la manipulación de fotografías. Tiene algunos efectos y comportamientos llamativos que asegurarán que la aplicación se comporte tal como cualquier otra aplicación táctil.

Charles Petzoldha sido editor colaborador durante largo tiempo de MSDN Magazine. Actualmente escribe “Programming Windows Phone 7” que se publicará como libro electrónico de descarga gratuita durante el otoño de 2010. Una edición de vista previa actualmente se encuentra disponible a través de su sitio web, charlespetzold.com.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Doug Kramer, Robert Levy y Anson Tsao