Estilo de un toque

Exploración de soporte técnico multitoque en Silverlight

Charles Petzold

Descargar el código de muestra

Cada vez que visito el Museo Americano de Historia Natural de Nueva York, siempre paso por la Sala de los primates. Esta sala, con una gran colección de esqueletos y especímenes disecados, expone un panorama evolutivo del orden de los Primates, animales cuyo tamaño abarca desde musarañas, lémures y monos tití diminutos hasta chimpancés, monos de gran tamaño y humanos.

Lo que se destaca en esta exposición es una sorprendente característica común a todos los primates: la estructura ósea de la mano, lo que incluye el dedo pulgar oponible. El mismo conjunto de articulaciones y dedos que permitieron que nuestros ancestros y primos lejanos se agarraran de las ramas de los árboles y treparan por ellas permite que nuestra especie manipule el mundo en torno a nosotros y que construya objetos. Es posible que nuestras manos tengan su origen en las patas de los primates diminutos de hace decenas de millones de años, aunque éstas poseen un factor importante que nos distingue como humanos.

¿Tiene algo de sorprendente que de forma instintiva apuntemos objetos o incluso los toquemos en la pantalla del equipo?

En respuesta a este deseo humano de que nuestros dedos tengan una conexión más íntima con el equipo, también han evolucionado nuestros dispositivos de entrada. El mouse es estupendo para realizar las funciones de seleccionar y arrastrar, pero no sirve para bocetos de forma libre ni escritura a mano. El lápiz de la tableta gráfica nos permite escribir, pero con frecuencia es difícil manejarlo. Las pantallas táctiles son similares a los cajeros automáticos y a los quioscos de museos, pero generalmente están restringidas a señalar y presionar.

Creo que la tecnología conocida como “multitoque” representa un gran avance. La tecnología multitoque, como lo indica su nombre, va más allá de las pantallas táctiles del pasado en cuanto a que detecta varios dedos y esto marca una gran diferencia en los tipos de movimiento y gestos que se pueden transmitir a través de la pantalla. La tecnología multitoque ha evolucionado desde los dispositivos de entrada táctiles del pasado, pero al mismo tiempo sugiere un paradigma de entrada intrínsicamente diferente.

La tecnología multitoque probablemente ha sido más evidente en la transmisión de noticias por televisión, con mapas en grandes pantallas que un experto o un meteorólogo residente manipula. Microsoft ha estado explorando la tecnología multitoque de varias formas, desde el equipo Microsoft Surface del tamaño de una mesa de café hasta pequeños dispositivos como Zune HD, y esta tecnología también se ha vuelto bastante estándar en los smartphones.

Aunque Microsoft Surface puede responder a varios toques simultáneos con los dedos (e incluso contiene cámaras internas para ver objetos en transparencia), la mayoría de los otros dispositivos multitoque se limitan a un número reducido de toques. Varios responden sólo a dos dedos, o puntos de toque, como son denominados. (Usaré los términos “dedo” y “punto de toque” como sinónimos). Sin embargo, aquí actúa la sinergia: en la pantalla del equipo, dos dedos son dos veces más poderosos que uno.

La limitación de dos puntos de toque es una característica de los monitores multitoque que se han puesto a disposición recientemente para los equipos de escritorio y los equipos portátiles, así como también el equipo portátil Acer Aspire 1420P distribuido a los asistentes a la conferencia Microsoft Professional Developers Conference (PDC) realizada en noviembre del año pasado, que se conoce comúnmente como el equipo portátil PDC. La distribución del equipo portátil PDC proporcionó una oportunidad única para miles de desarrolladores de escribir aplicaciones compatibles con tecnología multitoque.

El equipo portátil PDC es el equipo que utilizo para explorar soporte técnico multitoque en Silverlight 3.

Eventos y clases de Silverlight

El soporte técnico multitoque se está transformando en un estándar en los distintos marcos y API de Windows. El soporte técnico se crea en Windows 7 y en Windows Presentation Foundation (WPF) 4. (El equipo Microsoft Surface también se basa en torno a WPF, pero incluye extensiones personalizadas para sus capacidades muy especiales).

Para este artículo, me gustaría centrarme en el soporte técnico multitoque en Silverlight 3. El soporte técnico se encuentra en sus inicios, pero indudablemente es adecuado y muy útil para explorar conceptos básicos de la tecnología multitoque.

Si publica una aplicación Silverlight multitoque en su sitio web, ¿podrá utilizarlo? Por supuesto, el usuario necesitará un monitor multitoque, pero también será necesario que ejecute la aplicación Silverlight en un sistema operativo y un explorador que admitan tecnología multitoque. Por ahora, Internet Explorer 8 que se ejecuta en Windows 7 proporciona este soporte técnico y, probablemente, más sistemas operativos y exploradores admitirán la tecnología multitoque en el futuro.

El soporte técnico de Silverlight 3 para tecnología multitoque consiste en cinco clases, un delegado, una enumeración y un solo evento. No existe forma de determinar si el programa Silverlight se ejecuta en un dispositivo multitoque o, en caso de hacerlo, cuántos puntos de toque admite el dispositivo.

