Le premier site francophone dédié au développement Pocket PC


ATL à la rescousse de Embedded Visual Basic (partie 1)
 
   


Partie 1
Partie 2

Visual Basic est un langage de haut niveau permettant de programmer assez rapidement des applications axées essentiellement sur l'accès à des bases de données et la présentation d'informations dans des formulaires. Cependant, pour peu que l'application devienne plus conséquente et que le programme ait besoin de fonctionnalités supplémentaires, les limitations de Visual Basic se font très fortement ressentir.

Afin d'outrepasser ces limitations, plusieurs possibilités s'offrent au développeur :

Utiliser l'API Win32 directement dans eVB. Cette façon de procéder peut fonctionner dans certains cas mais il faut savoir que eVB ne supporte pas les types de données structurées, types largement utilisés dans l'API Win32.

Encapsuler les appels à l'API dans des DLL écrites en C. Cela présente la manière la plus rapide pour résoudre un problème ponctuel, l'inconvénient étant que les fonctionnalités présentées à l'application sont alors restreintes et la DLL a donc peu de chance de pouvoir être réutilisée telle quelle.

Une méthode beaucoup plus proche de VB réside dans la création de composants ActiveX. Les ActiveX ont l'avantage d'être supportés en natif par VB. De plus, une fois créés, ils sont réutilisables dans d'autres langages. En effet, les ActiveX reposent sur la technologie COM qui présente un modèle binaire et non une structure de langage. Programmer des composants COM reste une tâche ardue si l'on veut tout programmer. Heureusement pour le commun des mortels, Microsoft fournit un librairie de classes prenant en charge les tâches répétitives et ardues, ce qui permet au programmeur de ce focaliser sur l'aspect fonctionnel de son composant.

Partie 1 : Un composant simple

La première partie de cet article va vous décrire les étapes à suivre afin de créer un composant COM simple : il présente des propriétés, des méthodes et remonte des événements à l'application. Nous verrons aussi comment instancier le composant dans un client Visual Basic et répondre aux événements.

Pour démarrer, on crée un nouveau projet de type " WCE ATL COM AppWizard ". Il est fortement conseillé d'utiliser l'assistant, s'en passer n'ayant d'ailleurs aucun intérêt. Lorsque le projet vient d'être créé, il contient une série de fichiers décrits ci-dessous. A ce stade, aucun composant n'est encore créé car une DLL COM (aussi appelée in-proc server) peut contenir plusieurs composants de divers types.

nomprojet.cpp : points d'entrées de la DLL et fonctions requises par COM.
nomprojet.rc / .h : fichiers de ressources.
nomprojet.def : fichier de définition de la DLL.
Stdafx.cpp / .h utilisé pour la génération des en-têtes précompilées.
nomprojet.idl : fichier pour MIDL, contient la description de la librairie.

Il est temps de commencer à créer ses composants. Les opérations s'effectuent à partir du ClassView (onglet au même niveau que la workspace). Cliquez droit sur le projet et choisissez " New ATL Object ". Une boîte de dialogue propose alors plusieurs types de composants. Créons un objet simple pour débuter.

La page suivante vous permet de nommer votre composant, ce qui correspond à lui fournir un ProgId nécessaire pour sa création en VB. Vous avez aussi la possibilité d'ajouter des fonctionnalités sur la page d'attributs :

