reseau social

Découverte QT 3, 3ème partie

Un article de ToutProgrammer.com.

Après 2 articles présentant les nouvelles fonctionnalités de QT 3 et le concept de base de QT (signaux et slots), Hervé Lefebvre nous propose dans ce 3ème article la mise en œuvre d'une petite application nous permettant de voir la simplicité d'utilisation de QT.

Voici le troisième volet de notre découverte du toolkit Qt-3. Nous allons encore nous limiter aux fonctions de Qt en général, et non pas, comme je vous l'avais promis, aux véritables nouveautés de Qt 3. Pourquoi ? Tout simplement parce qu'il semblerait que l'article précédent présentant le développement sous Qt ait été jugé intéressant par les lecteurs. Mais aussi (je l'avoue) parce que votre humble serviteur n'a tout simplement pas eu le temps de tester à fond les nouveautés de Qt-3. Donc pour vous faire patienter, je vous propose deux nouveaux articles qui, je l'espère, vous intéresseront.

Dans un premier article (Découverte QT 3, 1ère partie) j'ai évoqué la version 3 de Qt, et dans un second article (Découverte QT 3, 2ème partie) j'ai présenté les concepts de base de la programmation sous Qt. Pour cet article, je vous propose le développement d'une véritable application Qt. Non, pas encore une version édulcorée du sempiternel "Hello world", mais bel et bien un petit utilitaire qui se révèle être réellement "utile". Enfin en tout cas, en ce qui me concerne, il m'est très utile! Cerise sur le gâteau, nous verrons comment compiler l'application grâce à l'utilitaire "qmake", qui permet de générer des fichiers de construction Makefiles en fonction de la plate-forme système que vous utilisez.

Sommaire

[modifier] L'application

Voici pourquoi, initialement, j'ai développé ce petit utilitaire. J'ai le bonheur depuis quelque temps d'être connecté au net par l'ADSL, et autant dire que je me sers de cette connexion comme une liaison spécialisée :-)

Dès que ma machine démarre, elle se connecte, déclare son IP sur un DNS, et je peux ainsi contacter ma machine à tout moment. Mais le net n'est pas toujours bien fréquenté. Ainsi, quand je consulte mes fichiers de log, je constate parfois que des individus ont tenté de se connecter sur ma machine en testant des login "standards". De plus, j'ai constaté dans ces mêmes logs que j'ai un petit souci avec ma carte SCSI dont l'origine n'est toujours pas identifiée (incompatibilité carte mère, bug dans le kernel ... ?).

J'avais donc pris l'habitude d'avoir en permanence une console ouverte, dans laquelle s'exécutait un "tail -f /var/log/messages", pour avoir sous les yeux les messages envoyés par le système.

Las ! Ma console était toujours dissimulée sous d'autres fenêtres lorsqu'il s'y passait quelque chose "d'intéressant", de plus il était difficile d'identifier les messages pertinents au milieu du flot continuel de messages systèmes.

Je me suis donc fait une petite application Qt qui se comporte comme un "tail -f", mais également qui va filtrer les messages pour ne laisser passer que ceux qui contiennent une expression particulière.

Un niveau d'alerte est affecté à chaque message (0 = information, 1= Attention, 2= Alerte), en fonction de ce niveau, le message est affiché dans une couleur différente (respectivement vert, bleu, rouge), et lorsqu'un message de niveau 1 ou 2 apparaît, la fenêtre "surgit" automatiquement, même si elle était réduite en icône.

Logmon

Je vais vous décrire dans cet article l'application complète. Cependant, pour cette première version, la configuration est "codée en dur" dans le programme source. Nous verrons dans le prochain article comment mettre en oeuvre une boîte de dialogue de configuration, et la sauvegarde/restauration dans un fichier de configuration.

[modifier] La structure de l'application

Il nous faut tout d'abord un objet qui lise le fichier log, et détecte si le fichier a changé depuis la lecture précédente. Si c'est le cas, cet objet lira les nouvelles lignes dans le log, et les transmettra à l'affichage. Cet objet se nomme "filelog".

Ainsi que je viens de l'évoquer, il nous faut donc un objet d'affichage klogmonview qui recevra les lignes à afficher, les filtrera, et les affichera dans la bonne couleur. Le cas échéant, cet objet fera "surgir" la fenêtre de l'application.