Una aplicación Silverlight que desea responder a tecnología multitoque debe adjuntar un controlador al evento estático Touch.FrameReported:

Touch.FrameReported += OnTouchFrameReported;

Puede adjuntar este controlador de eventos en equipos que no cuenten con monitores multitoque y no sucederá nada malo. El evento FrameReported es el único miembro público de la clase estática Touch. El controlador es similar a lo siguiente:

void OnTouchFrameReported(
  object sender, TouchFrameEventArgs args) {
  ...
}

Puede instalar varios controladores de eventos Touch.FrameReported en su aplicación y todos ellos informarán a todos los eventos de toque en cualquier parte de la aplicación.

TouchFrameEventArgs tiene una propiedad pública denominada TimeStamp que no he tenido la oportunidad de usar y tres métodos públicos esenciales:

  • TouchPoint GetPrimaryTouchPoint(UIElement relativeTo)
  • TouchPointCollection GetTouchPoints(UIElement relativeTo)
  • void SuspendMousePromotionUntilTouchUp()

El argumento a GetPrimaryTouchPoint o GetTouchPoints se utiliza exclusivamente para entregar información de posición en el objeto TouchPoint. Puede usar un valor nulo para este argumento; la información de posicionamiento será en relación con la esquina superior izquierda de la aplicación Silverlight completa.

La tecnología multitoque admite varios dedos que toquen la pantalla y cada dedo que toque la pantalla (hasta el número máximo, que actualmente es dos) es un punto de toque. El punto de toque principal hace referencia al dedo que toca la pantalla cuando ningún otro dedo está tocándola y el botón del mouse no está presionado.

Toque la pantalla con un dedo. Ese es el punto de toque principal. Con el primer dedo aún tocando la pantalla, toque la pantalla con un segundo dedo. Obviamente, ese segundo dedo es un punto de toque principal. Pero ahora, con el segundo dedo aún en contacto con la pantalla, levante el primer dedo y vuelva a tocar la pantalla con éste. ¿Es ese un punto de toque principal? No, no es así. Un punto de toque principal se produce sólo cuando ningún otro dedo está tocando la pantalla.

Un punto de toque principal asigna el punto de toque que se promoverá al mouse. En aplicaciones de multitoque reales, debe tener cuidado de no depender del punto de toque principal, porque el usuario normalmente no asocia un significado específico al primer toque.

Los eventos se activan sólo para los dedos que realmente tocan la pantalla. No existe una detección de movimiento del mouse para los dedos que están muy cerca de la pantalla, pero que no la están tocando.

De manera predeterminada, la actividad que implica el punto de toque principal se promueve a distintos eventos del mouse. Esto permite que las aplicaciones existentes respondan a un toque sin ninguna codificación especial. Tocar la pantalla se transforma en un evento MouseLeftButtonDown, mover el dedo mientras aún esté tocando la pantalla se transforma en un evento MouseMove y levantar el dedo es un evento MouseLeftButtonUp.

El objeto MouseEventArgs que acompaña los mensajes del mouse incluye una propiedad denominada StylusDevice que ayuda a diferenciar los eventos del mouse de los eventos de lápiz y de toque. Mi experiencia con el equipo portátil PDC indica que la propiedad DeviceType es igual que TabletDeviceType.Mouse cuando el evento proviene del mouse y que TabletDeviceType.Touch, sin importar si la pantalla se toca con el dedo o el lápiz.

Sólo el punto de toque principal se promueve a los eventos del mouse y, como el nombre del tercer método de TouchFrameEventArgs lo sugiere, puede inhibir esa promoción. Más adelante hablaremos sobre esto.

Un evento Touch.FrameReported específico se podría activar basado en un punto de toque o varios puntos de toque. TouchPointCollection que se devuelve del método GetTouchPoints contiene todos los puntos de toque asociados a un evento específico. TouchPoint que se devuelve de GetPrimaryTouchPoint siempre constituye un punto de toque principal. Si no existe ningún punto de toque principal asociado al evento específico, GetPrimaryTouchPoint devolverá un valor nulo.

Incluso si TouchPoint que se devuelve de GetPrimaryTouchPoint no es un valor nulo, no será el mismo objeto que uno de los objetos TouchPoint devueltos de GetTouchPoints, aunque todas las propiedades serán las mismas si el argumento transmitido a los métodos es el mismo.

La clase TouchPoint define las siguientes cuatro propiedades get-only, todas respaldadas por propiedades de dependencia:

  • Action de tipo TouchAction, una enumeración con los miembros Down, Move y Up.
  • Position de tipo Point en relación con el elemento transmitido como un argumento al método GetPrimaryTouchPoint o GetTouchPoints (o en relación con la esquina superior izquierda de la aplicación para un argumento de nulo).
  • Size de tipo Size. La información de tamaño no se encuentra disponible en el equipo portátil PDC, de modo que nunca he trabajado con esta propiedad.
  • TouchDevice de tipo TouchDevice.

