C++ en action : IRegistrar, recherche de sous-menus et autres éléments

Paul DiLascia

Consultez cet article en anglais  

Cliquez ici pour télécharger le code de cet article : C++atWork2006_10.exe (174 Ko)


Q: Lorsque je génère une DLL avec support d'automatisation en Visual C++® 6.0, certaines fonctions d'enregistrement sont générées, mais aucune fonction de suppression d'enregistrement ne l'est pour ma DLL. J'ai écrit une méthode DllUnregisterServer qui se présente ainsi :

      STDAPI DllUnregisterServer(void)
      {
      AFX_MANAGE_STATE(AfxGetStaticModuleState());
      if (!COleObjectFactory::UnregisterAll())
      return ResultFromScode(SELFREG_E_CLASS);
      return NOERROR;
      }
    

COleObjectFactory::UnregisterAll renvoie TRUE, mais ma DLL est toujours enregistrée. Comment corriger le code ?
Ivan Pavlik

R : Hélas, vous avez rencontré une petite faille de l'univers MFC. UnregisterAll semblerait être la fonction à appeler pour supprimer l'enregistrement de votre DLL. En examinant attentivement le fichier olefact.cpp dans lequel UnregisterAll est implémentée, vous pouvez trouver un code similaire à celui-ci :

      for (/* pFactory = all factories */) {
      pFactory->Unregister();
      }
    

Il effectue un traitement itératif sur toutes les fabriques du module et appelle Unregister à chaque boucle. Parfait. Que fait COleObjectFactory::Unregister ? Le code vous en dit plus :

      BOOL COleObjectFactory::Unregister()
      {
      return TRUE;
      }
    

Hé bien... Comme vous pouvez le constater, COleObjectFactory::Unregister ne fait rien ! Elle renvoie simplement TRUE. Cela s'avère vraiment étrange, puisque COleObjectFactory::Register enregistre votre classe COM en appelant ::CoRegisterClassObject. Par ailleurs, une autre fonction, COleObjectFactory::Revoke, appelle ::CoRevokeClassObject pour supprimer l'enregistrement de votre classe COM. MFC appelle Revoke automatiquement à partir de votre destructeur COleObjectFactory. Que se passe-t-il exactement ?

Le problème tient à une confusion malheureuse des termes liés aux différentes façons d'enregistrer des classes COM pour les fichiers DLL et les fichiers EXE. Pour les DLL, l'enregistrement d'une classe s'effectue en ajoutant des clés (CLSID, ProgID, etc.) dans le Registre Windows. Pour les EXE, vous devez appeler CoRegisterClassObject afin d'enregistrer la classe auprès du système COM au moment de l'exécution. La confusion est renforcée par le fait que pour les fichiers exécutables, le contraire d'enregistrer n'est pas supprimer l'enregistrement mais révoquer (CoRevokeClassObject).

En ajoutant le support COM dans MFC, les ingénieurs de Redmond se sont efforcé d'envisager le plus grand nombre possible de situations possibles. COleObjectFactory se présente ainsi :

      // in olefact.cpp
      BOOL COleObjectFactory::Register()
      {
      if (!afxContextIsDLL) {
      ::CoRegisterClassObject(...,
      CLSCTX_LOCAL_SERVER, ..);
      }
      }
    

Vous pouvez constater qu'elle n'effectue aucun traitement pour les DLL mais enregistre simplement les EXE à l'aide de CLSCTX_LOCAL_SERVER (contexte = serveur local, EXE s'exécutant sur l'ordinateur local). S'appuyant sur l'API C sous-jacente, les ingénieurs de Redmond ont utilisé le même nom Revoke pour la fonction qui supprime l'enregistrement de l'exécutable : COleObjectFactory::Revoke (qui est appelée automatiquement à partir du destructeur de fabrique de classes).

A quoi sert donc COleObjectFactory::Unregister, puisque cette fonction ne fait rien ? Les ingénieurs de Redmond ont peut-être envisagé d'appliquer la méthode d'enregistrement/suppression d'enregistrement aux DLL. En fait, pour enregistrer votre DLL, une fonction complètement différente est nécessaire : COleObjectFactory::UpdateRegistry. Cette fonction accepte un argument booléen qui indique à MFC si vous souhaitez enregistrer la classe COM ou supprimer l'enregistrement de celle-ci. Par ailleurs, la fonction UpdateRegistryAll effectue un traitement en boucle sur toutes les fabriques de classe, appelant UpdateRegistry pour chacune d'elles. Voici comment implémenter la fonction DllUnregisterServer:

      STDAPI DllUnregisterServer(void)
      {
      AFX_MANAGE_STATE(AfxGetStaticModuleState());
      return COleObjectFactory::UpdateRegistryAll(FALSE)
      ? S_OK : SELFREG_E_CLASS;
      }
    