Il nous faut bien sûr un objet application.

Il nous faut également un objet qui se charge, à intervalles réguliers, de déclencher l'objet filelog afin de vérifier si le fichier log a été modifié.

[modifier] L'implémentation

Commençons par le dernier objet. Pour cela c'est très simple, Qt a déjà fait tout le boulot. Il existe une classe nommée QTimer qui effectue ce genre de tâches. Il est donc inutile de ressortir votre doc pour savoir comment implémenter un timer sous Unix :-)

Attaquons-nous donc à l'objet filelog. Il nous faut un constructeur qui initialisera l'objet avec le nom du fichier à monitorer, ainsi que le temps devant s'écouler entre 2 vérifications. Nous avons également besoin d'une méthode sous forme de slot qui sera appelée par le "QTimer" afin de vérifier l'état du fichier. Et pour terminer un signal qui sera envoyé à chaque nouvelle ligne lue dans le fichier. Ce signal comportera en paramètre la chaîne de caractères composant cette nouvelle ligne. Voici donc à quoi va ressembler le fichier de déclaration de notre objet:

[modifier] fichier filelog.h

  1. #ifndef FILELOG_H
  2. #define FILELOG_H
  3.  
  4. #include <qobject.h>
  5. #include <qtimer.h>
  6. #include <qfile.h>
  7. #include <qtextstream.h>
  8.  
  9. // On fait hériter de QObject pour avoir le mécanisme signal/slot
  10. class fileLog : public QObject
  11. {
  12. Q_OBJECT
  13. public:
  14. // Interval est la durée en millisecondes entre 2 lectures du fichier
  15. fileLog(QString fileName, int interval = 5000, QObject * parent=0, const char * name=0 );
  16. ~fileLog();
  17.  
  18. // Le slot qui sera activé par notre timer
  19. public slots:
  20. void slotCheckFile();
  21.  
  22. // Émission d'une nouvelle ligne lue dans le fichier.
  23. signals:
  24. void sigNewLine ( QString);
  25.  
  26. // Notre descripteur de fichier, et la taille que
  27. // faisait le fichier lors de la précédente lecture.
  28. private:
  29. QFile *logFile;
  30. unsigned int previousSize;
  31. };
  32. #endif

Voilà, j'espère que pour l'instant c'est clair...

