> Pratique
PrograZine issue #10 - http://www.citeweb.net/discase/10/fprofil.htm Edito Sommaire Contribution Contacts


Gestion des fichiers de profil
par Denis ZIEGLER - denis.ziegler@worldonline.fr
Initié Windows C++
> fichiers attachés
fpro_src.zip
fpro_ex.zip



     Les fichiers de profil ou fichiers *.ini ont été largement popularisés par Windows 3.x, comme moyen simple d'enregistrer les paramètres du système (les fameux fichiers win.ini et system.ini) ou d'une application. Il s'agit de fichiers texte avec des lignes sous la forme parametre=valeur, regroupées en sections.

    Bien que remplacés par la base de registres depuis Windows 95, les fichiers de profil restent utilisés par de nombreuses applications (jetez un coup d'oeil à votre répertoire windows pour vous en convaincre). Leur emploi est en effet plus souple et surtout plus universel que celui du registre associé au système de Microsoft.

    De ce fait, l'utilisation de ces fichiers dans d'autres systèmes d'exploitation est tout à fait envisageable. Sous Linux par exemple, certaines applications les emploient (comme le fichier ~/.mc/ini de Midnight Commander ou les fichiers de configuration de KDE), même s'ils n'ont pas d'extension ini.

    Nous vous proposons d'implémenter les fonctions nécessaires à leur manipulation, indépendamment de l'API de Windows, directement en C++ à la norme ANSI, donc utilisables sur tout système disposant d'un compilateur à cette norme. Le C++ met à la disposition du programmeur des fonctionnalités puissantes (utilisation des classes, gestion des entrées/sorties à l'aide des flots, etc...) pour réaliser ce travail.

    Dans un premier temps, nous présenterons des classes génériques permettant de manipuler les fichiers en général et les fichiers texte en particulier, basées sur les flots de la bibliothèque standard C++.

    Dans un deuxième temps, la réalisation d'une classe de fichier de profil par héritage de la classe fichier texte sera détaillée. De plus, nous réaliserons l'implémentation des fonctions de gestion des fichiers de profil de Windows, avec la même syntaxe que celle de l'API native, mais basée sur la classe créée auparavant.

    Enfin, un petit programme exemple nous permettra de tester le tout.
>La classe Fich

    Listing 1. La classe Fich
#include <fstream.h>

// Modes d'ouverture des fichiers
#define MA_LECTURE   0
#define MA_LECTECR   1
#define MA_ECRITURE  2
#define MA_AJOUT     3  
#define MA_ECRITRAZ  4  //remise a zero du fichier a l'ouverture