DllRegisterServer doit être identique, hormis que vous transmettez TRUE au lieu de FALSE à UpdateRegistryAll.

Bien que l'implémentation par défaut de UpdateRegistry effectue un traitement satisfaisant d'ajout ou de suppression des clés de registre dans HKCR/CLSID–InprocServer32, ProgID, Insertable, ThreadingModel, etc., il se peut que vous deviez ajouter des clés spécifiques à votre classe COM. Par exemple, il peut être nécessaire d'enregistrer des catégories, qui sont une sorte d'extension de l'ancienne clé "Insertable" (ce qui signifie que votre classe COM peut être dirigée dans une fiche en mode conception). Dans ce cas, vous devez remplacer UpdateRegistry pour ajouter ou supprimer vos clés.

Dans les numéros de novembre et décembre de 1999 de Microsoft Systems Journal (rebaptisé MSDN®Magazine), j'ai expliqué comment générer un objet Band pour Internet Explorer à l'aide de l'interface IRegistrar ATL (Active Template Library). (Les objets Band doivent enregistrer une catégorie spéciale dénommée CATID_DeskBand.) IRegistrar est un outil résolument pratique permettant de concevoir un script d'enregistrement (fichier .RGS) destiné à écrire vos entrées de registre, au lieu d'appeler des fonctions de registre comme RegOpenKey et RegSetValue, entre autres. La figure 1 représente un script typique.

Comme vous pouvez le constater, IRegistrar permet de définir des variables comme %ClassName% et %ThreadingModel%, puis de les remplacer par des valeurs réelles lors de l'exécution. IRegistrar vous évite de rappeler l'API du registre. Vous pouvez écrire un script qui ressemble davantage aux entrées du registre, voire utiliser le même script pour enregistrer ou supprimer l'enregistrement de votre DLL. IRegistrar est suffisamment performant pour supprimer l'enregistrement. Officiellement, il n'existe pas de documentation sur IRegistrar, mais le code est disponible ici (dans atliface.h). Si vous n'utilisez pas encore IRegistrar pour enregistrer vos DLL COM et supprimer leur enregistrement, vous ne regretterez pas de l'essayer, tant cet outil vous facilitera la tâche. Pour plus de détails, voir mon article de novembre 1999.


Q: Mon application principale comprend un menu normal avec un sous-menu de commande Édition (Couper, Copier, Coller, etc.). Je souhaiterais que le sous-menu Édition s'affiche comme menu contextuel quand l'utilisateur clique avec le bouton droit de la souris dans la fenêtre principale. Le problème réside dans l'accès à ce sous-menu à partir du menu principal. Il ne semble pas exister d'identificateur de commande associé à un sous-menu. Je peux utiliser un index numéroté à partir de zéro mais, en raison de la personnalisation, le menu Édition risque de ne pas toujours être le second menu. Je ne peux pas rechercher le mot "Édition" en raison des différentes traductions du menu. Comment trouver le sous-menu Édition dans tous les cas ?
Brian Manlin

R : Notez tout d'abord que le menu Édition doit être le second sous-menu s'il en existe un. Selon les instructions officielles de définition de l'interface graphique Windows®, les trois premiers menus doivent être Fichier, Édition et Affichage, si vous les prenez en charge. Voir The Windows Interface Guidelines for Software Design (Microsoft Press®, 1995). Cela dit, je vais vous expliquer comment trouver votre sous-menu Édition ou tout autre sous-menu.

Vous avez raison : un sous-menu ne possède pas d'identificateur de commande. Windows utilise le champ d'identificateur de commande en interne afin de mémoriser la valeur HMENU du sous-menu si l'élément de menu est un sous-menu. Donc, en supposant que le sous-menu contient toujours une commande spécifique (Édition | Couper, par exemple) et que la commande possède toujours le même identificateur (ID_EDIT_CUT, par exemple), il est assez aisé d'écrire une fonction de recherche du sous-menu contenant une commande donnée. La figure 2 représente le code. CSubmenuFinder agit comme un espace de noms comprenant la fonction statique FindCommandID, qui s'appelle récursivement pour effectuer une recherche en profondeur dans le menu et les sous-menus pour un élément de menu dont l'identificateur de commande fourni doit correspondre.