Maintenant passons aux choses sérieuses: l'implémentation proprement dite de notre classe. Bon, tout d'abord le constructeur. Son rôle est somme toute assez simple, il va instancier et initialiser le QTimer, initialiser notre QFile (qui est un descripteur de fichier, mais implémenté sous forme d'objet Qt).

[modifier] fichier filelog.cpp

  1. #include "filelog.h"
  2.  
  3. fileLog::fileLog(const QString fileName,const int interval, QObject * parent, const char * name ) : QObject (parent, name)
  4. {
  5. // Créer le timer.
  6. QTimer *timer = new QTimer( this );
  7. // On connecte le signal du timer au slot de notre objet
  8. connect( timer, SIGNAL(timeout()), this, SLOT(slotCheckFile()) );
  9. // On démarre le timer, il enverra son signal à chaque interval, FALSE indique
  10. // que le timer doit être cyclique (ne pas s'arrêter au 1er interval)
  11. timer->start( interval, FALSE );
  12.  
  13.  
  14. // Initialisation du QFile
  15. logFile = new QFile( fileName);
  16. // Ouverture du fichier
  17. logFile->open( IO_ReadOnly ); // index set to 0
  18.  
  19. // Mémoriser la taille du fichier pour détecter les nouveaux messages
  20. previousSize=logFile->size();
  21. // Fermeture du fichier
  22. logFile->close();
  23. }
  24.  
  25. // ... Et le destructeur tant qu'on y est ... :-)
  26. fileLog::~fileLog(){
  27. }

Voilà, je pense que ce n'est pas bien sorcier non? Bon, il ne nous reste plus que la méthode "slotCheckFile()" qui est connectée au signal du timer. Vous allez voir, elle est tout aussi simple:

[modifier] fichier filelog.cpp (suite...)

  1. void fileLog::slotCheckFile()
  2. {
  3. QString newLine;
  4. logFile->open( IO_ReadOnly );
  5.  
  6. // Si la taille du fichier change, on lit les lignes jusqu'à la fin du fichier
  7. if (previousSize!=logFile-&gt;size())
  8. {
  9. // On vérifie des fois que le fichier aurait été
  10. // réinitialisé entre temps (logrotate...)
  11. if (previousSize>logFile->size())
  12. {
  13. previousSize = 0;
  14. }
  15.  
  16. // Création d'un flux de texte à partir de notre fichier. Très pratique...
  17. QTextStream tStream( logFile );
  18. // On se positionne là où le fichier a été modifié
  19. logFile->at(previousSize);
  20. // On lit les lignes de texte jusqu'à la fin du fichier
  21. while (!tStream.eof())
  22. {
  23. newLine = tStream.readLine();
  24. // Maintenant on émet le signal pour prévenir qu'on a une nouvelle ligne
  25. emit sigNewLine(newLine);
  26. }
  27. previousSize=logFile-&gt;size(); // mémoriser la nvlle taille du fichier
  28. }
  29. logFile-&gt;close(); // fermer le fichier
  30. }

Voilà ... c'est joli non ? Nous avons donc maintenant un objet qui se déclenche à intervalles réguliers, va vérifier si un fichier texte contient de nouvelles lignes, et si c'est le cas émet un signal avec en paramètre la nouvelle ligne. Si le fichier contient n nouvelles lignes, n signaux seront émis successivement. Au passage, admirez la beauté du mécanisme de signal/slot.

En effet dans notre objet nous n'avons pas besoin de savoir quoique ce soit sur le ou les objets qui seront connectés au signal. Si un jour nous voulons que les signaux soient traités dans 3 fenêtres différentes (une par niveau d'alerte), il nous suffira de développer ces 3 fenêtres, et de les connecter toutes les 3 au signal sigNewLine. Nous n'aurons absolument pas besoin de modifier quoi que ce soit dans notre objet fileLog. Ce mécanisme de signal/slot permet ainsi une bien meilleure encapsulation que les méthodes d'appel traditionnelles.

Allez, trêve de flâneries. Passons à notre objet d'affichage. Nous allons utiliser pour cela une QListView. Ce Widget Qt permet d'afficher toutes sortes de listes. Listes simples, arborescentes etc. C'est le "couteau suisse" de la listbox :-)

Là nous allons l'utiliser très simplement avec 4 colonnes : Pour le numéro du message, la date et l'heure, le niveau d'alerte, et le message proprement dit.

Il nous faut stocker également la liste des sous-chaînes qui caractériseront les messages à filtrer et afficher (error, login failed ...). Pour cela on va utiliser un template Qt nommé "QPtrList". Ce template offre une gestion complète de liste doublement chaînée. C'est un objet très pratique. Attention, avant Qt-3 ce template se nommait QList.

Notre objet va donc être une extension très simple de QListView.

  1. #ifndef KLOGMONVIEW_H
  2. #define KLOGMONVIEW_H
  3.  
  4. #include <qwidget.h>
  5. #include <qlistview.h>
  6. #include <qptrlist.h>
  7. #include <qstring.h>
  8. #include <qpainter.h>
  9. #include <qdatetime.h>
  10.  
  11. class KlogmonView : public QListView
  12. {
  13. Q_OBJECT
  14. public:
  15. KlogmonView(QWidget *parent = 0, const char *name=0);
  16. ~KlogmonView();
  17.  
  18. private:
  19. int compteur; // Pour numéroter les messages
  20.  
  21. // Voici maintenant un tableau de 3 listes de chaînes
  22. // de caractères. Ce sont les mots-clés définissant chacun
  23. // des 3 niveaux d'alerte
  24. QPtrList<QString> logLvl[3];
  25.  
  26. // Et voici le slot qui recevra les nouvelles lignes
  27. // en provenance de l'objet filelog
  28. public slots: // Public slots
  29. void slotNewLine(QString s);
  30. };
  31.  
  32. #endif // KLOGMONVIEW_H

