Delphi et le multi-touch

Tutoriel sur l'implémentation des fonctions tactiles multi-touch dans une fiche Delphi à l'aide des API Windows.

2 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

I. Introduction

Depuis l'arrivée sur le marché de dalles tactiles et maintenant multi-touch (plusieurs points de contact), de nouvelles fonctionnalités ont également fait leur apparition dans l'environnement Windows.

Nous connaissons les fonctions gestuelles, bien implémentées sous Delphi depuis la version 2010, mais peu sur le multi-touch. Ce tutoriel a pour but de vous faire découvrir cette autre facette du tactile et de vous donner un maximum d'indications pour bien débuter.

II. Minimum requis

  • Un moniteur équipé d'une dalle multi-touch (deux contacts ou plus).
  • Windows 7 ou supérieur, 2008 R2 ou supérieur.
  • Les composants Tablet PC installés.
  • Le service Panneau de saisie Tablet PC démarré.

Ce tutoriel repose principalement sur l'utilisation des API Windows et par conséquent ne requiert pas de version précise de Delphi. Cependant et par souci de simplification du code, l'exemple proposé en fin de document n'est compilable qu'avec Delphi 2009 ou supérieur.

III. Le tactile

Comme nous l'avons déjà signalé, deux notions s'affrontent :

  • la « gestuelle » qui en traçant une ligne plus ou moins complexe au doigt ou au stylet va exécuter une commande (un raccourci) ;
  • le « multi-touch » qui à l'aide de plusieurs doigts va permettre l'interaction avec des objets ; rotation, zoom, etc.

Mais pourquoi s'affrontent-elles me direz-vous ? Simplement parce qu'elles sont mutuellement exclusives. Entendez par là qu'il n'est pas possible d'utiliser les deux notions sur une même fiche au même instant !

IV. Principe

IV-A. Compatibilité

La première chose à faire est bien sûr de s'assurer que l'OS et le matériel sont compatibles multi-touch. Nous lirons pour cela la variable système SM_DIGITIZER à l'aide de GetSystemMetrics et appliquerons au résultat un masque de contrôle.

Ce masque peut contenir une ou plusieurs des constantes ci-dessous :

NID_INTEGRATED_TOUCH
$01
Digitizer intégré disponible.
NID_EXERNAL_TOUCH
$02
Digitizer externe disponible.
NID_INTEGRATED_PEN
$04
Stylet intégré disponible.
NID_EXERNAL_PEN
$08
Stylet externe disponible.
NID_MULTI_INPUT
$40
Support du multi-touch.
NID_READY
$80
État prêt.

Dans le cas d'une Tablette, le résultat renvoyé sera NID_INTEGRATED_TOUCH, NID_MULTI_INPUT et NID_READY ($C1).

IV-B. Activation

Par défaut, une fenêtre Windows reçoit les informations gestuelles. Pour recevoir les informations concernant le multi-touch, elle doit en faire la demande en s'enregistrant à l'aide de RegisterTouchWindow.

La fonction RegisterTouchWindow requiert deux paramètres : le handle de la fenêtre cible et un masque de bits définissant la « granularité » de la fonction.

La granularité s'exprime par deux constantes :

TWF_FINETOUCH
$1
Spécifie que tous les contacts doivent être notifiés.
Si non définie, des contacts considérés par le système comme redondants (identiques) seront ignorés, ou plutôt regroupés.
TWF_WANTPALM
$2
Spécifie que les contacts de la paume de la main doivent être notifiés.
Si non définie, un contact de la paume sera considéré comme involontaire et donc ignoré. Cet algorithme de contrôle est cependant gourmand en temps et une certaine latence peut se faire sentir.

IV-C. Notification

Les notifications se font par message WM_TOUCH ($240).