Cet extrait de code recherche le menu principal d'un sous-menu contenant un élément dont l'identificateur de commande est ID_EDIT_CUT et renvoie le sous-menu s'il le trouve. J’ai écrit un petit programme appelé EdMenu pour tester CSubmenuFinder. À l'aide du code précédent, EdMenu gère WM_CONTEXTMENU pour afficher un sous-menu Édition très similaire à celui du menu principal lorsque l'utilisateur clique à l'aide du bouton droit de la souris dans la fenêtre principale. CSubmenuFinder trouve le menu Édition où qu'il apparaisse dans le menu principal. Pour le vérifier, j'ai placé temporairement le menu Édition avant le menu Affichage dans une version précédente du programme. Je vous invite à télécharger le source.

Q: Comment convertir un objet CString MFC en objet String dans le langage C++ géré ? Prenons, par exemple, le code C++ suivant :

      void GetString(CString& msg)
      {
      msg = // build a string
      }
    

Comment réécrire cette fonction en utilisant le C++ géré et en remplaçant le paramètre CString par un paramètre String géré ? Le problème est que ce GetString modifie le CString de la fonction d'appel et je souhaite faire la même chose à l'aide d'un String géré.
Sumit Prakash

R : La réponse dépend de ce que vous utilisez : la nouvelle syntaxe C++/CLI ou les anciennes extensions gérées. Les deux syntaxes peuvent prêter à confusion mais, n'ayez crainte, je vais vous aider à clarifier les choses.

Puisque nous sommes en 2006, commençons par la nouvelle syntaxe . Pour ne pas faire d'erreurs, il convient de se rappeler deux points. Tout d'abord, en C++/CLI, un objet géré se caractérise par un accent circonflexe. Ainsi, CString devient String^. Par ailleurs, l'équivalent géré d'une référence (&) est une référence de suivi, notée % en C++/CLI. Je vous embrouille peut-être avec l'accent circonflexe (^), mais avec le temps, l'utilisation de % vous semblera naturelle également. Les nouvelles fonctions ressemblent à ceci :

      // using C++/CLI
      void GetString(String^% msg)
      {
      msg = // build a string
      }
    