Au sujet de la QListView, nous allons faire une petite bidouille intéressante. Chaque ligne de la QListView est un QListViewItem que l'on instancie de la manière suivante (entre autres) : new QListViewItem(_QListView,"Texte col1","Colonne 2",...) Comme je l'ai dit plus haut, je désire que mes lignes s'affichent dans une couleur différente en fonction du niveau d'alerte. Or, vous pouvez vérifier dans la doc, aucune méthode de la QListView ou QListViewItem ne permet de faire cela. Cependant, Qt est malgré tout bien fait, et QListViewItem possède une méthode virtuelle paintCell(QPainter *, QColorGroup, int, int, int) qui se charge de "peindre" la colonne d'un Item. Il nous suffit donc de créer un descendant de QListViewItem qui va surcharger cette méthode virtuelle, modifier le contexte d'affichage afin de modifier la couleur de texte en fonction du niveau d'alerte, appeler la méthode virtuelle de l'ancêtre, puis rétablir le contexte d'affichage. Pour information, la méthode texte(n) retourne le contenu de la colonne n de l'item, et QString::compare(char *) effectue une comparaison de chaînes de caractères. Notre fichier klogmonview.cpp va donc commencer de la manière suivante:

  1. #include "filelog.h"
  2. #include "klogmonview.h"
  3.  
  4. class logItem : public QListViewItem
  5. {
  6. public:
  7. logItem(QListView *parent,QString no,QString lvl, QString date, QString message, int level=1) : QListViewItem(parent,no,lvl,date,message)
  8. {
  9. }
  10. ~logItem()
  11. {
  12. }
  13. void paintCell( QPainter * p, const QColorGroup &amp; cg, int column, int width, int align )
  14. {
  15. // On déclare une nouvelle couleur
  16. QColor color;
  17.  
  18. // En fonction du niveau d'alerte, on met du rouge, vert, bleu ...
  19. if (text(1).compare("2")==0)
  20. color=QColor(255,0,0);
  21. else if(text(1).compare("1")==0)
  22. color=QColor(0,255,0);
  23. else if(text(1).compare("0")==0)
  24. color=QColor(0,0,255);
  25. else
  26. color=QColor(128,128,128);
  27.  
  28. // On sauvegarde la palette de couleurs
  29. QColorGroup _cg( cg );
  30. QColor oldText=_cg.text();
  31.  
  32. // On modifie la palette de couleurs en définissant
  33. // notre nouvelle couleur comme couleur de texte
  34. _cg.setColor( QColorGroup::Text, color );
  35.  
  36. // On appelle la méthode virtuelle de l'ancêtre avec la nouvelle palette
  37. QListViewItem::paintCell( p, _cg, column, width, align );
  38.  
  39. // On rétablit la palette de couleurs "standard"
  40. _cg.setColor( QColorGroup::Text, oldText );
  41.  
  42. }
  43.  
  44. };

Et voilà, c'est aussi simple que cela! Qu'est ce qu'on rigole hein? :-)

Bon, pour être tout à fait honnête, je dois dire que cela va bien tant qu'on se limite à changer la couleur. Si l'on désirait changer le corps de texte (gras, italique...) ou même carrément la fonte, il faudrait surcharger de la même manière la méthode virtuelle width() qui retourne la taille (en pixels) du texte se trouvant dans la case. Faute de quoi, les largeurs de colonnes par défaut seraient mal choisies, et l'ergonomie s'en ressentirait.

Bon, assez joué, voici la suite de notre objet:

  1. KlogmonView::KlogmonView(QWidget *parent, const char *name)&nbsp;: QListView(parent, name)
  2. {
  3. setBackgroundMode(PaletteBase);
  4. compteur=0;
  5. setMinimumSize(100,100);
  6. addColumn(QString("No"));
  7. addColumn(QString("Lvl"));
  8. addColumn(QString("Date"));
  9. addColumn(QString("Message"));
  10. fileLog *fl = new fileLog(QString("/var/log/messages"), 5000,this);
  11. logLvl[2].append(new QString("LOGIN"));
  12. logLvl[2].append(new QString("KERNEL"));
  13. logLvl[2].append(new QString("I/O"));
  14. logLvl[1].append(new QString("ftpd"));
  15. logLvl[1].append(new QString("error"));
  16. logLvl[0].append(new QString("session"));
  17. connect (fl, SIGNAL(sigNewLine(QString)), this, SLOT(slotNewLine(QString)));
  18. }
  19.  
  20. KlogmonView::~KlogmonView()
  21. {
  22. }
  23.  
  24. // Méthode appelée lorsqu'une nouvelle ligne arrive
  25. void KlogmonView::slotNewLine(QString s)
  26. {
  27. int i;
  28. QString *pattern;
  29.  
  30. // Tester le message avec nos 3 listes de mots-clés
  31. for (i=2; i&gt;=0; i--)
  32. {
  33. // Parcourir la liste des mots-clés
  34. for ( pattern = logLvl[i].first(); pattern&nbsp;; pattern = logLvl[i].next() )
  35. if (s.contains(*pattern,FALSE)) // Rechercher la sous-chaîne (non "case sensitive")
  36. {
  37. // QString("%1").arg(valeur,6,10) signifie
  38. // afficher sur 6 caractères la "valeur" et en base 10
  39. logItem *li = new logItem(this,QString("%1").arg(compteur++,6,10),QString("%1").arg(i,0,10), QDateTime::currentDateTime().toString(), s);
  40. parentWidget()-&gt;showNormal(); // restaurer la fenêtre (si elle était en icone)
  41. parentWidget()-&gt;raise(); // Faire "surgir" la fenêtre au dessus des autres
  42. }
  43. }
  44. }

