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.
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.
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
;
const
TWF_FINETOUCH = $1
;
TWF_WANTPALM = $2
;
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
;
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).
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.
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.
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.
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.
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 !
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.