Puede llamar al método SuspendMousePromotionUntilTouchUp desde el controlador de eventos sólo cuando GetPrimaryTouchPoint devuelve un objeto no nulo y la propiedad Action sea igual a TouchAction.Down.

El objeto TouchDevice tiene dos propiedades get-only, también todas respaldadas por propiedades de dependencia:

  • DirectlyOver de tipo UIElement, el elemento superior por debajo del dedo.
  • Id de tipo int.

Es necesario que DirectlyOver no sea un elemento secundario del elemento transmitido a GetPrimaryTouchPoint o GetTouchPoints. Esta propiedad puede ser nula si el dedo se encuentra dentro de la aplicación Silverlight (según lo definan las dimensiones del objeto de complemento Silverlight), pero no dentro del área que abarca un control con prueba de acceso. (Los paneles con un pincel de fondo nulo no son paneles con prueba de acceso).

La propiedad ID es fundamental para distinguir entre varios dedos. Un serie específica de eventos asociados a un dedo especifico siempre empezará con Action de Down cuando el dedo toque la pantalla, seguido de eventos Move y finalizará con un evento Up. Todos estos eventos se asociarán con el mismo ID. (Sin embargo, no se debe suponer que un punto de toque principal tendrá un valor ID de 0 o 1).

La mayoría del código multitoque trivial hará uso de una colección Dictionary donde la propiedad ID de TouchDevice es la clave del diccionario. Así es cómo almacenará información para un dedo específico en todos los eventos.

Análisis de los eventos

Al explorar un nuevo dispositivo de entrada, siempre es útil escribir una pequeña aplicación que registre los eventos en la pantalla, de modo que pueda tener una idea de cuál es su aspecto. Entre el código descargable que viene adjunto a este artículo, hay un proyecto denominado MultiTouchEvents. Este proyecto consiste en dos controles TextBox en paralelo que muestran los eventos de multitoque para dos dedos. Si tiene un monitor multitoque, puede ejecutar este programa en charlespetzold.com/silverlight/MultiTouchEvents.

El archivo XAML consiste en sólo una Cuadrícula de dos columnas que contiene dos controles TextBox denominados txtbox1 y txtbox2. El archivo de código se muestra en la figura 1.

Figura 1 Código para MultiTouchEvents

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace MultiTouchEvents {
  public partial class MainPage : UserControl {
    Dictionary<int, TextBox> touchDict = 
      new Dictionary<int, TextBox>();

    public MainPage() {
      InitializeComponent();
      Touch.FrameReported += OnTouchFrameReported;
    }

    void OnTouchFrameReported(
      object sender, TouchFrameEventArgs args) {

      TouchPoint primaryTouchPoint = 
        args.GetPrimaryTouchPoint(null);

      // Inhibit mouse promotion
      if (primaryTouchPoint != null && 
        primaryTouchPoint.Action == TouchAction.Down)
        args.SuspendMousePromotionUntilTouchUp();

      TouchPointCollection touchPoints = 
        args.GetTouchPoints(null);

      foreach (TouchPoint touchPoint in touchPoints) {
        TextBox txtbox = null;
        int id = touchPoint.TouchDevice.Id;
        // Limit touch points to 2
        if (touchDict.Count == 2 && 
          !touchDict.ContainsKey(id)) continue;

        switch (touchPoint.Action) {
          case TouchAction.Down:
            txtbox = touchDict.ContainsValue(txtbox1) ? 
              txtbox2 : txtbox1;
            touchDict.Add(id, txtbox);
            break;

          case TouchAction.Move:
            txtbox = touchDict[id];
            break;
 
          case TouchAction.Up:
            txtbox = touchDict[id];
            touchDict.Remove(id);
            break;
        }

        txtbox.Text += String.Format("{0} {1} {2}\r\n", 
          touchPoint.TouchDevice.Id, touchPoint.Action, 
          touchPoint.Position);
        txtbox.Select(txtbox.Text.Length, 0);
      }
    }
  }
}

Observe la definición de diccionario en la parte superior de la clase. El diccionario realiza un seguimiento de qué TextBox está asociado a los dos ID de punto de toque.

El controlador OnTouchFrameReported empieza por inhibir toda la promoción del mouse. Ese es el único motivo para la llamada a GetPrimaryTouchPoint y, con bastante frecuencia, el único motivo por el cual se llamará a este método en un programa real.

Un bucle Foreach se enumera a través de los miembros TouchPoint de TouchPointCollection que se devuelve de GetTouchPoints. Debido a que el programa contiene sólo dos controles TextBox y es el único equipado para controlar dos puntos de toque, ignora cualquier punto de toque en que el diccionario ya tenga dos y en que ID no se encuentre en ese diccionario. (Del mismo que desea que su programa Silverlight compatible con multitoque controle varios dedos, no desea que éste se bloquee si encuentra demasiados dedos). ID se agrega al diccionario en un evento y se elimina del diccionario en un evento Up.