Le mot de poids faible de WPARAM représente le nombre de contacts à traiter (les contacts survenus dans un même laps de temps ne génèrent qu'un seul message WM_TOUCH). Le mot de poids fort est réservé.

LPARAM représente un handle nous donnant accès aux informations proprement dites. GetTouchInputInfo est utilisée pour remplir un tableau avec les informations de chaque contact.

Une donnée de contact contient :

X,Y La position du contact en coordonnées écran (et non fenêtre) au 1/100e de pixel.
hSource Un handle sur la source ; le digitizer.
dwID L'identificateur du contact.
Cette valeur est arbitraire et ne représente nullement l'ordre dans lequel les événements sont survenus. Lorsqu'un contact est relâché, le système peut réutiliser la même valeur pour un contact futur.
dwFlags Masque de bits représentant l'action effectuée (down, move, up) ainsi que d'autres informations de bas niveau. Voir ci-dessous.
dwMask Masque de bits spécifiant les données valides en fonction du type de digitizer. Voir ci-dessous.
dwTime L'instant auquel le contact est survenu.
dwExtraInfo Des informations supplémentaires si nécessaires, généralement propres au digitizer.
cxContact, cyContact Les largeur/hauteur du point de contact ; la détection de la pression.

dwTime, dwExtraInfo et cxContact/cyContact ne contiennent pas forcément des données valides. DwMask donne la liste des fonctionnalités supportées qui dépendent des caractéristiques du digitizer.

Le masque dwFlags peut contenir une ou plusieurs de ces constantes :

TOUCHEVENTF_MOVE
$0001
Déplacer.
TOUCHEVENTF_DOWN
$0002
Presser.
TOUCHEVENTF_UP
$0004
Relâcher.
TOUCHEVENTF_INRANGE
$0008
Utilisée lors du survol (hovering). Ne fonctionne que sur certains matériels.
TOUCHEVENTF_PRIMARY
$0010
Contact primaire, le premier contact correspondant toujours au curseur de la souris.
TOUCHEVENTF_NOCOALESCE
$0020
Le message est identique au précédent, mais ils n'ont pas été regroupés.
TOUCHEVENTF_PALM
$0080
Le message a été généré par la paume de la main.

Le masque dwMask peut contenir une ou plusieurs de ces constantes :

TOUCHINPUTMASKF_CONTACTAREA
$0004
cxContact et cyContact contiennent des données valides.
TOUCHINPUTMASKF_EXTRAINFO
$0002
dwExtraInfo contient une donnée valide.
TOUCHINPUTMASKF_TIMEFROMSYSTEM
$0001
dwTime contient une donnée valide.

Enfin, il est de notre responsabilité de libérer la mémoire allouée une fois ces informations traitées à l'aide de la fonction CloseTouchInputHandle.

Le handle est également invalidé si le message est transféré par un appel à SendMessage, PostMessage ou fonctions dérivées ! Si tel est le cas, un appel subséquent à GetTouchInputInfo échouera.

IV-D. Désactivation

Lorsque le multi-touch n'est plus requis, on appellera simplement UnregisterTouchWindow en lui passant le handle de la fenêtre.

V. Implémentation de base

V-A. Déclarations et structures

En fonction de la version de Delphi, ces fonctions peuvent déjà être définies. Nous les répétons cependant ici.

La notation delayed (chargement différé) est utilisée et permet sur les versions récentes de Delphi de ne charger effectivement la DLL que lorsqu'une procédure/fonction est invoquée. Avec les versions plus anciennes, il sera nécessaire d'utiliser les fonctions LoadLibrary et GetProcAddress.

Exports
Sélectionnez
  function RegisterTouchWindow(aWnd :hWnd; aFlags :ulong):BOOL; stdcall; external 'user32.dll' delayed;
  function UnregisterTouchWindow(aWnd :hWnd):BOOL; stdcall; external 'user32.dll' delayed;
  function GetTouchInputInfo(aHandle :THandle; aCount :integer; aInputs :PTouchInput; aSize :integer):BOOL; stdcall; external 'user32.dll' delayed;
  function CloseTouchInputHandle(aHandle :THandle):BOOL; stdcall; external 'user32.dll' delayed;

En fonction de la version de Delphi, certaines de ces constantes seront déjà définies. Nous les répétons cependant ici.

Disponibilité
Sélectionnez
const
  SM_DIGITIZER           = 94;

  NID_INTEGRATED_TOUCH   = $01;
  NID_EXERNAL_TOUCH      = $02;
  NID_INTEGRATED_PEN     = $04;
  NID_EXERNAL_PEN        = $08;
  NID_MULTI_INPUT        = $40;
  NID_READY              = $80;
Granularité
Sélectionnez
const
  TWF_FINETOUCH          = $1;
  TWF_WANTPALM           = $2;
Notification
Sélectionnez
Const
  WM_TOUCH               = $240;

  TOUCHEVENTF_MOVE       = $0001;
  TOUCHEVENTF_DOWN       = $0002;
  TOUCHEVENTF_UP         = $0004;
  TOUCHEVENTF_INRANGE    = $0008;
  TOUCHEVENTF_PRIMARY    = $0010;
  TOUCHEVENTF_NOCOALESCE = $0020;
  TOUCHEVENTF_PALM       = $0080;
Définition d'un contact
Sélectionnez
type
  TTouchInput = record
    x         :longInt;
    y         :longInt;
    Source    :THandle;
    ID        :dword;
    Flags     :dword;
    Mask      :dword;
    Time      :dword;
    ExtraInfo :PLongInt;
    xContact  :dword;
    yContact  :dword;
  end;

V-B. S'assurer que le système est compatible et actif

Nous interrogerons pour cela la variable système SM_DIGITIZER à l'aide de GetSystemMetrics et lui appliquerons un masque correspondant à l'état prêt (NID_READY) et support multi-touch (NID_MULTI_INPUT).

Contrôle du support multi-touch
Sélectionnez
function IsMultiTouch :boolean;
const
  Mask = NID_MULTI_INPUT or NID_READY;

begin
  Result := GetSystemMetrics(SM_DIGITIZER) and Mask = Mask;
end;

Nous savons déjà que le multi-touch n'est disponible qu'à partir de Windows 7 ou 2008 R2. Nous n'avons cependant pas besoin de tester les versions antérieures puisque tout OS ou matériel non compatible renverra faux au test ci-dessus !

V-C. Définir le gestionnaire de messages

WM_TOUCH étant une constante, nous avons le choix entre définir une méthode de message ou surcharger WndProc. Nous utiliserons ici la première possibilité. Voici l'implémentation de base de cette méthode.

Gestion de WM_TOUCH
Sélectionnez
Interface

TForm1 = class(TForm)
protected
  procedure WMTouch(var Message :TMessage); message WM_TOUCH;
end;

implementation

procedure TForm1.WMTouch(var Message: TMessage);
var
  Count  :integer;               //Nombre de contacts
  Inputs :array of TTouchInput;  //Tableau de contacts
  Pt     :TPoint;                //Point en coordonnées fiche en pixel
  i      :integer;

begin
  //Récupère le nombre de contacts disponibles
  Count := LoWord(Message.WParam);

  //Fixe la taille du tableau
  SetLength(Inputs, Count);

  //et le remplit
  if GetTouchInputInfo(Message.LParam, Count, @Inputs[0], SizeOf(TTouchInput)) then
  begin

    //Traite chaque contact
    for i := 0 to high(Inputs) do
    begin
      //Le digitizer envoie le point au 1/100 de pixel et en coordonnées écran !
      //Conversion en pixel et coordonnées fiche
      Pt := Point(Inputs[i].x div 100, Inputs[i].y div 100);
      Pt := ScreenToClient(Pt);

      //Presser
      if (Inputs[i].Flags and TOUCHEVENTF_DOWN <> 0) then
      begin
        //Code pour gérer l'appui
      end

      //Déplacer
      else if (Inputs[i].Flags and TOUCHEVENTF_MOVE <> 0) then
      begin
        //Code pour gérer le déplacement 
      end

      //Relâcher
      else if (Inputs[i].Flags and TOUCHEVENTF_UP <> 0) then
      begin
        //Code pour gérer le relâchement
      end;
    end;
  end;

  //Libération de la mémoire associée
  CloseTouchInputHandle(Message.LParam);

  Message.Result := 0;
end.

V-D. Démarrage et arrêt

Nous allons demander à recevoir les notifications dès le démarrage du programme. On pourrait cependant le faire par l'appui sur un bouton.

Démarrage et arrêt
Sélectionnez
Interface

TForm1 = class(TForm)
  procedure FormCreate(Sender: TObject);
  procedure FormDestroy(Sender: TObject);
end;

implementation

procedure TForm1.FormCreate(Sender: TObject);
begin
  if IsMultiTouch then
    RegisterMultiTouch(Handle, 0);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  if IsMultiTouch then
    UnregisterMultiTouch(Handle);
end;

Voilà, les bases sont posées et votre fiche devrait maintenant supporter le multi-touch !

VI. Multi-touch et la souris

Notre application telle quelle est-elle utilisable à la souris ? Hé bien la réponse est non ! La souris ne génère pas de message WM_TOUCH, seuls les doigts et les stylets le font. Il sera donc nécessaire de définir les événements standards de la fiche pour assurer la compatibilité.

Par contre pour que les applications créées pour une utilisation à la souris soient utilisables en tactile, l'OS convertit systématiquement les informations reçues du digitizer en messages souris et les envoie en parallèle. C'est donc le cas pour les messages WM_LBUTTONxxx et WM_RBUTTONxxx si l'option Press&Hold est activée (elle l'est par défaut).