class Fich {
 protected :
   fstream fs;
   char Nom[128];
 public :
   Fich(const char *_Nom);
   const char * getNom(void);
   void ChangeNom(const char *_Nom);
   int Creer(void);
   int Detruire(void);
   int Vider(void);
   int Existe(void);
   virtual int Ouvrir(const unsigned ModeAcces);
   int Fermer(void);
   int Lire(char *dest, const int nboctets);
   int Ecrire(char *source, const int nboctets);
   int AllerDebL(void);
   int AllerFinL(void);
   int AllerPosL(const long n);
   int DecalerPosL(const long n);
   long PosCourL(void);
   int AllerDebE(void);
   int AllerFinE(void);
   int AllerPosE(const long n);
   int DecalerPosE(const long n);
   long PosCourE(void);
   int Ok(void);
   int Fin(void);
   int Erreur(void);
   long Taille(void);
};
l

    Le listing 1 présente le prototype de la classe Fich, qui regroupe les fonctions élémentaires de manipulation de fichiers. Les opérations d'entrées/sorties utilisent les classes de flots fournies par la bibliothèque standard C++ (cf encadré), à l'aide de la donnée membre protégée fs de type fstream.

    D'une manière générale, un flot représente un canal de données entre une source et une cible. On parlera d'extraction ou de lecture pour les opérations d'entrée, d'injection ou d'écriture pour les opérations de sortie. Un flot peut être connecté à un périphérique ou à un fichier. Les flots prédéfinis cout et cin correspondent aux sorties et entrées standard, c'est-à-dire respectivement aux fichiers stdout et stdin du langage C. Les opérateurs << et >> servent aux opérations d'injection ou d'extraction sur les flots (par exemple cout << n affiche le contenu de la variable n).

    La bibliothèque C++ fournit un certain nombre de classes de flots, basées sur la classe de base ios et les classes dérivées istream et ostream spécialisées respectivement pour les entrées et les sorties. Lors des opérations sur les fichiers, on emploiera les classes dérivées ifstream, ofstream et fstream. C'est cette dernière qui est utilisée dans notre classe Fich.

    Pour plus de détails sur les classes de flots, on se reportera à une documentation de référence C++, par exemple l'excellent ouvrage de Claude Delannoy "Programmer en langage C++" (éditions Eyrolles).

    Le constructeur de Fich sert uniquement à renseigner le nom du fichier sous-jacent qui pourra aussi être modifié par la fonction ChangeNom et récupéré par getNom. Il n'y a pas de destructeur de défini, car il n'y a pas de données dynamiques à désallouer.

    Comme son nom l'indique la fonction membre Ouvrir permet d'ouvrir le fichier en indiquant le mode d'ouverture souhaité. Les constantes de mode d'ouverture possibles sont définies dans le même fichier d'en-tête (cf listing 1). Il s'agit d'une encapsulation des constantes déclarées dans la classe ios, équivalent des modes de la fonction fopen du C. Tous les modes d'ouverture comprennent de manière implicite le mode ios::binary qui désactive certains traitements particuliers liés aux fichiers texte, dans la mesure où la classe Fich permet de traiter tous types de fichiers. Il n'en sera pas de même avec la classe FichTxt.

    Les fonctions Lire et Ecrire requièrent un pointeur sur la chaîne de caractères à lire ou écrire, ainsi que le nombre d'octets à transférer.

    L'accès direct à une position dans le fichier est possible grâce aux pointeurs de lecture et d'écriture des classes ifstream et oftsream hérités par fstream. En règle générale ces pointeurs sont positionnés en fin de fichier lors de l'ouverture. La classe Fich fournit des fonctions de positionnement de ces pointeurs : AllerDeb, AllerFin, AllerPos, DecalerPos pour le pointeur de lecture (suffixe L) ou d'écriture (suffixe E). Attention ! Bien que les pointeurs de lecture et d'écriture soient séparés, les mouvements de l'un peuvent entraîner les mêmes mouvements de l'autre.

    La gestion des erreurs d'entrées/sorties dans les différentes fonctions membres utilise la fonction good() de la classe ios qui teste les différentes conditions d'erreur qui peuvent se produire sur un flot (flot altéré, fin de fichier,...). Les fonctions membres Ok, Erreur ou Fin permettent de tester ces états.
>La classe FichTxt

    Listing 2. La classe FichTxt
// Taille maxi (par defaut) d'une ligne lue a partie d'un fichier texte
#define LMAXLGN 256