Observará que en ocasiones los controles TextBox se saturan con demasiado texto y que será necesario que seleccione todo el texto y lo elimine (Ctrl-A, Ctrl-X) para que el programa se vuelva a ejecutar correctamente.

Lo que observará en este programa es que la entrada de multitoque se captura en un nivel de aplicación. Por ejemplo, si presiona su dedo en la aplicación y después lo quita de la aplicación, esta última continuará recibiendo eventos Move y, finalmente, un evento Up cuando levante su dedo. De hecho, una vez que la aplicación obtiene alguna entrada de multitoque, se inhibe la entrada de multitoque a otras aplicaciones y el cursor del mouse desaparece.

Esta captura de entrada de multitoque centrada en la aplicación permite que la aplicación MultiTouchEvents sea muy segura de ella misma. Por ejemplo, en los eventos Move y Down, el programa simplemente supone que ID se encontrará en el diccionario. En una aplicación real, es posible que desee que sea más infalible en caso de que suceda algo extraño, pero siempre obtendrá el evento Down.

Manipulación con dos dedos

Uno de los escenarios multitoque estándar es una galería fotográfica que permite mover fotografías, cambiarles el tamaño y girarlas con los dedos. Dedicí probar algo similar, sólo con el propósito de conocer mejor los principios implicados, pero mucho más simple. Mi versión del programa tiene sólo un elemento para manipular, una cadena de texto de la palabra “TOUCH”. Puede ejecutar el programa TwoFingerManipulation en mi sitio web en charlespetzold.com/silverlight/TwoFingerManipulation.

Cuando codifique una aplicación para tecnología multitoque, probablemente siempre inhibirá la promoción del mouse para los controles compatibles con multitoque. Sin embargo, para que su programa se pueda utilizar sin un monitor multitoque, también agregará un procesamiento específico del mouse.

Si tiene sólo un mouse o un solo dedo, aún puede mover la cadena dentro del programa TwoFingerManipulation, pero puede cambiar sólo su posición: la operación gráfica conocida como traslación. Con dos dedos en una pantalla multitoque, también puede escalar el objeto y girarlo.

Cuando me senté a resolver el algoritmo que necesitaría para esta escala y rotación, pronto fue obvio que no existía sólo una solución.

Suponga que un dedo permanece fijo en el punto ptRef. (Todos los puntos aquí son en relación con una superficie de pantalla debajo del objeto que se manipula). El otro dedo se mueve desde el punto ptOld a ptNew. Como se muestra en la figura 2, puede usar estos tres puntos exclusivamente para calcular los factores de escala horizontal y vertical para el objeto.

Figura 2 Movimiento con dos dedos convertido a factores de escala

Por ejemplo, la escala horizontal es el aumento en la distancia de ptOld.X y ptNew.X desde ptRef.X o:

scaleX = (ptNew.X – ptRef.X) / (ptOld.X – ptRef.X)

La escala vertical es similar. Para el ejemplo de la figura 2, el factor de escala horizontal es 2 y el factor de escala vertical es ½.

Sin duda, esta es la forma más fácil de codificarlo. Sin embargo, el programa parece funcionar de forma más natural si los dos dedos también giran el objeto. Esto se muestra en la figura 3.

Figura 3 Movimiento con dos dedos convertido a rotación y escala

En primer lugar, se calculan los ángulos de los dos vectores, de ptRef a ptOld y de ptRef a ptNew. (El método Math.Atan2 es ideal para este trabajo). Luego, ptOld se gira en relación con ptRef por la diferencia en estos ángulos. Este ptOld girado luego se usa con ptRef y ptNew para calcular los factores de escala. Estos factores de escala son muchos menos porque se ha eliminado un componente de rotación.