Vous voyez certainement où je veux en venir : le tactile entraînant le message WM_TOUCH et WM_LBUTTONxxx, l'information du contact primaire (celui qui représente le curseur) et seulement celle-là sera traitée deux fois ! Une fois par le gestionnaire tactile et une fois par l'événement OnMousexxx.

Fort heureusement, il est possible de tester la provenance du message depuis le gestionnaire de souris et ainsi d'exécuter ou non son code. La fonction nécessaire pour cela est GetMessageExtraInfo. Nous allons de ce pas définir une méthode facilitant l'écriture des trois événements OnMouseDown, OnMouseMove et OnMouseUp.

Détection du digitizer dans le gestionnaire de souris
Sélectionnez
Interface

const
  MOUSEEVENTF_FROMTOUCH = $FF515700;

TForm1 = class(TForm)
protected
  function  IsFromTouch(aExtraInfo:integer) :boolean;
public
  procedure FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
end;

implementation

function TForm1.IsFromTouch(aExtraInfo:integer) :boolean;
begin
  Result := IsMultiTouch and (aExtraInfo and MOUSEEVENTF_FROMTOUCH = MOUSEEVENTF_FROMTOUCH);
end;

procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
if not IsFromTouch(GetMessageExtraInfo) then
  //Traitement
