Introduction
Constitution de la solution
Affichage de historyListBox
La classe ListBoxEx
Accès à l’historique
Filtrage des URLs par des expressions régulières
Utilisation d’un thread pour l’affichage de l’historique
Remplissages de formulaires Web
Accès au document HTML
Configuration de la barre d’outils
Conclusion
Introduction
Voici le dernier article consacré à la création de notre barre d’outils pour
Internet Explorer (IE). Rappelons rapidement l’objectif de notre projet :
il s’agit de créer une barre d’outils intégrée au navigateur Web IE, spécialement
conçue pour les TabletPC. L’utilisation de notre barre est en effet possible
exclusivement avec le stylet. Toutes les facilités sont apportées pour que l’utilisateur
puisse entrer une URL au plus vite avec son seul stylet, et il est également
possible de remplir en quelques gestes des formulaires Web. Tous les détails
de nos objectifs sont présents dans le
premier
article. Tout le développement est réalisé en .NET. Voici une copie d’écran
du résultat final :

Nous avons déjà expliqué comment implémenter une barre d’outils
pour IE et comment créer un fichier d’installation (article 1).
Dans les deux derniers articles, nous avons présenté la réalisation de l’IHM,
dont les éléments ont été regroupés dans une dll (voir articles 2
et 3).
Il nous reste maintenant à rassembler tous les morceaux et à écrire la logique
de notre barre. C’est cette dernière partie que nous allons décrire ici. Voici
d’ores et déjà le fichier d’installation de la version finale de notre barre
(attention, il s’agit d’une nouvelle version par rapport à celle proposée
dans le premier article) : téléchargez-le ici,
et voici le fichier d’aide associé. Les codes sources
complets, les projets, la solution et les fichiers annexes sont disponibles
ici. Notez que le fichier de signature
« inktoolbar for ie.snk » n’est pas celui avec lequel nous avons
compilé le fichier d’installation. Cela ne pose cependant aucun problème,
les paramètres de l’installation (voir la fin du premier
article) ayant été modifiés.
Nous allons tout d’abord présenter rapidement le squelette
de notre projet, puis nous décrirons les points techniques les plus délicats.
Constitution de la solution
Notre solution « InkToolBar for IE.sln »
est composée de quatre projets :
- « BandObjectLib » qui réalise l’interop entre COM et .NET pour
les opérations d’implémentation d’une barre d’outils en C#. Il est en compilé
en une dll nommée « BandObjectLib.dll » qui exporte la classe BandObject.
Cette partie a été décrite dans le premier article.
- « Setup » qui constitue le projet de déploiement.
Ce projet a également été décrit dans le premier article. La compilation de
ce projet résulte en un fichier d’installation « setup.msi » qui
est le fichier final de notre travail.
- « InkToolBarControls » qui, une fois compilé,
devient « InkToolBarControls.dll ». Ce fichier contient 3 contrôles,
décrits dans les articles 2
et 3.
- « InkToolBar for IE » est le projet principal.
Il est constitué d’une classe principale InkToolBar qui dérive de BandObject.
InkToolBar est donc un UserControl qui expose,
en tant qu’héritier de BandObject, toutes les méthodes qui en font
une barre d’outils. Nous ne reviendrons pas là-dessus sur BandObject.
Nous avons utilisé nos contrôles de InkToolBarControls
pour réaliser l’interface graphique à l’aide de. Nous avons implémenté toute
la logique de la barre dans le fichier « InkToolBar for IE.cs ».
Le fichier « ListBox.cs » contient un contrôle personnalisé très
particulier que nous n’avons pas souhaité partager dans un assembly séparé.
« ConfigurationForm.cs » est la boîte de dialogue de configuration
de la barre d’outils. Enfin, « Configuration.cs » contient la classe
utilisée pour la sérialisation en fichier XML des données de configuration.
Nous allons dans la suite de cet article exposer les plus
importants des problèmes que nous avons rencontrés lors de la réalisation
du projet « InkToolBar for IE ». Nous ne pouvons pas expliquer tout
le fonctionnement, ce serait bien trop long, et une bonne partie du code se
passe d’explications plus détaillées que les commentaires des sources.
Le design de la barre s’est fait simplement dans VS par glisser/déposer :
Voici le rôle de chaque contrôle :
| N° de l’élément |
Classe |
Nom |
Fonction |
Remarques |
| 4 |
InkInputEx
|
inkInput |
Pour
la saisie manuscrite ou au clavier de l’URL ou des expressions à rechercher
|
Cette
zone de saisie s’agrandit automatiquement lorsque l’utilisateur approche
le stylet. En outre, elle a l’avantage, par rapport à une InkEdit
classique, de posséder une propriété Strokes publique. Ce contrôle
a fait l’objet d’un article
détaillé. |
| 5 |
ListBoxEx |
historyListBox |
Pour
l’affichage de l’historique correspondant aux premières lettres entrées
dans la zone inkInput |
ListBoxEx
dérive de ListBox. Nous avons écrit cette classe pour des
raisons expliquées plus loin dans cet article. |
| 1, 2, 3 |
CheckBoxEx |
httpCheckBox
httpsCheckBox
wwwCheckBox |
Pour
ajouter des préfixes (http, https, www) à l’adresse entrée |
Les
images des boutons sont ajoutées dans le code, comment expliqué dans
cet
article. |
| 6, 7, 8,9 |
FlatIEButton |
button1
... |
Pour
lancer la navigation en complétant automatiquement l’adresse |
| 10 |
FlatIEButton |
okButton |
Pour
lancer la navigation sans extension |
| 11 |
FlatIEButton |
searchButton |
Pour
lancer une recherche avec son moteur de recherche favori |
| 12 |
ContextMenu |
paraButton |
Pour
accéder à la configuration de la barre et aux champs pour le remplissage
assisté des formulaires Web |
|
Il existe un autre contrôle, dissimulé sous historyListBoxEx :
il s’agit d’un FlatIEButton permettant de faire apparaître le menu
de la barre. Ce bouton est la plupart du temps accessible, car historyListBox
n’est visible que lorsque un historique doit apparaître, c’est-à-dire lorsque
l’utilisateur saisit une URL. Nous allons justement voir maintenant comment
faire apparaître puis disparaître cette ListBox.
Affichage de historyListBox
Notre contrôle responsable de l’affichage
de la liste de l’autocomplétion historyListBox doit apparaître lorsque
nécessaire, doit pouvoir dépasser de notre barre d’outils, et doit enfin ‘suivre’
la taille de la zone de saisie.
|
| Taille réduite (saisie au clavier
ou après reconnaissance des caractères manuscrits et réduction de la
zone d’écriture) |
|
|
| Grande taille (lors d’une saisie
manuscrite) |
Faire apparaître et disparaître historyListBox se
fait simplement grâce à ses propriétés Show() et Hide(). Pour
que la liste puisse sortir de la barre d’outils, et venir couvrir une autre
zone de IE, il faut changer son parent. Le parent par défaut de historyListBox
est évidement la barre d’outils. Si son parent devient IE lui-même, la liste
pourra apparaître n’importe où sur la surface de la fenêtre du navigateur.
Pour réaliser ce changement de parent, nous devons faire appel à une fonction
de l’API Win32 : SetParent. Nous déclarons donc SetParent dans
la classe de la barre d’outils, ainsi que GetParent, qui retourne le
parent d’une fenêtre donnée (rappelons que tout contrôle est considéré comme
une fenêtre par le système) :
// Import d'une fonction de l'API qui
change le parent d'un contrôle
[DllImport("user32")]
private static extern
bool SetParent(int
hWndChild, int hWndNewParent);
// Import d'une fonction de l'API qui
renvoie le parent d'un contrôle
[DllImport("user32")]
private static extern
int GetParent(int
hWnd);
Nous utilisons ces fonctions dans les deux méthodes HistoryDisplay()
et HistoryHide() :
private void
HistoryDisplay()
{
// Si la ListBox est cachée et qu'elle n'est pas vide
if((!historyListBox.Visible) &&
(historyListBox.Items.Count != 0))
// Si le Parent de la ListBox n'est pas IE (c'est InkToolBar)
if(GetParent(this.historyListBox.Handle.ToInt32())
!=
Explorer.HWND)
{
// On change le Parent de la ListBox en IE
SetParent(this.historyListBox.Handle.ToInt32(),
Explorer.HWND);
// On change la position relative de la ListBox
// (-> elle ne
bouge pas à l'écran)
this.historyListBox.Top += this.Top;
this.historyListBox.Left
+= this.Left;
// On affiche la ListBox
this.historyListBox.Show();<
}
}
private void
HistoryHide()
{
// On change le Parent de la ListBox en IE
SetParent(this.historyListBox.Handle.ToInt32(),
this.Handle.ToInt32());
// On change la position relative de la ListBox
// (-> elle ne bouge pas à l'écran)
this.historyListBox.Top = this.inkInput.Top
+ this.inkInput.Height;
this.historyListBox.Left
= this.inkInput.Left;
// On
cache la ListBox
this.historyListBox.Hide();
}
Notez que la position d’un contrôle est calculée par rapport
à son parent, ce qui nous oblige à modifier les propriétés Top et Left
de historyListBox lorsqu’on change son parent.
Enfin, en ce qui concerne la taille du contrôle, nous la
modifions lorsque l’événement SizeChanged de inkInput survient.
Il existe cependant un cas où même si la taille inkInput se réduit,
on maintient la grande taille de historyListBox : si l’utilisateur
est en train d’utiliser l’historique d’autocomplétion (ie le curseur de la
souris est au-dessus de historyListBox), on ne réduira pas la taille
de la liste :
|
|
| Ici, la liste reste dans sa grande
taille, même si la zone de saisie vient de se réduire. En effet, l’utilisateur
est en train de ‘scroller’ la liste. |
Il faut donc savoir à chaque instant si le pointeur de la
souris se trouve au-dessus de la zone de liste pour ne pas la réduire intempestivement.
La zone de la liste inclut la barre de défilement verticale. En effet, lorsque
l’utilisateur est en train d’utiliser la barre de défilement, il ne faudrait
pas que la position de celle-ci change ! C’est la raison principale qui
va nous obliger à écrire une nouvelle classe, ListBoxEx, dérivant de
ListBox, qui sera la classe de notre contrôle historyListBox.
La classe ListBoxEx
En fait, nous voulons ajouter deux propriétés à ListBox :
-
Sur un survol sans clic du pointeur, les éléments doivent passer en
surbrillance.
-
Et, comme dit, nous souhaitons créer un événement de la ListBox
qui indique le départ de la souris de la zone du contrôle. Nous pourrions
pour cela utiliser l’événement MouseLeave, mais malheureusement, cet
événement ne tient pas compte de la barre de défilement verticale présente
à droite de la liste, le cas échéant.
Nous avons donc créé une classe ListBoxEx, dérivant
de ListBox, et qui servira à l’instanciation de notre contrôle historyListBox.
Pour la mise en surbrillance des éléments survolés, rien de plus simple, cela
se fait dans la méthode de callback de l’événement MouseMove :
private void ListBoxEx_OnMouseMove(object
sender, MouseEventArgs e)
{
if(IndexFromPoint(e.X,
e.Y) != ListBox.NoMatches)
SelectedIndex = IndexFromPoint(e.X,
e.Y);
}
En ce qui concerne la génération d’un événement lorsque la
souris quitte la zone, nous devons tout d’abord déclarer cet événement dans
la classe ListBoxEx :
public event EventHandler
MouseLeaveListBoxZone;
La seule technique que nous avons ensuite envisagé pour générer
cet événement au bon moment en tenant compte de la barre de défilement repose
sur du polling. Toutes les x millisecondes, nous allons regarder où
se trouve le pointeur de la souris par rapport au contrôle. Nous instancions
et initialisons donc un timer :
private Timer mouseTimer; // Timer pour savoir si la souris est au dessus
// . . .
// . . .
public ListBoxEx()
{
// . . .
mouseTimer = new Timer(); // Création du
timer de polling
// Abonnement à l'événement de déclenchement
du timer :
mouseTimer.Tick += new
EventHandler(MouseTimer_OnElapsed);
mouseTimer.Interval = 100; // Initialisation de la durée du timer
mouseTimer.Start(); // Départ du timer
// . . .
}
Dans la méthode de callback du timer, nous
récupérons les coordonnées du pointeur de la souris (ou du stylet, évidemment)
grâce à une autre méthode statique Position de System.Windows.Forms.Cursor.
La position du curseur est donnée relativement à l’écran. La récupération
des coordonnées du rectangle du contrôle, toujours par rapport l’écran, est
en revanche plus délicate : il faut déclarer et utiliser une méthode
de l’API Win32, GetWindowRect :
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern
int GetWindowRect(IntPtr hwnd, ref
Rectangle aRect);
Malheureusement, il reste
encore une difficulté car il y a un problème lorsque la fonction de l’API
remplit l’objet aRect : elle remplit les champs width et
height par les coordonnées respectives du coin inférieur droit du contrôle.
Nous avons donc créé une méthode qui appelle GetWindowRect et qui corrige
les valeurs de aRect pour retourner le Rectangle correspondant
au contrôle ctrl passé en paramètre :
private Rectangle GetRectangleToScreen(System.Windows.Forms.Control ctrl)
{
Rectangle Rect = new Rectangle(0, 0, 0, 0);
// Appel à la méthode de l'API Windows permettant
de récupérer le rectangle d'un contrôle :
GetWindowRect((IntPtr)(ctrl.Handle.ToInt32()), ref Rect);
return new Rectangle( Rect.Left,
Rect.Top,
Rect.Width - Rect.Left,
Rect.Height
- Rect.Top);
}
Les coordonnés obtenues englobent toute la zone du contrôle,
barre de défilement éventuelle incluse. Savoir si la souris se trouve dans
le contrôle devient maintenant un jeu d’enfant :
if(myRect.Contains(Cursor.Position))
// . . .
Ceci nous permet, grâce au polling réalisé par le
timer et à un flag, de générer l’événement MouseLeaveListBoxZone
au bon moment.
Maintenant que la liste s’affiche et disparaît correctement,
nous devons la remplir avec les adresses de l’historique qui correspondent
au début de l’URL saisie par l’utilisateur.
Accès à l’historique
Il existe plusieurs possibilités pour accéder
à un historique des URLs.
On peut tout d’abord consigner dans un fichier ou dans la
base de registre toutes les URLs saisies et validées dans la zone inkInput.
Chaque fois que l’utilisateur entre et valide une URL qui n’était pas présente
jusqu’à présent dans la liste, elle est ajoutée. Lorsque l’utilisateur commence
à écrire une URL, on affiche la liste filtrée par rapport au début de la saisie.
Nous n’allons pas utiliser cette méthode car la liste des URLs ne contiendrait
que les adresses entrées par l’intermédiaire de notre barre d’outils, et pas
celles entrées par l’intermédiaire de la barre d’adresse de IE.
Une deuxième méthode consisterait à utiliser justement la
liste des adresses tapées dans la barre d’adresse de IE. Cette liste est conservée
par IE dans la base de registre sous la clé : « HKEY_CURRENT_USER\Software\Microsoft\Internet
Explorer\TypedURLs ».
La troisième méthode consiste justement à utiliser d’une
manière très pratique cette liste constituée par IE. Une fonction de la dll
« shlwapi.dll », SHAutoComplete(), permet d’associer à un
contrôle du type RichTextBox un menu déroulant proposant la liste des
URLs correspondant au texte tapé dans le contrôle, exactement comme dans la
barre d’adresse d’IE. L’utilisation de cette fonction est extrêmement simple.
On déclare SHAutoComplete dans un premier temps avec un attribut DllImport :
[System.Runtime.InteropServices.DllImport("SHlwapi.dll")]
private static extern
int SHAutoComplete(Int32 HWND, Int64 Flags);
Puis on l’associe à un contrôle :
SHAutoComplete(textBox1.Handle.ToInt32(), 0x10000000
| 0x00002 | 0x00004);
Et le tour est joué ! SHAutoComplete se charge
de tout :