//===== Fichier texte ==========================================================
class FichTxt : public Fich {
 public :
   FichTxt::FichTxt(const char * _Nom) : Fich(_Nom) { }
   virtual int Ouvrir(const unsigned ModeAcces);
   int LireLgn(char *dest, const int lmax = LMAXLGN);
   int EcrireLgn(const char *source);
};
l

    Cette classe est dérivée de la classe Fich par héritage, ce qui lui permet de disposer de toutes les fonctions et données membres de la classe parent. En plus, la redéfinition ou surcharge de fonctions héritées permettra de modifier le comportement de la classe et l'ajout de nouvelles fonctions lui conférera des fonctionnalités nouvelles.

    La fonction Ouvrir a été surchargée afin d'utiliser les modes d'ouverture propres aux fichiers texte, c'est-à-dire sans l'attribut ios::binary utilisé dans la classe Fich. On notera qu'il s'agit d'une fonction virtuelle, ce qui signifie qu'en utilisant un pointeur sur un objet de type Fich, qui peut recevoir l'adresse d'un objet de la classe de base ou de celle d'un descendant, la fonction correcte sera automatiquement appelée selon la classe de l'objet pointé. On parle dans ce cas de ligature dynamique (à l'exécution) par opposition à la ligature statique (à la compilation).

    Les nouvelles fonctions LireLgn et EcrireLgn assurent la lecture et l'écriture d'une ligne de texte, délimitée par un caractère fin de ligne. Selon l'environnement dans lequel sera compilé le programme, les particularités de gestion des fins de lignes seront prises en compte par ces fonctions. On rappellera en effet que les systèmes DOS ou Windows par exemple utilisent la combinaison retour chariot + fin de ligne pour délimiter les lignes des fichiers texte, alors que les systèmes de type Unix n'utilisent que le caractère fin de ligne.

    Nous disposons donc maintenant avec la classe FichTxt de tout le nécessaire pour créer une classe dérivée spécialisée dans le traitement des fichiers de profil (considérés comme un type particulier de fichiers texte), avec toutes les fonctionnalités attendues.
>La classe FichProf

    Listing 3. La classe FichProf
#include "Fich2.h"

class FichProf : public FichTxt {
 public :
  //--- Fonctions membres publiques --------------------------------------------
  FichProf(char *nomfich, int _lectauto=1, int _ecrauto=1);
  ~FichProf(void);
  int LireCh(char *sect, char *entr, char *defaut, char *dest, int dim);
  int LireInt(char *sect, char *entr, int defaut);
  int EcrireDonnees(char *sect, char *entr, char *val);
  int LireProfil(void);
  int EcrireProfil(void);
  int Modif(void) {return modif;}
 protected :
  //--- Structures de donnees privees ------------------------------------------
  struct Sentree {
    int comment;
    char *id;
    char *val;
    Sentree *suivt;
  };
  struct Ssection {
    int comment;
    char *id;
    Sentree *entr;
    Ssection *suivt;
  };
  //--- Donnees membres privees ------------------------------------------------
  Ssection *profil;
  Ssection *sectcour;
  Sentree *entrcour;
  int modif;
  int lectauto;
  int ecrauto;
  //--- Fonctions membres privees ----------------------------------------------
  int LireLgn(void);
  int LireSection(char *ch);
  int LireEntree(char *ch);
  int LireCommentaire(char *ch);
  int EcrireSection(void);
  int EcrireEntree(void);
  void MajProfil(char *sect, char *entr, char *val);
  void MajSection(Ssection* sect, char *entr, char *val);
  void MajEntree(Sentree* entr, char *val);
  Ssection *NouvelleSection(char *sect, char *entr, char *val);
  Sentree *NouvelleEntree(char *entr, char *val);
  void AjouteCh(char *dest, char *source, int &i, int imax);
  void DesallouerProfil(void);
  void DesallouerSection(Ssection *sect);
  void DesallouerEntree(Sentree *entr);
};
l

    Le listing 3 présente le prototype complet de la classe FichProf, directement dérivée de FichTxt. Elle assure la gestion complète d'un fichier de profil grâce à un ensemble de méthodes spécialisées : création, modification suppression d'entrées ou de sections, etc...

    Une des particularités de FichProf réside dans la gestion de la lecture et de l'écriture du fichier sur le disque. Ces opérations sont faites globalement, en une seule fois, afin d'éviter les opérations multiples d'entrées/sorties sur le disque. Cela signifie que le contenu du fichier est recopié en mémoire dans des structures de données spécifiques (voir plus loin), et que les opérations de modification des sections et entrées sont faites en mémoire, donc très rapidement.

    Par défaut la lecture et l'écriture du fichier sont automatiques : lecture lors de la création de l'instance de la classe, écriture lors de sa destruction si des modifications ont été effectuées. Ce mode de fonctionnement est bien adapté à l'utilisation classique d'un fichier de paramètres dans une application : lecture des paramètres au début, modifications éventuelles en cours d'exécution, écriture avant la fin "normale" de l'application, si besoin.

    Bien sûr, on pourra aussi déclencher une lecture ou une écriture manuellement, à la demande.

    Les données du fichier de profil sont stockées en mémoire sous forme de listes chaînées (cf figure 1).

    Figure 1. Listes chaînées des sections et entrées d'un fichier de profil

    Les deux structures Ssection et Sentree sont définies à l'intérieur de la classe FichProf.

    Section comprend une chaîne de caractères contenant l'identifiant de la section, un pointeur vers la première entrée de la section, et un pointeur vers la section suivante (initialisé à NULL pour la dernière section). Un indicateur permet de marquer les lignes de commentaires (commençant par un point-virgule).

    La composition de Ssection est similaire, chaque entrée étant chaînée à la suivante de la même section.

    Toutes ces données sont allouées dynamiquement en mémoire lors de la lecture du fichier de profil, soit par le constructeur (lecture automatique), soit à la demande. Elles seront désallouées par le destructeur.
    
    Examinons les autres membres de la classe FichProf :
  • + profil pointe sur la première section du profil et permet donc de parcourir tout le profil à l'aide des listes chaînées.
  • + Les pointeurs sectcour et entrcour pointent toujours sur la section et l'entrée courante.
  • + L'entrée modif indique que le profil a été modifié depuis sa lecture sur le disque (valeur différente de zéro). Cela forcera l'écriture automatique du profil lors de la destruction de l'objet.
  • + Les indicateurs lectauto et ecrauto (actifs par défaut, mais pouvant être passés en paramètre au constructeur) forcent la lecture automatique du profil par le constructeur et son écriture par le destructeur. Dans le cas contraire, il faudra déclencher ces opérations manuellement à l'aide des fonctions LireProfil et EcrireProfil..
  • + Les fonctions protégées LireLgn, LireSection, LireEntree et LireCommentaire sont appelées par LireProfil, de même que EcrireSection et EcrireEntree par EcrireProfil.
  • + Les données enregistrées en mémoire sont désallouées par les fonctions DesallouerProfil, DesallouerSection et DesallouerEntree, appelées par le destructeur.
  • + La fonction publique LireCh devra être utilisée pour lire des données dans le profil. Les paramètres sect et entr servent à préciser la section et l'entrée à lire. Si le paramètre entr est vide, toute la section est lue.
  • La valeur lue est renvoyée dans la chaîne dest, dont la longueur maximale (y compris le zéro terminal) doit être indiquée par dim. Si l'entrée n'est pas trouvée, la chaîne defaut est recopiée dans dest. LireCh renvoie un entier correspondant au nombre de caractères lus.
  • + LireInt permet de lire une entrée contenant une valeur entière (par exemple hauteur=600). La conversion de chaîne de caractères vers entier utilise la fonction C standard atoi, dont la valeur de retour (nombre lu ou zéro en cas d'erreur de conversion) est renvoyée.
  • + La fonction EcrireDonnees devra être utilisée pour toutes les opérations d'ajout, suppression et modification de profil. On peut modifier une entrée en indiquant la section, l'entrée et la valeur à modifier. Si l'entrée n'existe pas elle est crée, ainsi que la section, le cas échéant. La suppression d'une entrée se fait en appelant la fonction avec val = NULL, celle de toute une section avec entr = NULL. Les opérations de mise à jour en cascade sous-jacentes (profil, section, entrée) sont assurées par les fonctions membres protégées MajProfil, MajSection et MajEntree, ainsi que NouvelleSection et NouvelleEntree.


    On se reportera au code source pour plus de détail sur le fonctionnement des différentes fonctions membres.
>Les fonctions d'accès direct aux fichiers de profil

    Listing 4. Fonctions d'accès direct
int WritePrivateProfileString(char *section, char *entree, char *source, char *nomfich);
int GetPrivateProfileString(char *section, char *entree, char *defaut,
                            char *dest, int tdest, char *nomfich);
int GetPrivateProfileInt(char *section, char *entree, int defaut, char *nomfich);
l

    Ces fonctions reprennent intégralement la syntaxe des fonctions de l'API Windows utilisées pour l'accès aux fichiers *.INI. Un programme faisant appel à ces routines sera donc portable dans un autre environnement (Linux par exemple), grâce à l'utilisation de notre module.

    Ces fonctions encapsulent complètement l'utilisation de la classe FichProf, ce qui a pour avantage de rendre la manipulation de celle-ci complètement transparente.

    Par contre, en terme de performances, cette technique est nettement moins efficace que la création et l'utilisation manuelle d'une instance de la classe FichProf. En effet, chaque appel d'une fonction d'accès direct se traduit par la création, éventuellement la modification et la destruction d'un objet de type FichProf. Soit à chaque fois la lecture du fichier sur le disque et le cas échéant sa réécriture, ainsi que l'allocation et la désallocation des ressources mémoire correspondantes. On évitera donc d'utiliser ces fonctions pour des opérations multiples et successives de lecture/écriture de fichiers de profils.

    Trois fonctions de l'API Windows sont implémentées (consulter la documentation de Microsoft pour plus de détails) :
  • + GetPrivateProfileString permet de lire une entrée ou une section.
  • + GetPrivateProfileInt lit une entrée numérique entière.
  • + WritePrivateProfileString crée, modifie ou supprime une entrée ou une section


    Ces trois fonctions correspondent à l'appel respectif des méthodes LireCh, LireInt et EcrireDonnees de la classe FichProf.
>Exemple pratique

    Listing 5. Programme exemple
//==============================================================================
//
//    FICINI - Exemple d'utilisation des fichiers de profil
//
//==============================================================================

#include "FProfil.h"

FichProf *fp;


main()
{
  char NomFich[] = "Exemple.ini";
  char section[80], entree[80], valeur[80];
  char ch1[256], ch2[256];

  cout << "ficini - exemple d'utilisation de la classe fichprof\n">
  Attente();

  //----- Construction de l'objet fp -------------------------------------------
  fp = new FichProf(NomFich);  // => lecture automatique du fichier de profil
  if (!fp)
    return 0;
  cout << "1. lecture automatique du fichier au demarrage :\n">

  //----- Affichage du contenu du fichier --------------------------------------
  fp->Ouvrir(MA_LECTURE);
  while (!fp->Fin()) {
    fp->FichTxt::LireLgn(ch1);
    cout << ch1 << endl>
  }
  fp->Fermer();
  Attente();

  //----- Modification d'une valeur --------------------------------------------
  cout << "2. modification d'une valeur ([entree] pour conserver la valeur actuelle) :\n">
  fp->LireCh("application", "utilisateur", "", ch1, 256);
  cout << "valeur actuelle : " << ch1 << endl>
  cout << "nouvelle valeur : ">
  cin.getline(ch2, 255);
  if (*ch2)
    fp->EcrireDonnees("application", "utilisateur", ch2);
  Attente();

  //----- Ajout d'une nouvelle valeur ------------------------------------------
  cout << "3. ajout d'une valeur ([entree] pour annuler) :\n">
  cout << "section : ">
  cin.getline(section, 79);
  cout << "entree : ">
  cin.getline(entree, 79);
  cout << "valeur : ">
  cin.getline(valeur, 79);
  if (*section && *entree && *valeur)
    fp->EcrireDonnees(section, entree, valeur);
  Attente();

  //----- Ecriture manuelle et affichage du nouveau contenu du fichier ---------
  if (fp->Modif()) {
    cout << "4. ecriture du fichier avant la sortie de l'application :\n">
    fp->EcrireProfil();
    fp->Ouvrir(MA_LECTURE);
    while (!fp->Fin()) {
      fp->FichTxt::LireLgn(ch1);
      cout << ch1 << endl>
    }
    fp->Fermer();
  } else
      cout << "4. pas de modifications effectuees, fichier non enregistre\n\n">

  //----- Destruction de l'objet fp et fin -------------------------------------
  cout << "5. fin de l'application\n">
  Attente();
  delete fp;
  return 1;
}
l

    Le programme C++ FicIni (cf listing 5) illustre de manière simple l'utilisation de la classe FichProf dans une application.

    Un pointeur (fp) sur un objet de type FichProf est déclaré comme variable globale dans le module principal. Il sera donc accessible par tous les modules, où l'on pourra faire des opérations de lecture/écriture sur le profil.

    Cet objet est initialisé au début du programme principal (fonction main). Les attributs lectauto et ecrauto ayant été laissés à leur valeur par défaut, la création de l'objet entraîne la lecture du fichier de profil associé.

    Dans un programme plus élaboré, on prévoira le cas où le fichier de profil n'est pas trouvé sur le disque - le profil en mémoire est alors vide - et on utilisera des valeurs par défaut qui seront écrites dans un nouveau fichier.

    Tout au long du déroulement de l'application le profil sera accessible aussi bien en lecture qu'en écriture. Dans notre exemple, le contenu du fichier est d'abord affiché puis la modification d'une valeur existante et l'ajout d'une nouvelle valeur et/ou section sont testés.

    En fin de programme le profil est de nouveau écrit sur le disque s'il a été modifié depuis la dernière lecture.

    On voit donc à travers cet exemple, que l'on peut facilement manipuler les fichiers de profil à l'aide de la classe FichProf. On pourra essayer de modifier le programme afin de faire les mêmes opérations à l'aide des fonctions d'accès direct.

    Dans tous les cas, le code est parfaitement portable. Il est intéressant de le compiler et le tester sous Linux par exemple (attention aux particularités des fichiers texte sous Unix).

    Pour aller plus loin, on pourra chercher à améliorer ou compléter la classe FichProf, par exemple en utilisant le mécanisme des exceptions pour le traitement des erreurs, ou en dérivant des classes plus spécialisées pour la prise en compte de syntaxes plus élaborées dans les fichiers de paramètres...

    A vos claviers !
Cet article est la propriété de Denis ZIEGLER . La copie et la diffusion sont libres sauf dans un but lucratif sans accord explicite de l'auteur.