end;

VII. Désactivation de la fonction Press&Hold

Press&Hold est une fonctionnalité Windows permettant de générer un clic droit en conservant le doigt appuyé pendant quelques secondes sur l'écran (le fameux cercle gris sous Windows 7).

Dans notre application, cette fonction est gênante puisqu'elle ne permet pas le déplacement d'un point. Nous devons donc la désactiver !

Pour cela, il suffit d'ajouter une entrée MicrosoftTabletPenServiceProperty à la liste des propriétés (caractéristiques) de la fenêtre et de lui assigner la valeur TABLET_DISABLE_PRESSANDHOLD. La fonction utilisée pour cela est SetProp.

Cette propriété correspond à une entrée dans la table d'atomes. SetProp accepte en paramètre un pointeur sur la chaîne MicrosoftTabletPenServiceProperty ou un pseudopointeur contenant dans le mot de poids faible le numéro atomique de la chaîne. La première variante est utilisée ici.

Désactiver Press&Hold
Sélectionnez
type
  TForm1 = class(TForm)
  protected
    procedure DisablePressAndHold;
  end;

implementation

procedure TForm1.DisablePressAndHold;
var
  Atom :TAtom;

const
  TabletAtom = 'MicrosoftTabletPenServiceProperty';
  TABLET_DISABLE_PRESSANDHOLD = $00000001;