Malheureusement, cette fonction est inutilisable dans notre cas car elle se
base sur la propriété Text du contrôle associé, alors que nous souhaiterions
pouvoir passer une chaîne-filtre résultant de la conversion de l’encre numérique
saisie.
Notre dernière solution est d’accéder au véritable
historique d’Internet Explorer, celui que l’on peut consulter dans un panneau
du navigateur ou dans le dossier « C:\Documents and Settings\user\Local
Settings\Historique ». L’avantage de cette solution est la présence dans
la liste récupérée de toutes les adresses visitées. Pour mettre en œuvre cette
méthode, il faut déclarer une classe COM dont le GUID est « 3C374A40-BAE4-11CF-BF7D-00AA006946EE » :
[ComImport, Guid("3C374A40-BAE4-11CF-BF7D-00AA006946EE")]
class UrlHistory
{}
Cette classe implémente l’interface IUrlHistoryStg2
que nous devons également déclarer. Tout ceci est fait dans le fichier « HistoryManager.cs »
de notre projet (merci à Mattias Sjögren qui a mis en partage ce code sur
le forum microsoft.public.dotnet.framework.interop).
L’utilisation pour la lecture de l’historique se fait comme
suit :
private void HistoryFill(string
beginURL)
{
IUrlHistoryStg2 uhs2 = (IUrlHistoryStg2)new UrlHistory();
IEnumSTATURL estaturl = uhs2.EnumUrls();
STATURL staturl;
uint
fetched;
string currentURL;
// Parcours de l'historique de IE
while (0 == estaturl.Next(1,
out staturl, out fetched))
{
currentURL = staturl.pwcsUrl.ToLower();
// . . .
}
}
IEnumSTATURL est une autre interface COM également
déclarée dans notre fichier « HistoryManager.cs ».
Nous avons ainsi accès à la liste de toutes les URLs présentes
dans l’historique. Il nous faut maintenant les filtrer afin de ne conserver
que celles correspondant au début du texte écrit par l’utilisateur.
Filtrage des URLs par des expressions régulières
Pour ne faire figurer dans historyListBox
que les URLs correspondant au début de l’adresse tapée par l’utilisateur,
nous allons utiliser les expressions régulières, via la classe System.Text.RegularExpressions.Regex.
Pour tous les détails sur les expressions régulières, nous vous conseillons
la lecture de cet
article. Nous voulons ici comparer le début de l’adresse entrée aux adresses
de l’historique, mais sans les préfixes de protocoles et autres ‘parasites’.
Par exemple, si l’utilisateur entre « www.ms », les adresses telles
que « http://www.msdn.microsoft.com » ou « http://www.msn.fr » doivent
être proposées dans la liste. Pour cela, nous filtrons le début d’adresse
saisie et les adresses de l’historique de la même manière pour enlever les
protocoles (« http », « ftp »...), les ports (« :8080 »)
et autre « www ». Nous utilisons donc cette expression régulière
(nécessite un « using System.Text.RegularExpressions »)
:
Regex regURL = new Regex(
@"^((?<proto>\w+):/{2,})?(www.{0,2}\.)?(?<domain>[^/]+?)(?<port>:\d+
?((?<slash>/)(?<address>.+)?)?$");
Si la string beginURL contient par exemple
« https://www.jachetepaschersurinternet.org:443/confirmer.asp »
elle sera transformée en « jachetepaschersurinternet.org/confirmer.asp »
grâce au code suivant :
beginURL = UrlConvert(beginURL).ToLower(); //
passage en minuscules
if(regURL.Match(beginURL).Success)
beginURL =
regURL.Match(beginURL).Result("${domain}${slash}${address}");
Après ce traitement, nous emplissons notre historyListBox
de la manière suivante :
// Parcours de l'historique
de IE
while (0 == estaturl.Next(1, out staturl, out fetched))
{
currentURL = staturl.pwcsUrl.ToLower();
if(regURL.Match(currentURL).Success)
{
currentURL =
regURL.Match(currentURL).Result("${domain}/${address}");
// Ajout dans la ListBox si correspond au début de la chaîne
entrée :
if(currentURL.IndexOf(beginURL.ToLower())
== 0)
{
historyListBox.Items.Add(staturl.pwcsUrl);
}
}
}
Lorsque la liste des URLs à afficher devient conséquente, le remplissage
de la ListBoxEx ralentit considérablement car la méthode Add()
est appelée des dizaines de fois alors que la propriété Sorted du contrôle
est à true : le tri (un tri par insertion dans ce cas, on fait
mieux !) est très lent. Du coup, le programme reste bloqué pendant tout
ce temps, sans possibilité pour l’utilisateur de saisir la suite de son URL
(ie taper une lettre au clavier ou dessiner un trait pour former une nouvelle
lettre). Une solution que nous proposons est l’utilisation d’un second thread.
Utilisation d’un thread pour l’affichage de l’historique
Pour éviter que le programme
ne soit bloqué pendant le remplissage de la liste, nous allons faire remplir
celle-ci dans un thread différent du programme principal. Ainsi, plus rien
n’est bloqué. Lorsque l’utilisateur poursuit l’écriture de son URL (frappe
d’une touche au clavier, ou écriture d’un nouveau stroke), il faut
actualiser l’historique d’autocomplétion. Deux cas peuvent se produirent :
-
Un historique est déjà affiché : on lance un thread dans
lequel on vide l’historique puis on le remplit à nouveau en tenant compte
de la nouvelle saisie de l’utilisateur.
-
L’utilisateur a ajouté un élément à sa saisie alors que la
liste était en cours de constitution dans un thread séparé. On interrompt
le thread, puis on le relance en passant en paramètre la nouvelle chaîne saisie.
L’utilisation d’un thread en C# se fait en créant un objet
de classe Thread qui permet de contrôler le déroulement du thread (arrêt,
reprise...) et une instance du délégué ThreadStart qui servira à la
déclaration de la méthode de départ du thread créé :
private Thread historyThread;
private ThreadStart historyThreadStart;
// . . .
public InkToolBar()
{
// Lien vers la méthode lancée par le
Thread :
historyThreadStart = new
ThreadStart(HistoryUpdate);
historyThread = new
Thread(historyThreadStart);
historyThread.IsBackground = true; // Mise du Thread en
arrière plan
// . . .
historyUpdate est la méthode qui sera exécutée au
lancement du nouveau thread. Sa déclaration doit correspondre à la signature
du délégué ThreadStart (ie « void HistoryUpdate() »).
La propriété IsBackground détermine le comportement du thread en cas
d’arrêt de son application : un thread d’avant-plan poursuit son exécution
jusqu’à ce qu’il prenne fin de lui-même, au contraire d’un thread d’arrière-plan
qui s’arrête avec l’application qui l’héberge.
Nous devons lancer ou interrompre puis relancer notre thread
chaque fois que le Text de la zone de saisie change (frappe au clavier)
ou qu’un stroke a été dessiné (saisie manuscrite) : dans les événements
StrokeCompleted et TextChanged, nous ajoutons le code :
// On arrête un éventuel Thread de reconnaissance
en arrière plan
historyThread.Abort();
// On réinitialise le Thread
fraichement arrêté
historyThread = new Thread(historyThreadStart);
historyThread.IsBackground = true;
recoCase = 0; // recoCase = 1 dans StrokeCOmpleted
historyThread.Start();
La variable recoCase est un membre de la classe qui
permettra au thread de savoir s’il a été lancé par TextChanged ou par
StrokeCompleted : cela afin qu’il sache s’il faut récupérer du
Text ou des Strokes.
Grâce au thread que nous venons de créer, le
programme n’est plus bloqué. Cependant, la présence de ces deux threads peut
générer des accès concurrents à des ressources critiques telles que la propriété
Text de inkInput. Nous avons vu dans le dernier article
comment se protéger de cela, en créant et utilisant une propriété TextSafe
à la place de Text.
Voilà qui achève la description des points-clés
liés à la saisie d’adresses et à l’affichage de l’historique. Nous allons
maintenant nous intéresser au remplissage assisté de formulaires web.
Remplissages de formulaires Web
Notre but est de fournir à l’utilisateur la possibilité de
remplir en quelques clics un formulaire Web avec le stylet, et sans erreur.
L’utilisation de la reconnaissance manuscrite étant à proscrire dans le cas
d’adresses, de noms propres ou de numéros de téléphone, nous avons décidé
de fournir à l’utilisateur une liste des valeurs les plus courantes (ses adresses,
numéros de téléphone, noms, prénoms,...). L’utilisateur paramètre cette liste
en entrant toute donnée souhaitée. Lors de la saisie d’un formulaire, il clique
dans un champ pour lui donner le focus, puis sur la valeur de la liste qu’il
veut entrer :
Pour réaliser cela, on utilise simplement la
fonction statique suivante lorsque l’utilisateur clique sur une valeur du
menu :
private void
ParaContextMenu_SubItemClick(object sender,
EventArgs e)
{
// On envoie le contenu du champ
SendKeys.SendWait(((MenuItem)sender).Text);
//
. . .
}
Avec cette simple ligne, on simule la saisie au clavier d’un
texte. Comme c’est un champ d’une page HTML dans IE qui a le focus, ce champ
est automatiquement rempli.
Pour faciliter encore plus la tâche de l’utilisateur, nous
aimerions donner le focus à la zone suivant juste après avoir rempli automatiquement
un champ. En outre, comme le focus aura changé sans intervention de l’utilisateur,
il serait bon de distinguer le contrôle qui vient de recevoir le focus, en
le mettant en jaune par exemple.
Donner le focus au champ suivait se fait a priori
simplement grâce à une instruction de ce type :
SendKeys.SendWait(″{TAB}″);
Malheureusement, cela ne marche pas, pour une obscure raison
liée à la manière dont IE traite les messages Windows...
Nous allons donc utiliser une autre méthode, basée sur l’accès
direct au document HTML.
Accès au document HTML
Nous avons directement accès, depuis notre classe principale
InkToolBar, à l’objet Explorer, qui a été décrit dans le premier
article. C’est cet objet qui nous permet par exemple de naviguer vers
une URL dans IE grâce à sa méthode Navigate(). Cet objet permet en
fait encore plus de choses : grâce à la propriété Document de
Explorer, nous accédons au document HTML actuellement présent dans
IE. Pour pouvoir utiliser cet objet Document, nous devons tout d’abord
ajouter à votre projet la référence vers la dll « mshtml.dll ».
Pour cela, sélectionnez le composant « Microsoft.mshtml » dans l’onglet
« .NET » de la boîte de dialogue « Ajouter une référence ».
Cette référence est présente par défaut dans la liste.
Nous pouvons dès lors récupérer le document HTML courant :
mshtml.IHTMLDocument2 doc;
// On récupère le
document HTML courant
doc = (mshtml.IHTMLDocument2)(Explorer.Document);
doc contient alors une représentation de tout le document
chargé. Nous pouvons accéder à tous ses éléments et à leurs propriétés. Jetez
un œil à MSDN
pour connaître toutes les possibilités offertes par mshtml. La plus
grosse difficulté dans la manipulation de ces objets est le trans-typage (cast)
que nous devons sans cesse opérer. Dans notre cas, nous souhaitons récupérer
la liste de tous les champs texte ou mot de passe, puis sélectionner et mettre
en jaune le champ suivant cela qui a le focus actuellement. Cela se fait par
ces lignes :
private void
ActiveNextHTMLInputText(bool bReverse)
{
mshtml.IHTMLDocument2 doc;
mshtml.IHTMLElementCollection inputsCollection;
mshtml.IHTMLElement currentInput;
System.Collections.ArrayList textEditElements =
new System.Collections.ArrayList();
// On récupère le document HTML courant
doc = (mshtml.IHTMLDocument2)(Explorer.Document);
// On récupère la liste de tous les champs d'entrée
inputsCollection =
(mshtml.IHTMLElementCollection)(doc.all.tags("INPUT"));
// On parcourt tous les champs HTML d'entrée
for(int i=0; i < inputsCollection.length;
i++)
{
//On récupère le champ i de la liste inputsCollection
currentInput =
(mshtml.IHTMLElement)(inputsCollection).item(i,null);
if(
// Si le champs est une zone de texte
((((string)(currentInput.getAttribute("type",2))).ToLower()
== "text")
|| // ou une zone de password
(((string)(currentInput.getAttribute("type",2))).ToLower()
== "password"))
// et que ce champ appartient au même formulaire que le champ
actif :
&&
(((mshtml.IHTMLElement)((mshtml.IHTMLInputElement)(currentInput)).form).contains(doc.activeElement)))
// Alors :
textEditElements.Add((currentInput).sourceIndex);
} // Fin du For
// S'il existe des entrées texte ou password dans le même
formulaire que le champ actif
if(textEditElements.Count > 0)
{
// On reviendra au premier si c'était le dernier champ du
formulaire qui était sélectionné
currentInput =
(mshtml.IHTMLElement)(doc.all.item(textEditElements[0],null));
// On prend le champ immédiatement après
for(int i = 0; i < textEditElements.Count;
i++)
if(
(((int)(textEditElements[i])) > doc.activeElement.sourceIndex))
{
currentInput =
(mshtml.IHTMLElement)(doc.all.item(textEditElements[i],null));
break;
}
// On remet sa vraie couleur à l'élément précédemment sélectionné :
if(highlightedHTMLElement!=null)
highlightedHTMLElement.style.backgroundColor
= "";
// On donne le focus au premier champ text ou password rencontré :
((mshtml.IHTMLElement2)(currentInput)).focus();
// On met en surbrillance l'élément que l'on vient de sélectionner :
currentInput.style.backgroundColor = "#FFFF00";
// On enregistre l'élément dont on vient de changer la couleur :
highlightedHTMLElement = currentInput;
}
}
La méthode ci-dessus a largement été raccourcie par rapport
au code proposé en téléchargement, pour plus de clarté (hum)... Tous les cas
ne sont en effet pas prévus ici.
Il ne manque maintenant plus qu’un dernier détail pour que
notre barre soit complète : nous allons créer une boîte de dialogue pour
sa configuration.
Configuration de la barre d’outils
Voici la boîte de configuration de la barre :

La réalisation de cette boîte ne pose pas de problème particulier.
L’enregistrement des paramètres sur le disque se fait au moyen d’un fichier
XML. Ce fichier est unique pour chaque utilisateur et se trouve dans le répertoire :
« C:\Documents and Settings\user\Application Data\Inktoolbar ».
Lorsqu’un utilisateur lance la barre d’outils pour la première fois, un fichier
de configuration par défaut est copié dans ce dossier depuis l’emplacement
« C:\Program Files\Fichiers communs\Inktoolbar » créé à l’installation
de la barre (c’est dans cet emplacement que se trouve également le fichier
d’aide de la barre). Cela se fait par :
// Création de la chaîne d'adresse du
dossier du fichier XML de l’utilisateur
string confFolder = System.IO.Path.Combine(
System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),"Inktoolbar");
// Création de la chaîne d'adresse du
fichier XML de l’utilisateur
string confFile = System.IO.Path.Combine(
confFolder, "inktoolb
// Création de la chaîne d'adresse du
dossier du fichier XML par défaut
string defaultConfFolder = System.IO.Path.Combine(
System.Environment.GetFolderPath(System.Environment.SpecialFolder.CommonProgramFiles),
"Inktoolbar");
// Création de la chaîne d'adresse du
fichier XML par défaut
string defaultConfFile = System.IO.Path.Combine(
defaultConfFolder,
"inktoolbar_configuration.xml");
// Création du dossier dans lequel on
a le fichier de conf XML (s'il existe, ceci ne le crée pas)
System.IO.Directory.CreateDirectory(confFolder);
// Si le fichier perso n'existe pas,
on charge le fichier par défaut
if(!System.IO.File.Exists(confFile))
System.IO.File.Copy(defaultConfFile, confFile,
false);
Enfin, pour la lecture et l’écriture du fichier XLM, il existe
plusieurs solutions. L’une d’elle consiste à utiliser le DOM (Document Object
Model) fourni par les classes du Framework. Cette méthode est décrite en
détails à cette
adresse. Pour notre part, nous allons utiliser la sérialisation et la
désérialisation d’un objet vers (ou depuis) un fichier XML. Pour cela, il
faut tout d’abord créer une classe qui contient toutes les infos qui correspondent
aux données du fichier XML. Par exemple (le code est, encore une fois,
simplifié par rapport au code original) :
public class Configuration
{
public
class SearchUrls
{
public
string msn; // MSN
public
string google; //
Google
public
string other; //
Perso
}
public
class NavigationButton
{
public string text;
// Nom du bouton
public string suffix; //
Suffixe
}
public
class NavigationButtons
{
public
NavigationButton button1;
public
NavigationButton button2;
public
NavigationButtons()
{
button1 = new NavigationButton();
button2 = new NavigationButton();
}
}
// On crée les objets
// On initialise aux valeurs par défaut
public string
currentengine = "msn";
public SearchUrls urls = new
SearchUrls();
// Pour
un objet de type ArrayList
// Il faut préciser le type et le nom de chaque élément
// (c'est le même pour chaque élément de la liste)
// pour que la sérialisation puisse avoir lieu
[XmlArrayItem (typeof(string), ElementName = "sentence")]
public
System.Collections.ArrayList sentences =
new
System.Collections.ArrayList();
public
NavigationButtons buttons = new NavigationButtons();
public Configuration()
{
// On initialise tout les objets
urls.msn = "http://search.msn.com/results.aspx?q=%s";
urls.google = "http://www.google.com/search?q=%s";
urls.other = "http://search.msn.com/results.aspx?q=%s";
buttons.button1.suffix = ".com";
buttons.button1.text = ".com";
buttons.button2.suffix = ".fr";
buttons.button2.text = ".fr";
}
}
Pour la lecture d’un fichier XML, il suffit d’ouvrir le fichier
et de stocker son contenu dans un objet de type Configuration :
// Création du Serializer :
XmlSerializer mySerializer = new
XmlSerializer(typeof(Configuration));
// Lien entre le Reader et le fichier :
XmlTextReader myReader = new
XmlTextReader(confFile);
// Chargement des données du fichier
XML dans l'objet Configuration :
Configuration myConfiguration =
(Configuration)(mySerializer.Deserialize(myReader));
// Fermeture du flux fichier :
myReader.Close();
L’objet myConfiguration contient ensuite tous les
éléments du fichier XML. Une erreur est générée si les champs de l’objet ne
correspondent pas aux champs trouvés dans le fichier XML. L’écriture d’un
fichier XML à partir d’un objet, entièrement initialisé, de classe Configuration
se fait d’une manière similaire :
// On initiliase le Writer avec le nom
du fichier de conf
XmlTextWriter myWriter = new
XmlTextWriter(confFile,System.Text.Encoding.UTF8);
XmlSerializer mySerializer = new
XmlSerializer(typeof(Configuration));
// On sérialise l'objet Configuration
dans le fichier de conf XML
mySerializer.Serialize(myWriter,(object)myConfiguration);
myWriter.Close(); // On ferme le fichier
Nous répétons ce genre d’opérations dans le fichier « ConfigurationForm.cs »
qui est le fichier de la boîte de dialogue. Une lecture du fichier de configuration
est également effectuée dans la barre d’outils elle-même.
Voilà qui conclut la description des points principaux de
la barre d’outils TabletPC pour Internet Explorer.
Conclusion
Notre projet est désormais achevé. Nous avons réalisé une
barre d’outils pour Internet Explorer entièrement en .NET (C#). Cette barre
d’outils utilise la technologie d’encre numérique des TabletPC. Elle permet
la saisie d’URLs au stylet, avec affichage d’un historique pour n’avoir à
écrire que les premières lettres de l’adresse, et elle peut également faciliter
le remplissage des formulaires que l’on trouve fréquemment dans les pages
Web.
Sa réalisation a mis en jeu de nombreux domaines
de la programmation .NET : interop COM/.NET, utilisation de la SDK TabletPC,
manipulation de fichiers XML, création de threads, création de contrôles personnalisés
et d’assembly, génération d’un projet d’installation, utilisation du GAC et
de la base de registres, accès à un document HTML dans IE,...
Le programme d’installation de cette barre d’outils
TabletPC est disponible ici. Nous rappelons qu’il n’est pas exempt d’erreurs
et de bogues, mais nous l’espérons suffisamment testé et relu pour que la
barre d’outils soit utilisable. Le code source (que nous espérons suffisamment
commenté) est entièrement téléchargeable ici. Vous pouvez ouvrir le fichier de la solution,
« InkToolBar for IE.sln » pour accéder à tous les fichiers. Vous
pouvez librement récupérer les passages qui vous intéressent, ou bien corriger
et améliorer notre projet. Beaucoup de points peuvent être revus ou implémentés
dans la barre : lieu d’affichage du menu en fonction de la position des
champs HTML, ergonomie de l’interface, nouvelles fonctionnalités,...Nous espérons
que ce projet pourra vous aider dans vos propres développements, ou que la
barre d’outils elle-même vous sera utile. N’hésitez pas à nous contacter pour
la moindre remarque.