Bon, j'avais oublié le main.cpp :-)

[modifier] fichier main.cpp

  1. #include &lt;qapplication.h&gt;
  2. #include "klogmonview.h"
  3. #include <qvbox.h>
  4.  
  5. int main(int argc, char *argv[])
  6. {
  7. QApplication app( argc, argv );
  8.  
  9. QVBox box;
  10. KlogmonView *klogmon = new KlogmonView(&amp;box);
  11. app.setMainWidget(&amp;box);
  12. box.show();
  13.  
  14. return app.exec();
  15. }

[modifier] Utiliser qmake et compiler

qmake est un utilitaire merveilleux, et absolument indispensable pour un outil qui se prétend être multi-plate-forme comme QT. Un court exemple valant mieux qu'un long discours, voici la marche à suivre :

D'abord définir les variables d'environnement suivantes (sous Linux, sous windows je sais pas lesquelles définir, ni comment les définir ):

export QTDIR=/opt/qt3

Le répertoire où est installé Qt. Puis :

export PATH=$QTDIR/bin/ :$PATH

afin d'avoir accès à qmake et moc (le compilateur de méta-objets). Ensuite:

export LD_LIBRARY_PATH=$QTDIR/lib :$LD_LIBRARY_PATH

Afin de pouvoir linker avec la librairie de Qt. Pour terminer, un truc "bizarre" :

export QMAKESPEC=$QTDIR/mkspecs/linux-g++

Ça, c'est pour définir la plate-forme sur laquelle on désire compiler.

Une fois tout cet environnement de développement défini (vous trouvez ça compliqué ? Vous avez déjà écrit un Makefile ou bien paramétré un visual C++ ?), il nous suffit de dire ce que contient notre projet. Pour cela, mettons dans un fichier logmon.pro les lignes suivantes :

HEADERS = filelog.h klogmonview.h
SOURCES = main.cpp filelog.cpp klogmonview.cpp
TARGET = logmon

HEADERS indique la liste de nos fichiers .h, SOURCES les fichiers .cpp, TARGET le nom de l'exécutable que nous désirons générer. L'outil qmake se chargera de détecter les fichiers headers qui nécessitent un passage au méta-compilateur (moc).

Ensuite, avec la commande:

qmake logmon.pro -o Makefile

afin de générer le Makefile.

Puis, pour compiler :

make

Une dernière chose, comme notre exécutable doit lire un fichier réservé au super-utilisateur (fichier /var/log/messages), il nous faut changer les droits de l'exécutable (en tant que root):

[root]# chown root logmon
[root]# chmod u+s logmon

Et voilà ! C'est fini !

Dans le prochain article, nous verrons les boîtes de dialogue et les fichiers de configuration. Amusez -vous bien !

Vous trouverez en pièce jointe le code source et le fichier projet de cette petite application sous forme de tgz. Pour la compiler, vous aurez besoin de QT 3.0 bêta 3.

Source de logmon

[modifier] Historique de l'article

Cet article, réalisé par Hervé LEFEBVRE, a été publié pour la première fois le 7 août 2001 sur le site LinuxFrench.net sous licence Creative Commons License Non Commerciale.