begin
  Atom := GlobalAddAtom(tabletAtom);

  if Atom <> 0 then
  begin
    SetProp(Handle, tabletAtom, TABLET_DISABLE_PRESSANDHOLD);
    GlobalDeleteAtom(Atom);
  end;
end;

VIII. Limitations du multi-touch

Hé oui puisque rien n'est jamais parfait, voici quelques limitations.

  • Il est impossible de capturer le message WM_TOUCH avec un hook ! Ne me demandez pas pourquoi, je ne le sais pas !
  • Windows n'autorise qu'une seule fenêtre à détenir la focalisation. Il n'est donc pas possible de déplacer ou plus globalement d'agir sur deux fenêtres en même temps, qu'elles appartiennent à la même application ou pas.
  • Il y a toujours un seul curseur. L'utilisation conjointe du doigt et de la souris n'est pas possible. En fait, il faudrait appuyer plusieurs doigts, relâcher le premier (libération du contact primaire) pour que la souris puisse récupérer le contrôle du curseur !

IX. Exemple concret

Cette démo consiste à appliquer des transformations à une forme : translation, rotation et déformation.

Dû à l'utilisation d'un TDictionary pour la mémorisation des différents points de contact et de méthodes de record, Delphi 2009 ou supérieur est requis pour la compilation.

IX-A. Déclarations supplémentaires

Différentes structures et classes propres à ce programme ont été ajoutées :

TPointF Identique à un TPoint mais avec X,Y en valeurs flottantes. Permet d'éviter des erreurs d'arrondi, spécialement en rotation !
TShape La forme de travail dessinée à l'écran. Elle est représentée par quatre poignées (Handles) permettant les transformations.
TContactInfo Informations nécessaires au bon fonctionnement de ce programme. Dépendent des données lues par GetTouchInputInfo.
TContactList Classe dérivée de TDictionary contenant la liste des contacts en cours. Les clés correspondent aux identificateurs de contact et les valeurs pointent sur les TContactInfo correspondantes.
TContact TPair correspondant à un élément de TContactList. Est utilisée dans les boucles d'énumération.

IX-B. Fonctionnement

Le gestionnaire tactile et les événements souris transfèrent leurs appels à des méthodes de traitement communes : AddEvent, MoveEvent et RemoveEvent. La seule différence est que les événements souris utilisent toujours l'identificateur 0.

La transformation à appliquer est déterminée par la fonction CheckAction. Elle est appelée depuis MoveEvent.

IX-C. Utilisation

La transformation appliquée dépend du nombre de points de contact et de leur position (sur des poignées ou non) :

  • un contact sur une poignée entraîne une translation ;
  • deux contacts dont un sur une poignée entraînent une rotation. Le contact primaire, obligatoirement hors poignée, est le centre de rotation ;
  • deux contacts ou plus sur des poignées entraînent une déformation ;
  • un ou plusieurs contacts hors poignée restent sans effet !

Un test à la souris ne permettra que des translations !

IX-D. Codage

Et voici enfin le code complet !

Programme d'exemple
CacherSélectionnez

X. Conclusion

Voilà, ce tutoriel est maintenant terminé !

J'espère que vous aurez pris plaisir à le parcourir et peut-être vous aura-t-il donné des idées pour votre prochaine interface utilisateur.

La source de l'exemple accompagnée du programme compilé sont disponibles au format zip :
Delphi-et-le-multitouch.zip.

XI. Remerciements

Un grand merci à l'équipe Delphi et particulièrement Ero-Sennin, Evarisnea et Tourlourou pour leurs conseils et encouragements.

Un remerciement particulier à f-leb et ClaudeLELOUP pour la relecture orthographique.

XII. Références

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2012 Andnotor. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.