Qu'est-ce que cela signifie ? Pour prouver qu'il fonctionne vraiment, j'ai écrit un petit programme dénommé strnet.cpp (voir figure 3). Il implémente deux classes, CFoo1 et CFoo2, possédant chacune un membre GetName. La première utilise CString& ; la seconde, String^%. Si vous compilez ce programme, puis l'exécutez (à l'aide de /clr, bien entendu), vous pourrez constater que, dans les deux cas (celui qui utilise CString et celui qui utilise String), l'objet transmis (natif ou géré) est modifié par la fonction membre.

Ainsi, vous souhaiterez toujours utiliser une référence de suivi (%) pour les objets, et non pas une référence native (& qui permet également la compilation), car la référence de suivi contrôlera la référence même si le GC déplace l'objet à l'intérieur de son tas géré.

Si vous optez pour la syntaxe ancien style (/clr:oldSyntax), je me demande pourquoi vous vivez encore à l'âge de pierre. N'ayez crainte : si vous êtes "rétro", vous pouvez malgré tout utiliser une référence. Si vous employez les extensions gérées, rappelez-vous simplement que les objets gérés sont des pointeurs __gc ; ainsi, CString devient String __gc *. Et comme le compilateur considère String comme un type géré, le pointeur __gc est inutile ; un simple pointeur (*) fait l'affaire. La référence est la même. La conversion de la syntaxe ancien style ressemble à ceci :

      // __gc omitted
      void GetString(String*& msg)
      {
      msg = // build a string
      }
    

Compris ? La syntaxe n'est vraiment pas difficile. Lorsque la syntaxe C++ vous pose problème, je vous conseille de revoir les notions de base.

Q: Récemment, en générant une application console CLR avec Visual Studio® 2005, j'ai remarqué que ce logiciel créait une fonction ressemblant à ceci :

      int main(array<System::String ^>
        ^args)
        {
        ...
        return 0;
        }
      

À mon avis, il s'agit d'un changement par rapport à l'ancien argc/argv dont je maîtrise l'utilisation en C/C++. En tentant d'accéder à args[0], je me suis aperçu que [0] désignait non pas le nom du fichier (comme en C/C++) mais le premier paramètre de ligne de commande. Qu'est devenu le nom du fichier ? Pouvez-vous m'expliquer la raison de changement ?
Jason Landrew

R: Nous vivons une époque formidable. Rien n'est comme avant, n'est-ce pas ? Nous vivions avec argc/argv depuis l'apparition du langage en C en 1972. Mais les ingénieurs de Redmond ont débarqué et les choses ont changé. Peut-être souhaitaient-ils vérifier que tout le monde savait utiliser la nouvelle syntaxe de tableau.

La nouvelle syntaxe permet, entre autres, de générer un programme avec l'option /clr:safe, ce qui introduit un haut niveau de sécurité dans le code. Cela pourrait être comparé au verrouillage du programme dans une salle blanche — c'est-à-dire une pièce stérile dotée d'équipement chromé dans laquelle on pénètre, revêtu d'une combinaison, par l'intermédiaire d'un sas à pression négative. Lorsque vous utilisez l'option /clr:safe, le compilateur impose un grand nombre de restrictions drastiques. Vous ne pouvez pas utiliser de types natifs. Vous ne pouvez pas utiliser de fonctions globales. Vous ne pouvez pas appeler de fonctions non gérées. Inutile de dire que votre marge de manœuvre est vraiment étroite. Pourquoi vous imposeriez-vous de telles contraintes ? Pour être totalement certain que le code est sécurisé. En termes techniques, /clr:safe génère du code "vérifiable", ce qui signifie que le CLR (Common Language Runtime) peut vérifier que le code ne viole pas les paramètres de sécurité, par exemple, en tentant d'accéder à un fichier alors que les paramètres de sécurité de l'utilisateur l'interdisent. Les pointeurs natifs (plus simplement, les pointeurs) font partie des éléments bannis par l'option /clr:safe. (Le terme "natif" est redondant puisque un pointeur ne peut être dirigé vers un type géré.) Comme les pointeurs ne sont pas autorisés avec /clr:safe, l'ancienne déclaration argv ne convient pas. Les ingénieurs de Redmond l'ont modifiée pour qu'elle puisse utiliser un tableau String^. Et comme un tableau connaît sa longueur, argc est inutile. La bibliothèque d'exécution fournit le code de démarrage nécessaire à la création et à l'initialisation du tableau arg avant l'appel de la fonction principale. Si vous n'envisagez pas d'utiliser /clr:safe, vous pouvez toujours écrire une fonction utilisant l'ancienne signature argc/argv.

Nous avons vu l'intérêt d'opter pour un tableau géré, mais pourquoi args[0] désigne-t-il le premier paramètre de commande et non pas le nom du fichier ? Probablement parce que les ingénieurs de Redmond ont jugé que le nom du fichier était inutile. En tout cas, cela importe peu car il est aisé de récupérer le nom du fichier. Je suis certain qu'il existe de nombreux moyens d'y parvenir mais la classe System::Environment est sans doute le plus évident, car elle est axée sur le CLR et compatible avec /clr:safe. Elle expose la méthode suivante :

      static array<String^>^ GetCommandLineArgs ()
      

À la différence du paramètre args fourni à la méthode principale, le tableau renvoyé par Environment::GetCommandLineArgs ne démarre pas en fait avec le nom du fichier exécutable.

Finalement, à quoi bon savoir le nom de votre propre fichier EXE ? Comme c'est vous qui écrivez le programme, vous êtes censé savoir comment il se nomme. Dans certains cas, tel que l'affichage dynamique du nom du fichier dans un message d'aide, cette information peut s'avérer nécessaire. Le message d'aide pourrait ressembler à ceci :

      FooFile -- Turns every word in your file to "foo"
      usage:
      FooFile [/n:<number>
        ] filespec
        filespec = the files you want to change
        <number>
          = change only the first <number>
            occurrences
          

Dans le cas présent, FooFile est le nom du programme. Je pourrais me contenter d'écrire "FooFile" dans le texte d'aide, mais j'ai pour vieille habitude de renommer des programmes ou de copier du code entre programmes. En obtenant le nom du programme auprès du programme lui-même (par l'intermédiaire de argv ou de Environment::GetCommandLineArgs), mes messages d'aide affichent toujours l'information appropriée même si je renomme le fichier ou copie le code source dans un autre programme.

Bonne programmation !


Veuillez envoyer vos questions et vos commentaires à l'attention de Paul à l'adresse suivante : cppqa@microsoft.com.

Paul DiLascia est consultant logiciel indépendant et concepteur Web/interface utilisateur. Il est l'auteur de Windows++: Writing Reusable Windows Code in C++ (Addison-Wesley, 1992 - en anglais). Pendant son temps libre, Paul développe PixieLib, une bibliothèque de classes MFC disponible sur son site Web, http://www.dilascia.com/.

Paru dans le MSDN Magazine d'octobre 2006.


Haut de pageHaut de page