Threading Model : " single " (convient très bien pour l'exemple).
Interface : " dual ", pour un support complet en VB.
Aggregation : indique le type de support pour l'agrégation ; nous n'en avons pas besoin pour ce composant.
ISupportErrorInfo : afin de supporter la remontée d'exceptions, nous l'utiliserons dans cet exemple.
Connection Points : afin de supporter la remontée d'événements, nous les utiliserons dans cet exemple.


Après création du premier composant, les fichiers existants sont bien évidemment mis à jour et de nouveaux fichiers sont créés :

nomcomp.cpp / .h : contient l'implémentation du composant.
nomcomp.rgs : contient les clés de la base de registre du composant ; nous pouvons bien sûr personnaliser ce fichier.

Les étapes suivantes décrivent l'implémentation des propriétés, méthodes et événements de notre composant. Ces opérations s'effectuent à partir de l'interface du composant Inomcomp dans le ClassView.

Ajouter une propriété de type simple

Créer une propriété de type " long " est une étape des plus simples. Il suffit de cliquer sur " Add Property " :

Type = long
Nom = LongValue
Type d'accès : get / put / putref

Le type d'accès indique si la propriété est en lecture ou en écriture, toutes les combinaisons étant possibles. On peut très bien imaginer une propriété en lecture seule (ex. : handle d'un contrôle), en lecture et écriture (ex. : le titre d'une fenêtre), en écriture seule (ex. : envoi de données sur un port série).

Attirons l'attention sur le fait qu'il existe deux types d'écriture :

put : la fonction d'écriture reçoit la valeur en paramètre ; le composant doit mémoriser une copie la variable reçue, ceci étant utilisé en général pour les types simples comme les types "long" et aussi pour les types "variant".

putref : la fonction reçoit une référence en paramètre ; le composant doit alors uniquement la mémoriser, ceci étant généralement utilisé pour les types pointant sur d'autres composants (on reçoit alors un pointeur sur une interface). Le point délicat dans la gestion de ce type de propriété se situe au niveau du compteur de références. Il faut incrémenter le compteur de références de l'interface avant de relâcher la précédente. En effet, si la référence reçue est la même, il y a un risque de libération de l'interface avant incrémentation, l'interface devenant alors invalide. Lorsque le composant est libéré, il faut également libérer toutes les interfaces reçues sinon le système restera encombré par toute une série de composants inutilisés.

Pour notre propriété "long", on a donc les méthodes get et put. L'assistant crée alors les entrées suivantes dans le fichier .idl :

[propget, id(1), helpstring("property LongValue")] HRESULT LongValue([out, retval] long *pVal);
[propput, id(1), helpstring("property LongValue")] HRESULT LongValue([in] long newVal);

Le but de cet article n'étant pas de décrire en détail IDL (Interface Description Language), le MSDN reste la meilleure source d'informations pour une description détaillée des possibilités.

Au niveau du fichier nomcomp.h, les déclarations des méthodes d'accès ont été ajoutées :

STDMETHOD(get_LongValue)(/*[out, retval]*/ long *pVal);
STDMETHOD(put_LongValue)(/*[in]*/ long newVal);

STDMETHOD est une macro déclarant la fonction de la manière suivante :

virtual HRESULT __stdcall get_LongValue(/*[out, retval]*/ long *pVal);
virtual HRESULT __stdcall put_LongValue(/*[in]*/ long newVal);

L'implémentation basique est également ajoutée dans le fichier nomcomp.cpp. Par contre, l'assistant est incapable de savoir comment implémenter la propriété. Il en va donc de la responsabilité du programmeur de savoir ce qu'il a à faire. Pour notre exemple, on déclare un membre long m_lLongValue au niveau de la classe Cnomcomp. Ce membre contiendra la valeur de la propriété. Le get et le put ne font alors que retourner et modifier le membre.

STDMETHODIMP Cnomcomp::get_LongValue(long *pVal)
{
    *pVal = m_lLongValue;
    return S_OK;
}               
STDMETHODIMP Cnomcomp::put_LongValue(long newVal)
{
     m_lLongValue = newVal;
     return S_OK;
}

Il ne faut pas oublier d'initialiser le membre dans le constructeur.

Ajouter une propriété de type string

Il est souvent nécessaire d'avoir une propriété de type chaîne dans son composant. Les questions souvent posées sont : comment VB gère-t-il les chaînes et comment les récupérons-nous dans notre composant C++ ? En réalité, le type de chaîne utilisé en VB est un type défini au niveau de COM, et porte le nom de BSTR (Basic String). Il existe bien entendu plusieurs fonctions de gestion pour ce type de chaîne.

La Basic String se différencie d'une chaîne C par les points suivants :

Présence de la taille (en BYTE) codée sur 32 bits en mémoire.
C'est toujours une chaîne unicode.
Le zéro de fin de chaîne n'est pas obligatoire.
Elle peut contenir autant de caractère ASCII 0 que l'on veut. Puisque la taille de la chaîne est stockée, aucun délimiteur n'est requis.

Heureusement pour le programmeur C, le zéro est malgré tout ajouté directement en fin de chaîne par les fonctions d'allocations. Dans notre exemple, nous n'utilisons pas de chaîne C, le codage d'une telle propriété étant réduite à sa plus simple expression.

Une propriété string se crée de la même façon que les autres.

Dans l'idl :

[propget, id(2), helpstring("property StrValue")] HRESULT StrValue([out, retval] BSTR *pVal);
[propput, id(2), helpstring("property StrValue")] HRESULT StrValue([in] BSTR newVal);

Dans le .h :

STDMETHOD(get_StrValue)(/*[out, retval]*/ BSTR *pVal);
STDMETHOD(put_StrValue)(/*[in]*/ BSTR newVal);

Dans le .cpp :

STDMETHODIMP CMyFirstATL::get_StrValue(BSTR *pVal)
 {
   if (*pVal != NULL)
   ::SysFreeString(*pVal);               
    *pVal = ::SysAllocStringLen(m_strStrValue, ::SysStringLen(m_strStrValue));
    return S_OK;
 }
STDMETHODIMP CMyFirstATL::put_StrValue(BSTR newVal)
 {
    ::SysReAllocStringLen(&m_strStrValue, newVal, ::SysStringLen(newVal));

return S_OK; }

Il ne faut pas oublier d'initialiser la variable membre dans le constructeur :

...
m_strStrValue = ::SysAllocString(L"");
...

Il ne faut pas oublier également de libérer la chaîne dans le destructeur :

...
if (m_strStrValue != NULL)
::SysFreeString(m_strStrValue);
...

Pour une description détaillée des fonctions utilisées, reportez vous au MSDN.

Schéma de représentation de la Basic String en mémoire :

A. taille en bytes de la chaîne sur 32 bits
B. contenu de la chaîne sur n bytes
C. byte = 0 optionnel, marqueur de fin de chaîne C

Le "BSTR" pointe directement sur la chaîne et non pas sur le champ taille de la chaîne.

Ajouter une méthode

Bien que les propriétés d'un composant cachent en réalité des appels de fonctions, on préférera cependant utiliser des méthodes pour effectuer des traitements. Un nom de méthode est d'ailleurs généralement constitué d'un verbe explicite sur la nature du traitement. C'est pourquoi, il est rare de trouver des composants qui ne possèdent aucune méthode. Un tel composant serait d'une utilité très restreinte ou alors mal conçu.

Dans notre exemple, nous allons ajouter une méthode SetAttributes qui aura pour effet de modifier en un appel les propriétés LongValue et StrValue. L'ajout de la méthode se fait de la même façon que la propriété, excepté que l'on choisit " Add Method " au lieu de " Add Property ". Il suffit alors de nommer sa méthode et d'énumérer les paramètres.

Les paramètres peuvent être de plusieurs sortes :

[in] le paramètre est en entrée

[out] le paramètre est en sortie et doit être un pointeur

[in, out] le paramètre est en entrée et en sortie, cela signifie que le paramètre contient une valeur valide et que celle-ci est susceptible d'être modifiée au retour de la fonction . Le paramètre doit être un pointeur

[out, retval] indique que ce paramètre contient la valeur de retour. Le paramètre doit être un pointeur et le dernier de la liste (il est optionnel). Si la valeur de retour est une référence, le pointeur doit être double. Il influence la façon dont VB voit la fonction s'il est présent, VB utilise Function s'il est absent, VB utilise Sub.

Dans l'idl :

[id(3), helpstring("method SetAttributes")] HRESULT SetAttributes([in]long newLong, [in]BSTR newStr);

Dans le .h :

STDMETHOD(SetAttributes)(/*[in]*/long newLong, /*[in]*/BSTR newStr);

Dans le .cpp :

STDMETHODIMP CMyFirstATL::SetAttributes(long newLong, BSTR newStr)
 {
    put_LongValue(newLong);
    put_StrValue(newStr);                 
    return S_OK;
 }

Ajouter un événement

Un composant peut remontrer au client des événements. Ce n'est en fait rien d'autre qu'une méthode implémentée au niveau d'une autre interface du composant, visible au niveau du ClassView, _InomcompEvents. Puisqu'il s'agit d'une méthode, son implémentation se fera de la même façon qu'une méthode tout à fait classique.

Contrairement à une méthode, un événement doit être déclenché d'une certaine façon pour être remonté au client. Pour cela, l'assistant est toujours là pour nous aider. Dans le ClassView, au niveau de la classe Cnomcomp, le menu droit nous propose " Implement Connection Point ". En exécutant cette commande, l'assistant générera pour nous le code de déclenchement de l'événement.

L'événement peut bien entendu être lancé à tout moment, pour cela il suffit d'appeler la fonction générée : Fire_nomevent.

Dans notre exemple, on pourrait vérifier la valeur de la propriété LongValue (" est comprise entre -100 et 100 "). Si la valeur est incorrecte, on génère un événement BadValue.

if ((newVal < -100) || (newVal > 100))
 {
    Fire_BadValue(newVal);
    retrun S_OK;
  }

Générer des exceptions

Un composant ne serait pas complet sans une gestion poussée des erreurs. En COM, la gestion d'erreur se fait en remontant des exceptions. La manière la plus expéditive afin de remonter une exception est de retourner E_FAIL à la place de S_OK dans une méthode (ou méthode get / put / putref). Cependant, en agissant de la sorte, aucune information n'est fournie au client sur la nature de l'erreur. Pour cela, notre composant hérite d'une classe ATL encapsulant la génération des informations d'exceptions. Il suffit alors d'appeler la méthode Error.

Dans la méthode SetAttribtues on pourrait générer une exception si la valeur de LongValue est incorrecte.

if ((newLong < -100) || (newLong > 100))
return Error(L"Invalid Property Value [LongValue]", CLSID_nomcomp, CTL_E_INVALIDPROPERTYVALUE);

Le premier paramètre donne une description de l'exception (retrouvée parErr.Description), le deuxième donne le GUID de la source de l'exception (en eVB, Err.Source donne apparemment " VBScript... " comme source de l'exception) et le troisième paramètre donne la valeur du HRESULT retourné par la fonction. Il existe une liste de valeurs prédéfinies consultable dans le MSDN.

Ajouter des énumérations

Pour ceux qui ont déjà programmé en VB, vous aurez sans doute remarqué qu'il est possible de définir des constantes contenues au niveau de la librairie. Ceci est facile à faire. Dans le fichier idl, il suffit d'ajout la déclaration suivante dans la section library :

library :                 
 typedef 
  [
    uuid(23C0F2D2-355B-4a55-BA7B-270C4243410F),
    public
  ]
 enum Constants
  {
     First = 1,
     Second,
      Third
   } Constants; 

Créer le client VB

Le client en VB est très simple à faire bien qu'il existe une petite subtilité pour pouvoir recevoir les événements. Il faut d'abord créer un module (.bas) qui contiendra, la déclaration de la variable qui référencera notre objet.

Public objATL As Object

La création de l'objet se fera grâce à la fonction CreateObjectWithEvents qui permet d'instancier un objet et de spécifier un préfixe pour la récupération des événements sur cette instance.

Set objATL = CreateObjectWithEvents("mylib.mycomp", "objATL_")

Les fonctions commençant par objATL_ seront interprétées en tant que réponse aux événements. Par exemple, si nous avons déclaré l'événement BadValue, la fonction objATL_BadValue représente le gestionnaire pour cet événement pour l'instance référencée par la variable objATL. Il est clair que le préfixe de la fonction n'est pas forcément le même que le nom de la variable, de sorte que nous pourrions avoir la même fonction de réponse pour plusieurs instances. Le programmeur doit alors faire en sorte que chaque instance puisse être identifiée. Par exemple, le premier paramètre du message donne une référence sur l'objet qui l'a généré mais cette façon de faire n'est pas conforme à COM.

Une fois l'objet instancié, les appels aux méthodes et propriétés sont pareils à tous les objets.

Déboguer son composant

Lorsqu'on crée un composant, on aimerait pouvoir le déboguer. Ceci est facile à réaliser, il suffit d'écrire un client. Notre client VB fera très bien l'affaire. Comment dire au débogueur de lancer un fichier .vb pour exécuter notre composant ? En fait, il faut lancer le " loader " de VB en lui spécifiant le chemin de notre client VB.

Dans la configuration du projet ATL, dans l'onglet " Debug " :

Remote executable : \windows\pvbload.exe
Program arguments : \myclient.vb (donner le chemin complet)

Gérer les traces de l'ATL

Les classes de ATL fournissent un système de traces directement dans la fenêtre du débogueur. La quantité de messages reçus peut être configurée à l'aide de #define, l'idéal étant de les définir dans le fichier stdafx.h avant inclusion des autres fichiers.

#ifdef DEBUG
#define ATL_TRACE_LEVEL 4 // de 0 (le moins de trace, défaut) à 4 (le plus)
#define ATL_TRACE_CATEGORY // combinaison de flags (liste dans le MSDN)
#endif

Les flags de catégorie sont définis dans l'énumération enum atlTraceFlags. La valeur par défaut définit tous les flags à 1 (0xFFFFFFFF).

Laurent Bouffioux

 
       
   
 
   
Copyright 2001-2004 - Tous droits réservés
 
   

iPAQ est un produit de COMPAQ.
Visual Tools est un produit de Microsoft Corporation.
Toutes les autres marques et produits présents dans ces pages sont la propriété exclusive de leurs sociétés respectives.