El algoritmo real (implementado en el método ComputeMoveMatrix en el archivo C#) se vuelve bastante fácil. Sin embargo, el programa también necesitaba un grupo de código de soporte de transformación para compensar las deficiencias de las clases de transformación de Silverlight, que no tienen ninguna propiedad pública Value o multiplicación de matriz como en WPF.

En el programa real, ambos dedos se pueden mover al mismo tiempo y el control de la interacción entre los dos dedos es más simple de lo que parece inicialmente. Cada dedo que se mueve se controla de forma independiente usando el otro dedo como el punto de referencia. A pesar de la mayor complejidad del cálculo, el resultado parece más natural y creo que existe una explicación simple para esto: en la vida real, es muy común girar objetos con los dedos, pero es muy poco común escalarlos.

La rotación es tan común en el mundo real que tendría sentido implementarla cuando un objeto sea manipulado con un sólo dedo o el mouse. Esto se demuestra en el programa alternativo AltFingerManipulation (ejecutable en charlespetzold.com/silverlight/AltFingerManipulation). Para dos dedos, el programa funciona igual que TwoFingerManipulation. Para un dedo, calcula una rotación en relación con el centro del objeto y después usa cualquier movimiento en exceso lejos del centro para traslación.

Ajuste del evento con más eventos

En general, me gusta trabajar con clases que Microsoft proporciona cuidadosamente en un marco en lugar de empaquetarlas en mi propio código. Sin embargo, tenía en mente algunas aplicaciones multitoque que creía que se beneficiarían de una interfaz de evento más sofisticada.

Primero deseaba un sistema más modular. Deseaba combinar controles personalizados que controlaran su propia entrada de toque con controles existentes de Silverlight que simplemente permiten que una entrada de toque se convierta en una entrada de mouse. También deseaba implementar captura. Aunque la aplicación Silverlight misma captura el dispositivo multitoque, deseaba controles individuales para capturar de forma independiente un punto de toque específico.

Además deseaba los eventos Enter y Leave. En un sentido, estos eventos son lo opuesto a un paradigma de captura. Para entender la diferencia, piense en un teclado de piano en pantalla en que cada tecla sea una instancia del control PianoKey. Al principio, podría pensar en estas teclas como botones activados por mouse. En un evento down del mouse, la tecla del piano activa una nota y en un evento up del mouse, la desactiva.

Pero eso no es lo que desea para las teclas del piano. Desea poder arrastrar su dedo hacia arriba y hacia abajo del teclado para realizar efectos glissando. A las teclas realmente no les interesan los eventos Down y Up. En realidad, sólo les importan los eventos Enter y Leave.

WPF 4 y Microsoft Surface ya han enrutado eventos de toque y probablemente éstos se incorporen en Silverlight en el futuro. Pero pude satisfacer mis necesidades actuales con una clase que denominé TouchManager, implementada en el proyecto de biblioteca Petzold.MultiTouch en la solución TouchDialDemos. Una gran parte de TouchManager consiste en métodos estáticos, campos y un controlador estático para el evento Touch.FrameReported que le permite administrar eventos de toque en toda una aplicación.

Una clase que desea registrarse con TouchManager crea una instancia similar a lo siguiente:

TouchManager touchManager = new TouchManager(element);

El argumento de constructor es de tipo UIElement y, generalmente, será el elemento que cree el objeto:

TouchManager touchManager = new TouchManager(this);

Al registrarse con TouchManager, la clase indica que está interesada en todos los eventos multitoque en que la propiedad DirectlyOver de TouchDevice son secundarios del elemento transmitido al constructor TouchManager y que estos eventos de multitoque no se deben promover a los eventos del mouse. No existe ninguna manera de anular el registro de un elemento.

Después de crear una nueva instancia de TouchManager, una clase puede instalar controladores para los eventos denominados TouchDown, TouchMove, TouchUp, TouchEnter, TouchLeave y LostTouchCapture:

touchManager.TouchEnter += OnTouchEnter;

Todos los controladores se definen de acuerdo con el delegado EventHandler<TouchEventArgs>:

void OnTouchEnter(
  object sender, TouchEventArgs args) {
  ...
}

TouchEventArgs define cuatro propiedades:

  • Source de tipo UIElement, que es un elemento que originalmente se transmite al constructor TouchManager.
  • Position de tipo Point. Esta posición se encuentra en relación con Source.
  • DirectlyOver de tipo UIElement, que simplemente se copia del objeto TouchDevice.
  • Id de tipo int, también sólo se copia del objeto TouchDevice.

Sólo cuando se procesa el evento TouchDown, a una clase se le permite llamar al método Capture con la Id. de punto de toque asociada a ese evento:

touchManager.Capture(id);

Todas las entradas de toque posteriores para esa ID se dirigen al elemento asociado a esta instancia TouchManager hasta que el evento TouchUp o una llamada explícita llama a ReleaseTouchCapture. En cualquiera de los casos, TouchManager activa el evento LostTouchCapture.

Los eventos generalmente se encuentran en el siguiente orden: TouchEnter, TouchDown, TouchMove, TouchUp, TouchLeave y LostTouchCapture (si corresponde). Por supuesto, pueden existir múltiples eventos TouchMove entre TouchDown y TouchUp. Cuando un punto de toque no se captura, pueden existir varios eventos en el orden TouchLeave, TouchEnter y TouchMove, ya que el punto de toque sale de un elemento registrado e ingresa en otro.

El control TouchDial

Los cambios en los paradigmas de entrada de usuario con frecuencia necesitan que se cuestione antiguas suposiciones sobre el diseño adecuado de los controles y de otros mecanismos de entrada. Por ejemplo, pocos controles GUI están afianzados de forma tan sólida como la barra de desplazamiento y el control deslizante. Estos controles se utilizan para desplazarse por documentos o imágenes grandes, pero también como controles de volumen pequeños en los reproductores multimedia.

Cuando estaba considerando hacer un control de volumen en pantalla que respondiera al tacto, me preguntaba si el enfoque antiguo era realmente el enfoque correcto. En el mundo real, los controles deslizantes algunas veces se usan como controles de volumen, pero generalmente se restringen a paneles de mezcla de audio o ecualizadores gráficos profesionales. En el mundo, la mayoría de los controles de volumen son botones de sintonización. ¿Un botón de sintonización podría ser una mejor solución para un control de volumen habilitado para toque?

No pretendo tener la respuesta definitiva, pero les mostraré como construir uno.

El control TouchDial está incluido en la biblioteca Petzold.MultiTouch en la solución TouchDialDemos (para obtener detalles, consulte la descarga de código). TouchDial se deriva de RangeBase, de modo que puede sacar provecho de las propiedades Minimum, Maximum y Value, que incluye la lógica de coerción para mantener a Value dentro del intervalo Minimum y Maximum, y del evento ValueChanged. Pero en TouchDial, las propiedades Minimum, Maximum y Value todas son ángulos en unidades de grado.

TouchDial responde al mouse y al toque, y utiliza la clase TouchManager para capturar un punto de toque. Con el mouse o la entrada táctil, TouchDial cambia la propiedad Value durante un evento Move de acuerdo con la nueva ubicación y la ubicación anterior del mouse o del dedo en relación con un punto central. La acción es bastante similar a la figura 3, excepto que no hay implicada ninguna escala. El evento Move usa el método Math.Atan2 para convertir las coordenadas cartesianas en ángulos y después suma la diferencia entre los dos ángulos a Value.

TouchDial no incluye una plantilla predeterminada y, por lo tanto, no tiene ninguna apariencia visual predeterminada. Al usar TouchDial, su trabajo es proporcionar una plantilla, pero puede estar conformada por sólo algunos elementos. Es obvio que algo en esta plantilla debe girar probablemente de acuerdo con los cambios en la propiedad Value. Para su comodidad, TouchDial proporciona una propiedad get-only RotateTransform, en que la propiedad Angle es igual a la propiedad Value de RangeBase, y las propiedades CenterX y CenterY reflejan el centro del control.

En la figura 4 se muestra un archivo XAML con dos controles TouchDial que hacen referencia a un estilo y a una plantilla definidos como un recurso.

Figura 4 El archivo XAML para el proyecto SimpleTouchDialTemplate

<UserControl x:Class="SimpleTouchDialTemplate.MainPage"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:multitouch="clr-namespace:Petzold.MultiTouch;assembly=Petzold.MultiTouch">
  <UserControl.Resources>
    <Style x:Key="touchDialStyle" 
      TargetType="multitouch:TouchDial">
      <Setter Property="Maximum" Value="180" />
      <Setter Property="Minimum" Value="-180" />
      <Setter Property="Width" Value="200" />
      <Setter Property="Height" Value="200" />
      <Setter Property="HorizontalAlignment" Value="Center" />
      <Setter Property="VerticalAlignment" Value="Center" />
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="multitouch:TouchDial">
            <Grid>
              <Ellipse Fill="{TemplateBinding Background}" />
              <Grid RenderTransform="{TemplateBinding RotateTransform}">
                <Rectangle Width="20" Margin="10"
                  Fill="{TemplateBinding Foreground}" />
              </Grid>
            </Grid>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
  </UserControl.Resources>
    
  <Grid x:Name="LayoutRoot">
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
        
    <multitouch:TouchDial Grid.Column="0"
      Background="Blue" Foreground="Pink"
      Style="{StaticResource touchDialStyle}" />
        
    <multitouch:TouchDial Grid.Column="1"
      Background="Red" Foreground="Aqua"
      Style="{StaticResource touchDialStyle}" />
  </Grid>
</UserControl>

Observe que el estilo establece la propiedad Maximum en 180 y la propiedad Minimum en -180 para permitir que la barra gire en 180 grados a la izquierda y a la derecha. (Curiosamente, el programa no funcionó correctamente cuando cambié el orden de esas dos propiedades en la definición de estilo). El botón de sintonización consiste simplemente en una barra conformada de un elemento Rectangle dentro de un elemento Ellipse. La barra se encuentra dentro de una Cuadrícula de una sola celda, que tiene su RenderTransform enlazado a la propiedad RotateTransform calculada por TouchDial.

El programa SimpleTouchDialTemplate se muestra en ejecución en la figura 5.

Figura 5 El programa SimpleTouchDialTemplate

Puede probarlo (junto con los siguientes dos programas que analizaré en este documento) en charlespetzold.com/silverlight/TouchDialDemos.

Girar la barra con el mouse dentro del círculo es un poco difícil y se siente más natural con un dedo. Observe que puede girar la barra cuando presiona el botón primario del mouse (o toque la pantalla con su dedo) en cualquier parte dentro del círculo. Mientras gira la barra, puede alejar el mouse o el dedo porque ambos están capturados.

Si desea restringir que el usuario gire la barra, salvo que el mouse o el dedo esté presionado directamente sobre la barra, puede establecer la propiedad IsHitTestVisible de Ellipse en False.

Mi primera versión del control TouchDial no incluyó la propiedad RotateTransform. Tenía más sentido para mí que la plantilla pudiera incluir un RotateTransform explícito, en que la propiedad Angle fuera el destino de TemplateBinding en la propiedad Value del control. Sin embargo, en Silverlight 3, los enlaces no funcionan en las propiedades de clases que no se derivan de FrameworkElement, de modo que la propiedad Angle de RotateTransform no sea un destino de enlace (esto está corregido en Silverlight 4).

La rotación siempre está en referencia a un punto central y ese pequeño hecho complica el control TouchDial. TouchDial usa un punto central de dos formas: para calcular los ángulos que se muestran en la figura 3 y además para establecer las propiedades CenterX y CenterY de RotateTransform que crea. De manera predeterminada, TouchDial calcula ambos centros como la mitad de las propiedades ActualWidth y ActualHeight, que es el centro del control, pero existen varios casos en que eso no es lo que se desea.

Por ejemplo, en la plantilla de la figura 4, suponga que desea enlazar la propiedad RenderTransform de Rectangle a la propiedad RotateTransform de TouchDial. No funcionará correctamente porque TouchDial está estableciendo las propiedades CenterX y CenterY de RotateTransform en 100, pero el centro de Rectangle en relación a él mismo es realmente el punto (10, 90). Para permitirle invalidar estos valores predeterminados que TouchDial calcula a partir del tamaño del control, el control define las propiedades RenderCenterX y RenderCenterY. En la propiedad SimpleTouchDialTemplate, puede establecer estas propiedades en el estilo de la siguiente manera:

<Setter Property="RenderCenterX" Value="10" />
<Setter Property="RenderCenterY" Value="90" />

O bien, puede establecer estas propiedades en cero y establecer RenderTransformOrigin del elemento Rectangle para indicar el centro en relación a él mismo:

RenderTransformOrigin="0.5 0.5"

Es posible que también desee usar TouchDial en los casos en que el punto que se utiliza para referencia del movimiento del mouse o del dedo no se encuentre en el centro del control. En ese caso, puede establecer las propiedades InputCenterX e InputCenterY para invalidar los valores predeterminados.

En la figura 6 se muestra el archivo XAML del proyecto OffCenterTouchDial.

Figura 6 El archivo XAML de OffCenterTouchDial

<UserControl x:Class="OffCenterTouchDial.MainPage"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:multitouch="clr-namespace:Petzold.MultiTouch;assembly=Petzold.MultiTouch">
  <Grid x:Name="LayoutRoot">
    <multitouch:TouchDial Width="300" Height="200" 
      HorizontalAlignment="Center" VerticalAlignment="Center"
      Minimum="-20" Maximum="20"
      InputCenterX="35" InputCenterY="100"
      RenderCenterX="15" RenderCenterY="15">
      <multitouch:TouchDial.Template>
        <ControlTemplate TargetType="multitouch:TouchDial">
          <Grid Background="Pink">
            <Rectangle Height="30" Width="260"
              RadiusX="15" RadiusY="15" Fill="Lime"
              RenderTransform="{TemplateBinding RotateTransform}" />
            <Ellipse Width="10" Height="10"
              Fill="Black" HorizontalAlignment="Left"
              Margin="30" />
          </Grid>
        </ControlTemplate>
      </multitouch:TouchDial.Template>
    </multitouch:TouchDial>
  </Grid>
</UserControl>

Este archivo contiene un control único TouchDial, en que las propiedades se establecen en el control mismo y la propiedad Template se establece en una plantilla Control que contiene una Cuadrícula de una sola celda con Rectangle y Ellipse. Ellipse es un punto de pivote simbólico diminuto para Rectangle, que puede hacer girar hacia arriba o hacia abajo en 20 grados, como se muestra en la figura 7.


Figura 7 El programa OffCenterTouchDial

Las propiedades InputCenterX e InputCenterY siempre están en relación con el control completo, de modo que indican la ubicación del centro del elemento Ellipse dentro de la Cuadrícula rosa. Las propiedades RenderCenterX y RenderCenterY siempre están en relación con la parte del control al cual se aplica la propiedad RotateTransform.

Controles de volumen y PitchPipe

Los dos ejemplos anteriores demuestran cómo puede dar una apariencia visual a TouchDial al establecer la propiedad Template explícitamente en revisión o, si necesita compartir plantillas entre varios controles, al hacer referencia a ControlTemplate que se define como un recurso.

También puede derivar una nueva clase de TouchDial y utilizar el archivo XAML exclusivamente para establecer una plantilla. Este es el caso de RidgedTouchDial en la biblioteca Petzold.MultiTouch. RidgedTouchDial es lo mismo que TouchDial, salvo que tiene un tamaño y una apariencia visual específicos (lo que se explica más adelante).

También es posible usar TouchDial (o una clase derivada como RidgedTouchDial) dentro de una clase derivada de UserControl. La ventaja de este enfoque es que puede ocultar todas las propiedades definidas por RangeBase, que incluyen Minimum, Maximum y Value, y reemplazarlas por una nueva propiedad.

Este es el caso de VolumeControl. VolumeControl se deriva de RidgedTouchDial para su apariencia visual y define una nueva propiedad denominada Volume. La propiedad Volume está respaldada por una propiedad de dependencia y cualquier cambio a esa propiedad activa un evento VolumeChanged.

El archivo XAML para VolumeControl simplemente hace referencia al control RidgedTouchDial y establece varias propiedades, que incluyen Minimum, Maximum y Value:

<src:RidgedTouchDial 
  Name="touchDial"
  Background="{Binding Background}"
  Maximum="150"
  Minimum="-150"
  Value="-150"
  ValueChanged="OnTouchDialValueChanged" />

Por lo tanto, TouchDial puede girar 300 grados desde la posición mínima a la posición máxima. En la figura 8 se muestra VolumeControl.xaml.cs. El control traduce el intervalo de 300 grados del botón de sintonización a la escala logarítmica de decibeles de 0 a 96.

Figura 8 El archivo C# para VolumeControl

using System;
using System.Windows;
using System.Windows.Controls;

namespace Petzold.MultiTouch {
  public partial class VolumeControl : UserControl {
    public static readonly DependencyProperty VolumeProperty =
      DependencyProperty.Register("Volume",
      typeof(double),
      typeof(VolumeControl),
      new PropertyMetadata(0.0, OnVolumeChanged));

    public event DependencyPropertyChangedEventHandler VolumeChanged;

    public VolumeControl() {
      DataContext = this;
      InitializeComponent();
    }

    public double Volume {
      set { SetValue(VolumeProperty, value); }
      get { return (double)GetValue(VolumeProperty); }
    }

    void OnTouchDialValueChanged(object sender, 
      RoutedPropertyChangedEventArgs<double> args) {

      Volume = 96 * (args.NewValue + 150) / 300;
    }

    static void OnVolumeChanged(DependencyObject obj, 
      DependencyPropertyChangedEventArgs args) {

      (obj as VolumeControl).OnVolumeChanged(args);
    }

    protected virtual void OnVolumeChanged(
      DependencyPropertyChangedEventArgs args) {

      touchDial.Value = 300 * Volume / 96 - 150;

      if (VolumeChanged != null)
        VolumeChanged(this, args);
    }
  }
}

¿Por qué 96? Bueno, aunque la escala de decibeles se basa en números decimales (cada vez que la amplitud de una señal aumenta en un factor de 10, la sonoridad aumenta de forma lineal en 20 decibeles), también es verdadero que 10 elevado a 3 es aproximadamente 2 elevado a 10. Esto significa que cuando la amplitud se duplica, la sonoridad aumenta en 6 decibeles. Por lo tanto, si representa la amplitud con un valor de 16 bits, que es el caso del sonido de los CD y de los equipos, obtiene un intervalo de 16 bits por 6 decibeles por bit o 96 decibeles.

La clase PitchPipeControl también se deriva de UserControl y define una nueva propiedad denominada Frequency. El archivo XAML incluye un control TouchDial así como también un grupo de TextBlocks para mostrar las 12 notas de la octava. PitchPipeControl también hace uso de otra propiedad de TouchDial que aún no he analizado: si establece SnapIncrement en un valor distinto de cero en los ángulos, el movimiento del botón de sintonización no será suave, sino que saltará entre incrementos. Puesto que PitchPipeControl se puede establecer para las 12 notas de la octava, SnapIncrement se establece en 30 grados.

En la figura 9 se muestra el programa PitchPipe que combina VolumeControl y PitchPipeControl. Puede ejecutar PitchPipe en charlespetzold.com/silverlight/TouchDialDemos.

Figura 9 El programa PitchPipe

El programa Bonus

Anteriormente en este artículo, mencioné un control denominado PianoKey en el contexto de un ejemplo. PianoKey es un control real y uno de los muchos controles del programa Piano que puede ejecutar en charlespetzold.com/silverlight/Piano. El programa está diseñado para verse en una ventana maximizada del explorador. (O presione F11 para hacer que Internet Explorer vaya al modo Pantalla completa y obtenga incluso más espacio). Una representación muy diminuta se muestra en la figura 10. El teclado está dividido en agudos y bajos superpuestos. Los puntos rojos indican Do central.

Figura 10 El programa Piano

Es para este programa que escribí TouchManager porque el programa Piano usa toque en tres formas diferentes. Ya analicé VolumeControl azul, que captura el punto de toque en un evento TouchDown y libera la captura en TouchUp. Los controles de PianoKey que conforman los teclados también usan TouchManager, pero estos controles sólo se escuchan en los eventos TouchEnter y TouchLeave. De hecho, puede arrastrar sus dedos por las teclas para realizar efectos glissando. Los rectángulos marrón que funcionan como pedales de apoyo son controles comunes ToggleButton de Silverlight. Estos no están específicamente habilitados para toque; en lugar de eso, los puntos de toque se convierten en eventos de mouse.

El programa Piano demuestra tres formas diferentes de usar la tecnología multitoque. Creo que existen muchas otras formas más.

 

Charles Petzold ha sido editor colaborador durante largo tiempo de MSDN Magazine. Su libro más reciente es “The Annotated Turing: A Guided Tour through Alan Turing’s Historic Paper on Computability and the Turing Machine” (Wiley, 2008). Petzold mantiene un blog en su sitio web charlespetzold.com.

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