Vous êtes sur la page 1sur 571

Qt 4 et ++ C

http://www.free-livres.com/

CampusPress Rfrence

Programmation dinterfaces GUI

Jasmin Blanchette et Mark Summereld Prface de Matthias Ettrich

Rseaux et tlcom

Programmation

Gnie logiciel

Scurit

Systme dexploitation

Qt4 et C++
Programmation dinterfaces GUI
Jasmin Blanchette et Mark Summereld

CampusPress a apport le plus grand soin la ralisation de ce livre an de vous fournir une information complte et able. Cependant, CampusPress nassume de responsabilits, ni pour son utilisation, ni pour les contrefaons de brevets ou atteintes aux droits de tierces personnes qui pourraient rsulter de cette utilisation. Les exemples ou les programmes prsents dans cet ouvrage sont fournis pour illustrer les descriptions thoriques. Ils ne sont en aucun cas destins une utilisation commerciale ou professionnelle. CampusPress ne pourra en aucun cas tre tenu pour responsable des prjudices ou dommages de quelque nature que ce soit pouvant rsulter de lutilisation de ces exemples ou programmes. Tous les noms de produits ou marques cits dans ce livre sont des marques dposes par leurs propritaires respectifs. Publi par CampusPress 47 bis, rue des Vinaigriers 75010 PARIS Tl. : 01 72 74 90 00 Mise en pages : TyPAO Titre original : C++ GUI programming with Qt 4, Traduit de lamricain par Christine Eberhardt, Chantal Kolb, Dorothe Sittler

ISBN original : 0-13-187249-4 Copyright 2006 Trolltech S.A.

ISBN : 978-2-7440-4092-4 Copyright 2009 Pearson Education France Tous droits rservs
All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc. Aucune reprsentation ou reproduction, mme partielle, autre que celles prvues larticle L. 122-5 2 et 3 a) du code de la proprit intellectuelle ne peut tre faite sans lautorisation expresse de Pearson Education France ou, le cas chant, sans le respect des modalits prvues larticle L. 122-10 dudit code.

Table des matires

A propos des auteurs ............................. Avant-propos .......................................... Prface .................................................... Remerciements ...................................... Bref historique de Qt ............................ Partie I - Qt : notions de base................ CHAPITRE 1. Pour dbuter .................... Hello Qt ............................................. Etablir des connexions ....................... Disposer des widgets .......................... Utiliser la documentation de rfrence

VII IX XI XIII XV 1 3 4 6 7 10

Implmenter le menu File ................ Utiliser des botes de dialogue ........... Stocker des paramtres ....................... Documents multiples ......................... Pages daccueil ...................................

57 64 70 72 75

CHAPITRE 4. Implmenter la fonctionnalit dapplication ..................................... 77 Le widget central ................................ Drivation de QTableWidget ............. Chargement et sauvegarde ................. Implmenter le menu Edit ................. Implmenter les autres menus ............ Drivation de QTableWidgetItem ..... CHAPITRE 5. Crer des widgets personnaliss ..................................... Personnaliser des widgets Qt ............. Driver QWidget ............................... Intgrer des widgets personnaliss avec le Qt Designer ........................... Double mise en mmoire tampon ...... Partie II - Qt : niveau intermdiaire .... 78 79 85 88 92 96 105 106 108 118 122 141 143 144 150 152

CHAPITRE 2. Crer des botes de dialogue 15 Drivation de QDialog ...................... 16 Description dtaille des signaux et slots 22 Conception rapide dune bote de dialogue ...................... 25 Botes de dialogue multiformes ......... 32 Botes de dialogue dynamiques ......... 39 Classes de widgets et de botes de dialogue intgres ............................................ 40 CHAPITRE 3. Crer des fentres principales ......................................... Drivation de QMainWindow ........... Crer des menus et des barres doutils Congurer la barre dtat ................... 45 46 51 56

CHAPITRE 6. Gestion des dispositions .. Disposer des widgets sur un formulaire Dispositions empiles ........................ Sparateurs .........................................

IV

Qt4 et C++ : Programmation dinterfaces GUI

Zones droulantes ..................................... Widgets et barres doutils ancrables ........ MDI (Multiple Document Interface) ........ CHAPITRE 7. Traitement des vnements .... Rimplmenter les gestionnaires dvnements ............................................ Installer des ltres dvnements .............. Rester ractif pendant un traitement intensif CHAPITRE 8. Graphiques 2D et 3D .............. Dessiner avec QPainter ............................. Transformations du painter ....................... Afchage de haute qualit avec QImage . Impression ................................................ Graphiques avec OpenGL ......................... CHAPITRE 9. Glisser-dposer ....................... Activer le glisser-dposer .......................... Prendre en charge les types personnaliss de glisser ................................................... Grer le presse-papiers ..............................

155 157 159 169 170 175 178 183 184 188 197 199 207 213 214 219 224

CHAPITRE 13. Les bases de donnes ............ Connexion et excution de requtes ......... Prsenter les donnes sous une forme tabulaire .................................................... Implmenter des formulaires matre/dtail CHAPITRE 14. Gestion de rseau ................ Programmer les clients FTP .................... Programmer les clients HTTP ................. Programmer les applications client/serveur TCP ................................... Envoi et rception de datagrammes UDP . CHAPITRE 15. XML ...................................... Lire du code XML avec SAX ................... Lire du code XML avec DOM .................. Ecrire du code XML ................................. CHAPITRE 16. Aide en ligne ......................... Infobulles, informations dtat et aide "Quest-ce que cest ?" ............................. Utilisation de QTextBrowser comme moteur daide simple ............................................. Utilisation de lassistant pour une aide en ligne puissante ...................................... Partie III - Qt : tude avance .................... CHAPITRE 17. Internationalisation .............. Travailler avec Unicode ............................ Crer des applications ouvertes aux traductions ......................................... Passer dynamiquement dune langue une autre ................................................. Traduire les applications .......................... CHAPITRE 18. Environnement multithread Crer des threads ...................................... Synchroniser des threads .......................... Communiquer avec le thread principal .... Utiliser les classes Qt dans les threads secondaires ...............................................

309 310 317 321 329 330 339 342 353 359 360 365 370 373 374 376 379 383 385 386 390 396 402 407 408 411 418 423

CHAPITRE 10. Classes daf chage dlments 227 Utiliser les classes ddies lafchage dlments ................................................. Utiliser des modles prdnis ................. Implmenter des modles personnaliss ... Implmenter des dlgus personnaliss .. CHAPITRE 11. Classes conteneur ................. Conteneurs squentiels ............................ Conteneurs associatifs .............................. Algorithmes gnriques ........................... Chanes, tableaux doctets et variants ....... CHAPITRE 12. Entres/Sorties ...................... Lire et crire des donnes binaires ............ Lire et crire du texte ................................ Parcourir les rpertoires ............................ Intgration des ressources ........................ Communication inter-processus ............... 229 236 241 256 263 264 273 276 278 287 288 294 300 302 303

Table des matires

CHAPITRE 19. Crer des plug-in .................. Dvelopper Qt avec les plug-in ................ Crer des applications capables de grer les plug-in ................................................ Ecrire des plug-in dapplication ............... CHAPITRE 20. Fonctionnalits spci ques la plate-forme ....................................... Construire une interface avec les API natives ActiveX sous Windows ............................. Prendre en charge la gestion de session X11 CHAPITRE 21. Programmation embarque . Dmarrer avec Qtopia ............................... ANNEXE A. Installer Qt ................................
A propos des licences .............................

425 426 435 439

443 444 448 461 469 470 477 478 478 479 479

Installer Qt/Windows ................................ Installer Qt/Mac ........................................ Installer Qt/X11 ........................................

ANNEXE B. INTRODUCTION AU LANGAGE C++ POUR LES PROGRAMMEURS JAVA ET C# .......... 483. Dmarrer avec C++ ................................... 484 Principales diffrences de langage ............ 489 Les types de donnes primitifs ................ 489 Dnitions de classe .............................. 490 Les pointeurs ......................................... 497 Les rfrences ....................................... 500 Les tableaux .......................................... 502 Les chanes de caractres ...................... 505 Les numrations .................................. 507 TypeDef ................................................. 509 Conversions de type ............................... 509 Surcharge doprateur ........................... 512 Les types valeur ..................................... 514 Variables globales et fonctions ............... 516 Espaces de noms ................................... 518 Le prprocesseur ................................... 520 La bibliothque C++ standard .................. 523 Index ............................................................. 529

A propos des auteurs

Jasmin Blanchette

Jasmin a obtenu un diplme dinformatique en 2001 lUniversit de Sherbrooke, Qubec. Il a fait un stage chez Trolltech durant lt 2000 en tant quingnieur logiciel et travaille dans cette socit depuis dbut 2001. En 2003, Jasmin a co-crit C++ GUI Programming with Qt 3. Il assume dsormais la double responsabilit de directeur de la documentation et dingnieur logiciel senior chez Trolltech. Il a dirig la conception de loutil de traduction Qt Linguist et il reste un acteur cl dans la conception des classes conteneur de Qt 4. Il est galement le co-diteur de Qt Quarterly, la lettre dinformation technique de Trolltech.
Mark Summereld

Mark a obtenu un diplme dinformatique en 1993 lUniversit de Wales Swansea. Il sest ensuite consacr une anne de recherche avant de se lancer dans le monde du travail. Il a travaill pendant plusieurs annes comme ingnieur logiciel pour diverses entreprises avant de rejoindre Trolltech. Il a t directeur de la documentation de Trolltech pendant prs de trois ans, priode pendant laquelle il a cr Qt Quarterly et co-crit C++ GUI Programming with Qt 3. Mark dtient un Qtraining.eu et est formateur et consultant indpendant spcialis en langage C++, Qt et Python.

Avant-propos

Pourquoi Qt ? Pourquoi des programmeurs comme nous en viennent-ils choisir Qt ? Bien entendu, les rponses sont videntes : la compatibilit de Qt, les nombreuses fonctionnalits, les performances du C++, la disponibilit du code source, la documentation, le support technique de grande qualit et tous les autres lments mentionns dans les documents marketing de Trolltech. Pour nir, voici laspect le plus important : Qt rencontre un tel succs, parce que les programmeurs lapprcient. Pourquoi des programmeurs vont-ils apprcier une technologie et pas une autre ? Je pense personnellement que les ingnieurs logiciel aiment une technologie qui semble bonne, mais napprcient pas celles qui ne le sont pas. "Bonne" dans ce cas peut avoir diverses signications. Dans ldition Qt 3 du livre, jai voqu le systme tlphonique de Trolltech comme tant un exemple particulirement bon dune technologie particulirement mauvaise. Le systme tlphonique ntait pas bon, parce quil nous obligeait effectuer des tches apparemment alatoires qui dpendaient dun contexte galement alatoire. Le hasard nest pas une bonne chose. La rptition et la redondance sont dautres aspects qui ne sont pas bons non plus. Les bons programmeurs sont fainants. Ce que nous aimons dans linformatique par rapport au jardinage par exemple, cest que nous ne sommes pas contraints de faire continuellement les mmes choses. Laissez-moi approfondir sur ce point laide dun exemple issu du monde rel : les notes de frais. En gnral, ces notes de frais se prsentent sous forme de tableurs complexes ; vous les remplissez et vous recevez votre argent en retour. Technologie simple me direz-vous. De plus, vu lincitation pcuniaire, cela devrait constituer une tche aise pour un ingnieur expriment. Toutefois, la ralit est bien diffrente. Alors que personne dautre dans lentreprise ne semble avoir de soucis pour remplir ces formulaires, les ingnieurs en ont. Et pour en avoir discut avec des salaris dautres entreprises, cela parat tre un comportement commun. Nous repoussons le remboursement jusquau dernier moment, et il arrive mme parfois que nous loublions. Pourquoi ? Au vu de notre formulaire, cest une procdure standard simple. La personne doit rassembler les reus, les numroter et insrer

Qt4 et C++ : Programmation dinterfaces GUI

ces numros dans les champs appropris avec la date, lendroit, une description et le montant. La numrotation et la copie sont conues pour faciliter la tche, mais ce sont des oprations redondantes, sachant que la date, lendroit, la description et le montant identient sans aucun doute un reu. On pourrait penser quil sagit dun bien petit travail pour obtenir un remboursement. Le tarif journalier qui dpend du lieu de sjour est quelque peu embarrassant. Il existe des documents quelque part qui rpertorient les tarifs normaliss pour les divers lieux de sjour. Vous ne pouvez pas simplement choisir "Chicago" ; vous devez rechercher le tarif correspondant Chicago vous-mme. Il en va de mme pour le champ correspondant au taux de change. La personne doit trouver le taux de change en vigueur peut-tre laide de Google puis saisir le taux dans chaque champ. En clair, vous devez attendre que votre banque vous fasse parvenir une lettre dans laquelle ils vous informent du taux de change appliqu. Mme si lopration ne savre pas trs complique, il nest pas commode de rechercher toutes ces informations dans diffrentes sources, puis de les recopier plusieurs endroits dans le formulaire. La programmation peut normment ressembler ces notes de frais, mais en pire. Et cest l que Qt vient votre rescousse. Qt est diffrent. Dun ct, Qt est logique. Dun autre, Qt est amusant. Qt vous permet de vous concentrer sur vos tches. Quand les architectes de Qt rencontraient un problme, ils ne cherchaient pas uniquement une solution convenable ou la solution la plus simple. Ils recherchaient la bonne solution, puis lexpliquaient. Cest vrai quils faisaient des erreurs et que certaines de leurs dcisions de conception ne rsistaient pas au temps, mais ils proposaient quand mme beaucoup de bonnes choses et les mauvais cts pouvaient toujours tre amliors. Imaginez quun systme conu lorigine pour mettre en relation Windows 95 et Unix/Motif unie dsormais des systmes de bureau modernes aussi divers que Windows XP, Mac OS X et GNU/Linux, et pose les fondements de la plate-forme applicative Qtopia pour Linux Embarqu. Bien avant que Qt ne devienne ce produit si populaire et si largement utilis, la dvotion avec laquelle les dveloppeurs de ce framework recherchaient les bonnes solutions rendait Qt vraiment particulier. Ce dvouement est toujours aussi fort aujourdhui et affecte quiconque dveloppe et assure la maintenance de Qt. Pour nous, travailler avec Qt constitue une grande responsabilit et un privilge. Nous sommes ers de contribuer rendre vos existences professionnelles et open source plus simples et plus agrables. Matthias Ettrich Oslo, Norvge Juin 2006

Prface

Qt est un framework C++ permettant de dvelopper des applications GUI multiplatesformes en se basant sur lapproche suivante : "Ecrire une fois, compiler nimporte o." Qt permet aux programmeurs demployer une seule arborescence source pour des applications qui sexcuteront sous Windows 98 XP, Mac OS X, Linux, Solaris, HP-UX, et de nombreuses autres versions dUnix avec X11. Les bibliothques et outils Qt font galement partie de Qtopia Core, un produit qui propose son propre systme de fentrage au-dessus de Linux Embarqu. Le but de ce livre est de vous apprendre crire des programmes GUI avec Qt 4. Le livre commence par "Hello Qt" et progresse rapidement vers des sujets plus avancs, tels que la cration de widgets personnaliss et le glisser-dposer. Le code source des exemples de programmes est tlchargeable sur le site Pearson, www.pearson.fr, la page ddie cet ouvrage. LAnnexe A vous explique comment installer le logiciel. Ce manuel se divise en trois parties. La Partie I traite de toutes les notions et pratiques ncessaires pour programmer des applications GUI avec Qt. Si vous ntudiez que cette partie, vous serez dj en mesure dcrire des applications GUI utiles. La Partie II aborde des thmes importants de Qt trs en dtail et la Partie III propose des sujets plus spcialiss et plus avancs. Vous pouvez lire les chapitres des Parties II et III dans nimporte quel ordre, mais cela suppose que vous connaissez dj le contenu de la Partie I. Les lecteurs de ldition Qt 3 de ce livre trouveront cette nouvelle dition familire tant au niveau du contenu que du style. Cette dition a t mise jour pour proter des nouvelles fonctionnalits de Qt 4 (y compris certaines qui sont apparues avec Qt 4.1) et pour prsenter du code qui illustre les techniques de programmation Qt 4. Dans la plupart des cas, nous avons utilis des exemples similaires ceux de ldition Qt 3. Les nouveaux lecteurs nen seront pas affects, mais ceux qui ont lu la version prcdente pourront mieux se reprer dans le style plus clair, plus propre et plus expressif de Qt 4.

XII

Qt4 et C++ : Programmation dinterfaces GUI

Cette dition comporte de nouveaux chapitres analysant larchitecture modle/vue de Qt 4, le nouveau framework de plug-in et la programmation intgre avec Qtopia, ainsi quune nouvelle annexe. Et comme dans ldition Qt 3, nous nous sommes concentrs sur lexplication de la programmation Qt plutt que de simplement hacher ou rcapituler la documentation en ligne de Qt. Nous avons crit ce livre en supposant que vous connaissez les langages C++, Java ou C#. Les exemples de code utilisent un sous-ensemble du langage C++, tout en vitant de multiples fonctions C++ rarement ncessaires en programmation Qt. L o il ntait pas possible dviter une construction C++ plus avance, nous vous avons expliqu son utilisation. Si vous connaissez dj le langage Java ou C# mais que vous avez peu dexprience en langage C++, nous vous recommandons de commencer par la lecture de lAnnexe B, qui vous offre une introduction au langage C++ dans le but de pouvoir utiliser ce manuel. Pour une introduction plus pousse la programmation oriente objet en C++, nous vous conseillons les livres C++ How to Program de Harvey Deitel et Paul Deitel, et C++Primer de Stanley B. Lippman, Jose Lajoie, et Barbara E. Moo. Qt a bti sa rputation de framework multiplate-forme, mais en raison de son API intuitive et puissante, de nombreuses organisations utilisent Qt pour un dveloppement sur une seule plate-forme. Adobe Photoshop Album est un exemple dapplication Windows de masse crite en Qt. De multiples logiciels sophistiqus dentreprise, comme les outils danimation 3D, le traitement de lms numriques, la conception automatise lectronique (pour concevoir des circuits), lexploration de ptrole et de gaz, les services nanciers et limagerie mdicale sont gnrs avec Qt. Si vous travaillez avec un bon produit Windows crit en Qt, vous pouvez facilement conqurir de nouveaux marchs dans les univers Mac OS X et Linux simplement grce la recompilation. Qt est disponible sous diverses licences. Si vous souhaitez concevoir des applications commerciales, vous devez acheter une licence Qt commerciale ; si vous voulez gnrer des programmes open source, vous avez la possibilit dutiliser ldition open source (GPL). Qt constitue la base sur laquelle sont conus lenvironnement KDE (K Desktop Environment) et les nombreuses applications open source qui y sont lies. En plus des centaines de classes Qt, il existe des compagnons qui tendent la porte et la puissance de Qt. Certains de ces produits, tels que QSA (Qt Script for Applications) et les composants Qt Solutions, sont disponibles par le biais de Trolltech, alors que dautres sont fournis par dautres socits et par la communaut open source. Consultez le site http://www.trolltech.com/products/3rdparty/ pour plus dinformations sur les compagnons Qt. Qt possde galement une communaut bien tablie et prospre dutilisateurs qui emploie la liste de diffusion qt-interest; voir http://lists.trolltech.com/ pour davantage de dtails. Si vous reprez des erreurs dans ce livre, si vous avez des suggestions pour la prochaine dition ou si vous voulez simplement faire un commentaire, nhsitez pas nous contacter. Vous pouvez nous joindre par mail ladresse suivante : qt-book@trolltech.com. Un erratum sera disponible sur le site http://doc.trolltech.com/qt-book-errata.html.

Remerciements

Nous tenons tout dabord remercier Eirik Chambe-Eng, le prsident de Trolltech. Eirik ne nous a pas uniquement encourag avec enthousiasme crire ldition Qt 3 du livre, il nous a aussi permis de passer un temps considrable lcrire. Eirik et Haavard Nord, le chef de la direction de Trolltech, ont lu le manuscrit et lont approuv. Matthias Ettrich, le dveloppeur en chef de Trolltech, a galement fait preuve dune grande gnrosit. Il a allgrement accept que nous manquions nos tches lorsque nous tions obnubils par lcriture de la premire dition de ce livre et nous a fortement conseill de sorte adopter un bon style de programmation Qt. Pour ldition Qt 3, nous avons demand deux utilisateurs Qt, Paul Curtis et Klaus Schmidinger, de devenir nos critiques externes. Ce sont tous les deux des experts Qt trs pointilleux sur les dtails techniques qui ont prouv leur sens du dtail en reprant des erreurs trs subtiles dans notre manuscrit et en suggrant de nombreuses amliorations. En plus de Matthias, Reginald Stadlbauer tait notre plus loyal critique chez Trolltech. Ses connaissances techniques sont inestimables et il nous a appris faire certaines choses dans Qt que nous ne pensions mme pas possible. Pour cette dition Qt 4, nous avons galement prot de laide et du support dEirik, Haavard et Matthias. Klaus Schmidinger nous a aussi fait bncier de ses remarques et chez Trolltech, nos principaux critiques taient Andreas Aardal Hanssen, Henrik Hartz, Vivi Glckstad Karlsen, Trenton Schulz, Andy Shaw et Pl de Vibe. En plus des critiques mentionns ci-dessus, nous avons reu laide experte de Harald Fernengel (bases de donnes), Volker Hilsheimer (ActiveX), Bradley Hughes (multithread), Trond Kjernsen (graphiques 3D et bases de donnes), Lars Knoll (graphiques 2D et internationalisation), Sam Magnuson (qmake), Marius Bugge Monsen (classes dafchage dlments), Dimitri Papadopoulos (Qt/X11), Paul Olav Tvete (widgets personnaliss et programmation intgre), Rainer Schmid (mise en rseau et XML), Amrit Pal Singh (introduction C++) et Gunnar Sletta (graphiques 2D et traitement dvnements).

XIV

Qt4 et C++ : Programmation dinterfaces GUI

Nous tenons aussi remercier tout particulirement les quipes de Trolltech en charge de la documentation et du support pour avoir gr tous les problmes lis la documentation lorsque le livre nous prenait la majorit de notre temps, de mme que les administrateurs systme de Trolltech pour avoir laisser nos machines en excution et nos rseaux en communication tout au long du projet. Du ct de la production, Trenton Schulz a cr le CD compagnon de l'dition imprime de cet ouvrage et Cathrine Bore de Trolltech a gr les contrats et les lgalits pour notre compte. Un grand merci galement Nathan Clement pour les illustrations. Et enn, nous remercions Lara Wysong de Pearson pour avoir aussi bien gr les dtails pratiques de la production.

Bref historique de Qt

Qt a t mis disposition du public pour la premire fois en mai 1995. Il a t dvelopp lorigine par Haavard Nord (le chef de la direction de Trolltech) et Eirik Chambe-Eng (le prsident de Trolltech). Haavard et Eirik se sont rencontrs lInstitut Norvgien de Technologie de Trondheim, do ils sont diplms dun master en informatique. Lintrt dHaavard pour le dveloppement C++ dinterfaces graphiques utilisateurs (GUI) a dbut en 1988 quand il a t charg par une entreprise sudoise de dvelopper un framework GUI en langage C++. Quelques annes plus tard, pendant lt 1990, Haavard et Eirik travaillaient ensemble sur une application C++ de base de donnes pour les images ultrasons. Le systme devait pouvoir sexcuter avec une GUI sous Unix, Macintosh et Windows. Un jour de cet t-l, Haavard et Eirik sont sortis proter du soleil, et lorsquils taient assis sur un banc dans un parc, Haavard a dit, "Nous avons besoin dun systme dafchage orient objet". La discussion en rsultant a pos les bases intellectuelles dune GUI multiplate-forme oriente objet quils ne tarderaient pas concevoir. En 1991, Haavard a commenc crire les classes qui deviendraient Qt, tout en collaborant avec Eirik sur la conception. Lanne suivante, Eirik a eu lide des "signaux et slots", un paradigme simple mais puissant de programmation GUI qui est dsormais adopt par de nombreux autres kits doutils. Haavard a repris cette ide pour en produire une implmentation code. En 1993, Haavard et Eirik ont dvelopp le premier noyau graphique de Qt et taient en mesure dimplmenter leurs propres widgets. A la n de lanne, Haavard a suggr de crer une entreprise dans le but de concevoir "le meilleur framework GUI en langage C++". Lanne 1994 a commenc sous de mauvais auspices avec deux jeunes programmeurs souhaitant simplanter sur un march bien tabli, sans clients, avec un produit inachev et sans argent. Heureusement, leurs pouses taient leurs employes et pouvaient donc soutenir leurs maris pendant les deux annes ncessaires selon Eirik et Haavard pour dvelopper le produit et enn commencer percevoir un salaire.

XVI

Qt4 et C++ : Programmation dinterfaces GUI

La lettre "Q" a t choisie comme prxe de classe parce que ce caractre tait joli dans lcriture Emacs de Haavard. Le "t" a t ajout pour reprsenter "toolkit" inspir par Xt, the X Toolkit. La socit a t cre le 4 mars 1994, tout dabord sous le nom Quasar Technologies, puis Troll Tech et enn Trolltech. En avril 1995, grce lun des professeurs de Haavard, lentreprise norvgienne Metis a sign un contrat avec eux pour dvelopper un logiciel bas sur Qt. A cette priode, Trolltech a embauch Arnt Gulbrandsen, qui, pendant six ans, a imagin et implment un systme de documentation ingnieux et a contribu llaboration du code de Qt. Le 20 mai 1995, Qt 0.90 a t tlcharg sur sunsite.unc.edu. Six jours plus tard, la publication a t annonce sur comp.os.linux.announce. Ce fut la premire version publique de Qt. Qt pouvait tre utilis pour un dveloppement sous Windows et Unix, proposant la mme API sur les deux plates-formes. Qt tait disponible sous deux licences ds le dbut : une licence commerciale tait ncessaire pour un dveloppement commercial et une dition gratuite tait disponible pour un dveloppement open source. Le contrat avec Metis a permis Trolltech de rester ot, alors que personne na achet de licence commerciale Qt pendant dix longs mois. En mars 1996, lAgence spatiale europenne est devenue le deuxime client Qt, en achetant dix licences commerciales. Eirik et Haavard y croyaient toujours aussi fortement et ont embauch un autre dveloppeur. Qt 0.97 a t publi n mai et le 24 septembre 1996, Qt 1.0 a fait son apparition. A la n de la mme anne, Qt atteignait la version 1.1 : huit clients, chacun venant dun pays diffrent, ont achet 18 licences au total. Cette anne a galement vu la naissance du projet KDE, men par Matthias Ettrich. Qt 1.2 a t publi en avril 1997. Matthias Ettrich a dcid dutiliser Qt pour concevoir un environnement KDE, ce qui a favoris llvation de Qt au rang de standard de facto pour le dveloppement GUI C++ sous Linux. Qt 1.3 a t publi en septembre 1997. Matthias a rejoint Trolltech en 1998 et la dernire version principale de Qt 1, 1.40, a t conue en septembre de la mme anne. Qt 2.0 a t publi en juin 1999. Qt 2 avait une nouvelle licence open source, QPL (Q Public License), conforme la dnition OSD (Open Source De?nition). En aot 1999, Qt a reu le prix LinuxWorld en tant que meilleure bibliothque/ outil. A cette priode-l, Trolltech Pty Ltd (Australie) a t fonde. Trolltech a publi Qtopia Core (appel ensuite Qt/Embedded) en 2000. Il tait conu pour sexcuter sur les priphriques Linux Embarqu et proposait son propre systme de fentrage en remplacement de X11. Qt/X11 et Qtopia Core taient dsormais proposs sous la licence GNU GPL (General Public License) fortement utilise, de mme que sous des licences commerciales. A la n de lanne 2000, Trolltech a fond Trolltech Inc. (USA) et a publi la premire version de Qtopia, une plate-forme applicative pour tlphones mobiles et PDA. Qtopia Core a remport le prix LinuxWorld "Best Embedded Linux Solution" en 2001 et 2002, et Qtopia Phone a obtenu la mme rcompense en 2004. Qt 3.0 a t publi en 2001. Qt est dsormais disponible sous Windows, Mac OS X, Unix et Linux (Desktop et Embarqu). Qt 3 proposait 42 nouvelles classes et son code slevait 500 000 lignes. Qt 3 a constitu une avance incroyable par rapport Qt 2 : un support local et

Bref historique de Qt

XVII

Unicode largement amlior, un afchage de texte et une modication de widget totalement innovant et une classe dexpressions rgulires de type Perl. Qt 3 a remport le prix "Jolt Productivity Award" de Software Development Times en 2002. En t 2005, Qt 4.0 a t publi. Avec prs de 500 classes et plus de 9000 fonctions, Qt 4 est plus riche et plus important que nimporte quelle version antrieure. Il a t divis en plusieurs bibliothques, de sorte que les dveloppeurs ne doivent se rattacher quaux parties de Qt dont ils ont besoin. Qt 4 constitue une progression signicative par rapport aux versions antrieures avec diverses amliorations : un ensemble totalement nouveau de conteneurs template efcaces et faciles demploi, une fonctionnalit avance modle/vue, un framework de dessin rapide et exible en 2D et des classes de modication et dafchage de texte Unicode, sans mentionner les milliers de petites amliorations parmi toutes les classes Qt. Qt 4 est la premire dition Qt disponible pour le dveloppement commercial et open source sur toutes les plates-formes quil supporte. Toujours en 2005, Trolltech a ouvert une agence Beijing pour proposer aux clients de Chine et alentours un service de vente et de formation et un support technique pour Qtopia. Depuis la cration de Trolltech, la popularit de Qt na cess de saccrotre et continue gagner du terrain aujourdhui. Ce succs est une rexion sur la qualit de Qt et sur la joie que son utilisation procure. Pendant la dernire dcennie, Qt est pass du statut de produit utilis par quelques "connaisseurs" un produit utilis quotidiennement par des milliers de clients et des dizaines de milliers de dveloppeurs open source dans le monde entier.

I
Qt : notions de base
1 2 3 4 5

Pour dbuter Crer des botes de dialogue Crer des fentres principales Implmenter la fonctionnalit dapplication Crer des widgets personnaliss

1
Pour dbuter
Au sommaire de ce chapitre Utiliser Hello Qt Etablir des connexions Disposer des widgets Utiliser la documentation de rfrence

Ce chapitre vous prsente comment combiner le langage C++ de base la fonctionnalit fournie par Qt dans le but de crer quelques petites applications GUI ( interface utilisateur graphique). Ce chapitre introduit galement deux notions primordiales concernant Qt : les "signaux et slots" et les dispositions. Dans le Chapitre 2, vous approfondirez ces points, et dans le Chapitre 3, vous commencerez concevoir une application plus raliste. Si vous connaissez dj les langages Java ou C#, mais que vous ne disposez que dune maigre exprience concernant C++, nous vous recommandons de lire tout dabord lintroduction C++ en Annexe B.

Qt4 et C++ : Programmation dinterfaces GUI

Hello Qt
Commenons par un programme Qt trs simple. Nous ltudierons dabord ligne par ligne, puis nous verrons comment le compiler et lexcuter.
1 2 3 4 5 6 7 8 9

#include <QApplication> #include <QLabel> int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel *label = new QLabel("Hello Qt!"); label->show(); return app.exec(); }

Les lignes 1 et 2 contiennent les dnitions des classes QApplication et QLabel. Pour chaque classe Qt, il y a un chier den-tte qui porte le mme nom (en respectant la casse) que la classe qui renferme la dnition de classe. La ligne 5 cre un objet QApplication pour grer les ressources au niveau de lapplication. Le constructeur QApplication exige argc et argv parce que Qt prend personnellement en charge quelques arguments de ligne de commande. La ligne 6 cre un widget QLabel qui afche "Hello Qt !". Dans la terminologie Qt et Unix, un widget est un lment visuel dans une interface utilisateur. Ce terme provient de "gadget pour fentres" et correspond "contrle" et "conteneur" dans la terminologie Windows. Les boutons, les menus et les barres de dlement sont des exemples de widgets. Les widgets peuvent contenir dautres widgets ; par exemple, une fentre dapplication est habituellement un widget qui comporte un QMenuBar, quelques QToolBar, un QStatusBar et dautres widgets. La plupart des applications utilisent un QMainWindow ou un QDialog comme fentre dapplication, mais Qt est si exible que nimporte quel widget peut tre une fentre. Dans cet exemple, le widget QLabel correspond la fentre dapplication. La ligne 7 permet dafcher ltiquette. Lors de leur cration, les widgets sont toujours masqus, de manire pouvoir les personnaliser avant de les afcher et donc dviter le phnomne du scintillement. La ligne 8 transmet le contrle de lapplication Qt. A ce stade, le programme entre dans la boucle dvnement. Cest une sorte de mode dattente o le programme attend que lutilisateur agisse, en cliquant sur la souris ou en appuyant sur une touche par exemple. Les actions de lutilisateur dclenchent des vnements (aussi appels "messages") auquel le programme peut rpondre, gnralement en excutant une ou plusieurs fonctions. Par exemple, quand lutilisateur clique sur un widget, les vnements "bouton souris enfonc" et "bouton souris relch" sont dclenchs. A cet gard, les applications GUI diffrent normment des programmes par lots traditionnels, qui traitent habituellement lentre, produisent des rsultats et sachvent sans intervention de quiconque.

Chapitre 1

Pour dbuter

Pour des questions de simplicit, nous nappelons pas delete sur lobjet QLabel la n de la fonction main(). Cette petite fuite de mmoire est insigniante dans un programme de cette taille, puisque la mmoire est rcupre de toute faon par le systme dexploitation ds quil se termine.
Figure 1.1 Hello sur Linux

Vous pouvez dsormais tester le programme sur votre ordinateur. Vous devrez dabord installer Qt 4.1.1 (ou une version ultrieure de Qt 4), un processus expliqu en Annexe A. A partir de maintenant, nous supposons que vous avez correctement install une copie de Qt 4 et que le rpertoire bin de Qt se trouve dans votre variable denvironnement PATH. (Sous Windows, cest effectu automatiquement par le programme dinstallation de Qt.) Vous aurez galement besoin du code source du programme dans un chier appel hello.cpp situ dans un rpertoire nomm hello. Vous pouvez saisir le code de ce programme vous-mme ou le copier depuis le CD fourni avec ce livre, partir du chier /examples/chap01/hello/hello.cpp. Depuis une invite de commande, placez-vous dans le rpertoire hello, puis saisissez
qmake -project

pour crer un chier de projet indpendant de la plate-forme (hello.pro), puis tapez


qmake hello.pro

pour crer un chier Makele du chier de projet spcique la plate-forme. Tapez make pour gnrer le programme.1 Excutez-le en saisissant hello sous Windows, ./hello sous Unix et open hello.app sous Mac OS X. Pour terminer le programme, cliquez sur le bouton de fermeture dans la barre de titre de la fentre. Si vous utilisez Windows et que vous avez install Qt Open Source Edition et le compilateur MinGW, vous aurez accs un raccourci vers la fentre dinvite DOS o toutes les variables denvironnement sont correctement installes pour Qt. Si vous lancez cette fentre, vous pouvez y compiler des applications Qt grce qmake et make dcrits prcdemment. Les excutables produits sont placs dans les dossiers debug ou release de lapplication, par exemple C:\qt-book\hello\release\hello.exe.

1. Si vous obtenez une erreur du compilateur sur <QApplication>, cela signie certainement que vous utilisez une version plus ancienne de Qt. Assurez-vous dutiliser Qt 4.1.1 ou une version ultrieure de Qt 4.

Qt4 et C++ : Programmation dinterfaces GUI

Si vous vous servez de Microsoft Visual C++, vous devrez excuter nmake au lieu de make. Vous pouvez aussi crer un chier de projet Visual Studio depuis hello.pro en tapant
qmake -tp vc hello.pro

puis concevoir le programme dans Visual Studio. Si vous travaillez avec Xcode sous Mac OS X, vous avez la possibilit de gnrer un projet Xcode laide de la commande
qmake -spec macx-xcode

Figure 1.2 Une tiquette avec une mise en forme HTML basique

Avant de continuer avec lexemple suivant, amusons-nous un peu : remplacez la ligne


QLabel *label = new QLabel("Hello Qt!");

par
QLabel *label = new QLabel("<h2><i>Hello</i> " "<font color=red>Qt!</font></h2>");

et gnrez nouveau lapplication. Comme lillustre cet exemple, il est facile dgayer linterface utilisateur dune application Qt en utilisant une mise en forme simple de style HTML (voir Figure 1.2).

Etablir des connexions


Le deuxime exemple vous montre comment rpondre aux actions de lutilisateur. Lapplication propose un bouton sur lequel lutilisateur peut cliquer pour quitter. Le code source ressemble beaucoup Hello, sauf que nous utilisons un QPushButton en lieu et place de QLabel comme widget principal et que nous connectons laction dun utilisateur (cliquer sur un bouton) du code. Le code source de cette application se trouve sur le site web de Pearson, www.pearson.fr, la page ddie cet ouvrage, sous /examples/chap01/quit/quit.cpp. Voici le contenu du chier :
1 2 3 4 5 6 7 8

#include <QApplication> #include <QPushButton> int main(int argc, char *argv[]) { QApplication app(argc, argv); QPushButton *button = new QPushButton("Quit"); QObject::connect(button, SIGNAL(clicked()), &app, SLOT(quit()));

Chapitre 1

Pour dbuter

9 10 11

button->show(); return app.exec(); }

Les widgets de Qt mettent des signaux pour indiquer quune action utilisateur ou un changement dtat a eu lieu.1 Par exemple, QPushButton met un signal clicked() quand lutilisateur clique sur le bouton. Un signal peut tre connect une fonction (appel un slot dans ce cas), de sorte quau moment o le signal est mis, le slot soit excut automatiquement. Dans notre exemple, nous relions le signal clicked() du bouton au slot quit() de lobjet QApplication. Les macros SIGNAL() et SLOT() font partie de la syntaxe ; elles sont expliques plus en dtail dans le prochain chapitre.
Figure 1.3 Lapplication Quit

Nous allons dsormais gnrer lapplication. Nous supposons que vous avez cr un rpertoire appel quit contenant quit.cpp (voir Figure 1.3). Excutez qmake dans le rpertoire quit pour gnrer le chier de projet, puis excutez-le nouveau pour gnrer un chier Makele, comme suit :
qmake -project qmake quit.pro

A prsent, gnrez lapplication et excutez-la. Si vous cliquez sur Quit ou que vous appuyez sur la barre despace (ce qui enfonce le bouton), lapplication se termine.

Disposer des widgets


Dans cette section, nous allons crer une petite application qui illustre comment utiliser les dispositions, des outils pour grer la disposition des widgets dans une fentre et comment utiliser des signaux et des slots pour synchroniser deux widgets. Lapplication demande lge de lutilisateur, quil peut rgler par le biais dun pointeur toupie ou dun curseur (voir Figure 1.4).
Figure 1.4 Lapplication Age

Lapplication contient trois widgets : QSpinBox, QSlider et QWidget. QWidget correspond la fentre principale de lapplication. QSpinBox et QSlider sont afchs dans QWidget;
1. Les signaux Qt ne sont pas lis aux signaux Unix. Dans ce livre, nous ne nous intressons quaux signaux Qt.

Qt4 et C++ : Programmation dinterfaces GUI

ce sont des enfants de QWidget. Nous pouvons aussi dire que QWidget est le parent de QSpinBox et QSlider. QWidget na pas de parent puisque cest une fentre de niveau le plus haut. Les constructeurs de QWidget et toutes ses sous-classes reoivent un paramtre QWidget * qui spcie le widget parent. Voici le code source :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

#include #include #include #include

<QApplication> <QHBoxLayout> <QSlider> <QSpinBox>

int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget *window = new QWidget; window->setWindowTitle("Enter your age"); QSpinBox *spinBox = new QSpinBox; QSlider *slider = new QSlider(Qt::Horizontal); spinBox->setRange(0, 130); slider->setRange(0, 130); QObject::connect(spinBox, SIGNAL(valueChanged(int)), slider, SLOT(setValue(int))); QObject::connect(slider, SIGNAL(valueChanged(int)), spinBox, SLOT(setValue(int))); spinBox->setValue(35); QHBoxLayout *layout = new QHBoxLayout; layout->addWidget(spinBox); layout->addWidget(slider); window->setLayout(layout); window->show(); return app.exec(); }

Les lignes 8 et 9 congurent QWidget qui fera ofce de fentre principale de lapplication. Nous appelons setWindowTitle() pour dnir le texte afch dans la barre de titre de la fentre. Les lignes 10 et 11 crent QSpinBox et QSlider, et les lignes 12 et 13 dterminent leurs plages valides. Nous pouvons afrmer que lutilisateur est g au maximum de 130 ans. Nous pourrions transmettre window aux constructeurs de QSpinBox et QSlider, en spciant que le parent de ces widgets doit tre window, mais ce nest pas ncessaire ici, parce que le systme de positionnement le dduira lui-mme et dnira automatiquement le parent du pointeur toupie (spin box) et du curseur, comme nous allons le voir.

Chapitre 1

Pour dbuter

Les deux appels QObject::connect() prsents aux lignes 14 17 garantissent que le pointeur toupie et le curseur sont synchroniss, de sorte quils afchent toujours la mme valeur. Ds que la valeur dun widget change, son signal valueChanged(int) est mis et le slot setValue(int) de lautre widget est appel avec la nouvelle valeur. La ligne 18 dnit la valeur du pointeur toupie en 35. QSpinBox met donc le signal valueChanged(int) avec un argument int de 35. Cet argument est transmis au slot setValue(int) de QSlider, qui dnit la valeur du curseur en 35. Le curseur met ensuite le signal valueChanged(int) puisque sa propre valeur a chang, dclenchant le slot setValue(int) du pointeur toupie. Cependant, ce stade, setValue(int) nmet aucun signal, parce que la valeur du pointeur toupie est dj de 35. Vous vitez ainsi une rcursivit innie. La Figure 1.5 rcapitule la situation.
Figure 1.5 Changer la valeur dun widget entrane la modication des deux widgets

1.

0 setValue(35)

2.

35 valueChanged(35)

setValue(35)

3.

35

valueChanged(35)

setValue(35)

4.

35

Dans les lignes 19 22, nous positionnons le pointeur toupie et le curseur laide dun gestionnaire de disposition (layout manager). Ce gestionnaire est un objet qui dnit la taille et la position des widgets qui se trouvent sous sa responsabilit. Qt propose trois principaux gestionnaires de disposition :

QHBoxLayout dispose les widgets horizontalement de gauche droite (de droite gauche dans certaines cultures). QVBoxLayout dispose les widgets verticalement de haut en bas. QGridLayout dispose les widgets dans une grille. Lorsque vous appelez QWidget::setLayout() la ligne 22, le gestionnaire de disposition est install dans la fentre. En arrire-plan, QSpinBox et QSlider sont "reparents" pour devenir des enfants du widget sur lequel la disposition sapplique, et cest pour cette raison que

10

Qt4 et C++ : Programmation dinterfaces GUI

nous navons pas besoin de spcier un parent explicite quand nous construisons un widget qui sera insr dans une disposition.
Figure 1.6 Les widgets de lapplication Age

Window Title QWidget QSpinBox QSlider

QHBoxLayout

Mme si nous navons pas dni explicitement la position ou la taille dun widget, QSpinBox et QSlider apparaissent convenablement cte cte. Cest parce que QHBoxLayout assigne automatiquement des positions et des tailles raisonnables aux widgets dont il est responsable en fonction de leurs besoins. Grce aux gestionnaires de disposition, vous vitez la corve de coder les positions lcran dans vos applications et vous tes sr que les fentres seront redimensionnes correctement. Lapproche de Qt pour ce qui concerne la conception des interfaces utilisateurs est facile comprendre et savre trs exible. Habituellement, les programmeurs Qt instancient les widgets ncessaires puis dnissent leurs proprits de manire adquate. Les programmeurs ajoutent les widgets aux dispositions, qui se chargent automatiquement de leur taille et de leur position. Le comportement de linterface utilisateur est gr en connectant les widgets ensembles grce aux signaux et aux slots de Qt.

Utiliser la documentation de rfrence


La documentation de rfrence de Qt est un outil indispensable pour tout dveloppeur Qt, puisquelle contient toutes les classes et fonctions de cet environnement. Ce livre utilise de nombreuses classes et fonctions Qt, mais il ne les aborde pas toutes et ne fournit pas de plus amples dtails sur celles qui sont voques. Pour proter pleinement de Qt, vous devez vous familiariser avec la documentation de rfrence le plus rapidement possible. Cette documentation est disponible en format HTML dans le rpertoire doc/html de Qt et peut tre afche dans nimporte quel navigateur Web. Vous pouvez galement utiliser lAssistant Qt, le navigateur assistant de Qt, qui propose des fonctionnalits puissantes de recherche et dindex qui sont plus faciles et plus rapides utiliser quun navigateur Web. Pour lancer lAssistant Qt, cliquez sur Qt by Trolltech v4.x.y/Assistant dans le menu Dmarrer sous Windows, saisissez assistant dans la ligne de commande sous Unix ou double-cliquez sur Assistant dans le Finder de Mac OS X (voir Figure 1.7). Les liens dans la section "API Reference" sur la page daccueil fournissent diffrents moyens de localiser les classes de Qt. La page "All Classes" rpertorie chaque classe dans lAPI de Qt. La page "Main Classes" regroupe uniquement les classes Qt les plus frquemment utilises.

Chapitre 1

Pour dbuter

11

En guise dentranement, vous rechercherez les classes et les fonctions dont nous avons parles dans ce chapitre.

Figure 1.7 La documentation de Qt dans lAssistant sous Mac OS X

Notez que les fonctions hrites sont dtailles dans la classe de base ; par exemple, QPushButton ne possde pas de fonction show(), mais il en hrite une de son anctre QWidget. La Figure 1.8 vous montre comment les classes que nous avons tudies jusque l sont lies les unes aux autres.
Figure 1.8 Arbre dhritage des classes Qt tudies QCoreApplication jusqu prsent
QApplication QObject QWidget QLayout QBoxLayout

QAbstractButton QPushButton

QAbstractButton QSpinBox

QAbstractSlider QSlider

QFrame QLabel

QHBoxLayout

12

Qt4 et C++ : Programmation dinterfaces GUI

La documentation de rfrence pour la version actuelle de Qt et pour certaines versions antrieures est disponible en ligne ladresse suivante, http://doc.trolltech.com/. Ce site propose galement des articles slectionns dans Qt Quarterly, la lettre dinformation des programmeurs Qt envoye tous les dtenteurs de licences.

Styles des widgets


Les captures prsentes jusque l ont t effectues sous Linux, mais les applications Qt se fondent parfaitement dans chaque plate-forme prise en charge. Qt y parvient en mulant laspect et lapparence de la plate-forme, au lieu dadopter un ensemble de widgets de bote outils ou de plate-forme particulier.

Figure 1.9 Styles disponibles partout


Windows Plastique

CDE

Motif

Dans Qt/X11 et Qtopia Core, le style par dfaut est Plastique. Il utilise des dgrads et lanticrnelage pour proposer un aspect et une apparence modernes. Les utilisateurs de lapplication Qt peuvent passer outre ce style par dfaut grce loption de ligne de commande -style. Par exemple, pour lancer lapplication Age avec le style Motif sur X11, tapez simplement
./age -style motif

sur la ligne de commande.

Figure 1.10 Styles spciques la plate-forme


Windows XP Mac

Contrairement aux autres styles, les styles Windows XP et Mac ne sont disponibles que sur leurs plate-formes natives, puisquils se basent sur les gnrateurs de thme de ces plate-formes.

Chapitre 1

Pour dbuter

13

Ce chapitre vous a prsent les concepts essentiels des connexions signal slot et des dispositions. Il a galement commenc dvoiler lapproche totalement oriente objet et cohrente de Qt concernant la construction et lutilisation des widgets. Si vous parcourez la documentation de Qt, vous dcouvrirez que lapproche savre homogne. Vous comprendrez donc beaucoup plus facilement comment utiliser de nouveaux widgets et vous verrez aussi que les noms choisis minutieusement pour les fonctions, les paramtres, les numrations, etc. rendent la programmation dans Qt tonnamment plaisante et aise. Les chapitres suivants de la Partie I se basent sur les notions fondamentales tudies ici et vous expliquent comment crer des applications GUI compltes avec des menus, des barres doutils, des fentres de document, une barre dtat et des botes de dialogue, en plus des fonctionnalits sous-jacentes permettant de lire, traiter et crire des chiers.

2
Crer des botes de dialogue
Au sommaire de ce chapitre Drivation de QDialog Description dtaille des signaux et slots Conception rapide dune bote de dialogue Botes de dialogue multiformes Botes de dialogue dynamiques Classes de widgets et de botes de dialogue intgres

Dans ce chapitre, vous allez apprendre crer des botes de dialogue laide de Qt. Celles-ci prsentent diverses options et possibilits aux utilisateurs et leur permettent de dnir les valeurs des options et de faire des choix. On les appelle des botes de dialogue, puisquelles donnent la possibilit aux utilisateurs et aux applications de "discuter". La majorit des applications GUI consiste en une fentre principale quipe dune barre de menus et doutils, laquelle on ajoute des douzaines de botes de dialogue. Il est galement possible de crer des applications "bote de dialogue" qui rpondent directement aux choix de lutilisateur en accomplissant les actions appropries (par exemple, une application de calculatrice).

16

Qt4 et C++ : Programmation dinterfaces GUI

Nous allons crer notre premire bote de dialogue en crivant compltement le code pour vous en expliquer le fonctionnement. Puis nous verrons comment concevoir des botes de dialogue grce au Qt Designer, un outil de conception de Qt. Avec le Qt Designer, vous codez beaucoup plus rapidement et il est plus facile de tester les diffrentes conceptions et de les modier par la suite.

Drivation de QDialog
Notre premier exemple est une bote de dialogue Find crite totalement en langage C++ (voir Figure 2.1). Nous limplmenterons comme une classe part entire. Ainsi, elle deviendra un composant indpendant et autonome, comportant ses propres signaux et slots.
Figure 2.1 La bote de dialogue Find

Le code source est rparti entre deux chiers : finddialog.h et finddialog.cpp. Nous commencerons par finddialog.h.
1 2 3 4 5 6 7

#ifndef FINDDIALOG_H #define FINDDIALOG_H #include <QDialog> class class class class QCheckBox; QLabel; QLineEdit; QPushButton;

Les lignes 1 et 2 (et 27) protgent le chier den-tte contre les inclusions multiples. La ligne 3 contient la dnition de QDialog, la classe de base pour les botes de dialogue dans Qt. QDialog hrite de QWidget. Les lignes 4 7 sont des dclarations pralables des classes Qt que nous utiliserons pour implmenter la bote de dialogue. Une dclaration pralable informe le compilateur C++ quune classe existe, sans donner tous les dtails dune dnition de classe (gnralement situe dans un chier den-tte). Nous en parlerons davantage dans un instant. Nous dnissons ensuite FindDialog comme une sous-classe de QDialog:
8 9 10

class FindDialog: public QDialog { Q_OBJECT

Chapitre 2

Crer des botes de dialogue

17

11 12

public: FindDialog(QWidget *parent = 0);

La macro Q_OBJECT au dbut de la dnition de classe est ncessaire pour toutes les classes qui dnissent des signaux ou des slots. Le constructeur de FindDialog est typique des classes Qt de widgets. Le paramtre parent spcie le widget parent. Par dfaut, cest un pointeur nul, ce qui signie que la bote de dialogue na pas de parent.
13 14 15

signals: void findNext(const QString &str, Qt::CaseSensitivity cs); void findPrevious(const QString &str, Qt::CaseSensitivity cs);

La section des signaux dclare deux signaux que la bote de dialogue met quand lutilisateur clique sur le bouton Find. Si loption Search backward est active, la bote de dialogue met findPrevious(); sinon elle met findNext(). Le mot-cl signals est en fait une macro. Le prprocesseur C++ la convertit en langage C++ standard avant que le compilateur ne la voie. Qt::CaseSensitivity est un type numration qui peut prendre les valeurs Qt::CaseSensitive et Qt::CaseInsensitive.
16 17 18 19 20 21 22 23 24 25 26 27

private slots: void findClicked(); void enableFindButton(const QString &text); private: QLabel *label; QLineEdit *lineEdit; QCheckBox *caseCheckBox; QCheckBox *backwardCheckBox; QPushButton *findButton; QPushButton *closeButton; }; #endif

Dans la section prive de la classe, nous dclarons deux slots. Pour implmenter les slots, vous devez avoir accs la plupart des widgets enfants de la bote de dialogue, nous conservons donc galement des pointeurs vers eux. Le mot-cl slots, comme signals, est une macro qui se dveloppe pour produire du code que le compilateur C++ peut digrer. Sagissant des variables prives, nous avons utilis les dclarations pralables de leurs classes. Ctait possible puisque ce sont toutes des pointeurs et que nous ny accdons pas dans le chier den-tte, le compilateur na donc pas besoin des dnitions de classe compltes. Nous aurions pu inclure les chiers den-tte importants (<QCheckBox>, <QLabel>, etc.), mais la compilation se rvle plus rapide si vous utilisez les dclarations pralables ds que possible.

18

Qt4 et C++ : Programmation dinterfaces GUI

Nous allons dsormais nous pencher sur finddialog.cpp qui contient limplmentation de la classe FindDialog.
1 2

#include <QtGui> #include "finddialog.h"

Nous incluons dabord <QtGui>, un chier den-tte qui contient la dnition des classes GUI de Qt. Qt est constitu de plusieurs modules, chacun deux se trouvant dans sa propre bibliothque. Les modules les plus importants sont QtCore, QtGui, QtNetwork, QtOpenGL, QtSql, QtSvg et QtXml. Le chier den-tte <QtGui> renferme la dnition de toutes les classes qui font partie des mo dules QtCore et QtGui. En incluant cet en-tte, vous vitez la tche fastidieuse dinclure chaque classe sparment. Dans filedialog.h, au lieu dinclure <QDialog> et dutiliser des dclarations pralables pour QCheckBox, QLabel, QLineEdit et QPushButton, nous aurions simplement pu spcier <QtGui>. Toutefois, il est gnralement malvenu dinclure un chier den-tte si grand depuis un autre chier den-tte, notamment dans des applications plus importantes.
3 4 5 6 7 8 9 10 11 12 13

FindDialog::FindDialog(QWidget *parent) : QDialog(parent) { label = new QLabel(tr("Find &what:")); lineEdit = new QLineEdit; label->setBuddy(lineEdit); caseCheckBox = new QCheckBox(tr("Match &case")); backwardCheckBox = new QCheckBox(tr("Search &backward")); findButton = new QPushButton(tr("&Find")); findButton->setDefault(true); findButton->setEnabled(false); closeButton = new QPushButton(tr("Close"));

14

A la ligne 4, nous transmettons le paramtre parent au constructeur de la classe de base. Puis nous crons les widgets enfants. Les appels de la fonction tr() autour des littraux chane les marquent dans loptique dune traduction en dautres langues. La fonction est dclare dans QObject et chaque sous-classe qui contient la macro Q_OBJECT. Il est recommand de prendre lhabitude dencadrer les chanes visibles par lutilisateur avec tr(), mme si vous navez pas lintention de faire traduire vos applications en dautres langues dans limmdiat. La traduction des applications Qt est aborde au Chapitre 17. Dans les littraux chane, nous utilisons le caractre & pour indiquer des raccourcis clavier. Par exemple, la ligne 11 cre un bouton Find que lutilisateur peut activer en appuyant sur Alt+F sur les plates-formes qui prennent en charge les raccourcis clavier. Le caractre & peut galement tre employ pour contrler le focus, cest--dire llment actif : la ligne 6, nous crons une tiquette avec un raccourci clavier (Alt+W), et la ligne 8, nous dnissons lditeur de lignes comme widget compagnon de ltiquette. Ce compagnon (buddy) est un widget

Chapitre 2

Crer des botes de dialogue

19

qui reoit le focus quand vous appuyez sur le raccourci clavier de ltiquette. Donc, quand lutilisateur appuie sur Alt+W (le raccourci de ltiquette), lditeur de lignes reoit le contrle. A la ligne 12, nous faisons du bouton Find le bouton par dfaut de la bote de dialogue en appelant setDefault(true). Le bouton par dfaut est le bouton qui est press quand lutilisateur appuie sur Entre. A la ligne 13, nous dsactivons le bouton Find. Quand un widget est dsactiv, il apparat gnralement gris et ne rpondra pas en cas dinteraction de lutilisateur.
15 16 17 18 19 20 connect(lineEdit, SIGNAL(textChanged(const QString &)), this, SLOT(enableFindButton(const QString &))); connect(findButton, SIGNAL(clicked()), this, SLOT(findClicked())); connect(closeButton, SIGNAL(clicked()), this, SLOT(close()));

Le slot priv enableFindButton(const QString &) est appel ds que le texte change dans lditeur de lignes. Le slot priv findClicked() est invoqu lorsque lutilisateur clique sur le bouton Find. La bote de dialogue se ferme si lutilisateur clique sur Close. Le slot close() est hrit de QWidget et son comportement par dfaut consiste masquer le widget (sans le supprimer). Nous allons tudier le code des slots enableFindButton() et findClicked() ultrieurement. Etant donn que QObject est lun des anctres de FindDialog, nous pouvons omettre le prxe QObject:: avant les appels de connect().
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

QHBoxLayout *topLeftLayout = new QHBoxLayout; topLeftLayout->addWidget(label); topLeftLayout->addWidget(lineEdit); QVBoxLayout *leftLayout = new QVBoxLayout; leftLayout->addLayout(topLeftLayout); leftLayout->addWidget(caseCheckBox); leftLayout->addWidget(backwardCheckBox); QVBoxLayout *rightLayout = new QVBoxLayout; rightLayout->addWidget(findButton); rightLayout->addWidget(closeButton); rightLayout->addStretch(); QHBoxLayout *mainLayout = new QHBoxLayout; mainLayout->addLayout(leftLayout); mainLayout->addLayout(rightLayout); setLayout(mainLayout);

Nous disposons ensuite les widgets enfants laide des gestionnaires de disposition. Les dispositions peuvent contenir des widgets et dautres dispositions. En imbriquant QHBoxLayout, QVBoxLayout et QGridLayout dans diverses combinaisons, il est possible de concevoir des botes de dialogue trs sophistiques.

20

Qt4 et C++ : Programmation dinterfaces GUI

Figure 2.2 Les dispositions de la bote de dialogue Find


leftLayout topLeftLayout

Titre de la fentre

QLabel

QLineEdit

QPushButton QPushButton

rightLayout rightLayout

QCheckBox QCheckBox

Elment d'espacement

Pour la bote de dialogue Find, nous employons deux QHBoxLayout et deux QVBoxLayout, comme illustr en Figure 2.2. La disposition externe correspond la disposition principale ; elle est installe sur FindDialog la ligne 35 et est responsable de toute la zone de la bote de dialogue. Les trois autres dispositions sont des sous-dispositions. Le petit "ressort" en bas droite de la Figure 2.2 est un lment despacement (ou "tirement"). Il comble lespace vide sous les boutons Find et Close, ces boutons sont donc srs de se trouver en haut de leur disposition. Les gestionnaires de disposition prsentent une subtilit : ce ne sont pas des widgets. Ils hritent de QLayout, qui hrite son tour de QObject. Dans la gure, les widgets sont reprsents par des cadres aux traits pleins et les dispositions sont illustres par des cadres en pointills pour mettre en avant la diffrence qui existe entre eux. Dans une application en excution, les dispositions sont invisibles. Quand les sous-dispositions sont ajoutes la disposition parent (lignes 25, 33 et 34), les sousdispositions sont automatiquement reparentes. Puis, lorsque la disposition principale est installe dans la bote de dialogue (ligne 35), elle devient un enfant de cette dernire et tous les widgets dans les dispositions sont reparents pour devenir des enfants de la bote de dialogue. La hirarchie parent-enfant ainsi obtenue est prsente en Figure 2.3.
36 37 38

setWindowTitle(tr("Find")); setFixedHeight(sizeHint().height()); }
FindDialog QLabel (label) QLineEdit (lineEdit) QCheckBox (caseCheckBox) QCheckBox (backwardCheckBox) QPushButton (ndButton) QPushButton (closeButton) QHBoxLayout (mainLayout) QVBoxLayout (leftLayout) QHBoxLayout (topLeftLayout) QVBoxLayout (rightLayout)

Figure 2.3 Les relations parentenfant de la bote de dialogue Find

Enn, nous dnissons le titre afcher dans la barre de titre de la bote de dialogue et nous congurons la fentre pour quelle prsente une hauteur xe, tant donn quelle ne contient

Chapitre 2

Crer des botes de dialogue

21

aucun widget qui peut occuper de lespace supplmentaire verticalement. La fonction QWidget::sizeHint() retourne la taille "idale" dun widget. Ceci termine lanalyse du constructeur de FindDialog. Vu que nous avons cr les widgets et les dispositions de la bote de dialogue avec new, il semblerait logique dcrire un destructeur qui appelle delete sur chaque widget et disposition que nous avons crs. Toutefois, ce nest pas ncessaire, puisque Qt supprime automatiquement les objets enfants quand le parent est dtruit, et les dispositions et widgets enfants sont tous des descendants de FindDialog. Nous allons prsent analyser les slots de la bote de dialogue :
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54

void FindDialog::findClicked() { QString text = lineEdit->text(); Qt::CaseSensitivity cs = caseCheckBox->isChecked()? Qt::CaseSensitive : Qt::CaseInsensitive; if (backwardCheckBox->isChecked()) { emit findPrevious(text, cs); } else { emit findNext(text, cs); } } void FindDialog::enableFindButton(const QString &text) { findButton->setEnabled(!text.isEmpty()); }

Le slot findClicked() est appel lorsque lutilisateur clique sur le bouton Find. Il met le signal findPrevious() ou findNext(), en fonction de loption Search backward. Le motcl emit est spcique Qt ; comme les autres extensions Qt, il est converti en langage C++ standard par le prprocesseur C++. Le slot enableFindButton() est invoqu ds que lutilisateur modie le texte dans lditeur de lignes. Il active le bouton sil y a du texte dans cet diteur, sinon il le dsactive. Ces deux slots compltent la bote de dialogue. Nous avons dsormais la possibilit de crer un chier main.cpp pour tester notre widget FindDialog:
1 2 3 4 5 6 7 8 9

#include <QApplication> #include "finddialog.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); FindDialog *dialog = new FindDialog; dialog->show(); return app.exec(); }

Pour compiler le programme, excutez qmake comme dhabitude. Vu que la dnition de la classe FindDialog comporte la macro Q_OBJECT, le chier Makele gnr par qmake

22

Qt4 et C++ : Programmation dinterfaces GUI

contiendra des rgles particulires pour excuter moc, le compilateur de mta-objets de Qt. (Le systme de mta-objets de Qt est abord dans la prochaine section.) Pour que moc fonctionne correctement, vous devez placer la dnition de classe dans un chier den-tte, spar du chier dimplmentation. Le code gnr par moc inclut ce chier dentte et ajoute une certaine "magie" C++.

moc doit tre excut sur les classes qui se servent de la macro Q_OBJECT. Ce nest pas un problme parce que qmake ajoute automatiquement les rgles ncessaires dans le chier Makele. Toutefois, si vous oubliez de gnrer nouveau votre chier Makele laide de qmake et que moc nest pas excut, lditeur de liens indiquera que certaines fonctions sont dclares mais pas implmentes. Les messages peuvent tre plutt obscurs. GCC produit des avertissements comme celui-ci :
finddialog.o: In function FindDialog::tr(char const*, char const*): /usr/lib/qt/src/corelib/global/qglobal.h:1430: undefined reference to FindDialog::staticMetaObject

La sortie de Visual C++ commence ainsi :


finddialog.obj: error LNK2001: unresolved external symbol "public:~virtual int __thiscall MyClass::qt_metacall(enum QMetaObject ::Call,int,void * *)"

Si vous vous trouvez dans ce cas, excutez nouveau qmake pour mettre jour le chier Makele, puis gnrez nouveau lapplication. Excutez maintenant le programme. Si des raccourcis clavier apparaissent sur votre plateforme, vriez que les raccourcis Alt+W, Alt+C, Alt+B et Alt+F dclenchent le bon comportement. Appuyez sur la touche de tabulation pour parcourir les widgets en utilisant le clavier. Lordre de tabulation par dfaut est lordre dans lequel les widgets ont t crs. Vous pouvez le modier grce QWidget::setTabOrder(). Proposer un ordre de tabulation et des raccourcis clavier cohrents permet aux utilisateurs qui ne veulent pas (ou ne peuvent pas) utiliser une souris de proter pleinement de lapplication. Les dactylos apprcieront galement de pouvoir tout contrler depuis le clavier. Dans le Chapitre 3, nous utiliserons la bote de dialogue Find dans une application relle, et nous connecterons les signaux findPrevious() et findNext() certains slots.

Description dtaille des signaux et slots


Le mcanisme des signaux et des slots est une notion fondamentale en programmation Qt. Il permet au programmeur de lapplication de relier des objets sans que ces objets ne sachent quoi que ce soit les uns sur les autres. Nous avons dj connect certains signaux et slots ensemble, dclar nos propres signaux et slots, implment nos slots et mis nos signaux. Etudions dsormais ce mcanisme plus en dtail.

Chapitre 2

Crer des botes de dialogue

23

Les slots sont presque identiques aux fonctions membres ordinaires de C++. Ils peuvent tre virtuels, surchargs, publics, protgs ou privs, tre invoqus directement comme toute autre fonction membre C++, et leurs paramtres peuvent tre de nimporte quel type. La diffrence est quun slot peut aussi tre connect un signal, auquel cas il est automatiquement appel chaque fois que le signal est mis. Voici la syntaxe de linstruction connect():
connect(sender, SIGNAL(signal), receiver, SLOT(slot));

o sender et receiver sont des pointeurs vers QObject et o signal et slot sont des signatures de fonction sans les noms de paramtre. Les macros SIGNAL() et SLOT() convertissent leur argument en chane. Dans les exemples tudis jusque l, nous avons toujours connect les signaux aux divers slots. Il existe dautres possibilits envisager.

Un signal peut tre connect plusieurs slots :


connect(slider, SIGNAL(valueChanged(int)), spinBox, SLOT(setValue(int))); connect(slider, SIGNAL(valueChanged(int)), this, SLOT(updateStatusBarIndicator(int)));

Quand le signal est mis, les slots sont appels les uns aprs les autres, dans un ordre non spci.

Plusieurs signaux peuvent tre connects au mme slot :


connect(lcd, SIGNAL(overflow()), this, SLOT(handleMathError())); connect(calculator, SIGNAL(divisionByZero()), this, SLOT(handleMathError()));

Quand lun des signaux est mis, le slot est appel.

Un signal peut tre connect un autre signal :


connect(lineEdit, SIGNAL(textChanged(const QString &)), this, SIGNAL(updateRecord(const QString &)));

Quand le premier signal est mis, le second est galement mis. En dehors de cette caractristique, les connexions signal-signal sont en tout point identiques aux connexions signal-slot.

Les connexions peuvent tre supprimes :


disconnect(lcd, SIGNAL(overflow()), this, SLOT(handleMathError()));

Vous nen aurez que trs rarement besoin, parce que Qt supprime automatiquement toutes les connexions concernant un objet quand celui-ci est supprim.

24

Qt4 et C++ : Programmation dinterfaces GUI

Pour bien connecter un signal un slot (ou un autre signal), ils doivent avoir les mmes types de paramtre dans le mme ordre :
connect(ftp, SIGNAL(rawCommandReply(int, const QString &)), this, SLOT(processReply(int, const QString &)));

Exceptionnellement, si un signal comporte plus de paramtres que le slot auquel il est connect, les paramtres supplmentaires sont simplement ignors :
connect(ftp, SIGNAL(rawCommandReply(int, const QString &)), this, SLOT(checkErrorCode(int)));

Si les types de paramtre sont incompatibles, ou si le signal ou le slot nexiste pas, Qt mettra un avertissement au moment de lexcution si lapplication est gnre en mode dbogage. De mme, Qt enverra un avertissement si les noms de paramtre se trouvent dans les signatures du signal ou du slot. Jusqu prsent, nous navons utilis que des signaux et des slots avec des widgets. Cependant, le mcanisme en soi est implment dans QObject et ne se limite pas la programmation dinterfaces graphiques utilisateurs. Il peut tre employ par nimporte quelle sous-classe de QObject:
class Employee: public QObject { Q_OBJECT public: Employee() { mySalary = 0; } int salary() const { return mySalary; } public slots: void setSalary(int newSalary); signals: void salaryChanged(int newSalary); private: int mySalary; }; void Employee::setSalary(int newSalary) { if (newSalary!= mySalary) { mySalary = newSalary; emit salaryChanged(mySalary); } }

Vous remarquerez la manire dont le slot setSalary() est implment. Nous nmettons le signal salaryChanged() que si newSalary!= mySalary. Vous tes donc sr que les connexions cycliques ne dbouchent pas sur des boucles innies.

Chapitre 2

Crer des botes de dialogue

25

Systme de mta-objets de Qt
Lune des amliorations majeures de Qt a t lintroduction dans le langage C++ dun mcanisme permettant de crer des composants logiciels indpendants qui peuvent tre relis les uns aux autres sans quils ne sachent absolument rien sur les autres composants auxquels ils sont connects. Ce mcanisme est appel systme de mta-objets et propose deux services essentiels : les signaux-slots et lintrospection. La fonction dintrospection est ncessaire pour implmenter des signaux et des slots et permet aux programmeurs dapplications dobtenir des "mtainformations" sur les sous-classes de QObject lexcution, y compris la liste des signaux et des slots pris en charge par lobjet et le nom de sa classe. Le mcanisme supporte galement des proprits (pour le Qt Designer) et des traductions de texte (pour linternationalisation), et il pose les fondements de QSA (Qt Script for Applications). Le langage C++ standard ne propose pas de prise en charge des mta-informations dynamiques ncessaires pour le systme de mta-objets de Qt. Qt rsout ce problme en fournissant un outil, moc, qui analyse les dnitions de classe Q_OBJECT et rend les informations disponibles par le biais de fonctions C++. Etant donn que moc implmente toute sa fonctionnalit en utilisant un langage C++ pur, le systme de mta-objets de Qt fonctionne avec nimporte quel compilateur C++. Ce mcanisme fonctionne de la manire suivante :

La macro Q_OBJECT dclare certaines fonctions dintrospection qui doivent tre implmentes dans chaque sous-classe de QObject: metaObject(), tr(), qt_metacall() et quelques autres. Loutil moc de Qt gnre des implmentations pour les fonctions dclares par Q_OBJECT et pour tous les signaux. Les fonctions membres de QObject, telles que connect() et disconnect(), utilisent les fonctions dintrospection pour effectuer leurs tches.

Tout est gr automatiquement par qmake, moc et QObject, vous ne vous en souciez donc que trs rarement. Nanmoins, par curiosit, vous pouvez parcourir la documentation de la classe QMetaObject et analyser les chiers sources C++ gnrs par moc pour dcouvrir comment fonctionne limplmentation.

Conception rapide dune bote de dialogue


Qt est conu pour tre agrable et intuitif crire, et il nest pas inhabituel que des programmeurs dveloppent des applications Qt compltes en saisissant la totalit du code source C++. De nombreux programmeurs prfrent cependant utiliser une approche visuelle pour concevoir

26

Qt4 et C++ : Programmation dinterfaces GUI

les formulaires. Ils la trouvent en effet plus naturelle et plus rapide, et ils veulent tre en mesure de tester et de modier leurs conceptions plus rapidement et facilement quavec des formulaires cods manuellement. Le Qt Designer dveloppe les options disposition des programmeurs en proposant une fonctionnalit visuelle de conception. Le Qt Designer peut tre employ pour dvelopper tous les formulaires dune application ou juste quelques-uns. Les formulaires crs laide du Qt Designer tant uniquement constitus de code C++, le Qt Designer peut tre utilis avec une chane doutils traditionnelle et nimpose aucune exigence particulire au compilateur. Dans cette section, nous utiliserons le Qt Designer an de crer la bote de dialogue Go-to-Cell prsente en Figure 2.4. Quelle que soit la mthode de conception choisie, la cration dune bote de dialogue implique toujours les mmes tapes cls :

crer et initialiser les widgets enfants ; placer les widgets enfants dans des dispositions ; dnir lordre de tabulation ; tablir les connexions signal-slot ; implmenter les slots personnaliss de la bote de dialogue.

Figure 2.4 La bote de dialogue Go-to-Cell

Pour lancer le Qt Designer, cliquez sur Qt by Trolltech v4.x.y > Designer dans le menu Dmarrer sous Windows, saisissez designer dans la ligne de commande sous Unix ou doublecliquez sur Designer dans le Finder de Mac OS X. Quand le Qt Designer dmarre, une liste de modles safche. Cliquez sur le modle "Widget", puis sur OK. (Le modle "Dialog with Buttons Bottom" peut tre tentant, mais pour cet exemple nous crerons les boutons OK et Cancel manuellement pour vous expliquer le processus.) Vous devriez prsent vous trouver dans une fentre appele "Untitled". Par dfaut, linterface utilisateur du Qt Designer consiste en plusieurs fentres de haut niveau. Si vous prfrez une interface de style MDI, avec une fentre de haut niveau et plusieurs sousfentres, cliquez sur Edit > User Interface Mode > Docked Window (voir Figure 2.5). La premire tape consiste crer les widgets enfants et les placer sur le formulaire. Crez une tiquette, un diteur de lignes, un lment despacement horizontal et deux boutons de commande. Pour chaque lment, faites glisser son nom ou son icne depuis la bote des widgets du Qt Designer vers son emplacement sur le formulaire. Llment despacement, qui est invisible dans le formulaire nal, est afch dans le Qt Designer sous forme dun ressort bleu.

Chapitre 2

Crer des botes de dialogue

27

Figure 2.5 Le Qt Designer en mode dafchage fentres ancres sous Windows

Faites glisser le bas du formulaire vers le haut pour le rtrcir. Vous devriez voir un formulaire similaire celui de la Figure 2.6. Ne perdez pas trop de temps positionner les lments sur le formulaire ; les gestionnaires de disposition de Qt les disposeront prcisment par la suite.
Figure 2.6 Le formulaire avec quelques widgets

Congurez les proprits de chaque widget laide de lditeur de proprits du Qt Designer : 1. Cliquez sur ltiquette de texte. Assurez-vous que la proprit objectName est label et dnissez la proprit text en &Cell Location:. 2. Cliquez sur lditeur de lignes. Vriez que la proprit objectName est lineEdit. 3. Cliquez sur le premier bouton. Congurez la proprit objectName en okButton, la proprit enabled en false, la proprit text en OK et la proprit default en true. 4. Cliquez sur le second bouton. Dnissez la proprit objectName en cancelButton et la proprit text en Cancel. 5. Cliquez sur larrire-plan du formulaire pour slectionner ce dernier. Dnissez objectName en GoToCellDialog et windowTitle en Go to Cell. Tous les widgets sont correctement prsents, sauf ltiquette de texte, qui afche &Cell Location. Cliquez sur Edit > Edit Buddies pour passer dans un mode spcial qui vous permet de congurer les compagnons. Cliquez ensuite sur ltiquette et faites glisser la che rouge vers lditeur de lignes. Ltiquette devrait prsenter le texte "Cell Location" et lditeur de lignes devrait tre son widget compagnon. Cliquez sur Edit > Edit widgets pour quitter le mode des compagnons.

28

Qt4 et C++ : Programmation dinterfaces GUI

Figure 2.7 Le formulaire dont les proprits sont dnies

La prochaine tape consiste disposer les widgets sur le formulaire : 1. Cliquez sur ltiquette Cell Location et maintenez la touche Maj enfonce quand vous cliquez sur lditeur de lignes, de manire ce quils soient slectionns tous les deux. Cliquez sur Form > Lay Out Horizontally. 2. Cliquez sur llment despacement, maintenez la touche Maj enfonce et appuyez sur les boutons OK et Cancel du formulaire. Cliquez sur Form > Lay Out Horizontally. 3. Cliquez sur larrire-plan du formulaire pour annuler toute slection dlment, puis cliquez sur Form > Lay Out Vertically. 4. Cliquez sur Form > Ajust Size pour redimensionner le formulaire. Les lignes rouges qui apparaissent sur le formulaire montrent les dispositions qui ont t cres. Elles ne safchent pas lorsque le formulaire est excut.
Figure 2.8 Le formulaire avec les dispositions

Cliquez prsent sur Edit > Edit Tab Order. Un nombre apparatra dans un rectangle bleu ct de chaque widget qui peut devenir actif (voir Figure 2.9). Cliquez sur chaque widget dans lordre dans lequel vous voulez quils reoivent le focus, puis cliquez sur Edit > Edit widgets pour quitter le mode ddition de lordre de tabulation.
Figure 2.9 Dnir lordre de tabulation du formulaire

Pour avoir un aperu de la bote de dialogue, slectionnez loption Form > Preview du menu. Vriez lordre de tabulation en appuyant plusieurs fois sur la touche Tab. Fermez la bote de dialogue en appuyant sur le bouton de fermeture dans la barre de titre.

Chapitre 2

Crer des botes de dialogue

29

Enregistrez la bote de dialogue sous gotocelldialog.ui dans un rpertoire appel gotocell, et crez un chier main.cpp dans le mme rpertoire grce un diteur de texte ordinaire :
#include <QApplication> #include <QDialog> #include "ui_gotocelldialog.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); Ui::GoToCellDialog ui; QDialog *dialog = new QDialog; ui.setupUi(dialog); dialog->show(); return app.exec(); }

Excutez maintenant qmake pour crer un chier .pro et un Makele (qmake -project; qmake goto-cell.pro). Loutil qmake est sufsamment intelligent pour dtecter le chier de linterface utilisateur gotocelldialog.ui et pour gnrer les rgles appropries du chier Makele. Il va donc appeler uic, le compilateur Qt de linterface utilisateur. Loutil uic convertit gotocelldialog.ui en langage C++ et intgre les rsultats dans ui_gotocelldialog.h. Le chier ui_gotocelldialog.h gnr contient la dnition de la classe Ui::GoToCellDialog qui est un quivalent C++ du chier gotocelldialog.ui. La classe dclare des variables membres qui stockent les widgets enfants et les dispositions du formulaire, et une fonction setupUi() qui initialise le formulaire. Voici la syntaxe de la classe gnre :
class Ui::GoToCellDialog { public: QLabel *label; QLineEdit *lineEdit; QSpacerItem *spacerItem; QPushButton *okButton; QPushButton *cancelButton; ...

void setupUi(QWidget *widget) { ... } };

Cette classe nhrite pas de nimporte quelle classe Qt. Quand vous utilisez le formulaire dans main.cpp, vous crez un QDialog et vous le transmettez setupUi().

30

Qt4 et C++ : Programmation dinterfaces GUI

Si vous excutez le programme maintenant, la bote de dialogue fonctionnera, mais pas exactement comme vous le souhaitiez : Le bouton OK est toujours dsactiv. Le bouton Cancel ne fait rien. Lditeur de lignes accepte nimporte quel texte, au lieu daccepter uniquement des emplacements de cellule valides. Pour faire fonctionner correctement la bote de dialogue, vous devrez crire du code. La meilleure approche consiste crer une nouvelle classe qui hrite la fois de QDialog et de Ui::GoToCellDialog et qui implmente la fonctionnalit manquante (ce qui prouve que tout problme logiciel peut tre rsolu simplement en ajoutant une autre couche dindirection). Notre convention de dnomination consiste attribuer cette nouvelle classe le mme nom que la classe gnre par uic, mais sans le prxe Ui::.

A laide dun diteur de texte, crez un chier nomm gotocelldialog.h qui contient le code suivant :
#ifndef GOTOCELLDIALOG_H #define GOTOCELLDIALOG_H #include <QDialog> #include "ui_gotocelldialog.h" class GoToCellDialog: public QDialog, public Ui::GoToCellDialog { Q_OBJECT public: GoToCellDialog(QWidget *parent = 0); private slots: void on_lineEdit_textChanged(); }; #endif

Limplmentation fait partie de gotocelldialog.cpp:


#include <QtGui> #include "gotocelldialog.h" GoToCellDialog::GoToCellDialog(QWidget *parent) : QDialog(parent) { setupUi(this); QRegExp regExp("[A-Za-z][1-9][0-9]{0,2}");

Chapitre 2

Crer des botes de dialogue

31

lineEdit->setValidator(new QRegExpValidator(regExp, this)); connect(okButton, SIGNAL(clicked()), this, SLOT(accept())); connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject())); } void GoToCellDialog::on_lineEdit_textChanged() { okButton->setEnabled(lineEdit->hasAcceptableInput()); }

Dans le constructeur, nous appelons setupUi() pour initialiser le formulaire. Grce lhritage multiple, nous pouvons accder directement aux membres de Ui::GoToCellDialog. Aprs avoir cr linterface utilisateur, setupUi() connectera galement automatiquement tout slot qui respecte la convention de dnomination on_Nomobjet_NomSignal() au signal nomSignal() correspondant de objectName. Dans notre exemple, cela signie que setupUi() tablira la connexion signal-slot suivante :
connect(lineEdit, SIGNAL(textChanged(const QString &)), this, SLOT(on_lineEdit_textChanged()));

Toujours dans le constructeur, nous dnissons un validateur qui restreint la plage des valeurs en entre. Qt propose trois classes de validateurs :QIntValidator, QDoubleValidator et QRegExpValidator. Nous utilisons ici QRegExpValidator avec lexpression rgulire "[A-Za-z][1-9][0-9]{0,2}", qui signie : autoriser une lettre majuscule ou minuscule, suivie dun chiffre entre 1 et 9, puis de zro, un ou deux chiffres entre 0 et 9. (En guise dintroduction aux expressions rgulires, consultez la documentation de la classe QRegExp.) Si vous transmettez ceci au constructeur de QRegExpValidator, vous en faites un enfant de lobjet GoToCellDialog. Ainsi, vous navez pas besoin de prvoir la suppression de QRegExpValidator; il sera supprim automatiquement en mme temps que son parent. Le mcanisme parent-enfant de Qt est implment dans QObject. Quand vous crez un objet (un widget, un validateur, ou autre) avec un parent, le parent ajoute lobjet sa liste denfants. Quand le parent est supprim, il parcourt sa liste denfants et les supprime. Les enfants eux-mmes effacent ensuite tous leurs enfants, et ainsi de suite jusqu ce quil nen reste plus aucun. Ce mcanisme parent-enfant simplie nettement la gestion de la mmoire, rduisant les risques de fuites de mmoire. Les seuls objets que vous devrez supprimer explicitement sont les objets que vous crez avec new et qui nont pas de parent. Et si vous supprimez un objet enfant avant son parent, Qt supprimera automatiquement cet objet de la liste des enfants du parent. Sagissant des widgets, le parent a une signication supplmentaire : les widgets enfants sont afchs dans la zone du parent. Quand vous supprimez le widget parent, lenfant est effac de la mmoire mais galement de lcran. A la n du constructeur, nous connectons le bouton OK au slot accept() de QDialog et le bouton Cancel au slot reject(). Les deux slots ferment la bote de dialogue, mais accept() dnit la valeur de rsultat de la bote de dialogue en QDialog::Accepted (qui est gal 1),

32

Qt4 et C++ : Programmation dinterfaces GUI

et reject() congure la valeur en QDialog::Rejected (gal 0). Quand nous utilisons cette bote de dialogue, nous avons la possibilit dutiliser la valeur de rsultat pour voir si lutilisateur a cliqu sur OK et agir de faon approprie. Le slot on_lineEdit_textChanged() active ou dsactive le bouton OK, selon que lditeur de lignes contient un emplacement de cellule valide ou non. QLineEdit::hasAcceptableInput() emploie le validateur que nous avons dni dans le constructeur. Ceci termine la bote de dialogue. Vous pouvez dsormais rcrire main.cpp pour lutiliser :
#include <QApplication> #include "gotocelldialog.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); GoToCellDialog *dialog = new GoToCellDialog; dialog->show(); return app.exec(); }

Rgnrez lapplication (qmake project; qmake gotocell.pro) et excutez-la nouveau. Tapez "A12" dans lditeur de lignes et vous verrez que le bouton OK sactive. Essayez de saisir du texte alatoire pour vrier que le validateur effectue bien sa tche. Cliquez sur Cancel pour fermer la bote de dialogue. Lun des avantages du Qt Designer, cest que les programmeurs sont libres de modier la conception de leurs formulaires sans tre obligs de changer leur code source. Quand vous dveloppez un formulaire simplement en rdigeant du code C++, les modications apportes la conception peuvent vous faire perdre normment de temps. Grce au Qt Designer, vous gagnez en efcacit parce que uic rgnre simplement le code source pour tout formulaire modi. Linterface utilisateur de la bote de dialogue est enregistre dans un chier .ui (un format de chier bas sur le langage XML), alors que la fonctionnalit personnalise est implmente en sous-classant la classe gnre par uic.

Botes de dialogue multiformes


Nous avons vu comment crer des botes de dialogue qui afchent toujours les mmes widgets lors de leur utilisation. Dans certains cas, il est souhaitable de proposer des botes de dialogue dont la forme peut varier. Les deux types les plus courants de botes de dialogue multiformes sont les botes de dialogue extensibles et les botes de dialogue multipages. Ces deux types de botes de dialogue peuvent tre implments dans Qt, simplement dans du code ou par le biais du Qt Designer.

Chapitre 2

Crer des botes de dialogue

33

Les botes de dialogue extensibles ont habituellement une apparence simple, mais elles proposent un bouton de basculement qui permet lutilisateur dalterner entre les apparences simple et dveloppe de la bote de dialogue. Ces botes de dialogue sont gnralement utilises dans des applications qui tentent de rpondre la fois aux besoins des utilisateurs occasionnels et ceux des utilisateurs expriments, masquant les options avances moins que lutilisateur ne demande explicitement les voir. Dans cette section, nous utiliserons le Qt Designer an de crer la bote de dialogue extensible prsente en Figure 2.10. Cest une bote de dialogue Sort dans un tableur, o lutilisateur a la possibilit de slectionner une ou plusieurs colonnes trier. Lapparence simple de cette bote de dialogue permet lutilisateur de saisir une seule cl de tri, et son apparence dveloppe propose deux cls de tri supplmentaires. Grce au bouton More, lutilisateur bascule entre les apparences simple et dveloppe.
Figure 2.10 La bote de dialogue Sort dans ses deux versions, simple et dveloppe

Nous crerons le widget avec son apparence dveloppe dans le Qt Designer, et nous masquerons les cls secondaires et tertiaires lexcution. Le widget semble complexe, mais il est assez simple raliser dans le Qt Designer. Lastuce est de se charger dabord de la cl primaire, puis de la dupliquer deux fois pour obtenir les cls secondaires et tertiaires : 1. Cliquez sur File > New Form et slectionnez le modle "Dialog with Buttons Right". 2. Crez le bouton More et faites-le glisser dans la disposition verticale, sous llment despacement vertical. Dnissez la proprit text du bouton More en &More et sa proprit checkable en true. Congurez la proprit default du bouton OK en true. 3. Crez une zone de groupe, deux tiquettes, deux zones de liste droulante et un lment despacement horizontal, puis placez-les sur le formulaire. 4. Faites glisser le coin infrieur droit de la zone de groupe pour lagrandir. Puis dplacez les autres widgets dans la zone de groupe pour les positionner peu prs comme dans la Figure 2.11 (a).

34

Qt4 et C++ : Programmation dinterfaces GUI

5. Faites glisser le bord droit de la seconde zone de liste droulante, de sorte quelle soit environ deux fois plus grande que la premire zone de liste. 6. Dnissez la proprit title de la zone de groupe en &Primary Key, la proprit text de la premire tiquette en Column: et celle de la deuxime tiquette en Order:. 7. Cliquez du bouton droit sur la premire zone de liste droulante et slectionnez Edit Items dans le menu contextuel pour ouvrir lditeur de zone de liste droulante du Qt Designer. Crez un lment avec le texte "None". 8. Cliquez du bouton droit sur la seconde zone de liste droulante et slectionnez Edit Items. Crez les lments "Ascending" et "Descending". 9. Cliquez sur la zone de groupe, puis sur Form > Lay Out in a Grid. Cliquez nouveau sur la zone de groupe et sur Form > Adjust Size. Vous aboutirez la disposition afche dans la Figure 2.11 (b).
Figure 2.11 Disposer les enfants de la zone de groupe dans une grille

(a) Sans disposition

(b) Avec disposition

Si une disposition ne savre pas trs bonne ou si vous avez fait une erreur, vous pouvez toujours cliquer sur Edit > Undo ou Form > Break Layout, puis repositionner les widgets et ressayer.
Figure 2.12 Disposer les enfants du formulaire dans une grille

(a) Sans disposition

(b) Avec disposition

Chapitre 2

Crer des botes de dialogue

35

Nous allons maintenant ajouter les zones de groupe Secondary Key et Tertiary Key : 1. Prenez garde ce que la fentre soit assez grande pour accueillir les composants supplmentaires. 2. Maintenez la touche Ctrl enfonce (Alt sur Mac) et cliquez sur la zone de groupe Primary Key pour en crer une copie (et de son contenu) au-dessus de loriginal. Faites glisser la copie sous la zone de groupe originale en gardant toujours la touche Ctrl (ou Alt) enfonce. Rptez ce processus pour crer une troisime zone de groupe, en la faisant glisser sous la deuxime zone. 3. Transformez leurs proprits title en &Secondary Key et &Tertiary Key. 4. Crez un lment despacement vertical et placez-le entre la zone de la cl primaire et celle de la cl secondaire. 5. Disposez les widgets comme illustr en Figure 2.12 (a). 6. Cliquez sur le formulaire pour annuler la slection de tout widget, puis sur Form > Lay Out in a Grid. Le formulaire devrait dsormais correspondre celui de la Figure 2.12 (b). 7. Dnissez la proprit sizeHint des deux lments despacement verticaux en [20, 0]. La disposition de type grille qui en rsulte comporte deux colonnes et quatre lignes, ce qui fait un total de huit cellules. La zone de groupe Primary Key, llment despacement vertical le plus gauche, les zones de groupe Secondary Key et Tertiary Key occupent chacun une seule cellule. La disposition verticale qui contient les boutons OK, Cancel et More occupe deux cellules. Il reste donc deux cellules vides en bas droite de la bote de dialogue. Si ce nest pas le cas, annulez la disposition, repositionnez les widgets et essayez nouveau. Renommez le formulaire "SortDialog" et changez le titre de la fentre en "Sort".Dnissez les noms des widgets enfants comme dans la Figure 2.13.
Figure 2.13 Nommer les widgets du formulaire

primaryGroupBox primaryColumnCombo primaryOrderCombo

okButton cancelButton

moreButton secondaryGroupBox secondaryColumnCombo secondaryOrderCombo tertiaryGroupBox tertiaryColumnCombo tertiaryOrderCombo

36

Qt4 et C++ : Programmation dinterfaces GUI

Cliquez sur Edit > Edit Tab Order. Cliquez sur chaque zone de liste droulante de haut en bas, puis cliquez sur les boutons OK, Cancel et More situs droite. Cliquez sur Edit > Edit Widgets pour quitter le mode dition de lordre de tabulation. Maintenant que le formulaire a t conu, nous sommes prts le rendre fonctionnel en congurant certaines connexions signal-slot. Le Qt Designer vous permet dtablir des connexions entre les widgets qui font partie du mme formulaire. Nous devons tablir deux connexions. Cliquez sur Edit > Edit Signals/Slots pour passer en mode de connexion dans le Qt Designer. Les connexions sont reprsentes par des ches bleues entre les widgets du formulaire. Vu que nous avons choisi le modle "Dialog with Buttons Right", les boutons OK et Cancel sont dj connects aux slots accept() et reject() de QDialog. Les connexions sont galement rpertories dans lditeur de signal/slot du Qt Designer. Pour tablir une connexion entre deux widgets, cliquez sur le widget "expditeur" et faites glisser la che rouge vers le widget "destinataire".Une bote de dialogue souvre o vous pouvez choisir le signal et le slot connecter.
Figure 2.14 Connecter les widgets du formulaire

La premire connexion est tablir entre moreButton et secondaryGroupBox. Faites glisser la che rouge entre ces deux widgets, puis choisissez toggled(bool) comme signal et setVisible(bool) comme slot. Par dfaut, le Qt Designer ne rpertorie pas setVisible(bool) dans sa liste de slots, mais il apparatra si vous activez loption Show all signals and slots. Vous devez ensuite crer une connexion entre le signal toggled(bool) de moreButton et le slot setVisible(bool) de tertiaryGroupBox. Lorsque les connexions sont effectues, cliquez sur Edit > Edit Widgets pour quitter le mode de connexion.

Chapitre 2

Crer des botes de dialogue

37

Figure 2.15 Lditeur de connexions du Qt Designer

Enregistrez la bote de dialogue sous sortdialog.ui dans un rpertoire appel sort. Pour ajouter du code au formulaire, vous emploierez la mme technique dhritage multiple que celle utilise pour la bote de dialogue Go-to-Cell de la section prcdente. Crez tout dabord un chier sortdialog.h qui comporte les lments suivants :
#ifndef SORTDIALOG_H #define SORTDIALOG_H #include <QDialog> #include "ui_sortdialog.h" class SortDialog: public QDialog, public Ui::SortDialog { Q_OBJECT public: SortDialog(QWidget *parent = 0); void setColumnRange(QChar first, QChar last); }; #endif

Puis crez sortdialog.cpp:


1 2 3 4 5 6 7 8

#include <QtGui> #include "sortdialog.h" SortDialog::SortDialog(QWidget *parent) : QDialog(parent) { setupUi(this); secondaryGroupBox->hide(); tertiaryGroupBox->hide();

38

Qt4 et C++ : Programmation dinterfaces GUI

9 10 11 } 12 13 14 15 16 17 18 19 20

layout()->setSizeConstraint(QLayout::SetFixedSize); setColumnRange(A, Z); void SortDialog::setColumnRange(QChar first, QChar last) { primaryColumnCombo->clear(); secondaryColumnCombo->clear(); tertiaryColumnCombo->clear(); secondaryColumnCombo->addItem(tr("None")); tertiaryColumnCombo->addItem(tr("None")); primaryColumnCombo->setMinimumSize( secondaryColumnCombo->sizeHint()); QChar ch = first; while (ch <= last) { primaryColumnCombo->addItem(QString(ch)); secondaryColumnCombo->addItem(QString(ch)); tertiaryColumnCombo->addItem(QString(ch)); ch = ch.unicode() + 1; }

21 22 23 24 25 26 27 28 }

Le constructeur masque les zones secondaire et tertiaire de la bote de dialogue. Il dnit aussi la proprit sizeConstraint de la disposition du formulaire en QLayout::SetFixedSize, lutilisateur ne pourra donc pas la redimensionner. La disposition se charge ensuite de redimensionner automatiquement la bote de dialogue quand des widgets enfants sont afchs ou masqus, vous tre donc sr que la bote de dialogue sera toujours prsente dans sa taille optimale. Le slot setColumnRange() initialise le contenu des zones de liste droulante en fonction des colonnes slectionnes dans le tableur. Nous insrons un lment "None" dans ces zones de liste pour les cls secondaire et tertiaire (facultatives). Les lignes 19 et 20 prsentent un comportement subtil de la disposition. La fonction QWidget::sizeHint() retourne la taille "idale" dun widget, ce que le systme de disposition essaie de respecter. Ceci explique pourquoi les diffrents types de widgets, ou des widgets similaires avec un contenu diffrent, peuvent se voir attribuer des tailles diffrentes par le systme de disposition. Concernant les zones de liste droulante, cela signie que les zones secondaire et tertiaire qui contiennent "None" seront plus grandes que la zone primaire qui ne contient que des entres une lettre. Pour viter cette incohrence, nous dnissons la taille minimale de la zone de liste droulante primaire en taille idale de la zone secondaire. Voici une fonction de test main() qui congure la plage de manire inclure les colonnes C F, puis afche la bote de dialogue :
#include <QApplication> #include "sortdialog.h"

Chapitre 2

Crer des botes de dialogue

39

int main(int argc, char *argv[]) { QApplication app(argc, argv); SortDialog *dialog = new SortDialog; dialog->setColumnRange(C, F); dialog->show(); return app.exec(); }

Ceci termine la bote de dialogue extensible. Vous pouvez constater que ce type de bote de dialogue nest pas plus compliqu concevoir quune bote de dialogue ordinaire : tout ce dont vous avez besoin, cest un bouton de basculement, quelques connexions signal-slot supplmentaires et une disposition non redimensionnable. Dans des applications de production, il est assez frquent que le bouton qui contrle lextension afche le texte Advanced >>> quand seule la bote de dialogue de base est visible et Advanced <<< quand elle est dveloppe. Cest facile concevoir dans Qt en appelant setText() sur QPushButton ds quon clique dessus. Lautre type courant de bote de dialogue multiforme, les botes de dialogue multipages, est encore plus facile concevoir dans Qt, soit en crant le code, soit par le biais du Qt Designer. De telles botes de dialogue peuvent tre gnres de diverses manires.

Un QTabWidget peut tre exploit indpendamment. Il propose une barre donglets en haut qui contrle un QStackedWidget intgr. Un QListWidget et un QStackedWidget peuvent tre employs ensemble, avec llment en cours de QListWidget qui dtermine quelle page est afche par QStackedWidget, en connectant le signal QListWidget::currentRowChanged() au slot QStackedWidget::setCurrentIndex(). Un QTreeWidget peut tre utilis avec un QStackedWidget de la mme faon quavec un QListWidget. La classe QStackedWidget est aborde au Chapitre 6.

Botes de dialogue dynamiques


Les botes de dialogue dynamiques sont cres depuis les chiers .ui du Qt Designer au moment de lexcution. Au lieu de convertir le chier .ui en code C++ grce uic, vous pouvez charger le chier lexcution laide de la classe QUiLoader:
QUiLoader uiLoader; QFile file("sortdialog.ui"); QWidget *sortDialog = uiLoader.load(&file); if (sortDialog) { ... }

40

Qt4 et C++ : Programmation dinterfaces GUI

Vous pouvez accder aux widgets enfants du formulaire en utilisant QObject::findChild<T>():


QComboBox *primaryColumnCombo = sortDialog->findChild<QComboBox *>("primaryColumnCombo"); if (primaryColumnCombo) { ... }

La fonction findChild<T>() est une fonction membre modle qui retourne lobjet enfant qui correspond au nom et au type donn. Vu les limites du compilateur, elle nest pas disponible pour MSVC 6. Si vous devez utiliser le compilateur MSVC 6, appelez plutt la fonction globale qFindChild<T>() qui fonctionne exactement de la mme faon. La classe QUiLoader se situe dans une bibliothque part. Pour employer QUiLoader depuis une application Qt, vous devez ajouter cette ligne de code au chier .pro de lapplication :
CONFIG += uitools

Les botes de dialogue dynamiques vous permettent de modier la disposition dun formulaire sans recompiler lapplication. Elles peuvent aussi servir crer des applications client lger, o lexcutable intgre principalement un formulaire frontal et o tous les autres formulaires sont crs comme ncessaire.

Classes de widgets et de botes de dialogue intgres


Qt propose un ensemble complet de widgets intgrs et de botes de dialogue courantes adapts la plupart des situations. Dans cette section, nous allons prsenter une capture de la plupart dentre eux. Quelques widgets spcialiss ne sont tudis quultrieurement : les widgets de fentre principale comme QMenuBar, QToolBar et QStatusBar sont abords dans le Chapitre 3, et les widgets lis la disposition, tels que QSplitter et QScrollArea, sont analyss dans le Chapitre 6. La majorit des widgets intgrs et des botes de dialogue est prsente dans les exemples de ce livre. Dans les captures suivantes, les widgets sont afchs avec le style Plastique.
Figure 2.16 Les widgets bouton de Qt
QPushButton QToolButton QCheckBox QRadioButton

Qt propose quatre types de "boutons" : QPushButton, QToolButton, QCheckBox et QRadioButton. QPushButton et QToolButton sont le plus souvent ajouts pour initier une action quand on clique dessus, mais ils peuvent aussi se comporter comme des boutons de basculement

Chapitre 2

Crer des botes de dialogue

41

(clic pour enfoncer, clic pour restaurer). QCheckBox peut servir pour les options indpendantes on/off, alors que les QRadioButton sexcluent mutuellement.
Figure 2.17 Les widgets conteneurs une seule page de Qt

QGroupBox

QFrame

Les widgets conteneurs de Qt sont des widgets qui contiennent dautres widgets. QFrame peut aussi tre employ seul pour tracer simplement des lignes et il est hrit par la plupart des autres classes de widgets, dont QToolBox et QLabel.
Figure 2.18 Les widgets conteneurs multipages de Qt

QTabWidget

QToolBox

QTabWidget et QToolBox sont des widgets multipages. Chaque page est un widget enfant, et les pages sont numrotes en commenant 0.
Figure 2.19 Les widgets dafchage dlments de Qt

QListView(liste)

QTreeView

QListView(icnes)

QTableView

42

Qt4 et C++ : Programmation dinterfaces GUI

Les afchages dlments sont optimiss pour grer de grandes quantits de donnes et font souvent appel des barres de dlement. Le mcanisme de la barre de dlement est implment dans QAbstractScrollArea, une classe de base pour les afchages dlments et dautres types de widgets dlement. Qt fournit quelques widgets simplement destins la prsentation des informations. QLabel est le plus important dentre eux et peut tre employ pour afcher un texte enrichi (grce une syntaxe simple de style HTML) et des images. QTextBrowser est une sous-classe de QTextEdit en lecture seule qui prend en charge la syntaxe HTML de base, y compris les listes, les tables, les images et les liens hypertexte. LAssistant de Qt se sert de QTextBrowser pour prsenter la documentation lutilisateur.
Figure 2.20 Les widgets dafchage de Qt
QLabel(texte) QLCDNumber QProgressBar

QLabel (image)

QTextBrowser

Qt propose plusieurs widgets pour les entres de donnes. QLineEdit peut contrler son entre par le biais dun masque de saisie ou dun validateur. QTextEdit est une sous-classe de QAbstractScrollArea capable de modier de grandes quantits de texte. Qt met votre disposition un ensemble standard de botes de dialogue courantes pratiques pour demander lutilisateur de choisir une couleur, une police ou un chier ou dimprimer un document. Sous Windows et Mac OS X, Qt exploite les botes de dialogue natives plutt que ses propres botes de dialogue si possible. Une bote de message polyvalente et une bote de dialogue derreur qui conserve les messages afchs apparaissent. La progression des oprations longues peut tre indique dans un QProgressDialog ou QProgressBar prsent prcdemment. QInputDialog se rvle trs pratique quand une seule ligne de texte ou un seul chiffre est demand lutilisateur. Les widgets intgrs et les botes de dialogue courantes mettent votre disposition de nombreuses fonctionnalits prtes lemploi. Des exigences plus particulires peuvent souvent tre satisfaites en congurant les proprits du widget ou en connectant des signaux des slots et en implmentant un comportement personnalis dans les slots.

Chapitre 2

Crer des botes de dialogue

43

Figure 2.21 Les widgets dentre de Qt

QSpinBox

QDoubleSpinBox

QComboBox

QDateEdit

QTimeEdit

QDateTimeEdit

QScrollBar

QSlider

QLineEdit

QTextEdit

QDial

Figure 2.22 Les botes de dialogue relatives la couleur et la police de Qt

QColorDialog

QFontDialog

Figure 2.23 Les botes de dialogue relatives limpression et aux chiers de Qt


QPageSetupDialog

QFileDialog

QPrintDialog

44

Qt4 et C++ : Programmation dinterfaces GUI

Figure 2.24 Les botes de dialogue de feedback de Qt


QInputDialog QProgressDialog

QMessageBox

QErrorMessage

Dans certains cas, il est judicieux de crer un widget personnalis en partant de zro. Qt facilite normment ce processus, et les widgets personnaliss peuvent accder la mme fonction de dessin indpendante de la plate-forme que les widgets intgrs de Qt. Les widgets personnaliss peuvent mme tre intgrs par le biais du Qt Designer, de sorte quils puissent tre employs de la mme faon que les widgets intgrs de Qt. Le Chapitre 5 vous explique comment crer des widgets personnaliss.

3
Crer des fentres principales
Au sommaire de ce chapitre Drivation de QMainWindow Crer des menus et des barres doutils Congurer la barre dtat Implmenter le menu File Utiliser des botes de dialogue Stocker des paramtres Documents multiples Pages daccueil

Ce chapitre vous apprendra crer des fentres principales avec Qt. Vous serez ainsi capable de concevoir toute linterface utilisateur dune application, constitue de menus, de barres doutils, dune barre dtat et dautant de botes de dialogue que ncessaire. La fentre principale dune application fournit le cadre dans lequel linterface utilisateur est gnre. Celle de lapplication Spreadsheet illustre en Figure 3.1 servira de base pour ltude dans ce chapitre. Cette application emploie les botes de dialogue Find, Go-to-Cell et Sort cres au Chapitre 2.

46

Qt4 et C++ : Programmation dinterfaces GUI

Figure 3.1 Lapplication Spreadsheet

Derrire la plupart des applications GUI se cache du code qui fournit les fonctionnalits sousjacentes par exemple, le code qui lit et crit des chiers ou qui traite les donnes prsentes dans linterface utilisateur. Au Chapitre 4, vous verrez comment implmenter de telles fonctionnalits, toujours en utilisant lapplication Spreadsheet comme exemple.

Drivation de QMainWindow
La fentre principale dune application est cre en drivant QMainWindow. La plupart des techniques tudies dans le Chapitre 2 pour crer des botes de dialogue sappliquent galement la conception de fentres principales, puisque QDialog et QMainWindow hritent de QWidget. Les fentres principales peuvent tre cres laide du Qt Designer, mais dans ce chapitre nous effectuerons tout le processus dans du code pour vous montrer le fonctionnement. Si vous prfrez une approche plus visuelle, consultez le chapitre "Creating Main Windows in Qt Designer" dans le manuel en ligne de cet outil. Le code source de la fentre principale de lapplication Spreadsheet est rparti entre mainwindow.h et mainwindow.cpp. Commenons par examiner le chier den-tte :
#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> class class class class QAction; QLabel; FindDialog; Spreadsheet;

Chapitre 3

Crer des fentres principales

47

class MainWindow: public QMainWindow { Q_OBJECT public: MainWindow(); protected: void closeEvent(QCloseEvent *event);

Nous dnissons la classe MainWindow comme une sous-classe de QMainWindow. Elle contient la macro Q_ OBJECT puisquelle fournit ses propres signaux et slots. La fonction closeEvent() est une fonction virtuelle dans QWidget qui est appele automatiquement quand lutilisateur ferme la fentre. Elle est rimplmente dans MainWindow, de sorte que vous puissiez poser lutilisateur la question standard "Voulez-vous enregistrer vos modications ?" et sauvegarder les prfrences de lutilisateur sur le disque.
private slots: void newFile(); void open(); bool save(); bool saveAs(); void find(); void goToCell(); void sort(); void about();

Certaines options de menu, telles que File > New (Fichier > Nouveau) et Help > About (Aide > A propos), sont implmentes comme des slots privs dans MainWindow. La majorit des slots ont une valeur de retour void, mais save() et saveAs() retournent une valeur bool. La valeur de retour est ignore quand un slot est excut en rponse un signal, mais lorsque vous invoquez un slot comme une fonction, la valeur de retour est disponible, comme si vous aviez appel nimporte quelle fonction C++ ordinaire.
void openRecentFile(); void updateStatusBar(); void spreadsheetModified(); private: void void void void void void void bool bool bool

createActions(); createMenus(); createContextMenu(); createToolBars(); createStatusBar(); readSettings(); writeSettings(); okToContinue(); loadFile(const QString &fileName); saveFile(const QString &fileName);

48

Qt4 et C++ : Programmation dinterfaces GUI

void setCurrentFile(const QString &fileName); void updateRecentFileActions(); QString strippedName(const QString &fullFileName);

La fentre principale a besoin de slots privs et de plusieurs fonctions prives pour prendre en charge linterface utilisateur.
Spreadsheet *spreadsheet; FindDialog *findDialog; QLabel *locationLabel; QLabel *formulaLabel; QStringList recentFiles; QString curFile; enum { MaxRecentFiles = 5 }; QAction *recentFileActions[MaxRecentFiles]; QAction *separatorAction; QMenu *fileMenu; QMenu *editMenu; ... QToolBar *fileToolBar; QToolBar *editToolBar; QAction *newAction; QAction *openAction; ... QAction *aboutQtAction; }; #endif

En plus de ses slots et fonctions privs, MainWindow possde aussi de nombreuses variables prives. Elles seront analyses au fur et mesure que vous les rencontrerez. Nous allons dsormais passer en revue limplmentation :
#include #include #include #include #include #include <QtGui> "finddialog.h" "gotocelldialog.h" "mainwindow.h" "sortdialog.h" "spreadsheet.h"

Nous incluons le chier den-tte <QtGui>, qui contient la dnition de toutes les classes Qt utilises dans notre sous-classe. Nous englobons aussi certains chiers den-tte personnaliss, notamment finddialog.h, gotocelldialog.h et sortdialog.h du Chapitre 2.
MainWindow::MainWindow() { spreadsheet = new Spreadsheet; setCentralWidget(spreadsheet); createActions();

Chapitre 3

Crer des fentres principales

49

createMenus(); createContextMenu(); createToolBars(); createStatusBar(); readSettings(); findDialog = 0; setWindowIcon(QIcon(":/images/icon.png")); setCurrentFile(""); }

Dans le constructeur, nous commenons par crer un widget Spreadsheet et nous le congurons de manire ce quil devienne le widget central de la fentre principale. Le widget central se trouve au milieu de la fentre principale (voir Figure 3.2). La classe Spreadsheet est une sous-classe de QTableWidget avec certaines fonctions de tableur, comme la prise en charge des formules de tableur. Nous limplmenterons dans le Chapitre 4. Nous appelons les fonctions prives createActions(), createMenus(), createContextMenu(), createToolBars() et createStatusBar() pour congurer le reste de la fentre principale. Nous invoquons galement la fonction prive readSettings() an de lire les paramtres stocks de lapplication. Nous initialisons le pointeur findDialog pour que ce soit un pointeur nul ; au premier appel de MainWindow::find(), nous crerons lobjet FindDialog. A la n du constructeur, nous dnissons licne de la fentre en icon.png, un chier PNG. Qt supporte de nombreux formats dimage, dont BMP, GIF1, JPEG, PNG, PNM, XBM et XPM. Lappel de QWidget::setWindowIcon() dnit licne afche dans le coin suprieur gauche de la fentre. Malheureusement, il nexiste aucun moyen indpendant de la plate-forme pour congurer licne de lapplication qui apparat sur le bureau. Les procdures spciques la plate-forme sont expliques ladresse suivante : http://doc.trolltech.com/4.1/appicon.html. Les applications GUI utilisent gnralement beaucoup dimages. Il existe plusieurs mthodes pour introduire des images dans une application. Les plus communes sont les suivantes :

stocker des images dans des chiers et les charger lexcution ; inclure des chiers XPM dans le code source (Cela fonctionne parce que les chiers XPM sont aussi des chiers C++ valides.) ; utiliser le mcanisme des ressources de Qt.

1. La prise en charge du format GIF est dsactive dans Qt par dfaut, parce que lalgorithme de dcompression utilis par les chiers GIF tait brevet dans certains pays o les brevets logiciels taient reconnus. Nous pensons que ce brevet est arriv expiration dans le monde entier. Pour activer le support GIF dans Qt, transmettez loption de ligne de commande -qt-gif au script configure ou dnissez loption approprie dans le programme dinstallation de Qt.

50

Qt4 et C++ : Programmation dinterfaces GUI

Figure 3.2 Les zones de QMainWindow

Titre de la fentre
Barre de menus Zones de la barre d'outils Zones de la fentre ancre

Widget central

Barre d'tat

Dans notre cas, nous employons le mcanisme des ressources de Qt, puisquil savre plus pratique que de charger des chiers lexcution et il est compatible avec nimporte quel format dimage pris en charge. Nous avons choisi de stocker les images dans larborescence source dans un sous-rpertoire nomm images. Pour utiliser le systme des ressources de Qt, nous devons crer un chier de ressources et ajouter une ligne au chier .pro qui identie le chier de ressources. Dans cet exemple, nous avons appel le chier de ressources spreadsheet.qrc, nous insrons donc la ligne suivante dans le chier .pro:
RESOURCES = spreadsheet.qrc

Le chier de ressources lui-mme utilise un format XML simple. Voici un extrait de celui que nous avons employ :
<!DOCTYPE RCC><RCC version="1.0"> <qresource> <file>images/icon.png</file> ... <file>images/gotocell.png</file> </qresource> </RCC>

Les chiers de ressources sont compils dans lexcutable de lapplication, vous ne pouvez donc pas les perdre. Quand vous vous rfrez aux ressources, vous codez le prxe :/ (double point, slash), cest pourquoi licne est spcie comme suit, :/images/icon.png. Les ressources peuvent tre de nimporte quel type (pas uniquement des images) et vous avez la possibilit de les utiliser la plupart des emplacements o Qt attend un nom de chier. Vous les tudierez plus en dtail au Chapitre 12.

Chapitre 3

Crer des fentres principales

51

Crer des menus et des barres doutils


La majorit des applications GUI modernes proposent des menus, des menus contextuels et des barres doutils. Les menus permettent aux utilisateurs dexplorer lapplication et dapprendre connatre de nouvelles commandes, alors que les menus contextuels et les barres doutils fournissent un accs rapide aux fonctionnalits frquemment utilises.

Figure 3.3 Les menus de lapplication Spreadsheet

Qt simplie la programmation des menus et des barres doutils grce son concept daction. Une action est un lment qui peut tre ajout nimporte quel nombre de menus et barres doutils. Crer des menus et des barres doutils dans Qt implique ces tapes :

crer et congurer les actions ; crer des menus et y introduire des actions ; crer des barres doutils et y introduire des actions.

Dans lapplication Spreadsheet, les actions sont cres dans createActions():


void MainWindow::createActions() { newAction = new QAction(tr("&New"), this); newAction->setIcon(QIcon(":/images/new.png")); newAction->setShortcut(tr("Ctrl+N")); newAction->setStatusTip(tr("Create a new spreadsheet file")); connect(newAction, SIGNAL(triggered()), this, SLOT(newFile()));

Laction New a un bouton daccs rapide (New), un parent (la fentre principale), une icne (new.png), un raccourci clavier (Ctrl+N) et une infobulle lie ltat. Nous connectons le signal triggered() de laction au slot priv newFile() de la fentre principale, que nous implmenterons dans la prochaine section. Cette connexion garantit que lorsque lutilisateur slectionne File > New, clique sur le bouton New de la barre doutils, ou appuie sur Ctrl+N, le slot newFile() est appel.

52

Qt4 et C++ : Programmation dinterfaces GUI

Les actions Open, Save et Save As ressemblent beaucoup laction New, nous passerons donc directement la partie "chiers ouverts rcemment" du menu File :
... for (int i = 0; i < MaxRecentFiles; ++i) { recentFileActions[i] = new QAction(this); recentFileActions[i]->setVisible(false); connect(recentFileActions[i], SIGNAL(triggered()), this, SLOT(openRecentFile())); }

Nous alimentons le tableau recentFileActions avec des actions. Chaque action est masque et connecte au slot openRecentFile(). Plus tard, nous verrons comment afcher et utiliser les actions relatives aux chiers rcents. Nous pouvons donc passer laction Select All:
... selectAllAction = new QAction(tr("&All"), this); selectAllAction->setShortcut(tr("Ctrl+A")); selectAllAction->setStatusTip(tr("Select all the cells in the " "spreadsheet")); connect(selectAllAction, SIGNAL(triggered()), spreadsheet, SLOT(selectAll()));

Le slot selectAll() est fourni par lun des anctres de QTableWidget, QAbstractItemView, nous navons donc pas limplmenter nous-mmes. Continuons donc par laction Show Grid dans le menu Options :
... showGridAction = new QAction(tr("&Show Grid"), this); showGridAction->setCheckable(true); showGridAction->setChecked(spreadsheet->showGrid()); showGridAction->setStatusTip(tr("Show or hide the spreadsheets " "grid")); connect(showGridAction, SIGNAL(toggled(bool)), spreadsheet, SLOT(setShowGrid(bool)));

Show Grid est une action cocher. Elle est afche avec une coche dans le menu et est implmente comme un bouton bascule dans la barre doutils. Quand laction est active, le composant Spreadsheet afche une grille. Nous initialisons laction avec la valeur par dfaut du composant Spreadsheet, de sorte quelles soient synchronises au dmarrage. Puis nous connectons le signal toggled(bool) de laction Show Grid au slot setShowGrid(bool) du composant Spreadsheet, quil hrite de QTableWidget. Lorsque cette action est ajoute un menu ou une barre doutils, lutilisateur peut activer ou dsactiver lafchage de la grille. Les actions Show Grid et Auto-Recalculate sont des actions cocher indpendantes. Qt prend aussi en charge des actions qui sexcluent mutuellement par le biais de la classe QActionGroup.

Chapitre 3

Crer des fentres principales

53

... aboutQtAction = new QAction(tr("About &Qt"), this); aboutQtAction->setStatusTip(tr("Show the Qt librarys About box")); connect(aboutQtAction, SIGNAL(triggered()), qApp, SLOT(aboutQt())); }

Concernant laction About Qt, nous utilisons le slot aboutQt() de lobjet QApplication, accessible via la variable globale qApp.
Figure 3.4 About Qt

Maintenant que nous avons cr les actions, nous pouvons poursuivre en concevant un systme de menus qui les englobe :
void MainWindow::createMenus() { fileMenu = menuBar()->addMenu(tr("&File")); fileMenu->addAction(newAction); fileMenu->addAction(openAction); fileMenu->addAction(saveAction); fileMenu->addAction(saveAsAction); separatorAction = fileMenu->addSeparator(); for (int i = 0; i < MaxRecentFiles; ++i) fileMenu->addAction(recentFileActions[i]); fileMenu->addSeparator(); fileMenu->addAction(exitAction);

Dans Qt, les menus sont des instances de QMenu. La fonction addMenu() cre un widget QMenu avec le texte spci et lajoute la barre de menus. La fonction QMainWindow::menuBar() retourne un pointeur vers un QMenuBar. La barre de menus est cre la premire fois que menuBar() est appel. Nous commenons par crer le menu File, puis nous y ajoutons les actions New, Open, Save et Save As. Nous insrons un sparateur pour regrouper visuellement des lments connexes. Nous utilisons une boucle for pour ajouter les actions (masques lorigine) du tableau recentFileActions, puis nous ajoutons laction exitAction la n.

54

Qt4 et C++ : Programmation dinterfaces GUI

Nous avons conserv un pointeur vers lun des sparateurs. Nous avons ainsi la possibilit de le masquer (sil ny a pas de chiers rcents) ou de lafcher, parce que nous ne voulons pas afcher deux sparateurs sans rien entre eux.
editMenu = menuBar()->addMenu(tr("&Edit")); editMenu->addAction(cutAction); editMenu->addAction(copyAction); editMenu->addAction(pasteAction); editMenu->addAction(deleteAction); selectSubMenu = editMenu->addMenu(tr("&Select")); selectSubMenu->addAction(selectRowAction); selectSubMenu->addAction(selectColumnAction); selectSubMenu->addAction(selectAllAction); editMenu->addSeparator(); editMenu->addAction(findAction); editMenu->addAction(goToCellAction);

Occupons-nous dsormais de crer le menu Edit, en ajoutant des actions avec QMenu::addAction() comme nous lavons fait pour le menu File et en ajoutant le sous-menu avec QMenu::addMenu() lendroit o nous souhaitons quil apparaisse. Le sous-menu, comme le menu auquel il appartient, est un QMenu.
toolsMenu = menuBar()->addMenu(tr("&Tools")); toolsMenu->addAction(recalculateAction); toolsMenu->addAction(sortAction); optionsMenu = menuBar()->addMenu(tr("&Options")); optionsMenu->addAction(showGridAction); optionsMenu->addAction(autoRecalcAction); menuBar()->addSeparator(); helpMenu = menuBar()->addMenu(tr("&Help")); helpMenu->addAction(aboutAction); helpMenu->addAction(aboutQtAction); }

Nous crons les menus Tools, Options et Help de manire similaire. Nous introduisons un sparateur entre les menus Options et Help. En styles Motif et CDE, le sparateur aligne le menu Help droite ; dans les autres styles, le sparateur est ignor.
Figure 3.5 Barre de menus en styles Motif et Windows
void MainWindow::createContextMenu() { spreadsheet->addAction(cutAction);

Chapitre 3

Crer des fentres principales

55

spreadsheet->addAction(copyAction); spreadsheet->addAction(pasteAction); spreadsheet->setContextMenuPolicy(Qt::ActionsContextMenu); }

Tout widget Qt peut avoir une liste de QAction associe. Pour proposer un menu contextuel pour lapplication, nous ajoutons les actions souhaites au widget Spreadsheet et nous dnissons la stratgie du menu contextuel de ce widget de sorte quil afche un menu contextuel avec ces actions. Les menus contextuels sont invoqus en cliquant du bouton droit sur un widget ou en appuyant sur une touche spcique la plate-forme.
Figure 3.6 Le menu contextuel de lapplication Spreadsheet

Il existe un moyen plus labor de proposer des menus contextuels : implmenter nouveau la fonction QWidget::contextMenuEvent(), crer un widget QMenu, lalimenter avec les actions voulues et appeler exec().
void MainWindow::createToolBars() { fileToolBar = addToolBar(tr("&File")); fileToolBar->addAction(newAction); fileToolBar->addAction(openAction); fileToolBar->addAction(saveAction); editToolBar = addToolBar(tr("&Edit")); editToolBar->addAction(cutAction); editToolBar->addAction(copyAction); editToolBar->addAction(pasteAction); editToolBar->addSeparator(); editToolBar->addAction(findAction); editToolBar->addAction(goToCellAction); }

La cration de barres doutils ressemble beaucoup celle des menus. Nous concevons les barres doutils File et Edit. Comme un menu, une barre doutils peut possder des sparateurs.
Figure 3.7 Les barres doutils de lapplication Spreadsheet

56

Qt4 et C++ : Programmation dinterfaces GUI

Congurer la barre dtat


Lorsque les menus et les barres doutils sont termins, vous tes prt vous charger de la barre dtat de lapplication Spreadsheet. Normalement, cette barre dtat contient deux indicateurs : lemplacement et la formule de la cellule en cours.
Figure 3.8 La barre dtat de lapplication Spreadsheet

Normal

Infobulle sur l'tat

Message temporaire

Le constructeur de MainWindow appelle createStatusBar() pour congurer la barre dtat :


void MainWindow::createStatusBar() { locationLabel = new QLabel(" W999 "); locationLabel->setAlignment(Qt::AlignHCenter); locationLabel->setMinimumSize(locationLabel->sizeHint()); formulaLabel = new QLabel; formulaLabel->setIndent(3); statusBar()->addWidget(locationLabel); statusBar()->addWidget(formulaLabel, 1); connect(spreadsheet, SIGNAL(currentCellChanged(int, int, int, int)), this, SLOT(updateStatusBar())); connect(spreadsheet, SIGNAL(modified()), this, SLOT(spreadsheetModified())); updateStatusBar(); }

La fonction QMainWindow::statusBar() retourne un pointeur vers la barre dtat. (La barre dtat est cre la premire fois que statusBar() est appele.) Les indicateurs dtat sont simplement des QLabel dont nous modions le texte ds que cela savre ncessaire. Nous avons ajout une indentation formulaLabel, pour que le texte qui y est afch soit lgrement dcal du bord gauche. Quand les QLabel sont ajouts la barre dtat, ils sont automatiquement reparents pour devenir des enfants de cette dernire. La Figure 3.8 montre que les deux tiquettes ont des exigences diffrentes sagissant de lespace. Lindicateur relatif lemplacement de la cellule ne ncessite que trs peu de place, et lorsque la fentre est redimensionne, tout espace supplmentaire devrait revenir lindicateur de la formule de la cellule sur la droite. Vous y parvenez en spciant un facteur de redimensionnement

Chapitre 3

Crer des fentres principales

57

de 1 dans lappel QStatusBar::addWidget() de ltiquette de la formule. Lindicateur demplacement prsente un facteur de redimensionnement par dfaut de 0, ce qui signie quil prfre ne pas tre tir. Quand QStatusBar organise lafchage des widgets indicateur, il essaie de respecter la taille idale de chaque widget spcie par QWidget::sizeHint(), puis redimensionne tout widget tirable pour combler lespace disponible. La taille idale dun widget dpend du contenu de ce widget et varie en fonction des modications du contenu. Pour viter de redimensionner constamment lindicateur demplacement, nous congurons sa taille minimale de sorte quelle sufse pour contenir le texte le plus grand possible ("W999"), avec trs peu despace supplmentaire. Nous dnissons aussi son alignement en Qt::AlignHCenter pour centrer le texte horizontalement. Vers la n de la fonction, nous connectons deux des signaux de Spreadsheet deux des slots de MainWindow: updateStatusBar() et spreadsheetModified().
void MainWindow::updateStatusBar() { locationLabel->setText(spreadsheet->currentLocation()); formulaLabel->setText(spreadsheet->currentFormula()); }

Le slot updateStatusBar() met jour les indicateurs relatifs lemplacement et la formule de la cellule. Il est invoqu ds que lutilisateur dplace le curseur vers une autre cellule. Le slot sert galement de fonction ordinaire la n de createStatusBar() pour initialiser les indicateurs. Il se rvle ncessaire puisque Spreadsheet nmet pas le signal currentCellChanged() au dmarrage.
void MainWindow::spreadsheetModified() { setWindowModified(true); updateStatusBar(); }

Le slot spreadsheetModified() dnit la proprit windowModified en true, ce qui met jour la barre de titre. La fonction met galement jour les indicateurs demplacement et de formule, pour quils retent les circonstances actuelles.

Implmenter le menu File


Dans cette section, nous allons implmenter les slots et les fonctions prives ncessaires pour faire fonctionner les options du menu File et pour grer la liste des chiers ouverts rcemment.
void MainWindow::newFile() { if (okToContinue()) { spreadsheet->clear();

58

Qt4 et C++ : Programmation dinterfaces GUI

setCurrentFile(""); } }

Le slot newFile() est appel lorsque lutilisateur clique sur loption File > New ou sur le bouton New de la barre doutils. La fonction prive okToContinue() demande lutilisateur sil dsire enregistrer ses modications, si certaines modications nont pas t sauvegardes (voir Figure 3.9). Elle retourne true si lutilisateur choisit Yes ou No (vous enregistrez le document en appuyant sur Yes), et false si lutilisateur clique sur Cancel. La fonction Spreadsheet::clear() efface toutes les cellules et formules du tableur. La fonction prive setCurrentFile() met jour le titre de la fentre pour indiquer quun document sans titre est en train dtre modi, en plus de congurer la variable prive curFile et de mettre jour la liste des chiers ouverts rcemment.
Figure 3.9 "Voulez-vous enregistrer vos modications ?"

bool MainWindow::okToContinue() { if (isWindowModified()) { int r = QMessageBox::warning(this, tr("Spreadsheet"), tr("The document has been modified.\n" "Do you want to save your changes?"), QMessageBox::Yes | QMessageBox::Default, QMessageBox::No, QMessageBox::Cancel | QMessageBox::Escape); if (r == QMessageBox::Yes) { return save(); } else if (r == QMessageBox::Cancel) { return false; } } return true; }

Dans okToContinue(), nous contrlons ltat de la proprit windowModified. Sil est correct, nous afchons la bote de message illustre en Figure 3.9. Celle-ci propose les boutons Yes, No et Cancel. QMessageBox::Default dnit le bouton Yes comme bouton par dfaut. QMessageBox::Escape dnit la touche Echap comme synonyme de Cancel. Lappel de warning() peut sembler assez complexe de prime abord, mais la syntaxe gnrale est simple :
QMessageBox::warning(parent, titre, message, bouton0, bouton1, ...);

Chapitre 3

Crer des fentres principales

59

QMessageBox propose aussi information(), question() et critical(), chacun possdant sa propre icne.
Figure 3.10 Icnes de bote de message

Information

Question

Avertissement

Critique

void MainWindow::open() { if (okToContinue()) { QString fileName = QFileDialog::getOpenFileName(this, tr("Open Spreadsheet"), ".", tr("Spreadsheet files (*.sp)")); if (!fileName.isEmpty()) loadFile(fileName); } }

Le slot open() correspond File > Open. Comme newFile(), il appelle dabord okToContinue() pour grer toute modication non sauvegarde. Puis il utilise la fonction statique QFileDialog::getOpenFileName() trs pratique pour demander le nom du nouveau chier lutilisateur. La fonction ouvre une bote de dialogue, permet lutilisateur de choisir un chier et retourne le nom de ce dernier ou une chane vide si lutilisateur clique sur Cancel. Le premier argument de QFileDialog::getOpenFileName() est le widget parent. La relation parent-enfant ne signie pas la mme chose pour les botes de dialogue et pour les autres widgets. Une bote de dialogue est toujours une fentre en soi, mais si elle a un parent, elle est centre en haut de ce dernier par dfaut. Une bote de dialogue enfant partage aussi lentre de la barre des tches de son parent. Le second argument est le titre que la bote de dialogue doit utiliser. Le troisime argument indique le rpertoire depuis lequel il doit dmarrer, dans notre cas le rpertoire en cours. Le quatrime argument spcie les ltres de chier. Un ltre de chier est constitu dun texte descriptif et dun modle gnrique. Si nous avions pris en charge les chiers de valeurs spares par des virgules et les chiers Lotus 1-2-3, en plus du format de chier natif de Spreadsheet, nous aurions employ le ltre suivant :
tr("Spreadsheet files (*.sp)\n" "Comma-separated values files (*.csv)\n" "Lotus 1-2-3 files (*.wk1 *.wks)")

La fonction prive loadFile() a t invoque dans open() pour charger le chier. Nous en avons fait une fonction indpendante, parce que nous aurons besoin de la mme fonctionnalit pour charger les chiers ouverts rcemment :
bool MainWindow::loadFile(const QString &fileName) { if (!spreadsheet->readFile(fileName)) {

60

Qt4 et C++ : Programmation dinterfaces GUI

statusBar()->showMessage(tr("Loading canceled"), 2000); return false; } setCurrentFile(fileName); statusBar()->showMessage(tr("File loaded"), 2000); return true; }

Nous utilisons Spreadsheet::readFile() pour lire le chier sur le disque. Si le chargement est effectu avec succs, nous appelons setCurrentFile() pour mettre jour le titre de la fentre ; sinon, Spreadsheet::readFile() aurait dj inform lutilisateur du problme par une bote de message. En gnral, il est recommand de laisser les composants de bas niveau mettre des messages derreur, parce quils peuvent apporter des dtails prcis sur ce qui sest pass. Dans les deux cas, nous afchons un message dans la barre dtat pendant 2 secondes (2000 millisecondes) pour informer lutilisateur des tches effectues par lapplication.
bool MainWindow::save() { if (curFile.isEmpty()) { return saveAs(); } else { return saveFile(curFile); } } bool MainWindow::saveFile(const QString &fileName) { if (!spreadsheet->writeFile(fileName)) { statusBar()->showMessage(tr("Saving canceled"), 2000); return false; } setCurrentFile(fileName); statusBar()->showMessage(tr("File saved"), 2000); return true; }

Le slot save() correspond File > Save. Si le chier porte dj un nom puisque il a dj t ouvert ou enregistr, save() appelle saveFile() avec ce nom ; sinon il invoque simplement saveAs().
bool MainWindow::saveAs() { QString fileName = QFileDialog::getSaveFileName(this, tr("Save Spreadsheet"), ".", tr("Spreadsheet files (*.sp)")); if (fileName.isEmpty()) return false;

Chapitre 3

Crer des fentres principales

61

return saveFile(fileName); }

Le slot saveAs() correspond File > Save As. Nous appelons QFileDialog::getSaveFileName() pour que lutilisateur indique un nom de chier. Si lutilisateur clique sur Cancel, nous retournons false, qui est ensuite transmis son appelant (save() ou okToContinue()). Si le chier existe dj, la fonction getSaveFileName() demandera lutilisateur de conrmer quil veut bien le remplacer. Ce comportement peut tre modi en transmettant QFileDialog::DontConfirmOverwrite comme argument supplmentaire getSaveFileName().
void MainWindow::closeEvent(QCloseEvent *event) { if (okToContinue()) { writeSettings(); event->accept(); } else { event->ignore(); } }

Quand lutilisateur clique sur File > Exit ou sur le bouton de fermeture dans la barre de titre de la fentre, le slot QWidget::close() est invoqu. Un vnement "close" est donc envoy au widget. En implmentant nouveau QWidget::closeEvent(), nous avons la possibilit de fermer la fentre principale et de dcider si nous voulons aussi fermer la fentre ou non. Si certaines modications nont pas t enregistres et si lutilisateur slectionne Cancel, nous "ignorons" lvnement et la fentre nen sera pas affecte. En temps normal, nous acceptons lvnement ; Qt masque donc la fentre. Nous invoquons galement la fonction prive writeSettings() an de sauvegarder les paramtres en cours de lapplication. Quand la dernire fentre est ferme, lapplication se termine. Si ncessaire, nous pouvons dsactiver ce comportement en congurant la proprit quitOnLastWindowClosed de QApplication en false, auquel cas lapplication continue tre excute jusqu ce que nous appelions QApplication::quit().
void MainWindow::setCurrentFile(const QString &fileName) { curFile = fileName; setWindowModified(false); QString shownName = "Untitled"; if (!curFile.isEmpty()) { shownName = strippedName(curFile); recentFiles.removeAll(curFile); recentFiles.prepend(curFile); updateRecentFileActions(); }

62

Qt4 et C++ : Programmation dinterfaces GUI

setWindowTitle(tr("%1[*] - %2").arg(shownName) .arg(tr("Spreadsheet"))); } QString MainWindow::strippedName(const QString &fullFileName) { return QFileInfo(fullFileName).fileName(); }

Dans setCurrentFile(), nous dnissons la variable prive curFile qui stocke le nom du chier en cours de modication. Avant dafcher le nom du chier dans la barre de titre, nous supprimons le chemin daccs du chier avec strippedName() pour le rendre plus convivial. Chaque QWidget possde une proprit windowModified qui doit tre dnie en true si le document prsente des modications non sauvegardes et en false dans les autres cas. Sous Mac OS X, les documents non sauvegards sont indiqus par un point dans le bouton de fermeture de la barre de titre de la fentre ; sur les autres plates-formes, ils sont indiqus par un astrisque aprs le nom de chier. Qt se charge automatiquement de ce comportement, tant que nous mettons jour la proprit windowModified et que nous plaons le marqueur "[*]" dans le titre de la fentre lendroit o nous souhaitons voir apparatre lastrisque. Le texte transmis la fonction setWindowTitle() tait le suivant :
tr("%1[*] - %2").arg(shownName) .arg(tr("Spreadsheet"))

La fonction QString::arg() remplace le paramtre "%n" de numro le plus bas par son argument et retourne la chane ainsi obtenue. Dans ce cas, arg() est utilis avec deux paramtres "%n". Le premier appel de arg() remplace "%1" ; le second appel remplace "%2".Si le nom de chier est "budget.sp" et quaucun chier de traduction nest charg, la chane obtenue serait "budget.sp[*] Spreadsheet".Il aurait t plus simple dcrire
setWindowTitle(shownName + tr("[*] - Spreadsheet"));

mais arg() offre une plus grande souplesse pour les traducteurs. Sil existe un nom de chier, nous mettons jour recentFiles, la liste des chiers ouverts rcemment. Nous invoquons removeAll() pour supprimer toutes les occurrences du nom de chier dans la liste an dviter les copies ; puis nous appelons prepend() pour ajouter le nom de chier en tant que premier lment. Aprs la mise jour de la liste, nous appelons la fonction prive updateRecentFileActions() de manire mettre jour les entres dans le menu File.
void MainWindow::updateRecentFileActions() { QMutableStringListIterator i(recentFiles); while (i.hasNext()) { if (!QFile::exists(i.next())) i.remove();

Chapitre 3

Crer des fentres principales

63

} for (int j = 0; j < MaxRecentFiles; ++j) { if (j < recentFiles.count()) { QString text = tr("&%1%2") .arg(j + 1) .arg(strippedName(recentFiles[j])); recentFileActions[j]->setText(text); recentFileActions[j]->setData(recentFiles[j]); recentFileActions[j]->setVisible(true); } else { recentFileActions[j]->setVisible(false); } } separatorAction->setVisible(!recentFiles.isEmpty()); }

Nous commenons par supprimer tout chier qui nexiste plus laide dun itrateur de style Java. Certains chiers peuvent avoir t utiliss dans une session antrieure, mais ont t supprims depuis. La variable recentFiles est de type QStringList (liste de QString). Le Chapitre 11 tudie en dtail les classes conteneur comme QStringList vous expliquant comment elles sont lies la bibliothque C++ STL (Standard Template Library), et vous explique comment employer les classes ditrateurs de style Java dans Qt. Nous parcourons ensuite nouveau la liste des chiers, mais cette fois-ci en utilisant une indexation de style tableau. Pour chaque chier, nous crons une chane compose dun caractre &, dun chiffre (j + 1), dun espace et du nom de chier (sans son chemin daccs). Nous dnissons laction correspondante pour quelle utilise ce texte. Par exemple, si le premier chier tait C:\My Documents\tab04.sp, le texte de la premire action serait "&1 tab04.sp".
Figure 3.11 Le menu File avec les chiers ouverts rcemment
separatorAction recentFileActions[0] recentFileActions[1] recentFileActions[2] recentFileActions[3] recentFileActions[4]

Chaque action peut avoir un lment "donne" associ de type QVariant. Le type QVariant peut contenir des valeurs de plusieurs types C++ et Qt ; cest expliqu au Chapitre 11. Ici, nous

64

Qt4 et C++ : Programmation dinterfaces GUI

stockons le nom complet du chier dans llment "donne" de laction, pour pouvoir le rcuprer facilement par la suite. Nous congurons galement laction de sorte quelle soit visible. Sil y a plus dactions de chiers que de chiers rcents, nous masquons simplement les actions supplmentaires. Enn, sil y a au moins un chier rcent, nous dnissons le sparateur pour quil safche.
void MainWindow::openRecentFile() { if (okToContinue()) { QAction *action = qobject_cast<QAction *>(sender()); if (action) loadFile(action->data().toString()); } }

Quand lutilisateur slectionne un chier rcent, le slot openRecentFile() est appel. La fonction okToContinue() est excute si des changements nont pas t sauvegards, et si lutilisateur nannule pas, nous identions quelle action a appel le slot grce QObject::sender(). La fonction qobject_cast<T>() accomplit une conversion dynamique base sur les mtainformations gnres par moc, le compilateur des mta-objets de Qt. Elle retourne un pointeur vers la sous-classe QObject demande, ou 0 si lobjet na pas pu tre converti dans ce type. Contrairement dynamic_cast<T>() du langage C++ standard, qobject_cast<T>() de Qt fonctionne correctement dans les bibliothques dynamiques. Dans notre exemple, nous utilisons qobject_cast<T>() pour convertir un pointeur QObject en une action QAction. Si la conversion a t effectue avec succs (ce devrait tre le cas), nous appelons loadFile() avec le nom complet du chier que nous extrayons des donnes de laction. Notez qutant donn que nous savons que lexpditeur est de type QAction, le programme fonctionnerait toujours si nous avions utilis static_cast<T>() ou une conversion traditionnelle de style C. Consultez la section "Conversions de type" en Annexe B pour connatre les diverses conversions C++.

Utiliser des botes de dialogue


Dans cette section, nous allons vous expliquer comment utiliser des botes de dialogue dans Qt comment les crer et les initialiser, les excuter et rpondre aux slections effectues par lutilisateur interagissant avec elles. Nous emploierons les botes de dialogue Find, Go-to-Cell et Sort cres au Chapitre 2. Nous crerons aussi une bote simple About. Nous commenons par la bote de dialogue Find. Nous voulons que lutilisateur puisse basculer volont entre la fentre principale Spreadsheet et la bote de dialogue Find, cette dernire doit donc tre non modale. Une fentre non modale est excute indpendamment de toute autre fentre dans lapplication.

Chapitre 3

Crer des fentres principales

65

Figure 3.12 La bote de dialogue Find de lapplication Spreadsheet

Lorsque des botes de dialogue non modales sont cres, leurs signaux sont normalement connects aux slots qui rpondent aux interactions de lutilisateur.
void MainWindow::find() { if (!findDialog) { findDialog = new FindDialog(this); connect(findDialog, SIGNAL(findNext(const QString &, Qt::CaseSensitivity)), spreadsheet, SLOT(findNext(const QString &, Qt::CaseSensitivity))); connect(findDialog, SIGNAL(findPrevious(const QString &, Qt::CaseSensitivity)), spreadsheet, SLOT(findPrevious(const QString &, Qt::CaseSensitivity))); } findDialog->show(); findDialog->activateWindow(); }

La bote de dialogue Find est une fentre qui permet lutilisateur de rechercher du texte dans le tableur. Le slot find() est appel lorsque lutilisateur clique sur Edit > Find pour ouvrir la bote de dialogue Find. A ce stade, plusieurs scnarios sont possibles : Cest la premire fois que lutilisateur appelle la bote de dialogue Find. La bote de dialogue Find a dj t appele auparavant, mais lutilisateur la ferme. La bote de dialogue Find a dj t appele auparavant et est toujours afche. Si la bote de dialogue Find nexiste pas encore, nous la crons et nous connectons ses signaux findNext() et findPrevious() aux slots Spreadsheet correspondants. Nous aurions aussi pu crer la bote de dialogue dans le constructeur de MainWindow, mais ajourner sa cration rend le dmarrage plus rapide. De mme, si la bote de dialogue nest jamais utilise, elle nest jamais cre, ce qui vous fait gagner du temps et de la mmoire. Nous invoquons ensuite show() et activateWindow() pour nous assurer que la fentre est visible et active. Un seul appel de show() est sufsant pour afcher et activer une fentre masque, mais la bote de dialogue Find peut tre appele quand sa fentre est dj visible, auquel cas show() ne fait rien et activateWindow() est ncessaire pour activer la fentre. Vous auriez aussi pu crire
if (findDialog->isHidden()) {

66

Qt4 et C++ : Programmation dinterfaces GUI

findDialog->show(); } else { findDialog->activateWindow(); }

Ce code revient regarder des deux cts dune route sens unique avant de traverser. Nous allons prsent analyser la bote de dialogue Go-to-Cell. Nous voulons que lutilisateur louvre, lutilise, puis la ferme sans pouvoir basculer vers dautres fentres dans lapplication. Cela signie que la bote de dialogue Go-to-Cell doit tre modale. Une fentre modale est une fentre qui safche quand elle est appele et bloque lapplication. Tout autre traitement ou interaction est impossible tant que la fentre nest pas ferme. Les botes de dialogue douverture de chier et les botes de message utilises prcdemment taient modales.
Figure 3.13 La bote de dialogue Go-to-Cell de lapplication Spreadsheet

Une bote de dialogue nest pas modale si elle est appele laide de show() ( moins que nous appelions setModal() au pralable pour la rendre modale) ; elle est modale si elle est invoque avec exec().
void MainWindow::goToCell() { GoToCellDialog dialog(this); if (dialog.exec()) { QString str = dialog.lineEdit->text().toUpper(); spreadsheet->setCurrentCell(str.mid(1).toInt() - 1, str[0].unicode() - A); } }

La fonction QDialog::exec() retourne une valeur true (QDialog::Accepted) si la bote de dialogue est accepte, et une valeur false (QDialog::Rejected) dans les autres cas. Souvenez-vous que lorsque nous avons cr la bote de dialogue Go-to-Cell avec le Qt Designer au Chapitre 2, nous avons connect OK accept() et Cancel reject(). Si lutilisateur clique sur OK, nous dnissons la cellule actuelle avec la valeur prsente dans lditeur de lignes. La fonction QTableWidget::setCurrentCell() reoit deux arguments : un index des lignes et un index des colonnes. Dans lapplication Spreadsheet, la cellule A1 correspond la cellule (0, 0) et la cellule B27 la cellule (26, 1). Pour obtenir lindex des lignes du QString retourn par QLineEdit::text(), nous devons extraire le nombre de lignes avec QString::mid() (qui retourne une sous-chane allant du dbut la n de la chane), la convertir en type int avec QString::toInt() et soustraire 1. Pour le nombre de colonnes, nous soustrayons la valeur numrique de A de la valeur numrique du premier caractre en

Chapitre 3

Crer des fentres principales

67

majuscule de la chane. Nous savons que la chane aura le bon format parce que QRegExpValidator cr pour la bote de dialogue nautorise lactivation du bouton OK que sil y a une lettre suivie par 3 chiffres maximum. La fonction goToCell() diffre de tout le code tudi jusqu prsent, puisquelle cre un widget (GoToCellDialog) sous la forme dune variable sur la pile. En ajoutant une ligne, nous aurions pu utiliser tout aussi facilement new et delete:
void MainWindow::goToCell() { GoToCellDialog *dialog = new GoToCellDialog(this); if (dialog->exec()) { QString str = dialog->lineEdit->text().toUpper(); spreadsheet->setCurrentCell(str.mid(1).toInt() - 1, str[0].unicode() - A); } delete dialog; }

La cration de botes de dialogue modales (et de menus contextuels dans des rimplmentations QWidget::contextMenuEvent()) sur la pile est un modle de programmation courant, parce quen rgle gnrale, nous navons plus besoin de la bote de dialogue (ou du menu) aprs lavoir utilise, et elle sera automatiquement dtruite la n de la porte dans laquelle elle volue. Examinons maintenant la bote de dialogue Sort. Celle-ci est une bote de dialogue modale qui permet lutilisateur de trier la zone slectionne sur les colonnes quil spcie. La Figure 3.14 montre un exemple de tri, avec la colonne B comme cl de tri primaire et la colonne A comme cl de tri secondaire (toutes les deux par ordre croissant).
Figure 3.14 Trier la zone slectionne du tableur

(b) Avant le tri

(b) Aprs le tri

void MainWindow::sort() { SortDialog dialog(this); QTableWidgetSelectionRange range = spreadsheet->selectedRange(); dialog.setColumnRange(A + range.leftColumn(), A + range.rightColumn()); if (dialog.exec()) {

68

Qt4 et C++ : Programmation dinterfaces GUI

SpreadsheetCompare compare; compare.keys[0] = dialog.primaryColumnCombo->currentIndex(); compare.keys[1] = dialog.secondaryColumnCombo->currentIndex() - 1; compare.keys[2] = dialog.tertiaryColumnCombo->currentIndex() - 1; compare.ascending[0] = (dialog.primaryOrderCombo->currentIndex() == 0); compare.ascending[1] = (dialog.secondaryOrderCombo->currentIndex() == 0); compare.ascending[2] = (dialog.tertiaryOrderCombo->currentIndex() == 0); spreadsheet->sort(compare); } }

Le code dans sort() suit un modle similaire celui utilis pour goToCell():

Nous crons la bote de dialogue sur la pile et nous linitialisons. Nous ouvrons la bote de dialogue avec exec(). Si lutilisateur clique sur OK, nous extrayons les valeurs saisies par ce dernier partir des widgets de la bote de dialogue et nous les utilisons.

Lappel de setColumnRange() dnit les colonnes disponibles pour le tri sur les colonnes slectionnes. Par exemple, en utilisant la slection illustre en Figure 3.14, range.leftColumn() produirait 0, ce qui fait A + 0 = A, et range.rightColumn() produirait 2, ce qui fait A + 2 = C. Lobjet compare stocke les cls de tri primaire, secondaire et tertiaire, ainsi que leurs ordres de tri. (Nous verrons la dnition de la classe SpreadsheetCompare dans le prochain chapitre.) Lobjet est employ par Spreadsheet::sort() pour comparer deux lignes. Le tableau keys stocke les numros de colonne des cls. Par exemple, si la slection stend de C2 E5, la colonne C correspond la position 0. Le tableau ascending conserve lordre associ chaque cl comme une valeur bool. QComboBox::currentIndex() retourne lindex de llment slectionn, en commenant 0. Concernant les cls secondaire et tertiaire, nous soustrayons un de llment en cours pour prendre en compte llment "None (Aucun)". La fonction sort() rpond la demande, mais elle manque de abilit. Elle suppose que la bote de dialogue Sort est implmente de manire particulire, avec des zones de liste droulante et des lments "None". Cela signie que si nous concevons nouveau la bote de dialogue Sort, nous devrions galement rcrire ce code. Alors que cette approche convient pour une bote de dialogue qui est toujours appele depuis le mme emplacement, elle conduit un vritable cauchemar pour la maintenance si elle est employe plusieurs endroits. Une mthode plus able consiste rendre la classe SortDialog plus intelligente en la faisant crer un objet SpreadsheetCompare auquel son appelant peut ensuite accder. Cela simplie signicativement MainWindow::sort():

Chapitre 3

Crer des fentres principales

69

void MainWindow::sort() { SortDialog dialog(this); QTableWidgetSelectionRange range = spreadsheet->selectedRange(); dialog.setColumnRange(A + range.leftColumn(), A + range.rightColumn()); if (dialog.exec()) spreadsheet->performSort(dialog.comparisonObject()); }

Cette approche conduit des composants relativement indpendants et constitue presque toujours le bon choix pour des botes de dialogue qui seront invoques depuis plusieurs emplacements. Une technique plus radicale serait de transmettre un pointeur lobjet Spreadsheet au moment de linitialisation de lobjet SortDialog et de permettre la bote de dialogue doprer directement sur Spreadsheet. SortDialog devient donc moins gnral, parce quil ne fonctionnera que dans certains types de widgets, mais cela simplie davantage le code en liminant la fonction SortDialog::setColumnRange(). La fonction MainWindow::sort() devient donc
void MainWindow::sort() { SortDialog dialog(this); dialog.setSpreadsheet(spreadsheet); dialog.exec(); }

Cette approche reproduit la premire : lappelant na pas besoin de connatre la bote de dialogue dans les moindres dtails, mais cest la bote de dialogue qui doit totalement connatre les structures de donnes fournies par lappelant. Cette technique peut tre pratique quand la bote de dialogue doit appliquer des changements en direct. Mais comme le code dappel peu able de la premire approche, cette troisime mthode ne fonctionne plus si les structures de donnes changent. Certains dveloppeurs choisissent une approche quant lutilisation des botes de dialogue et nen changent plus. Cela prsente lavantage de favoriser la familiarit et la simplicit, parce que toutes leurs botes de dialogue respectent le mme schma, mais ils passent ct des bnces apports par les autres approches. La meilleure approche consiste choisir la mthode au cas par cas. Nous allons clore cette section avec la bote de dialogue About. Nous pourrions crer une bote de dialogue personnalise comme pour les botes de dialogue Find ou Go-to-Cell pour prsenter les informations relatives lapplication, mais vu que la plupart des botes About adoptent le mme style, Qt propose une solution plus simple.
void MainWindow::about() { QMessageBox::about(this, tr("About Spreadsheet"), tr("<h2>Spreadsheet 1.1</h2>"

70

Qt4 et C++ : Programmation dinterfaces GUI

"<p>Copyright &copy; 2006 Software Inc." "<p>Spreadsheet is a small application that " "demonstrates QAction, QMainWindow, QMenuBar, " "QStatusBar, QTableWidget, QToolBar, and many other " "Qt classes.")); }

Vous obtenez la bote About en appelant tout simplement la fonction statique QMessageBox::about(). Cette fonction ressemble beaucoup QMessageBox::warning(), sauf quelle emploie licne de la fentre parent au lieu de licne standard davertissement.
Figure 3.15 La bote About de Spreadsheet

Jusqu prsent, nous avons utilis plusieurs fonctions statiques commodes dans QMessageBox et QFileDialog. Ces fonctions crent une bote de dialogue, linitialisent et appellent exec(). Il est galement possible, mme si cest moins pratique, de crer un widget QMessageBox ou QFileDialog comme nimporte quel autre widget et dappeler explicitement exec() ou mme show().

Stocker des paramtres


Dans le constructeur MainWindow, nous avons invoqu readSettings() an de charger les paramtres stocks de lapplication. De mme, dans closeEvent(), nous avons appel writeSettings() pour sauvegarder les paramtres. Ces deux fonctions sont les dernires fonctions membres MainWindow qui doivent tre implmentes.
void MainWindow::writeSettings() { QSettings settings("Software Inc.", "Spreadsheet"); settings.setValue("geometry", geometry()); settings.setValue("recentFiles", recentFiles); settings.setValue("showGrid", showGridAction->isChecked()); settings.setValue("autoRecalc", autoRecalcAction->isChecked()); }

Chapitre 3

Crer des fentres principales

71

La fonction writeSettings() enregistre la disposition (position et taille) de la fentre principale, la liste des chiers ouverts rcemment et les options Show Grid et Auto-Recalculate. Par dfaut, QSettings stocke les paramtres de lapplication des emplacements spciques la plate-forme. Sous Windows, il utilise le registre du systme ; sous Unix, il stocke les donnes dans des chiers texte ; sous Mac OS X, il emploie lAPI des prfrences de Core Foundation. Les arguments du constructeur spcient les noms de lorganisation et de lapplication. Ces informations sont exploites dune faon spcique la plate-forme pour trouver un emplacement aux paramtres.

QSettings stocke les paramtres sous forme de paires cl-valeur. La cl est similaire au chemin daccs du systme de chiers. Des sous-cls peuvent tre spcies grce une syntaxe de style chemin daccs (par exemple, ndDialog/matchCase) ou beginGroup() et endGroup():
settings.beginGroup("findDialog"); settings.setValue("matchCase", caseCheckBox->isChecked()); settings.setValue("searchBackward", backwardCheckBox->isChecked()); settings.endGroup();

La valeur peut tre de type int, bool, double, QString, QStringList, ou de nimporte quel autre type pris en charge par QVariant, y compris des types personnaliss enregistrs.
void MainWindow::readSettings() { QSettings settings("Software Inc.", "Spreadsheet"); QRect rect = settings.value("geometry", QRect(200, 200, 400, 400)).toRect(); move(rect.topLeft()); resize(rect.size()); recentFiles = settings.value("recentFiles").toStringList(); updateRecentFileActions(); bool showGrid = settings.value("showGrid", true).toBool(); showGridAction->setChecked(showGrid); bool autoRecalc = settings.value("autoRecalc", true).toBool(); autoRecalcAction->setChecked(autoRecalc); }

La fonction readSettings() charge les paramtres qui taient sauvegards par writeSettings(). Le deuxime argument de la fonction value() indique une valeur par dfaut, dans le cas o aucun paramtre nest disponible. Les valeurs par dfaut sont utilises la premire fois que lapplication est excute. Etant donn quaucun second argument nest indiqu pour la liste des chiers rcents, il sera dni en liste vide la premire excution.

72

Qt4 et C++ : Programmation dinterfaces GUI

Qt propose une fonction QWidget::setGeometry() pour complter QWidget::geometry(), mais elle ne fonctionne pas toujours comme prvu sous X11 en raison des limites de la plupart des gestionnaires de fentre. Cest pour cette raison que nous utilisons plutt move() et resize(). (Voir http://doc.trolltech.com/4.1/geometry.html pour une explication plus dtaille.) Nous avons opt pour une organisation dans MainWindow parmi de nombreuses approches possibles, avec tout le code associ QSettings dans readSettings() et writeSettings(). Un objet QSettings peut tre cr pour identier ou modier un paramtre pendant lexcution de lapplication et nimporte o dans le code. Nous avons dsormais implment MainWindow dans Spreadsheet. Dans les sections suivantes, nous verrons comment lapplication Spreadsheet peut tre modie de manire grer plusieurs documents, et comment implmenter une page daccueil. Nous complterons ses fonctionnalits, notamment avec la gestion des formules et le tri, dans le prochain chapitre.

Documents multiples
Nous sommes dsormais prts coder la fonction main() de lapplication Spreadsheet :
#include <QApplication> #include "mainwindow.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow mainWin; mainWin.show(); return app.exec(); }

Cette fonction main() est lgrement diffrente de celles que nous avons crites jusque l : nous avons cr linstance MainWindow comme une variable sur la pile au lieu dutiliser new. Linstance MainWindow est ensuite automatiquement dtruite quand la fonction se termine. Avec la fonction main() prsente ci-dessus, lapplication Spreadsheet propose une seule fentre principale et ne peut grer quun document la fois. Si vous voulez modier plusieurs documents en mme temps, vous pourriez dmarrer plusieurs instances de lapplication Spreadsheet. Mais ce nest pas aussi pratique pour les utilisateurs que davoir une seule instance de lapplication proposant plusieurs fentres principales, tout comme une instance dun navigateur Web peut fournir plusieurs fentres de navigateur simultanment. Nous modierons lapplication Spreadsheet de sorte quelle puisse grer plusieurs documents. Nous avons tout dabord besoin dun menu File lgrement diffrent : File > New cre une nouvelle fentre principale avec un document vide, au lieu de rutiliser la fentre principale existante.

Chapitre 3

Crer des fentres principales

73

File > Close ferme la fentre principale active. File > Exit ferme toutes les fentres. Dans la version originale du menu File, il ny avait pas doption Close parce quelle aurait eu la mme fonction quExit.

Figure 3.16 Le nouveau menu File

Voici la nouvelle fonction main():


int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow *mainWin = new MainWindow; mainWin->show(); return app.exec(); }

Avec plusieurs fentres, il est maintenant intressant de crer MainWindow avec new, puisque nous avons ensuite la possibilit dexcuter delete sur une fentre principale quand nous avons ni an de librer la mmoire. Voici le nouveau slot MainWindow::newFile():
void MainWindow::newFile() { MainWindow *mainWin = new MainWindow; mainWin->show(); }

Nous crons simplement une nouvelle instance de MainWindow. Cela peut sembler stupide de ne pas conserver un pointeur vers la nouvelle fentre, mais ce nest pas un problme tant donn que Qt assure le suivi de toutes les fentres pour nous. Voici les actions pour Close et Exit :
void MainWindow::createActions() { ... closeAction = new QAction(tr("&Close"), this); closeAction->setShortcut(tr("Ctrl+W")); closeAction->setStatusTip(tr("Close this window"));

74

Qt4 et C++ : Programmation dinterfaces GUI

connect(closeAction, SIGNAL(triggered()), this, SLOT(close())); exitAction = new QAction(tr("E&xit"), this); exitAction->setShortcut(tr("Ctrl+Q")); exitAction->setStatusTip(tr("Exit the application")); connect(exitAction, SIGNAL(triggered()), qApp, SLOT(closeAllWindows())); ... }

Le slot QApplication::closeAllWindows() ferme toutes les fentres de lapplication, moins quune delles ne refuse lvnement close. Cest exactement le comportement dont nous avons besoin ici. Nous navons pas nous soucier des modications non sauvegardes parce que MainWindow::closeEvent() sen charge ds quune fentre est ferme. Il semble que notre application est maintenant capable de grer plusieurs fentres. Malheureusement, il reste un problme masqu : si lutilisateur continue crer et fermer des fentres principales, la machine pourrait ventuellement manquer de mmoire. Cest parce que nous continuons crer des widgets MainWindow dans newFile(), sans jamais les effacer. Quand lutilisateur ferme une fentre principale, le comportement par dfaut consiste la masquer, elle reste donc en mmoire. Vous risquez donc de rencontrer des problmes si le nombre de fentres principales est important. La solution est de dnir lattribut Qt::WA_DeleteOnClose dans le constructeur :
MainWindow::MainWindow() { ... setAttribute(Qt::WA_DeleteOnClose); ... }

Il ordonne Qt de supprimer la fentre lorsquelle est ferme. Lattribut Qt::WA_DeleteOnClose est lun des nombreux indicateurs qui peuvent tre dnis sur un QWidget pour inuencer son comportement. La fuite de mmoire nest pas le seul problme rencontr. La conception de notre application dorigine supposait que nous aurions une seule fentre principale. Dans le cas de plusieurs fentres, chaque fentre principale possde sa propre liste de chiers ouverts rcemment et ses propres options. Il est vident que la liste des chiers ouverts rcemment doit tre globale toute lapplication. En fait, il suft de dclarer la variable recentFiles comme statique pour quune seule instance soit gre par lapplication. Mais nous devons ensuite garantir que tous les appels de updateRecentFileActions() destins mettre jour le menu File concernent bien toutes les fentres principales. Voici le code pour obtenir ce rsultat :
foreach (QWidget *win, QApplication::topLevelWidgets()) { if (MainWindow *mainWin = qobject_cast<MainWindow *>(win)) mainWin->updateRecentFileActions(); }

Chapitre 3

Crer des fentres principales

75

Ce code sappuie sur la construction foreach de Qt (explique au Chapitre 11) pour parcourir toutes les fentres de lapplication et appelle updateRecentFileActions() sur tous les widgets de type MainWindow. Un code similaire peut tre employ pour synchroniser les options Show Grid et Auto-Recalculate ou pour sassurer que le mme chier nest pas charg deux fois. Les applications qui proposent un document par fentre principale sont appeles des applications SDI (single document interface). Il existe une alternative courante sous Windows : MDI (multiple document interface), o lapplication comporte une seule fentre principale qui gre plusieurs fentres de document dans sa zone dafchage centrale. Qt peut tre utilis pour crer des applications SDI et MDI sur toutes les plates-formes prises en charge. La Figure 3.17 montre les deux versions de lapplication Spreadsheet. MDI est abord au Chapitre 6.
Figure 3.17 SDI versus MDI

Pages daccueil
De nombreuses applications afchent une page daccueil au dmarrage. Certains dveloppeurs se servent de cette page pour dissimuler un dmarrage lent, alors que dautres lexploitent pour leurs services marketing. La classe QSplashScreen facilite lajout dune page daccueil aux applications Qt. Cette classe afche une image avant lapparition de la fentre principale. Elle peut aussi crire des messages sur limage pour informer lutilisateur de la progression du processus dinitialisation de lapplication. En gnral, le code de la page daccueil se situe dans main(), avant lappel de QApplication::exec(). Le code suivant est un exemple de fonction main() qui utilise QSplashScreen pour prsenter une page daccueil dans une application qui charge des modules et tablit des connexions rseau au dmarrage.
int main(int argc, char *argv[]) { QApplication app(argc, argv);

76

Qt4 et C++ : Programmation dinterfaces GUI

QSplashScreen *splash = new QSplashScreen; splash->setPixmap(QPixmap(":/images/splash.png")); splash->show(); Qt::Alignment topRight = Qt::AlignRight | Qt::AlignTop; splash->showMessage(QObject::tr("Setting up the main window..."), topRight, Qt::white); MainWindow mainWin; splash->showMessage(QObject::tr("Loading modules..."), topRight, Qt::white); loadModules(); splash->showMessage(QObject::tr("Establishing connections..."), topRight, Qt::white); establishConnections(); mainWin.show(); splash->finish(&mainWin); delete splash; return app.exec(); }

Figure 3.18 Une page daccueil

Nous avons dsormais termin ltude de linterface utilisateur de lapplication Spreadsheet. Dans le prochain chapitre, vous complterez lapplication en implmentant la fonctionnalit principale du tableur.

4
Implmenter la fonctionnalit dapplication
Au sommaire de ce chapitre Le widget central Drivation de QTable Widget Chargement et sauvegarde Implmenter le menu Edit Implmenter les autres menus Drivation de QTableWidgetItem

Dans les deux prcdents chapitres, nous vous avons expliqu comment crer linterface utilisateur de lapplication Spreadsheet. Dans ce chapitre, nous terminerons le programme en codant sa fonctionnalit sous-jacente. Nous verrons entre autres comment charger et sauvegarder des chiers, stocker des donnes en mmoire, implmenter des oprations du presse-papiers et ajouter une prise en charge des formules de la feuille de calcul QTableWidget.

78

Qt4 et C++ : Programmation dinterfaces GUI

Le widget central
La zone centrale dun QMainWindow peut tre occupe par nimporte quel type de widget. Voici quelques possibilits : 1. Utiliser un widget Qt standard. Un widget standard comme QTableWidget ou QTextEdit peut tre employ comme widget central. Dans ce cas, la fonctionnalit de lapplication, telle que le chargement et la sauvegarde des chiers, doit tre implmente quelque part (par exemple dans une sousclasse QMainWindow). 2. Utiliser un widget personnalis. Des applications spcialises ont souvent besoin dafcher des donnes dans un widget personnalis. Par exemple, un programme dditeur dicnes aurait un widget IconEditor comme widget central. Le Chapitre 5 vous explique comment crire des widgets personnaliss dans Qt. 3. Utiliser un QWidget ordinaire avec un gestionnaire de disposition. Il peut arriver que la zone centrale de lapplication soit occupe par plusieurs widgets. Cest possible grce lutilisation dun QWidget comme parent de tous les autres widgets et de gestionnaires de disposition pour dimensionner et positionner les widgets enfants. 4. Utiliser un sparateur. Il existe un autre moyen dutiliser plusieurs widgets ensembles : un QSplitter. QSplitter dispose ses widgets enfants horizontalement ou verticalement et le sparateur offre la possibilit lutilisateur dagir sur cette disposition. Les sparateurs peuvent contenir tout type de widgets, y compris dautres sparateurs. 5. Utiliser un espace de travail MDI. Si lapplication utilise MDI, la zone centrale est occupe par un widget QWorkspace et chaque fentre MDI est un enfant de ce widget. Les dispositions, les sparateurs et les espaces de travail MDI peuvent tre combins des widgets Qt standards ou personnaliss. Le Chapitre 6 traite de ces classes trs en dtail. Concernant lapplication Spreadsheet, une sous-classe QTableWidget sert de widget central. La classe QTableWidget propose dj certaines fonctionnalits de feuille de calcul dont nous avons besoin, mais elle ne prend pas en charge les oprations du presse-papiers et ne comprend pas les formules comme "=A1+A2+A3".Nous implmenterons cette fonction manquante dans la classe Spreadsheet.

Chapitre 4

Implmenter la fonctionnalit dapplication

79

Drivation de QTableWidget
La classe Spreadsheet hrite de QTableWidget. Un QTableWidget est une grille qui reprsente un tableau en deux dimensions. Il afche nimporte quelle cellule que lutilisateur fait dler, dans ses dimensions spcies. Quand lutilisateur saisit du texte dans une cellule vide, QTableWidget cre automatiquement un QTableWidgetItem pour stocker le texte. Implmentons Spreadsheet en commenant par le chier den-tte :
#ifndef SPREADSHEET_H #define SPREADSHEET_H #include <QTableWidget> class Cell; class SpreadsheetCompare;

Len-tte commence par les dclarations pralables des classes Cell et SpreadsheetCompare.
Figure 4.1 Arbres dhritage pour Spreadsheet et Cell
QObject QWidget QTableWidget Spreadsheet QTableWidgetItem Cell

Les attributs dune cellule QTableWidget, tels que son texte et son alignement, sont conservs dans un QTableWidgetItem. Contrairement QTableWidget, QTableWidgetItem nest pas une classe de widget ; cest une classe de donnes. La classe Cell hrite de QTableWidgetItem. Elle est dtaille pendant la prsentation de son implmentation dans la dernire section de ce chapitre.
class Spreadsheet: public QTableWidget { Q_OBJECT public: Spreadsheet(QWidget *parent = 0); bool autoRecalculate() const { return autoRecalc; } QString currentLocation() const; QString currentFormula() const; QTableWidgetSelectionRange selectedRange() const; void clear(); bool readFile(const QString &fileName); bool writeFile(const QString &fileName); void sort(const SpreadsheetCompare &compare);

80

Qt4 et C++ : Programmation dinterfaces GUI

La fonction autoRecalculate() est implmente en mode inline (en ligne) parce quelle indique simplement si le recalcul automatique est activ ou non. Dans le Chapitre 3, nous nous sommes bass sur des fonctions publiques dans Spreadsheet lorsque nous avons implment MainWindow. Par exemple, nous avons appel clear() depuis MainWindow::newFile() pour rinitialiser la feuille de calcul. Nous avons aussi utilis certaines fonctions hrites de QTableWidget, notamment setCurrentCell() et setShowGrid().
public slots: void cut(); void copy(); void paste(); void del(); void selectCurrentRow(); void selectCurrentColumn(); void recalculate(); void setAutoRecalculate(bool recalc); void findNext(const QString &str, Qt::CaseSensitivity cs); void findPrevious(const QString &str, Qt::CaseSensitivity cs); signals: void modified();

Spreadsheet propose plusieurs slots implmentant des actions depuis les menus Edit, Tools et Options, de mme quun signal, modified(), pour annoncer tout changement.
private slots: void somethingChanged();

Nous dnissons un slot priv exploit en interne par la classe Spreadsheet.


private: enum { MagicNumber = 0x7F51C883, RowCount = 999, ColumnCount = 26 }; Cell *cell(int row, int column) const; QString text(int row, int column) const; QString formula(int row, int column) const; void setFormula(int row, int column, const QString &formula); bool autoRecalc; };

Dans la section prive de la classe, nous dclarons trois constantes, quatre fonctions et une variable.
class SpreadsheetCompare { public: bool operator()(const QStringList &row1, const QStringList &row2) const; enum { KeyCount = 3 }; int keys[KeyCount];

Chapitre 4

Implmenter la fonctionnalit dapplication

81

bool ascending[KeyCount]; }; #endif

Le chier den-tte se termine par la dnition de la classe SpreadsheetCompare. Nous reviendrons sur ce point lorsque nous tudierons Spreadsheet::sort(). Nous allons dsormais passer en revue limplmentation :
#include <QtGui> #include "cell.h" #include "spreadsheet.h" Spreadsheet::Spreadsheet(QWidget *parent) : QTableWidget(parent) { autoRecalc = true; setItemPrototype(new Cell); setSelectionMode(ContiguousSelection); connect(this, SIGNAL(itemChanged(QTableWidgetItem *)), this, SLOT(somethingChanged())); clear(); }

Normalement, quand lutilisateur saisit du texte dans une cellule vide, QTableWidget cre automatiquement un QTableWidgetItem pour contenir le texte. Dans notre feuille de calcul, nous voulons plutt crer des lments Cell. Pour y parvenir, nous appelons setItemPrototype() dans le constructeur. En interne, QTableWidget copie llment transmis comme un prototype chaque fois quun nouvel lment est requis. Toujours dans le constructeur, nous dnissons le mode de slection en QAbstractItemView::ContiguousSelection pour autoriser une seule slection rectangulaire. Nous connectons le signal itemChanged() du widget de la table au slot priv somethingChanged(); lorsque lutilisateur modie une cellule, nous sommes donc srs que le slot somethingChanged() est appel. Enn, nous invoquons clear() pour redimensionner la table et congurer les en-ttes de colonne.
void Spreadsheet::clear() { setRowCount(0); setColumnCount(0); setRowCount(RowCount); setColumnCount(ColumnCount); for (int i = 0; i < ColumnCount; ++i) { QTableWidgetItem *item = new QTableWidgetItem; item->setText(QString(QChar(A + i)));

82

Qt4 et C++ : Programmation dinterfaces GUI

setHorizontalHeaderItem(i, item); } setCurrentCell(0, 0); }

La fonction clear() est appele depuis le constructeur Spreadsheet pour initialiser la feuille de calcul. Elle est aussi invoque partir de MainWindow::newFile(). Nous aurions pu utiliser QTableWidget::clear() pour effacer tous les lments et toutes les slections, mais les en-ttes auraient conserv leurs tailles actuelles. Au lieu de cela, nous redimensionnons la table en 0 0. Toute la feuille de calcul est donc efface, y compris les enttes. Nous redimensionnons ensuite la table en ColumnCount _ RowCount (26 _ 999) et nous alimentons len-tte horizontal avec des QTableWidgetItem qui contiennent les noms de colonne "A", "B", , "Z".Nous ne sommes pas obligs de dnir les intituls des en-ttes verticaux, parce quils prsentent par dfaut les valeurs suivantes : "1", "2",..., "999". Pour terminer, nous plaons le curseur au niveau de la cellule A1.
Figure 4.2 Les widgets qui constituent QTableWidget
horizontalHeader() verticalHeader() verticalScrollBar()

viewport()

horizontalScrollBar()

Un QTableWidget se compose de plusieurs widgets enfants (voir Figure 4.2). Un QHeaderView horizontal est positionn en haut, un QHeaderView vertical gauche et deux QScrollBar terminent cette composition. La zone au centre est occupe par un widget spcial appel viewport, sur lequel notre QTableWidget dessine les cellules. Les divers widgets enfants sont accessibles par le biais de fonctions hrites de QTableView et QAbstractScrollArea (voir Figure 4.2). QAbstractScrollArea fournit un viewport quip de deux barres de dlement, que vous pouvez activer ou dsactiver. Sa sous-classe QScrollArea est aborde au Chapitre 6.

Stocker des donnes en tant qulments


Dans lapplication Spreadsheet, chaque cellule non vide est stocke en mmoire sous forme dobjet QTableWidgetItem individuel. Stocker des donnes en tant qulments est une approche galement utilise par QListWidget et QTreeWidget, qui agissent sur QListWidgetItem et QTreeWidgetItem.

Chapitre 4

Implmenter la fonctionnalit dapplication

83

Les classes dlments de Qt peuvent tre directement employes comme des conteneurs de donnes. Par exemple, un QTableWidgetItem stocke par dnition quelques attributs, y compris une chane, une police, une couleur et une icne, ainsi quun pointeur vers QTableWidget. Les lments ont aussi la possibilit de contenir des donnes (QVariant), dont des types personnaliss enregistrs, et en drivant la classe dlments, nous pouvons proposer des fonctionnalits supplmentaires. Dautres kits doutils fournissent un pointeur void dans leurs classes dlments pour conserver des donnes personnalises. Dans Qt, lapproche la plus naturelle consiste utiliser setData() avec un QVariant, mais si un pointeur void est ncessaire, vous driverez simplement une classe dlments et vous ajouterez une variable membre pointeur void. Quand la gestion des donnes devient plus exigeante, comme dans le cas de jeux de donnes de grande taille, dlments de donnes complexes, dune intgration de base de donnes et de vues multiples de donnes, Qt propose un ensemble de classes modle/vue qui sparent les donnes de leur reprsentation visuelle. Ces thmes sont traits au Chapitre 10.

Cell *Spreadsheet::cell(int row, int column) const { return static_cast<Cell *>(item(row, column)); }

La fonction prive cell() retourne lobjet Cell pour une ligne et une colonne donnes. Elle est presque identique QTableWidget::item(), sauf quelle renvoie un pointeur de Cell au lieu dun pointeur de QTableWidgetItem.
QString Spreadsheet::text(int row, int column) const { Cell *c = cell(row, column); if (c) { return c->text(); } else { return ""; } }

La fonction prive text() retourne le texte dune cellule particulire. Si cell() retourne un pointeur nul, la cellule est vide, une chane vide est donc renvoye.
QString Spreadsheet::formula(int row, int column) const { Cell *c = cell(row, column); if (c) { return c->formula(); } else { return ""; } }

84

Qt4 et C++ : Programmation dinterfaces GUI

La fonction formula() retourne la formule de la cellule. Dans la plupart des cas, la formule et le texte sont identiques ; par exemple, la formule "Hello" dtermine la chane "Hello", donc si lutilisateur tape "Hello" dans une cellule et appuie sur Entre, cette cellule afchera le texte "Hello". Mais il y a quelques exceptions : Si la formule est un nombre, elle est interprte en tant que tel. Par exemple, la formule "1,50" interprte la valeur en type double 1,5, qui est afch sous la forme "1,5" justi droite dans la feuille de calcul. Si la formule commence par une apostrophe, le reste de la formule est considr comme du texte. Par exemple, la formule " 12345" interprte cette valeur comme la chane "12345." Si la formule commence par un signe gal (=), elle est considre comme une formule arithmtique. Par exemple, si la cellule A1 contient "12" et si la cellule A2 comporte le chiffre "6", la formule "=A1+A2" est gale 18. La tche qui consiste convertir une formule en valeur est accomplie par la classe Cell. Pour linstant, limportant est de se souvenir que le texte afch dans la cellule est le rsultat de lvaluation dune formule et pas la formule en elle-mme.
void Spreadsheet::setFormula(int row, int column, const QString &formula) { Cell *c = cell(row, column); if (!c) { c = new Cell; setItem(row, column, c); } c->setFormula(formula); }

La fonction prive setFormula() dnit la formule dune cellule donne. Si la cellule contient dj un objet Cell, nous le rutilisons. Sinon, nous crons un nouvel objet Cell et nous appelons QTableWidget::setItem() pour linsrer dans la table. Pour terminer, nous invoquons la propre fonction setFormula() de la cellule, pour actualiser cette dernire si elle est afche lcran. Nous navons pas nous soucier de supprimer lobjet Cell par la suite ; QTableWidget prend en charge la cellule et la supprimera automatiquement au moment voulu.
QString Spreadsheet::currentLocation() const { return QChar(A + currentColumn()) + QString::number(currentRow() + 1); }

La fonction currentLocation() retourne lemplacement de la cellule actuelle dans le format habituel de la feuille de calcul, soit la lettre de la colonne suivie du numro de la ligne. MainWindow::updateStatusBar() lutilise pour afcher lemplacement dans la barre dtat.
QString Spreadsheet::currentFormula() const { return formula(currentRow(), currentColumn()); }

Chapitre 4

Implmenter la fonctionnalit dapplication

85

La fonction currentFormula() retourne la formule de la cellule en cours. Elle est invoque partir de MainWindow::updateStatusBar().
void Spreadsheet::somethingChanged() { if (autoRecalc) recalculate(); emit modified(); }

Le slot priv somethingChanged() recalcule lensemble de la feuille de calcul si loption de "recalcul automatique" est active. Il met galement le signal modified().

Chargement et sauvegarde
Nous allons dsormais implmenter le chargement et la sauvegarde des chiers Spreadsheet grce un format binaire personnalis. Pour ce faire, nous emploierons QFile et QDataStream qui, ensemble, fournissent des entres/sorties binaires indpendantes de la plate-forme. Nous commenons par crire un chier Spreadsheet :
bool Spreadsheet::writeFile(const QString &fileName) { QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { QMessageBox::warning(this, tr("Spreadsheet"), tr("Cannot write file %1:\n%2.") .arg(file.fileName()) .arg(file.errorString())); return false; } QDataStream out(&file); out.setVersion(QDataStream::Qt_4_1); out << quint32(MagicNumber); QApplication::setOverrideCursor(Qt::WaitCursor); for (int row = 0; row < RowCount; ++row) { for (int column = 0; column < ColumnCount; ++column) { QString str = formula(row, column); if (!str.isEmpty()) out << quint16(row) << quint16(column) << str; } } QApplication::restoreOverrideCursor(); return true; }

La fonction writeFile() est appele depuis MainWindow::saveFile() pour crire le chier sur le disque. Elle retourne true en cas de succs et false en cas derreur.

86

Qt4 et C++ : Programmation dinterfaces GUI

Nous crons un objet QFile avec un nom de chier donn et nous invoquons open() pour ouvrir le chier en criture. Nous crons aussi un objet QDataStream qui agit sur le QFile et sen sert pour crire les donnes. Juste avant dcrire les donnes, nous changeons le pointeur de lapplication en pointeur dattente standard (gnralement un sablier) et nous restaurons le pointeur normal lorsque toutes les donnes ont t crites. A la n de la fonction, le chier est ferm automatiquement par le destructeur de QFile. QDataStream prend en charge les types C++ de base, de mme que plusieurs types de Qt. La syntaxe est conue selon les classes <iostream> du langage C++ Standard. Par exemple,
out << x << y << z;

crit les variables x, y et z dans un ux, et


in >> x >> y >> z;

les lit depuis un ux. Etant donn que les types C++ de base char, short, int, long et long long peuvent prsenter des tailles diffrentes selon les plates-formes, il est plus sr de convertir ces valeurs en qint8, quint8, qint16, quint16, qint32, quint32, qint64 ou quint64; vous avez ainsi la garantie de travailler avec un type de la taille annonce (en bits). Le format de chier de lapplication Spreadsheet est assez simple (voir Figure 4.3). Un chier Spreadsheet commence par un numro sur 32 bits qui identie le format de chier (MagicNumber, dni par 0x7F51C883 dans spreadsheet.h, un chiffre alatoire). Ce numro est suivi dune srie de blocs, chacun deux contenant la ligne, la colonne et la formule dune seule cellule. Pour conomiser de lespace, nous ncrivons pas de cellules vides.
Figure 4.3 Le format du chier Spreadsheet
0x7F51C883 123 5 Fr 123 6 Francium

La reprsentation binaire prcise des types de donnes est dtermine par QDataStream. Par exemple, un type quint16 est stock sous forme de deux octets en ordre big-endian, et un QString se compose de la longueur de la chane suivie des caractres Unicode. La reprsentation binaire des types Qt a largement volu depuis Qt 1.0. Il est fort probable quelle continue son dveloppement dans les futures versions de Qt pour suivre lvolution des types existants et pour tenir compte des nouveaux types Qt. Par dfaut, QDataStream utilise la version la plus rcente du format binaire (version 7 dans Qt 4.1), mais il peut tre congur de manire lire des versions antrieures. Pour viter tout problme de compatibilit si lapplication est recompile par la suite avec une nouvelle version de Qt, nous demandons explicitement QDataStream demployer la version 7 quelle que soit la version de Qt utilise pour la compilation. (QDataStream::Qt_4_1 est une constante pratique qui vaut 7.) QDataStream est trs polyvalent. Il peut tre employ sur QFile, mais aussi sur QBuffer, QProcess, QTcpSocket ou QUdpSocket. Qt propose galement une classe QTextStream qui

Chapitre 4

Implmenter la fonctionnalit dapplication

87

peut tre utilise la place de QDataStream pour lire et crire des chiers texte. Le Chapitre 12 se penche en dtail sur ces classes et dcrit les diverses approches consistant grer les diffrentes versions de QDataStream.
bool Spreadsheet::readFile(const QString &fileName) { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { QMessageBox::warning(this, tr("Spreadsheet"), tr("Cannot read file %1:\n%2.") .arg(file.fileName()) .arg(file.errorString())); return false; } QDataStream in(&file); in.setVersion(QDataStream::Qt_4_1); quint32 magic; in >> magic; if (magic!= MagicNumber) { QMessageBox::warning(this, tr("Spreadsheet"), tr("The file is not a Spreadsheet file.")); return false; } clear(); quint16 row; quint16 column; QString str; QApplication::setOverrideCursor(Qt::WaitCursor); while (!in.atEnd()) { in >> row >> column >> str; setFormula(row, column, str); } QApplication::restoreOverrideCursor(); return true; }

La fonction readFile() ressemble beaucoup writeFile(). Nous utilisons QFile pour lire le chier, mais cette fois-ci avec QIODevice::ReadOnly et pas QIODevice::WriteOnly. Puis nous dnissons la version de QDataStream en 7. Le format de lecture doit toujours tre le mme que celui de lcriture. Si le chier dbute par le nombre magique appropri, nous appelons clear() pour vider toutes les cellules de la feuille de calcul et nous lisons les donnes de la cellule. Vu que le chier ne contient que des donnes pour des cellules non vides, et quil est trs improbable que chaque cellule de la feuille de calcul soit dnie, nous devons nous assurer que toutes les cellules sont effaces avant la lecture.

88

Qt4 et C++ : Programmation dinterfaces GUI

Implmenter le menu Edit


Nous sommes dsormais prt implmenter les slots qui correspondent au menu Edit de lapplication. Ce menu est prsent en Figure 4.4.
void Spreadsheet::cut() { copy(); del(); }

Le slot cut() correspond Edit > Cut. Limplmentation est simple parce que Cut est quivalent Copy suivi de Delete.
Figure 4.4 Le menu Edit de lapplication Spreadsheet

void Spreadsheet::copy() { QTableWidgetSelectionRange range = selectedRange(); QString str; for (int i = 0; i < range.rowCount(); ++i) { if (i > 0) str += "\n"; for (int j = 0; j < range.columnCount(); ++j) { if (j > 0) str += "\t"; str += formula(range.topRow() + i, range.leftColumn() + j); } } QApplication::clipboard()->setText(str); }

Le slot copy() correspond Edit > Copy. Il parcourt la slection actuelle (qui est simplement la cellule en cours sil ny a pas de slection explicite). La formule de chaque cellule slectionne est ajoute QString, avec des lignes spares par des sauts de ligne et des colonnes spares par des tabulations. Le presse-papiers est disponible dans Qt par le biais de la fonction statique QApplication::clipboard(). En appelant QClipboard::setText(), le texte est disponible dans le

Chapitre 4

Implmenter la fonctionnalit dapplication

89

presse-papiers, la fois pour cette application et pour dautres qui prennent en charge le texte brut (voir Figure 4.5). Notre format, bas sur les tabulations et les sauts de lignes en tant que sparateurs, est compris par une multitude dapplications, dont Microsoft Excel.
Figure 4.5 Copier une slection dans le presse-papiers

"Red\tGreen\tBlue\nCyan\tMagenta\tYellow"

La fonction QTableWidget::selectedRanges() retourne une liste de plages de slection. Nous savons quil ne peut pas y en avoir plus dune, parce que nous avons dni le mode de slection en QAbstractItemView::ContiguousSelection dans le constructeur. Par souci de commodit, nous congurons une fonction selectedRange() qui retourne la plage de slection :
QTableWidgetSelectionRange Spreadsheet::selectedRange() const { QList<QTableWidgetSelectionRange> ranges = selectedRanges(); if (ranges.isEmpty()) return QTableWidgetSelectionRange(); return ranges.first(); }

Sil ny a quune slection, nous retournons simplement la premire (et unique). Vous ne devriez jamais vous trouver dans le cas o il ny a aucune slection, tant donn que le mode ContiguousSelection considre la cellule en cours comme slectionne. Toutefois pour prvenir tout bogue dans notre programme, nous grons ce cas de gure.
void Spreadsheet::paste() { QTableWidgetSelectionRange range = selectedRange(); QString str = QApplication::clipboard()->text(); QStringList rows = str.split(\n); int numRows = rows.count(); int numColumns = rows.first().count(\t) + 1; if (range.rowCount() * range.columnCount()!= 1 && (range.rowCount()!= numRows || range.columnCount()!= numColumns)) { QMessageBox::information(this, tr("Spreadsheet"), tr("The information cannot be pasted because the copy " "and paste areas arent the same size.")); return; } for (int i = 0; i < numRows; ++i) {

90

Qt4 et C++ : Programmation dinterfaces GUI

QStringList columns = rows[i].split(\t); for (int j = 0; j < numColumns; ++j) { int row = range.topRow() + i; int column = range.leftColumn() + j; if (row < RowCount && column < ColumnCount) setFormula(row, column, columns[j]); } } somethingChanged(); }

Le slot paste() correspond Edit > Paste. Nous rcuprons le texte dans le presse-papiers et nous appelons la fonction statique QString::split() pour adapter la chane au QStringList. Chaque ligne devient une chane dans la liste. Nous dterminons ensuite la dimension de la zone de copie. Le nombre de lignes correspond au nombre de chanes dans QStringList; le nombre de colonnes est le nombre de tabulations la premire ligne, plus 1. Si une seule cellule est slectionne, nous nous servons de cette cellule comme coin suprieur gauche de la zone de collage ; sinon, nous utilisons la slection actuelle comme zone de collage. Pour effectuer le collage, nous parcourons les lignes et nous les divisons en cellules grce QString::split(), mais cette fois-ci avec une tabulation comme sparateur. La Figure 4.6 illustre ces tapes.
Figure 4.6 Coller le texte du pressepapiers dans la feuille de calcul
"Red\tGreen\tBlue\nCyan\tMagenta\tYellow" ["Red\tGreen\tBlue","Cyan\tMagenta\tYellow"] ["Red","Green","Blue"] ["Cyan","Magenta","Yellow"]

void Spreadsheet::del() { foreach (QTableWidgetItem *item, selectedItems()) delete item; }

Le slot del() correspond Edit > Delete. Il suft dutiliser delete sur chaque objet Cell de la slection pour effacer les cellules. QTableWidget remarque quand ses QTableWidgetItem sont supprims et se redessine automatiquement si lun des lments tait visible. Si nous invoquons cell() avec lemplacement dune cellule supprime, il renverra un pointeur nul.

Chapitre 4

Implmenter la fonctionnalit dapplication

91

void Spreadsheet::selectCurrentRow() { selectRow(currentRow()); } void Spreadsheet::selectCurrentColumn() { selectColumn(currentColumn()); }

Les fonctions selectCurrentRow() et selectCurrentColumn() correspondent aux options Edit > Select > Row et Edit > Select > Column. Les implmentations reposent sur les fonctions selectRow() et selectColumn() de QTableWidget. Nous navons pas implmenter la fonctionnalit correspondant Edit > Select > All, tant donn quelle est propose par la fonction hrite QAbstractItemView::selectAll() de QTableWidget.
void Spreadsheet::findNext(const QString &str, Qt::CaseSensitivity cs) { int row = currentRow(); int column = currentColumn() + 1; while (row < RowCount) { while (column < ColumnCount) { if (text(row, column).contains(str, cs)) { clearSelection(); setCurrentCell(row, column); activateWindow(); return; } ++column; } column = 0; ++row; } QApplication::beep(); }

Le slot findNext() parcourt les cellules en commenant par la cellule droite du pointeur et en se dirigeant vers la droite jusqu la dernire colonne, puis il poursuit par la premire colonne dans la ligne du dessous et ainsi de suite jusqu trouver le texte recherch ou jusqu atteindre la toute dernire cellule. Par exemple, si la cellule en cours est la cellule C24, nous recherchons D24, E24, , Z24, puis A25, B25, C25, , Z25, et ainsi de suite jusqu Z999. Si nous trouvons une correspondance, nous supprimons la slection actuelle, nous dplaons le pointeur vers cette cellule et nous activons la fentre qui contient Spreadsheet. Si aucune correspondance nest dcouverte, lapplication met un signal sonore pour indiquer que la recherche na pas abouti.
void Spreadsheet::findPrevious(const QString &str, Qt::CaseSensitivity cs) {

92

Qt4 et C++ : Programmation dinterfaces GUI

int row = currentRow(); int column = currentColumn() - 1; while (row >= 0) { while (column >= 0) { if (text(row, column).contains(str, cs)) { clearSelection(); setCurrentCell(row, column); activateWindow(); return; } --column; } column = ColumnCount - 1; --row; } QApplication::beep(); }

Le slot findPrevious() est similaire findNext(), sauf quil effectue une recherche dans lautre sens et sarrte la cellule A1.

Implmenter les autres menus


Nous allons maintenant implmenter les slots des menus Tools et Options. Ces menus sont illustrs en Figure 4.7.
Figure 4.7 Les menus Tools et Options de lapplication Spreadsheet
void Spreadsheet::recalculate() { for (int row = 0; row < RowCount; ++row) { for (int column = 0; column < ColumnCount; ++column) { if (cell(row, column)) cell(row, column)->setDirty(); } } viewport()->update(); }

Le slot recalculate() correspond Tools > Recalculate. Il est aussi appel automatiquement par Spreadsheet si ncessaire.

Chapitre 4

Implmenter la fonctionnalit dapplication

93

Nous parcourons toutes les cellules et invoquons setDirty() sur chacune delles pour signaler celles qui doivent tre recalcules. La prochaine fois que QTableWidget appelle text() sur Cell pour obtenir la valeur afcher dans la feuille de calcul, la valeur sera recalcule. Nous appelons ensuite update() sur le viewport pour redessiner la feuille de calcul complte. Le code de rafchage dans QTableWidget invoque ensuite text() sur chaque cellule visible pour obtenir la valeur afcher. Vu que nous avons appel setDirty() sur chaque cellule, les appels de text() utiliseront une valeur nouvellement calcule. Le calcul pourrait exiger que les cellules non visibles soient recalcules, rpercutant la mme opration jusqu ce que chaque cellule qui a besoin dtre recalcule pour afcher le bon texte dans le viewport ait une valeur ractualise. Le calcul est effectu par la classe Cell.
void Spreadsheet::setAutoRecalculate(bool recalc) { autoRecalc = recalc; if (autoRecalc) recalculate(); }

Le slot setAutoRecalculate() correspond Options > Auto-Recalculate. Si la fonction est active, nous recalculons immdiatement la feuille de calcul pour nous assurer quelle est jour ; ensuite, recalculate() est appel automatiquement dans somethingChanged(). Nous navons pas besoin dimplmenter quoi que ce soit pour Options > Show Grid, parce que QTableWidget a dj un slot setShowGrid() quil a hrit de sa classe de base QTableView. Tout ce qui reste, cest Spreadsheet::sort() qui est invoque dans MainWindow::sort():
void Spreadsheet::sort(const SpreadsheetCompare &compare) { QList<QStringList> rows; QTableWidgetSelectionRange range = selectedRange(); int i; for (i = 0; i < range.rowCount(); ++i) { QStringList row; for (int j = 0; j < range.columnCount(); ++j) row.append(formula(range.topRow() + i, range.leftColumn() + j)); rows.append(row); } qStableSort(rows.begin(), rows.end(), compare); for (i = 0; i < range.rowCount(); ++i) { for (int j = 0; j < range.columnCount(); ++j) setFormula(range.topRow() + i, range.leftColumn() + j, rows[i][j]); } clearSelection(); somethingChanged(); }

94

Qt4 et C++ : Programmation dinterfaces GUI

Le tri sopre sur la slection actuelle et rorganise les lignes selon les cls et les ordres de tri stocks dans lobjet compare. Nous reprsentons chaque ligne de donnes avec QStringList et nous conservons la slection sous forme de liste de lignes (voir Figure 4.8). Nous nous servons de lalgorithme qStableSort() de Qt et pour une question de simplicit, le tri seffectue sur la formule plutt que sur la valeur. Les algorithmes standards, de mme que les structures de donnes de Qt sont abords au Chapitre 11.
Figure 4.8 Stocker la slection sous forme de liste de lignes
index value ["Edsger","Dijkstra","1930-05-11"] ["Tony","Hoare","1934-01-11"] ["Niklaus","Wirth","1934-02-15"] ["Donald","Knuth","1938-01-10"]

0 1 2 3

La fonction qStableSort() reoit un itrateur de dbut et de n, ainsi quune fonction de comparaison. La fonction de comparaison est une fonction qui reoit deux arguments (deux QStringList) et qui retourne true si le premier argument est "infrieur" au second argument et false dans les autres cas. Lobjet compare que nous transmettons comme fonction de comparaison nest pas vraiment une fonction, mais il peut tre utilis comme telle, comme nous allons le voir.
Figure 4.9 Rintgrer les donnes dans la table aprs le tri
index 0 1 2 3 value ["Donald","Knuth","1938-01-10"] ["Edsger","Dijkstra","1930-05-11"] ["Niklaus","Wirth","1934-02-15"] ["Tony","Hoare","1934-01-11"]

Aprs avoir excut qStableSort(), nous rintgrons les donnes dans la table (voir Figure 4.9), nous effaons la slection et nous appelons somethingChanged(). Dans spreadsheet.h, la classe SpreadsheetCompare tait dnie comme suit :
class SpreadsheetCompare { public: bool operator()(const QStringList &row1, const QStringList &row2) const; enum { KeyCount = 3 }; int keys[KeyCount]; bool ascending[KeyCount]; };

La classe SpreadsheetCompare est spciale parce quelle implmente un oprateur (). Nous avons donc la possibilit dutiliser la classe comme si ctait une fonction. De telles classes sont appeles des objets fonction, ou foncteurs. Pour comprendre comment fonctionnent les foncteurs, nous dbutons par un exemple simple :

Chapitre 4

Implmenter la fonctionnalit dapplication

95

class Square { public: int operator()(int x) const { return x * x; } }

La classe Square fournit une fonction, operator()(int), qui retourne le carr de son paramtre. En nommant la fonction operator()(int), au lieu de compute(int) par exemple, nous avons la possibilit dutiliser un objet de type Square comme si ctait une fonction :
Square square; int y = square(5);

A prsent, analysons un exemple impliquant SpreadsheetCompare:


QStringList row1, row2; QSpreadsheetCompare compare; ... if (compare(row1, row2)) { // row1 est infrieure }

Lobjet compare peut tre employ comme sil tait une fonction compare() ordinaire. De plus, son implmentation peut accder toutes les cls et ordres de tri stocks comme variables membres. Il existe une alternative : nous aurions pu conserver tous les ordres et cls de tri dans des variables globales et utiliser une fonction compare() ordinaire. Cependant, la communication via les variables globales nest pas trs lgante et peut engendrer des bogues subtils. Les foncteurs sont plus puissants pour interfacer avec des fonctions modles comme qStableSort(). Voici limplmentation de la fonction employe pour comparer deux lignes de la feuille de calcul :
bool SpreadsheetCompare::operator()(const QStringList &row1, const QStringList &row2) const { for (int i = 0; i < KeyCount; ++i) { int column = keys[i]; if (column!= -1) { if (row1[column]!= row2[column]) { if (ascending[i]) { return row1[column] < row2[column]; } else { return row1[column] > row2[column]; } } } } return false; }

96

Qt4 et C++ : Programmation dinterfaces GUI

Loprateur retourne true si la premire ligne est infrieure la seconde et false dans les autres cas. La fonction qStableSort() utilise ce rsultat pour effectuer le tri. Les tables keys et ascending de lobjet SpreadsheetCompare sont alimentes dans la fonction MainWindow::sort() (vue au Chapitre 2). Chaque cl possde un index de colonne ou 1 ("None"). Nous comparons les entres de cellules correspondantes dans les deux lignes pour chaque cl dans lordre. Ds que nous dcouvrons une diffrence, nous retournons une valeur true ou false. Sil savre que toutes les comparaisons sont gales, nous retournons false. La fonction qStableSort() sappuie sur lordre avant le tri pour rsoudre les situations dgalit ; si row1 prcdait row2 lorigine et nest jamais "infrieur " lautre, row1 prcdera toujours row2 dans le rsultat. Cest ce qui distingue qStableSort() de son cousin qSort() dont le rsultat est moins prvisible. Nous avons dsormais termin la classe Spreadsheet. Dans la prochaine section, nous allons analyser la classe Cell. Cette classe est employe pour contenir les formules des cellules et propose une rimplmentation de la fonction QTableWidgetItem::data() que Spreadsheet appelle indirectement par le biais de la fonction QTableWidgetItem::text(). Lobjectif est dafcher le rsultat du calcul de la formule dune cellule.

Drivation de QTableWidgetItem
La classe Cell hrite de QTableWidgetItem. Cette classe est conue pour bien fonctionner avec Spreadsheet, mais elle ne dpend pas spciquement de cette classe et pourrait, en thorie, tre utilise dans nimporte quel QTableWidget. Voici le chier den-tte :
#ifndef CELL_H #define CELL_H #include <QTableWidgetItem> class Cell: public QTableWidgetItem { public: Cell(); QTableWidgetItem *clone() const; void setData(int role, const QVariant &value); QVariant data(int role) const; void setFormula(const QString &formula); QString formula() const; void setDirty(); private: QVariant value() const; QVariant evalExpression(const QString &str, int &pos) const; QVariant evalTerm(const QString &str, int &pos) const;

Chapitre 4

Implmenter la fonctionnalit dapplication

97

QVariant evalFactor(const QString &str, int &pos) const; mutable QVariant cachedValue; mutable bool cacheIsDirty; }; #endif

La classe Cell dveloppe QTableWidgetItem en ajoutant deux variables prives :


cachedValue met en cache la valeur de la cellule sous forme de QVariant. cacheIsDirty est true si la valeur mise en cache nest pas jour.

Nous utilisons QVariant parce que certaines cellules ont une valeur double alors que dautres ont une valeur QString. Les variables cachedValue et cacheIsDirty sont dclares avec le mot-cl C++ mutable. Nous avons ainsi la possibilit de modier ces variables dans des fonctions const. Nous pourrions aussi recalculer la valeur chaque fois que text() est appele, mais ce serait tout fait inefcace. Notez quil ny a pas de macro Q_OBJECT dans la dnition de classe. Cell est une classe C++ ordinaire, sans signaux ni slots. En fait, vu que QTableWidgetItem nhrite pas de QObject, nous ne pouvons pas avoir de signaux et de slots dans Cell. Les classes dlments de Qt nhritent pas de QObject pour optimiser les performances. Si des signaux et des slots se rvlent ncessaires, ils peuvent tre implments dans le widget qui contient les lments ou, exceptionnellement, en utilisant lhritage multiple avec QObject. Voici le dbut de cell.cpp:
#include <QtGui> #include "cell.h" Cell::Cell() { setDirty(); }

Dans le constructeur, nous devons simplement dnir le cache comme tant actualiser (dirty). Vous navez pas besoin de transmettre un parent ; quand la cellule est insre dans un QTableWidget avec setItem(), le QTableWidget prend automatiquement possession de celle-ci. Chaque QTableWidgetItem peut contenir des donnes, jusqu un QVariant pour chaque "rle" de donnes. Les rles les plus couramment utiliss sont Qt::EditRole et Qt::DisplayRole. Le rle de modication est employ pour des donnes qui doivent tre modies et le rle dafchage pour des donnes qui doivent tre afches. Il arrive souvent que ces donnes soient les mmes, mais dans Cell le rle de modication correspond la

98

Qt4 et C++ : Programmation dinterfaces GUI

formule de la cellule et le rle dafchage la valeur de la cellule (le rsultat de lvaluation de la formule).
QTableWidgetItem *Cell::clone() const { return new Cell(*this); }

La fonction clone() est invoque par QTableWidget quand il doit crer une nouvelle cellule par exemple quand lutilisateur commence taper dans une cellule vide qui na encore jamais t utilise. Linstance transmise QTableWidget::setItemPrototype() est llment qui est clon. Vu que la copie au niveau du membre est sufsante pour Cell, nous nous basons sur le constructeur de copie par dfaut cr automatiquement par C++ dans le but de crer de nouvelles instances Cell dans la fonction clone().
void Cell::setFormula(const QString &formula) { setData(Qt::EditRole, formula); }

La fonction setFormula() dnit la formule de la cellule. Cest simplement une fonction pratique permettant dappeler setData() avec le rle de modication. Elle est invoque dans Spreadsheet::setFormula().
QString Cell::formula() const { return data(Qt::EditRole).toString(); }

La fonction formula() est appele dans Spreadsheet::formula(). Comme setFormula(), cest une fonction commode, mais cette fois-ci qui rcupre les donnes EditRole de llment.
void Cell::setData(int role, const QVariant &value) { QTableWidgetItem::setData(role, value); if (role == Qt::EditRole) setDirty(); }

Si nous avons affaire une nouvelle formule, nous dnissons cacheIsDirty en true pour garantir que la cellule sera recalcule la prochaine fois que text() est appel. Aucune fonction text() nest dnie dans Cell, mme si nous appelons text() sur des instances Cell dans Spreadsheet::text(). La fonction text() est une fonction de convenance propose par QTableWidgetItem; cela revient au mme que dappeler data(Qt::DisplayRole).toString().
void Cell::setDirty() { cacheIsDirty = true; }

Chapitre 4

Implmenter la fonctionnalit dapplication

99

La fonction setDirty() est invoque pour forcer le recalcul de la valeur dune cellule. Elle dnit simplement cacheIsDirty en true, ce qui signie que cachedValue nest plus jour. Le recalcul nest effectu que lorsquil est ncessaire.
QVariant Cell::data(int role) const { if (role == Qt::DisplayRole) { if (value().isValid()) { return value().toString(); } else { return "####"; } } else if (role == Qt::TextAlignmentRole) { if (value().type() == QVariant::String) { return int(Qt::AlignLeft | Qt::AlignVCenter); } else { return int(Qt::AlignRight | Qt::AlignVCenter); } } else { return QTableWidgetItem::data(role); } }

La fonction data() est rimplmente dans QTableWidgetItem. Elle retourne le texte qui doit tre afch dans la feuille de calcul si elle est appele avec Qt::DisplayRole, et la formule si elle est invoque avec Qt::EditRole. Elle renvoie lalignement appropri si elle est appele avec Qt::TextAlignmentRole. Dans le cas de DisplayRole, elle se base sur value() pour calculer la valeur de la cellule. Si la valeur nest pas valide (parce que la formule est mauvaise), nous retournons "####". La fonction Cell::value() utilise dans data() retourne un QVariant. Un QVariant peut stocker des valeurs de diffrents types, comme double et QString, et propose des fonctions pour convertir les variants dans dautres types. Par exemple, appeler toString() sur un variant qui contient une valeur double produit une chane de double. Un QVariant construit avec le constructeur par dfaut est un variant "invalide".
const QVariant Invalid; QVariant Cell::value() const { if (cacheIsDirty) { cacheIsDirty = false; QString formulaStr = formula(); if (formulaStr.startsWith(\)) { cachedValue = formulaStr.mid(1); } else if (formulaStr.startsWith(=)) { cachedValue = Invalid; QString expr = formulaStr.mid(1); expr.replace(" ", ""); expr.append(QChar::Null);

100

Qt4 et C++ : Programmation dinterfaces GUI

int pos = 0; cachedValue = evalExpression(expr, pos); if (expr[pos]!= QChar::Null) cachedValue = Invalid; } else { bool ok; double d = formulaStr.toDouble(&ok); if (ok) { cachedValue = d; } else { cachedValue = formulaStr; } } } return cachedValue; }

La fonction prive value() retourne la valeur de la cellule. Si cacheIsDirty est true, nous devons la recalculer. Si la formule commence par une apostrophe (par exemple " 12345"), lapostrophe se trouve la position 0 et la valeur est la chane allant de la position 1 la n. Si la formule commence par un signe gal (=), nous prenons la chane partir de la position 1 et nous supprimons tout espace quelle contient. Nous appelons ensuite evalExpression() pour calculer la valeur de lexpression. Largument pos est transmis par rfrence ; il indique la position du caractre o lanalyse doit commencer. Aprs lappel de evalExpression(), le caractre la position pos doit tre le caractre QChar::Null que nous avons ajout, sil a t correctement analys. Si lanalyse a chou avant la n, nous dnissons cachedValue de sorte quil soit Invalid. Si la formule ne commence pas par une apostrophe ni par un signe gal, nous essayons de la convertir en une valeur virgule ottante laide de toDouble(). Si la conversion fonctionne, nous congurons cachedValue pour y stocker le nombre obtenu ; sinon, nous dnissons cachedValue avec la chane de la formule. Par exemple, avec une formule de "1,50," toDouble() dnit ok en true et retourne 1,5, alors quavec une formule de "World Population" toDouble() dnit ok en false et renvoie 0,0. En transmettant toDouble() un pointeur de type bool, nous sommes en mesure de faire une distinction entre la conversion dune chane qui donne la valeur numrique 0,0 et une erreur de conversion (o 0,0 est aussi retourn mais bool est dni en false). Il est cependant parfois ncessaire de retourner une valeur zro sur un chec de conversion, auquel cas nous navons pas besoin de transmettre de pointeur de bool. Pour des questions de performances et de portabilit, Qt nutilise jamais dexceptions C++ pour rapporter des checs. Ceci ne vous empche pas de les utiliser dans des programmes Qt, condition que votre compilateur les prenne en charge. La fonction value() est dclare const. Nous devions dclarer cachedValue et cacheIsValid comme des variables mutable, de sorte que le compilateur nous permette de les modier dans des fonctions const. Ce pourrait tre tentant de rendre value() non-const et de supprimer

Chapitre 4

Implmenter la fonctionnalit dapplication

101

les mots cls mutable, mais le rsultat ne compilerait pas parce que nous appelons value() depuis data(), une fonction const. Nous avons dsormais ni lapplication Spreadsheet, except lanalyse des formules. Le reste de cette section est ddie evalExpression() et les deux fonctions evalTerm() et evalFactor(). Le code est un peu compliqu, mais il est introduit ici pour complter lapplication. Etant donn que le code nest pas li la programmation dinterfaces graphiques utilisateurs, vous pouvez tranquillement lignorer et continuer lire le Chapitre 5. La fonction evalExpression() retourne la valeur dune expression dans la feuille de calcul. Une expression est dnie sous la forme dun ou plusieurs termes spars par les oprateurs "+" ou "".Les termes eux-mmes sont dnis comme un ou plusieurs facteurs spars par les oprateurs "*" ou "/".En divisant les expressions en termes et les termes en facteurs, nous sommes srs que les oprateurs sont appliqus dans le bon ordre. Par exemple, "2*C5+D6" est une expression avec "2*C5" comme premier terme et "D6" comme second terme. Le terme "2*C5" a "2" comme premier facteur et "C5" comme deuxime facteur, et le terme "D6" est constitu dun seul facteur "D6".Un facteur peut tre un nombre ("2"), un emplacement de cellule ("C5") ou une expression entre parenthses, prcde facultativement dun signe moins unaire.
Expression Terme
+

Terme Facteur
/

Facteur Nombre

Emplacement de cellule
(

Expression

Figure 4.10 Diagramme de la syntaxe des expressions de la feuille de calcul

La syntaxe des expressions de la feuille de calcul est prsente dans la Figure 4.10. Pour chaque symbole de la grammaire (Expression, Terme et Facteur), il existe une fonction membre correspondante qui lanalyse et dont la structure respecte scrupuleusement cette grammaire. Les analyseurs crits de la sorte sont appels des analyseurs vers le bas rcursifs. Commenons par evalExpression(), la fonction qui analyse une Expression :
QVariant Cell::evalExpression(const QString &str, int &pos) const { QVariant result = evalTerm(str, pos); while (str[pos]!= QChar::Null) { QChar op = str[pos]; if (op!= + && op!= -) return result; ++pos;

102

Qt4 et C++ : Programmation dinterfaces GUI

QVariant term = evalTerm(str, pos); if (result.type() == QVariant::Double && term.type() == QVariant::Double) { if (op == +) { result = result.toDouble() + term.toDouble(); } else { result = result.toDouble() - term.toDouble(); } } else { result = Invalid; } } return result; }

Nous appelons tout dabord evalTerm() pour obtenir la valeur du premier terme. Si le caractre suivant est "+" ou "", nous appelons evalTerm() une deuxime fois ; sinon, lexpression est constitue dun seul terme et nous retournons sa valeur en tant que valeur de toute lexpression. Une fois que nous avons les valeurs des deux premiers termes, nous calculons le rsultat de lopration en fonction de loprateur. Si les deux termes ont t valus en double, nous calculons le rsultat comme tant double; sinon nous dnissons le rsultat comme tant Invalid. Nous continuons de cette manire jusqu ce quil ny ait plus de termes. Ceci fonctionne correctement parce que les additions et les soustractions sont de type associatif gauche ; cest-dire que "123" signie "(12)3" et non "1(23)."
QVariant Cell::evalTerm(const QString &str, int &pos) const { QVariant result = evalFactor(str, pos); while (str[pos]!= QChar::Null) { QChar op = str[pos]; if (op!= * && op!= /) return result; ++pos; QVariant factor = evalFactor(str, pos); if (result.type() == QVariant::Double && factor.type() == QVariant::Double) { if (op == *) { result = result.toDouble() * factor.toDouble(); } else { if (factor.toDouble() == 0.0) { result = Invalid; } else { result = result.toDouble() / factor.toDouble(); } } } else {

Chapitre 4

Implmenter la fonctionnalit dapplication

103

result = Invalid; } } return result; }

La fonction evalTerm() ressemble beaucoup evalExpression(), sauf quelle traite des multiplications et des divisions. La seule subtilit dans evalTerm(), cest que vous devez viter de diviser par zro, parce que cela constitue une erreur dans certains processeurs. Bien quil ne soit pas recommand de tester lgalit de valeurs de type virgule ottante en raison des problmes darrondis, vous pouvez tester sans problmes lgalit par rapport 0,0 pour viter toute division par zro.
QVariant Cell::evalFactor(const QString &str, int &pos) const { QVariant result; bool negative = false; if (str[pos] == -) { negative = true; ++pos; } if (str[pos] == () { ++pos; result = evalExpression(str, pos); if (str[pos]!= )) result = Invalid; ++pos; } else { QRegExp regExp("[A-Za-z][1-9][0-9]{0,2}"); QString token; while (str[pos].isLetterOrNumber() || str[pos] == .) { token += str[pos]; ++pos; } if (regExp.exactMatch(token)) { int column = token[0].toUpper().unicode() - A; int row = token.mid(1).toInt() - 1; Cell *c = static_cast<Cell *>( tableWidget()->item(row, column)); if (c) { result = c->value(); } else { result = 0.0; } } else { bool ok; result = token.toDouble(&ok);

104

Qt4 et C++ : Programmation dinterfaces GUI

if (!ok) result = Invalid; } } if (negative) { if (result.type() == QVariant::Double) { result = -result.toDouble(); } else { result = Invalid; } } return result; }

La fonction evalFactor() est un peu plus complique que evalExpression() et evalTerm(). Nous regardons dabord si le facteur est prcd du signe ngatif. Nous examinons ensuite sil commence par une parenthse ouverte. Si cest le cas, nous valuons le contenu des parenthses comme une expression en appelant evalExpression(). Lorsque nous valuons une expression entre parenthses, evalExpression() appelle evalTerm(), qui invoque evalFactor(), qui appelle nouveau evalExpression(). Cest l quintervient la rcursivit dans lanalyseur. Si le facteur nest pas une expression imbrique, nous extrayons le prochain jeton, qui devrait tre un emplacement de cellule ou un nombre. Si le jeton correspond QRegExp, nous le considrons comme une rfrence de cellule et nous appelons value() sur la cellule lemplacement donn. La cellule pourrait se trouver nimporte o dans la feuille de calcul et pourrait tre dpendante dautres cellules. Les dpendances ne sont pas un problme ; elles dclencheront simplement plus dappels de value() et (pour les cellules " recalculer") plus danalyse jusqu ce que les valeurs des cellules dpendantes soient calcules. Si le jeton nest pas un emplacement de cellule, nous le considrons comme un nombre. Que se passe-t-il si la cellule A1 contient la formule "=A1" ? Ou si la cellule A1 contient "=A2" et la cellule A2 comporte "=A1" ? Mme si nous navons pas crit de code spcial pour dtecter des dpendances circulaires, lanalyseur gre ces cas en retournant un QVariant invalide. Ceci fonctionne parce que nous dnissons cacheIsDirty en false et cachedValue en Invalid dans value() avant dappeler evalExpression(). Si evalExpression() appelle de manire rcursive value() sur la mme cellule, il renvoie immdiatement Invalid et toute lexpression est donc value en Invalid. Nous avons dsormais termin lanalyseur de formules. Il nest pas compliqu de ltendre pour quil gre des fonctions prdnies de la feuille de calcul, comme sum() et avg(), en dveloppant la dnition grammaticale de Facteur. Une autre extension facile consiste implmenter loprateur "+" avec des oprandes de chane (comme une concatnation) ; aucun changement de grammaire nest exig.

5
Crer des widgets personnaliss
Au sommaire de ce chapitre Personnaliser des widgets Qt Driver QWidget Intgrer des widgets personnaliss avec le Qt Designer Double mise en mmoire tampon

Ce chapitre vous explique comment concevoir des widgets personnaliss laide de Qt. Les widgets personnaliss peuvent tre crs en drivant un widget Qt existant ou en drivant directement QWidget. Nous vous prsenterons les deux approches et nous verrons galement comment introduire un widget personnalis avec le Qt Designer de sorte quil puisse tre utilis comme nimporte quel widget Qt intgr. Nous terminerons ce chapitre en vous parlant dun widget personnalis qui emploie la double mise en mmoire tampon, une technique puissante pour actualiser trs rapidement lafchage.

106

Qt4 et C++ : Programmation dinterfaces GUI

Personnaliser des widgets Qt


Il arrive quil ne soit pas possible dobtenir la personnalisation requise pour un widget Qt simplement en congurant ses proprits dans le Qt Designer ou en appelant ses fonctions. Une solution simple et directe consiste driver la classe de widget approprie et ladapter pour satisfaire vos besoins.
Figure 5.1 Le widget HexSpinBox

Dans cette section, nous dvelopperons un pointeur toupie hexadcimal pour vous prsenter son fonctionnement (voir Figure 5.1). QSpinBox ne prend en charge que les nombres dcimaux, mais grce la drivation, il est plutt facile de lui faire accepter et afcher des valeurs hexadcimales.
#ifndef HEXSPINBOX_H #define HEXSPINBOX_H #include <QSpinBox> class QRegExpValidator; class HexSpinBox: public QSpinBox { Q_OBJECT public: HexSpinBox(QWidget *parent = 0); protected: QValidator::State validate(QString &text, int &pos) const; int valueFromText(const QString &text) const; QString textFromValue(int value) const; private: QRegExpValidator *validator; }; #endif

HexSpinBox hrite la majorit de ses fonctionnalits de QSpinBox. Il propose un constructeur typique et rimplmente trois fonctions virtuelles de QSpinBox.
#include <QtGui> #include "hexspinbox.h" HexSpinBox::HexSpinBox(QWidget *parent)

Chapitre 5

Crer des widgets personnaliss

107

: QSpinBox(parent) { setRange(0, 255); validator = new QRegExpValidator(QRegExp("[0-9A-Fa-f]{1,8}"), this); }

Nous dnissons la plage par dfaut avec les valeurs 0 255 (0x00 0xFF), qui est plus approprie pour un pointeur toupie hexadcimal que les valeurs par dfaut de QSpinBox allant de 0 99. Lutilisateur peut modier la valeur en cours dun pointeur toupie, soit en cliquant sur ses flches vers le haut et le bas, soit en saisissant une valeur dans son diteur de lignes. Dans le second cas, nous souhaitons restreindre lentre de lutilisateur aux nombres hexadcimaux valides. Pour ce faire, nous employons QRegExpValidator qui accepte entre un et huit caractres, chacun deux devant appartenir lun des ensembles suivants, "0" "9," "A" "F" et "a" "f".
QValidator::State HexSpinBox::validate(QString &text, int &pos) const { return validator->validate(text, pos); }

Cette fonction est appele par QSpinBox pour vrier que le texte saisi jusqu prsent est valide. Il y a trois possibilits : Invalid (le texte ne correspond pas lexpression rgulire), Intermediate (le texte est une partie plausible dune valeur valide) et Acceptable (le texte est valide). QRegExpValidator possde une fonction validate() approprie, nous retournons donc simplement le rsultat de son appel. En thorie, nous devrions renvoyer Invalid ou Intermediate pour les valeurs qui se situent en dehors de la plage du pointeur toupie, mais QSpinBox est assez intelligent pour dtecter cette condition sans aucune aide.
QString HexSpinBox::textFromValue(int value) const { return QString::number(value, 16).toUpper(); }

La fonction textFromValue() convertit une valeur entire en chane. QSpinBox lappelle pour mettre jour la partie "diteur" du pointeur toupie quand lutilisateur appuie sur les ches haut et bas du pointeur. Nous utilisons la fonction statique QString::number() avec un second argument de 16 pour convertir la valeur en hexadcimal minuscule et nous appelons QString::toUpper() sur le rsultat pour le passer en majuscule.
int HexSpinBox::valueFromText(const QString &text) const { bool ok; return text.toInt(&ok, 16); }

La fonction valueFromText() effectue une conversion inverse, dune chane en une valeur entire. Elle est appele par QSpinBox quand lutilisateur saisit une valeur dans la zone de lditeur du pointeur toupie et appuie sur Entre. Nous excutons la fonction QString::toInt()

108

Qt4 et C++ : Programmation dinterfaces GUI

pour essayer de convertir le texte en cours en une valeur entire, toujours en base 16. Si la chane nest pas au format hexadcimal, ok est dni en false et toInt() retourne 0. Ici, nous ne sommes pas obligs denvisager cette possibilit, parce que le validateur naccepte que la saisie de chanes hexadcimales valides. Au lieu de transmettre ladresse dune variable sans intrt (ok), nous pourrions transmettre un pointeur nul comme premier argument de toInt(). Nous avons termin le pointeur toupie hexadcimal. La personnalisation dautres widgets Qt suit le mme processus : choisir un widget Qt adapt, le driver et rimplmenter certaines fonctions virtuelles pour modier son comportement.

Driver QWidget
De nombreux widgets personnaliss sont simplement obtenus partir dune combinaison de widgets existants, que ce soit des widgets Qt intgrs ou dautres widgets personnaliss comme HexSpinBox. Les widgets personnaliss ainsi conus peuvent gnralement tre dvelopps dans le Qt Designer :

crez un nouveau formulaire laide du modle "Widget" ; ajoutez les widgets ncessaires au formulaire, puis disposez-les ; tablissez les connexions entre les signaux et les slots. Si vous avez besoin dun comportement pour lequel de simples signaux et slots sont insufsants, crivez le code ncessaire dans une classe qui hrite de QWidget et de celle gnre par uic.

Il est vident que combiner des widgets existants peut se faire entirement dans du code. Quelle que soit lapproche choisie, la classe en rsultant hrite directement de QWidget. Si le widget ne possde aucun signal ni slot et quil ne rimplmente pas de fonction virtuelle, il est mme possible de concevoir le widget simplement en combinant des widgets existants sans sous-classe. Cest la technique que nous avons employe dans le Chapitre 1 pour crer lapplication Age, avec QWidget, QSpinBox et QSlider. Pourtant nous aurions pu tout aussi facilement driver QWidget et crer QSpinBox et QSlider dans le constructeur de la sous-classe. Lorsquaucun des widgets Qt ne convient une tche particulire et lorsquil nexiste aucun moyen de combiner ou dadapter des widgets existants pour obtenir le rsultat souhait, nous pouvons toujours crer le widget que nous dsirons. Pour ce faire, nous devons driver QWidget et rimplmenter quelques gestionnaires dvnements pour dessiner le widget et rpondre aux clics de souris. Cette approche nous autorise une libert totale quant la dnition et au contrle de lapparence et du comportement de notre widget. Les widgets intgrs de Qt, comme QLabel, QPushButton et QTableWidget, sont implments de cette manire. Sils nexistaient pas dans Qt, il serait encore possible de les crer nous-mmes en utilisant les fonctions publiques fournies par QWidget de faon totalement indpendante de la plate-forme.

Chapitre 5

Crer des widgets personnaliss

109

Pour vous montrer comment crire un widget personnalis en se basant sur cette technique, nous allons crer le widget IconEditor illustr en Figure 5.2. IconEditor est un widget qui pourrait tre utilis dans un programme dditeur dicnes.
Figure 5.2 Le widget IconEditor

Commenons par analyser le chier den-tte.


#ifndef ICONEDITOR_H #define ICONEDITOR_H #include <QColor> #include <QImage> #include <QWidget> class IconEditor: public QWidget { Q_OBJECT Q_PROPERTY(QColor penColor READ penColor WRITE setPenColor) Q_PROPERTY(QImage iconImage READ iconImage WRITE setIconImage) Q_PROPERTY(int zoomFactor READ zoomFactor WRITE setZoomFactor) public: IconEditor(QWidget *parent = 0); void setPenColor(const QColor &newColor); QColor penColor() const { return curColor; } void setZoomFactor(int newZoom); int zoomFactor() const { return zoom; } void setIconImage(const QImage &newImage); QImage iconImage() const { return image; } QSize sizeHint() const;

La classe IconEditor utilise la macro Q_PROPERTY() pour dclarer trois proprits personnalises : penColor, iconImage et zoomFactor. Chaque proprit a un type de donnes, une fonction de "lecture" et une fonction facultative "dcriture".Par exemple, la proprit penColor est de type QColor et peut tre lue et crite grce aux fonctions penColor() et setPenColor().

110

Qt4 et C++ : Programmation dinterfaces GUI

Quand nous utilisons le widget dans le Qt Designer, les proprits personnalises apparaissent dans lditeur de proprits du Qt Designer sous les proprits hrites de QWidget. Ces proprits peuvent tre de nimporte quel type pris en charge par QVariant. La macro Q_OBJECT est ncessaire pour les classes qui dnissent des proprits.
protected: void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void paintEvent(QPaintEvent *event); private: void setImagePixel(const QPoint &pos, bool opaque); QRect pixelRect(int i, int j) const; QColor curColor; QImage image; int zoom; }; #endif

IconEditor rimplmente trois fonctions protges de QWidget et possde quelques fonctions et variables prives. Les trois variables prives contiennent les valeurs des trois proprits. Le chier dimplmentation commence par le constructeur de IconEditor:
#include <QtGui> #include "iconeditor.h" IconEditor::IconEditor(QWidget *parent) : QWidget(parent) { setAttribute(Qt::WA_StaticContents); setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); curColor = Qt::black; zoom = 8; image = QImage(16, 16, QImage::Format_ARGB32); image.fill(qRgba(0, 0, 0, 0)); }

Le constructeur prsente certains aspects subtils, tels que lattribut Qt::WA_StaticContents et lappel de setSizePolicy(). Nous y reviendrons dans un instant. La couleur du crayon est dnie en noir. Le facteur de zoom est de 8, ce qui signie que chaque pixel de licne sera afch sous forme dun carr de 8 8. Les donnes de licne sont stockes dans la variable membre image et sont disponibles par le biais des fonctions setIconImage() et iconImage(). Un programme dditeur dicnes appellerait normalement setIconImage() quand lutilisateur ouvre un chier dicne et

Chapitre 5

Crer des widgets personnaliss

111

iconImage() pour rcuprer licne quand lutilisateur veut la sauvegarder. La variable image est de type QImage. Nous linitialisons 16 16 pixels et au format ARGB 32 bits, un format qui prend en charge la semi-transparence. Nous effaons les donnes de limage en la remplissant avec une couleur transparente. La classe QImage stocke une image indpendamment du matriel. Elle peut tre dnie avec une qualit de 1, 8 ou 32 bits. Une image avec une qualit de 32 bits utilise 8 bits pour chaque composante rouge, vert et bleu dun pixel. Les 8 bits restants stockent le canal alpha du pixel (opacit). Par exemple, les composantes rouge, vert, bleu et alpha dune couleur rouge pure prsentent les valeurs 255, 0, 0 et 255. Dans Qt, cette couleur peut tre spcie comme telle :
QRgb red = qRgba(255, 0, 0, 255);

ou, tant donn que la couleur est opaque, comme


QRgb red = qRgb(255, 0, 0);

QRgb est simplement le typedef dun type unsigned int, et qRgb() et qRgba() sont des fonctions en ligne qui combinent leurs arguments en une valeur entire 32 bits. Il est aussi possible dcrire
QRgb red = 0xFFFF0000;

o le premier FF correspond au canal alpha et le second FF la composante rouge. Dans le constructeur de IconEditor, nous remplissons QImage avec une couleur transparente en utilisant 0 comme canal alpha. Qt propose deux types permettant de stocker les couleurs : QRgb et QColor. Alors que QRgb est un simple typedef employ dans QImage pour stocker les donnes 32 bits du pixel, QColor est une classe dote de nombreuses fonctions pratiques qui est souvent utilise dans Qt pour stocker des couleurs. Dans le widget IconEditor, nous employons uniquement QRgb lorsque nous travaillons avec QImage; nous utilisons QColor pour tout le reste, notamment la proprit penColor.
QSize IconEditor::sizeHint() const { QSize size = zoom * image.size(); if (zoom >= 3) size += QSize(1, 1); return size; }

La fonction sizeHint() est rimplmente dans QWidget et retourne la taille idale dun widget. Dans ce cas, nous recevons la taille de limage multiplie par le facteur de zoom, avec un pixel supplmentaire dans chaque direction pour sadapter une grille si le facteur de zoom est de 3 ou plus. (Nous nafchons pas de grille si le facteur de zoom est de 2 ou 1, parce quelle ne laisserait presque pas de place pour les pixels de licne.)

112

Qt4 et C++ : Programmation dinterfaces GUI

La taille requise dun widget est utile dans la plupart des cas lorsquelle est associe aux dispositions. Les gestionnaires de disposition de Qt essaient au maximum de respecter cette taille quand ils disposent les widgets enfants dun formulaire. Pour que IconEditor se comporte correctement, il doit signaler une taille requise crdible. En plus de cette taille requise, la taille des widgets suit une stratgie qui indique au systme de disposition sils peuvent tre tirs ou rtrcis. En appelant setSizePolicy() dans le constructeur avec les stratgies horizontale et verticale QSizePolicy::Minimum, tout gestionnaire de disposition responsable de ce widget sait que la taille requise de ce dernier correspond vraiment sa taille minimale. En dautres termes, le widget peut tre tir si ncessaire, mais ne doit jamais tre rtrci une taille infrieure la taille requise. Vous pouvez annuler ce comportement dans le Qt Designer en congurant la proprit sizePolicy du widget. La signication des diverses stratgies lies la taille est explique au Chapitre 6.
void IconEditor::setPenColor(const QColor &newColor) { curColor = newColor; }

La fonction setPenColor() dnit la couleur du crayon. La couleur sera utilise pour les pixels que vous dessinerez.
void IconEditor::setIconImage(const QImage &newImage) { if (newImage!= image) { image = newImage.convertToFormat(QImage::Format_ARGB32); update(); updateGeometry(); } }

La fonction setIconImage() dtermine limage modier. Nous invoquons convertToFormat() pour obtenir une image 32 bits avec une mmoire tampon alpha si elle nest pas dans ce format. Ailleurs dans le code, nous supposerons que les donnes de limage sont stockes sous forme de valeurs ARGB 32 bits. Aprs avoir congur la variable image, nous appelons QWidget::update() pour forcer le rafrachissement de lafchage du widget avec la nouvelle image. Nous invoquons ensuite QWidget::updateGeometry() pour informer toute disposition qui contient le widget que la taille requise du widget a chang. La disposition sadaptera automatiquement cette nouvelle taille.
void IconEditor::setZoomFactor(int newZoom) { if (newZoom < 1) newZoom = 1; if (newZoom!= zoom) { zoom = newZoom;

Chapitre 5

Crer des widgets personnaliss

113

update(); updateGeometry(); } }

La fonction setZoomFactor() dnit le facteur de zoom de limage. Pour viter une division par zro, nous corrigeons toute valeur infrieure 1. A nouveau, nous appelons update() et updateGeometry() pour actualiser lafchage du widget et pour informer tout gestionnaire de disposition de la modication de la taille requise. Les fonctions penColor(), iconImage() et zoomFactor() sont implmentes en tant que fonctions en ligne dans le chier den-tte. Nous allons maintenant passer en revue le code de la fonction paintEvent(). Cette fonction est la fonction la plus importante de IconEditor. Elle est invoque ds que le widget a besoin dtre redessin. Limplmentation par dfaut dans QWidget na aucune consquence, le widget reste vide. Tout comme closeEvent(), que nous avons rencontr dans le Chapitre 3, paintEvent() est un gestionnaire dvnements. Qt propose de nombreux autres gestionnaires dvnements, chacun deux correspondant un type diffrent dvnement. Le Chapitre 7 aborde en dtail le traitement des vnements. Il existe beaucoup de situations o un vnement paint est dclench et o paintEvent() est appel : Quand un widget est afch pour la premire fois, le systme gnre automatiquement un vnement paint pour obliger le widget se dessiner lui-mme. Quand un widget est redimensionn, le systme dclenche un vnement paint. Si le widget est masqu par une autre fentre, puis afch nouveau, un vnement paint est dclench pour la zone qui tait masque ( moins que le systme de fentrage ait stock la zone). Nous avons aussi la possibilit de forcer un vnement paint en appelant QWidget::update() ou QWidget::repaint(). La diffrence entre ces deux fonctions est que repaint() impose un rafrachissement immdiat de lafchage, alors que update() planie simplement un vnement paint pour le prochain traitement dvnements de Qt. (Ces deux fonctions ne font rien si le widget nest pas visible lcran.) Si update() est invoqu plusieurs fois, Qt compresse les vnements paint conscutifs en un seul vnement paint pour viter le phnomne du scintillement. Dans IconEditor, nous utilisons toujours update(). Voici le code :
void IconEditor::paintEvent(QPaintEvent *event) { QPainter painter(this); if (zoom >= 3) { painter.setPen(palette().foreground().color()); for (int i = 0; i <= image.width(); ++i)

114

Qt4 et C++ : Programmation dinterfaces GUI

painter.drawLine(zoom * i, 0, zoom * i, zoom * image.height()); for (int j = 0; j <= image.height(); ++j) painter.drawLine(0, zoom * j, zoom * image.width(), zoom * j); } for (int i = 0; i < image.width(); ++i) { for (int j = 0; j < image.height(); ++j) { QRect rect = pixelRect(i, j); if (!event->region().intersect(rect).isEmpty()) { QColor color = QColor::fromRgba(image.pixel(i, j)); painter.fillRect(rect, color); } } } }

Nous commenons par construire un objet QPainter sur le widget. Si le facteur de zoom est de 3 ou plus, nous dessinons des lignes horizontales et verticales qui forment une grille laide de la fonction QPainter::drawLine(). Un appel de QPainter::drawLine() prsente la syntaxe suivante :
painter.drawLine(x1, y1, x2, y2);

o (x1, y1) est la position dune extrmit de la ligne et (x2, y2) la position de lautre extrmit. Il existe galement une version surcharge de la fonction qui reoit deux QPoint au lieu de quatre int. Le pixel en haut gauche dun widget Qt se situe la position (0, 0), et le pixel en bas droite se trouve (width() 1, height() 1). Cela ressemble au systme traditionnel de coordonnes cartsiennes, mais lenvers. Nous avons la possibilit de modier le systme de coordonnes de QPainter grce aux transformations, comme la translation, la mise lchelle, la rotation et le glissement. Ces notions sont abordes au Chapitre 8 (Graphiques 2D et 3D).
Figure 5.3 Tracer une ligne avec QPainter
(0, 0) (x1, y1)

(x2, y2) (x (width() -1,height() -1)

Chapitre 5

Crer des widgets personnaliss

115

Avant dappeler drawLine() sur QPainter, nous dnissons la couleur de la ligne au moyen de setPen(). Nous pourrions coder une couleur, comme noir ou gris, mais il est plus judicieux dutiliser la palette du widget. Chaque widget est dot dune palette qui spcie quelles couleurs doivent tre utilises selon les situations. Par exemple, il existe une entre dans la palette pour la couleur darrire-plan des widgets (gnralement gris clair) et une autre pour la couleur du texte sur ce fond (habituellement noir). Par dfaut, la palette dun widget adopte le modle de couleur du systme de fentrage. En utilisant des couleurs de la palette, nous sommes srs que IconEditor respecte les prfrences de lutilisateur. La palette dun widget consiste en trois groupes de couleurs : active, inactive et disabled. Vous choisirez le groupe de couleurs en fonction de ltat courant du widget : Le groupe Active est employ pour des widgets situs dans la fentre actuellement active. Le groupe Inactive est utilis pour les widgets des autres fentres. Le groupe Disabled est utilis pour les widgets dsactivs dans nimporte quelle fentre. La fonction QWidget::palette() retourne la palette du widget sous forme dobjet QPalette. Les groupes de couleurs sont spcis comme des numrations de type QPalette::ColorGroup. Lorsque nous avons besoin dun pinceau ou dune couleur approprie pour dessiner, la bonne approche consiste utiliser la palette courante, obtenue partir de QWidget::palette(), et le rle requis, par exemple, QPalette::foreground(). Chaque fonction de rle retourne un pinceau, qui correspond normalement ce que nous souhaitons, mais si nous navons besoin que de la couleur, nous pouvons lextraire du pinceau, comme nous avons fait dans paintEvent(). Par dfaut, les pinceaux retourns sont adapts ltat du widget, nous ne sommes donc pas forcs de spcier un groupe de couleurs. La fonction paintEvent() termine en dessinant limage elle-mme. Lappel de IconEditor::pixelRect() retourne un QRect qui dnit la rgion redessiner. Pour une question doptimisation simple, nous ne redessinons pas les pixels qui se trouvent en dehors de cette rgion.
Figure 5.4 Dessiner un rectangle avec QPainter
(0, 0) (x, y) h w (width() -1,height() -1)

116

Qt4 et C++ : Programmation dinterfaces GUI

Nous invoquons QPainter::fillRect() pour dessiner un pixel sur lequel un zoom a t effectu. QPainter::fillRect() reoit un QRect et un QBrush. En transmettant QColor comme pinceau, nous obtenons un modle de remplissage correct.
QRect IconEditor::pixelRect(int i, int j) const { if (zoom >= 3) { return QRect(zoom * i + 1, zoom * j + 1, zoom - 1, zoom - 1); } else { return QRect(zoom * i, zoom * j, zoom, zoom); } }

La fonction pixelRect() retourne un QRect adapt QPainter::fillRect(). Les paramtres i et j sont les coordonnes du pixel dans QImage pas dans le widget. Si le facteur de zoom est de 1, les deux systmes de coordonnes concident parfaitement. Le constructeur de QRect suit la syntaxe QRect(x, y, width, height), o (x, y) est la position du coin suprieur gauche du rectangle et width_height correspond la taille du rectangle. Si le facteur de zoom est de 3 ou plus, nous rduisons la taille du rectangle dun pixel horizontalement et verticalement, de sorte que le remplissage ne dborde pas sur les lignes de la grille.
void IconEditor::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { setImagePixel(event->pos(), true); } else if (event->button() == Qt::RightButton) { setImagePixel(event->pos(), false); } }

Quand lutilisateur appuie sur un bouton de la souris, le systme dclenche un vnement "bouton souris enfonc". En rimplmentant QWidget::mousePressEvent(), nous avons la possibilit de rpondre cet vnement et de dnir ou effacer le pixel de limage sous le pointeur de la souris. Si lutilisateur a appuy sur le bouton gauche de la souris, nous appelons la fonction prive setImagePixel() avec true comme second argument, lui demandant de dnir le pixel dans la couleur actuelle du crayon. Si lutilisateur a appuy sur le bouton droit de la souris, nous invoquons aussi setImagePixel(), mais nous transmettons false pour effacer le pixel.
void IconEditor::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { setImagePixel(event->pos(), true); } else if (event->buttons() & Qt::RightButton) { setImagePixel(event->pos(), false); } }

Chapitre 5

Crer des widgets personnaliss

117

mouseMoveEvent() gre les vnements "dplacement de souris".Par dfaut, ces vnements ne sont dclenchs que lorsque lutilisateur enfonce un bouton. Il est possible de changer ce comportement en appelant QWidget::setMouseTracking(), mais nous navons pas besoin dagir de la sorte dans cet exemple.
Tout comme le fait dappuyer sur les boutons droit ou gauche de la souris congure ou efface un pixel, garder ce bouton enfonc et se placer sur un pixel suft aussi dnir ou supprimer un pixel. Vu quil est possible de maintenir enfonc plus dun bouton la fois, la valeur retourne par QMouseEvent::buttons() est un oprateur OR bit bit des boutons de la souris. Nous testons si un certain bouton est enfonc laide de loprateur &, et si cest le cas, nous invoquons setImagePixel().
void IconEditor::setImagePixel(const QPoint &pos, bool opaque) { int i = pos.x() / zoom; int j = pos.y() / zoom; if (image.rect().contains(i, j)) { if (opaque) { image.setPixel(i, j, penColor().rgba()); } else { image.setPixel(i, j, qRgba(0, 0, 0, 0)); } update(pixelRect(i, j)); } }

La fonction setImagePixel() est appele depuis mousePressEvent() et mouseMoveEvent() pour dnir ou effacer un pixel. Le paramtre pos correspond la position de la souris dans le widget. La premire tape consiste convertir la position de la souris dans les coordonnes du widget vers les coordonnes de limage. Pour ce faire, les composants x() et y() de la position de la souris sont diviss par le facteur de zoom. Puis nous vrions si le point se trouve dans une plage correcte. Ce contrle seffectue facilement en utilisant QImage::rect() et QRect::contains(); vous vriez ainsi que i se situe entre 0 et image.width() 1 et que j est entre 0 et image.height() 1. Selon le paramtre opaque, nous dnissons ou nous effaons le pixel dans limage. Effacer un pixel consiste le rendre transparent. Nous devons convertir le crayon QColor en une valeur ARGB 32 bits pour lappel de QImage::setPixel(). Nous terminons en appelant update() avec un QRect de la zone qui doit tre redessine. Maintenant que nous avons analys les fonctions membres, nous allons retourner lattribut Qt::WA_StaticContents que nous avons utilis dans le constructeur. Cet attribut informe Qt que le contenu du widget ne change pas quand le widget est redimensionn et que le contenu reste ancr dans le coin suprieur gauche du widget. Qt se sert de ces informations pour viter

118

Qt4 et C++ : Programmation dinterfaces GUI

tout retraage inutile des zones qui sont dj afches au moment du redimensionnement du widget. Normalement, quand un widget est redimensionn, Qt dclenche un vnement paint pour toute la zone visible du widget (voir Figure 5.5). Mais si le widget est cr avec lattribut Qt::WA_StaticContents, la rgion de lvnement paint se limite aux pixels qui ntaient pas encore afchs auparavant. Ceci implique que si le widget est redimensionn dans une taille plus petite, aucun vnement paint ne sera dclench.
Figure 5.5 Redimensionner un widget Qt::WA_StaticContents

Le widget IconEditor est maintenant termin. Grce aux informations et aux exemples des chapitres prcdents, nous pourrions crire un code qui utilise IconEditor comme une vritable fentre, comme un widget central dans QMainWindow, comme un widget enfant dans une disposition ou comme un widget enfant dans QScrollArea. Dans la prochaine section, nous verrons comment lintgrer avec le Qt Designer.

Intgrer des widgets personnaliss avec le Qt Designer


Avant de pouvoir utiliser des widgets personnaliss dans le Qt Designer, celui-ci doit en avoir connaissance. Il existe deux techniques : la "promotion" et le plug-in. Lapproche de la promotion est la plus rapide et la plus simple. Elle consiste choisir un widget Qt intgr qui possde une API similaire celle que nous voulons pour notre widget personnalis, puis saisir quelques informations propos de ce widget personnalis dans une bote de dialogue du Qt Designer. Le widget peut ensuite tre exploit dans des formulaires dvelopps avec le Qt Designer, mme sil sera reprsent par le widget Qt intgr associ lors de ldition ou de la prvisualisation du formulaire. Voici comment insrer un widget HexSpinBox dans un formulaire avec cette approche : 1. Crez un QSpinBox en le faisant glisser depuis la bote des widgets du Qt Designer vers le formulaire. 2. Cliquez du bouton droit sur le pointeur toupie et slectionnez Promote to Custom Widget dans le menu contextuel. 3. Compltez la bote de dialogue qui souvre avec "HexSpinBox" comme nom de classe et "hexspinbox.h" comme chier den-tte (voir Figure 5.6).

Chapitre 5

Crer des widgets personnaliss

119

Voil ! Le code gnr par uic contiendra hexspinbox.h au lieu de <QSpinBox> et instanciera un HexSpinBox. Dans le Qt Designer, le widget HexSpinBox sera reprsent par un QSpinBox, ce qui nous permet de dnir toutes les proprits dun QSpinBox (par exemple, la plage et la valeur actuelle).
Figure 5.6 La bote de dialogue du widget personnalis dans le Qt Designer

Les inconvnients de lapproche de la promotion sont que les proprits spciques au widget personnalis ne sont pas accessibles dans le Qt Designer et que le widget nest pas afch en tant que tel. Ces deux problmes peuvent tre rsolus en utilisant lapproche du plug-in. Lapproche du plug-in ncessite la cration dune bibliothque de plug-in que le Qt Designer peut charger lexcution et utiliser pour crer des instances du widget. Le vritable widget est ensuite employ par le Qt Designer pendant la modication du formulaire et la prvisualisation, et grce au systme de mta-objets de Qt, le Qt Designer peut obtenir dynamiquement la liste de ses proprits. Pour voir comment cela fonctionne, nous intgrerons le widget IconEditor de la section prcdente comme plug-in. Nous devons dabord driver QDesignerCustomWidgetInterface et rimplmenter certaines fonctions virtuelles. Nous supposerons que le code source du plug-in se situe dans un rpertoire appel iconeditorplugin et que le code source de IconEditor se trouve dans un rpertoire parallle nomm iconeditor. Voici la dnition de classe :
#include <QDesignerCustomWidgetInterface> class IconEditorPlugin: public QObject, public QDesignerCustomWidgetInterface { Q_OBJECT Q_INTERFACES(QDesignerCustomWidgetInterface) public: IconEditorPlugin(QObject *parent = 0); QString name() const; QString includeFile() const; QString group() const; QIcon icon() const; QString toolTip() const;

120

Qt4 et C++ : Programmation dinterfaces GUI

QString whatsThis() const; bool isContainer() const; QWidget *createWidget(QWidget *parent); };

La sous-classe IconEditorPlugin est une classe spcialise qui encapsule le widget IconEditor. Elle hrite de QObject et de QDesignerCustomWidgetIterface et se sert de la macro Q_INTERFACES() pour signaler moc que la seconde classe de base est une interface de plug-in. Les fonctions sont utilises par le Qt Designer pour crer des instances de la classe et obtenir des informations son sujet.
IconEditorPlugin::IconEditorPlugin(QObject *parent) : QObject(parent) { }

Le constructeur est trs simple.


QString IconEditorPlugin::name() const { return "IconEditor"; }

La fonction name() retourne le nom du widget fourni par le plug-in.


QString IconEditorPlugin::includeFile() const { return "iconeditor.h"; }

La fonction includeFile() retourne le nom du chier den-tte pour le widget spci encapsul par le plug-in. Le chier den-tte se trouve dans le code gnr par loutil uic.
QString IconEditorPlugin::group() const { return tr("Image Manipulation Widgets"); }

La fonction group() retourne le nom du groupe de widgets auquel doit appartenir ce widget personnalis. Si le nom nest pas encore utilis, le Qt Designer crera un nouveau groupe pour le widget.
QIcon IconEditorPlugin::icon() const { return QIcon(":/images/iconeditor.png"); }

La fonction icon() renvoie licne utiliser pour reprsenter le widget personnalis dans la bote des widgets du Qt Designer. Dans notre cas, nous supposons que IconEditorPlugin possde un chier de ressources Qt associ avec une entre adapte pour limage de lditeur dicnes.

Chapitre 5

Crer des widgets personnaliss

121

QString IconEditorPlugin::toolTip() const { return tr("An icon editor widget"); }

La fonction toolTip() renvoie linfobulle afcher quand la souris se positionne sur le widget personnalis dans la bote des widgets du Qt Designer.
QString IconEditorPlugin::whatsThis() const { return tr("This widget is presented in Chapter 5 of <i>C++ GUI " "Programming with Qt 4</i> as an example of a custom Qt " "widget."); }

La fonction whatsThis() retourne le texte "Whats this ?" que le Qt Designer doit afcher.
bool IconEditorPlugin::isContainer() const { return false; }

La fonction isContainer() retourne true si le widget peut contenir dautres widgets ; sinon elle retourne false. Par exemple, QFrame est un widget qui peut comporter dautres widgets. En gnral, tout widget Qt peut renfermer dautres widgets, mais le Qt Designer ne lautorise pas quand isContainer() renvoie false.
QWidget *IconEditorPlugin::createWidget(QWidget *parent) { return new IconEditor(parent); }

La fonction create() est invoque par le Qt Designer pour crer une instance dune classe de widget avec le parent donn.
Q_EXPORT_PLUGIN2(iconeditorplugin, IconEditorPlugin)

A la n du chier source qui implmente la classe de plug-in, nous devons utiliser la macro Q_EXPORT_PLUGIN2() pour que le Qt Designer puisse avoir accs au plug-in. Le premier argument est le nom que nous souhaitons attribuer au plug-in ; le second argument est le nom de la classe qui limplmente. Voici le code dun chier .pro permettant de gnrer le plug-in :
TEMPLATE CONFIG HEADERS SOURCES RESOURCES = lib += designer plugin release = ../iconeditor/iconeditor.h \ iconeditorplugin.h = ../iconeditor/iconeditor.cpp \ iconeditorplugin.cpp = iconeditorplugin.qrc

122

Qt4 et C++ : Programmation dinterfaces GUI

DESTDIR

= $(QTDIR)/plugins/designer

Le chier .pro suppose que la variable denvironnement QTDIR contient le rpertoire o Qt est install. Quand vous tapez make ou nmake pour gnrer le plug-in, il sinstallera automatiquement dans le rpertoire plugins du Qt Designer. Une fois le plug-in gnr, le widget IconEditor peut tre utilis dans le Qt Designer de la mme manire que nimporte quel autre widget intgr de Qt. Si vous voulez intgrer plusieurs widgets personnaliss avec le Qt Designer, vous avez la possibilit soit de crer un plug-in pour chacun deux, soit de les combiner dans un seul plug-in en drivant QDesignerCustomWidgetCollectionInterface.

Double mise en mmoire tampon


La double mise en mmoire tampon est une technique de programmation GUI qui consiste afcher un widget dans un pixmap hors champ puis copier le pixmap lcran. Avec les versions antrieures de Qt, cette technique tait frquemment utilise pour liminer le phnomne du scintillement et pour offrir une interface utilisateur plus confortable. Dans Qt 4, QWidget gre ce phnomne automatiquement, nous sommes donc rarement obligs de nous soucier du scintillement des widgets. La double mise en mmoire tampon explicite reste tout de mme avantageuse si le rendu du widget est complexe et doit tre ralis de faon rptitive. Nous pouvons alors stocker un pixmap de faon permanente avec le widget, toujours prt pour le prochain vnement paint, et copier le pixmap dans le widget ds que nous dtectons cet vnement paint. Il se rvle particulirement utile si nous souhaitons effectuer de lgres modications, comme dessiner un rectangle de slection, sans avoir recalculer chaque fois le rendu complet du widget. Nous allons clore ce chapitre en tudiant le widget personnalis Plotter. Ce widget utilise la double mise en mmoire tampon et illustre galement certains aspects de la programmation Qt, notamment la gestion des vnements du clavier, la disposition manuelle et les systmes de coordonnes. Le widget Plotter afche une ou plusieurs courbes spcies sous forme de vecteurs de coordonnes. Lutilisateur peut tracer un rectangle de slection sur limage et Plotter zoomera sur la zone dlimite par ce trac (voir Figure 5.7). Lutilisateur dessine le rectangle en cliquant un endroit dans le graphique, en faisant glisser la souris vers une autre position en maintenant le bouton gauche enfonc puis en relchant le bouton de la souris. Lutilisateur peut zoomer de manire rpte en traant des rectangles de slection plusieurs fois, faire un zoom arrire grce au bouton Zoom Out, puis zoomer nouveau au moyen du bouton Zoom In. Les boutons Zoom In et Zoom Out apparaissent la premire fois quils deviennent accessibles, ils nencombrent donc pas lcran si lutilisateur ne fait pas de zoom sur le graphique.

Chapitre 5

Crer des widgets personnaliss

123

Figure 5.7 Zoomer sur le widget Plotter

Le widget Plotter peut enregistrer les donnes de nombreuses courbes. Il assure aussi la maintenance dune pile dobjets PlotSettings, chacun deux correspondant un niveau particulier de zoom. Analysons dsormais la classe, en commenant par plotter.h:
#ifndef PLOTTER_H #define PLOTTER_H #include #include #include #include <QMap> <QPixmap> <QVector> <QWidget>

class QToolButton; class PlotSettings; class Plotter: public QWidget { Q_OBJECT public: Plotter(QWidget *parent = 0); void setPlotSettings(const PlotSettings &settings); void setCurveData(int id, const QVector<QPointF> &data); void clearCurve(int id); QSize minimumSizeHint() const; QSize sizeHint() const; public slots: void zoomIn(); void zoomOut();

Nous commenons par inclure les chiers den-tte des classes Qt utilises dans len-tte du chier du traceur (plotter) puis nous dclarons les classes dsignes par des pointeurs ou des rfrences dans len-tte.

124

Qt4 et C++ : Programmation dinterfaces GUI

Dans la classe Plotter, nous fournissons trois fonctions publiques pour congurer le trac et deux slots publics pour faire des zooms avant et arrire. Nous rimplmentons aussi minimumSizeHint() et sizeHint() dans QWidget. Nous enregistrons les points dune courbe sous forme de QVector<QPointF>, o QPointF est la version virgule ottante de QPoint.
protected: void paintEvent(QPaintEvent *event); void resizeEvent(QResizeEvent *event); void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); void keyPressEvent(QKeyEvent *event); void wheelEvent(QWheelEvent *event);

Dans la section protge de la classe, nous dclarons tous les gestionnaires dvnements de QWidget que nous dsirons rimplmenter.
private: void updateRubberBandRegion(); void refreshPixmap(); void drawGrid(QPainter *painter); void drawCurves(QPainter *painter); enum { Margin = 50 }; QToolButton *zoomInButton; QToolButton *zoomOutButton; QMap<int, QVector<QPointF> > curveMap; QVector<PlotSettings> zoomStack; int curZoom; bool rubberBandIsShown; QRect rubberBandRect; QPixmap pixmap; };

Dans la section prive de la classe, nous dclarons quelques fonctions pour dessiner le widget, une constante et quelques variables membres. La constante Margin sert introduire un peu despace autour du graphique. Parmi les variables membres, on compte un pixmap de type QPixmap. Cette variable conserve une copie du rendu de tout le widget, identique celui afch lcran. Le trac est toujours dessin sur ce pixmap dabord hors champ ; puis le pixmap est copi dans le widget.
class PlotSettings { public: PlotSettings(); void scroll(int dx, int dy); void adjust(); double spanX() const { return maxX - minX; }

Chapitre 5

Crer des widgets personnaliss

125

double spanY() const { return maxY - minY; } double minX; double maxX; int numXTicks; double minY; double maxY; int numYTicks; private: static void adjustAxis(double &min, double &max, int &numTicks); }; #endif

La classe PlotSettings spcie la plage des axes x et y et le nombre de graduations pour ces axes. La Figure 5.8 montre la correspondance entre un objet PlotSettings et un widget Plotter. Par convention, numXTicks et numYTicks ont une unit de moins ; si numXTicks a la valeur 5, Plotter dessinera 6 graduations sur laxe x. Cela simplie les calculs par la suite.
Figure 5.8 Les variables membres de PlotSettings

maxY

numYTicks
numXTicks minY minX maxX

Analysons prsent le chier dimplmentation :


#include <QtGui> #include <cmath> #include "plotter.h"

Nous incluons les chiers den-ttes prvus et nous importons tous les symboles de lespace de noms std dans lespace de noms global. Ceci nous permet daccder aux fonctions dclares dans <cmath> sans les prxer avec std:: (par exemple, floor() au lieu de std::floor()).
Plotter::Plotter(QWidget *parent) : QWidget(parent) { setBackgroundRole(QPalette::Dark); setAutoFillBackground(true); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

126

Qt4 et C++ : Programmation dinterfaces GUI

setFocusPolicy(Qt::StrongFocus); rubberBandIsShown = false; zoomInButton = new QToolButton(this); zoomInButton->setIcon(QIcon(":/images/zoomin.png")); zoomInButton->adjustSize(); connect(zoomInButton, SIGNAL(clicked()), this, SLOT(zoomIn())); zoomOutButton = new QToolButton(this); zoomOutButton->setIcon(QIcon(":/images/zoomout.png")); zoomOutButton->adjustSize(); connect(zoomOutButton, SIGNAL(clicked()), this, SLOT(zoomOut())); setPlotSettings(PlotSettings()); }

Lappel de setBackgroundRole() demande QWidget dutiliser le composant "dark" de la palette comme couleur pour effacer le widget, la place du composant "window".Qt se voit donc attribuer une couleur par dfaut quil peut employer pour remplir nimporte quel pixel nouvellement afch quand le widget est redimensionn dans une taille plus grande, avant mme que paintEvent() nait lopportunit de dessiner les nouveaux pixels. Nous devons aussi invoquer setAutoFillBackground(true) dans le but dactiver ce mcanisme. (Par dfaut, les widgets enfants hritent de larrire-plan de leur widget parent.) Lappel de setSizePolicy() dnit la stratgie de taille du widget en QSizePolicy:: Expanding dans les deux directions. Tout gestionnaire de disposition responsable du widget sait donc que ce dernier peut tre agrandi, mais peut aussi tre rtrci. Ce paramtre est typique des widgets qui peuvent prendre beaucoup de place lcran. La valeur par dfaut est QSizePolicy::Preferred dans les deux directions, ce qui signie que le widget prfre tre sa taille requise, mais quil peut tre rtrci sa taille minimale ou agrandi linni si ncessaire. Lappel de setFocusPolicy(Qt::StrongFocus) permet au widget de recevoir le focus lorsque lutilisateur clique ou appuie sur Tab. Quand le Plotter est actif, il recevra les vnements lis aux touches du clavier enfonces. Le widget Plotter ragit quelques touches : + pour un zoom avant ; pour un zoom arrire ; et les ches directionnelles pour faire dler vers la droite ou la gauche, le haut ou le bas (voir Figure 5.9).
Figure 5.9 Faire dler le widget Plotter

Chapitre 5

Crer des widgets personnaliss

127

Toujours dans le constructeur, nous crons deux QToolButton, chacun avec une icne. Ces boutons permettent lutilisateur de faire des zooms avant et arrire. Les icnes du bouton sont stockes dans un chier de ressources, donc toute application qui utilise le widget Plotter aura besoin de cette entre dans son chier .pro:
RESOURCES = plotter.qrc

Le chier de ressources ressemble celui que nous avons utilis pour lapplication Spreadsheet :
<!DOCTYPE RCC><RCC version="1.0"> <qresource> <file>images/zoomin.png</file> <file>images/zoomout.png</file> </qresource> </RCC>

Les appels de adjustSize() sur les boutons dnissent leurs tailles de sorte quelles correspondent la taille requise. Les boutons ne font pas partie dune disposition ; nous les positionnerons manuellement dans lvnement resize de Plotter. Vu que nous ne nous servons pas des dispositions, nous devons spcier explicitement le parent des boutons en le transmettant au constructeur de QPushButton. Lappel de setPlotSettings() la n termine linitialisation.
void Plotter::setPlotSettings(const PlotSettings &settings) { zoomStack.clear(); zoomStack.append(settings); curZoom = 0; zoomInButton->hide(); zoomOutButton->hide(); refreshPixmap(); }

La fonction setPlotSettings() est employe pour spcier le PlotSettings utiliser pour afcher le trac. Elle est appele par le constructeur Plotter et peut tre employe par des utilisateurs de la classe. Le traceur commence son niveau de zoom par dfaut. A chaque fois que lutilisateur fait un zoom avant, une nouvelle instance de PlotSettings est cre et place sur la pile de zoom. La pile de zoom est reprsente par deux variables membres :

zoomStack contient les divers paramtres de zoom sous forme de QVector<PlotSettings>. curZoom comporte lindex du PlotSettings actuel dans zoomStack. Aprs lappel de setPlotSettings(), la pile de zoom ne contient quune seule entre et les boutons Zoom In et Zoom Out sont masqus. Ces boutons ne safcheront que lorsque nous appellerons show() sur eux dans les slots zoomIn() et zoomOut(). (Normalement il suft dinvoquer show() sur le widget de niveau suprieur pour afcher tous les enfants.

128

Qt4 et C++ : Programmation dinterfaces GUI

Mais quand nous appelons explicitement hide() sur un widget enfant, il est masqu jusqu ce nous appelions nouveau show() sur ce widget.) Lappel de refreshPixmap() est ncessaire pour mettre jour lafchage. Normalement, nous invoquerions update(), mais dans ce cas, nous agissons lgrement diffremment parce que nous voulons conserver un QPixmap toujours mis jour. Aprs avoir rgnr le pixmap, refreshPixmap() appelle update() pour copier le pixmap dans le widget.
void Plotter::zoomOut() { if (curZoom > 0) { --curZoom; zoomOutButton->setEnabled(curZoom > 0); zoomInButton->setEnabled(true); zoomInButton->show(); refreshPixmap(); } }

Le slot zoomOut() fait un zoom arrire si vous avez dj zoom sur le graphique. Il dcrmente le niveau actuel de zoom et active le bouton Zoom Out selon quil est encore possible de faire un zoom arrire ou pas. Le bouton Zoom In est activ et afch, et lafchage est mis jour avec un appel de refreshPixmap().
void Plotter::zoomIn() { if (curZoom < zoomStack.count() - 1) { ++curZoom; zoomInButton->setEnabled(curZoom < zoomStack.count() - 1); zoomOutButton->setEnabled(true); zoomOutButton->show(); refreshPixmap(); } }

Si lutilisateur a fait un zoom avant puis un zoom arrire, le PlotSettings du prochain niveau de zoom sera dans la pile de zoom et nous pourrons zoomer. (Sinon, il est toujours possible de faire un zoom avant avec un rectangle de slection.) Le slot incrmente curZoom pour descendre dun niveau dans la pile de zoom, active ou dsactive le bouton Zoom In selon quil est possible de faire encore un zoom avant ou non, et active et afche le bouton Zoom Out. A nouveau, nous appelons refreshPixmap() pour que le traceur utilise les derniers paramtres du zoom.
void Plotter::setCurveData(int id, const QVector<QPointF> &data) { curveMap[id] = data; refreshPixmap(); }

Chapitre 5

Crer des widgets personnaliss

129

La fonction setCurveData() dnit les donnes de courbe pour un ID de courbe donn. Sil existe dj une courbe portant le mme ID dans curveMap, elle est remplace par les nouvelles donnes de courbe ; sinon, la nouvelle courbe est simplement insre. La variable membre curveMap est de type QMap<int,QVector<QPointF>>.
void Plotter::clearCurve(int id) { curveMap.remove(id); refreshPixmap(); }

La fonction clearCurve() supprime la courbe spcie dans curveMap.


QSize Plotter::minimumSizeHint() const { return QSize(6 * Margin, 4 * Margin); }

La fonction minimumSizeHint() est similaire sizeHint(); tout comme sizeHint() spcie la taille idale dun widget, minimumSizeHint() spcie la taille minimale idale dun widget. Une disposition ne redimensionne jamais un widget en dessous de sa taille requise minimale. La valeur que nous retournons est 300 _ 200 (vu que Margin est gal 50) pour laisser une marge des quatre cts et un peu despace pour le trac. En dessous de cette taille, le trac serait trop petit pour tre utile.
QSize Plotter::sizeHint() const { return QSize(12 * Margin, 8 * Margin); }

Dans sizeHint nous retournons une taille "idale" proportionnelle la constante Margin et avec le mme format dimage de 3:2 que nous avons utilis pour minimumSizeHint(). Ceci termine lanalyse des slots et des fonctions publiques de Plotter. Etudions prsent les gestionnaires dvnements protgs.
void Plotter::paintEvent(QPaintEvent * /* event */) { QStylePainter painter(this); painter.drawPixmap(0, 0, pixmap); if (rubberBandIsShown) { painter.setPen(palette().light().color()); painter.drawRect(rubberBandRect.normalized() .adjusted(0, 0, -1, -1)); } if (hasFocus()) { QStyleOptionFocusRect option; option.initFrom(this); option.backgroundColor = palette().dark().color();

130

Qt4 et C++ : Programmation dinterfaces GUI

painter.drawPrimitive(QStyle::PE_FrameFocusRect, option); } }

Normalement, cest dans paintEvent() que nous effectuons toutes les oprations de dessin. Cependant, dans notre exemple, nous avons dessin tout le trac auparavant dans refreshPixmap(), nous avons donc la possibilit dafcher tout le trac simplement en copiant le pixmap dans le widget la position (0, 0). Si le rectangle de slection est visible, nous le dessinons au-dessus du trac. Nous utilisons le composant "light" du groupe de couleurs actuel du widget comme couleur du crayon pour garantir un bon contraste avec larrire-plan "dark".Notez que nous dessinons directement sur le widget, nous ne touchons donc pas au pixmap hors champ. Utiliser QRect::normalized() vous assure que le rectangle de slection prsente une largeur et une hauteur positives (en changeant les coordonnes si ncessaire), et adjusted() rduit la taille du rectangle dun pixel pour tenir compte de son contour dun pixel. Si le Plotter est activ, un rectangle "de focus" est dessin au moyen de la fonction drawPrimitive() correspondant au style de widget, avec QStyle::PE_FrameFocusRect comme premier argument et QStyleOptionFocusRect comme second argument. Les options graphiques du rectangle de focus sont hrites du widget Plotter (par lappel de initFrom()). La couleur darrire-plan doit tre spcie explicitement. Si vous voulez dessiner en utilisant le style actuel, vous pouvez appeler directement une fonction QStyle, par exemple,
style()->drawPrimitive(QStyle::PE_FrameFocusRect, &option, &painter, this);

ou utiliser un QStylePainter au lieu dun QPainter normal, comme nous avons procd dans Plotter. Vous dessinez ainsi plus confortablement. La fonction QWidget::style() retourne le style qui doit tre utilis pour dessiner le widget. Dans Qt, le style de widget est une sous-classe de QStyle. Les styles intgrs englobent QWindowsStyle, QWindowsXPStyle, QMotifStyle, QCDEStyle, QMacStyle et QPlastiqueStyle. Chacun de ces styles rimplmente les fonctions virtuelles dans QStyle an dadapter le dessin la plate-forme pour laquelle le style est mul. La fonction drawPrimitive() de QStylePainter appelle la fonction QStyle du mme nom, qui peut tre employe pour dessiner des "lments primitifs" comme les panneaux, les boutons et les rectangles de focus. Le style de widget est gnralement le mme pour tous les widgets dune application (QApplication::style()), mais vous pouvez ladapter au cas par cas laide de QWidget::setStyle(). En drivant QStyle, il est possible de dnir un style personnalis. Vous pouvez ainsi attribuer un aspect trs particulier une application ou une suite dapplications. Alors quil est habituellement recommand dadopter laspect et lapparence natifs de la plate-forme cible, Qt offre une grande exibilit si vous souhaitez intervenir dans ce domaine. Les widgets intgrs de Qt se basent presque exclusivement sur QStyle pour se dessiner. Cest pourquoi ils ressemblent aux widgets natifs sur toutes les plates-formes prises en charge par Qt.

Chapitre 5

Crer des widgets personnaliss

131

Les widgets personnaliss peuvent adopter le style courant soit en utilisant QStyle pour se tracer eux-mmes, soit en employant les widgets Qt intgrs comme widgets enfants. Sagissant de Plotter, nous utilisons une combinaison des deux approches : le rectangle de focus est dessin avec QStyle (via un QStylePainter) et les boutons Zoom In et Zoom Out sont des widgets Qt intgrs.
void Plotter::resizeEvent(QResizeEvent * /* event */) { int x = width() - (zoomInButton->width() + zoomOutButton->width() + 10); zoomInButton->move(x, 5); zoomOutButton->move(x + zoomInButton->width() + 5, 5); refreshPixmap(); }

Quand le widget Plotter est redimensionn, Qt dclenche un vnement "resize". Ici, nous implmentons resizeEvent() pour placer les boutons Zoom In et Zoom Out en haut droite du widget Plotter. Nous dplaons les boutons Zoom In et Zoom Out pour quils soient cte cte, spars par un espace de 5 pixels et dcals de 5 pixels par rapport aux bords suprieur et droit du widget parent. Si nous avions voulu que les boutons restent ancrs dans le coin suprieur gauche, dont les coordonnes sont (0, 0), nous les aurions simplement placs cet endroit dans le constructeur de Plotter. Nanmoins, nous souhaitons assurer le suivi du coin suprieur droit, dont les coordonnes dpendent de la taille du widget. Cest pour cette raison quil est ncessaire de rimplmenter resizeEvent() et dy dnir la position des boutons. Nous navons pas congur les positions des boutons dans le constructeur de Plotter. Ce nest pas un problme parce que Qt dclenche toujours un vnement resize avant dafcher un widget pour la premire fois. Plutt que de rimplmenter resizeEvent() et de disposer les widgets enfants manuellement, nous aurions pu faire appel un gestionnaire de disposition (par exemple, QGridLayout). Lutilisation dune disposition aurait t un peu plus complique et aurait consomm davantage de ressources, mais les dispositions de droite gauche aurait t mieux gres, notamment pour des langues comme larabe et lhbreu. Nous terminons en invoquant refreshPixmap() pour redessiner le pixmap sa nouvelle taille.
void Plotter::mousePressEvent(QMouseEvent *event) { QRect rect(Margin, Margin, width() - 2 * Margin, height() - 2 * Margin); if (event->button() == Qt::LeftButton) { if (rect.contains(event->pos())) { rubberBandIsShown = true; rubberBandRect.setTopLeft(event->pos()); rubberBandRect.setBottomRight(event->pos()); updateRubberBandRegion();

132

Qt4 et C++ : Programmation dinterfaces GUI

setCursor(Qt::CrossCursor); } } }

Quand lutilisateur appuie sur le bouton gauche de la souris, nous commenons afcher un rectangle de slection. Ceci implique de dnir rubberBandIsShown en true, dinitialiser la variable membre rubberBandRect la position actuelle du pointeur de la souris, de planier un vnement paint pour tracer le rectangle et de changer le pointeur de la souris pour afcher un pointeur rticule. La variable rubberBandRect est de type QRect. Un QRect peut tre dni soit sous forme dun quadruple (x, y, width, height) o (x, y) est la position du coin suprieur gauche et width _ height correspond la taille du rectangle soit comme une paire de coordonnes suprieur-gauche et infrieur-droit. Dans ce cas, nous avons employ la reprsentation avec des paires de coordonnes. Nous dnissons le point o lutilisateur a cliqu la fois comme tant le coin suprieur gauche et le coin infrieur droit. Puis nous appelons updateRubberBandRegion() pour forcer le rafrachissement de lafchage de la (toute petite) zone couverte par le rectangle de slection. Qt propose deux mcanismes pour contrler la forme du pointeur de la souris :

QWidget::setCursor() dnit la forme du pointeur utiliser quand la souris se place sur un widget particulier. Si aucun pointeur nest congur pour le widget, cest le pointeur du widget parent qui est employ. Les widgets de haut niveau proposent par dfaut un pointeur en forme de che.

QApplication::setOverrideCursor() dnit la forme du pointeur pour toute lapplication, ignorant les pointeurs congurs par chaque widget jusqu ce que restoreOverrideCursor() soit invoque. Dans le Chapitre 4, nous avons appel QApplication::setOverrideCursor() avec Qt::WaitCursor pour changer le pointeur de lapplication en sablier.

void Plotter::mouseMoveEvent(QMouseEvent *event) { if (rubberBandIsShown) { updateRubberBandRegion(); rubberBandRect.setBottomRight(event->pos()); updateRubberBandRegion(); } }

Quand lutilisateur dplace le pointeur de la souris alors quil maintient le bouton gauche enfonc, nous appelons dabord updateRubberBandRegion() pour planier un vnement paint an de redessiner la zone o se trouvait le rectangle de slection, puis nous recalculons rubberBandRect pour tenir compte du dplacement de la souris, et enn nous invoquons updateRubberBandRegion() une deuxime fois pour retracer la zone vers laquelle sest

Chapitre 5

Crer des widgets personnaliss

133

dplac le rectangle de slection. Ce rectangle est donc effectivement supprim et redessin aux nouvelles coordonnes. Si lutilisateur dplace la souris vers le haut ou la gauche, il est probable que le coin infrieur droit de rubberBandRect se retrouve au-dessus ou gauche de son coin suprieur gauche. Si cest le cas, QRect aura une largeur ou une hauteur ngative. Nous avons utilis QRect::normalized() dans paintEvent() pour nous assurer que les coordonnes suprieur-gauche et infrieur-droit sont ajustes de manire ne pas avoir de largeur et de hauteur ngatives.
void Plotter::mouseReleaseEvent(QMouseEvent *event) { if ((event->button() == Qt::LeftButton) && rubberBandIsShown) { rubberBandIsShown = false; updateRubberBandRegion(); unsetCursor(); QRect rect = rubberBandRect.normalized(); if (rect.width() < 4 || rect.height() < 4) return; rect.translate(-Margin, -Margin); PlotSettings prevSettings = zoomStack[curZoom]; PlotSettings settings; double dx = prevSettings.spanX() / (width() - 2 * Margin); double dy = prevSettings.spanY() / (height() - 2 * Margin); settings.minX = prevSettings.minX settings.maxX = prevSettings.minX settings.minY = prevSettings.maxY settings.maxY = prevSettings.maxY settings.adjust(); zoomStack.resize(curZoom + 1); zoomStack.append(settings); zoomIn(); } } + + dx dx dy dy * * * * rect.left(); rect.right(); rect.bottom(); rect.top();

Quand lutilisateur relche le bouton gauche de la souris, nous supprimons le rectangle de slection et nous restaurons le pointeur standard sous forme de che. Si le rectangle est au moins de 4 4, nous effectuons un zoom. Si le rectangle de slection est plus petit, il est probable que lutilisateur a cliqu sur le widget par erreur ou uniquement pour lactiver, nous ne faisons donc rien. Le code permettant de zoomer est quelque peu complexe. Cest parce que nous traitons des coordonnes du widget et de celles du traceur en mme temps. La plupart des tches effectues ici servent convertir le rubberBandRect, pour transformer les coordonnes du widget en coordonnes du traceur. Une fois la conversion effectue, nous invoquons

134

Qt4 et C++ : Programmation dinterfaces GUI

PlotSettings::adjust() pour arrondir les chiffres et trouver un nombre raisonnable de graduations pour chaque axe. Les Figures 5.10 et 5.11 illustrent la situation.
Figure 5.10 Convertir les coordonnes dun rectangle de slection du widget en coordonnes du traceur
(0, 0) 10 8 6 4 2 0 0 2 4 6 8 10 135 68 (94, 73)

10 8

2.4

6.8

6 4 2 0 0 2 4 6 8

6.5 3.2

10

Figure 5.11 Ajuster les coordonnes du traceur et zoomer sur le rectangle de slection

10 8

2.0

7.0

7 7.0 6

6 4 3.0 2 0 0 2 4 6 8 10

5 4 3 2 3 4 5 6 7

Puis nous zoomons. Pour zoomer, nous devons appuyer sur le nouveau PlotSettings que nous venons de calculer en haut de la pile de zoom et nous appelons zoomIn() qui se chargera de la tche.
void Plotter::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key_Plus: zoomIn(); break; case Qt::Key_Minus: zoomOut(); break; case Qt::Key_Left: zoomStack[curZoom].scroll(-1, 0); refreshPixmap(); break; case Qt::Key_Right: zoomStack[curZoom].scroll(+1, 0); refreshPixmap(); break; case Qt::Key_Down:

Chapitre 5

Crer des widgets personnaliss

135

zoomStack[curZoom].scroll(0, -1); refreshPixmap(); break; case Qt::Key_Up: zoomStack[curZoom].scroll(0, +1); refreshPixmap(); break; default: QWidget::keyPressEvent(event); } }

Quand lutilisateur appuie sur une touche et que le widget Plotter est actif, la fonction keyPressEvent() est invoque. Nous la rimplmentons ici pour rpondre six touches : +, , Haut, Bas, Gauche et Droite. Si lutilisateur a appuy sur une touche que nous ne grons pas, nous appelons limplmentation de la classe de base. Pour une question de simplicit, nous ignorons les touches de modication Maj, Ctrl et Alt, disponibles via QKeyEvent::modifiers().
void Plotter::wheelEvent(QWheelEvent *event) { int numDegrees = event->delta() / 8; int numTicks = numDegrees / 15; if (event->orientation() == Qt::Horizontal) { zoomStack[curZoom].scroll(numTicks, 0); } else { zoomStack[curZoom].scroll(0, numTicks); } refreshPixmap(); }

Les vnements wheel se dclenchent quand la molette de la souris est actionne. La majorit des souris ne proposent quune molette verticale, mais certaines sont quipes dune molette horizontale. Qt prend en charge les deux types de molette. Les vnements wheel sont transmis au widget actif. La fonction delta() retourne la distance parcourue par la molette en huitimes de degr. Les souris proposent habituellement une plage de 15 degrs. Dans notre exemple, nous faisons dler le nombre de graduations demandes en modiant llment le plus haut dans la pile de zoom et nous mettons jour lafchage au moyen de refreshPixmap(). Nous utilisons la molette de la souris le plus souvent pour faire drouler une barre de dlement. Quand nous employons QScrollArea (trait dans le Chapitre 6) pour proposer des barres de dlement, QScrollArea gre automatiquement les vnements lis la molette de la souris, nous navons donc pas rimplmenter wheelEvent() nous-mmes. Ceci achve limplmentation des gestionnaires dvnements. Passons maintenant en revue les fonctions prives.
void Plotter::updateRubberBandRegion() {

136

Qt4 et C++ : Programmation dinterfaces GUI

QRect rect = rubberBandRect.normalized(); update(rect.left(), rect.top(), rect.width(), 1); update(rect.left(), rect.top(), 1, rect.height()); update(rect.left(), rect.bottom(), rect.width(), 1); update(rect.right(), rect.top(), 1, rect.height()); }

La fonction updateRubberBand() est appele depuis mousePressEvent(), mouseMoveEvent() et mouseReleaseEvent() pour effacer ou redessiner le rectangle de slection. Elle est constitue de quatre appels de update() qui planient un vnement paint pour les quatre petites zones rectangulaires couvertes par le rectangle de slection (deux lignes verticales et deux lignes horizontales). Qt propose la classe QRubberBand pour dessiner des rectangles de slection, mais dans ce cas, lcriture du code permet de mieux contrler lopration.
void Plotter::refreshPixmap() { pixmap = QPixmap(size()); pixmap.fill(this, 0, 0); QPainter painter(&pixmap); painter.initFrom(this); drawGrid(&painter); drawCurves(&painter); update(); }

La fonction refreshPixmap() redessine le trac sur le pixmap hors champ et met lafchage jour. Nous redimensionnons le pixmap de sorte quil ait la mme taille que le widget et nous le remplissons avec la couleur deffacement du widget. Cette couleur correspond au composant "dark" de la palette en raison de lappel de setBackgroundRole() dans le constructeur de Plotter. Si larrire-plan nest pas uni, QPixmap::fill() doit connatre la position du pixmap dans le widget pour aligner correctement le motif de couleur. Dans notre cas, le pixmap correspond la totalit du widget, nous spcions donc la position (0, 0). Nous crons ensuite un QPainter pour dessiner sur le pixmap. Lappel de initFrom() dnit le crayon, larrire-plan et la police pour quils soient identiques ceux du widget Plotter. Puis nous invoquons drawGrid() et drawCurves() pour raliser le dessin. Nous appelons enn update() pour planier un vnement paint pour la totalit du widget. Le pixmap est copi dans le widget dans la fonction paintEvent().
void Plotter::drawGrid(QPainter *painter) { QRect rect(Margin, Margin, width() - 2 * Margin, height() - 2 * Margin); if (!rect.isValid()) return; PlotSettings settings = zoomStack[curZoom]; QPen quiteDark = palette().dark().color().light(); QPen light = palette().light().color();

Chapitre 5

Crer des widgets personnaliss

137

for (int i = 0; i <= settings.numXTicks; ++i) { int x = rect.left() + (i * (rect.width() - 1) / settings.numXTicks); double label = settings.minX + (i * settings.spanX() / settings.numXTicks); painter->setPen(quiteDark); painter->drawLine(x, rect.top(), x, rect.bottom()); painter->setPen(light); painter->drawLine(x, rect.bottom(), x, rect.bottom() + 5); painter->drawText(x - 50, rect.bottom() + 5, 100, 15, Qt::AlignHCenter | Qt::AlignTop, QString::number(label)); } for (int j = 0; j <= settings.numYTicks; ++j) { int y = rect.bottom() - (j * (rect.height() - 1) / settings.numYTicks); double label = settings.minY + (j * settings.spanY() / settings.numYTicks); painter->setPen(quiteDark); painter->drawLine(rect.left(), y, rect.right(), y); painter->setPen(light); painter->drawLine(rect.left() - 5, y, rect.left(), y); painter->drawText(rect.left() - Margin, y - 10, Margin - 5, 20, Qt::AlignRight | Qt::AlignVCenter, QString::number(label)); } painter->drawRect(rect.adjusted(0, 0, -1, -1)); }

La fonction drawGrid() dessine la grille derrire les courbes et les axes. La zone dans laquelle nous dessinons la grille est spcie par rect. Si le widget nest pas assez grand pour sadapter au graphique, nous retournons immdiatement. La premire boucle for trace les lignes verticales de la grille et les graduations sur laxe x. La seconde boucle for trace les lignes horizontales de la grille et les graduations sur laxe y. A la n, nous dessinons un rectangle le long des marges. La fonction drawText() dessine les numros correspondants aux graduations sur les deux axes. Les appels de drawText() ont la syntaxe suivante :
painter->drawText(x, y, width, height, alignment, text);

o (x, y, width, height) dnit un rectangle, alignment la position du texte dans ce rectangle et text le texte dessiner.
void Plotter::drawCurves(QPainter *painter) { static const QColor colorForIds[6] = { Qt::red, Qt::green, Qt::blue, Qt::cyan, Qt::magenta, Qt::yellow }; PlotSettings settings = zoomStack[curZoom]; QRect rect(Margin, Margin, width() - 2 * Margin, height() - 2 * Margin);

138

Qt4 et C++ : Programmation dinterfaces GUI

if (!rect.isValid()) return; painter->setClipRect(rect.adjusted(+1, +1, -1, -1)); QMapIterator<int, QVector<QPointF> > i(curveMap); while (i.hasNext()) { i.next(); int id = i.key(); const QVector<QPointF> &data = i.value(); QPolygonF polyline(data.count()); for (int j = 0; j < data.count(); ++j) { double dx = data[j].x() - settings.minX; double dy = data[j].y() - settings.minY; double x = rect.left() + (dx * (rect.width() - 1) / settings.spanX()); double y = rect.bottom() - (dy * (rect.height() - 1) / settings.spanY()); polyline[j] = QPointF(x, y); } painter->setPen(colorForIds[uint(id) % 6]); painter->drawPolyline(polyline); } }

La fonction drawCurves() dessine les courbes au-dessus de la grille. Nous commenons par appeler setClipRect() pour dnir la zone daction de QPainter comme gale au rectangle qui contient les courbes (except les marges et le cadre autour du graphique). QPainter ignorera ensuite les oprations de dessin sur les pixels situs en dehors de cette zone. Puis, nous parcourons toutes les courbes laide dun itrateur de style Java, et pour chacune delles, nous parcourons les QPointF dont elle est constitue. La fonction key() donne lID de la courbe et la fonction value() donne les donnes de courbe correspondantes comme un QVector<QPointF>. La boucle interne for convertit chaque QPointF pour transformer les coordonnes du traceur en coordonnes du widget et les stocke dans la variable polyline. Une fois que nous avons converti tous les points dune courbe en coordonnes du widget, nous dterminons la couleur de crayon pour la courbe (en utilisant un des ensembles de couleurs prdnies) et nous appelons drawPolyline() pour tracer une ligne qui passe par tous les points de cette dernire. Voici la classe Plotter termine. Tout ce qui reste, ce sont quelques fonctions dans PlotSettings.
PlotSettings::PlotSettings() { minX = 0.0; maxX = 10.0; numXTicks = 5;

Chapitre 5

Crer des widgets personnaliss

139

minY = 0.0; maxY = 10.0; numYTicks = 5; }

Le constructeur de PlotSettings initialise les deux axes avec une plage de 0 10 en 5 graduations.
void PlotSettings::scroll(int dx, int dy) { double stepX = spanX() / numXTicks; minX += dx * stepX; maxX += dx * stepX; double stepY = spanY() / numYTicks; minY += dy * stepY; maxY += dy * stepY; }

La fonction scroll() incrmente (ou dcrmente) minX, maxX, minY et maxY de la valeur de lintervalle entre deux graduations multiplie par un nombre donn. Cette fonction est utilise pour implmenter le dlement dans Plotter::keyPressEvent().
void PlotSettings::adjust() { adjustAxis(minX, maxX, numXTicks); adjustAxis(minY, maxY, numYTicks); }

La fonction adjust() est invoque dans mouseReleaseEvent() pour arrondir les valeurs minX, maxX, minY et maxY en valeurs "conviviales" et pour dterminer le bon nombre de graduations pour chaque axe. La fonction prive adjustAxis() sexcute sur un axe la fois.
void PlotSettings::adjustAxis(double &min, double &max, int &numTicks) { const int MinTicks = 4; double grossStep = (max - min) / MinTicks; double step = pow(10.0, floor(log10(grossStep))); if (5 * step < grossStep) { step *= 5; } else if (2 * step < grossStep) { step *= 2; } numTicks = int(ceil(max / step) - floor(min / step)); if (numTicks < MinTicks) numTicks = MinTicks; min = floor(min / step) * step; max = ceil(max / step) * step; }

140

Qt4 et C++ : Programmation dinterfaces GUI

La fonction adjustAxis() convertit ses paramtres min et max en nombres "conviviaux" et dnit son paramtre numTicks en nombre de graduations quelle calcule comme tant appropries pour la plage [min, max] donne. Vu que adjustAxis() a besoin de modier les variables relles (minX, maxX, numXTicks, etc.) et pas uniquement des copies, ses paramtres sont des rfrences non-const. Le code de adjustAxis() est principalement consacr dterminer une valeur adquate pour lintervalle entre deux graduations ("lchelon"). Pour obtenir des nombres convenables sur laxe, nous devons slectionner lchelon avec soin. Par exemple, une valeur de 3,8 engendrerait des multiples de 3,8 sur un axe, ce qui nest pas trs signicatif pour les utilisateurs. Pour les axes avec une notation dcimale, des valeurs dchelons "conviviales" sont des chiffres de la forme 10n, 2 10n ou 5 10n. Nous commenons par calculer "lchelon brut," une sorte de valeur maximum pour lchelon. Puis nous recherchons le nombre correspondant sous la forme 10n qui est infrieur ou gal lchelon brut. Pour ce faire, nous prenons le logarithme dcimal de lchelon brut, en arrondissant cette valeur vers le bas pour obtenir un nombre entier, puis en ajoutant 10 la puissance de ce chiffre arrondi. Par exemple, si lchelon brut est de 236, nous calculons log 236 = 2,37291 ; puis nous larrondissons vers le bas pour aboutir 2 et nous obtenons 102 = 100 comme valeur dchelon sous la forme 10n. Une fois que la premire valeur dchelon est dtermine, nous pouvons lutiliser pour calculer les deux autres candidats : 2 10n et 5 10n. Dans lexemple ci-dessus, les deux autres candidats sont 200 et 500. Le candidat 500 est suprieur lchelon brut, nous navons donc pas la possibilit de lemployer. Mais 200 est infrieur 236, nous utilisons ainsi 200 comme taille dchelon dans cet exemple. Il est assez facile de calculer numTicks, min et max partir de la valeur dchelon. La nouvelle valeur min est obtenue en arrondissant la valeur min dorigine vers le bas vers le multiple le plus proche de lchelon, et la nouvelle valeur max est obtenue en arrondissant vers le haut vers le multiple le plus proche de lchelon. La nouvelle valeur numTicks correspond au nombre dintervalles entre les valeurs min et max arrondies. Par exemple, si min est gal 240 et max 1184 au moment de la saisie de la fonction, la nouvelle plage devient [200, 1200], avec cinq graduations. Cet algorithme donnera des rsultats optimaux dans certains cas. Un algorithme plus sophistiqu est dcrit dans larticle "Nice Numbers for Graph Labels" de Paul S. Heckbert publi dans Graphics Gems (ISBN 0-12-286166-3). Ce chapitre achve la Partie I. Il vous a expliqu comment personnaliser un widget Qt existant et comment gnrer un widget en partant de zro laide de QWidget comme classe de base. Nous avons aussi vu comment composer un widget partir de widgets existants dans le Chapitre 2 et nous allons explorer ce thme plus en dtail dans le Chapitre 6. A ce stade, vous en savez sufsamment pour crire des applications GUI compltes avec Qt. Dans les Parties II et III, nous tudierons Qt en profondeur pour pouvoir proter de toute la puissance de ce framework.

II
Qt : niveau intermdiaire
6 7 8 9 10 11 12 13 14 15 16

Gestion des dispositions Traitement des vnements Graphiques 2D et 3D Glisser-dposer Classes dafchage dlments Classes conteneur Entres/Sorties Les bases de donnes Gestion de rseau XML Aide en ligne

6
Gestion des dispositions
Au sommaire de ce chapitre Disposer des widgets sur un formulaire Dispositions empiles Sparateurs Zones droulantes Widgets et barres doutils ancrables MDI (Multiple Document Interface)

Chaque widget plac dans un formulaire doit se voir attribuer une taille et une position appropries. Qt propose plusieurs classes qui disposent les widgets dans un formulaire : QHBoxLayout, QVBoxLayout, QGridLayout et QStackLayout. Ces classes sont si pratiques et faciles utiliser que presque tous les dveloppeurs Qt sen servent, soit directement dans du code source, soit par le biais du Qt Designer. Il existe une autre raison demployer les classes de disposition (layout) de Qt : elles garantissent que les formulaires sadaptent automatiquement aux diverses polices, langues et plates-formes. Si lutilisateur modie les paramtres de police du systme, les formulaires de lapplication rpondront immdiatement en se redimensionnant euxmmes si ncessaire. Si vous traduisez linterface utilisateur de lapplication en dautres langues, les classes de disposition prennent en compte le contenu traduit des widgets pour viter toute coupure de texte.

144

Qt4 et C++ : Programmation dinterfaces GUI

QSplitter, QScrollArea, QMainWindow et QWorkspace sont dautres classes qui se chargent de grer la disposition. Le point commun de ces classes cest quelles procurent une disposition trs exible sur laquelle lutilisateur peut agir. Par exemple, QSplitter propose un sparateur que lutilisateur peut faire glisser pour redimensionner les widgets, et QWorkspace prend en charge MDI (multiple document interface), un moyen dafcher plusieurs documents simultanment dans la fentre principale dune application. Etant donn quelles sont souvent utilises comme des alternatives aux classes de disposition, elles sont aussi prsentes dans ce chapitre.

Disposer des widgets sur un formulaire


Il y a trois moyens de grer la disposition des widgets enfants dans un formulaire : le positionnement absolu, la disposition manuelle et les gestionnaires de disposition. Nous allons tudier chacun deux tour de rle, en nous basant sur la bote de dialogue Find File illustre en Figure 6.1.
Figure 6.1 La bote de dialogue Find File

Le positionnement absolu est le moyen le plus rudimentaire de disposer des widgets. Il suft dassigner dans du code des tailles et des positions aux widgets enfants du formulaire et une taille xe au formulaire. Voici quoi ressemble le constructeur de FindFileDialog avec le positionnement absolu :
FindFileDialog::FindFileDialog(QWidget *parent) : QDialog(parent) { ... namedLabel->setGeometry(9, 9, 50, 25); namedLineEdit->setGeometry(65, 9, 200, 25); lookInLabel->setGeometry(9, 40, 50, 25); lookInLineEdit->setGeometry(65, 40, 200, 25);

Chapitre 6

Gestion des dispositions

145

subfoldersCheckBox->setGeometry(9, 71, 256, 23); tableWidget->setGeometry(9, 100, 256, 100); messageLabel->setGeometry(9, 206, 256, 25); findButton->setGeometry(271, 9, 85, 32); stopButton->setGeometry(271, 47, 85, 32); closeButton->setGeometry(271, 84, 85, 32); helpButton->setGeometry(271, 199, 85, 32); setWindowTitle(tr("Find Files or Folders")); setFixedSize(365, 240); }

Le positionnement absolu prsente de nombreux inconvnients : Lutilisateur ne peut pas redimensionner la fentre. Une partie du texte peut tre coupe si lutilisateur choisit une police trop grande ou si lapplication est traduite dans une autre langue. Les widgets peuvent prsenter des tailles inadaptes pour certains styles. Les positions et les tailles doivent tre calcules manuellement. Cette mthode est fastidieuse et sujette aux erreurs ; de plus, elle complique la maintenance. Lalternative au positionnement absolu est la disposition manuelle. Avec cette technique, les widgets ont toujours des positions absolues donnes, mais leurs tailles sont proportionnelles la taille de la fentre au lieu dtre totalement codes. Il convient donc de rimplmenter la fonction resizeEvent() du formulaire pour dnir les gomtries de ses widgets enfants :
FindFileDialog::FindFileDialog(QWidget *parent) : QDialog(parent) { ... setMinimumSize(265, 190); resize(365, 240); } void FindFileDialog::resizeEvent(QResizeEvent * /* event */) { int extraWidth = width() - minimumWidth(); int extraHeight = height() - minimumHeight(); namedLabel->setGeometry(9, 9, 50, 25); namedLineEdit->setGeometry(65, 9, 100 + extraWidth, 25); lookInLabel->setGeometry(9, 40, 50, 25); lookInLineEdit->setGeometry(65, 40, 100 + extraWidth, 25); subfoldersCheckBox->setGeometry(9, 71, 156 + extraWidth, 23); tableWidget->setGeometry(9, 100, 156 + extraWidth, 50 + extraHeight); messageLabel->setGeometry(9, 156 + extraHeight, 156 + extraWidth, 25); findButton->setGeometry(171 + extraWidth, 9, 85, 32); stopButton->setGeometry(171 + extraWidth, 47, 85, 32);

146

Qt4 et C++ : Programmation dinterfaces GUI

closeButton->setGeometry(171 + extraWidth, 84, 85, 32); helpButton->setGeometry(171 + extraWidth, 149 + extraHeight, 85, 32); }

Dans le constructeur de FindFileDialog, nous congurons la taille minimale du formulaire en 265 190 et la taille initiale en 365 240. Dans le gestionnaire resizeEvent(), nous accordons de lespace supplmentaire aux widgets qui veulent sagrandir. Nous sommes ainsi certains que le formulaire se met lchelle quand lutilisateur le redimensionne, comme illustr en Figure 6.2.

Figure 6.2 Redimensionner une bote de dialogue redimensionnable

Tout comme le positionnement absolu, la disposition manuelle oblige le programmeur calculer beaucoup de constantes codes. Ecrire du code de cette manire se rvle pnible, notamment si la conception change. De plus, le texte court toujours le risque dtre coup. Nous pouvons viter ce problme en tenant compte des tailles requises des widgets enfants, mais cela compliquerait encore plus le code. La solution la plus pratique pour disposer des widgets sur un formulaire consiste utiliser les gestionnaires de disposition de Qt. Ces gestionnaires proposent des valeurs par dfaut raisonnables pour chaque type de widget et tiennent compte de la taille requise de chacun deux, qui dpend de la police, du style et du contenu du widget. Ces gestionnaires respectent galement des dimensions minimales et maximales, et ajustent automatiquement la disposition en rponse des changements de police ou de contenu et un redimensionnement de la fentre. Les trois gestionnaires de disposition les plus importants sont QHBoxLayout, QVBoxLayout et QGridLayout. Ces classes hritent de QLayout, qui fournit le cadre de base des dispositions. Ces trois classes sont totalement prises en charge par le Qt Designer et peuvent aussi tre utilises directement dans du code. Voici le code de FindFileDialog avec des gestionnaires de disposition :
FindFileDialog::FindFileDialog(QWidget *parent)

Chapitre 6

Gestion des dispositions

147

: QDialog(parent) { ... QGridLayout *leftLayout = new QGridLayout; leftLayout->addWidget(namedLabel, 0, 0); leftLayout->addWidget(namedLineEdit, 0, 1); leftLayout->addWidget(lookInLabel, 1, 0); leftLayout->addWidget(lookInLineEdit, 1, 1); leftLayout->addWidget(subfoldersCheckBox, 2, 0, 1, 2); leftLayout->addWidget(tableWidget, 3, 0, 1, 2); leftLayout->addWidget(messageLabel, 4, 0, 1, 2); QVBoxLayout *rightLayout = new QVBoxLayout; rightLayout->addWidget(findButton); rightLayout->addWidget(stopButton); rightLayout->addWidget(closeButton); rightLayout->addStretch(); rightLayout->addWidget(helpButton); QHBoxLayout *mainLayout = new QHBoxLayout; mainLayout->addLayout(leftLayout); mainLayout->addLayout(rightLayout); setLayout(mainLayout); setWindowTitle(tr("Find Files or Folders")); }

La disposition est gre par QHBoxLayout, QGridLayout et QVBoxLayout. QGridLayout gauche et QVBoxLayout droite sont placs cte cte par le QHBoxLayout externe. Les marges autour de la bote de dialogue et lespace entre les widgets enfants prsentent des valeurs par dfaut en fonction du style de widget ; elles peuvent tre modies grce QLayout::setMargin() et QLayout::setSpacing(). La mme bote de dialogue aurait pu tre cre visuellement dans le Qt Designer en plaant les widgets enfants leurs positions approximatives, en slectionnant ceux qui doivent tre disposs ensemble et en cliquant sur Form > Lay Out Horizontally, Form > Lay Out Vertically ou Form > Lay Out in a Grid. Nous avons employ cette approche dans le Chapitre 2 pour crer les botes de dialogue Go-to-Cell et Sort de lapplication Spreadsheet. Utiliser QHBoxLayout et QVBoxLayout est plutt simple mais lutilisation de QGridLayout se rvle un peu plus complexe. QGridLayout se base sur une grille de cellules deux dimensions. Le QLabel dans le coin suprieur gauche de la disposition se trouve la position (0, 0) et le QLineEdit correspondant se situe la position (0, 1). Le QCheckBox stend sur deux colonnes ; il occupe les cellules aux positions (2, 0) et (2, 1). Les QTreeWidget et QLabel en dessous prennent aussi deux colonnes (voir Figure 6.3). Les appels de addWidget() ont la syntaxe suivante :
layout->addWidget(widget, row, column, rowSpan, columnSpan);

148

Qt4 et C++ : Programmation dinterfaces GUI

Figure 6.3 La disposition de la bote de dialogue Find File

Titre de fentre

QLabel
mainLayout

QLineEdit QLineEdit

QPushButton QPushButton QPushButton QPushButton


rightLayout

QLabel

QCheckBox
leftLayout

QTreeWidget

QLabel

Dans ce cas, widget est le widget enfant insrer dans la disposition, (row, column) est la cellule en haut gauche occupe par le widget, rowSpan correspond au nombre de lignes occupes par le widget et columnSpan est le nombre de colonnes occupes par le widget. Sils sont omis, les paramtres rowSpan et columnSpan ont la valeur 1 par dfaut. Lappel de addStretch() ordonne au gestionnaire de disposition dutiliser lespace cet endroit dans la disposition. En ajoutant un lment dtirement, nous avons demand au gestionnaire de disposition de placer tout espace excdentaire entre les boutons Close et Help. Dans le Qt Designer, nous pouvons aboutir au mme effet en insrant un lment despacement. Les lments despacement apparaissent dans le Qt Designer sous forme de "ressorts" bleus. Utiliser des gestionnaires de disposition prsente des avantages supplmentaires par rapport ceux dcrits jusque l. Si nous ajoutons ou supprimons un widget dans une disposition, celle-ci sadaptera automatiquement la nouvelle situation. Il en va de mme si nous invoquons hide() ou show() sur un widget enfant. Si la taille requise dun widget enfant change, la disposition sera automatiquement corrige, en tenant compte de cette nouvelle taille. En outre, les gestionnaires de disposition dnissent automatiquement une taille minimale pour le formulaire, en fonction des tailles minimales et des tailles requises des widgets enfants de ce dernier. Dans les exemples donns jusqu prsent, nous avons simplement plac les widgets dans des dispositions et utilis des lments despacement pour combler tout espace excdentaire. Dans certains cas, ce nest pas sufsant pour que la disposition ressemble exactement ce que nous voulons. Nous pouvons donc ajuster la disposition en changeant les stratgies de taille (rgles auxquelles la taille est soumise, voir chapitre prcdent) et les tailles requises des widgets disposer. Grce la stratgie de taille dun widget, le systme de disposition sait comment ce widget doit tre tir ou rtrci. Qt propose des stratgies par dfaut raisonnables pour tous ses widgets intgrs. Cependant, puisquil nexiste pas de valeur par dfaut qui pourrait tenir compte de toutes les dispositions possibles, il est courant que les dveloppeurs modient les

Chapitre 6

Gestion des dispositions

149

stratgies pour un ou deux widgets dans un formulaire. Un QSizePolicy possde un composant horizontal et un vertical. Voici les valeurs les plus utiles : Fixed signie que le widget ne peut pas tre rtrci ou tir. Le widget conserve toujours sa taille requise. Minimum signie que la taille requise dun widget correspond sa taille minimale. Le widget ne peut pas tre rtrci en dessous de la taille requise, mais il peut sagrandir pour combler lespace disponible si ncessaire. Maximum signie que la taille requise dun widget correspond sa taille maximale. Le widget peut tre rtrci jusqu sa taille requise minimum. Preferred signie que la taille requise dun widget correspond sa taille favorite, mais que le widget peut toujours tre rtrci ou tir si ncessaire. Expanding signie que le widget peut tre rtrci ou tir, mais quil prfre tre agrandi. La Figure 6.4 rcapitule la signication des diffrentes stratgies, en utilisant un QLabel afchant le texte "Some Text" comme exemple.
Figure 6.4 La signication des diffrentes stratgies de taille
taille requise min Fixed Minimum Maximum Preferred Expanding Som Som Som taille requise SomeText SomeText SomeText SomeText SomeText SomeText SomeText SomeText

Dans la gure, Preferred et Expanding donnent le mme rsultat. O se situe la diffrence ? Quand un formulaire qui contient les widgets Preferred et Expanding est redimensionn, lespace supplmentaire est attribu aux widgets Expanding, alors que les widgets Preferred conservent leur taille requise. Il existe deux autres stratgies : MinimumExpanding et Ignored. La premire tait ncessaire dans quelques rares cas dans les versions antrieures de Qt, mais elle ne prsente plus dintrt ; la meilleure approche consiste utiliser Expanding et rimplmenter minimumSizeHint() de faon approprie. La seconde est similaire Expanding, sauf quelle ignore la taille requise et la taille requise minimum du widget. En plus des composants verticaux et horizontaux de la stratgie, la classe QSizePolicy stocke un facteur dtirement horizontal et vertical. Ces facteurs dtirement peuvent tre utiliss pour indiquer que les divers widgets enfants doivent stirer diffrents niveaux quand le formulaire sagrandit. Par exemple, si nous avons un QTreeWidget au-dessus dun QTextEdit et que nous voulons que le QTextEdit soit deux fois plus grand que le QTreeWidget, nous avons la

150

Qt4 et C++ : Programmation dinterfaces GUI

possibilit de dnir un facteur dtirement vertical de QTextEdit de 2 et un facteur dtirement vertical de QTreeWidget de 1. Cependant, un autre moyen dinuencer une disposition consiste congurer une taille minimale ou maximale, ou une taille xe pour les widgets enfants. Le gestionnaire de disposition respectera ces contraintes lorsquil disposera les widgets. Et si ce nest pas sufsant, nous pouvons toujours driver de la classe du widget enfant et rimplmenter sizeHint() pour obtenir la taille requise dont nous avons besoin.

Dispositions empiles
La classe QStackedLayout dispose un ensemble de widgets enfants, ou "pages," et nen afche quun seul la fois, en masquant les autres lutilisateur. QStackedLayout est invisible en soi et lutilisateur na aucun moyen de changer une page. Les petites ches et le cadre gris fonc dans la Figure 6.5 sont fournis par le Qt Designer pour faciliter la conception avec la disposition. Pour des questions pratiques, Qt inclut galement un QStackedWidget qui propose un QWidget avec un QStackedLayout intgr.
Figure 6.5 QStackedLayout

Les pages sont numrotes en commenant 0. Pour afcher un widget enfant spcique, nous pouvons appeler setCurrentIndex() avec un numro de page. Le numro de page dun widget enfant est disponible grce indexOf().
Figure 6.6 Deux pages de la bote de dialogue Preferences

Chapitre 6

Gestion des dispositions

151

La bote de dialogue Preferences illustre en Figure 6.6 est un exemple qui utilise QStackedLayout. Elle est constitue dun QListWidget gauche et dun QStackedLayout droite. Chaque lment dans QListWidget correspond une page diffrente dans QStackedLayout. Voici le code du constructeur de la bote de dialogue :
PreferenceDialog::PreferenceDialog(QWidget *parent) : QDialog(parent) { ... listWidget = new QListWidget; listWidget->addItem(tr("Appearance")); listWidget->addItem(tr("Web Browser")); listWidget->addItem(tr("Mail & News")); listWidget->addItem(tr("Advanced")); stackedLayout = new QStackedLayout; stackedLayout->addWidget(appearancePage); stackedLayout->addWidget(webBrowserPage); stackedLayout->addWidget(mailAndNewsPage); stackedLayout->addWidget(advancedPage); connect(listWidget, SIGNAL(currentRowChanged(int)), stackedLayout, SLOT(setCurrentIndex(int))); ... listWidget->setCurrentRow(0); }

Nous crons un QListWidget et nous lalimentons avec les noms des pages. Nous crons ensuite un QStackedLayout et nous invoquons addWidget() pour chaque page. Nous connectons le signal currentRowChanged(int) du widget liste setCurrentIndex(int) de la disposition empile pour implmenter le changement de page, puis nous appelons setCurrentRow() sur le widget liste la n du constructeur pour commencer la page 0. Ce genre de formulaire est aussi trs facile crer avec le Qt Designer : 1. crez un nouveau formulaire en vous basant sur les modles "Dialog" ou "Widget" ; 2. ajoutez un QListWidget et un QStackedWidget au formulaire ; 3. remplissez chaque page avec des widgets enfants et des dispositions ; (Pour crer une nouvelle page, cliquez du bouton droit et slectionnez Insert Page ; pour changer de page, cliquez sur la petite che gauche ou droite situe en haut droite du QStackedWidget) 4. disposez les widgets cte cte grce une disposition horizontale ; 5. connectez le signal currentRowChanged(int) du widget liste au slot setCurrentIndex(int) du widget empil ; 6. dnissez la valeur de la proprit currentRow du widget liste en 0. Etant donn que nous avons implment le changement de page en utilisant des signaux et des slots prdnis, la bote de dialogue prsentera le bon comportement quand elle sera prvisualise dans le Qt Designer.

152

Qt4 et C++ : Programmation dinterfaces GUI

Sparateurs
QSplitter est un widget qui comporte dautres widgets. Les widgets dans un sparateur sont spars par des poignes. Les utilisateurs peuvent changer les tailles des widgets enfants du sparateur en faisant glisser ces poignes. Les sparateurs peuvent souvent tre utiliss comme une alternative aux gestionnaires de disposition, pour accorder davantage de contrle lutilisateur.
Figure 6.7 Lapplication Splitter

Les widgets enfants dun QSplitter sont automatiquement placs cte cte (ou un endessous de lautre) dans lordre dans lequel ils sont crs, avec des barres de sparation entre les widgets adjacents. Voici le code permettant de crer la fentre illustre en Figure 6.7 :
int main(int argc, char *argv[]) { QApplication app(argc, argv); QTextEdit *editor1 = new QTextEdit; QTextEdit *editor2 = new QTextEdit; QTextEdit *editor3 = new QTextEdit; QSplitter splitter(Qt::Horizontal); splitter.addWidget(editor1); splitter.addWidget(editor2); splitter.addWidget(editor3); ... splitter.show(); return app.exec(); }

Lexemple est constitu de trois QTextEdit disposs horizontalement par un widget QSplitter. Contrairement aux gestionnaires de disposition qui se contentent dorganiser les widgets enfants dun formulaire et ne proposent aucune reprsentation visuelle, QSplitter hrite de QWidget et peut tre utilis comme nimporte quel autre widget. Vous obtenez des dispositions complexes en imbriquant des QSplitter horizontaux et verticaux. Par exemple, lapplication Mail Client prsente en Figure 6.9 consiste en un QSplitter horizontal qui contient un QSplitter vertical sur sa droite.

Chapitre 6

Gestion des dispositions

153

Figure 6.8 Les widgets de lapplication Splitter

Titre de fentre
QSplitter QTextEdit QTextEdit QTextEdit

Figure 6.9 Lapplication Mail Client sous Mac OS X

Voici le code dans le constructeur de la sous-classe QMainWindow de lapplication Mail Client :


MailClient::MailClient() { ... rightSplitter = new QSplitter(Qt::Vertical); rightSplitter->addWidget(messagesTreeWidget); rightSplitter->addWidget(textEdit); rightSplitter->setStretchFactor(1, 1); mainSplitter = new QSplitter(Qt::Horizontal); mainSplitter->addWidget(foldersTreeWidget); mainSplitter->addWidget(rightSplitter); mainSplitter->setStretchFactor(1, 1); setCentralWidget(mainSplitter); setWindowTitle(tr("Mail Client")); readSettings(); }

Aprs avoir cr les trois widgets que nous voulons afcher, nous crons un sparateur vertical, rightSplitter, et nous ajoutons les deux widgets dont nous avons besoin sur la droite.

154

Qt4 et C++ : Programmation dinterfaces GUI

Nous crons ensuite un sparateur horizontal, mainSplitter, et nous ajoutons le widget que nous voulons quil afche sur la gauche. Nous crons aussi rightSplitter dont les widgets doivent safcher droite. mainSplitter devient le widget central de QMainWindow. Quand lutilisateur redimensionne une fentre, QSplitter distribue normalement lespace de sorte que les tailles relatives des widgets enfants restent les mmes. Dans lexemple Mail Client, nous ne souhaitons pas ce comportement ; nous voulons plutt que QTreeWidget et QTableWidget conservent leurs dimensions et nous voulons attribuer tout espace supplmentaire QTextEdit (voir Figure 6.10). Nous y parvenons grce aux deux appels de setStretchFactor(). Le premier argument est lindex de base zro du widget enfant du sparateur et le second argument est le facteur dtirement que nous dsirons dnir ; la valeur par dfaut est gale 0.
Figure 6.10 Index du sparateur de lapplication Mail Client
mainSplitter

0
foldersTreeWidget

1
messagesTableWidget

0
rightSplitter

textEdit

Le premier appel de setStretchFactor() est effectu sur rightSplitter et dnit le widget la position 1 (textEdit) pour avoir un facteur dtirement de 1. Le deuxime appel de setStretchFactor() se fait sur mainSplitter et xe le widget la position 1 (rightSplitter) pour obtenir un facteur dtirement de 1. Ceci garantit que tout espace supplmentaire disponible reviendra textEdit. Quand lapplication est lance, QSplitter attribue aux widgets enfants des tailles appropries en fonction de leurs dimensions initiales (ou en fonction de leur taille requise si la dimension initiale nest pas spcie). Nous pouvons grer le dplacement des poignes du sparateur dans le code en appelant QSplitter::setSizes(). La classe QSplitter procure galement un moyen de sauvegarder et restaurer son tat la prochaine fois que lapplication est excute. Voici la fonction writeSettings() qui enregistre les paramtres de Mail Client :
void MailClient::writeSettings() { QSettings settings("Software Inc.", "Mail Client"); settings.beginGroup("mainWindow"); settings.setValue("size", size()); settings.setValue("mainSplitter", mainSplitter->saveState()); settings.setValue("rightSplitter", rightSplitter->saveState()); settings.endGroup(); }

Chapitre 6

Gestion des dispositions

155

Voil la fonction readSettings() correspondante :


void MailClient::readSettings() { QSettings settings("Software Inc.", "Mail Client"); settings.beginGroup("mainWindow"); resize(settings.value("size", QSize(480, 360)).toSize()); mainSplitter->restoreState( settings.value("mainSplitter").toByteArray()); rightSplitter->restoreState( settings.value("rightSplitter").toByteArray()); settings.endGroup(); }

QSplitter est totalement pris en charge par le Qt Designer. Pour placer des widgets dans un sparateur, positionnez les widgets enfants plus ou moins leurs emplacements, slectionnez-les et cliquez sur Form > Lay Out Horizontally in Splitter ou Form > Lay Out Vertically in Splitter.

Zones droulantes
La classe QScrollArea propose une fentre dafchage droulante et deux barres de dlement, comme le montre la Figure 6.11. Si vous voulez ajouter des barres de dlement un widget, le plus simple est dutiliser un QScrollArea au lieu dinstancier vos propres QScrollBar et dimplmenter la fonctionnalit droulante vous-mme.
verticalScrollBar()

Figure 6.11 Les widgets qui constituent QScrollArea


viewport()

horizontalScrollBar()

Pour se servir de QScrollArea, il faut appeler setWidget() avec le widget auquel vous souhaitez ajouter des barres de dlement. QScrollArea reparente automatiquement le widget pour quil devienne un enfant de la fentre dafchage (accessible via QScrollArea::viewport()) si ce nest pas encore le cas. Par exemple, si vous voulez des barres de dlement autour du widget IconEditor dvelopp au Chapitre 5, vous avez la possibilit dcrire ceci :
int main(int argc, char *argv[]) { QApplication app(argc, argv);

156

Qt4 et C++ : Programmation dinterfaces GUI

IconEditor *iconEditor = new IconEditor; iconEditor->setIconImage(QImage(":/images/mouse.png")); QScrollArea scrollArea; scrollArea.setWidget(iconEditor); scrollArea.viewport()->setBackgroundRole(QPalette::Dark); scrollArea.viewport()->setAutoFillBackground(true); scrollArea.setWindowTitle(QObject::tr("Icon Editor")); scrollArea.show(); return app.exec(); }

QScrollArea prsente le widget dans sa taille actuelle ou utilise la taille requise si le widget na pas encore t redimensionn. En appelant setWidgetResizable(true), vous pouvez dire QScrollArea de redimensionner automatiquement le widget pour proter de tout espace supplmentaire au-del de sa taille requise. Par dfaut, les barres de dlement ne sont afches que lorsque la fentre dafchage est plus petite que le widget enfant. Nous pouvons obliger les barres de dlement tre toujours visibles en congurant les stratgies de barre de dlement :
scrollArea.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); scrollArea.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);

Figure 6.12 Redimensionner un QScrollArea

QScrollArea hrite la majorit de ses fonctionnalits de QAbstractScrollArea. Des classes comme QTextEdit et QAbstractItemView (la classe de base des classes dafchage dlments de Qt) drivent de QAbstractScrollArea, nous navons donc pas les encadrer dans un QScrollArea pour obtenir des barres de dlement.

Chapitre 6

Gestion des dispositions

157

Widgets et barres doutils ancrables


Les widgets ancrables sont des widgets qui peuvent tre ancrs dans un QMainWindow ou rester ottants comme des fentres indpendantes. QMainWindow propose quatre zones daccueil pour les widgets ancrables : une en dessous, une au-dessus, une gauche et une droite du widget central. Des applications telles que Microsoft Visual Studio et Qt Linguist utilisent normment les fentres ancrables pour offrir une interface utilisateur trs exible. Dans Qt, les widgets ancrables sont des instances de QDockWidget. Chaque widget ancrable possde sa propre barre de titre, mme sil est ancr (voir Figure 6.13). Les utilisateurs peuvent dplacer les fentres ancrables dune zone une autre en faisant glisser la barre de titre. Ils peuvent aussi dtacher une fentre ancre dune zone et en faire une fentre ottante indpendante en la faisant glisser en dehors de tout point dancrage. Les fentres ancrables sont toujours afches "au-dessus" de leur fentre principale lorsquelles sont ottantes. Les utilisateurs peuvent fermer QDockWidget en cliquant sur le bouton de fermeture dans la barre de titre du widget. Toute combinaison de ces fonctions peut tre dsactive en appelant QDockWidget::setFeatures().
Figure 6.13 QMainWindow avec un widget ancrable

Dans les versions antrieures de Qt, les barres doutils taient considres comme des widgets ancrables et partageaient les mmes points dancrage. Avec Qt 4, les barres doutils occupent leurs propres zones autour du widget central (comme illustr en Figure 6.14) et ne peuvent pas tre dtaches. Si une barre doutils ottante savre ncessaire, nous pouvons simplement la placer dans un QDockWindow.

158

Qt4 et C++ : Programmation dinterfaces GUI

Figure 6.14 Les points dancrage et les zones de barres doutils de QMainWindow
Zone gauche de barre d'outils

Titre de la fentre Barre de menus Zone suprieure de barre d'outils Point d'ancrage suprieur

Point d'ancrage infrieur Zone infrieure de barre d'outils Barre d'tat

Les coins matrialiss avec des lignes pointilles peuvent appartenir lun des deux points dancrage adjacents. Par exemple, nous pourrions instaurer que le coin suprieur gauche appartient la zone dancrage gauche en appelant QMainWindow::setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea). Lextrait de code suivant montre comment encadrer un widget existant (dans ce cas, QTreeWidget) dans un QDockWidget et comment linsrer dans le point dancrage droit :
QDockWidget *shapesDockWidget = new QDockWidget(tr("Shapes")); shapesDockWidget->setWidget(treeWidget); shapesDockWidget->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); addDockWidget(Qt::RightDockWidgetArea, shapesDockWidget);

Lappel de setAllowedAreas() spcie les contraintes selon lesquelles les points dancrage peuvent accepter la fentre ancrable. Ici, nous autorisons uniquement lutilisateur faire glisser le widget ancrable vers les zones gauche et droite, o il y a sufsamment despace vertical pour quil safche convenablement. Si aucune zone autorise nest explicitement spcie, lutilisateur a la possibilit de faire glisser ce widget vers lun des quatre points dancrage. Voici comment crer une barre doutils contenant un QComboBox, un QSpinBox et quelques QToolButton dans le constructeur dune sous-classe de QMainWindow:
QToolBar *fontToolBar = new QToolBar(tr("Font")); fontToolBar->addWidget(familyComboBox); fontToolBar->addWidget(sizeSpinBox); fontToolBar->addAction(boldAction); fontToolBar->addAction(italicAction);

Zone droite de barre d'outils

Point d'ancrage gauche

Point d'ancrage droit

Chapitre 6

Gestion des dispositions

159

fontToolBar->addAction(underlineAction); fontToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); addToolBar(fontToolBar);

Si nous voulons sauvegarder la position de tous les widgets ancrables et barres doutils de manire pouvoir les restaurer la prochaine fois que lapplication sera excute, nous pouvons crire un code similaire celui utilis pour enregistrer ltat dun QSplitter laide des fonctions saveState() et restoreState() de QMainWindow:
void MainWindow::writeSettings() { QSettings settings("Software Inc.", "Icon Editor"); settings.beginGroup("mainWindow"); settings.setValue("size", size()); settings.setValue("state", saveState()); settings.endGroup(); } void MainWindow::readSettings() { QSettings settings("Software Inc.", "Icon Editor"); settings.beginGroup("mainWindow"); resize(settings.value("size").toSize()); restoreState(settings.value("state").toByteArray()); settings.endGroup(); }

Enn, QMainWindow propose un menu contextuel qui rpertorie toutes les fentres ancrables et toutes les barres doutils, comme illustr en Figure 6.15. Lutilisateur peut fermer et restaurer ces fentres et masquer et restaurer des barres doutils par le biais de ce menu.
Figure 6.15 Le menu contextuel de QMainWindow

MDI (Multiple Document Interface)


Les applications qui proposent plusieurs documents dans la zone centrale de la fentre principale sont appeles des applications MDI (multiple document interface). Dans Qt, une application MDI est cre en utilisant la classe QWorkspace comme widget central et en faisant de chaque fentre de document un enfant de QWorkspace.

160

Qt4 et C++ : Programmation dinterfaces GUI

Les applications MDI fournissent gnralement un menu Fentre (Window) partir duquel vous grez les fentres et les listes de fentres. La fentre active est identie par une coche. Lutilisateur peut activer nimporte quelle fentre en cliquant sur son entre dans ce menu. Dans cette section, nous dvelopperons lapplication MDI Editor prsente en Figure 6.16 pour vous montrer comment crer une application MDI et comment implmenter son menu Window.
Figure 6.16 Lapplication MDI Editor

Lapplication comprend deux classes : MainWindow et Editor. Le code se trouve sur le site www.pearson.fr, la page ddie cet ouvrage, et vu quune grande partie de celui-ci est identique ou similaire lapplication Spreadsheet de la Partie I, nous ne prsenterons que les parties nouvelles.
Figure 6.17 Les menus de lapplication MDI Editor

Chapitre 6

Gestion des dispositions

161

Commenons par la classe MainWindow.


MainWindow::MainWindow() { workspace = new QWorkspace; setCentralWidget(workspace); connect(workspace, SIGNAL(windowActivated(QWidget *)), this, SLOT(updateMenus())); createActions(); createMenus(); createToolBars(); createStatusBar(); setWindowTitle(tr("MDI Editor")); setWindowIcon(QPixmap(":/images/icon.png")); }

Dans le constructeur MainWindow, nous crons un widget QWorkspace qui devient le widget central. Nous connectons le signal windowActivated() de QWorkspace au slot que nous voulons utiliser pour conserver le menu Window jour.
void MainWindow::newFile() { Editor *editor = createEditor(); editor->newFile(); editor->show(); }

Le slot newFile() correspond loption File > New. Il dpend de la fonction prive createEditor() pour crer un widget enfant Editor.
Editor *MainWindow::createEditor() { Editor *editor = new Editor; connect(editor, SIGNAL(copyAvailable(bool)), cutAction, SLOT(setEnabled(bool))); connect(editor, SIGNAL(copyAvailable(bool)), copyAction, SLOT(setEnabled(bool))); workspace->addWindow(editor); windowMenu->addAction(editor->windowMenuAction()); windowActionGroup->addAction(editor->windowMenuAction()); return editor; }

La fonction createEditor() cre un widget Editor et tablit deux connexions signal-slot. Ces connexions garantissent que Edit > Cut et Edit > Copy sont activs ou dsactivs selon que du texte est slectionn ou non.

162

Qt4 et C++ : Programmation dinterfaces GUI

Avec MDI, il est possible que plusieurs widgets Editor soient utiliss. Cest un souci parce que nous ne voulons rpondre quau signal copyAvailable(bool) de la fentre Editor active et pas aux autres. Cependant, ces signaux ne peuvent tre mis que par la fentre active, ce nest donc pas un problme en pratique. Lorsque nous avons congur Editor, nous avons ajout un QAction reprsentant la fentre au menu Window. Laction est fournie par la classe Editor que nous tudierons plus loin. Nous ajoutons galement laction un objet QActionGroup. Avec QActionGroup, nous sommes srs quun seul lment du menu Window est coch la fois.
void MainWindow::open() { Editor *editor = createEditor(); if (editor->open()) { editor->show(); } else { editor->close(); } }

La fonction open() correspond File > Open. Elle cre un Editor pour le nouveau document et appelle open() sur ce dernier. Il est prfrable dimplmenter des oprations de chier dans la classe Editor que dans la classe MainWindow, parce que chaque Editor a besoin dassurer le suivi de son propre tat indpendant. Si open() choue, nous fermons simplement lditeur parce que lutilisateur aura dj t inform de lerreur. Nous navons pas supprimer explicitement lobjet Editor nous-mmes ; cest fait automatiquement par Editor par le biais de lattribut Qt::WA_DeleteOnClose, qui est dni dans le constructeur de Editor.
void MainWindow::save() { if (activeEditor()) activeEditor()->save(); }

Le slot save() invoque Editor::save() sur lditeur actif, sil y en a un. Une fois encore, le code qui accomplit le vritable travail se situe dans la classe Editor.
Editor *MainWindow::activeEditor() { return qobject_cast<Editor *>(workspace->activeWindow()); }

La fonction prive activeEditor() retourne la fentre enfant active sous la forme dun pointeur de Editor, ou dun pointeur nul sil ny en a pas.
void MainWindow::cut() { if (activeEditor()) activeEditor()->cut(); }

Chapitre 6

Gestion des dispositions

163

Le slot cut() invoque Editor::cut() sur lditeur actif. Nous ne montrons pas les slots copy() et paste() puisquils suivent le mme modle.
void MainWindow::updateMenus() { bool hasEditor = (activeEditor()!= 0); bool hasSelection = activeEditor() && activeEditor()->textCursor().hasSelection(); saveAction->setEnabled(hasEditor); saveAsAction->setEnabled(hasEditor); pasteAction->setEnabled(hasEditor); cutAction->setEnabled(hasSelection); copyAction->setEnabled(hasSelection); closeAction->setEnabled(hasEditor); closeAllAction->setEnabled(hasEditor); tileAction->setEnabled(hasEditor); cascadeAction->setEnabled(hasEditor); nextAction->setEnabled(hasEditor); previousAction->setEnabled(hasEditor); separatorAction->setVisible(hasEditor); if (activeEditor()) activeEditor()->windowMenuAction()->setChecked(true); }

Le slot updateMenus() est invoqu ds quune fentre est active (et quand la dernire fentre est ferme) pour mettre jour le systme de menus, en raison de la connexion signal-slot dans le constructeur de MainWindow. La plupart des options de menu ne sont intressantes que si une fentre est active, nous les dsactivons donc quand ce nest pas le cas. Nous terminons en appelant setChecked() sur QAction reprsentant la fentre active. Grce QActionGroup, nous navons pas besoin de dcocher explicitement la fentre active prcdente.
void MainWindow::createMenus() { ... windowMenu = menuBar()->addMenu(tr("&Window")); windowMenu->addAction(closeAction); windowMenu->addAction(closeAllAction); windowMenu->addSeparator(); windowMenu->addAction(tileAction); windowMenu->addAction(cascadeAction); windowMenu->addSeparator(); windowMenu->addAction(nextAction); windowMenu->addAction(previousAction); windowMenu->addAction(separatorAction); ... }

164

Qt4 et C++ : Programmation dinterfaces GUI

La fonction prive createMenus() introduit des actions dans le menu Window. Les actions sont toutes typiques de tels menus et sont implmentes facilement laide des slots closeActiveWindow(), closeAllWindows(), tile() et cascade() de QWorkspace. A chaque fois que lutilisateur ouvre une nouvelle fentre, elle est ajoute la liste dactions du menu Window. (Ceci est effectu dans la fonction createEditor() tudie prcdemment.) Ds que lutilisateur ferme une fentre dditeur, son action dans le menu Window est supprime (tant donn que laction appartient la fentre dditeur), et elle disparat de ce menu.
void MainWindow::closeEvent(QCloseEvent *event) { workspace->closeAllWindows(); if (activeEditor()) { event->ignore(); } else { event->accept(); } }

La fonction closeEvent() est rimplmente pour fermer toutes les fentres enfants, chaque enfant reoit donc un vnement close. Si lun des widgets enfants "ignore" cet vnement (parce que lutilisateur a annul une bote de message "modications non enregistres"), nous ignorons lvnement close pour MainWindow; sinon, nous lacceptons, ce qui a pour consquence de fermer la fentre complte. Si nous navions pas rimplment closeEvent() dans MainWindow, lutilisateur naurait pas eu la possibilit de sauvegarder des modications non enregistres. Nous avons termin notre analyse de MainWindow, nous pouvons donc passer limplmentation dEditor. La classe Editor reprsente une fentre enfant. Elle hrite de QTextEdit qui propose une fonctionnalit de modication de texte. Tout comme nimporte quel widget Qt peut tre employ comme une fentre autonome, tout widget Qt peut tre utilis comme une fentre enfant dans un espace de travail MDI. Voici la dnition de classe :
class Editor: public QTextEdit { Q_OBJECT public: Editor(QWidget *parent = 0); void newFile(); bool open(); bool openFile(const QString &fileName); bool save(); bool saveAs(); QSize sizeHint() const; QAction *windowMenuAction() const { return action; }

Chapitre 6

Gestion des dispositions

165

protected: void closeEvent(QCloseEvent *event); private slots: void documentWasModified(); private: bool okToContinue(); bool saveFile(const QString &fileName); void setCurrentFile(const QString &fileName); bool readFile(const QString &fileName); bool writeFile(const QString &fileName); QString strippedName(const QString &fullFileName); QString curFile; bool isUntitled; QString fileFilters; QAction *action; };

Quatre des fonctions prives qui se trouvaient dans la classe MainWindow de lapplication Spreadsheet sont galement prsentes dans la classe Editor: okToContinue(), saveFile(), setCurrentFile() et strippedName().
Editor::Editor(QWidget *parent) : QTextEdit(parent) { action = new QAction(this); action->setCheckable(true); connect(action, SIGNAL(triggered()), this, SLOT(show())); connect(action, SIGNAL(triggered()), this, SLOT(setFocus())); isUntitled = true; fileFilters = tr("Text files (*.txt)\n" "All files (*)"); connect(document(), SIGNAL(contentsChanged()), this, SLOT(documentWasModified())); setWindowIcon(QPixmap(":/images/document.png")); setAttribute(Qt::WA_DeleteOnClose); }

Nous crons dabord un QAction reprsentant lditeur dans le menu Window de lapplication et nous connectons cette action aux slots show() et setFocus(). Etant donn que nous autorisons les utilisateurs crer autant de fentres dditeurs quils le souhaitent, nous devons prendre certaines dispositions concernant leur dnomination, dans le but de faire une distinction avant le premier enregistrement. Un moyen courant de grer cette situation est dattribuer des noms qui incluent un chiffre (par exemple, document1.txt). Nous utilisons la variable isUntitled pour faire une distinction entre les noms fournis par lutilisateur et les noms crs par programme.

166

Qt4 et C++ : Programmation dinterfaces GUI

Nous connectons le signal contentsChanged() du document au slot priv documentWasModified(). Ce slot appelle simplement setWindowModified(true). Enn, nous dnissons lattribut Qt::WA_DeleteOnClose pour viter toute fuite de mmoire quand lutilisateur ferme une fentre Editor. Aprs le constructeur, il est logique dappeler newFile() ou open().
void Editor::newFile() { static int documentNumber = 1; curFile = tr("document%1.txt").arg(documentNumber); setWindowTitle(curFile + "[*]"); action->setText(curFile); isUntitled = true; ++documentNumber; }

La fonction newFile() gnre un nom au format document1.txt pour le nouveau document. Ce code est plac dans newFile() plutt que dans le constructeur, parce quil ny a aucun intrt numroter quand nous invoquons open() pour ouvrir un document existant dans un Editor nouvellement cr. documentNumber tant dclar statique, il est partag par toutes les instances dEditor. Le symbole "[*]" dans le titre de la fentre rserve lemplacement de lastrisque qui doit apparatre quand le chier contient des modications non sauvegardes sur des plates-formes autres que Mac OS X. Nous avons parl de ce symbole dans le Chapitre 3.
bool Editor::open() { QString fileName = QFileDialog::getOpenFileName(this, tr("Open"), ".", fileFilters); if (fileName.isEmpty()) return false; return openFile(fileName); }

La fonction open() essaie douvrir un chier existant avec openFile().


bool Editor::save() { if (isUntitled) { return saveAs(); } else { return saveFile(curFile); } }

Chapitre 6

Gestion des dispositions

167

La fonction save() sappuie sur la variable isUntitled pour dterminer si elle doit appeler saveFile() ou saveAs().
void Editor::closeEvent(QCloseEvent *event) { if (okToContinue()) { event->accept(); } else { event->ignore(); } }

La fonction closeEvent() est rimplmente pour permettre lutilisateur de sauvegarder des modications non enregistres. La logique est code dans la fonction okToContinue() qui ouvre une bote de message demandant, "Voulez-vous enregistrer vos modications ?" Si okToContinue() retourne true, nous acceptons lvnement close; sinon, nous "lignorons" et la fentre nen sera pas affecte.
void Editor::setCurrentFile(const QString &fileName) { curFile = fileName; isUntitled = false; action->setText(strippedName(curFile)); document()->setModified(false); setWindowTitle(strippedName(curFile) + "[*]"); setWindowModified(false); }

La fonction setCurrentFile() est appele dans openFile() et saveFile() pour mettre jour les variables curFile et isUntitled, pour dnir le titre de la fentre et le texte de laction et pour congurer lindicateur "modi?ed" du document en false. Ds que lutilisateur modie le texte dans lditeur, le QTextDocument sous-jacent met le signal contentsChanged() et dnit son indicateur "modied" interne en true.
QSize Editor::sizeHint() const { return QSize(72 * fontMetrics().width(x), 25 * fontMetrics().lineSpacing()); }

La fonction sizeHint() retourne une taille en fonction de la largeur de la lettre "x" et de la hauteur de la ligne de texte. QWorkspace se sert de la taille requise pour attribuer une dimension initiale la fentre. Voici le chier main.cpp de lapplication MDI Editor :
#include <QApplication> #include "mainwindow.h"

168

Qt4 et C++ : Programmation dinterfaces GUI

int main(int argc, char *argv[]) { QApplication app(argc, argv); QStringList args = app.arguments(); MainWindow mainWin; if (args.count() > 1) { for (int i = 1; i < args.count(); ++i) mainWin.openFile(args[i]); } else { mainWin.newFile(); } mainWin.show(); return app.exec(); }

Si lutilisateur spcie des chiers dans la ligne de commande, nous tentons de les charger. Sinon, nous dmarrons avec un document vide. Les options de ligne de commande spciques Qt, comme -style et -font, sont automatiquement supprimes de la liste darguments par le constructeur QApplication. Donc si nous crivons :
mdieditor -style motif readme.txt

dans la ligne de commande, QApplication::arguments() retourne un QStringList contenant deux lments ("mdieditor" et "readme.txt") et lapplication MDI Editor dmarre avec le document readme.txt. MDI est un moyen de grer simultanment plusieurs documents. Sous Mac OS X, la meilleure approche consiste utiliser plusieurs fentres de haut niveau. Cette technique est traite dans la section "Documents multiples" du Chapitre 3.

7
Traitement des vnements
Au sommaire de ce chapitre Rimplmenter les gestionnaires dvnements Installer des ltres dvnements Rester ractif pendant un traitement intensif

Les vnements sont dclenchs par le systme de fentrage ou par Qt en rponse diverses circonstances. Quand lutilisateur enfonce ou relche une touche ou un bouton de la souris, un vnement key ou mouse est dclench ; lorsquune fentre safche pour la premire fois, un vnement paint est gnr pour informer la fentre nouvellement afche quelle doit se redessiner. La plupart des vnements sont dclenchs en rponse des actions utilisateur, mais certains, comme les vnements timer, sont dclenchs indpendamment par le systme.

170

Qt4 et C++ : Programmation dinterfaces GUI

Quand nous programmons avec Qt, nous avons rarement besoin de penser aux vnements, parce que les widgets Qt mettent des signaux lorsque quelque chose de signicatif se produit. Les vnements deviennent utiles quand nous crivons nos propres widgets personnaliss ou quand nous voulons modier le comportement des widgets Qt existants. Il ne faut pas confondre vnements et signaux. En rgle gnrale, les signaux savrent utiles lors de lemploi dun widget, alors que les vnements prsentent une utilit au moment de limplmentation dun widget. Par exemple, quand nous utilisons QPushButton, nous nous intressons plus son signal clicked() quaux vnements mouse ou key de bas niveau qui provoquent lmission du signal. Cependant, si nous implmentons une classe telle que QPushButton, nous devons crire un code qui grera les vnements mouse et key et qui mettra le signal clicked() si ncessaire.

Rimplmenter les gestionnaires dvnements


Dans Qt, un vnement est un objet qui hrite de QEvent. Qt gre plus dune centaine de types dvnements, chacun deux tant identi par une valeur dnumration. Par exemple, QEvent::type() retourne QEvent::MouseButtonPress pour les vnements "bouton souris enfonc". De nombreux types dvnements exigent plus dinformations que ce qui peut tre stock dans un objet QEvent ordinaire ; par exemple, les vnements "bouton souris enfonc" doivent stocker quel bouton de la souris a dclench lvnement et lendroit o le pointeur de la souris se trouvait quand lvnement sest dclench. Ces informations supplmentaires sont conserves dans des sous-classes QEvent spciales, comme QMouseEvent. Les vnements sont notis aux objets par le biais de leur fonction event(), hrite de QObject. Limplmentation de event() dans QWidget transmet les types les plus courants dvnements des gestionnaires dvnements spciques, tels que mousePressEvent(), keyPressEvent() et paintEvent(). Nous avons dj tudi plusieurs gestionnaires dvnements lorsque nous avons implment MainWindow, IconEditor et Plotter dans les chapitres prcdents. Il existe beaucoup dautres types dvnements rpertoris dans la documentation de rfrence de QEvent, et il est aussi possible de crer des types dvnements personnaliss et denvoyer les vnements soi-mme. Dans notre cas, nous analyserons deux types courants dvnements qui mritent davantage dexplications : les vnements key et timer. Les vnements key sont grs en rimplmentant keyPressEvent() et keyReleaseEvent(). Le widget Plotter rimplmente keyPressEvent(). Normalement, nous ne devons rimplmenter que keyPressEvent() puisque les seules touches pour lesquelles il faut contrler quelles ont t relches sont les touches de modication Ctrl, Maj et Alt, et vous pouvez contrler leur tat dans un keyPressEvent() en utilisant QKeyEvent::modifiers().

Chapitre 7

Traitement des vnements

171

Par exemple, si nous implmentions un widget CodeEditor, voici le code de sa fonction keyPressEvent() qui devra interprter diffremment Home et Ctrl+Home:
void CodeEditor::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key_Home: if (event->modifiers() & Qt::ControlModifier) { goToBeginningOfDocument(); } else { goToBeginningOfLine(); } break; case Qt::Key_End: ... default: QWidget::keyPressEvent(event); } }

Les touches de tabulation et de tabulation arrire (Maj+Tab) sont des cas particuliers. Elles sont gres par QWidget::event() avant lappel de keyPressEvent(), avec la consigne de transmettre le focus au widget suivant ou prcdent dans lordre de la chane de focus. Ce comportement correspond habituellement ce que nous recherchons, mais dans un widget CodeEditor, nous prfrerions que la touche Tab produise le dcalage dune ligne par rapport la marge. Voici comment event() pourrait tre rimplment :
bool CodeEditor::event(QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); if (keyEvent->key() == Qt::Key_Tab) { insertAtCurrentPosition(\t); return true; } } return QWidget::event(event); }

Si lvnement est li une touche sur laquelle lutilisateur a appuy, nous convertissons lobjet QEvent en QKeyEvent et nous vrions quelle touche a t presse. Sil sagit de la touche Tab, nous effectuons un traitement et nous retournons true pour informer Qt que nous avons gr lvnement. Si nous avions retourn false, Qt transmettrait lvnement au widget parent. Une approche plus intelligente pour implmenter les combinaisons de touches consiste se servir de QAction. Par exemple, si goToBeginningOfLine() et goToBeginningOfDocument() sont des slots publics dans le widget CodeEditor, et si CodeEditor fait

172

Qt4 et C++ : Programmation dinterfaces GUI

ofce de widget central dans une classe MainWindow, nous pourrions ajouter des combinaisons de touches avec le code suivant :
MainWindow::MainWindow() { editor = new CodeEditor; setCentralWidget(editor); goToBeginningOfLineAction = new QAction(tr("Go to Beginning of Line"), this); goToBeginningOfLineAction->setShortcut(tr("Home")); connect(goToBeginningOfLineAction, SIGNAL(activated()), editor, SLOT(goToBeginningOfLine())); goToBeginningOfDocumentAction = new QAction(tr("Go to Beginning of Document"), this); goToBeginningOfDocumentAction->setShortcut(tr("Ctrl+Home")); connect(goToBeginningOfDocumentAction, SIGNAL(activated()), editor, SLOT(goToBeginningOfDocument())); ... }

Cela permet dajouter facilement des commandes un menu ou une barre doutils, comme nous lavons vu dans le Chapitre 3. Si les commandes napparaissent pas dans linterface utilisateur, les objets QAction pourraient tre remplacs par un objet QShortcut, la classe employe par QAction en interne pour prendre en charge les combinaisons de touches. Par dfaut, les combinaisons de touches dnies laide de QAction ou QShortcut sur un widget sont actives ds que la fentre contenant le widget est active. Vous pouvez modier ce comportement grce QAction::setShortcutContext() ou QShortcut::setContext(). Lautre type courant dvnement est lvnement timer. Alors que la plupart des autres types dvnements se dclenchent suite une action utilisateur, les vnements timer permettent aux applications deffectuer un traitement intervalles rguliers. Les vnements timer peuvent tre utiliss pour implmenter des curseurs clignotants et dautres animations, ou simplement pour ractualiser lafchage. Pour analyser les vnements timer, nous implmenterons un widget Ticker illustr en Figure 7.1. Ce widget prsente une bannire qui dle dun pixel vers la gauche toutes les 30 millisecondes. Si le widget est plus large que le texte, le texte est rpt autant de fois que ncessaire pour remplir toute la largeur du widget.
Figure 7.1 Le widget Ticker

Voici le chier den-tte :


#ifndef TICKER_H #define TICKER_H

Chapitre 7

Traitement des vnements

173

#include <QWidget> class Ticker: public QWidget { Q_OBJECT Q_PROPERTY(QString text READ text WRITE setText) public: Ticker(QWidget *parent = 0); void setText(const QString &newText); QString text() const { return myText; } QSize sizeHint() const; protected: void paintEvent(QPaintEvent *event); void timerEvent(QTimerEvent *event); void showEvent(QShowEvent *event); void hideEvent(QHideEvent *event); private: QString myText; int offset; int myTimerId; }; #endif

Nous rimplmentons quatre gestionnaires dvnements dans Ticker, dont trois que nous avons dj vus auparavant : timerEvent(), showEvent() et hideEvent(). Analysons prsent limplmentation :
#include <QtGui> #include "ticker.h" Ticker::Ticker(QWidget *parent) : QWidget(parent) { offset = 0; myTimerId = 0; }

Le constructeur initialise la variable offset 0. Les coordonnes x auxquelles le texte est dessin sont calcules partir de la valeur offset. Les ID du timer sont toujours diffrents de zro, nous utilisons donc 0 pour indiquer quaucun timer na t dmarr.
void Ticker::setText(const QString &newText) { myText = newText; update(); updateGeometry(); }

174

Qt4 et C++ : Programmation dinterfaces GUI

La fonction setText() dtermine le texte afcher. Elle invoque update() pour demander le rafrachissement de lafchage et updateGeometry() pour informer tout gestionnaire de disposition responsable du widget Ticker dun changement de taille requise.
QSize Ticker::sizeHint() const { return fontMetrics().size(0, text()); }

La fonction sizeHint() retourne lespace requis par le texte comme tant la taille idale du widget. QWidget::fontMetrics() renvoie un objet QFontMetrics qui peut tre interrog pour obtenir des informations lies la police du widget. Dans ce cas, nous demandons la taille exige par le texte. (Puisque le premier argument de QFontMetrics::size() est un indicateur qui nest pas ncessaire pour les chanes simples, nous transmettons simplement 0.)
void Ticker::paintEvent(QPaintEvent * /* event */) { QPainter painter(this); int textWidth = fontMetrics().width(text()); if (textWidth < 1) return; int x = -offset; while (x < width()) { painter.drawText(x, 0, textWidth, height(), Qt::AlignLeft | Qt::AlignVCenter, text()); x += textWidth; } }

La fonction paintEvent() dessine le texte avec QPainter::drawText(). Elle dtermine la quantit despace horizontal exig par le texte laide de fontMetrics(), puis dessine le texte autant de fois que ncessaire pour remplir toute la largeur du widget, en tenant compte du dcalage offset.
void Ticker::showEvent(QShowEvent * /* event */) { myTimerId = startTimer(30); }

La fonction showEvent() lance un timer. Lappel de QObject::startTimer() retourne un ID, qui peut tre utilis ultrieurement pour identier le timer. QObject prend en charge plusieurs timers indpendants, chacun possdant son propre intervalle de temps. Aprs lappel de startTimer(), Qt dclenche un vnement timer environ toutes les 30 millisecondes ; la prcision dpend du systme dexploitation sous-jacent. Nous aurions pu appeler startTimer() dans le constructeur de Ticker, mais nous conomisons des ressources en ne dclenchant des vnements timer que lorsque le widget est visible.
void Ticker::timerEvent(QTimerEvent *event)

Chapitre 7

Traitement des vnements

175

{ if (event->timerId() == myTimerId) { ++offset; if (offset >= fontMetrics().width(text())) offset = 0; scroll(-1, 0); } else { QWidget::timerEvent(event); } }

La fonction timerEvent() est invoque intervalles rguliers par le systme. Elle incrmente offset de 1 pour simuler un mouvement, encadrant la largeur du texte. Puis elle fait dler le contenu du widget dun pixel vers la gauche grce QWidget::scroll(). Nous aurions pu simplement appeler update() au lieu de scroll(), mais scroll() est plus efcace, parce quelle dplace simplement les pixels existants lcran et ne dclenche un vnement paint que pour la zone nouvellement afche du widget (une bande dun pixel de large dans ce cas). Si lvnement timer ne correspond pas au timer qui nous intresse, nous le transmettons notre classe de base.
void Ticker::hideEvent(QHideEvent * /* event */) { killTimer(myTimerId); }

La fonction hideEvent() invoque QObject::killTimer() pour arrter le timer. Les vnements timer sont de bas niveau, et si nous avons besoin de plusieurs timers, il peut tre fastidieux dassurer le suivi de tous les ID de timer. Dans de telles situations, il est gnralement plus facile de crer un objet QTimer pour chaque timer. QTimer met le signal timeout() chaque intervalle de temps. QTimer propose aussi une interface pratique pour les timers usage unique (les timers qui ne chronomtrent quune seule fois).

Installer des ltres dvnements


Lune des fonctionnalits vraiment puissante du modle dvnement de Qt est quune instance de QObject peut tre congure de manire contrler les vnements dune autre instance de QObject avant mme que cette dernire ne les dtecte. Supposons que nous avons un widget CustomerInfoDialog compos de plusieurs QLineEdit et que nous voulons utiliser la barre despace pour activer le prochain QLineEdit. Ce comportement inhabituel peut se rvler appropri pour une application interne laquelle les utilisateurs sont forms. Une solution simple consiste driver QLineEdit et rimplmenter keyPressEvent() pour appeler focusNextChild(), comme dans le code suivant :
void MyLineEdit::keyPressEvent(QKeyEvent *event) {

176

Qt4 et C++ : Programmation dinterfaces GUI

if (event->key() == Qt::Key_Space) { focusNextChild(); } else { QLineEdit::keyPressEvent(event); } }

Cette approche prsente un inconvnient de taille : si nous utilisons plusieurs types de widgets dans le formulaire (par exemple, QComboBoxes et QSpinBoxes), nous devons galement les driver pour quils afchent le mme comportement. Il existe une meilleure solution : CustomerInfoDialog contrle les vnements "bouton souris enfonc" de ses widgets enfants et implmente le comportement ncessaire dans le code de contrle. Pour ce faire, vous utiliserez des ltres dvnements. Dnir un ltre dvnement implique deux tapes : 1. enregistrer lobjet contrleur avec lobjet cible en appelant installEventFilter() sur la cible ; 2. grer les vnements de lobjet cible dans la fonction eventFilter() de lobjet contrleur. Le code du constructeur de CustomerInfoDialog constitue un bon endroit pour enregistrer lobjet contrleur :
CustomerInfoDialog::CustomerInfoDialog(QWidget *parent) : QDialog(parent) { ... firstNameEdit->installEventFilter(this); lastNameEdit->installEventFilter(this); cityEdit->installEventFilter(this); phoneNumberEdit->installEventFilter(this); }

Ds que le ltre dvnement est enregistr, les vnements qui sont envoys aux widgets firstNameEdit, lastNameEdit, cityEdit et phoneNumberEdit sont dabord transmis la fonction eventFilter() de CustomerInfoDialog avant dtre envoys vers la destination prvue. Voici la fonction eventFilter() qui reoit les vnements :
bool CustomerInfoDialog::eventFilter(QObject *target, QEvent *event) { if (target == firstNameEdit || target == lastNameEdit || target == cityEdit || target == phoneNumberEdit) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); if (keyEvent->key() == Qt::Key_Space) { focusNextChild(); return true; } } } return QDialog::eventFilter(target, event); }

Chapitre 7

Traitement des vnements

177

Nous vrions tout dabord que le widget cible est un des QLineEdit. Si lvnement est li lenfoncement dune touche, nous le convertissons en QKeyEvent et nous vrions quelle touche a t presse. Si la touche enfonce correspondait la barre despace, nous invoquons focusNextChild() pour activer le prochain widget dans la chane de focus, et nous retournons true pour dire Qt que nous avons gr lvnement. Si nous avions renvoy false, Qt aurait envoy lvnement sa cible prvue, ce qui aurait introduit un espace parasite dans QLineEdit. Si le widget cible nest pas un QLineEdit, ou si lvnement ne rsulte pas de lenfoncement de la barre despace, nous passons le contrle limplmentation de eventFilter() de la classe de base. Le widget cible aurait aussi pu tre un widget que la classe de base, QDialog, est en train de contrler. (Dans Qt 4.1, ce nest pas le cas pour QDialog. Cependant, dautres classes de widgets Qt, comme QScrollArea, surveillent certains de leurs widgets enfants pour diverses raisons.) Qt propose cinq niveaux auxquels des vnements peuvent tre traits et ltrs : 1. Nous pouvons rimplmenter un gestionnaire dvnements spcique. Rimplmenter des gestionnaires dvnements comme mousePressEvent(), keyPressEvent() et paintEvent() est de loin le moyen le plus commun de traiter des vnements. Nous en avons dj vu de nombreux exemples. 2. Nous pouvons rimplmenter QObject::event(). En rimplmentant la fonction event(), nous avons la possibilit de traiter des vnements avant quils natteignent les gestionnaires dvnements spciques. Cette approche est surtout employe pour rednir la signication par dfaut de la touche Tab, comme expliqu prcdemment. Elle est aussi utilise pour grer des types rares dvnements pour lesquels il nexiste aucun gestionnaire dvnements spcique (par exemple, QEvent::HoverEnter). Quand nous rimplmentons event(), nous devons appeler la fonction event() de la classe de base pour grer les cas que nous ne grons pas explicitement. 3. Nous pouvons installer un ltre dvnement sur un seul QObject. Lorsquun objet a t enregistr avec installEventFilter(), tous les vnements pour lobjet cible sont dabord envoys la fonction eventFilter() de lobjet contrleur. Si plusieurs ltres dvnements sont installs sur le mme objet, les ltres sont activs tour de rle, du plus rcemment install au premier install. 4. Nous pouvons installer un ltre dvnement sur lobjet QApplication. Lorsquun ltre dvnement a t enregistr pour qApp (lunique objet de QApplication), chaque vnement de chaque objet de lapplication est envoy la fonction eventFilter() avant dtre transmis un autre ltre dvnement. Cette technique est trs utile pour le dbogage. Elle peut aussi tre employe pour grer des vnements mouse transmis aux widgets dsactivs que QApplication ignore normalement.

178

Qt4 et C++ : Programmation dinterfaces GUI

5. Nous pouvons driver QApplication et rimplmenter notify(). Qt appelle QApplication::notify() pour envoyer un vnement. Rimplmenter cette fonction est le seul moyen de rcuprer tous les vnements avant quun ltre dvnement quelconque nait lopportunit de les analyser. Les ltres dvnements sont gnralement plus pratiques, parce que le nombre de ltres concomitants nest pas limit alors quil ne peut y avoir quune seule fonction notify(). De nombreux types dvnements, dont les vnements mouse et key, peuvent se propager. Si lvnement na pas t gr lors de son trajet vers son objet cible ou par lobjet cible luimme, tout le traitement de lvnement est rpt, mais cette fois-ci avec comme cible le parent de lobjet cible initial. Ce processus se poursuit, en passant dun parent lautre, jusqu ce que lvnement soit gr ou que lobjet de niveau suprieur soit atteint. La Figure 7.2 vous montre comment un vnement "bouton souris enfonc" est transmis dun enfant vers un parent dans une bote de dialogue. Quand lutilisateur appuie sur une touche, lvnement est dabord envoy au widget actif, dans ce cas le QCheckBox en bas droite. Si le QCheckBox ne gre pas lvnement, Qt lenvoie au QGroupBox et enn lobjet QDialog.
Figure 7.2 Propagation dun vnement dans une bote de dialogue

Window Title QDialog QGroupBox QCheckBox QCheckBox QCheckBox QCheckBox

Rester ractif pendant un traitement intensif


Quand nous appelons QApplication::exec(), nous dmarrons une boucle dvnement de Qt. Qt met quelques vnements au dmarrage pour afcher et dessiner les widgets. Puis, la boucle dvnement est excute, contrlant en permanence si des vnements se sont dclenchs et envoyant ces vnements aux QObject dans lapplication. Pendant quun vnement est trait, des vnements supplmentaires peuvent tre dclenchs et ajouts la le dattente dvnements de Qt. Si nous passons trop de temps traiter un vnement particulier, linterface utilisateur ne rpondra plus. Par exemple, tout vnement dclench par le systme de fentrage pendant que lapplication enregistre un chier sur le disque ne sera pas trait tant que le chier na pas t sauvegard. Pendant lenregistrement,

Chapitre 7

Traitement des vnements

179

lapplication ne rpondra pas aux requtes du systme de fentrage demandant le rafrachissement de lafchage. Une solution consiste utiliser plusieurs threads : un thread pour linterface utilisateur de lapplication et un autre pour accomplir la sauvegarde du chier (ou toute autre opration de longue dure). De cette faon, linterface utilisateur de lapplication continuera rpondre pendant lenregistrement du chier. Nous verrons comment y parvenir dans le Chapitre 18. Une solution plus simple consiste appeler frquemment QApplication::processEvents() dans le code de sauvegarde du chier. Cette fonction demande Qt de traiter tout vnement en attente, puis retourne le contrle lappelant. En fait, QApplication::exec() ne se limite pas une simple boucle while autour dun appel de fonction processEvents(). Voici par exemple comment vous pourriez obtenir de linterface utilisateur quelle reste ractive laide de processEvents(), face au code de sauvegarde de chier de lapplication Spreadsheet (voir Chapitre 4) :
bool Spreadsheet::writeFile(const QString &fileName) { QFile file(fileName); ... for (int row = 0; row < RowCount; ++row) { for (int column = 0; column < ColumnCount; ++column) { QString str = formula(row, column); if (!str.isEmpty()) out << quint16(row) << quint16(column) << str; } qApp->processEvents(); } return true; }

Cette approche prsente un risque : lutilisateur peut fermer la fentre principale alors que lapplication est toujours en train deffectuer la sauvegarde, ou mme cliquer sur File > Save une seconde fois, ce qui provoque un comportement indtermin. La solution la plus simple ce problme est de remplacer
qApp->processEvents();

par
qApp->processEvents(QEventLoop::ExcludeUserInputEvents);

qui demande Qt dignorer les vnements mouse et key. Nous avons souvent besoin dafcher un QProgressDialog alors quune longue opration se produit. QProgressDialog propose une barre de progression qui informe lutilisateur de lavancement de lopration. QProgressDialog propose aussi un bouton Cancel qui permet

180

Qt4 et C++ : Programmation dinterfaces GUI

lutilisateur dannuler lopration. Voici le code permettant denregistrer une feuille de calcul avec cette approche :
bool Spreadsheet::writeFile(const QString &fileName) { QFile file(fileName); ... QProgressDialog progress(this); progress.setLabelText(tr("Saving %1").arg(fileName)); progress.setRange(0, RowCount); progress.setModal(true); for (int row = 0; row < RowCount; ++row) { progress.setValue(row); qApp->processEvents(); if (progress.wasCanceled()) { file.remove(); return false; } for (int column = 0; column < ColumnCount; ++column) { QString str = formula(row, column); if (!str.isEmpty()) out << quint16(row) << quint16(column) << str; } } return true; }

Nous crons un QProgressDialog avec NumRows comme nombre total dtapes. Puis, pour chaque ligne, nous appelons setValue() pour mettre jour la barre de progression. QProgressDialog calcule automatiquement un pourcentage en divisant la valeur actuelle davancement par le nombre total dtapes. Nous invoquons QApplication::processEvents() pour traiter tout vnement de rafrachissement dafchage, tout clic ou toute touche enfonce par lutilisateur (par exemple pour permettre lutilisateur de cliquer sur Cancel). Si lutilisateur clique sur Cancel, nous annulons la sauvegarde et nous supprimons le chier. Nous nappelons pas show() sur QProgressDialog parce que les botes de dialogue de progression le font. Si lopration se rvle plus courte, peut-tre parce que le chier enregistrer est petit ou parce que lordinateur est rapide, QProgressDialog le dtectera et ne safchera pas du tout. En complment du multithread et de lutilisation de QProgressDialog, il existe une manire totalement diffrente de traiter les longues oprations : au lieu daccomplir le traitement la demande de lutilisateur, nous pouvons ajourner ce traitement jusqu ce que lapplication soit inactive. Cette solution peut tre envisage si le traitement peut tre interrompu et repris en toute scurit, parce que nous ne pouvons pas prdire combien de temps lapplication sera inactive.

Chapitre 7

Traitement des vnements

181

Dans Qt, cette approche peut tre implmente en utilisant un timer de 0 milliseconde. Ces timers chronomtrent ds quil ny a pas dvnements en attente. Voici un exemple dimplmentation de timerEvent() qui prsente cette approche :
void Spreadsheet::timerEvent(QTimerEvent *event) { if (event->timerId() == myTimerId) { while (step < MaxStep &&!qApp->hasPendingEvents()) { performStep(step); ++step; } } else { QTableWidget::timerEvent(event); } }

Si hasPendingEvents() retourne true, nous interrompons le traitement et nous redonnons le contrle Qt. Le traitement reprendra quand Qt aura gr tous ses vnements en attente.

8
Graphiques 2D et 3D
Au sommaire de ce chapitre Dessiner avec QPainter Transformations du painter Afchage de haute qualit avec QImage Impression Graphiques avec OpenGL

Les graphiques 2D de Qt se basent sur la classe QPainter. QPainter peut tracer des formes gomtriques (points, lignes, rectangles, ellipses, arcs, cordes, segments, polygones et courbes de Bzier), de mme que des objets pixmaps, des images et du texte. De plus, QPainter prend en charge des fonctionnalits avances, telles que lanticrnelage (pour les bords du texte et des formes), le mlange alpha, le remplissage dgrad et les tracs de vecteur. QPainter supporte aussi les transformations qui permettent de dessiner des graphiques 2D indpendants de la rsolution. QPainter peut galement tre employe pour dessiner sur un "priphrique de dessin" tel quun QWidget, QPixmap ou QImage. Cest utile quand nous crivons des widgets personnaliss ou des classes dlments personnalises avec leurs propres aspect et apparence. Il est aussi possible dutiliser QPainter en association avec QPrinter pour imprimer et gnrer des chiers PDF. Cela signie que nous pouvons souvent nous servir du mme code pour afcher des donnes lcran et pour produire des rapports imprims. Il existe une alternative QPainter: OpenGL. OpenGL est une bibliothque standard permettant de dessiner des graphiques 2D et 3D. Le module QtOpenGL facilite lintgration de code OpenGL dans des applications Qt.

184

Qt4 et C++ : Programmation dinterfaces GUI

Dessiner avec QPainter


Pour commencer dessiner sur un priphrique de dessin (gnralement un widget), nous crons simplement un QPainter et nous transmettons un pointeur au priphrique. Par exemple :
void MyWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); ... }

Nous avons la possibilit de dessiner diffrentes formes laide des fonctions draw...() de QPainter. La Figure 8.1 rpertorie les plus importantes. Les paramtres de QPainter inuencent la faon de dessiner.
Figure 8.1 Les fonctions draw...() de QPainter les plus frquemment utilises
(x1, y1) (x, y) (x2, y2) drawPoint() p2 p3 drawLine() p2 p3 p1 drawPolyline() p2 p3 p4 p2 p3

p1 drawPoints() (x, y)

p4

p1 drawLines() (x, y)

p4

p1 drawPolygon() (x, y)

p4

h w drawRect() (x, y) + w drawArc() (x, y) h

h w drawRoundRect() (x, y) + w drawChord() h

+ w drawEllipse() (x, y) + w drawPie()

(x, y)

Ag
drawText() drawPixmap() drawPath()

Chapitre 8

Graphiques 2D et 3D

185

Certains dentre eux proviennent du priphrique, dautres sont initialiss leurs valeurs par dfaut. Les trois principaux paramtres sont le crayon, le pinceau et la police : Le crayon est utilis pour tracer des lignes et les contours des formes. Il est constitu dune couleur, dune largeur, dun style de trait, de capuchon et de jointure (Figures 8.2 et 8.3). Le pinceau permet de remplir des formes gomtriques. Il est compos normalement dune couleur et dun style, mais peut galement appliquer une texture (un pixmap rpt linni) ou un dgrad (Voir Figure 8.4). La police est utilise pour dessiner le texte. Une police possde de nombreux attributs, dont une famille et une taille.
Figure 8.2 Styles de capuchon et de jointure
FlatCap SquareCap RoundCap

MiterJoin

BevelJoin

RoundJoin

Figure 8.3 Styles de crayon


NoPen SolidLine DashLine DotLine DashDotLine DashDotDotLine

Largeur de trait 3 2

Figure 8.4 Styles prdnis de pinceau


SolidPattern Dense1Pattern Dense2Pattern Dense3Pattern Dense4Pattern

Dense5Pattern

Dense6Pattern

Dense7Pattern

HorPattern

VerPattern

CrossPattern

BDiagPattern

FDiagPattern

DiagCrossPat.

NoBrush

186

Qt4 et C++ : Programmation dinterfaces GUI

Ces paramtres peuvent tre modis tout moment en appelant setPen(), setBrush() et setFont() avec un objet QPen, QBrush ou QFont.
Figure 8.5 Exemples de formes gomtriques

(a) Une ellipse

(b) Un segment

(c) Une courbe de Bzier

Analysons quelques exemples pratiques. Voici le code permettant de dessiner lellipse illustre en Figure 8.5 (a) :
QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(QPen(Qt::black, 12, Qt::DashDotLine, Qt::RoundCap)); painter.setBrush(QBrush(Qt::green, Qt::SolidPattern)); painter.drawEllipse(80, 80, 400, 240);

Lappel de setRenderHint() active lanticrnelage, demandant QPainter dutiliser diverses intensits de couleur sur les bords pour rduire la distorsion visuelle qui se produit habituellement quand les contours dune forme sont convertis en pixels. Les bords sont donc plus homognes sur les plates-formes et les priphriques qui prennent en charge cette fonctionnalit. Voici le code permettant de dessiner le segment illustr en Figure 8.5 (b) :
QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(QPen(Qt::black, 15, Qt::SolidLine, Qt::RoundCap, Qt::MiterJoin)); painter.setBrush(QBrush(Qt::blue, Qt::DiagCrossPattern)); painter.drawPie(80, 80, 400, 240, 60 * 16, 270 * 16);

Les deux derniers arguments de drawPie() sont exprims en seizimes de degr. Voici le code permettant de tracer la courbe de Bzier illustre en Figure 8.5 (c) :
QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); QPainterPath path; path.moveTo(80, 320); path.cubicTo(200, 80, 320, 80, 480, 320); painter.setPen(QPen(Qt::black, 8)); painter.drawPath(path);

La classe QPainterPath peut spcier des formes vectorielles arbitraires en regroupant des lments graphiques de base : droites, ellipses, polygones, arcs, courbes de Bzier cubiques et

Chapitre 8

Graphiques 2D et 3D

187

quadratiques et autres tracs de dessin. Les tracs de dessin constituent la primitive graphique ultime, dans le sens o on peut dsigner toute forme ou toute combinaison de formes par le terme de trac. Un trac spcie un contour, et la zone dcrite par le contour peut tre remplie laide dun pinceau. Dans lexemple de la Figure 8.5 (c), nous navons pas utilis de pinceau, seul le contour est donc dessin. Les trois exemples prcdents utilisent des modles de pinceau intgrs (Qt::SolidPattern, Qt::DiagCrossPattern et Qt::NoBrush). Dans les applications modernes, les remplissages dgrads reprsentent une alternative populaire aux remplissages monochromes. Les dgrads reposent sur une interpolation de couleur permettant dobtenir des transitions homognes entre deux ou plusieurs couleurs. Ils sont frquemment utiliss pour produire des effets 3D ; par exemple, le style Plastique se sert des dgrads pour afcher des QPushButton. Qt prend en charge trois types de dgrads : linaire, conique et circulaire. Lexemple Oven Timer dans la section suivante combine les trois types de dgrads dans un seul widget pour le rendre plus rel. Les dgrads linaires sont dnis par deux points de contrle et par une srie "darrts couleur" sur la ligne qui relie ces deux points. Par exemple, le dgrad linaire de la Figure 8.6 est cr avec le code suivant :
QLinearGradient gradient(50, 100, 300, 350); gradient.setColorAt(0.0, Qt::white); gradient.setColorAt(0.2, Qt::green); gradient.setColorAt(1.0, Qt::black);

Figure 8.6 Les pinceaux dgrads de QPainter

(x1, y1) (xc, yc) r

(xc, yc)

(x2, y2)

(xf, yf)

QLinearGradient

QRadialGradient

QRadialGradient

Nous spcions trois couleurs trois positions diffrentes entre les deux points de contrle.

188

Qt4 et C++ : Programmation dinterfaces GUI

Les positions sont indiques comme des valeurs virgule ottante entre 0 et 1, o 0 correspond au premier point de contrle et 1 au second. Les couleurs situes entre les interruptions spcies sont interpoles. Les dgrads circulaires sont dnis par un centre (xc, yc), un rayon r et une focale (xf, yf), en complment des interruptions de dgrad. Le centre et le rayon spcient un cercle. Les couleurs se diffusent vers lextrieur partir de la focale, qui peut tre le centre ou tout autre point dans le cercle. Les dgrads coniques sont dnis par un centre (xc, yc) et un angle . Les couleurs se diffusent autour du point central comme la trajectoire de la petite aiguille dune montre.

Jusqu prsent, nous avons mentionn les paramtres de crayon, de pinceau et de police de QPainter. En plus de ceux-ci, QPainter propose dautres paramtres qui inuencent la faon dont les formes et le texte sont dessins : Le pinceau de fond est utilis pour remplir le fond des formes gomtriques (sous le modle de pinceau), du texte ou des bitmaps quand le mode arrire-plan est congur en Qt::OpaqueMode (la valeur par dfaut est Qt::TransparentMode). Lorigine du pinceau correspond au point de dpart des modles de pinceau, normalement le coin suprieur gauche du widget. La zone daction est la zone du priphrique de dessin qui peut tre peinte. Dessiner en dehors de cette zone na aucun effet. Le viewport, la fentre et la matrice "world" dterminent la manire dont les coordonnes logiques de QPainter correspondent aux coordonnes physiques du priphrique de dessin. Par dfaut, celles-ci sont dnies de sorte que les systmes de coordonnes logiques et physiques concident. Les systmes de coordonnes sont abords dans la prochaine section. Le mode de composition spcie comment les pixels qui viennent dtre dessins doivent interagir avec les pixels dj prsents sur le priphrique de dessin. La valeur par dfaut est "source over," o les pixels sont dessins au-dessus des pixels existants. Ceci nest pris en charge que sur certains priphriques et est trait ultrieurement dans ce chapitre. Vous pouvez sauvegarder ltat courant dun module de rendu nomm painter tout moment sur une pile interne en appelant save() et en le restaurant plus tard en invoquant restore(). Cela permet par exemple de changer temporairement certains paramtres, puis de les rinitialiser leurs valeurs antrieures, comme nous le verrons dans la prochaine section.

Transformations du painter
Avec le systme de coordonnes par dfaut du QPainter, le point (0, 0) se situe dans le coin suprieur gauche du priphrique de dessin ; les coordonnes x augmentent vers la droite et les coordonnes y sont orientes vers le bas. Chaque pixel occupe une zone dune taille de 1 1 dans le systme de coordonnes par dfaut.

Chapitre 8

Graphiques 2D et 3D

189

Il est important de comprendre que le centre dun pixel se trouve aux coordonnes dun "demi pixel".Par exemple, le pixel en haut gauche couvre la zone entre les points (0, 0) et (1, 1) et son centre se trouve (0,5, 0,5). Si nous demandons QPainter de dessiner un pixel (100, 100) par exemple, il se rapprochera du rsultat en dcalant les coordonnes de +0,5 dans les deux sens, le pixel sera ainsi centr sur le point (100,5, 100,5). Cette distinction peut sembler plutt acadmique de prime abord, mais elle prsente des consquences importantes en pratique. Premirement, le dcalage de +0,5 ne se produit que si lanticrnelage est dsactiv (par dfaut) ; si lanticrnelage est activ et si nous essayons de dessiner un pixel (100, 100) en noir, QPainter coloriera les quatre pixels (99,5, 99,5), (99,5, 100,5), (100,5, 99,5) et (100,5, 100,5) en gris clair pour donner limpression quun pixel se trouve exactement au point de rencontre de ces quatre pixels. Si cet effet ne vous plat pas, vous pouvez lviter en spciant les coordonnes dun demi pixel, par exemple, (100,5, 100,5). Lorsque vous tracez des formes comme des lignes, des rectangles et des ellipses, des rgles similaires sappliquent. La Figure 8.7 vous montre comment le rsultat dun appel de drawRect(2, 2, 6, 5) varie en fonction de la largeur du crayon quand lanticrnelage est dsactiv. Il est notamment important de remarquer quun rectangle de 6 5 dessin avec une largeur de crayon de 1 couvre en fait une zone de 7 6. Cest diffrent des anciens kits doutils, y compris des versions antrieures de Qt, mais cest essentiel pour pouvoir dessiner des images vectorielles rellement ajustables et indpendantes de la rsolution.
(0, 0)

(0,

Pas de crayon

Largeur de crayon 1

Largeur de crayon 2

Largeur de crayon 3

Figure 8.7 Dessiner un rectangle de 6 5 sans anticrnelage

Maintenant que nous avons compris le systme de coordonnes par dfaut, nous pouvons nous concentrer davantage sur la manire de le modier en utilisant le viewport, la fentre et la matrice world de QPainter. (Dans ce contexte, le terme de "fentre" ne se rfre pas une fentre au sens de widget de niveau suprieur, et le "viewport" na rien voir avec le viewport de QScrollArea.) Le viewport et la fentre sont troitement lis. Le viewport est un rectangle arbitraire spci en coordonnes physiques. La fentre spcie le mme rectangle, mais en coordonnes logiques.

190

Qt4 et C++ : Programmation dinterfaces GUI

Au moment du trac, nous indiquons des points en coordonnes logiques qui sont converties en coordonnes physiques de manire algbrique linaire, en fonction des paramtres actuels de la fentre et du viewport. Par dfaut, le viewport et la fentre correspondent au rectangle du priphrique. Par exemple, si ce dernier est un widget de 320 200, le viewport et la fentre reprsentent le mme rectangle de 320 200 avec son coin suprieur gauche la position (0, 0). Dans ce cas, les systmes de coordonnes logiques et physiques sont identiques. Le mcanisme fentre-viewport est utile pour que le code de dessin soit indpendant de la taille ou de la rsolution du priphrique de dessin. Par exemple, si nous voulons que les coordonnes logiques stendent de (50, 50) (+50, +50) avec (0, 0) au milieu, nous pouvons congurer la fentre comme suit :
painter.setWindow(-50, -50, 100, 100);

La paire (50, 50) spcie lorigine et la paire (100, 100) indique la largeur et la hauteur. Cela signie que les coordonnes logiques (50, 50) correspondent dsormais aux coordonnes physiques (0, 0), et que les coordonnes logiques (+50, +50) correspondent aux coordonnes physiques (320, 200) (voir Figure 8.8). Dans cet exemple, nous navons pas modi le viewport.
(-50, -50) (-30, -20) (+10, +20) fentre (+50, +50) (0,0) (64, 60) (192, 140) viewport (320 ,200)

Figure 8.8 Convertir des coordonnes logiques en coordonnes physiques

Venons-en prsent la matrice world. La matrice world est une matrice de transformation qui sapplique en plus de la conversion fentre-viewport. Elle nous permet de translater, mettre lchelle, pivoter et faire glisser les lments que nous dessinons. Par exemple, si nous voulions dessiner un texte un angle de 45, nous utiliserions ce code :
QMatrix matrix; matrix.rotate(45.0); painter.setMatrix(matrix); painter.drawText(rect, Qt::AlignCenter, tr("Revenue"));

Les coordonnes logiques transmises drawText() sont transformes par la matrice world, puis mappes aux coordonnes physiques grce aux paramtres fentre-viewport.

Chapitre 8

Graphiques 2D et 3D

191

Si nous spcions plusieurs transformations, elles sont appliques dans lordre dans lequel nous les avons indiques. Par exemple, si nous voulons utiliser le point (10, 20) comme pivot pour la rotation, nous pouvons translater la fentre, accomplir la rotation, puis translater nouveau la fentre vers sa position dorigine :
QMatrix matrix; matrix.translate(-10.0, -20.0); matrix.rotate(45.0); matrix.translate(+10.0, +20.0); painter.setMatrix(matrix); painter.drawText(rect, Qt::AlignCenter, tr("Revenue"));

Il existe un moyen plus simple de spcier des transformations : exploiter les fonctions pratiques translate(), scale(), rotate() et shear() de QPainter:
painter.translate(-10.0, -20.0); painter.rotate(45.0); painter.translate(+10.0, +20.0); painter.drawText(rect, Qt::AlignCenter, tr("Revenue"));

Cependant, si nous voulons appliquer les mmes transformations de faon rptitive, il est plus efcace de les stocker dans un objet QMatrix et de congurer la matrice world sur le painter ds que les transformations sont ncessaires.
Figure 8.9 Le widget OvenTimer

Pour illustrer les transformations du painter, nous allons analyser le code du widget OvenTimer prsent en Figure 8.9. Ce widget est conu daprs les minuteurs de cuisine que nous utilisions avant que les fours soient quips dhorloges intgres. Lutilisateur peut cliquer sur un cran pour dnir la dure. La molette tournera automatiquement dans le sens inverse des aiguilles dune montre jusqu 0, cest ce moment-l que OvenTimer mettra le signal timeout().
class OvenTimer: public QWidget { Q_OBJECT public: OvenTimer(QWidget *parent = 0); void setDuration(int secs);

192

Qt4 et C++ : Programmation dinterfaces GUI

int duration() const; void draw(QPainter *painter); signals: void timeout(); protected: void paintEvent(QPaintEvent *event); void mousePressEvent(QMouseEvent *event); private: QDateTime finishTime; QTimer *updateTimer; QTimer *finishTimer; };

La classe OvenTimer hrite de QWidget et rimplmente deux fonctions virtuelles : paintEvent() et mousePressEvent().
const const const const const double DegreesPerMinute = 7.0; double DegreesPerSecond = DegreesPerMinute / 60; int MaxMinutes = 45; int MaxSeconds = MaxMinutes * 60; int UpdateInterval = 1;

Nous dnissons dabord quelques constantes qui contrlent laspect et lapparence du minuteur de four.
OvenTimer::OvenTimer(QWidget *parent) : QWidget(parent) { finishTime = QDateTime::currentDateTime(); updateTimer = new QTimer(this); connect(updateTimer, SIGNAL(timeout()), this, SLOT(update())); finishTimer = new QTimer(this); finishTimer->setSingleShot(true); connect(finishTimer, SIGNAL(timeout()), this, SIGNAL(timeout())); connect(finishTimer, SIGNAL(timeout()), updateTimer, SLOT(stop())); }

Dans le constructeur, nous crons deux objets QTimer : updateTimer est employ pour actualiser lapparence du widget toutes les secondes, et nishTimer met le signal timeout() du widget quand le minuteur du four atteint 0. nishTimer ne doit minuter quune seule fois, nous appelons donc setSingleShot(true) ; par dfaut, les minuteurs se dclenchent de manire rpte jusqu ce quils soient stopps ou dtruits. Le dernier appel de connect() permet darrter la mise jour du widget chaque seconde quand le minuteur est inactif.
void OvenTimer::setDuration(int secs) { if (secs > MaxSeconds) {

Chapitre 8

Graphiques 2D et 3D

193

secs = MaxSeconds; } else if (secs <= 0) { secs = 0; } finishTime = QDateTime::currentDateTime().addSecs(secs); if (secs > 0) { updateTimer->start(UpdateInterval * 1000); finishTimer->start(secs * 1000); } else { updateTimer->stop(); finishTimer->stop(); } update(); }

La fonction setDuration() dnit la dure du minuteur du four en nombre donn de secondes. Nous calculons lheure de n en ajoutant la dure lheure courante (obtenue partir de QDateTime::currentDateTime()) et nous la stockons dans la variable prive finishTime. Nous nissons en invoquant update() pour redessiner le widget avec la nouvelle dure. La variable finishTime est de type QDateTime. Vu que la variable contient une date et une heure, nous vitons ainsi tout bogue lorsque lheure courante se situe avant minuit et lheure de n aprs minuit.
int OvenTimer::duration() const { int secs = QDateTime::currentDateTime().secsTo(finishTime); if (secs < 0) secs = 0; return secs; }

La fonction duration() retourne le nombre de secondes restantes avant que le minuteur ne sarrte. Si le minuteur est inactif, nous retournons 0.
void OvenTimer::mousePressEvent(QMouseEvent *event) { QPointF point = event->pos() - rect().center(); double theta = atan2(-point.x(), -point.y()) * 180 / 3.14159265359; setDuration(duration() + int(theta / DegreesPerSecond)); update(); }

Si lutilisateur clique sur le widget, nous recherchons le cran le plus proche grce une formule mathmatique subtile mais efcace, et nous utilisons le rsultat pour dnir la nouvelle dure. Puis nous planions un rafchage. Le cran sur lequel lutilisateur a cliqu sera dsormais en haut et se dplacera dans le sens inverse des aiguilles dune montre au fur et mesure que le temps scoule jusqu atteindre 0.

194

Qt4 et C++ : Programmation dinterfaces GUI

void OvenTimer::paintEvent(QPaintEvent * /* event */) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); int side = qMin(width(), height()); painter.setViewport((width() - side) / 2, (height() - side) / 2, side, side); painter.setWindow(-50, -50, 100, 100); draw(&painter); }

Dans paintEvent(), nous dnissons le viewport de sorte quil devienne le carr le pus grand qui peut entrer dans le widget et nous dnissons la fentre en rectangle (50, 50, 100, 100), cest--dire le rectangle de 100 _ 100 allant de (50, 50) (+50, +50). La fonction modle qMin() retourne le plus bas de ses deux arguments. Nous appelons ensuite la fonction draw() qui se chargera du dessin.
Figure 8.10 Le widget OvenTimer en trois tailles diffrentes

Si nous navions pas dni le viewport en carr, le minuteur du four se transformerait en ellipse quand le widget serait redimensionn en rectangle (non carr). Pour viter de telles dformations, nous devons congurer le viewport et la fentre en rectangles ayant le mme format dimage. Analysons prsent le code de dessin :
void OvenTimer::draw(QPainter *painter) { static const int triangle[3][2] = { { -2, -49 }, { +2, -49 }, { 0, -47 } }; QPen thickPen(palette().foreground(), 1.5); QPen thinPen(palette().foreground(), 0.5); QColor niceBlue(150, 150, 200);

Chapitre 8

Graphiques 2D et 3D

195

painter->setPen(thinPen); painter->setBrush(palette().foreground()); painter->drawPolygon(QPolygon(3, &triangle[0][0]));

Nous commenons par dessiner le petit triangle qui symbolise la position 0 en haut du widget. Le triangle est spci par trois coordonnes codes et nous utilisons drawPolygon() pour lafcher. Laspect pratique du mcanisme fentre-viewport, cest que nous avons la possibilit de coder les coordonnes utilises dans les commandes de dessin et de toujours obtenir un bon comportement lors du redimensionnement.
QConicalGradient coneGradient(0, 0, -90.0); coneGradient.setColorAt(0.0, Qt::darkGray); coneGradient.setColorAt(0.2, niceBlue); coneGradient.setColorAt(0.5, Qt::white); coneGradient.setColorAt(1.0, Qt::darkGray); painter->setBrush(coneGradient); painter->drawEllipse(-46, -46, 92, 92);

Nous traons le cercle extrieur et nous le remplissons avec un dgrad conique. Le centre du dgrad se trouve la position (0, 0) et son angle est de 90.
QRadialGradient haloGradient(0, 0, 20, 0, 0); haloGradient.setColorAt(0.0, Qt::lightGray); haloGradient.setColorAt(0.8, Qt::darkGray); haloGradient.setColorAt(0.9, Qt::white); haloGradient.setColorAt(1.0, Qt::black); painter->setPen(Qt::NoPen); painter->setBrush(haloGradient); painter->drawEllipse(-20, -20, 40, 40);

Nous nous servons dun dgrad radial pour le cercle intrieur. Le centre et la focale du dgrad se situent (0, 0). Le rayon du dgrad est gal 20.
QLinearGradient knobGradient(-7, -25, 7, -25); knobGradient.setColorAt(0.0, Qt::black); knobGradient.setColorAt(0.2, niceBlue); knobGradient.setColorAt(0.3, Qt::lightGray); knobGradient.setColorAt(0.8, Qt::white); knobGradient.setColorAt(1.0, Qt::black); painter->rotate(duration() * DegreesPerSecond); painter->setBrush(knobGradient); painter->setPen(thinPen); painter->drawRoundRect(-7, -25, 14, 50, 150, 50); for (int i = 0; i <= MaxMinutes; ++i) { if (i % 5 == 0) {

196

Qt4 et C++ : Programmation dinterfaces GUI

painter->setPen(thickPen); painter->drawLine(0, -41, 0, -44); painter->drawText(-15, -41, 30, 25, Qt::AlignHCenter | Qt::AlignTop, QString::number(i)); } else { painter->setPen(thinPen); painter->drawLine(0, -42, 0, -44); } painter->rotate(-DegreesPerMinute); } }

Nous appelons rotate() pour faire pivoter le systme de coordonnes du painter. Dans lancien systme de coordonnes, la marque correspondant 0 minute se trouvait en haut ; maintenant, elle se dplace vers lendroit appropri pour le temps restant. Nous dessinons le bouton rectangulaire aprs la rotation, parce que son orientation dpend de langle de cette rotation. Dans la boucle for, nous dessinons les graduations tout autour du cercle extrieur et les nombres pour chaque multiple de 5 minutes. Le texte est dessin dans un rectangle invisible en dessous de la graduation. A la n de chaque itration, nous faisons pivoter le painter dans le sens des aiguilles dune montre de 7, ce qui correspond une minute. La prochaine fois que nous dessinons une graduation, elle sera une position diffrente autour du cercle, mme si les coordonnes transmises aux appels de drawLine() et drawText() sont toujours les mmes. Le code de la boucle for souffre dun dfaut mineur qui deviendrait rapidement apparent si nous effectuions davantage ditrations. A chaque fois que nous appelons rotate(), nous multiplions la matrice courante par une matrice de rotation, engendrant ainsi une nouvelle matrice world. Les problmes darrondis associs larithmtique en virgule ottante entranent une matrice world trs imprcise. Voici un moyen de rcrire le code pour viter ce problme, en excutant save() et restore() pour sauvegarder et recharger la matrice de transformation originale pour chaque itration :
for (int i = 0; i <= MaxMinutes; ++i) { painter->save(); painter->rotate(-i * DegreesPerMinute); if (i % 5 == 0) { painter->setPen(thickPen); painter->drawLine(0, -41, 0, -44); painter->drawText(-15, -41, 30, 25, Qt::AlignHCenter | Qt::AlignTop, QString::number(i)); } else { painter->setPen(thinPen); painter->drawLine(0, -42, 0, -44); } painter->restore(); }

Chapitre 8

Graphiques 2D et 3D

197

Il existe un autre moyen dimplmenter un minuteur de four : calculer les positions (x, y) soimme, en utilisant sin() et cos() pour trouver les positions autour du cercle. Mais nous aurions encore d employer une translation et une rotation pour dessiner le texte un certain angle.

Afchage de haute qualit avec QImage


Au moment de dessiner, vous pourriez avoir trouver un compromis entre vitesse et prcision. Par exemple, sous X11 et Mac OS X, le dessin sur un QWidget ou un QPixmap repose sur le moteur de dessin natif de la plate-forme. Sous X11, ceci rduit au maximum les communications avec le serveur X ; seules les commandes de dessin sont envoyes plutt que les donnes de limage. Le principal inconvnient de cette approche, cest que le champ daction de Qt est limit celui de la prise en charge native de la plate-forme : Sous X11, des fonctionnalits, telles que lanticrnelage et le support des coordonnes fractionnaires, ne sont disponibles que si lextension X Render se trouve sur le serveur X. Sous Mac OS X, le moteur graphique natif crnel sappuie sur des algorithmes diffrents pour dessiner des polygones par rapport X11 et Windows, avec des rsultats lgrement diffrents. Quand la prcision est plus importante que lefcacit, nous pouvons dessiner un QImage et copier le rsultat lcran. Celui-ci utilise toujours le moteur de dessin interne de Qt, aboutissant ainsi des rsultats identiques sur toutes les plates-formes. La seule restriction, cest que le QImage, sur lequel nous dessinons, doit tre cr avec un argument de type QImage::Format_RGB32 ou QImage::Format_ARGB32_Premultiplied. Le format ARGB32 prmultipli est presque identique au format traditionnel ARGB32 (0xaarrggbb), la diffrence prs que les canaux rouge, vert et bleu sont "prmultiplis" par le canal alpha. Cela signie que les valeurs RVB, qui stendent normalement de 0x00 0xFF, sont mises lchelle de 0x00 la valeur alpha. Par exemple, une couleur bleue transparente 50 % est reprsente par 0x7F0000FF en format ARGB32, mais par 0x7F00007F en format ARGB32 prmultipli. De mme, une couleur vert fonc transparente 75 % reprsente par 0x3F008000 en format ARGB32 deviendrait 0x3F002000 en format ARGB32 prmultipli. Supposons que nous souhaitons utiliser lanticrnelage pour dessiner un widget et que nous voulons obtenir de bons rsultats mme sous des systmes X11 sans lextension X Render. Voici la syntaxe du gestionnaire paintEvent() dorigine, qui se base sur X Render pour lanticrnelage :
void MyWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); draw(&painter); }

198

Qt4 et C++ : Programmation dinterfaces GUI

Voil comment rcrire la fonction paintEvent() du widget pour exploiter le moteur graphique de Qt indpendant de la plate-forme :
void MyWidget::paintEvent(QPaintEvent *event) { QImage image(size(), QImage::Format_ARGB32_Premultiplied); QPainter imagePainter(&image); imagePainter.initFrom(this); imagePainter.setRenderHint(QPainter::Antialiasing, true); imagePainter.eraseRect(rect()); draw(&imagePainter); imagePainter.end(); QPainter widgetPainter(this); widgetPainter.drawImage(0, 0, image); }

Nous crons un QImage de la mme taille que le widget en format ARGB32 prmultipli, et un QPainter pour dessiner sur limage. Lappel de initFrom() initialise le crayon, le fond et la police en fonction du widget. Nous effectuons notre dessin avec QPainter comme dhabitude, et la n, nous rutilisons lobjet QPainter pour copier limage sur le widget. Cette approche produit dexcellents rsultats identiques sur toutes les plates-formes, lexception de lafchage de la police qui dpend des polices installes. Une fonctionnalit particulirement puissante du moteur graphique de Qt est sa prise en charge des modes de composition. Ceux-ci spcient comment un pixel source et un pixel de destination fusionnent pendant le dessin. Ceci sapplique toutes les oprations de dessin, y compris le crayon, le pinceau, le dgrad et limage. Le mode de composition par dfaut est QImage::CompositionMode_SourceOver, ce qui signie que le pixel source (celui que nous dessinons) remplace le pixel de destination (le pixel existant) de telle manire que le composant alpha de la source dnit sa translucidit. Dans la Figure 8.11, vous voyez un papillon moiti transparent dessin sur un motif damiers avec les diffrents modes.
Figure 8.11 Les modes de composition de QPainter
Source SourceOver SourceIn SourceOut SourceAtop Clear

Destination

DestinationOver

DestinationIn

DestinationOut

DestinationAtop

Xor

Les modes de composition sont dnis laide de QPainter::setCompositionMode(). Par exemple, voici comment crer un QImage qui combine en XOR le papillon et le motif damiers :

Chapitre 8

Graphiques 2D et 3D

199

QImage resultImage = checkerPatternImage; QPainter painter(&resultImage); painter.setCompositionMode(QPainter::CompositionMode_Xor); painter.drawImage(0, 0, butterflyImage);

Il faut tre conscient du problme li au fait que lopration QImage::CompositionMode_Xor sapplique au canal alpha. Cela signie que si nous appliquons XOR (OU exclusif) la couleur blanche (0xFFFFFFFF), nous obtenons une couleur transparente (0x00000000), et pas noire (0xFF000000).

Impression
Limpression dans Qt est quivalente au dessin sur QWidget, QPixmap ou QImage. Plusieurs tapes sont ncessaires : 1. crer un QPrinter qui fera ofce de priphrique de dessin ; 2. ouvrir un QPrintDialog, qui permet lutilisateur de choisir une imprimante et de congurer certaines options ; 3. crer un QPainter pour agir sur le QPrinter; 4. dessiner une page laide de QPainter; 5. appeler QPrinter::newPage() pour passer la page suivante ; 6. rpter les tapes 4 et 5 jusqu ce que toutes les pages soient imprimes. Sous Windows et Mac OS X, QPrinter utilise les pilotes dimprimante du systme. Sous Unix, il gnre un PostScript et lenvoie lp ou lpr (ou au programme dni en excutant QPrinter::setPrintProgram()). QPrinter peut aussi servir gnrer des chiers PDF en appelant setOutputFormat(QPrinter::PdfFormat). Commenons par quelques exemples simples qui simpriment tous sur une seule page. Le premier exemple imprime un QImage(voir Figure 8.12) :
void PrintWindow::printImage(const QImage &image) { QPrintDialog printDialog(&printer, this); if (printDialog.exec()) { QPainter painter(&printer); QRect rect = painter.viewport(); QSize size = image.size(); size.scale(rect.size(), Qt::KeepAspectRatio); painter.setViewport(rect.x(), rect.y(), size.width(), size.height()); painter.setWindow(image.rect()); painter.drawImage(0, 0, image); } }

200

Qt4 et C++ : Programmation dinterfaces GUI

Figure 8.12 Imprimer un QImage

Nous supposons que la classe PrintWindow possde une variable membre appele printer de type QPrinter. Nous aurions simplement pu crer QPrinter sur la pile dans printImage(), mais les paramtres de lutilisateur auraient t perdus entre deux impressions. Nous crons un QPrintDialog et nous invoquons exec() pour lafcher. Il retourne true si lutilisateur a cliqu sur le bouton OK ; sinon il retourne false. Aprs lappel de exec(), lobjet QPrinter est prt tre utilis. (Il est aussi possible dimprimer sans utiliser QPrintDialog, en appelant directement des fonctions membres de QPrinter pour congurer les divers aspects.) Nous crons ensuite un QPainter pour dessiner sur le QPrinter. Nous dnissons la fentre en rectangle de limage et le viewport en un rectangle du mme format dimage, puis nous dessinons limage la position (0, 0). Par dfaut, la fentre de QPainter est initialise de sorte que limprimante semble avoir une rsolution similaire lcran (en gnral entre 72 et 100 points par pouce), ce qui facilite la rutilisation du code de dessin du widget pour limpression. Ici, ce ntait pas un problme, parce que nous avons dni notre propre fentre. Imprimer des lments qui ne stendent pas sur plus dune page se rvle trs simple, mais de nombreuses applications ont besoin dimprimer plusieurs pages. Pour celles-ci, nous devons dessiner une page la fois et appeler newPage() pour passer la page suivante.

Chapitre 8

Graphiques 2D et 3D

201

Ceci soulve un problme : dterminer la quantit dinformations que nous pouvons imprimer sur chaque page. Il existe deux approches principales pour grer les documents multipages avec Qt : Nous pouvons convertir nos donnes en format HTML et les afcher avec QTextDocument, le moteur de texte de Qt. Nous pouvons effectuer le dessin et la rpartition sur les pages manuellement. Nous allons analyser les deux approches. En guise dexemple, nous allons imprimer un guide des eurs : une liste de noms de eurs, chacun comprenant une description sous forme de texte. Chaque entre du guide est stocke sous forme de chane au format "nom: description," par exemple :

Miltonopsis santanae: une des espces dorchides les plus dangereuses.

Vu que les donnes relatives chaque eur sont reprsentes par une seule chane, nous pouvons reprsenter toutes les eurs dans le guide avec un QStringList. Voici la fonction qui imprime le guide des eurs au moyen du moteur de texte de Qt :
void PrintWindow::printFlowerGuide(const QStringList &entries) { QString html; foreach (QString entry, entries) { QStringList fields = entry.split(": "); QString title = Qt::escape(fields[0]); QString body = Qt::escape(fields[1]); html += "<table width=\"100%\" border=1 cellspacing=0>\n" "<tr><td bgcolor=\"lightgray\"><font size=\"+1\">" "<b><i>" + title + "</i></b></font>\n<tr><td>" + body + "\n</table>\n<br>\n"; } printHtml(html); }

La premire tape consiste convertir QStringList en format HTML. Chaque eur est reprsente par un tableau HTML avec deux cellules. Nous excutons Qt::escape() pour remplacer les caractres spciaux "&", "<", ">" par les entits HTML correspondantes ("&amp;", "&lt;", "&gt;"). Nous appelons ensuite printHtml() pour imprimer le texte.
void PrintWindow::printHtml(const QString &html) { QPrintDialog printDialog(&printer, this); if (printDialog.exec()) { QPainter painter(&printer); QTextDocument textDocument; textDocument.setHtml(html); textDocument.print(&printer); } }

202

Qt4 et C++ : Programmation dinterfaces GUI

La fonction printHtml() ouvre un QPrintDialog et se charge dimprimer un document HTML. Elle peut tre rutilise "telle quelle" dans nimporte quelle application Qt pour imprimer des pages HTML arbitraires.
Figure 8.13 Imprimer un guide des eurs avec QTextDocument

Aponogeton distachyos
The Cape pondweed (water hawthorn) is a deciduous perennial that has floating, oblong, dark green leaves which are sometimes splashed purple. The waxy-white flowers have a characteristic 'forked' appearance, sweet scent and black stamens. They appear from early spring until fall. They grow in deep or shallow water and spread to 1.2 m.

Trapa natans
The Jesuit's nut (or water chestnut) has mid-green diamond-shaped leaves with deeply toothed edges that grow in neat rosettes. The center of each leaf is often marked with deep purple blotches. White flowers are produced in summer. Each floating plant can spread to 23 cm.

Cabomba caroliniana
The Fish grass (or fanwort or Washington grass) is a useful oxygenator for ponds. It is a deciduous or semi-evergreen submerged perennial that is used by fish as a source of food and as a place in which to spawn. Plants form spreading hummocks of fan-shaped, coarsly divided leaves which are bright green. Tiny white flowers appear in the summer.

Zantedeschia aethiopica
The Arum lily is a South African native that grows well in shallow water. It flowers throughout the summer, with the erect funnel-shaped spathes being held well above the arrow-shaped glossy, deep green leaves. Each spathe surrounds a central yellow spadix. The leaves and flowering stems arise from a tuber. Plants can reach up to 90 cm in height, spreading to 45 cm.

Caltha palustris
The Marsh marigold (or kingcup) is a deciduous perennial that grows in shallow water around the edges of ponds. It is equally well suited to a bog garden, moist rock garden or herbaceous border. The rounded dark green leaves set off its large, cup-shaped golden-yellow flowers. Plants can grow to 60 cm in height, with a spread of 45 cm. The double-flowered cultivar 'Flore Plena' only reaches 10 cm.

Ceratophyllum demersum
The Hornwort is a deciduous perennial that produces feathery submerged foliage. It sometimes floats and spreads over a large area. It is a good oxygenator and grows best in cool deep water. It has no roots.

Juncus effusus 'Spiralis'


The Corkscrew rush is a tufted evergreen perennial with mid-green leafless stems which are twisted and curled like a corkscrew. The stems often lie on the ground. The greenish-brown flowers appear in summer. Plants are best used at the edge of a pond, so that the stems can be seen against the reflective water surface. Strong plants can send up 90 cm-tall twisted shoots which are used in modern flower arranging.

Nuphar lutea
The Yellow water lily has small (6 cm diameter) yellow flowers that are bottle-shaped and sickly smelling. They are held above a mat of broad, oval, mid-green leaves which are about 40 cm wide, giving the plant a spread of up to 1.5 m. The seed heads are rounded and warty. This hardy deciduous perennial thrives in deep water, in sun or shade, and is useful for a water-lily effect where Nymphaea will not grow.

Orontium aquaticum
The Golden club's flowers lack the spathe typical of other aroids, leaving the central yellow and white spadix to provide color. A deciduous perennial, the golden club grows equally well in shallow or deep water. In spring, the pencil-like flower spikes (spadices) emerge from among the floating mass of waxy leaves which are a bluish or greyish green. Plants grow to 25 cm high spreading up to 60 cm. Large seeds develop later in the summer and are used to propagate plants while they are still fresh.

Convertir un document au format HTML et utiliser QTextDocument pour limprimer est de loin la mthode la plus pratique pour imprimer des rapports et dautres documents complexes. Ds que vous avez besoin dun niveau de contrle suprieur, vous pouvez envisager de grer la mise en page et le dessin manuellement. Voyons maintenant comment nous pouvons utiliser cette approche pour imprimer le guide des eurs (voir Figure 8.13). Voil la nouvelle fonction printFlowerGuide():
void PrintWindow::printFlowerGuide(const QStringList &entries) { QPrintDialog printDialog(&printer, this); if (printDialog.exec()) { QPainter painter(&printer); QList<QStringList> pages; paginate(&painter, &pages, entries); printPages(&painter, pages); } }

Aprs avoir congur limprimante et construit le painter, nous appelons la fonction paginate() pour dterminer quelle entre doit apparatre sur quelle page. Vous obtenez donc une liste de QStringList, chacun contenant les entres dune page. Nous transmettons ce rsultat printPages().

Chapitre 8

Graphiques 2D et 3D

203

Supposons, par exemple, que le guide des eurs contient 6 entres, que nous appellerons A, B, C, D, E et F. Imaginons maintenant quil y a sufsamment de place pour A et B sur la premire page, pour C, D et E sur la deuxime page et pour F sur la troisime page. La liste pages contiendrait donc la liste [A, B] la position dindex 0, la liste [C, D, E] la position dindex 1 et la liste [F] la position dindex 2.
void PrintWindow::paginate(QPainter *painter, QList<QStringList> *pages, const QStringList &entries) { QStringList currentPage; int pageHeight = painter->window().height() - 2 * LargeGap; int y = 0; foreach (QString entry, entries) { int height = entryHeight(painter, entry); if (y + height > pageHeight &&!currentPage.empty()) { pages->append(currentPage); currentPage.clear(); y = 0; } currentPage.append(entry); y += height + MediumGap; } if (!currentPage.empty()) pages->append(currentPage); }

La fonction paginate() rpartit les entres du guide des eurs sur les pages. Elle se base sur la fonction entryHeight() qui calcule la hauteur dune entre. Elle tient galement compte des espaces vides verticaux en haut et en bas de la page, de taille LargeGap. Nous parcourons les entres et nous les ajoutons la page en cours jusqu ce quune entre nait plus sufsamment de place sur cette page ; puis nous ajoutons la page en cours la liste pages et nous commenons une nouvelle page.
int PrintWindow::entryHeight(QPainter *painter, const QString &entry) { QStringList fields = entry.split(": "); QString title = fields[0]; QString body = fields[1]; int textWidth = painter->window().width() - 2 * SmallGap; int maxHeight = painter->window().height(); painter->setFont(titleFont); QRect titleRect = painter->boundingRect(0, 0, textWidth, maxHeight, Qt::TextWordWrap, title); painter->setFont(bodyFont); QRect bodyRect = painter->boundingRect(0, 0, textWidth, maxHeight, Qt::TextWordWrap, body); return titleRect.height() + bodyRect.height() + 4 * SmallGap; }

204

Qt4 et C++ : Programmation dinterfaces GUI

La fonction entryHeight() se sert de QPainter::boundingRect() pour calculer lespace vertical ncessaire pour une entre. La Figure 8.14 prsente la disposition dune entre et la signication des constantes SmallGap et MediumGap.
Figure 8.14 La disposition dune entre
SmallGap
Titre

SmallGap SmallGap
SmallGap SmallGap
Corps

MediumGap

SmallGap

void PrintWindow::printPages(QPainter *painter, const QList<QStringList> &pages) { int firstPage = printer.fromPage() - 1; if (firstPage >= pages.size()) return; if (firstPage == -1) firstPage = 0; int lastPage = printer.toPage() - 1; if (lastPage == -1 || lastPage >= pages.size()) lastPage = pages.size() - 1; int numPages = lastPage - firstPage + 1; for (int i = 0; i < printer.numCopies(); ++i) { for (int j = 0; j < numPages; ++j) { if (i!= 0 || j!= 0) printer.newPage(); int index; if (printer.pageOrder() == QPrinter::FirstPageFirst) { index = firstPage + j; } else { index = lastPage - j; } printPage(painter, pages[index], index + 1); } } }

Chapitre 8

Graphiques 2D et 3D

205

Le rle de la fonction printPages() est dimprimer chaque page laide de printPage() dans le bon ordre et les bonnes quantits. Grce QPrintDialog, lutilisateur peut demander plusieurs copies, spcier une plage dimpression ou demander les pages en ordre inverse. Cest nous dhonorer ces options ou de les dsactiver au moyen de QPrintDialog::setEnabledOptions(). Nous dterminons tout dabord la plage imprimer. Les fonctions fromPage() et toPage() de QPrinter retournent les numros de page slectionns par lutilisateur, ou 0 si aucune plage na t choisie. Nous soustrayons 1, parce que notre liste pages est indexe partir de 0, et nous dnissons firstPage et lastPage de sorte couvrir la totalit de la plage si lutilisateur na rien prcis. Puis nous imprimons chaque page. La boucle externe for effectue une itration autant de fois que ncessaire pour produire le nombre de copies demand par lutilisateur. La plupart des pilotes dimprimante prennent en charge les copies multiples, QPrinter::numCopies() retourne toujours 1 pour celles-ci. Si le pilote ne peut pas grer plusieurs copies, numCopies() renvoie le nombre de copies demand par lutilisateur, et lapplication se charge dimprimer ce nombre de copies. (Dans lexemple QImage prcdent, nous avons ignor numCopies() pour une question de simplicit.)
Figure 8.15 Imprimer un guide des eurs avec QPainter

Aponogeton distachyos
The Cape pondweed (water hawthorn) is a deciduous perennial that has floating, oblong, dark green leaves which are sometimes splashed purple. The waxy-white flowers have a characteristic 'forked' appearance, sweet scent and black stamens. They appear from early spring until fall. They grow in deep or shallow water and spread to 1.2 m.

Nuphar lutea
The Yellow water lily has small (6 cm diameter) yellow flowers that are bottle-shaped and sickly smelling. They are held above a mat of broad, oval, mid-green leaves which are about 40 cm wide, giving the plant a spread of up to 1.5 m. The seed heads are rounded and warty. This hardy deciduous perennial thrives in deep water, in sun or shade, and is useful for a water-lily effect where Nymphaea will not grow.

Cabomba caroliniana Orontium aquaticum


The Fish grass (or fanwort or Washington grass) is a useful oxygenator for ponds. It is a deciduous or semi-evergreen submerged perennial that is used by fish as a source of food and as a place in which to spawn. Plants form spreading hummocks of fan-shaped, coarsly divided leaves which are bright green. Tiny white flowers appear in the summer. The Golden club's flowers lack the spathe typical of other aroids, leaving the central yellow and white spadix to provide color. A deciduous perennial, the golden club grows equally well in shallow or deep water. In spring, the pencil-like flower spikes (spadices) emerge from among the floating mass of waxy leaves which are a bluish or greyish green. Plants grow to 25 cm high spreading up to 60 cm. Large seeds develop later in the summer and are used to propagate plants while they are still fresh.

Caltha palustris
The Marsh marigold (or kingcup) is a deciduous perennial that grows in shallow water around the edges of ponds. It is equally well suited to a bog garden, moist rock garden or herbaceous border. The rounded dark green leaves set off its large, cup-shaped golden-yellow flowers. Plants can grow to 60 cm in height, with a spread of 45 cm. The double-flowered cultivar 'Flore Plena' only reaches 10 cm.

Trapa natans
The Jesuit's nut (or water chestnut) has mid-green diamond-shaped leaves with deeply toothed edges that grow in neat rosettes. The center of each leaf is often marked with deep purple blotches. White flowers are produced in summer. Each floating plant can spread to 23 cm.

Ceratophyllum demersum
The Hornwort is a deciduous perennial that produces feathery submerged foliage. It sometimes floats and spreads over a large area. It is a good oxygenator and grows best in cool deep water. It has no roots.

Zantedeschia aethiopica
The Arum lily is a South African native that grows well in shallow water. It flowers throughout the summer, with the erect funnel-shaped spathes being held well above the arrow-shaped glossy, deep green leaves. Each spathe surrounds a central yellow spadix. The leaves and flowering stems arise from a tuber. Plants can reach up to 90 cm in height, spreading to 45 cm.

Juncus effusus 'Spiralis'


The Corkscrew rush is a tufted evergreen perennial with mid-green leafless stems which are twisted and curled like a corkscrew. The stems often lie on the ground. The greenish-brown flowers appear in summer. Plants are best used at the edge of a pond, so that the stems can be seen against the reflective water surface. Strong plants can send up 90 cm-tall twisted shoots which are used in modern flower arranging.

La boucle interne for parcourt les pages. Si la page nest pas la premire page, nous appelons newPage() pour supprimer lancienne page de la mmoire et pour commencer dessiner sur une nouvelle page. Nous invoquons printPage() pour dessiner chaque page.
void PrintWindow::printPage(QPainter *painter, const QStringList &entries, int pageNumber) { painter->save();

206

Qt4 et C++ : Programmation dinterfaces GUI

painter->translate(0, LargeGap); foreach (QString entry, entries) { QStringList fields = entry.split(": "); QString title = fields[0]; QString body = fields[1]; printBox(painter, title, titleFont, Qt::lightGray); printBox(painter, body, bodyFont, Qt::white); painter->translate(0, MediumGap); } painter->restore(); painter->setFont(footerFont); painter->drawText(painter->window(), Qt::AlignHCenter | Qt::AlignBottom, QString::number(pageNumber)); }

La fonction printPage() parcourt toutes les entres du guide des eurs et les imprime grce deux appels de printBox(): un pour le titre (le nom de la eur) et un pour le corps (sa description). Elle dessine galement le numro de page centr au bas de la page (voir Figure 8.16).
Figure 8.16 La disposition dune page du guide des eurs
(0, 0) LargeGap (0, Large Gap) Fentre

page Height

Zone d'impression des entres de fleur

Large Gap

[Numro de page]

void PrintWindow::printBox(QPainter *painter, const QString &str, const QFont &font, const QBrush &brush) { painter->setFont(font); int boxWidth = painter->window().width(); int textWidth = boxWidth - 2 * SmallGap; int maxHeight = painter->window().height();

Chapitre 8

Graphiques 2D et 3D

207

QRect textRect = painter->boundingRect(SmallGap, SmallGap, textWidth, maxHeight, Qt::TextWordWrap, str); int boxHeight = textRect.height() + 2 * SmallGap; painter->setPen(QPen(Qt::black, 2, Qt::SolidLine)); painter->setBrush(brush); painter->drawRect(0, 0, boxWidth, boxHeight); painter->drawText(textRect, Qt::TextWordWrap, str); painter->translate(0, boxHeight); }

La fonction printBox() trace les contours dune bote, puis dessine le texte lintrieur.

Graphiques avec OpenGL


OpenGL est une API standard permettant dafcher des graphiques 2D et 3D. Les applications Qt peuvent tracer des graphiques 3D en utilisant le module QtOpenGL, qui se base sur la bibliothque OpenGL du systme. Cette section suppose que vous connaissez dj OpenGL. Si vous dcouvrez OpenGL, consultez dabord http://www.opengl.org/ pour en apprendre davantage.
Figure 8.17 Lapplication Tetrahedron

Dessiner des graphiques avec OpenGL dans une application Qt se rvle trs facile : vous devez driver QGLWidget, rimplmenter quelques fonctions virtuelles et relier lapplication aux bibliothques QtOpenGL et OpenGL. Etant donn que QGLWidget hrite de QWidget, la majorit des notions que nous connaissons dj peuvent sappliquer ce cas. La principale diffrence cest que nous utilisons des fonctions OpenGL standards pour dessiner en lieu et place de QPainter.

208

Qt4 et C++ : Programmation dinterfaces GUI

Pour vous montrer comment cela fonctionne, nous allons analyser le code de lapplication Tetrahedron prsente en Figure 8.17. Lapplication prsente un ttradre en 3D, ou une matrice quatre faces, chaque face ayant une couleur diffrente. Lutilisateur peut faire pivoter le ttradre en appuyant sur un bouton de la souris et en le faisant glisser. Il peut aussi dnir la couleur dune face en double-cliquant dessus et en choisissant une couleur dans le QColorDialog qui souvre.
class Tetrahedron: public QGLWidget { Q_OBJECT public: Tetrahedron(QWidget *parent = 0); protected: void initializeGL(); void resizeGL(int width, int height); void paintGL(); void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void mouseDoubleClickEvent(QMouseEvent *event); private: void draw(); int faceAtPosition(const QPoint &pos); GLfloat rotationX; GLfloat rotationY; GLfloat rotationZ; QColor faceColors[4]; QPoint lastPos; };

La classe Tetrahedron hrite de QGLWidget. Les fonctions initializeGL(), resizeGL() et paintGL() sont rimplmentes dans QGLWidget. Les gestionnaires dvnements mouse sont rimplments dans QWidget comme dhabitude.
Tetrahedron::Tetrahedron(QWidget *parent) : QGLWidget(parent) { setFormat(QGLFormat(QGL::DoubleBuffer | QGL::DepthBuffer)); rotationX = -21.0; rotationY = -57.0; rotationZ = 0.0; faceColors[0] = Qt::red; faceColors[1] = Qt::green; faceColors[2] = Qt::blue; faceColors[3] = Qt::yellow; }

Chapitre 8

Graphiques 2D et 3D

209

Dans le constructeur, nous appelons QGLWidget::setFormat() pour spcier le contexte dafchage OpenGL et nous initialisons les variables prives de la classe.
void Tetrahedron::initializeGL() { qglClearColor(Qt::black); glShadeModel(GL_FLAT); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); }

La fonction initializeGL() est invoque une seule fois avant que paintGL() soit appele. Cest donc dans le code de cette fonction que nous allons congurer le contexte dafchage OpenGL, dnir les listes dafchage et accomplir dautres initialisations. Tout le code est en OpenGL standard, sauf lappel de la fonction qglClearColor() de QGLWidget. Si nous voulions uniquement du code OpenGL standard, nous aurions pu appeler glClearColor() en mode RGBA et glClearIndex() en mode table des couleurs.
void Tetrahedron::resizeGL(int width, int height) { glViewport(0, 0, width, height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); GLfloat x = GLfloat(width) / height; glFrustum(-x, x, -1.0, 1.0, 4.0, 15.0); glMatrixMode(GL_MODELVIEW); }

La fonction resizeGL() est invoque avant le premier appel de paintGL(), mais aprs lappel de initializeGL(). Elle est aussi invoque ds que le widget est redimensionn. Cest l que nous avons la possibilit de congurer le viewport OpenGL, la projection et tout autre paramtre qui dpend de la taille du widget.
void Tetrahedron::paintGL() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); draw(); }

La fonction paintGL() est invoque ds que le widget doit tre redessin. Elle ressemble QWidget::paintEvent(), mais des fonctions OpenGL remplacent les fonctions de QPainter. Le dessin est effectu par la fonction prive draw().
void Tetrahedron::draw() { static const GLfloat P1[3] static const GLfloat P2[3] static const GLfloat P3[3] static const GLfloat P4[3]

= = = =

{ { { {

0.0, -1.0, +2.0 }; +1.73205081, -1.0, -1.0 }; -1.73205081, -1.0, -1.0 }; 0.0, +2.0, 0.0 };

210

Qt4 et C++ : Programmation dinterfaces GUI

static const GLfloat * const coords[4][3] = { { P1, P2, P3 }, { P1, P3, P4 }, { P1, P4, P2 }, { P2, P4, P3 } }; glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0, 0.0, -10.0); glRotatef(rotationX, 1.0, 0.0, 0.0); glRotatef(rotationY, 0.0, 1.0, 0.0); glRotatef(rotationZ, 0.0, 0.0, 1.0); for (int i = 0; i < 4; ++i) { glLoadName(i); glBegin(GL_TRIANGLES); qglColor(faceColors[i]); for (int j = 0; j < 3; ++j) { glVertex3f(coords[i][j][0], coords[i][j][1], coords[i][j][2]); } glEnd(); } }

Dans draw(), nous dessinons le ttradre, en tenant compte des rotations x, y et z et des couleurs conserves dans le tableau faceColors. Tout est en code OpenGL standard, sauf lappel de qglColor(). Nous aurions pu choisir plutt une des fonctions OpenGL glColor3d() ou glIndex() en fonction du mode.
void Tetrahedron::mousePressEvent(QMouseEvent *event) { lastPos = event->pos(); } void Tetrahedron::mouseMoveEvent(QMouseEvent *event) { GLfloat dx = GLfloat(event->x() - lastPos.x()) / width(); GLfloat dy = GLfloat(event->y() - lastPos.y()) / height(); if (event->buttons() & Qt::LeftButton) { rotationX += 180 * dy; rotationY += 180 * dx; updateGL(); } else if (event->buttons() & Qt::RightButton) { rotationX += 180 * dy; rotationZ += 180 * dx; updateGL(); } lastPos = event->pos(); }

Les fonctions mousePressEvent() et mouseMoveEvent() sont rimplmentes dans QWidget pour permettre lutilisateur de faire pivoter la vue en cliquant dessus et en la faisant glisser.

Chapitre 8

Graphiques 2D et 3D

211

Le bouton gauche de la souris contrle la rotation autour des axes x et y, et le bouton droit autour des axes x et z. Aprs avoir modi la variable rotationX et une des variables rotationY ou rotationZ, nous appelons updateGL() pour redessiner la scne.
void Tetrahedron::mouseDoubleClickEvent(QMouseEvent *event) { int face = faceAtPosition(event->pos()); if (face!= -1) { QColor color = QColorDialog::getColor(faceColors[face], this); if (color.isValid()) { faceColors[face] = color; updateGL(); } } }

mouseDoubleClickEvent() est rimplmente dans QWidget pour permettre lutilisateur de dnir la couleur dune des faces du ttradre en double-cliquant dessus. Nous invoquons la fonction prive faceAtPosition() pour dterminer quelle face se situe sous le curseur, sil y en a une. Si lutilisateur a double-cliqu sur une face, nous appelons QColorDialog::getColor() pour obtenir une nouvelle couleur pour cette face. Nous mettons ensuite jour le tableau faceColors pour tenir compte de la nouvelle couleur et nous invoquons updateGL() pour redessiner la scne.
int Tetrahedron::faceAtPosition(const QPoint &pos) { const int MaxSize = 512; GLuint buffer[MaxSize]; GLint viewport[4]; glGetIntegerv(GL_VIEWPORT, viewport); glSelectBuffer(MaxSize, buffer); glRenderMode(GL_SELECT); glInitNames(); glPushName(0); glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); gluPickMatrix(GLdouble(pos.x()), GLdouble(viewport[3] - pos.y()), 5.0, 5.0, viewport); GLfloat x = GLfloat(width()) / height(); glFrustum(-x, x, -1.0, 1.0, 4.0, 15.0); draw(); glMatrixMode(GL_PROJECTION); glPopMatrix();

212

Qt4 et C++ : Programmation dinterfaces GUI

if (!glRenderMode(GL_RENDER)) return -1; return buffer[3]; }

La fonction faceAtPosition() retourne le nombre de faces une certaine position sur le widget, ou 1 si aucune face ne se trouve cet endroit. Le code OpenGL permettant de dterminer ceci est quelque peu complexe. En rsum, nous afchons la scne en mode GL_SELECT pour proter des fonctionnalits de slection dOpenGL, puis nous rcuprons le numro de la face (son "nom") dans lenregistrement du nombre daccs dOpenGL. Voici main.cpp:
#include <QApplication> #include <iostream> #include "tetrahedron.h" using namespace std; int main(int argc, char *argv[]) { QApplication app(argc, argv); if (!QGLFormat::hasOpenGL()) { cerr << "This system has no OpenGL support" << endl; return 1; } Tetrahedron tetrahedron; tetrahedron.setWindowTitle(QObject::tr("Tetrahedron")); tetrahedron.resize(300, 300); tetrahedron.show(); return app.exec(); }

Si le systme de lutilisateur ne prend pas en charge OpenGL, nous imprimons un message derreur sur la console et nous retournons immdiatement. Pour associer lapplication au module QtOpenGL et la bibliothque OpenGL du systme, le chier .pro doit contenir cette entre :
QT += opengl

Lapplication Tetrahedron est termine. Pour plus dinformations sur le module QtOpenGL, consultez la documentation de rfrence de QGLWidget, QGLFormat, QGLContext, QGLColormap et QGLPixelBuffer.

9
Glisser-dposer
Au sommaire de ce chapitre Activer le glisser-dposer Prendre en charge les types personnaliss de glisser Grer le presse-papiers

Le glisser-dposer est un moyen moderne et intuitif de transfrer des informations dans une application ou entre diffrentes applications. Cette technique est souvent propose en complment du support du presse-papiers pour dplacer et copier des donnes. Dans ce chapitre, nous verrons comment ajouter la prise en charge du glisser-dposer une application et comment grer des formats personnaliss. Nous tudierons galement la manire de rutiliser le code du glisser-dposer pour ajouter le support du pressepapiers. Cette rutilisation du code est possible parce que les deux mcanismes sont bass sur QMimeData, une classe capable de fournir des donnes dans divers formats.

214

Qt4 et C++ : Programmation dinterfaces GUI

Activer le glisser-dposer
Le glisser-dposer implique deux actions distinctes : glisser et dposer. On peut faire glisser et/ou dposer des lments sur les widgets Qt. Notre premier exemple vous prsente comment faire accepter une application Qt un glisser initi par une autre application. Lapplication Qt est une fentre principale avec un widget central QTextEdit. Quand lutilisateur fait glisser un chier texte du bureau ou de lexplorateur de chiers vers lapplication, celle-ci charge le chier dans le QTextEdit. Voici la dnition de la classe MainWindow de notre exemple :
class MainWindow: public QMainWindow { Q_OBJECT public: MainWindow(); protected: void dragEnterEvent(QDragEnterEvent *event); void dropEvent(QDropEvent *event); private: bool readFile(const QString &fileName); QTextEdit *textEdit; };

La classe MainWindow rimplmente dragEnterEvent() et dropEvent() dans QWidget. Vu que lobjectif de notre exemple est de prsenter le glisser-dposer, la majorit des fonctionnalits quune classe de fentre principale devrait contenir a t omise.
MainWindow::MainWindow() { textEdit = new QTextEdit; setCentralWidget(textEdit); textEdit->setAcceptDrops(false); setAcceptDrops(true); setWindowTitle(tr("Text Editor")); }

Dans le constructeur, nous crons un QTextEdit et nous le dnissons comme widget central. Par dfaut, QTextEdit accepte des glisser sous forme de texte provenant dautres applications, et si lutilisateur y dpose un chier, le nom de chier sera intgr dans le texte. Les vnements drop tant transmis de lenfant au parent, nous obtenons les vnements drop pour toute la fentre dans MainWindow en dsactivant le dposer dans QTextEdit et en lactivant dans la fentre principale.

Chapitre 9

Glisser-dposer

215

void MainWindow::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasFormat("text/uri-list")) event->acceptProposedAction(); }

dragEnterEvent() est appele ds que lutilisateur fait glisser un objet sur un widget. Si nous invoquons acceptProposedAction() sur lvnement, nous indiquons que lutilisateur est en mesure de dposer cet objet sur ce widget. Par dfaut, le widget naccepterait pas le glisser. Qt modie automatiquement le pointeur pour signaler lutilisateur si le widget est en mesure daccepter le dpt.
Dans notre cas, nous voulons que lutilisateur puisse faire glisser des chiers, mais rien dautre. Pour ce faire, nous vrions le type MIME du glisser. Le type MIME text/uri-list est utilis pour stocker une liste dURI (universal resource identi?er), qui peuvent tre des noms de chiers, des URL (comme des chemins daccs HTTP ou FTP) ou dautres identiants globaux de ressource. Les types MIME standards sont dnis par lIANA (Internet Assigned Numbers Authority). Ils sont constitus dun type et dun sous-type spars par un slash. Les types MIME sont employs par le presse-papiers et par le systme du glisser-dposer pour identier les diffrents types de donnes. La liste ofcielle des types MIME est disponible ladresse suivante : http://www.iana.org/assignments/media-types/.
void MainWindow::dropEvent(QDropEvent *event) { QList<QUrl> urls = event->mimeData()->urls(); if (urls.isEmpty()) return; QString fileName = urls.first().toLocalFile(); if (fileName.isEmpty()) return; if (readFile(fileName)) setWindowTitle(tr("%1 - %2").arg(fileName) .arg(tr("Drag File"))); }

dropEvent() est appele ds que lutilisateur dpose un objet sur un widget. Nous appelons QMimeData::urls() pour obtenir une liste des QUrl. En gnral, les utilisateurs ne font glisser quun seul chier la fois, mais il est possible den faire glisser plusieurs en mme temps grce une slection. Sil y a plusieurs URL ou si lURL ne correspond pas un nom de chier local, nous retournons immdiatement. QWidget propose aussi dragMoveEvent() et dragLeaveEvent(), mais ces fonctions nont gnralement pas besoin dtre rimplmentes. Le deuxime exemple illustre la faon dinitier un glisser et daccepter un dposer. Nous allons crer une sous-classe QListWidget qui prend en charge le glisser-dposer et

216

Qt4 et C++ : Programmation dinterfaces GUI

nous lutiliserons en tant que composant de lapplication Project Chooser prsente en Figure 9.1.
Figure 9.1 Lapplication Project Chooser

Lapplication Project Chooser prsente lutilisateur deux widgets liste remplis de noms. Chaque widget reprsente un projet. Lutilisateur peut faire glisser et dposer les noms dans les widgets liste pour dplacer une personne dun projet un autre. Le code du glisser-dposer se situe en globalit dans la sous-classe QListWidget. Voici la dnition de classe :
class ProjectListWidget: public QListWidget { Q_OBJECT public: ProjectListWidget(QWidget *parent = 0); protected: void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void dragEnterEvent(QDragEnterEvent *event); void dragMoveEvent(QDragMoveEvent *event); void dropEvent(QDropEvent *event); private: void startDrag(); QPoint startPos; };

La classe ProjectListWidget rimplmente cinq gestionnaires dvnements dclars dans QWidget.


ProjectListWidget::ProjectListWidget(QWidget *parent) : QListWidget(parent) { setAcceptDrops(true); }

Chapitre 9

Glisser-dposer

217

Dans le constructeur, nous activons le dposer sur le widget liste.


void ProjectListWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) startPos = event->pos(); QListWidget::mousePressEvent(event); }

Quand lutilisateur appuie sur le bouton gauche de la souris, nous stockons lemplacement de cette dernire dans la variable prive startPos. Nous appelons limplmentation de mousePressEvent() du QListWidget pour nous assurer que ce dernier a la possibilit de traiter des vnements "bouton souris enfonc" comme dhabitude.
void ProjectListWidget::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { int distance = (event->pos() - startPos).manhattanLength(); if (distance >= QApplication::startDragDistance()) startDrag(); } QListWidget::mouseMoveEvent(event); }

Quand lutilisateur dplace le pointeur tout en maintenant le bouton gauche de la souris enfonc, nous commenons un glisser. Nous calculons la distance entre la position actuelle de la souris et la position o le bouton gauche a t enfonc. Si la distance est suprieure la distance recommande pour dmarrer un glisser de QApplication (normalement 4 pixels), nous appelons la fonction prive startDrag() pour dbuter le glisser. Ceci vite dinitier un glisser si la main de lutilisateur a trembl.
void ProjectListWidget::startDrag() { QListWidgetItem *item = currentItem(); if (item) { QMimeData *mimeData = new QMimeData; mimeData->setText(item->text()); QDrag *drag = new QDrag(this); drag->setMimeData(mimeData); drag->setPixmap(QPixmap(":/images/person.png")); if (drag->start(Qt::MoveAction) == Qt::MoveAction) delete item; } }

Dans startDrag(), nous crons un objet de type QDrag, this tant son parent. Lobjet QDrag enregistre les donnes dans un objet QMimeData. Dans notre exemple, nous fournissons les donnes sous forme de chane text/plain au moyen de QMimeData::setText(). QMimeData propose plusieurs fonctions permettant de grer les types les plus courants de glisser (images, URL, couleurs, etc.) et peut grer des types MIME arbitraires reprsents comme

218

Qt4 et C++ : Programmation dinterfaces GUI

tant des QByteArray. Lappel de QDrag::setPixmap() dnit licne qui suit le pointeur pendant le glisser. Lappel de QDrag::start() dbute le glisser et se bloque jusqu ce que lutilisateur dpose ou annule le glisser. Elle reoit en argument une combinaison des glisser pris en charge (Qt::CopyAction, Qt::MoveAction et Qt::LinkAction) et retourne le glisser qui a t excut (ou Qt::IgnoreAction si aucun glisser na t excut). Laction excute dpend de ce que le widget source autorise, de ce que la cible supporte et des touches de modication enfonces au moment de dposer. Aprs lappel de start(), Qt prend possession de lobjet gliss et le supprimera quand il ne sera plus ncessaire.
void ProjectListWidget::dragEnterEvent(QDragEnterEvent *event) { ProjectListWidget *source = qobject_cast<ProjectListWidget *>(event->source()); if (source && source!= this) { event->setDropAction(Qt::MoveAction); event->accept(); } }

Le widget ProjectListWidget ne sert pas uniquement initialiser des glisser, il accepte aussi des glisser provenant dun autre ProjectListWidget de la mme application. QDragEnterEvent::source() retourne un pointeur vers le widget lorigine du glisser si ce widget fait partie de la mme application ; sinon elle renvoie un pointeur nul. Nous utilisons qobject_cast<T>() pour nous assurer que le glisser provient dun ProjectListWidget. Si tout est correct, nous informons Qt que nous sommes prts accepter laction en tant quaction de dplacement.
void ProjectListWidget::dragMoveEvent(QDragMoveEvent *event) { ProjectListWidget *source = qobject_cast<ProjectListWidget *>(event->source()); if (source && source!= this) { event->setDropAction(Qt::MoveAction); event->accept(); } }

Le code dans dragMoveEvent() est identique ce que nous effectu dans dragEnterEvent(). Il est ncessaire parce que nous devons remplacer limplmentation de la fonction dans QListWidget (en fait dans QAbstractItemView).
void ProjectListWidget::dropEvent(QDropEvent *event) { ProjectListWidget *source = qobject_cast<ProjectListWidget *>(event->source()); if (source && source!= this) { addItem(event->mimeData()->text()); event->setDropAction(Qt::MoveAction);

Chapitre 9

Glisser-dposer

219

event->accept(); } }

Dans dropEvent(), nous rcuprons le texte gliss laide de QMimeData::text() et nous crons un lment avec ce texte. Nous avons galement besoin daccepter lvnement comme tant une "action de dplacement" an de signaler au widget source quil peut maintenant supprimer la version originale de llment gliss. Le glisser-dposer est un mcanisme puissant permettant de transfrer des donnes entre des applications. Cependant, dans certains cas, il est possible dimplmenter le glisser-dposer sans utiliser les fonctionnalits de glisser-dposer de Qt. Si tout ce que nous souhaitons se limite dplacer des donnes dans un widget dune application, il suft de rimplmenter mousePressEvent() et mouseReleaseEvent().

Prendre en charge les types personnaliss de glisser


Jusqu prsent, nous nous sommes bass sur la prise en charge de QMimeData des types MIME communs. Nous avons donc appel QMimeData::setText() pour crer un glisser de texte et nous avons excut QMimeData:urls() pour rcuprer le contenu dun glisser text/uri-list. Si nous voulons faire glisser du texte brut, du texte HTML, des images, des URL ou des couleurs, nous pouvons employer QMimeData sans formalit. Mais si nous souhaitons faire glisser des donnes personnalises, nous devons faire un choix entre plusieurs possibilits : 1. Nous pouvons fournir des donnes arbitraires sous forme de QByteArray en utilisant QMimeData::setData() et les extraire ultrieurement avec QMimeData::data(). 2. Nous pouvons driver QMimeData et rimplmenter formats() et retrieveData() pour grer nos types personnaliss de donnes. 3. Sagissant du glisser-dposer dans une seule application, nous avons la possibilit de driver QMimeData et de stocker les donnes dans la structure de notre choix. La premire approche nimplique pas de drivation, mais prsente certains inconvnients : nous devons convertir notre structure de donnes en QByteArray mme si le glisser nest pas accept la n, et si nous voulons proposer plusieurs types MIME pour interagir correctement avec une large gamme dapplications, nous devons enregistrer les donnes plusieurs fois (une fois pour chaque type MIME). Si les donnes sont nombreuses, lapplication peut tre ralentie inutilement. Les deuxime et troisime approches permettent dviter ou de minimiser ces problmes. Ainsi, nous avons un contrle total et nous pouvons les utiliser ensemble. Pour vous prsenter le fonctionnement de ces approches, nous vous montrerons comment ajouter des fonctions de glisser-dposer un QTableWidget. Le glisser prendra en charge les types

220

Qt4 et C++ : Programmation dinterfaces GUI

MIME suivants : text/plain, text/html et text/csv. En utilisant la premire approche, voici comment dbute un glisser :
void MyTableWidget::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { int distance = (event->pos() - startPos).manhattanLength(); if (distance >= QApplication::startDragDistance()) startDrag(); } QTableWidget::mouseMoveEvent(event); } void MyTableWidget::startDrag() { QString plainText = selectionAsPlainText(); if (plainText.isEmpty()) return; QMimeData *mimeData = new QMimeData; mimeData->setText(plainText); mimeData->setHtml(toHtml(plainText)); mimeData->setData("text/csv", toCsv(plainText).toUtf8()); QDrag *drag = new QDrag(this); drag->setMimeData(mimeData); if (drag->start(Qt::CopyAction | Qt::MoveAction) == Qt::MoveAction) deleteSelection(); }

La fonction prive startDrag() a t invoque dans mouseMoveEvent() pour commencer faire glisser une slection rectangulaire. Nous dnissons les types MIME text/plain et text/ html avec setText() et setHtml() et nous congurons le type text/csv avec setData(), qui reoit un type MIME arbitraire et un QByteArray. Le code de selectionAsString() est plus ou moins le mme que la fonction Spreadsheet::copy() du Chapitre 4.
QString MyTableWidget::toCsv(const QString &plainText) { QString result = plainText; result.replace("\\", "\\\\"); result.replace("\"", "\\\""); result.replace("\t", "\", \""); result.replace("\n", "\"\n\""); result.prepend("\""); result.append("\""); return result; } QString MyTableWidget::toHtml(const QString &plainText) { QString result = Qt::escape(plainText); result.replace("\t", "<td>");

Chapitre 9

Glisser-dposer

221

result.replace("\n", "\n<tr><td>"); result.prepend("<table>\n<tr><td>"); result.append("\n</table>"); return result; }

Les fonctions toCsv() et toHtml() convertissent une chane "tabulations et sauts de ligne" en une chane CSV (comma-separated values, valeurs spares par des virgules) ou HTML. Par exemple, les donnes
Red Cyan Green Yellow Blue Magenta

sont converties en
"Red", "Cyan", "Green", "Yellow", "Blue" "Magenta"

ou en
<table> <tr><td>Red<td>Green<td>Blue <tr><td>Cyan<td>Yellow<td>Magenta </table>

La conversion est effectue de la manire la plus simple possible, en excutant QString::replace(). Pour viter les caractres spciaux HTML, nous employons Qt::escape().
void MyTableWidget::dropEvent(QDropEvent *event) { if (event->mimeData()->hasFormat("text/csv")) { QByteArray csvData = event->mimeData()->data("text/csv"); QString csvText = QString::fromUtf8(csvData); ... event->acceptProposedAction(); } else if (event->mimeData()->hasFormat("text/plain")) { QString plainText = event->mimeData()->text(); ... event->acceptProposedAction(); } }

Mme si nous fournissons les donnes dans trois formats diffrents, nous nacceptons que deux dentre eux dans dropEvent(). Si lutilisateur fait glisser des cellules depuis un QTableWidget vers un diteur HTML, nous voulons que les cellules soient converties en un tableau HTML. Mais si lutilisateur fait glisser un code HTML arbitraire vers un QTableWidget, nous ne voulons pas laccepter. Pour que cet exemple fonctionne, nous devons galement appeler setAcceptDrops(true) et setSelectionMode(ContiguousSelection) dans le constructeur de MyTableWidget.

222

Qt4 et C++ : Programmation dinterfaces GUI

Nous allons refaire notre exemple, mais cette fois-ci nous driverons QMimeData pour ajourner ou viter les conversions (potentiellement onreuses en termes de performances) entre QTableWidgetItem et QByteArray. Voici la dnition de notre sous-classe :
class TableMimeData: public QMimeData { Q_OBJECT public: TableMimeData(const QTableWidget *tableWidget, const QTableWidgetSelectionRange &range); const QTableWidget *tableWidget() const { return myTableWidget; } QTableWidgetSelectionRange range() const { return myRange; } QStringList formats() const; protected: QVariant retrieveData(const QString &format, QVariant::Type preferredType) const; private: static QString toHtml(const QString &plainText); static QString toCsv(const QString &plainText); QString text(int row, int column) const; QString rangeAsPlainText() const; const QTableWidget *myTableWidget; QTableWidgetSelectionRange myRange; QStringList myFormats; };

Au lieu de stocker les donnes relles, nous enregistrons un QTableWidgetSelectionRange qui spcie quelles cellules ont t glisses et nous conservons un pointeur vers QTableWidget. Les fonctions formats() et retrieveData() sont rimplmentes dans QMimeData.
TableMimeData::TableMimeData(const QTableWidget *tableWidget, const QTableWidgetSelectionRange &range) { myTableWidget = tableWidget; myRange = range; myFormats << "text/csv" << "text/html" << "text/plain"; }

Dans le constructeur, nous initialisons les variables prives.


QStringList TableMimeData::formats() const { return myFormats; }

Chapitre 9

Glisser-dposer

223

La fonction formats() retourne une liste de types MIME fournie par lobjet MIME. Lordre prcis des formats nest gnralement pas important, mais il est recommand de placer les "meilleurs" formats en premier. Il arrive en effet que les applications qui prennent en charge de nombreux formats choisissent le premier qui convient.
QVariant TableMimeData::retrieveData(const QString &format, QVariant::Type preferredType) const { if (format == "text/plain") { return rangeAsPlainText(); } else if (format == "text/csv") { return toCsv(rangeAsPlainText()); } else if (format == "text/html") { return toHtml(rangeAsPlainText()); } else { return QMimeData::retrieveData(format, preferredType); } }

La fonction retrieveData() retourne les donnes dun type MIME particulier sous forme de QVariant. La valeur du paramtre de format correspond normalement une des chanes retournes par formats(), mais nous ne pouvons pas en attester, tant donn que toutes les applications ne comparent pas le type MIME formats(). Les fonctions daccs text(), html(), urls(), imageData(), colorData() et data() proposes par QMimeData sont implmentes en termes de retrieveData(). Le paramtre preferredType est un bon indicateur du type que nous devrions placer dans QVariant. Ici, nous lignorons et nous faisons conance QMimeData pour convertir la valeur de retour dans le type souhait, si ncessaire.
void MyTableWidget::dropEvent(QDropEvent *event) { const TableMimeData *tableData = qobject_cast<const TableMimeData *>(event->mimeData()); if (tableData) { const QTableWidget *otherTable = tableData->tableWidget(); QTableWidgetSelectionRange otherRange = tableData->range(); ... event->acceptProposedAction(); } else if (event->mimeData()->hasFormat("text/csv")) { QByteArray csvData = event->mimeData()->data("text/csv"); QString csvText = QString::fromUtf8(csvData); ... event->acceptProposedAction(); } else if (event->mimeData()->hasFormat("text/plain")) { QString plainText = event->mimeData()->text(); ... event->acceptProposedAction(); } QTableWidget::mouseMoveEvent(event); }

224

Qt4 et C++ : Programmation dinterfaces GUI

La fonction dropEvent() ressemble celle prsente prcdemment dans cette section, mais cette fois-ci nous loptimisons en vriant dabord si nous pouvons convertir en toute scurit lobjet QMimeData en TableMimeData. Si qobject_cast<T>() fonctionne, cela signie quun MyTableWidget de la mme application tait lorigine du glisser, et nous pouvons directement accder aux donnes de la table au lieu de passer par lAPI de QMimeData. Si la conversion choue, nous extrayons les donnes de faon habituelle. Dans cet exemple, nous avons cod le texte CSV avec le format de codage UTF-8. Si nous voulions tre srs dutiliser le bon codage, nous aurions pu utiliser le paramtre charset du type MIME text/plain de sorte spcier un codage explicite. Voici quelques exemples :
text/plain;charset=US-ASCII text/plain;charset=ISO-8859-1 text/plain;charset=Shift_JIS text/plain;charset=UTF-8

Grer le presse-papiers
La plupart des applications emploient la gestion intgre du presse-papiers de Qt dune manire ou dune autre. Par exemple, la classe QTextEdit propose les slots cut(), copy() et paste(), de mme que des raccourcis clavier. Vous navez donc pas besoin de code supplmentaire, ou alors trs peu. Quand vous crivez vos propres classes, vous pouvez accder au presse-papiers par le biais de QApplication::clipboard(), qui retourne un pointeur vers lobjet QClipboard de lapplication. Grer le presse-papiers savre assez facile : vous appelez setText(), setImage() ou setPixmap() pour placer des donnes dans le presse-papiers, puis vous appelez text(), image() ou pixmap() pour y rcuprer les donnes. Nous avons dj analys des exemples dutilisation du presse-papiers dans lapplication Spreadsheet du Chapitre 4. Pour certaines applications, la fonctionnalit intgre peut tre insufsante. Par exemple, nous voulons pouvoir fournir des donnes qui ne soient pas uniquement du texte ou une image, ou proposer des donnes en diffrents formats pour un maximum dinteroprabilit avec dautres applications. Ce problme ressemble beaucoup ce que nous avons rencontr prcdemment avec le glisser-dposer et la rponse est similaire : nous pouvons driver QMimeData et rimplmenter quelques fonctions virtuelles. Si notre application prend en charge le glisser-dposer via une sous-classe QMimeData personnalise, nous pouvons simplement rutiliser cette sous-classe et la placer dans le presse-papiers en employant la fonction setMimeData(). Pour rcuprer les donnes, nous avons la possibilit dinvoquer mimeData() sur le presse-papiers. Sous X11, il est habituellement possible de coller une slection en cliquant sur le bouton du milieu dune souris dote de trois boutons. Vous faites alors appel un presse-papiers de "slection" distinct. Si vous souhaitez que vos widgets supportent ce genre de presse-papiers en complment du presse-papiers standard, vous devez transmettre QClipboard::Selection

Chapitre 9

Glisser-dposer

225

comme argument supplmentaire aux divers appels du presse-papiers. Voici par exemple comment nous rimplmenterions mouseReleaseEvent() dans un diteur de texte pour prendre en charge le collage avec le bouton du milieu de la souris :
void MyTextEditor::mouseReleaseEvent(QMouseEvent *event) { QClipboard *clipboard = QApplication::clipboard(); if (event->button() == Qt::MidButton && clipboard->supportsSelection()) { QString text = clipboard->text(QClipboard::Selection); pasteText(text); } }

Sous X11, la fonction supportsSelection() retourne true. Sur les autres plates-formes, elle renvoie false. Si vous voulez tre inform ds que le contenu du presse-papiers change, vous pouvez connecter le signal QClipboard::dataChanged() un slot personnalis.

10
Classes dafchage dlments
Au sommaire de ce chapitre Utiliser les classes ddies lafchage dlments Utiliser des modles prdnis Implmenter des modles personnaliss Implmenter des dlgus personnaliss

Beaucoup dapplications ont pour objectif la recherche, lafchage et la modication dlments individuels appartenant un ensemble de donnes. Ces donnes peuvent se trouver dans des chiers, des bases de donnes ou des serveurs de rseau. Lapproche standard relative au traitement des ensembles de donnes consiste utiliser les classes dafchage dlments de Qt. Dans les versions antrieures de Qt, les widgets dafchage dlments taient aliments avec la totalit de lensemble de donnes ; les utilisateurs pouvaient effectuer toutes leurs recherches et modications sur les donnes hberges dans le widget, et un

228

Qt4 et C++ : Programmation dinterfaces GUI

moment donn, les modications taient sauvegardes dans la source de donnes. Mme si elle est simple comprendre et utiliser, cette approche nest pas adapte aux trs grands ensembles de donnes, ni pour lafchage du mme ensemble de donnes dans deux ou plusieurs widgets diffrents. Le langage Smalltalk a fait connatre une mthode exible permettant de visualiser de grands ensembles de donnes : MVC (Modle-Vue-Contrleur). Dans lapproche MVC, le modle reprsente lensemble de donnes et il se charge de rcuprer les donnes ncessaires pour afcher et enregistrer toute modication. Chaque type densemble de donnes possde son propre modle, mais lAPI que les modles proposent aux vues est identique quel que soit lensemble de donnes sous-jacent. La vue prsente les donnes lutilisateur. Seule une quantit limite de donnes dun grand ensemble sera visible en mme temps, cest--dire celles demandes par la vue. Le contrleur sert dintermdiaire entre lutilisateur et la vue ; il convertit les actions utilisateur en requtes pour rechercher ou modier des donnes, que la vue transmet ensuite au modle si ncessaire.
Figure 10.1 Larchitecture modle/vue de Qt
Source de donnes Modle

Dlgu

Vue

Qt propose une architecture modle/vue inspire de lapproche MVC (voir Figure 10.1). Dans Qt, le modle se comporte de la mme manire que pour le MVC classique. Mais la place du contrleur, Qt utilise une notion lgrement diffrente : le dlgu. Le dlgu offre un contrle prcis de la manire dont les lments sont afchs et modis. Qt fournit un dlgu par dfaut pour chaque type de vue. Cest sufsant pour la plupart des applications, cest pourquoi nous navons gnralement pas besoin de nous en proccuper. Grce larchitecture modle/vue de Qt, nous avons la possibilit dutiliser des modles qui ne rcuprent que les donnes ncessaires lafchage de la vue. Nous grons donc de trs grands ensembles de donnes beaucoup plus rapidement et nous consommons moins de mmoire que si nous devions lire toutes les donnes. De plus, en enregistrant un modle avec deux vues ou plus, nous donnons lopportunit lutilisateur dafcher et dinteragir avec les donnes de diffrentes manires, avec peu de surcharge (voir Figure 10.2). Qt synchronise automatiquement plusieurs vues, retant les changements apports dans lune delles dans toutes les autres. Larchitecture modle/vue prsente un autre avantage : si nous dcidons de modier la faon dont lensemble de donnes sous-jacent est enregistr, nous navons qu changer le modle ; les vues continueront se comporter correctement. En gnral, nous ne devons prsenter quun nombre relativement faible dlments lutilisateur. Dans ces cas frquents, nous pouvons utiliser les classes dafchage dlments de Qt (QListWidget, QTableWidget et QTreeWidget) spcialement conues cet effet et directement y enregistrer des lments. Ces classes se comportent de manire similaire aux

Chapitre 10

Classes dafchage dlments

229

classes dafchage dlments proposes par les versions antrieures de Qt. Elles stockent leurs donnes dans des "lments" (par exemple, un QTableWidget contient des QTableWidgetItem). En interne, ces classes sappuient sur des modles personnaliss qui afchent les lments destins aux vues.
Vue de liste 1 Vue de liste 2 Vue de liste 3 Vue tableau 4 Vue tableau 5

Modle

Source de donnes

Figure 10.2 Un modle peut desservir plusieurs vues

Sagissant des grands ensembles de donnes, la duplication des donnes est souvent peu recommande. Dans ces cas, nous pouvons utiliser les vues de Qt (QListView, QTableView, et QTreeView, ), en association avec un modle de donnes, qui peut tre un modle personnalis ou un des modles prdnis de Qt. Par exemple, si lensemble de donnes se trouve dans une base de donnes, nous pouvons combiner un QTableView avec un QSqlTableModel.

Utiliser les classes ddies lafchage dlments


Utiliser les sous-classes dafchage dlments de Qt est gnralement plus simple que de dnir un modle personnalis, et cette solution est plus approprie quand la sparation du modle et de la vue ne prsente aucun intrt particulier. Nous avons employ cette technique dans le Chapitre 4 quand nous avons driv QTableWidget et QTableWidgetItem pour implmenter la fonctionnalit de feuille de calcul. Dans cette section, nous verrons comment utiliser les sous-classes dafchage dlments pour afcher des lments. Le premier exemple vous prsente un QListWidget en lecture seule, le deuxime exemple vous montre un QTableWidget modiable et le troisime vous expose un QTreeWidget en lecture seule. Nous commenons par une bote de dialogue simple qui propose lutilisateur de choisir un symbole dorganigramme dans une liste, comme illustr en Figure 10.3. Chaque lment est compos dune icne, de texte et dun ID unique.

230

Qt4 et C++ : Programmation dinterfaces GUI

Figure 10.3 Lapplication Flowchart Symbol Picker

Analysons dabord un extrait du chier den-tte de la bote de dialogue :


class FlowChartSymbolPicker: public QDialog { Q_OBJECT public: FlowChartSymbolPicker(const QMap<int, QString> &symbolMap, QWidget *parent = 0); int selectedId() const { return id; } void done(int result); ... };

Quand nous construisons la bote de dialogue, nous devons lui transmettre un QMap<int,QString>, et aprs son excution, nous pouvons rcuprer lID choisi (ou 1 si lutilisateur na choisi aucun lment) en appelant selectedId().
FlowChartSymbolPicker::FlowChartSymbolPicker( const QMap<int, QString> &symbolMap, QWidget *parent) : QDialog(parent) { id = -1; listWidget = new QListWidget; listWidget->setIconSize(QSize(60, 60)); QMapIterator<int, QString> i(symbolMap); while (i.hasNext()) { i.next(); QListWidgetItem *item = new QListWidgetItem(i.value(), listWidget); item->setIcon(iconForSymbol(i.value())); item->setData(Qt::UserRole, i.key()); } ... }

Chapitre 10

Classes dafchage dlments

231

Nous initialisons id (le dernier ID slectionn) 1. Puis nous construisons un QListWidget, un widget ddi lafchage dlments. Nous parcourons chaque lment dans la liste des symboles dorganigramme et nous crons un QListWidgetItem pour reprsenter chacun deux. Le constructeur de QListWidgetItem reoit un QString qui reprsente le texte afcher, suivi par le parent QListWidget. Nous dnissons ensuite licne de llment et nous invoquons setData() pour enregistrer notre ID arbitraire dans le QListWidgetItem. La fonction prive iconForSymbol() retourne un QIcon pour un nom de symbole donn. Les QListWidgetItem endossent plusieurs rles, chacun ayant un QVariant associ. Les rles les plus courants sont Qt::DisplayRole, Qt::EditRole et Qt::IconRole, pour lesquels il existe des fonctions ddies daccs et de rglage (setText(), setIcon()). Toutefois, il existe plusieurs autres rles. Nous pouvons aussi dnir des rles personnaliss en spciant une valeur numrique de Qt::UserRole ou plus haut. Dans notre exemple, nous utilisons Qt::UserRole pour stocker lID de chaque lment. La partie non reprsente du constructeur se charge de crer les boutons, de disposer les widgets et de dnir le titre de la fentre.
void FlowChartSymbolPicker::done(int result) { id = -1; if (result == QDialog::Accepted) { QListWidgetItem *item = listWidget->currentItem(); if (item) id = item->data(Qt::UserRole).toInt(); } QDialog::done(result); }

La fonction done() est rimplmente dans QDialog. Elle est appele quand lutilisateur appuie sur OK ou Cancel. Si lutilisateur a cliqu sur OK, nous rcuprons llment pertinent et nous extrayons lID grce la fonction data(). Si nous tions intresss par le texte de llment, nous aurions pu le rcuprer en invoquant item->data(Qt::DisplayRole).toString() ou, ce qui est plus pratique, item->text(). Par dfaut, QListWidget est en lecture seule. Si nous voulions que lutilisateur puisse modier les lments, nous aurions pu dnir les dclencheurs de modication de la vue au moyen de QAbstractItemView::setEditTriggers(); par exemple, congurer QAbstractItemView::AnyKeyPressed signie que lutilisateur peut modier un lment simplement en commenant taper quelque chose. Nous aurions aussi pu proposer un bouton Edit (ou peut-tre des boutons Add et Delete) et les connecter aux slots, de sorte dtre en mesure de grer les oprations de modication par programme. Maintenant que nous avons vu comment utiliser une classe ddie lafchage dlments pour afcher et slectionner des donnes, nous allons tudier un exemple o nous pouvons modier des donnes. Nous utilisons nouveau une bote de dialogue, mais cette fois-ci, elle prsente un ensemble de coordonnes (x, y) que lutilisateur peut modier (voir Figure 10.4).

232

Qt4 et C++ : Programmation dinterfaces GUI

Figure 10.4 Lapplication Coordinate Setter

Comme pour lexemple prcdent, nous nous concentrerons sur le code dafchage de llment, en commenant par le constructeur.
CoordinateSetter::CoordinateSetter(QList<QPointF> *coords, QWidget *parent) : QDialog(parent) { coordinates = coords; tableWidget = new QTableWidget(0, 2); tableWidget->setHorizontalHeaderLabels( QStringList() << tr("X") << tr("Y")); for (int row = 0; row < coordinates->count(); ++row) { QPointF point = coordinates->at(row); addRow(); tableWidget->item(row, 0)->setText(QString::number(point.x())); tableWidget->item(row, 1)->setText(QString::number(point.y())); } ... }

Le constructeur de QTableWidget reoit le nombre initial de lignes et de colonnes du tableau afcher. Chaque lment dans un QTableWidget est reprsent par un QTableWidgetItem, y compris les en-ttes horizontaux et verticaux. La fonction setHorizontalHeaderLabels() inscrit dans chaque lment horizontal du widget tableau le texte qui lui est fourni sous forme dune liste de chanes en argument. Par dfaut, QTableWidget propose un en-tte vertical avec des lignes intitules partir de 1, ce qui correspond exactement ce que nous recherchons, nous ne sommes donc pas contraints de congurer manuellement les intituls de len-tte vertical. Une fois que nous avons cr et centr les intituls des colonnes, nous parcourons les coordonnes transmises. Pour chaque paire (x, y), nous crons deux QTableWidgetItem correspondant aux coordonnes x et y. Les lments sont ajouts au tableau grce QTableWidget::setItem(), qui reoit une ligne et une colonne en plus de llment.

Chapitre 10

Classes dafchage dlments

233

Par dfaut, QTableWidget autorise les modications. Lutilisateur peut modier toute cellule du tableau en la recherchant puis en appuyant sur F2 ou simplement en saisissant quelque chose. Tous les changements effectus par lutilisateur dans la vue se reteront automatiquement dans les QTableWidgetItem. Pour viter les modications, nous avons la possibilit dappeler setEditTriggers(QAbstractItemView::NoEditTriggers).
void CoordinateSetter::addRow() { int row = tableWidget->rowCount(); tableWidget->insertRow(row); QTableWidgetItem *item0 = new QTableWidgetItem; item0->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); tableWidget->setItem(row, 0, item0); QTableWidgetItem *item1 = new QTableWidgetItem; item1->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); tableWidget->setItem(row, 1, item1); tableWidget->setCurrentItem(item0); }

Le slot addRow() est appel lorsque lutilisateur clique sur le bouton Add Row. Nous ajoutons une nouvelle ligne laide de insertRow(). Si lutilisateur essaie de modier une cellule dans la nouvelle ligne, QTableWidget crera automatiquement un nouveau QTableWidgetItem.
void CoordinateSetter::done(int result) { if (result == QDialog::Accepted) { coordinates->clear(); for (int row = 0; row < tableWidget->rowCount(); ++row) { double x = tableWidget->item(row, 0)->text().toDouble(); double y = tableWidget->item(row, 1)->text().toDouble(); coordinates->append(QPointF(x, y)); } } QDialog::done(result); }

Enn, quand lutilisateur clique sur OK, nous effaons les coordonnes qui avaient t transmises la bote de dialogue et nous crons un nouvel ensemble bas sur les coordonnes des lments du QTableWidget. Pour notre troisime et dernier exemple de widget ddi lafchage dlments de Qt, nous allons analyser quelques extraits de code dune application qui afche les paramtres dune application Qt grce un QTreeWidget (voir Figure 10.5). La lecture seule est loption par dfaut de QTreeWidget.

234

Qt4 et C++ : Programmation dinterfaces GUI

Figure 10.5 Lapplication Settings Viewer

Voici un extrait du constructeur :


SettingsViewer::SettingsViewer(QWidget *parent) : QDialog(parent) { organization = "Trolltech"; application = "Designer"; treeWidget = new QTreeWidget; treeWidget->setColumnCount(2); treeWidget->setHeaderLabels( QStringList() << tr("Key") << tr("Value")); treeWidget->header()->setResizeMode(0, QHeaderView::Stretch); treeWidget->header()->setResizeMode(1, QHeaderView::Stretch); ... setWindowTitle(tr("Settings Viewer")); readSettings(); }

Pour accder aux paramtres dune application, un objet QSettings doit tre cr avec le nom de lorganisation et le nom de lapplication comme paramtres. Nous dnissons des noms par dfaut ("Designer" par "Trolltech"), puis nous construisons un nouveau QTreeWidget. Pour terminer, nous appelons la fonction readSettings().
void SettingsViewer::readSettings() { QSettings settings(organization, application); treeWidget->clear(); addChildSettings(settings, 0, ""); treeWidget->sortByColumn(0); treeWidget->setFocus(); setWindowTitle(tr("Settings Viewer - %1 by %2") .arg(application).arg(organization)); }

Chapitre 10

Classes dafchage dlments

235

Les paramtres dapplication sont stocks dans une hirarchie de cls et de valeurs. La fonction prive addChildSettings() reoit un objet settings, un parent QTreeWidgetItem et le "groupe" en cours. Un groupe est lquivalent QSettings dun rpertoire de systme de chiers. La fonction addChildSettings() peut sappeler elle-mme de manire rcursive pour faire dler une arborescence arbitraire. Le premier appel de la fonction readSettings() transmet 0 comme lment parent pour reprsenter la racine.
void SettingsViewer::addChildSettings(QSettings &settings, QTreeWidgetItem *parent, const QString &group) { QTreeWidgetItem *item; settings.beginGroup(group); foreach (QString key, settings.childKeys()) { if (parent) { item = new QTreeWidgetItem(parent); } else { item = new QTreeWidgetItem(treeWidget); } item->setText(0, key); item->setText(1, settings.value(key).toString()); } foreach (QString group, settings.childGroups()) { if (parent) { item = new QTreeWidgetItem(parent); } else { item = new QTreeWidgetItem(treeWidget); } item->setText(0, group); addChildSettings(settings, item, group); } settings.endGroup(); }

La fonction addChildSettings() est utilise pour crer tous les QTreeWidgetItem. Elle parcourt toutes les cls au niveau en cours dans la hirarchie des paramtres et cre un QTableWidgetItem par cl. Si 0 est transmis en tant qulment parent, nous crons llment comme tant un enfant de QTreeWidget (il devient donc un lment de haut niveau) ; sinon, nous crons llment comme tant un enfant de parent. La premire colonne correspond au nom de la cl et la seconde la valeur correspondante. La fonction parcourt ensuite chaque groupe du niveau en cours. Pour chacun deux, un nouveau QTreeWidgetItem est cr avec sa premire colonne dnie en nom du groupe. Puis, la fonction sappelle elle-mme de manire rcursive avec llment de groupe comme parent pour alimenter le QTreeWidget avec les lments enfants du groupe. Les widgets dafchage dlments prsents dans cette section nous permettent dutiliser un style de programmation trs similaire celui utilis dans les versions antrieures de Qt : lire tout un ensemble de donnes dans un widget dafchage dlments, utiliser les objets des

236

Qt4 et C++ : Programmation dinterfaces GUI

lments pour reprsenter les lments de donnes, et (si les lments sont modiables) sauvegarder sur la source de donnes. Dans les sections suivantes, nous irons plus loin que cette approche simple et nous proterons pleinement de larchitecture modle/vue de Qt.

Utiliser des modles prdnis


Qt propose plusieurs modles prdnis utiliser avec les classes dafchage :
QStringListModel QStandardItemModel QDirModel QSqlQueryModel QSqlTableModel QSqlRelationalTableModel QSortFilterProxyModel Stocke une liste de chanes Stocke des donnes hirarchiques arbitraires Encapsule le systme de chiers local Encapsule un jeu de rsultats SQL Encapsule une table SQL Encapsule une table SQL avec des cls trangres Trie et/ou ltre un autre modle

Dans cette section, nous verrons comment employer QStringListModel, QDirModel et QSortFilterProxyModel. Les modles SQL sont traits au Chapitre 13. Commenons par une bote de dialogue simple dont les utilisateurs peuvent se servir pour ajouter, supprimer et modier un QStringList, o chaque chane reprsente un chef dquipe. Celle-ci est prsente en Figure 10.6.
Figure 10.6 Lapplication Team Leaders

Voici un extrait pertinent du constructeur :


TeamLeadersDialog::TeamLeadersDialog(const QStringList &leaders, QWidget *parent)

Chapitre 10

Classes dafchage dlments

237

: QDialog(parent) { model = new QStringListModel(this); model->setStringList(leaders); listView = new QListView; listView->setModel(model); listView->setEditTriggers(QAbstractItemView::AnyKeyPressed | QAbstractItemView::DoubleClicked); ... }

Nous crons et alimentons dabord un QStringListModel. Nous crons ensuite un QListView et nous lui affectons comme modle un de ceux que nous venons de crer. Nous congurons galement des dclencheurs de modication pour permettre lutilisateur de modier une chane simplement en commenant taper quelque chose ou en double-cliquant dessus. Par dfaut, aucun dclencheur de modication nest dni sur un QListView, la vue est donc congure en lecture seule.
void TeamLeadersDialog::insert() { int row = listView->currentIndex().row(); model->insertRows(row, 1); QModelIndex index = model->index(row); listView->setCurrentIndex(index); listView->edit(index); }

Le slot insert() est invoqu lorsque lutilisateur clique sur le bouton Insert. Le slot commence par rcuprer le numro de ligne de llment en cours dans la vue de liste. Chaque lment de donnes dans un modle possde un "index de modle" correspondant qui est reprsent par un objet QModelIndex. Nous allons tudier les index de modle plus en dtail dans la prochaine section, mais pour linstant il suft de savoir quun index comporte trois composants principaux : une ligne, une colonne et un pointeur vers le modle auquel il appartient. Pour un modle liste unidimensionnel, la colonne est toujours 0. Lorsque nous connaissons le numro de ligne, nous insrons une nouvelle ligne cet endroit. Linsertion est effectue sur le modle et le modle met automatiquement jour la vue de liste. Nous dnissons ensuite lindex en cours de la vue de liste sur la ligne vide que nous venons dinsrer. Enn, nous dnissons la vue de liste en mode de modication sur la nouvelle ligne, comme si lutilisateur avait appuy sur une touche ou double-cliqu pour initier la modication.
void TeamLeadersDialog::del() { model->removeRows(listView->currentIndex().row(), 1); }

Dans le constructeur, le signal clicked() du bouton Delete est reli au slot del(). Vu que nous avons supprim la ligne en cours, nous pouvons appeler removeRows() avec la position

238

Qt4 et C++ : Programmation dinterfaces GUI

actuelle dindex et un nombre de lignes de 1. Comme avec linsertion, nous nous basons sur le modle pour mettre jour la vue de faon approprie.
QStringList TeamLeadersDialog::leaders() const { return model->stringList(); }

Enn, la fonction leaders() procure un moyen de lire les chanes modies quand la bote de dialogue est ferme. TeamLeadersDialog pourrait devenir une bote de dialogue gnrique de modication de liste de chanes simplement en paramtrant le titre de sa fentre. Une autre bote de dialogue gnrique souvent demande est une bote qui prsente une liste de chiers ou de rpertoires lutilisateur. Le prochain exemple exploite la classe QDirModel, qui encapsule le systme de chiers de lordinateur et qui peut afcher (et masquer) les divers attributs de chiers. Ce modle peut appliquer un ltre pour limiter les types dentres du systme de chiers qui sont afches et peut organiser les entres de plusieurs manires diffrentes.
Figure 10.7 Lapplication Directory Viewer

Nous analyserons dabord la cration et nous congurerons le modle et la vue dans le constructeur de la bote de dialogue Directory Viewer (voir Figure 10.7).
DirectoryViewer::DirectoryViewer(QWidget *parent) : QDialog(parent) { model = new QDirModel; model->setReadOnly(false); model->setSorting(QDir::DirsFirst | QDir::IgnoreCase | QDir::Name); treeView = new QTreeView; treeView->setModel(model); treeView->header()->setStretchLastSection(true); treeView->header()->setSortIndicator(0, Qt::AscendingOrder); treeView->header()->setSortIndicatorShown(true); treeView->header()->setClickable(true);

Chapitre 10

Classes dafchage dlments

239

QModelIndex index = model->index(QDir::currentPath()); treeView->expand(index); treeView->scrollTo(index); treeView->resizeColumnToContents(0); ... }

Lorsque le modle a t construit, nous faisons le ncessaire pour quil puisse tre modi et nous dnissons les divers attributs dordre de tri. Nous crons ensuite le QTreeView qui afchera les donnes du modle. Len-tte du QTreeView peut tre utilis pour proposer un tri contrl par lutilisateur. Si cet en-tte est cliquable, lutilisateur est en mesure de trier nimporte quelle colonne en cliquant sur ce dernier ; en cliquant plusieurs fois dessus, il choisit entre les tris croissants et dcroissants. Une fois que len-tte de larborescence a t congur, nous obtenons lindex de modle du rpertoire en cours et nous sommes srs que ce rpertoire est visible en dveloppant ses parents si ncessaire laide de expand() et en le localisant grce scrollTo(). Nous nous assurons galement que la premire colonne est sufsamment grande pour afcher toutes les entres sans utiliser de points de suspension (...). Dans la partie du code du constructeur qui nest pas prsente ici, nous avons connect les boutons Create Directory (Crer un rpertoire) et Remove (Supprimer) aux slots pour effectuer ces actions. Nous navons pas besoin de bouton Rename parce que les utilisateurs peuvent renommer directement en appuyant sur F2 et en tapant du texte.
void DirectoryViewer::createDirectory() { QModelIndex index = treeView->currentIndex(); if (!index.isValid()) return; QString dirName = QInputDialog::getText(this, tr("Create Directory"), tr("Directory name")); if (!dirName.isEmpty()) { if (!model->mkdir(index, dirName).isValid()) QMessageBox::information(this, tr("Create Directory"), tr("Failed to create the directory")); } }

Si lutilisateur entre un nom de rpertoire dans la bote de dialogue, nous essayons de crer un rpertoire avec ce nom comme enfant du rpertoire en cours. La fonction QDirModel::mkdir() reoit lindex du rpertoire parent et le nom du nouveau rpertoire, et retourne lindex de modle du rpertoire quil a cr. Si lopration choue, elle retourne un index de modle invalide.
void DirectoryViewer::remove() { QModelIndex index = treeView->currentIndex();

240

Qt4 et C++ : Programmation dinterfaces GUI

if (!index.isValid()) return; bool ok; if (model->fileInfo(index).isDir()) { ok = model->rmdir(index); } else { ok = model->remove(index); } if (!ok) QMessageBox::information(this, tr("Remove"), tr("Failed to remove %1").arg(model->fileName(index))); }

Si lutilisateur clique sur Remove, nous tentons de supprimer le chier ou le rpertoire associ llment en cours. Pour ce faire, nous pourrions utiliser QDir, mais QDirModel propose des fonctions pratiques qui fonctionnent avec QModelIndexes. Le dernier exemple de cette section vous montre comment employer QSortFilterProxyModel. Contrairement aux autres modles prdnis, ce modle encapsule un modle existant et manipule les donnes qui sont transmises entre le modle sous-jacent et la vue. Dans notre exemple, le modle sous-jacent est un QStringListModel initialis avec la liste des noms de couleur reconnues par Qt (obtenue via QColor::colorNames()). Lutilisateur peut saisir une chane de ltre dans un QLineEdit et spcier la manire dont cette chane doit tre interprte (comme une expression rgulire, un modle gnrique ou une chane xe) grce une zone de liste droulante (voir Figure 10.8).
Figure 10.8 Lapplication Color Names

Voici un extrait du constructeur de ColorNamesDialog:


ColorNamesDialog::ColorNamesDialog(QWidget *parent) : QDialog(parent) { sourceModel = new QStringListModel(this); sourceModel->setStringList(QColor::colorNames()); proxyModel = new QSortFilterProxyModel(this);

Chapitre 10

Classes dafchage dlments

241

proxyModel->setSourceModel(sourceModel); proxyModel->setFilterKeyColumn(0); listView = new QListView; listView->setModel(proxyModel); ... syntaxComboBox = new QComboBox; syntaxComboBox->addItem(tr("Regular expression"), QRegExp::RegExp); syntaxComboBox->addItem(tr("Wildcard"), QRegExp::Wildcard); syntaxComboBox->addItem(tr("Fixed string"), QRegExp::FixedString); ... }

QStringListModel est cr et aliment de manire habituelle. Puis, nous construisons QSortFilterProxyModel. Nous transmettons le modle sous-jacent laide de setSourceModel() et nous demandons au proxy de ltrer en se basant sur la colonne 0 du modle original. La fonction QComboBox::addItem() reoit un argument facultatif "donne" de type QVariant; nous lutilisons pour enregistrer la valeur QRegExp::PatternSyntax qui correspond au texte de chaque lment.
void ColorNamesDialog::reapplyFilter() { QRegExp::PatternSyntax syntax = QRegExp::PatternSyntax(syntaxComboBox->itemData( syntaxComboBox->currentIndex()).toInt()); QRegExp regExp(filterLineEdit->text(), Qt::CaseInsensitive, syntax); proxyModel->setFilterRegExp(regExp); }

Le slot reapplyFilter() est invoqu ds que lutilisateur modie la chane de ltre ou la zone de liste droulante correspondant au modle. Nous crons un QRegExp en utilisant le texte prsent dans lditeur de lignes. Nous faisons ensuite correspondre la syntaxe de son modle celle stocke dans les donnes de llment en cours dans la zone de liste droulante relative la syntaxe. Puis nous appelons setFilterRegExp(), le nouveau ltre sactive et la vue est mise jour automatiquement.

Implmenter des modles personnaliss


Les modles prdnis de Qt sont pratiques pour grer et afcher des donnes. Cependant, certaines sources de donnes ne peuvent pas tre utilises efcacement avec les modles prdnis, cest pourquoi il est parfois ncessaire de crer des modles personnaliss optimiss pour la source de donnes sous-jacente. Avant de commencer crer des modles personnaliss, analysons dabord les concepts essentiels utiliss dans larchitecture modle/vue de Qt. Chaque lment de donnes dans un modle possde un index de modle et un ensemble dattributs, appels rles, qui peuvent prendre des valeurs arbitraires. Nous avons vu prcdemment que les rles les plus couramment employs

242

Qt4 et C++ : Programmation dinterfaces GUI

sont Qt::EditRole et Qt::DisplayRole. Dautres rles sont utiliss pour des donnes supplmentaires (par exemple Qt::ToolTipRole, Qt::StatusTipRole et Qt::WhatsThisRole) et dautres encore pour contrler les attributs dafchage de base (tels que Qt::FontRole, Qt::TextAlignmentRole, Qt::TextColorRole et Qt::BackgroundColorRole).
Figure 10.9 Vue schmatique des modles de Qt
Modle liste Modle de tableau racine ligne 0 1 2 1 2 colonne 0 1 2 Modle arborescence

racine
ligne 0 1 2

racine
ligne 0 0 1

colonne 0

Pour un modle liste, le seul composant dindex pertinent est le nombre de lignes, accessible depuis QModelIndex::row(). Pour un modle de tableau, les composants dindex pertinents sont les nombres de lignes et de colonnes, accessibles depuis QModelIndex::row() et QModelIndex::column(). Pour les modles liste et tableau, le parent de chaque lment est la racine, qui est reprsente par un QModelIndex invalide. Les deux premiers exemples de cette section vous montrent comment implmenter des modles de tableau personnaliss. Un modle arborescence ressemble un modle de tableau, quelques diffrences prs. Comme un modle de tableau, la racine est le parent des lments de haut niveau (un QModelIndex invalide), mais le parent de tout autre lment est un autre lment dans la hirarchie. Les parents sont accessibles depuis QModelIndex::parent(). Chaque lment possde ses donnes de rle et aucun ou plusieurs enfants, chacun tant un lment en soi. Vu que les lments peuvent avoir dautres lments comme enfants, il est possible de reprsenter des structures de donnes rcursives ( la faon dune arborescence), comme vous le montrera le dernier exemple de cette section. Le premier exemple de cette section est un modle de tableau en lecture seule qui afche des valeurs montaires en relation les unes avec les autres (voir Figure 10.10). Lapplication pourrait tre implmente partir dun simple tableau, mais nous voulons nous servir dun modle personnalis pour proter de certaines proprits des donnes qui minimisent le stockage. Si nous voulions conserver les 162 devises actuellement cotes dans un tableau, nous devrions stocker 162 162 = 26 244 valeurs ; avec le modle personnalis prsent ci-aprs, nous nenregistrons que 162 valeurs (la valeur de chaque devise par rapport au dollar amricain).

Chapitre 10

Classes dafchage dlments

243

Figure 10.10 Lapplication Currencies

La classe CurrencyModel sera utilise avec un QTableView standard. Elle est alimente avec un QMap<QString,double>; chaque cl correspond au code de la devise et chaque valeur correspond la valeur de la devise en dollars amricains. Voici un extrait de code qui montre comment le tableau de correspondance est aliment et comment le modle est utilis :
QMap<QString, double> currencyMap; currencyMap.insert("AUD", 1.3259); currencyMap.insert("CHF", 1.2970); ... currencyMap.insert("SGD", 1.6901); currencyMap.insert("USD", 1.0000); CurrencyModel currencyModel; currencyModel.setCurrencyMap(currencyMap); QTableView tableView; tableView.setModel(&currencyModel); tableView.setAlternatingRowColors(true);

Etudions dsormais limplmentation du modle, en commenant par son en-tte :


class CurrencyModel: public QAbstractTableModel { public: CurrencyModel(QObject *parent = 0); void setCurrencyMap(const QMap<QString, double> &map); int rowCount(const QModelIndex &parent) const; int columnCount(const QModelIndex &parent) const; QVariant data(const QModelIndex &index, int role) const; QVariant headerData(int section, Qt::Orientation orientation, int role) const; private: QString currencyAt(int offset) const; QMap<QString, double> currencyMap; };

244

Qt4 et C++ : Programmation dinterfaces GUI

Nous avons choisi de driver QAbstractTableModel pour notre modle, parce que cela correspond le plus notre source de donnes. Qt propose plusieurs classes de base de modle, y compris QAbstractListModel, QAbstractTableModel et QAbstractItemModel (voir Figure 10.11). La classe QAbstractItemModel est employe pour supporter une grande varit de modles, dont ceux qui se basent sur des structures de donnes rcursives, alors que les classes QAbstractListModel et QAbstractTableModel sont proposes pour une question de commodit lors de lutilisation densembles de donnes une ou deux dimensions.
Figure 10.11 Arbre dhritage des classes de modle abstraites
QObject QAbstractItemModel QAbstractListModel QAbstractTableModel

Pour un modle de tableau en lecture seule, nous devons rimplmenter trois fonctions : rowCount(), columnCount() et data(). Dans ce cas, nous avons aussi rimplment headerData() et nous fournissons une fonction pour initialiser les donnes (setCurrencyMap()).
CurrencyModel::CurrencyModel(QObject *parent) : QAbstractTableModel(parent) { }

Nous navons pas besoin de faire quoi que ce soit dans le constructeur, sauf transmettre le paramtre parent la classe de base.
int CurrencyModel::rowCount(const QModelIndex & /* parent */) const { return currencyMap.count(); } int CurrencyModel::columnCount(const QModelIndex & /* parent */) const { return currencyMap.count(); }

Pour ce modle de tableau, les nombres de lignes et de colonnes correspondent aux nombres de devises dans le tableau de correspondance des devises. Le paramtre parent na aucune signication pour un modle de tableau ; il est prsent parce que rowCount() et columnCount() sont hrits de la classe de base QAbstractItemModel plus gnrique, qui prend en charge les hirarchies.
QVariant CurrencyModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant();

Chapitre 10

Classes dafchage dlments

245

if (role == Qt::TextAlignmentRole) { return int(Qt::AlignRight | Qt::AlignVCenter); } else if (role == Qt::DisplayRole) { QString rowCurrency = currencyAt(index.row()); QString columnCurrency = currencyAt(index.column()); if (currencyMap.value(rowCurrency) == 0.0) return "####"; double amount = currencyMap.value(columnCurrency) / currencyMap.value(rowCurrency); return QString("%1").arg(amount, 0, f, 4); } return QVariant(); }

La fonction data() retourne la valeur de nimporte quel rle dun lment. Llment est spci sous forme de QModelIndex. Pour un modle de tableau, les composants intressants dun QModelIndex sont ses nombres de lignes et de colonnes, disponibles grce row() et column(). Si le rle est Qt::TextAlignmentRole, nous retournons un alignement adapt aux nombres. Si le rle dafchage est Qt::DisplayRole, nous recherchons la valeur de chaque devise et nous calculons le taux de change. Nous pourrions retourner la valeur calcule sous forme de type double, mais nous naurions aucun contrle sur le nombre de chiffres aprs la virgule ( moins dutiliser un dlgu personnalis). Nous retournons donc plutt la valeur sous forme de chane, mise en forme comme nous le souhaitons.
QVariant CurrencyModel::headerData(int section, Qt::Orientation /* orientation */, int role) const { if (role!= Qt::DisplayRole) return QVariant(); return currencyAt(section); }

La fonction headerData() est appele par la vue pour alimenter ses en-ttes verticaux et horizontaux. Le paramtre section correspond au nombre de lignes ou de colonnes (selon lorientation). Vu que les lignes et les colonnes ont les mmes codes de devise, nous ne nous soucions pas de lorientation et nous retournons simplement le code de la devise pour le numro de section donn.
void CurrencyModel::setCurrencyMap(const QMap<QString, double> &map) { currencyMap = map; reset(); }

246

Qt4 et C++ : Programmation dinterfaces GUI

Lappelant peut modier le tableau de correspondance des devises en excutant setCurrencyMap(). Lappel de QAbstractItemModel::reset() informe nimporte quelle vue qui utilise le modle que toutes leurs donnes sont invalides ; ceci les oblige demander des donnes actualises pour les lments visibles.
QString CurrencyModel::currencyAt(int offset) const { return (currencyMap.begin() + offset).key(); }

La fonction currencyAt() retourne la cl (le code de la devise) la position donne dans le tableau de correspondance des devises. Nous utilisons un itrateur de style STL pour trouver llment et appeler key(). Comme nous venons de le voir, il nest pas difcile de crer des modles en lecture seule, et en fonction de la nature des donnes sous-jacentes, il est possible dconomiser de la mmoire et dacclrer les temps de rponse avec un modle bien conu. Le prochain exemple, lapplication Cities, se base aussi sur un tableau, mais cette fois-ci les donnes sont saisies par lutilisateur (voir Figure 10.12). Cette application est utilise pour enregistrer des valeurs indiquant la distance entre deux villes. Comme lexemple prcdent, nous pourrions simplement utiliser un QTableWidget et stocker un lment pour chaque paire de villes. Cependant, un modle personnalis pourrait tre plus efcace, parce que la distance entre une ville A et une ville B est la mme que vous alliez de A B ou de B A, les lments se retent donc le long de la diagonale principale. Pour voir comment un modle personnalis se compare un simple tableau, supposons que nous avons trois villes, A, B et C. Si nous conservions une valeur pour chaque combinaison, nous devrions stocker neuf valeurs. Un modle bien conu ne ncessiterait que trois lments (A, B), (A, C) et (B, C).
Figure 10.12 Lapplication Cities

Voici comment nous avons congur et exploit le modle :


QStringList cities; cities << "Arvika" << "Boden" << "Eskilstuna" << "Falun" << "Filipstad" << "Halmstad" << "Helsingborg" << "Karlstad"

Chapitre 10

Classes dafchage dlments

247

<< "Kiruna" << "Kramfors" << "Motala" << "Sandviken" << "Skara" << "Stockholm" << "Sundsvall" << "Trelleborg"; CityModel cityModel; cityModel.setCities(cities); QTableView tableView; tableView.setModel(&cityModel); tableView.setAlternatingRowColors(true);

Nous devons rimplmenter les mmes fonctions que pour lexemple prcdent. De plus, nous devons aussi rimplmenter setData() et flags() pour que le modle puisse tre modi. Voici la dnition de classe :
class CityModel: public QAbstractTableModel { Q_OBJECT public: CityModel(QObject *parent = 0); void setCities(const QStringList &cityNames); int rowCount(const QModelIndex &parent) const; int columnCount(const QModelIndex &parent) const; QVariant data(const QModelIndex &index, int role) const; bool setData(const QModelIndex &index, const QVariant &value, int role); QVariant headerData(int section, Qt::Orientation orientation, int role) const; Qt::ItemFlags flags(const QModelIndex &index) const; private: int offsetOf(int row, int column) const; QStringList cities; QVector<int> distances; };

Pour ce modle, nous utilisons deux structures de donnes : cities de type QStringList pour contenir les noms de ville, et distances de type QVector<int> pour enregistrer la distance entre chaque paire unique de villes.
CityModel::CityModel(QObject *parent) : QAbstractTableModel(parent) { }

Le constructeur ne fait rien part transmettre le paramtre parent la classe de base.


int CityModel::rowCount(const QModelIndex & /* parent */) const { return cities.count();

248

Qt4 et C++ : Programmation dinterfaces GUI

} int CityModel::columnCount(const QModelIndex & /* parent */) const { return cities.count(); }

Vu que nous avons une grille carre de villes, le nombre de lignes et de colonnes correspond au nombre de villes de notre liste.
QVariant CityModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); if (role == Qt::TextAlignmentRole) { return int(Qt::AlignRight | Qt::AlignVCenter); } else if (role == Qt::DisplayRole) { if (index.row() == index.column()) return 0; int offset = offsetOf(index.row(), index.column()); return distances[offset]; } return QVariant(); }

La fonction data() est similaire ce que nous effectu dans CurrencyModel. Elle retourne 0 si la ligne et la colonne sont identiques, parce que cela correspond au cas o les deux villes sont les mmes ; sinon elle recherche lentre de la ligne et de la colonne dans le vecteur distances et renvoie la distance pour cette paire de villes particulire.
QVariant CityModel::headerData(int section, Qt::Orientation /* orientation */, int role) const { if (role == Qt::DisplayRole) return cities[section]; return QVariant(); }

La fonction headerData() est simple puisque nous avons un tableau carr o chaque ligne possde un en-tte de colonne identique. Nous retournons simplement le nom de la ville la position donne dans la liste de chane cities.
bool CityModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (index.isValid() && index.row()!= index.column() && role == Qt::EditRole) { int offset = offsetOf(index.row(), index.column()); distances[offset] = value.toInt();

Chapitre 10

Classes dafchage dlments

249

QModelIndex transposedIndex = createIndex(index.column(), index.row()); emit dataChanged(index, index); emit dataChanged(transposedIndex, transposedIndex); return true; } return false; }

La fonction setData() est invoque quand lutilisateur modie un lment. En supposant que lindex de modle est valide, que les deux villes sont diffrentes et que llment de donnes modier est Qt::EditRole, la fonction stocke la valeur que lutilisateur a saisie dans le vecteur distances. La fonction createIndex() sert gnrer un index de modle. Nous en avons besoin pour obtenir lindex de modle de llment symtrique de llment congur par rapport la diagonale principale, vu que les deux lments doivent afcher les mmes donnes. La fonction createIndex() reoit la ligne avant la colonne ; ici, nous inversons les paramtres pour obtenir lindex de modle de llment symtrique celui spci par index. Nous mettons le signal dataChanged() avec lindex de modle de llment qui a t modi. Ce signal reoit deux index de modle, parce quun changement peut affecter une rgion rectangulaire constitue de plusieurs lignes et colonnes. Les index transmis reprsentent llment situ en haut gauche et llment en bas droite de la zone modie. Nous mettons aussi le signal dataChanged() lattention de lindex transpos an que la vue actualise lafchage de llment. Enn, nous retournons true ou false pour indiquer si la modication a t effectue avec succs ou non.
Qt::ItemFlags CityModel::flags(const QModelIndex &index) const { Qt::ItemFlags flags = QAbstractItemModel::flags(index); if (index.row()!= index.column()) flags |= Qt::ItemIsEditable; return flags; }

Le modle se sert de la fonction flags() pour annoncer les possibilits daction sur llment (par exemple, sil peut tre modi ou non). Limplmentation par dfaut de QAbstractTableModel retourne Qt::ItemIsSelectable | Qt::ItemIsEnabled. Nous ajoutons lindicateur Qt::ItemIsEditable pour tous les lments sauf ceux qui se trouvent sur les diagonales (qui sont toujours nuls).
void CityModel::setCities(const QStringList &cityNames) { cities = cityNames; distances.resize(cities.count() * (cities.count() - 1) / 2); distances.fill(0); reset(); }

250

Qt4 et C++ : Programmation dinterfaces GUI

Si nous recevons une nouvelle liste de villes, nous dnissons le QStringList priv en nouvelle liste, nous redimensionnons et nous effaons le vecteur distances puis nous appelons QAbstractItemModel::reset() pour informer toutes les vues que leurs lments visibles doivent tre nouveau rcuprs.
int CityModel::offsetOf(int row, int column) const { if (row < column) qSwap(row, column); return (row * (row - 1) / 2) + column; }

La fonction prive offsetOf() calcule lindex dune paire de villes donne dans le vecteur distances. Par exemple, si nous avions les villes A, B, C et D et si lutilisateur avait mis jour la ligne 3, colonne 1, B D, le dcalage serait de 3 _ (3 1)/2 + 1 = 4. Si lutilisateur avait mis jour la ligne 1, colonne 3, D B, grce qSwap(), exactement le mme calcul aurait t accompli et un dcalage identique aurait t retourn.
Figure 10.13 Les structures de donnes cities et distances et le modle de tableau
Villes A Distances B C D A B C D Modle de tableau A 0 B C D A B A C A D 0

A B

B C B D

A B A C A D B C B D C D

0 A C B C C D 0 A D B D C D

Le dernier exemple de cette section est un modle qui prsente larbre danalyse dune expression rgulire donne. Une expression rgulire est constitue dun ou plusieurs termes, spars par des caractres "|". Lexpression rgulire "alpha|bravo|charlie" contient donc trois termes. Chaque terme est une squence dun ou plusieurs facteurs ; par exemple, le terme "bravo" est compos de cinq facteurs (chaque lettre est un facteur). Les facteurs peuvent encore tre dcomposs en atome et en quanticateur facultatif, comme "*", "+" et "?".Vu que les expressions rgulires peuvent contenir des sous-expressions entre parenthses, les arbres danalyse correspondants seront rcursifs. Lexpression rgulire prsente en Figure 10.14, "ab|(cd)?e", correspond un a suivi dun b, ou dun c suivi dun d puis dun e, ou simplement dun e. Elle correspondra ainsi "ab" et "cde", mais pas "bc" ou "cd". Lapplication Regexp Parser se compose de quatre classes :

RegExpWindow est une fentre qui permet lutilisateur de saisir une expression rgulire et qui afche larbre danalyse correspondant. RegExpParser gnre un arbre danalyse partir dune expression rgulire. RegExpModel est un modle darborescence qui encapsule un arbre danalyse. Node reprsente un lment dans un arbre danalyse.

Chapitre 10

Classes dafchage dlments

251

Figure 10.14 Lapplication Regexp Parser

Commenons par la classe Node:


class Node { public: enum Type { RegExp, Expression, Term, Factor, Atom, Terminal }; Node(Type type, const QString &str = ""); ~Node(); Type type; QString str; Node *parent; QList<Node *> children; };

Chaque nud possde un type, une chane (qui peut tre vide), un parent (qui peut tre 0) et une liste de nuds enfants (qui peut tre vide).
Node::Node(Type type, const QString &str) { this->type = type; this->str = str; parent = 0; }

Le constructeur initialise simplement le type et la chane du nud. Etant donn que toutes les donnes sont publiques, le code qui utilise Node peut oprer directement sur le type, la chane, le parent et les enfants.
Node::~Node() { qDeleteAll(children); }

252

Qt4 et C++ : Programmation dinterfaces GUI

La fonction qDeleteAll() parcourt un conteneur de pointeurs et appelle delete sur chacun deux. Elle ne dnit pas les pointeurs en 0, donc si elle est utilise en dehors dun destructeur, il est frquent de la voir suivie dun appel de clear() sur le conteneur qui renferme les pointeurs. Maintenant que nous avons dni nos lments de donnes (chacun reprsent par un Node), nous sommes prts crer un modle :
class RegExpModel: public QAbstractItemModel { public: RegExpModel(QObject *parent = 0); ~RegExpModel(); void setRootNode(Node *node); QModelIndex index(int row, int column, const QModelIndex &parent) const; QModelIndex parent(const QModelIndex &child) const; int rowCount(const QModelIndex &parent) const; int columnCount(const QModelIndex &parent) const; QVariant data(const QModelIndex &index, int role) const; QVariant headerData(int section, Qt::Orientation orientation, int role) const; private: Node *nodeFromIndex(const QModelIndex &index) const; Node *rootNode; };

Cette fois-ci nous avons hrit de QAbstractItemModel plutt que de sa sous-classe ddie QAbstractTableModel, parce que nous voulons crer un modle hirarchique. Les fonctions essentielles que nous devons rimplmenter sont toujours les mmes, sauf que nous devons aussi implmenter index() et parent(). Pour dnir les donnes du modle, une fonction setRootNode() doit tre invoque avec le nud racine de larbre danalyse.
RegExpModel::RegExpModel(QObject *parent) : QAbstractItemModel(parent) { rootNode = 0; }

Dans le constructeur du modle, nous navons qu congurer le nud racine en valeur nulle et transmettre le parent la classe de base.
RegExpModel::~RegExpModel() { delete rootNode; }

Chapitre 10

Classes dafchage dlments

253

Dans le destructeur, nous supprimons le nud racine. Si le nud racine a des enfants, chacun deux est supprim par le destructeur Node, et ainsi de suite de manire rcursive.
void RegExpModel::setRootNode(Node *node) { delete rootNode; rootNode = node; reset(); }

Quand un nouveau nud racine est dni, nous supprimons dabord tout nud racine prcdent (et tous ses enfants). Nous congurons ensuite le nouveau nud racine et nous appelons reset() pour informer les vues quelles doivent nouveau rcuprer les donnes des lments visibles.
QModelIndex RegExpModel::index(int row, int column, const QModelIndex &parent) const { if (!rootNode) return QModelIndex(); Node *parentNode = nodeFromIndex(parent) return createIndex(row, column, parentNode->children[row]); }

La fonction index() est rimplmente dans QAbstractItemModel. Elle est appele ds que le modle ou la vue doit crer un QModelIndex pour un lment enfant particulier (ou un lment de haut niveau si parent est un QModelIndex invalide). Pour les modles de tableau et de liste, nous navons pas besoin de rimplmenter cette fonction, parce que les implmentations par dfaut de QAbstractListModel et QAbstractTableModel sont normalement sufsantes. Dans notre implmentation dindex(), si aucun arbre danalyse nest congur, nous retournons un QModelIndex invalide. Sinon, nous crons un QModelIndex avec la ligne et la colonne donnes et avec un Node * pour lenfant demand. Sagissant des modles hirarchiques, il nest pas sufsant de connatre la ligne et la colonne dun lment par rapport son parent pour lidentier ; nous devons aussi savoir qui est son parent. Pour rsoudre ce problme, nous pouvons stocker un pointeur vers le nud interne dans le QModelIndex. QModelIndex nous permet de conserver un void * ou un int en plus des nombres de lignes et de colonnes. Le Node * de lenfant est obtenu par le biais de la liste children du nud parent. Le nud parent est extrait de lindex de modle parent grce la fonction prive nodeFromIndex():
Node *RegExpModel::nodeFromIndex(const QModelIndex &index) const { if (index.isValid()) { return static_cast<Node *>(index.internalPointer()); } else { return rootNode; } }

254

Qt4 et C++ : Programmation dinterfaces GUI

La fonction nodeFromIndex() convertit le void * de lindex donn en Node * ou retourne le nud racine si lindex est invalide, puisquun index de modle invalide reprsente la racine dans un modle.
int RegExpModel::rowCount(const QModelIndex &parent) const { Node *parentNode = nodeFromIndex(parent); if (!parentNode) return 0; return parentNode->children.count(); }

Le nombre de lignes dun lment particulier correspond simplement au nombre denfants quil possde.
int RegExpModel::columnCount(const QModelIndex & /* parent */) const { return 2; }

Le nombre de colonnes est x 2. La premire colonne contient les types de nuds ; la seconde comporte les valeurs des nuds.
QModelIndex RegExpModel::parent(const QModelIndex &child) const { Node *node = nodeFromIndex(child); if (!node) return QModelIndex(); Node *parentNode = node->parent; if (!parentNode) return QModelIndex(); Node *grandparentNode = parentNode->parent; if (!grandparentNode) return QModelIndex(); int row = grandparentNode->children.indexOf(parentNode); return createIndex(row, child.column(), parentNode); }

Rcuprer le parent QModelIndex dun enfant est un peu plus complexe que de rechercher lenfant dun parent. Nous pouvons facilement rcuprer le nud parent laide de nodeFromIndex() et poursuivre en utilisant le pointeur du parent de Node, mais pour obtenir le numro de ligne (la position du parent parmi ses pairs), nous devons remonter jusquau grand-parent et rechercher la position dindex du parent dans la liste des enfants de son parent (cest--dire celle du grand-parent de lenfant).
QVariant RegExpModel::data(const QModelIndex &index, int role) const { if (role!= Qt::DisplayRole) return QVariant();

Chapitre 10

Classes dafchage dlments

255

Node *node = nodeFromIndex(index); if (!node) return QVariant(); if (index.column() == 0) { switch (node->type) { case Node::RegExp: return tr("RegExp"); case Node::Expression: return tr("Expression"); case Node::Term: return tr("Term"); case Node::Factor: return tr("Factor"); case Node::Atom: return tr("Atom"); case Node::Terminal: return tr("Terminal"); default: return tr("Unknown"); } } else if (index.column() == 1) { return node->str; } return QVariant(); }

Dans data(), nous rcuprons le Node * de llment demand et nous nous en servons pour accder aux donnes sous-jacentes. Si lappelant veut une valeur pour nimporte quel rle except Qt::DisplayRole ou sil ne peut pas rcuprer un Node pour lindex de modle donn, nous retournons un QVariant invalide. Si la colonne est 0, nous renvoyons le nom du type du nud ; si la colonne est 1, nous retournons la valeur du nud (sa chane).
QVariant RegExpModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { if (section == 0) { return tr("Node"); } else if (section == 1) { return tr("Value"); } } return QVariant(); }

256

Qt4 et C++ : Programmation dinterfaces GUI

Dans notre rimplmentation de headerData(), nous retournons les intituls appropris des en-ttes horizontaux. La classe QTreeView, qui est employe pour visualiser des modles hirarchiques, ne possde pas den-tte vertical, nous ignorons donc cette ventualit. Maintenant que nous avons tudi les classes Node et RegExpModel, voyons comment le nud racine est cr quand lutilisateur modie le texte dans lditeur de lignes :
void RegExpWindow::regExpChanged(const QString &regExp) { RegExpParser parser; Node *rootNode = parser.parse(regExp); regExpModel->setRootNode(rootNode); }

Quand lutilisateur change le texte dans lditeur de lignes de lapplication, le slot regExpChanged() de la fentre principale est appel. Dans ce slot, le texte de lutilisateur est analys et lanalyseur retourne un pointeur vers le nud racine de larbre danalyse. Nous navons pas tudi la classe RegExpParser parce quelle nest pas pertinente pour les interfaces graphiques ou la programmation modle/vue. Le code source complet de cet exemple se trouve sur la page ddie cet ouvrage sur le site web de Pearson, www.pearson.fr. Dans cette section, nous avons vu comment crer trois modles personnaliss diffrents. De nombreux modles sont beaucoup plus simples que ceux prsents ici, avec des correspondances uniques entre les lments et les index de modle. Dautres exemples modle/vue sont fournis avec Qt, accompagns dune documentation dtaille.

Implmenter des dlgus personnaliss


Les lments individuels dans les vues sont afchs et modis laide de dlgus. Dans la majorit des cas, le dlgu par dfaut propos par une vue savre sufsant. Si nous voulons contrler davantage lafchage des lments, nous pouvons atteindre notre objectif simplement en utilisant un modle personnalis : dans notre rimplmentation de data(), nous avons la possibilit de grer Qt::FontRole, Qt::TextAlignmentRole, Qt::TextColorRole et Qt::BackgroundColorRole et ceux-ci sont employs par le dlgu par dfaut. Par exemple, dans les exemples Cities et Currencies prsents auparavant, nous avons gr Qt::TextAlignmentRole pour obtenir des nombres justis droite. Si nous voulons encore plus de contrle, nous pouvons crer notre propre classe de dlgu et la dnir sur les vues qui lutiliseront. La bote de dialogue Track Editor illustre en Figure 10.15 est base sur un dlgu personnalis. Elle afche les titres des pistes de musique ainsi que leur dure. Les donnes stockes dans le modle seront simplement des QString (pour les titres) et des int (pour les secondes), mais les dures seront divises en minutes et en secondes et pourront tre modies laide de QTimeEdit.

Chapitre 10

Classes dafchage dlments

257

Figure 10.15 La bote de dialogue Track Editor

La bote de dialogue Track Editor se sert dun QTableWidget, une sous-classe ddie lafchage dlments qui agit sur des QTableWidgetItem. Les donnes sont proposes sous forme dune liste de Track:
class Track { public: Track(const QString &title = "", int duration = 0); QString title; int duration; };

Voici un extrait du constructeur qui prsente la cration et lalimentation en donnes du widget tableau :
TrackEditor::TrackEditor(QList<Track> *tracks, QWidget *parent) : QDialog(parent) { this->tracks = tracks; tableWidget = new QTableWidget(tracks->count(), 2); tableWidget->setItemDelegate(new TrackDelegate(1)); tableWidget->setHorizontalHeaderLabels( QStringList() << tr("Track") << tr("Duration")); for (int row = 0; row < tracks->count(); ++row) { Track track = tracks->at(row); QTableWidgetItem *item0 = new QTableWidgetItem(track.title); tableWidget->setItem(row, 0, item0); QTableWidgetItem *item1 = new QTableWidgetItem(QString::number(track.duration)); item1->setTextAlignment(Qt::AlignRight); tableWidget->setItem(row, 1, item1); } ... }

258

Qt4 et C++ : Programmation dinterfaces GUI

Le constructeur cre un widget tableau et au lieu dutiliser simplement le dlgu par dfaut, nous dnissons notre TrackDelegate personnalis, lui transmettant la colonne qui contient les donnes de temps. Nous congurons dabord les en-ttes des colonnes, puis nous parcourons les donnes, en alimentant les lignes avec le nom et la dure de chaque piste. Le reste du constructeur et de la bote de dialogue TrackEditor ne prsentent aucune particularit, nous allons donc analyser maintenant le TrackDelegate qui gre le rendu et la modication des donnes de la piste.
class TrackDelegate: public QItemDelegate { Q_OBJECT public: TrackDelegate(int durationColumn, QObject *parent = 0); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; void setEditorData(QWidget *editor, const QModelIndex &index) const; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; private slots: void commitAndCloseEditor(); private: int durationColumn; };

Nous utilisons QItemDelegate comme classe de base an de bncier de limplmentation du dlgu par dfaut. Nous aurions aussi pu utiliser QAbstractItemDelegate si nous avions voulu tout commencer zro. Pour proposer un dlgu qui peut modier des donnes, nous devons implmenter createEditor(), setEditorData() et setModelData(). Nous implmentons aussi paint() pour modier lafchage de la colonne de dure.
TrackDelegate::TrackDelegate(int durationColumn, QObject *parent) : QItemDelegate(parent) { this->durationColumn = durationColumn; }

Le paramtre durationColumn du constructeur indique au dlgu quelle colonne contient la dure de la piste.
void TrackDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (index.column() == durationColumn) { int secs = index.model()->data(index, Qt::DisplayRole).toInt();

Chapitre 10

Classes dafchage dlments

259

QString text = QString("%1:%2") .arg(secs / 60, 2, 10, QChar(0)) .arg(secs % 60, 2, 10, QChar(0)); QStyleOptionViewItem myOption = option; myOption.displayAlignment = Qt::AlignRight | Qt::AlignVCenter; drawDisplay(painter, myOption, myOption.rect, text); drawFocus(painter, myOption, myOption.rect); } else{ QItemDelegate::paint(painter, option, index); } }

Vu que nous voulons afcher la dure sous la forme "minutes : secondes" nous avons rimplment la fonction paint(). Les appels de arg() reoivent un nombre entier afcher sous forme de chane, la quantit de caractres que la chane doit contenir, la base de lentier (10 pour un nombre dcimal) et le caractre de remplissage. Pour justier le texte droite, nous copions les options de style en cours et nous remplaons lalignement par dfaut. Nous appelons ensuite QItemDelegate::drawDisplay() pour dessiner le texte, suivi de QItemDelegate::drawFocus() qui tracera un rectangle de focus si llment est actif et ne fera rien dans les autres cas. La fonction drawDisplay() se rvle trs pratique, notamment avec nos propres options de style. Nous pourrions aussi dessiner directement en utilisant le painter.
QWidget *TrackDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (index.column() == durationColumn) { QTimeEdit *timeEdit = new QTimeEdit(parent); timeEdit->setDisplayFormat("mm:ss"); connect(timeEdit, SIGNAL(editingFinished()), this, SLOT(commitAndCloseEditor())); return timeEdit; } else { return QItemDelegate::createEditor(parent, option, index); } }

Nous ne voulons modier que la dure des pistes, le changement des noms de piste reste la charge du dlgu par dfaut. Pour ce faire, nous vrions pour quelle colonne un diteur a t demand au dlgu. Sil sagit de la colonne de dure, nous crons un QTimeEdit, nous dnissons le format dafchage de manire approprie et nous relions son signal editingFinished() notre slot commitAndCloseEditor(). Pour toute autre colonne, nous cdons la gestion des modications au dlgu par dfaut.
void TrackDelegate::commitAndCloseEditor() {

260

Qt4 et C++ : Programmation dinterfaces GUI

QTimeEdit *editor = qobject_cast<QTimeEdit *>(sender()); emit commitData(editor); emit closeEditor(editor); }

Si lutilisateur appuie sur Entre ou dplace le focus hors du QTimeEdit (mais pas sil appuie sur Echap), le signal editingFinished() est mis et le slot commitAndCloseEditor() est invoqu. Ce slot met le signal commitData() pour informer la vue quil y a des donnes modies qui remplacent les donnes existantes. Il met aussi le signal closeEditor() pour informer la vue que cet diteur nest plus requis, le modle le supprimera donc. Lditeur est rcupr laide de QObject::sender() qui retourne lobjet qui a mis le signal qui a dclench le slot. Si lutilisateur annule (en appuyant sur Echap), la vue supprimera simplement lditeur.
void TrackDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { if (index.column() == durationColumn) { int secs = index.model()->data(index, Qt::DisplayRole).toInt(); QTimeEdit *timeEdit = qobject_cast<QTimeEdit *>(editor); timeEdit->setTime(QTime(0, secs / 60, secs % 60)); } else { QItemDelegate::setEditorData(editor, index); } }

Quand lutilisateur initie une modication, la vue appelle createEditor() pour crer un diteur, puis setEditorData() pour initialiser lditeur avec les donnes en cours de llment. Si lditeur concerne la colonne de dure, nous extrayons la dure de la piste en secondes et nous dnissons le temps de QTimeEdit avec le nombre correspondant de minutes et de secondes ; sinon nous laissons le dlgu par dfaut soccuper de linitialisation.
void TrackDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { if (index.column() == durationColumn) { QTimeEdit *timeEdit = qobject_cast<QTimeEdit *>(editor); QTime time = timeEdit->time(); int secs = (time.minute() * 60) + time.second(); model->setData(index, secs); } else { QItemDelegate::setModelData(editor, model, index); } }

Si lutilisateur termine la modication (par exemple en cliquant en dehors du widget ou en appuyant sur Entre ou Tab) au lieu de lannuler, le modle doit tre mis jour avec les donnes de lditeur. Si la dure a chang, nous extrayons les minutes et les secondes du QTimeEdit et nous congurons les donnes avec le nombre quivalent en secondes.

Chapitre 10

Classes dafchage dlments

261

Mme si ce nest pas ncessaire dans ce cas, il est tout fait possible de crer un dlgu personnalis qui contrle troitement la modication et lafchage de nimporte quel lment dun modle. Nous avons choisi de nous occuper dune colonne particulire, mais vu que QModelIndex est transmis toutes les fonctions de QItemDelegate que nous rimplmentons, nous pouvons prendre le contrle par colonne, ligne, zone rectangulaire, parent ou toute combinaison de ceux-ci jusquaux lments individuels si ncessaire. Dans ce chapitre, nous vous avons prsent un large aperu de larchitecture modle/vue de Qt. Nous avons vu comment utiliser les sous-classes ddies lafchage et les modles prdnis de Qt et comment crer des modles et des dlgus personnaliss. Toutefois, larchitecture modle/vue est si riche que nous naurions pas sufsamment de place pour traiter tous ses aspects. Par exemple, nous pourrions crer une vue personnalise qui nafche pas ses lments sous forme de liste, de tableau ou darborescence. Cest ce que propose lexemple Chart situ dans le rpertoire examples/itemviews/chart de Qt, qui prsente une vue personnalise afchant des donnes du modle sous forme de graphique secteurs. Il est galement possible demployer plusieurs vues pour afcher le mme modle sans mise en forme. Toute modication effectue via une vue se retera automatiquement et immdiatement dans les autres vues. Ce type de fonctionnalit est particulirement utile pour afcher de grands ensembles de donnes o lutilisateur veut voir des sections de donnes qui sont logiquement loignes les unes des autres. Larchitecture prend en charge les slections : quand deux vues ou plus utilisent le mme modle, chaque vue peut tre dnie de manire avoir ses propres slections indpendantes, ou alors les slections peuvent se rpartir entre les vues. La documentation en ligne de Qt aborde la programmation dafchage dlments et les classes qui limplmentent. Consultez le site http://doc.trolltech.com/4.1/model-view.html pour obtenir une liste des classes pertinentes et http://doc.trolltech.com/4.1/model-viewprogramming.html pour des informations supplmentaires et des liens vers les exemples fournis avec Qt.

11
Classes conteneur
Au sommaire de ce chapitre Conteneurs squentiels Conteneurs associatifs Algorithmes gnriques Chanes, tableaux doctets et variants

Les classes conteneur sont des classes template polyvalentes qui stockent des lments dun type donn en mmoire. C++ offre dj de nombreux conteneurs dans la STL (Standard Template Library), qui est incluse dans la bibliothque C++ standard. Qt fournissant ses propres classes conteneur, nous pouvons utiliser la fois les conteneurs STL et Qt pour les programmes Qt. Les conteneurs Qt prsentent lavantage de se comporter de la mme faon sur toutes les plates-formes et dtre partags implicitement. Le partage implicite, ou la technique de "copie lcriture", est une optimisation qui permet la transmission de conteneurs entiers comme valeurs sans cot signicatif pour les performances. Les conteneurs Qt comportent galement des classes ditrateurs simple demploi inspires par Java. Elles peuvent tre diffuses au moyen dun QDataStream et elles ncessitent moins de code dans lexcutable que les conteneurs STL correspondants. Enn, sur certaines plates-formes matrielles supportes par Qtopia Core (la version Qt pour priphriques mobiles), les conteneurs Qt sont les seuls disponibles.

264

Qt4 et C++ : Programmation dinterfaces GUI

Qt offre la fois des conteneurs squentiels tels que QVector<T>, QLinkedList<T> et QList<T> et des conteneurs associatifs comme QMap<K,T> et QHash<K,T>. Logiquement, les conteneurs squentiels stockent les lments les uns aprs les autres, alors que les conteneurs associatifs stockent des paires cl/valeur. Qt fournit galement des algorithmes gnriques qui ralisent des oprations sur les conteneurs. Par exemple, lalgorithme qSort() trie un conteneur squentiel et qBinaryFind() effectue une recherche binaire sur un conteneur squentiel tri. Ces algorithmes sont similaires ceux offerts par la STL. Si vous tes dj familier avec les conteneurs de la STL et si vous disposez de cette bibliothque sur vos plates-formes cibles, vous pouvez les utiliser la place ou en plus des conteneurs Qt. Pour plus dinformations au sujet des fonctions et des classes de la STL, rendez-vous sur le site Web de SGI ladresse http://www.sgi.com/tech/stl/. Dans ce chapitre, nous tudierons galement les classes QString, QByteArray et QVariant, qui ont toutes de nombreux points en commun avec les conteneurs. QString est une chane Unicode 16 bits utilise dans lAPI de Qt. QByteArray est un tableau de caractres de 8 bits utilis pour stocker des donnes binaires brutes. QVariant est un type susceptible de stocker la plupart des types de valeurs Qt et C++.

Conteneurs squentiels
Un QVector<T> est une structure de donnes de type tableau qui stocke ses lments des emplacements adjacents en mmoire. Un vecteur se distingue dun tableau C++ brut par le fait quil connat sa propre taille et peut tre redimensionn. Lajout dlments supplmentaires la n dun vecteur est assez efcace, alors que linsertion dlments devant ou au milieu de celui-ci peut savrer coteux. (voir Figure 11.1)
Figure 11.1 Un vecteur dlments de type double
0 937.81 1 25.984 2 308.74 3 310.92 4 40.9

Si nous savons lavance combien dlments nous seront ncessaires, nous pouvons attribuer au vecteur une taille initiale lors de sa dnition et utiliser loprateur [] pour affecter une valeur aux lments. Dans le cas contraire, nous devons redimensionner le vecteur ultrieurement ou ajouter les lments. Voici un exemple dans lequel nous spcions la taille initiale :
QVector<double> vect(3); vect[0] = 1.0; vect[1] = 0.540302; vect[2] = -0.416147;

Chapitre 11

Classes conteneur

265

Voici le mme exemple, commenant cette fois avec un vecteur vide et utilisant la fonction append() pour ajouter des lments la n :
QVector<double> vect; vect.append(1.0); vect.append(0.540302); vect.append(-0.416147);

Nous pouvons galement remplacer append() par loprateur <<:


vect << 1.0 << 0.540302 << -0.416147;

Vous parcourez les lments du vecteur laide de [] et count():


double sum = 0.0; for (int i = 0; i < vect.count(); ++i) sum += vect[i];

Les entres de vecteur cres sans quune valeur explicite ne leur soit attribue sont initialises au moyen du constructeur par dfaut de la classe de llment. Les types de base et les types pointeur sont initialiss en zro. Linsertion dlments au dbut ou au milieu dun QVector<T>, ou la suppression dlments ces emplacements, risque de ne pas tre efcace pour de gros vecteurs. Cest pourquoi Qt offre galement QLinkedList<T>, une structure de donnes qui stocke ses lments des emplacements non adjacents en mmoire. Contrairement aux vecteurs, les listes chanes ne prennent pas en charge laccs alatoire, mais elles garantissent les performances des insertions et des suppressions. (Voir Figure 11.2)

937.81

25.984

308.74

310.92

40.9

Figure 11.2 Une liste chane dlments de type double

Les listes chanes ne fournissent pas loprateur []. Il est donc ncessaire de recourir aux itrateurs pour parcourir leurs lments. Les itrateurs sont galement utiliss pour spcier la position des lments. Par exemple, le code suivant insre la chane "Tote Hosen" entre "Clash" et "Ramones" :
QLinkedList<QString> list; list.append("Clash"); list.append("Ramones"); QLinkedList<QString>::iterator i = list.find("Ramones"); list.insert(i, "Tote Hosen");

Nous tudierons les itrateurs en dtail ultrieurement dans cette section.

266

Qt4 et C++ : Programmation dinterfaces GUI

Le conteneur squentiel QList<T> est une "liste-tableau" qui combine les principaux avantages de QVector<T> et de QLinkedList<T> dans une seule classe. Il prend en charge laccs alatoire et son interface est base sur les index, de la mme faon que celle de QVector. Lajout ou la suppression dun lment une extrmit dun QList<T> est trs rapide. En outre, une insertion au sein dune liste contenant jusqu un millier dlments est galement trs simple. A moins que nous souhaitions raliser des insertions au milieu de listes de taille trs importante ou que nous ayons besoin que les lments de la liste occupent des adresses conscutives en mmoire, QList<T> constitue gnralement la classe conteneur polyvalente la plus approprie. La classe QStringList est une sous-classe de QList<QString> qui est largement utilise dans lAPI de Qt. En plus des fonctions quelle hrite de sa classe de base, elle fournit des fonctions supplmentaires qui la rendent plus souple demploi pour la gestion de chane. QStringList est tudie dans la dernire section de ce chapitre.

QStack<T> et QQueue<T> sont deux exemples supplmentaires de sous-classes utilitaires. QStack<T> est un vecteur qui fournit push(), pop() et top(). QQueue<T> est une liste qui fournit enqueue(), dequeue() et head(). Pour toutes les classes conteneur rencontres jusqu prsent, le type de valeur T peut tre un type de base tel que int ou double, un type pointeur ou une classe qui possde un constructeur par dfaut (un constructeur qui ne reoit aucun argument), un constructeur de copie et un oprateur daffectation. Les classes qui remplissent les conditions requises incluent QByteArray, QDateTime, QRegExp, QString et QVariant. Les classes Qt qui hritent de QObject savrent inadquates, car il leur manque un constructeur de copie et un oprateur daffectation. Ceci ne pose pas de problme dans la pratique, car nous pouvons simplement stocker des pointeurs vers des types QObject plutt que les objets eux-mmes. Le type de valeur T peut galement tre un conteneur, auquel cas nous devons sparer deux crochets conscutifs par des espaces. Sinon, le compilateur butera sur ce quil pense tre un oprateur >>. Par exemple :
QList<QVector<double> > list;

En plus des types que nous venons de mentionner, le type de valeur dun conteneur peut tre toute classe personnalise correspondant aux critres dcrits prcdemment. Voici un exemple de classe de ce type :
class Movie { public: Movie(const QString &title = "", int duration = 0); void setTitle(const QString &title) { myTitle = title; } QString title() const { return myTitle; } void setDuration(int duration) { myDuration = duration; } QString duration() const { return myDuration; }

Chapitre 11

Classes conteneur

267

private: QString myTitle; int myDuration; };

La classe possde un constructeur qui nexige aucun argument (bien quil puisse en recevoir jusqu deux). Elle possde galement un constructeur de copie et un oprateur daffectation, tous deux tant implicitement fournis par C++. Pour cette classe, la copie au membre par membre est sufsante. Il nest donc pas ncessaire dimplmenter votre propre constructeur de copie et votre oprateur daffectation. Qt fournit deux catgories ditrateurs an de parcourir les lments stocks dans un conteneur. Les itrateurs de style Java et ceux de style STL. Les itrateurs de style Java sont plus faciles utiliser, alors que ceux de style STL sont plus puissants et peuvent tre combins avec les algorithmes gnriques de Qt et de STL. Pour chaque classe conteneur, il existe deux types ditrateurs de style Java : un itrateur en lecture seulement et un itrateur en lecture-criture. Les classes ditrateur en lecture seulement sont QVectorIterator<T>, QLinkedListIterator<T> et QListIterator<T>. Les itrateurs en lecture/criture correspondants comportent le terme Mutable dans leur nom (par exemple, QMutableVectorIterator<T>). Dans cette discussion, nous allons surtout tudier les itrateurs de QList; les itrateurs pour les listes chanes et les vecteurs possdent la mme API. (Voir Figure 11.3)
Figure 11.3 Emplacements valides pour les itrateurs de style Java
A B C D E

Le premier point garder lesprit lors de lutilisation ditrateurs de style Java est quils ne pointent pas directement vers des lments. Ils peuvent tre situs avant le premier lment, aprs le dernier ou entre deux. Voici la syntaxe dune boucle ditration typique :
QList<double> list; ... QListIterator<double> i(list); while (i.hasNext()) { do_something(i.next()); }

Litrateur est initialis avec le conteneur parcourir. A ce stade, litrateur est situ juste avant le premier lment. Lappel hasNext() retourne true si un lment se situe sur la droite de litrateur. La fonction next() retourne llment situ sur la droite de litrateur et avance ce dernier jusqu la prochaine position valide.

268

Qt4 et C++ : Programmation dinterfaces GUI

Litration vers larrire est similaire, si ce nest que nous devons tout dabord appeler toBack() pour placer litrateur aprs le dernier lment :
QListIterator<double> i(list); i.toBack(); while (i.hasPrevious()) { do_something(i.previous()); }

La fonction hasPrevious() retourne true si un lment se trouve sur la gauche de litrateur ; previous() retourne cet lment et le dplace vers larrire. Les itrateurs next() et previous() retournent llment que litrateur vient de passer. (Voir Figure 11.4).
Figure 11.4 Effet de previous() et de next() sur un itrateur de style Java
A
previous()

D
next()

Les itrateurs mutables fournissent des fonctions destines insrer, modier et supprimer des lments lors de litration. La boucle suivante supprime tous les nombres ngatifs dune liste :
QMutableListIterator<double> i(list); while (i.hasNext()) { if (i.next() < 0.0) i.remove(); }

La fonction remove() opre toujours sur le dernier lment pass. Elle fonctionne galement lors de litration vers larrire.
QMutableListIterator<double> i(list); i.toBack(); while (i.hasPrevious()) { if (i.previous() < 0.0) i.remove(); }

De la mme faon, les itrateurs mutables de style Java fournissent une fonction setValue() qui modie le dernier lment pass. Voici comment nous remplacerions des nombres ngatifs par leur valeur absolue :
QMutableListIterator<double> i(list); while (i.hasNext()) { int val = i.next(); if (val < 0.0) i.setValue(-val); }

Chapitre 11

Classes conteneur

269

Il est galement possible dinsrer un lment lemplacement courant de litrateur en appelant insert(). Litrateur est alors avanc lemplacement se situant entre le nouvel lment et llment suivant. En plus des itrateurs de style Java, chaque classe conteneur squentiel C<T> possde deux types ditrateurs de style STL : C<T>::iterator et C<T>::const_iterator. La diffrence entre les deux est que const_iterator ne nous permet pas de modier les donnes. La fonction begin() dun conteneur retourne un itrateur de style STL faisant rfrence au premier lment du conteneur (par exemple list[0]), alors que end() retourne un itrateur pointant vers llment suivant le dernier (par exemple, list[5] pour une liste de taille 5). Si un conteneur est vide, begin() est gal end(). Cette caractristique peut tre utilise pour dterminer si le conteneur comporte des lments, bien quil soit gnralement plus appropri dappeler isEmpty() cette n. (Voir Figure 11.5)
Figure 11.5 Emplacements valides pour les itrateurs de style STL
A B C D E

begin()

end()

La syntaxe dun itrateur de style STL est modele sur celle des pointeurs C++ dans un tableau. Nous pouvons utiliser les oprateurs ++ et -- pour passer llment prcdent ou suivant et loprateur * unaire pour rcuprer llment en cours. Pour QVector<T>, litrateur et les types const_iterator sont simplement des typedefs de T* et constT*. (Ceci est possible parce que QVector<T> stocke ses lments dans des emplacements conscutifs en mmoire.) Lexemple suivant remplace chaque valeur dun QList<double> par sa valeur absolue :
QList<double>::iterator i = list.begin(); while (i!= list.end()) { *i = qAbs(*i); ++i; }

Quelques fonctions Qt retournent un conteneur. Si nous voulons parcourir la valeur de retour dune fonction au moyen dun itrateur de style STL, nous devons prendre une copie du conteneur et parcourir cette copie. Le code suivant, par exemple, illustre comment parcourir correctement le QList<int> retourn par QSplitter::sizes():
QList<int> list = splitter->sizes(); QList<int>::const_iterator i = list.begin(); while (i!= list.end()) { do_something(*i); ++i; }

270

Qt4 et C++ : Programmation dinterfaces GUI

Le code suivant est incorrect :


// INEXACT QList<int>::const_iterator i = splitter->sizes().begin(); while (i!= splitter->sizes().end()) { do_something(*i); ++i; }

En effet, QSplitter::sizes() retourne un nouveau QList<int> par valeur chacun de ses appels. Si nous ne stockons pas la valeur de retour, C++ la dtruit automatiquement avant mme que nous ayons dbut litration, nous laissant avec un itrateur sans liaison. Pire encore, chaque excution de la boucle, QSplitter::sizes() doit gnrer une nouvelle copie de la liste cause de lappel splitter_>sizes().end(). En rsum : lorsque vous utilisez des itrateurs de style STL, parcourez toujours vos lments sur une copie dun conteneur. Avec les itrateurs de style Java en lecture seulement, il est inutile de recourir une copie. Litrateur se charge de crer cette copie en arrire-plan. Par exemple :
QListIterator<int> i(splitter->sizes()); while (i.hasNext()) { do_something(i.next()); }

La copie dun conteneur tel que celui-ci semble coteuse, mais il nen est rien, grce loptimisation obtenue par le partage implicite. La copie dun conteneur Qt est pratiquement aussi rapide que celle dun pointeur unique. Les donnes ne sont vritablement copies que si lune des copies est change et tout ceci est gr automatiquement larrire-plan. Cest pourquoi le partage implicite est quelquefois nomm "copie lcriture". Lintrt du partage implicite est quil sagit dune optimisation dont nous bncions sans intervention de la part du programmeur. En outre, le partage implicite favorise un style de programmation clair, o les objets sont retourns par valeur. Considrez la fonction suivante :
QVector<double> sineTable() { QVector<double> vect(360); for (int i = 0; i < 360; ++i) vect[i] = sin(i / (2 * M_PI)); return vect; }

Voici lappel la fonction :


QVector<double> table = sineTable();

STL, nous incite plutt transmettre le vecteur comme rfrence non const pour viter lexcution de la copie lorsque la valeur de retour de la fonction est stocke dans une variable :

Chapitre 11

Classes conteneur

271

using namespace std; void sineTable(vector<double> &vect) { vect.resize(360); for (int i = 0; i < 360; ++i) vect[i] = sin(i / (2 * M_PI)); }

Lappel devient alors plus difcile crire et lire :


vector<double> table; sineTable(table);

Qt utilise le partage implicite pour tous ses conteneurs ainsi que pour de nombreuses autres classes, dont QByteArray, QBrush, QFont, QImage, QPixmap et QString . Le partage implicite est une garantie de la part de Qt que les donnes ne seront pas copies si nous ne les modions pas. Pour obtenir le meilleur du partage implicite, nous pouvons adopter deux nouvelles habitudes de programmation. Lune consiste coder la fonction at() au lieu de loprateur [] pour un accs en lecture seulement sur une liste ou un vecteur (non const). Les conteneurs Qt ne pouvant pas dterminer si [] apparat sur le ct gauche dune affectation ou non, le pire est envisag et une copie intgrale est dclenche alors que at() nest pas autoris sur le ct gauche dune affectation. Un problme similaire se pose lorsque nous parcourons un conteneur avec des itrateurs de style STL. Ds que nous appelons begin() ou end() sur un conteneur non const, Qt force une copie complte si les donnes sont partages. Pour viter ceci, la solution consiste utiliser const_iterator, constBegin() et constEnd() ds que possible. Qt fournit une dernire mthode pour parcourir les lments situs dans un conteneur squentiel : la boucle foreach. Voici sa syntaxe :
QLinkedList<Movie> list; ... foreach (Movie movie, list) { if (movie.title() == "Citizen Kane") { cout << "Found Citizen Kane" << endl; break; } }

Le pseudo mot-cl foreach est implment sous la forme dune boucle for standard. A chaque itration de la boucle, la variable ditration (movie) est dnie en un nouvel lment, commenant au premier lment du conteneur et progressant vers lavant. La boucle foreach reoit automatiquement une copie du conteneur. Elle ne sera donc pas affecte si le conteneur est modi durant litration.

272

Qt4 et C++ : Programmation dinterfaces GUI

Fonctionnement du partage implicite


Le partage implicite seffectue automatiquement en arrire-plan. Aucune action nest donc ncessaire dans notre code pour que cette optimisation se produise. Mais comme il est intressant de comprendre comment les choses fonctionnent, nous allons tudier un exemple et voir ce qui se passe en interne. Lexemple utilise QString, une des nombreuses classes implicitement partages de Qt.
QString str1 = "Humpty"; QString str2 = str1;

Nous dnissons str1 en "Humpty" et str2 de sorte quil soit gal str1. A ce stade, les deux objets QString pointent vers la mme structure de donnes interne en mmoire. Avec les donnes de type caractre, il existe pour une structure de donnes un compteur de rfrence indiquant le nombre de QString pointant vers celle-ci. str1 et str2 pointant vers la mme donne, le compteur de rfrence indique 2.
str2[0] = D;

Lorsque nous modions str2, il ralise tout dabord une copie intgrale des donnes pour sassurer que str1 et str2 pointent vers des structures de donnes diffrentes, puis il applique la modication sa propre copie des donnes. Le compteur de rfrence des donnes de str1 ("Humpty") indique alors 1 et celui des donnes de str2 ("Dumpty") est dni en 1. Quand un compteur de rfrence indique 1, les donnes ne sont pas partages.
str2.truncate(4);

Si nous modions de nouveau str2, aucune copie ne se produit car le compteur de rfrence des donnes de str2 indique 1. La fonction truncate() agit directement sur les donnes de str2, rsultant en la chane "Dump". Le compteur de rfrence reste 1.
str1 = str2;

Lorsque nous affectons str2 str1, le compteur de rfrence des donnes de str1 descend 0, ce qui signie quaucun QString nutilise plus la donne "Humpty". La donne est alors libre de la mmoire. Les deux QString pointent vers "Dump", dont le compteur de rfrence indique maintenant 2. Le partage de donnes est une option souvent ignore dans les programmes multithread, cause des conditions de comptition dans le dcompte des rfrences. Avec Qt, ceci nest plus un problme. En interne, les classes conteneur utilisent des instructions du langage dassembly pour effectuer un dcompte de rfrences atomique. Cette technologie est la porte des utilisateurs de Qt par le biais des classes QSharedData et QSharedDataPointer.

Chapitre 11

Classes conteneur

273

Les instructions de boucle break et continue sont prises en charge. Si le corps est constitu dune seule instruction, les accolades ne sont pas ncessaires. Comme pour une instruction for, la variable ditration peut tre dnie lextrieur de la boucle, comme suit :
QLinkedList<Movie> list; Movie movie; ... foreach (movie, list) { if (movie.title() == "Citizen Kane") { cout << "Found Citizen Kane" << endl; break; } }

La dnition de la variable ditration lextrieur de la boucle est la seule solution pour les conteneurs comportant des types de donnes avec une virgule (par exemple, QPair<QString,int>).

Conteneurs associatifs
Un conteneur associatif comporte un nombre arbitraire dlments du mme type, indexs par une cl. Qt fournit deux classes de conteneurs associatifs principales : QMap<K,T> et QHash<K,T>. Un QMap<K,T> est une structure de donnes qui stocke des paires cl/valeur dans un ordre croissant des cls. Cette organisation permet dobtenir de bonnes performances en matire de recherche et dinsertion ainsi quune itration ordonne. En interne, QMap<K,T> est implment sous forme de liste branchement. (Voir Figure 11.6)
Figure 11.6 Un map de QString vers int
Mexico City Seoul Tokyo 22 350 000 22 050 000 34 000 000

Un moyen simple dinsrer des lments dans un map consiste appeler insert():
QMap<QString, int> map; map.insert("eins", 1); map.insert("sieben", 7); map.insert("dreiundzwanzig", 23);

Nous avons aussi la possibilit daffecter simplement une valeur une cl donne comme suit :
map["eins"] = 1; map["sieben"] = 7; map["dreiundzwanzig"] = 23;

274

Qt4 et C++ : Programmation dinterfaces GUI

Loprateur [] peut tre utilis la fois pour linsertion et la rcupration. Si [] est utilis pour rcuprer une valeur dune cl non existante dans un map non const, un nouvel lment sera cr avec la cl donne et une valeur vide. Lutilisation de la fonction value() la place de [] pour rcuprer les lments permet dviter la cration accidentelle de valeurs vides :
int val = map.value("dreiundzwanzig");

Si la cl nexiste pas, une valeur par dfaut est retourne et aucun nouvel lment nest cr. Pour les types de base et les types pointeur, la valeur retourne est nulle. Nous pouvons spcier une autre valeur par dfaut comme second argument pour value():
int int seconds = map.value("delay", 30);

Cette ligne de code est quivalente :


int seconds = 30; if (map.contains("delay")) seconds = map.value("delay");

Les types de donnes K et T dun QMap<K,T> peuvent tre des types de base tels que int et double, des types pointeur ou de classes possdant un constructeur par dfaut, un constructeur de copie et un oprateur daffectation. En outre, le type K doit fournir un operator<() car QMap<K,T> utilise cet oprateur pour stocker les lments dans un ordre de cl croissant. QMap<K,T> possde deux fonctions utilitaires keys() et values(), qui savrent particulirement intressantes pour travailler avec de petits ensembles de donnes. Elles retournent des QList des cls et valeurs dun map. Les maps sont gnralement valeur unique : si une nouvelle valeur est affecte une cl existante, lancienne valeur est remplace par la nouvelle. De cette faon, deux lments ne partagent pas la mme cl. Il est possible davoir des valeurs multiples pour la mme cl en utilisant la fonction insertMulti() ou la sous-classe utilitaire QMultiMap<K,T>. QMap<K,T> possde une surcharge values(constK &) qui retourne un QList de toutes les valeurs pour une cl donne. Par exemple :
QMultiMap<int, QString> multiMap; multiMap.insert(1, "one"); multiMap.insert(1, "eins"); multiMap.insert(1, "uno"); QList<QString> vals = multiMap.values(1);

Un QHash<K,T> est une structure de donnes qui stocke des paires cl/valeur dans une table de hachage. Son interface est pratiquement identique celle de QMap<K,T>, mais ses exigences concernant le type template K sont diffrentes et il offre des oprations de recherche beaucoup plus rapides que QMap<K,T>. Une autre diffrence est que QHash<K,T> nest pas ordonn. En complment des conditions standard concernant tout type de valeur stock dans un conteneur, le type K dun QHash<K,T> doit fournir un operator==() et tre support par une

Chapitre 11

Classes conteneur

275

fonction QHash() globale qui retourne une valeur de hachage pour une cl. Qt fournit dj des fonctions QHash() pour les types entiers, les types pointeur, QChar, QString et QByteArray.

QHash<K,T> alloue automatiquement un nombre principal de blocs pour sa table de hachage interne et redimensionne celle-ci lors de linsertion ou de la suppression dlments. Il est galement possible de rgler avec prcision les performances en appelant reserve() pour spcier le nombre dlments stocker dans la table et squeeze() pour rduire cette table en fonction du nombre dlments en cours. Une pratique courante consiste appeler reserve() avec le nombre maximum dlments susceptibles dtre stocks, puis insrer les donnes et nalement appeler squeeze() pour rduire lutilisation de la mmoire si les lments sont moins nombreux que prvu.
Les hachages sont gnralement valeur unique, mais plusieurs valeurs peuvent tre affectes la mme cl laide de la fonction insertMulti() ou de la sous-classe utilitaire QMultihash<K,T>. En plus de QHash<K,T>, Qt fournit une classe QCache<K,T> qui peut tre utilise pour placer en cache des objets associs une cl, et un conteneur QSet<K> qui ne stocke que des cls. En interne, tous deux reposent sur QHash<K,T> et prsentent les mmes exigences concernant le type K. Le moyen le plus simple de parcourir toutes les paires cl/valeur stockes dans un conteneur associatif consiste utiliser un itrateur de style Java. Les itrateurs de ce style utiliss pour les conteneurs associatifs ne fonctionnent pas tout fait de la mme faon que leurs homologues squentiels. La principale diffrence est la suivante : les fonctions next() et previous() retournent un objet qui reprsente une paire cl/valeur, et non simplement une valeur. Les composants cl et valeur sont accessibles depuis cet objet en tant que key() et value(). Par exemple :
QMap<QString, int> map; ... int sum = 0; QMapIterator<QString, int> i(map); while (i.hasNext()) sum += i.next().value();

Si nous devons accder la fois la cl et la valeur, nous pouvons simplement ignorer la valeur de retour de next() ou de previous() et excuter les fonctions key() et value() de litrateur, qui oprent sur le dernier lment franchi :
QMapIterator<QString, int> i(map); while (i.hasNext()) { i.next(); if (i.value() > largestValue) { largestKey = i.key(); largestValue = i.value(); } }

276

Qt4 et C++ : Programmation dinterfaces GUI

Les itrateurs mutables possdent une fonction setValue() qui modie la valeur associe un lment courant :
QMutableMapIterator<QString, int> i(map); while (i.hasNext()) { i.next(); if (i.value() < 0.0) i.setValue(-i.value()); }

Les itrateurs de style STL fournissent galement des fonctions key() et value(). Avec des types ditrateur non const, value() retourne une rfrence non const, ce qui nous permet de changer la valeur au cours de litration. Remarquez que, bien que ces itrateurs soient "de style STL", ils prsentent des diffrences signicatives avec les itrateurs map<K,T> de STL, qui sont bass sur pair<K,T>. La boucle foreach fonctionne galement avec les conteneurs associatifs, mais uniquement avec le composant valeur des paires cl/valeur. Si nous avons besoin des deux composants cl et valeur des lments, nous pouvons appeler les fonctions keys() et values(constK &) dans des boucles foreach imbriques comme suit :
QMultiMap<QString, int> map; ... foreach (QString key, map.keys()) { foreach (int value, map.values(key)) { do_something(key, value); } }

Algorithmes gnriques
Len-tte <QtAlgorithms> dclare un ensemble de fonctions template globales qui implmentent des algorithmes de base sur les conteneurs. La plupart de ces fonctions agissent sur des itrateurs de style STL. Len-tte STL <algorithm> fournit un ensemble dalgorithmes gnriques plus complet. Ces algorithmes peuvent tre employs avec des conteneurs Qt ainsi que des conteneurs STL. Si les implmentations STL sont disponibles sur toutes vos plates-formes, il ny a probablement aucune raison de ne pas utiliser les algorithmes STL lorsque Qt ne propose pas lalgorithme quivalent. Nous prsenterons ici les algorithmes Qt les plus importants. Lalgorithme qFind() recher286che une valeur particulire dans un conteneur. Il reoit un itrateur "begin" et un itrateur "end" et retourne un itrateur pointant sur le premier lment correspondant, ou "end" sil nexiste pas de correspondance. Dans lexemple suivant, i est dni en list.begin() + 1 alors que j est dni en list.end().
QStringList list;

Chapitre 11

Classes conteneur

277

list << "Emma" << "Karl" << "James" << "Mariette"; QStringList::iterator i = qFind(list.begin(), list.end(), "Karl"); QStringList::iterator j = qFind(list.begin(), list.end(), "Petra");

Lalgorithme qBinaryFind() effectue une recherche similaire qFind(), mais il suppose que les lments sont tris dans un ordre croissant et utilise la recherche binaire rapide au lieu de la recherche linaire de qFind(). Lalgorithme qFill() remplit un conteneur avec une valeur particulire :
QLinkedList<int> list(10); qFill(list.begin(), list.end(), 1009);

Comme les autres algorithmes bass sur les itrateurs, nous pouvons galement utiliser qFill() sur une partie du conteneur en variant les arguments. Lextrait de code suivant initialise les cinq premiers lments dun vecteur en 1009 et les cinq derniers lments en 2013 :
QVector<int> vect(10); qFill(vect.begin(), vect.begin() + 5, 1009); qFill(vect.end() - 5, vect.end(), 2013);

Lalgorithme qCopy() copie des valeurs dun conteneur un autre :


QVector<int> vect(list.count()); qCopy(list.begin(), list.end(), vect.begin());

qCopy() peut galement tre utilis pour copier des valeurs dans le mme conteneur, ceci tant que la plage source et la plage cible ne se chevauchent pas. Dans lextrait de code suivant, nous lutilisons pour remplacer les deux derniers lments dune liste par les deux premiers :
qCopy(list.begin(), list.begin() + 2, list.end() - 2);

Lalgorithme qSort() trie les lments du conteneur en ordre croissant :


qSort(list.begin(), list.end());

Par dfaut, qSort() se sert de loprateur < pour comparer les lments. Pour trier les lments en ordre dcroissant, transmettez qGreater<T>() comme troisime argument (o T est le type des valeurs du conteneur), comme suit :
qSort(list.begin(), list.end(), qGreater<int>());

Nous pouvons utiliser le troisime paramtre pour dnir un critre de tri personnalis. Voici, par exemple, une fonction de comparaison "infrieur " qui compare des QString sans prendre la casse (majuscule-minuscule) en considration :
bool insensitiveLessThan(const QString &str1, const QString &str2) { return str1.toLower() < str2.toLower();

278

Qt4 et C++ : Programmation dinterfaces GUI

Lappel qSort() devient alors :


QStringList list; ... qSort(list.begin(), list.end(), insensitiveLessThan);

Lalgorithme qStableSort() est similaire qSort(), si ce nest quil garantit que les lments gaux apparaissent dans le mme ordre avant et aprs le tri. Cette caractristique est utile si le critre de tri ne prend en compte que des parties de la valeur et si les rsultats sont visibles pour lutilisateur. Nous avons utilis qStableSort() dans le Chapitre 4 pour implmenter le tri dans lapplication Spreadsheet. Lalgorithme qDeleteAll() appelle delete sur chaque pointeur stock dans un conteneur. Ceci nest utile que pour les conteneurs dont le type de valeur est un type pointeur. Aprs lappel, les lments sont toujours prsents, vous excutez clear() sur le conteneur. Par exemple :
qDeleteAll(list); list.clear();

Lalgorithme qSwap() change la valeur de deux variables. Par exemple :


int x1 = line.x1(); int x2 = line.x2(); if (x1 > x2) qSwap(x1, x2);

Enn, len-tte <QtGlobal> fournit plusieurs dnitions utiles, dont la fonction qAbs(), qui retourne la valeur absolue de son argument ainsi que les fonctions qMin() et qMax() qui retournent le minimum ou le maximum entre deux valeurs.

Chanes, tableaux doctets et variants


QString, QByteArray et QVariant sont trois classes ayant de nombreux points en commun avec les conteneurs. Elles sont susceptibles dtre utilises la place de ceux-ci dans certaines situations. En outre, comme les conteneurs, ces classes utilisent le partage implicite pour optimiser la mmoire et la vitesse. Nous allons commencer par QString. Les chanes sont utilises par tout programme GUI, non seulement pour linterface utilisateur, mais galement en tant que structures de donnes. C++ fournit en natif deux types de chanes : les traditionnels tableaux de caractres termins par "0" et la classe std::string. Contrairement celles-ci, QString contient des valeurs Unicode 16 bits. Unicode comprend les systmes ASCII et Latin-1, avec leurs valeurs numriques habituelles. Mais QString tant une classe 16 bits, elle peut reprsenter des milliers de caractres diffrents utiliss dans la plupart des langues mondiales. Reportez-vous au Chapitre 17 pour de plus amples informations concernant Unicode.

Chapitre 11

Classes conteneur

279

Lorsque vous utilisez QString, vous navez pas besoin de vous proccuper de dtails comme lallocation dune mmoire sufsante ou de vrier que la donne est termine par "0". Conceptuellement, les objets QString peuvent tre considrs comme des vecteurs de QChar. Un QString peut intgrer des caractres "0". La fonction length() retourne la taille de la chane entire, dont les caractres "0" intgrs.

QString fournit un oprateur + binaire destin concatner deux chanes ainsi quun oprateur += dont la fonction est daccoler une chane une autre. Voici un exemple combinant + et +=.
QString str = "User: "; str += userName + "\n";

Il existe galement une fonction QString::append() dont la tche est identique celle de loprateur +=:
str = "User: "; str.append(userName); str.append("\n");

Un moyen totalement diffrent de combiner des chanes consiste utiliser la fonction sprintf() de QString:
str.sprintf("%s %.1f%%", "perfect competition", 100.0);

Cette fonction prend en charge les mmes spcicateurs de format que la fonction sprintf() de la bibliothque C++. Dans lexemple ci-dessus, "perfect competition 100.0 %" est affect str. Un moyen supplmentaire de crer une chane partir dautres chanes ou de nombres consiste utiliser arg():
str = QString("%1%2 (%3s-%4s)") .arg("permissive").arg("society").arg(1950).arg(1970);

Dans cet exemple, "%1", "%2", "%3" et "%4" sont remplacs par "permissive", "society", "1950" et "1970", respectivement. On obtient "permissive society (1950s-1970s)". Il existe des surcharges de arg() destines grer divers types de donnes. Certaines surcharges comportent des paramtres supplmentaires an de contrler la largeur de champ, la base numrique ou la prcision de la virgule ottante. En gnral, arg() reprsente une solution bien meilleure que sprintf(), car elle est de type scuris, prend totalement en charge Unicode et autorise les convertisseurs rordonner les paramtres "%n".

QString peut convertir des nombres en chanes au moyen de la fonction statique QString: :number():
str = QString::number(59.6);

280

Qt4 et C++ : Programmation dinterfaces GUI

Ou en utilisant la fonction setNum():


str.setNum(59.6);

La conversion inverse, dune chane en un nombre, est ralise laide de toInt(), toLongLong(), toDouble() et ainsi de suite. Par exemple :
bool ok; double d = str.toDouble(&ok);

Ces fonctions peuvent recevoir en option un pointeur facultatif vers une variable bool et elles dnissent la variable en true ou false selon le succs de la conversion. Si la conversion choue, les fonctions retournent zro. Il est souvent ncessaire dextraire des parties dune chane. La fonction mid() retourne la sous-chane dbutant un emplacement donn (premier argument) et de longueur donne (second argument). Par exemple, le code suivant afche "pays" sur la console1 :
QString str = "polluter pays principle"; qDebug() << str.mid(9, 4);

Si nous omettons le second argument, mid() retourne la sous-chane dbutant lemplacement donn et se terminant la n de la chane. Par exemple, le code suivant afche "pays principle" sur la console :
QString str = "polluter pays principle"; qDebug() << str.mid(9);

Il existe aussi des fonctions left() et right() dont la tche est similaire. Toutes deux reoivent un nombre de caractres, n, et retournent les n premiers ou derniers caractres de la chane. Par exemple, le code suivant afche "polluter principle" sur la console :
QString str = "polluter pays principle"; qDebug() << str.left(8) << " " << str.right(9);

Pour rechercher si un chane contient un caractre particulier, une sous-chane ou une expression rgulire, nous pouvons utiliser lune des fonctions indexOf() de QString :
QString str = "the middle bit"; int i = str.indexOf("middle");

Dans ce cas, i sera dni en 4. La fonction indexOf() retourne 1 en cas dchec et reoit en option un emplacement de dpart ainsi quun indicateur de sensibilit la casse.

1. La syntaxe qDebug()<<arg utilise ici ncessite linclusion du chier den-tte <QtDebug>, alors que la syntaxe qDebug("",arg) est disponible dans tout chier incluant au moins un en-tte Qt.

Chapitre 11

Classes conteneur

281

Si nous souhaitons simplement vrier si une chane commence ou se termine par quelque chose, nous pouvons utiliser les fonctions startsWith() et endsWith():
if (url.startsWith("http:") && url.endsWith(".png")) ...

Ce qui est la fois plus rapide et plus simple que :


if (url.left(5) == "http:" && url.right(4) == ".png") ...

La comparaison de chanes avec loprateur == diffrencie les majuscules des minuscules. Si nous comparons des chanes visibles pour lutilisateur, localeAwareCompare() reprsente gnralement un bon choix, et si nous souhaitons effectuer des comparaisons sensibles la casse, nous pouvons utiliser toUpper() ou toLower(). Par exemple :
if (fileName.toLower() == "readme.txt") ...

Pour remplacer une certaine partie dune chane par une autre chane, nous codons replace():
QString str = "a cloudy day"; str.replace(2, 6, "sunny");

On obtient "a sunny day". Le code peut tre rcrit de faon excuter remove() et insert().
str.remove(2, 6); str.insert(2, "sunny");

Dans un premier temps, nous supprimons six caractres en commenant lemplacement 2, ce qui aboutit la chane "a day" (avec deux espaces), puis nous insrons "sunny" ce mme emplacement. Des versions surcharges de replace() permettent de remplacer toutes les occurrences de leur premier argument par leur second argument. Par exemple, voici comment remplacer toutes les occurrences de "&" par "&amp;" dans une chane :
str.replace("&", "&amp;");

Il est trs souvent ncessaire de supprimer les blancs (tels que les espaces, les tabulations et les retours la lignes) dans une chane. QString possde une fonction qui limine les espaces situs aux deux extrmits dune chane :
QString str = " BOB \t THE \nDOG \n"; qDebug() << str.trimmed();

282

Qt4 et C++ : Programmation dinterfaces GUI

La chane str peut tre reprsente comme suit :


B O B \t T H E \n D O G \n

La chane retourne par trimmed() est


B O B \t T H E \n D O G

Lorsque nous grons les entres utilisateur, nous avons souvent besoin de remplacer les squences internes dun ou de plusieurs caractres despace par un espace unique, ainsi que dliminer les espaces aux deux extrmits. Voici laction de la fonction simplified():
QString str = " BOB \t THE \nDOG \n"; qDebug() << str.simplified();

La chane retourne par simplified() est :


B O B T H E D O G

Une chane peut tre divise en un QStringList de sous-chanes au moyen de QString::split():


QString str = "polluter pays principle"; QStringList words = str.split(" ");

Dans lexemple ci-dessus, nous divisons la chane "polluter pays principle" en trois souschanes : "polluter", "pays" et "principle". La fonction split() possde un troisime argument facultatif qui spcie si les sous-chanes vides doivent tre conserves (option par dfaut) ou limines. Les lments dun QStringList peuvent tre unis pour former une chane unique au moyen de join(). Largument de join() est insr entre chaque paire de chanes jointes. Par exemple, voici comment crer une chane unique qui est compose de toutes les chanes contenues dans un QStringList tries par ordre alphabtique et spares par des retours la lignes :
words.sort(); str = words.join("\n");

En travaillant avec les chanes, il est souvent ncessaire de dterminer si elles sont vides ou non. Pour ce faire, appelez isEmpty() ou vriez si length() est gal 0. La conversion de chanes const char* en QString est automatique dans la plupart des cas. Par exemple :
str += " (1870)";

Chapitre 11

Classes conteneur

283

Ici nous ajoutons un constchar* un QString sans formalit. Pour convertir explicitement un const char* en un QString, il suft dutiliser une conversion QString ou encore dappeler fromAscii() ou fromLatin1(). (Reportez-vous au Chapitre 17 pour obtenir une explication concernant la gestion des chanes littrales et autres codages.) Pour convertir un QString en un const char *, excutez toAscii() ou toLatin1(). Ces fonctions retournent un QByteArray, qui peut tre converti en un const char* en codant QByteArray::data() ou QByteArray::constData(). Par exemple :
printf("User: %s\n", str.toAscii().data());

Pour des raisons pratiques, Qt fournit la macro qPrintable() dont laction est identique celle de la squence toAscii().constData():
printf("User: %s\n", qPrintable(str));

Lorsque nous appelons data() ou constData() sur un QByteArray, la chane retourne appartient lobjet QByteArray, ce qui signie que nous navons pas nous proccuper de problmes de pertes de mmoire. Qt se chargera de librer la mmoire. Dautre part, nous devons veiller ne pas utiliser le pointeur trop longtemps. Si le QByteArray nest pas stock dans une variable, il sera automatiquement supprim la n de linstruction. La classe QByteArray possde une API trs similaire celle de QString. Des fonctions telles que left(), right(), mid(), toLower(), toUpper(), trimmed() et simplified() ont la mme smantique dans QByteArray que leurs homologues QString. QByteArray est utile pour stocker des donnes binaires brutes et des chanes de texte codes en 8 bits. En gnral, nous conseillons dutiliser QString plutt que QByteArray pour stocker du texte, car cette classe supporte Unicode. Pour des raisons de commodit, QByteArray sassure automatiquement que loctet suivant le dernier lment est toujours "0", ce qui facilite la transmission dun QByteArray une fonction recevant un const char *. QByteArray prend aussi en charge les caractres "0" intgrs, ce qui nous permet de lutiliser pour stocker des donnes binaires arbitraires. Dans certaines situations, il est ncessaire de stocker des donnes de types diffrents dans la mme variable. Une approche consiste coder les donnes en tant que QByteArray ou QString. Ces approches offrent une exibilit totale, mais annule certains avantages du C++, et notamment la scurit et lefcacit des types. Qt offre une bien meilleure solution pour grer des variables contenant diffrents types : QVariant. La classe QVariant peut contenir des valeurs de nombreux types Qt, dont QBrush, QColor, QCursor, QDateTime, QFont, QKeySequence, QPalette, QPen, QPixmap, QPoint, QRect, QRegion, QSize et QString, ainsi que des types numriques C++ de base tels que double et int. Cette classe est galement susceptible de contenir des conteneurs : QMap<QString, QVariant>, QStringList et QList<QVariant>.

284

Qt4 et C++ : Programmation dinterfaces GUI

Les variants sont abondamment utiliss par les classes dafchage dlments, le module de base de donnes et QSettings, ce qui nous permet de lire et dcrire des donnes dlment, des donnes de base de donnes et des prfrences utilisateur pour tout type compatible QVariant. Nous en avons dj rencontr un exemple dans le Chapitre 3, o nous avons transmis un QRect, un QStringList et deux bool en tant que variants QSettings::setValue(). Nous les avons rcuprs ultrieurement comme variants. Il est possible de crer arbitrairement des structures de donnes complexes utilisant QVariant en imbriquant des valeurs de types conteneur :
QMap<QString, QVariant> pearMap; pearMap["Standard"] = 1.95; pearMap["Organic"] = 2.25; QMap<QString, QVariant> fruitMap; fruitMap["Orange"] = 2.10; fruitMap["Pineapple"] = 3.85; fruitMap["Pear"] = pearMap;

Ici, nous avons cr un map avec des cls sous forme de chanes (noms de produit) et des valeurs qui sont soit des nombres virgule ottante (prix), soit des maps. Le map de niveau suprieur contient trois cls : "Orange", "Pear" et "Pineapple". La valeur associe la cl "Pear" est un map qui contient deux cls ("Standard" et "Organic"). Lorsque nous parcourons un map contenant des variants, nous devons utiliser type() pour en contrler le type de faon pouvoir rpondre de faon approprie. La cration de telles structures de donnes peut sembler trs sduisante, car nous pouvons ainsi organiser les donnes exactement comme nous le souhaitons. Mais le caractre pratique de QVariant est obtenu au dtriment de lefcacit et de la lisibilit. En rgle gnrale, il convient de dnir une classe C++ correcte pour stocker les donnes ds que possible.

QVariant est utilis par le systme mtaobjet de Qt et fait donc partie du module QtCore. Nanmoins, lorsque nous le rattachons au module QtGui, QVariant peut stocker des types en liaison avec linterface utilisateur graphique tels que QColor, QFont, QIcon, QImage et QPixmap:
QIcon icon("open.png"); QVariant variant = icon;

Pour rcuprer la valeur dun tel type partir dun QVariant, nous pouvons utiliser la fonction membre template QVariant::Value<T>() comme suit :
QIcon icon = variant.value<QIcon>();

La fonction value<T>() permet galement deffectuer des conversions entre des types de donnes non graphiques et QVariant, mais en pratique, nous utilisons habituellement les fonctions de conversion to() (par exemple, toString()) pour les types non graphiques.

Chapitre 11

Classes conteneur

285

QVariant peut aussi tre utilis pour stocker des types de donnes personnaliss, en supposant quils fournissent un constructeur par dfaut et un constructeur de copie. Pour que ceci fonctionne, nous devons tout dabord enregistrer le type au moyen de la macro Q_DECLARE_METATYPE(), gnralement dans un chier den-tte en dessous de la dnition de classe :
Q_DECLARE_METATYPE(BusinessCard)

Cette technique nous permet dcrire du code tel que celui-ci :


BusinessCard businessCard; QVariant variant = QVariant::fromValue(businessCard); ... if (variant.canConvert<BusinessCard>()) { BusinessCard card = variant.value<BusinessCard>(); ... }

Ces fonctions membre template ne sont pas disponibles pour MSVC 6, cause dune limite du compilateur. Si vous devez employer ce compilateur, utilisez plutt les fonctions globales qVariantFromValue(), qVariantValue<T>()et qVariantCanConvert<T>(). Si le type de donnes personnalis possde des oprateurs << et >> pour effectuer des oprations de lecture et dcriture dans un QDataStream, nous pouvons les enregistrer au moyen de qRegisterMetaTypeStreamOperators<T>(). Il est ainsi possible de stocker les prfrences des types de donnes personnaliss laide de QSettings, entre autres. Par exemple :
qRegisterMetaTypeStreamOperators<BusinessCard>("BusinessCard");

Dans ce chapitre, nous avons principalement tudi les conteneurs Qt, ainsi que QString, QByteArray et QVariant. En complment de ces classes, Qt fournit quelques autres conteneurs. QPair<T1,T2> en fait partie, qui stocke simplement deux valeurs et prsente des similitudes avec std::pair<T1,T2>. QBitArray est un autre conteneur que nous utiliserons dans la premire partie du Chapitre 19. Il existe enn QVarLengthArray<T,Prealloc>, une alternative de bas niveau QVector<T>. Comme il pralloue de la mmoire sur la pile et nest pas implicitement partag, sa surcharge est infrieure celle de QVector<T>, ce qui le rend plus appropri pour les boucles troites. Les algorithmes de Qt, dont quelques-uns nayant pas t tudis ici tels que qCopyBackward() et qEqual(), sont dcrits dans la documentation de Qt ladresse http://doc.trolltech.com/ 4.1/algorithms.html. Vous trouverez des dtails complmentaires concernant les conteneurs de Qt ladresse http://doc.trolltech.com/4.1/containers.html.

12
Entres/Sorties
Au sommaire de ce chapitre Lire et crire des donnes binaires Lire et crire du texte Parcourir des rpertoires Intgrer des ressources Communication inter-processus

Le besoin deffectuer des oprations de lecture et dcriture dans des chiers ou sur un autre support est commun presque toute application. Qt fournit un excellent support pour ces oprations par le biais de QIODevice, une abstraction puissante qui encapsule des "priphriques" capables de lire et dcrire des blocs doctets. Qt inclut les sousclasses QIODevice suivantes :
QFile
Accde aux chiers dun systme de chiers local et de ressources intgres.

QTemporaryFile Cre et accde des chiers temporaires du systme de chiers local. QBuffer QProcess QTcpSocket QUdpSocket
Effectue des oprations de lecture et dcriture de donnes dans un QByteArray. Excute des programmes externes et gre la communication inter-processus. Transfre un ux de donnes sur le rseau au moyen de TCP. Envoie ou reoit des datagrammes UDP sur le rseau.

288

Qt4 et C++ : Programmation dinterfaces GUI

QProcess, QTcpSocket et QUdpSocket sont des classes squentielles, ce qui implique un accs unique aux donnes, en commenant par le premier octet et en progressant dans lordre jusquau dernier octet. QFile, QTemporaryFile et QBuffer sont des classes accs alatoire. Les octets peuvent donc tre lus plusieurs fois partir de tout emplacement. Elles fournissent la fonction QIODevice::seek() qui permet de repositionner le pointeur de chier. En plus de ces classes de priphrique, Qt fournit deux classes de ux de niveau plus lev qui excutent des oprations de lecture et criture sur tout priphrique dE/S : QDataStream pour les donnes binaires et QTextStream pour le texte. Ces classes grent des problmes tels que le classement des octets et les codages de texte, de sorte que des applications Qt sexcutant sur dautres plates-formes ou pays puissent effectuer des oprations de lecture et dcriture sur leurs chiers respectifs. Ceci rend les classes dE/S de Qt beaucoup plus pratiques que les classes C++ standard correspondantes, qui laissent la gestion de ces problmes au programmeur de lapplication. QFile facilite laccs aux chiers individuels, quils soient dans le systme de chier ou intgrs dans lexcutable de lapplication en tant que ressources. Pour les applications ayant besoin didentier des jeux complets de chiers sur lesquels travailler, Qt fournit les classes QDir et QFileInfo, qui grent des rpertoires et fournissent des informations concernant leurs chiers. La classe QProcess nous permet de lancer des programmes externes et de communiquer avec ceux-ci par le biais de leurs canaux dentre, de sortie et derreur standard (cin, cout et cerr). Nous pouvons dnir les variables denvironnement et le rpertoire de travail qui seront utiliss par lapplication externe. Par dfaut, la communication avec le processus est asynchrone (non bloquante), mais il est possible de parvenir un blocage pour certaines oprations. La gestion de rseau ainsi que la lecture et lcriture XML sont des thmes importants qui seront traits sparment dans leurs propres chapitres (Chapitre 14 et Chapitre 15).

Lire et crire des donnes binaires


La faon la plus simple de charger et denregistrer des donnes binaires avec Qt consiste instancier un QFile, ouvrir le chier et y accder par le biais dun objet QDataStream. Ce dernier fournit un format de stockage indpendant de la plate-forme qui supporte les types C++ de base tels que int et double, de nombreux types de donnes Qt, dont QByteArray, QFont, QImage, QPixmap, QString et QVariant ainsi que des classes conteneur telles que QList<T> et QMap<K,T>. Voici comment stocker un entier, un QImage et un QMap<QString,QColor> dans un chier nomm facts.dat:
QImage image("philip.png"); QMap<QString, QColor> map; map.insert("red", Qt::red);

Chapitre 12

Entres/Sorties

289

map.insert("green", Qt::green); map.insert("blue", Qt::blue); QFile file("facts.dat"); if (!file.open(QIODevice::WriteOnly)) { cerr << "Cannot open file for writing: " << qPrintable(file.errorString()) << endl; return; } QDataStream out(&file); out.setVersion(QDataStream::Qt_4_1); out << quint32(0x12345678) << image << map;

Si nous ne pouvons pas ouvrir le chier, nous en informons lutilisateur et rendons le contrle. La macro qPrintable() retourne un constchar* pour un QString. (Une autre approche consiste excuter QString::toStdString(), qui retourne un std::string, pour lequel <iostream> possde une surcharge <<.) Si le chier souvre avec succs, nous crons un qDataStream et dnissons son numro de version. Le numro de version est un entier qui inuence la faon dont les types de donnes Qt sont reprsents (les types de donnes C++ de base sont toujours reprsents de la mme faon). Dans Qt 4.1, le format le plus complet est la version 7. Nous pouvons soit coder en dur la constante 7, soit utiliser le nom symbolique QDataStream::Qt_4_1. Pour garantir que le nombre 0x12345678 sera bien enregistr en tant quentier non sign de 32 bits sur toutes les plates-formes, nous le convertissons en quint32, un type de donnes dont les 32 bits sont garantis. Pour assurer linteroprabilit, QDataStream est bas par dfaut sur Big-Endian, ce qui peut tre modi en appelant setByteOrder(). Il est inutile de fermer explicitement le chier, cette opration tant effectue automatiquement lorsque la variable QFile sort de la porte. Si nous souhaitons vrier que les donnes ont bien t crites, nous appelons flush() et vrions sa valeur de retour (true en cas de succs). Le code destin lire les donnes rete celui que nous avons utilis pour les crire :
quint32 n; QImage image; QMap<QString, QColor> map; QFile file("facts.dat"); if (!file.open(QIODevice::ReadOnly)) { cerr << "Cannot open file for reading: " << qPrintable(file.errorString()) << endl; return; } QDataStream in(&file); in.setVersion(QDataStream::Qt_4_1); in >> n >> image >> map;

290

Qt4 et C++ : Programmation dinterfaces GUI

La version de QDataStream que nous employons pour la lecture est la mme que celle utilise pour lcriture, ce qui doit toujours tre le cas. En codant en dur le numro de version, nous garantissons que lapplication est toujours en mesure de lire et dcrire les donnes. QDataStream stocke les donnes de telle faon que nous puissions les lire parfaitement. Par exemple, un QByteArray est reprsent sous la forme dun dcompte doctets, suivi des octets eux-mmes. QDataStream peut aussi tre utilis pour lire et crire des octets bruts, sans entte de dcompte doctets, au moyen de readRawBytes() et de writeRawBytes(). Lors de la lecture partir dun QDataStream, la gestion des erreurs est assez facile. Le ux possde une valeur status() qui peut tre QDataStream::Ok, QDataStream::ReadPastEnd ou QDataStream::ReadCorruptData. Quand une erreur se produit, loprateur >> lit toujours zro ou des valeurs vides. Nous pouvons ainsi lire un chier entier sans nous proccuper derreurs potentielles et vrier la valeur de status() la n pour dterminer si ce que nous avons lu tait valide. QDataStream gre plusieurs types de donnes C++ et Qt. La liste complte est disponible ladresse http://doc.trolltech.com/4.1/datastreamformat.html. Nous pouvons galement ajouter la prise en charge de nos propres types personnaliss en surchargeant les oprateurs << et >>. Voici la dnition dun type de donnes personnalis susceptible dtre utilis avec QDataStream:
class Painting { public: Painting() { myYear = 0; } Painting(const QString &title, const QString &artist, int year) { myTitle = title; myArtist = artist; myYear = year; } void setTitle(const QString &title) { myTitle = title; } QString title() const { return myTitle; } ... private: QString myTitle; QString myArtist; int myYear; }; QDataStream &operator<<(QDataStream &out, const Painting &painting); QDataStream &operator>>(QDataStream &in, Painting &painting); Voici comment nous implmenterions loprateur <<: QDataStream &operator<<(QDataStream &out, const Painting &painting) { out << painting.title() << painting.artist() << quint32(painting.year()); return out; }

Chapitre 12

Entres/Sorties

291

Pour mettre en sortie un Painting, nous mettons simplement deux QString et un quint32. A la n de la fonction, nous retournons le ux. Cest une expression C++ courante qui nous permet dutiliser une chane doprateurs << avec un ux de sortie. Par exemple :
out << painting1 << painting2 << painting3;

Limplmentation de operator>>() est similaire celle de operator<<():


QDataStream &operator>>(QDataStream &in, Painting &painting) { QString title; QString artist; quint32 year; in >> title >> artist >> year; painting = Painting(title, artist, year); return in; }

Il existe plusieurs avantages fournir des oprateurs de ux pour les types de donnes personnaliss. Lun deux est que nous pouvons ainsi transmettre des conteneurs qui utilisent le type personnalis. Par exemple :
QList<Painting> paintings = ...; out << paintings;

Nous pouvons lire les conteneurs tout aussi facilement :


QList<Painting> paintings; in >> paintings;

Ceci provoquerait une erreur de compilateur si Painting ne supportait pas << ou >>. Un autre avantage des oprateurs de ux pour les types personnaliss est que nous pouvons stocker les valeurs de ces types en tant que QVariant, ce qui les rend plus largement utilisables, par exemple par les QSetting. Ceci ne fonctionne que si nous enregistrons pralablement le type en excutant qRegisterMetaTypeStreamOperators<T>(), comme expliqu dans le Chapitre 11. Lorsque nous utilisons QDataStream, Qt se charge de lire et dcrire chaque type, dont les conteneurs avec un nombre arbitraire dlments. Cette caractristique nous vite de structurer ce que nous crivons et dappliquer une conversion ce que nous lisons. Notre seule obligation consiste nous assurer que nous lisons tous les types dans leur ordre dcriture, en laissant Qt le soin de grer tous les dtails. QDataStream est utile la fois pour nos formats de chiers dapplication personnaliss et pour les formats binaires standard. Nous pouvons lire et crire des formats binaires standard en utilisant les oprateurs de ux sur les types de base (tels que quint16 ou float) ou au moyen de readRawBytes() et de writeRawBytes(). Si le QDataStream est purement utilis pour lire et crire des types de donnes C++ de base, il est inutile dappeler setVersion().

292

Qt4 et C++ : Programmation dinterfaces GUI

Jusqu prsent, nous avons charg et enregistr les donnes avec la version code en dur du ux sous la forme QDataStream::Qt_4_1. Cette approche est simple et sre, mais elle prsente un lger inconvnient : nous ne pouvons pas tirer parti des formats nouveaux ou mis jour. Par exemple, si une version ultrieure de Qt ajoutait un nouvel attribut QFont (en plus de sa famille, de sa taille, etc.) et que nous ayons cod en dur le numro de version en Qt_4_1, cet attribut ne serait pas enregistr ou charg. Deux solutions soffrent vous. La premire approche consiste intgrer le numro de version QDataStream dans le chier :
QDataStream out(&file); out << quint32(MagicNumber) << quint16(out.version());

(MagicNumber est une constante qui identie de faon unique le type de chier.) Avec cette approche, nous crivons toujours les donnes en utilisant la version la plus rcente de QDataStream. Lorsque nous en venons lire le chier, nous lisons la version du ux :
quint32 magic; quint16 streamVersion; QDataStream in(&file); in >> magic >> streamVersion; if (magic!= MagicNumber) { cerr << "File is not recognized by this application" << endl; } else if (streamVersion > in.version()) { cerr << "File is from a more recent version of the application" << endl; return false; } in.setVersion(streamVersion);

Nous pouvons lire les donnes tant que la version du ux est infrieure ou gale la version utilise par lapplication. Dans le cas contraire, nous signalons une erreur. Si le format de chier contient un numro de version personnel, nous pouvons lutiliser pour dduire le numro de version du ux au lieu de le stocker explicitement. Supposons, par exemple, que le format de chier est destin la version 1.3 de notre application. Nous pouvons alors crire les donnes comme suit :
QDataStream out(&file); out.setVersion(QDataStream::Qt_4_1); out << quint32(MagicNumber) << quint16(0x0103);

Lorsque nous les relisons, nous dterminons quelle version de QDataStream utiliser selon le numro de version de lapplication :
QDataStream in(&file); in >> magic >> appVersion;

Chapitre 12

Entres/Sorties

293

if (magic!= MagicNumber) { cerr << "File is not recognized by this application" << endl; return false; } else if (appVersion > 0x0103) { cerr << "File is from a more recent version of the application" << endl; return false; } if (appVersion < 0x0103) { in.setVersion(QDataStream::Qt_3_0); } else { in.setVersion(QDataStream::Qt_4_1); }

Dans cet exemple, nous spcions que tout chier enregistr avec une version antrieure la version 1.3 de lapplication utilise la version de ux de donnes 4 (Qt_3_0) et que les chiers enregistrs avec la version 1.3 de lapplication utilisent la version de ux de donnes 7 (Qt_4_1). En rsum, il existe trois stratgies pour grer les versions de QDataStream: coder en dur le numro de version, crire et lire explicitement le numro de version et utiliser diffrents numros de version cods en dur en fonction de la version de lapplication. Toutes peuvent tre employes an dassurer que les donnes crites par une ancienne version dune application peuvent tre lues par une nouvelle version. Une fois cette stratgie de gestion des versions de QDataStream choisie, la lecture et lcriture de donnes binaires au moyen de Qt est la fois simple et able. Si nous souhaitons lire ou crire un chier en une seule fois, nous pouvons viter lutilisation de QDataStream et recourir la place aux fonctions write() et readAll() de QIODevice. Par exemple :
bool copyFile(const QString &source, const QString &dest) { QFile sourceFile(source); if (!sourceFile.open(QIODevice::ReadOnly)) return false; QFile destFile(dest); if (!destFile.open(QIODevice::WriteOnly)) return false; destFile.write(sourceFile.readAll()); return sourceFile.error() == QFile::NoError && destFile.error() == QFile::NoError; }

Sur la ligne de lappel de readAll(), le contenu entier du chier dentre est lu et plac dans un QByteArray. Il est alors transmis la fonction write() pour tre crit dans le chier de sortie. Le fait davoir toutes les donnes dans un QByteArray ncessite plus de mmoire que

294

Qt4 et C++ : Programmation dinterfaces GUI

de les lire lment par lment, mais offre quelques avantages. Nous pouvons excuter, par exemple, qCompress() et qUncompress() pour les compresser et les dcompresser. Il existe dautres scnarios o il est plus appropri daccder directement QIODevice que dutiliser QDataStream. QIODevice fournit une fonction peek() qui retourne les octets de donne suivants sans changer lemplacement du priphrique ainsi quune fonction ungetChar() qui permet de revenir un octet en arrire. Ceci fonctionne la fois pour les priphriques daccs alatoire (tels que les chiers) et pour les priphriques squentiels (tels que les sockets rseau). Il existe galement une fonction seek() qui dnit la position des priphriques supportant laccs alatoire. Les formats de chier binaires offrent les moyens les plus souples et les plus compacts de stockage de donnes. QDataStream facilite laccs aux donnes binaires. En plus des exemples de cette section, nous avons dj tudi lutilisation de QDataStream au Chapitre 4 pour lire et crire des chiers de tableur, et nous lutiliserons de nouveau dans le Chapitre 19 pour lire et crire des chiers de curseur Windows.

Lire et crire du texte


Les formats de chiers binaires sont gnralement plus compacts que ceux bass sur le texte, mais ils ne sont pas lisibles ou modiables par lhomme. Si cela reprsente un problme, il est possible dutiliser la place les formats texte. Qt fournit la classe QTextStream pour lire et crire des chiers de texte brut et pour des chiers utilisant dautres formats texte, tels que HTML, XML et du code source. La gestion des chiers XML est traite dans le Chapitre 15.

QTextStream se charge de la conversion entre Unicode et le codage local du systme ou tout autre codage, et gre de faon transparente les conventions de n de ligne utilises par les diffrents systmes dexploitation ("\r\n" sur Windows, "n" sur Unix et Mac OS X). QTextStream utilise le type QChar 16 bits comme unit de donne fondamentale. En plus des caractres et des chanes, QTextStream prend en charge les types numriques de base de C++, quil convertit en chanes. Par exemple, le code suivant crit "Thomas M. Disch : 334" dans le chier sf-book.txt:
QFile file("sf-book.txt"); if (!file.open(QIODevice::WriteOnly)) { cerr << "Cannot open file for writing: " << qPrintable(file.errorString()) << endl; return; } QTextStream out(&file); out << "Thomas M. Disch: " << 334 << endl;

Chapitre 12

Entres/Sorties

295

Lcriture du texte est trs facile, mais sa lecture peut reprsenter un vritable dt, car les donnes textuelles (contrairement aux donnes binaires crites au moyen de QDataStream) sont fondamentalement ambigus. Considrons lexemple suivant :
out << "Norway" << "Sweden";

Si out est un QTextStream, les donnes vritablement crites sont la chane "NorwaySweden". Nous ne pouvons pas vraiment nous attendre ce que le code suivant lise les donnes correctement :
in >> str1 >> str2;

En fait, str1 obtient le mot "NorwaySweden" entier et str2 nobtient rien. Ce problme ne se pose pas avec QDataStream, car la longueur de chaque chane est stocke devant les donnes. Pour les formats de chier complexes, un analyseur risque dtre requis. Un tel analyseur peut fonctionner en lisant les donnes caractre par caractre en utilisant un >> sur un QChar, ou ligne par ligne au moyen de QTextStream::readLine(). A la n de cette section, nous prsentons deux petits exemples. Le premier lit un chier dentre ligne par ligne et lautre le lit caractre par caractre. Pour ce qui est des analyseurs qui traitent le texte entier, nous pouvons lire le chier complet en une seule fois laide de QTextStream::readAll() si nous ne nous proccupons pas de lutilisation de la mmoire, ou si nous savons que le chier est petit. Par dfaut, QTextStream utilise le codage local du systme (par exemple, ISO 8859-1 ou ISO 8859-15 aux Etats-Unis et dans une grande partie de lEurope) pour les oprations de lecture et dcriture. Vous pouvez changer ceci en excutant setCodec() comme suit :
stream.setCodec("UTF-8");

UTF-8 est un codage populaire compatible ASCII capable de reprsenter la totalit du jeu de caractres Unicode. Pour plus dinformations concernant Unicode et la prise en charge des codages de QTextStream, reportez-vous au Chapitre 17 (Internationalisation). QTextStream possde plusieurs options modeles sur celles offertes par <iostream>. Elles peuvent tre dnies en transmettant des objets spciaux, nomms manipulateurs de ux, au ux pour modier son tat. Lexemple suivant dnit les options showbase, uppercasedigits et hex avant la sortie de lentier 12345678, produisant le texte "0xBC614E" :
out << showbase << uppercasedigits << hex << 12345678;

Les options peuvent galement tre dnies en utilisant les fonctions membres :
out.setNumberFlags(QTextStream::ShowBase | QTextStream::UppercaseDigits); out.setIntegerBase(16); out << 12345678;

296

Qt4 et C++ : Programmation dinterfaces GUI

setIntegerBase(int) 0 2 8 10 16 setNumberFlags(NumberFlags) ShowBase ForceSign ForcePoint UppercaseBase UppercaseDigits Afche un prxe si la base est 2 ("0b"), 8 ("0") ou 16 ("0x") Afche toujours le signe des nombres rels Place toujours le sparateur de dcimale dans les nombres Utilise les versions majuscules des prxes de base ("0X", "0B") Utilise des lettres majuscules dans les nombres hexadcimaux Dtection automatique base sur le prxe (lors de la lecture) Binaire Octal Dcimal Hexadcimal

setRealNumberNotation(RealNumberNotation) FixedNotation ScientificNotation SmartNotation setRealNumberPrecision(int) Dnit le nombre maximum de chiffres devant tre gnrs (6 par dfaut) setFieldWidth(int) Dnit la taille minimum dun champ (0 par dfaut) setFieldAlignment(FieldAlignment) AlignLeft AlignRight AlignCenter AlignAccountingStyle setPadChar(QChar) Dni le caractre utiliser pour lalignement (espace par dfaut) Figure 12.1 Fonctions destines dnir les options de QTextStream Force un alignement sur le ct gauche du champ Force un alignement sur le ct droit du champ Force un alignement sur les deux cts du champ Force un alignement entre le signe et le nombre Notation point xe (par exemple, "0.000123") Notation scientique (par exemple, "1.234568e-04") Notation la plus compacte entre point xe ou scientique

Chapitre 12

Entres/Sorties

297

Comme QDataStream, QTextStream agit sur une sous-classe de QIODevice, qui peut tre un QFile, un QTemporaryFile, un QBuffer, un QProcess, un QTcpSocket ou un QUdpSocket. En outre, il peut tre utilis directement sur un QString. Par exemple :
QString str; QTextStream(&str) << oct << 31 << " " << dec << 25 << endl;

Le contenu de str est donc "37 25/n", car le nombre dcimal 31 est exprim sous la forme 37 en base huit. Dans ce cas, il nest pas ncessaire de dnir un codage sur le ux, car QString est toujours Unicode. Examinons un exemple simple de format de chier bas sur du texte. Dans lapplication Spreadsheet dcrite dans la Partie 1, nous avons utilis un format binaire pour stocker les donnes. Ces donnes consistaient en une squence de triplets (ligne, colonne, formule), une pour chaque cellule non vide. Il nest pas difcile dcrire les donnes sous forme de texte. Voici un extrait dune version rvise de Spreadsheet::writeFile().
QTextStream out(&file); for (int row = 0; row < RowCount; ++row) { for (int column = 0; column < ColumnCount; ++column) { QString str = formula(row, column); if (!str.isEmpty()) out << row << " " << column << " " << str << endl; } }

Nous avons utilis un format simple, chaque ligne reprsentant une cellule avec des espaces entre la ligne et la colonne ainsi quentre la colonne et la formule. La formule contient des espaces, mais nous pouvons supposer quelle ne comporte aucun /n (qui insre un retour la ligne). Examinons maintenant le code de lecture correspondant :
QTextStream in(&file); while (!in.atEnd()) { QString line = in.readLine(); QStringList fields = line.split( ); if (fields.size() >= 3) { int row = fields.takeFirst().toInt(); int column = fields.takeFirst().toInt(); setFormula(row, column, fields.join( )); } }

Nous lisons les donnes de Spreadsheet ligne par ligne. La fonction readLine() supprime le /n de n. QString::split() retourne une liste de chanes de caractres qui est scinde lemplacement dapparition du sparateur qui lui est fournit. Par exemple, la ligne "5 19 Total Value" rsulte en une liste de quatre lments ["5", "19", "Total", "Value"]. Si nous disposons au moins de trois champs, nous sommes prts extraire les donnes. La fonction QStringList::takeFirst() supprime le premier lment dune liste et retourne llment supprim. Nous lutilisons pour extraire les nombres de ligne et de colonne.

298

Qt4 et C++ : Programmation dinterfaces GUI

Nous ne ralisons aucune vrication derreur. Si nous lisons une valeur de ligne ou de colonne non entire, QString::toInt() retourne 0. Lorsque nous appelons setFormula(), nous devons concatner les champs restants en une seule chane. Dans notre deuxime exemple de QTextStream, nous utilisons une approche caractre par caractre pour implmenter un programme qui lit un chier texte et met en sortie le mme texte avec les espaces de n supprims et toutes les tabulations remplaces par des espaces. Le programme accomplit cette tche dans la fonction tidyFile():
void tidyFile(QIODevice *inDevice, QIODevice *outDevice) { QTextStream in(inDevice); QTextStream out(outDevice); const int TabSize = 8; int endlCount = 0; int spaceCount = 0; int column = 0; QChar ch; while (!in.atEnd()) { in >> ch; if (ch == \n) { ++endlCount; spaceCount = 0; column = 0; } else if (ch == \t) { int size = TabSize - (column % TabSize); spaceCount += size; column += size; } else if (ch == ) { ++spaceCount; ++column; } else { while (endlCount > 0) { out << endl; --endlCount; column = 0; } while (spaceCount > 0) { out << ; --spaceCount; ++column; } out << ch; ++column; } } out << endl; }

Chapitre 12

Entres/Sorties

299

Nous crons un QTextStream dentre et de sortie bas sur les QIODevice transmis la fonction. Nous conservons trois lments dtat : un dcompte des nouvelles lignes, un dcompte des espaces et lemplacement de colonne courant dans la ligne en cours (pour convertir les tabulations en un nombre despaces correct). Lanalyse est faite dans une boucle while qui parcourt chaque caractre du chier dentre. Le code prsente quelques subtilits certains endroits. Par exemple, bien que nous dnissions tabSize en 8, nous remplaons les tabulations par le nombre despaces qui permet datteindre le taquet de tabulation suivant, au lieu de remplacer grossirement chaque tabulation par huit espaces. Dans le cas dune nouvelle ligne, dune nouvelle tabulation ou dun nouvel espace, nous mettons simplement jour les donnes dtat. Pour les autres types de caractres, nous produisons une sortie. Avant dcrire le caractre nous introduisons tous les espaces et les nouvelles lignes ncessaires (pour respecter les lignes vierges et prserver le retrait) et mettons jour ltat.
int main() { QFile inFile; QFile outFile; inFile.open(stdin, QFile::ReadOnly); outFile.open(stdout, QFile::WriteOnly); tidyFile(&inFile, &outFile); return 0; }

Pour cet exemple, nous navons pas besoin dobjet QApplication, car nous nutilisons que les classes doutils de Qt. Vous trouverez la liste de toutes les classes doutils ladresse http://doc.trolltech.com/4.1/tools.html. Nous avons suppos que le programme est utilis en tant que ltre, par exemple :
tidy < cool.cpp > cooler.cpp

Il serait facile de le dvelopper an de grer les noms de chier qui seraient transmis sur la ligne de commande ainsi que pour ltrer cin en cout. Comme il sagit dune application de console, le chier .pro diffre lgrement de ceux que nous avons rencontrs pour les applications GUI :
TEMPLATE QT CONFIG CONFIG SOURCES = = += -= = app core console app_bundle tidy.cpp

Nous ntablissons de liaison quavec QtCore, car nous nutilisons aucune fonctionnalit GUI. Puis nous spcions que nous souhaitons activer la sortie de la console sur Windows et que nous ne voulons pas que lapplication soit hberge dans un package sur Mac OS X.

300

Qt4 et C++ : Programmation dinterfaces GUI

Pour lire et crire des chiers ASCII ou ISO 8859-1 (Latin-1) bruts, il est possible dutiliser directement lAPI de QIODevice au lieu de QTextStream. Mais cette mthode est rarement conseille dans la mesure o la plupart des applications doivent prendre en charge dautres codages un stade ou un autre, et o seul QTextStream offre une prise en charge parfaite de ces codages. Si vous souhaitez nanmoins crire le texte directement dans un QIODevice, vous devez spcier explicitement la balise QIODevice::Text dans la fonction open(). Par exemple :
file.open(QIODevice::WriteOnly | QIODevice::Text);

Lors de lcriture, cette balise indique QIODevice de convertir les caractres \n en des squences "\r\n" sur Windows. Lors de la lecture, elle indique au priphrique dignorer les caractres r sur toutes les plates-formes. Nous pouvons alors supposer que la n de chaque ligne est marque par un caractre de nouvelle ligne n quelle que soit la convention de n de ligne utilise par le systme dexploitation.

Parcourir les rpertoires


La classe QDir fournit un moyen indpendant de la plate-forme de parcourir les rpertoires et de rcuprer des informations concernant les chiers. Pour dterminer comment QDir est utilise, nous allons crire une petite application de console qui calcule lespace occup par toutes les images dun rpertoire particulier et de tous ses sous-rpertoires, quelle que soit leur profondeur. Le cur de lapplication est la fonction imageSpace(), qui calcule rcursivement la taille cumule des images dun rpertoire donn :
qlonglong imageSpace(const QString &path) { QDir dir(path); qlonglong size = 0; QStringList filters; foreach (QByteArray format, QImageReader::supportedImageFormats()) filters += "*." + format; foreach (QString file, dir.entryList(filters, QDir::Files)) size += QFileInfo(dir, file).size(); foreach (QString subDir, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) size += imageSpace(path + QDir::separator() + subDir); return size; }

Chapitre 12

Entres/Sorties

301

Nous commenons par crer un objet QDir en utilisant un chemin donn, qui peut tre relatif au rpertoire courant ou absolu. Nous transmettons deux arguments la fonction entryList(). Le premier est une liste de ltres de noms de chiers. Ils peuvent contenir les caractres gnriques * et ?. Dans cet exemple, nous ralisons un ltrage de faon ninclure que les formats de chiers susceptibles dtre lus par QImage. Le second argument spcie le type dentre souhait (chiers normaux, rpertoires, etc.). Nous parcourons la liste de chiers, en additionnant leurs tailles. La classe QFileInfo nous permet daccder aux attributs dun chier, tels que la taille, les autorisations, le propritaire et lhorodateur de ce chier. Le deuxime appel de entryList() rcupre tous les sous-rpertoires de ce rpertoire. Nous les parcourons et nous appelons imageSpace() rcursivement pour tablir la taille cumule de leurs images. Pour crer le chemin de chaque sous-rpertoire, nous combinons de chemin du rpertoire en cours avec le nom du sous-rpertoire, en les sparant par un slash (/).

QDir traite / comme un sparateur de rpertoires sur toutes les plates-formes. Pour prsenter les chemins lutilisateur, nous pouvons appeler la fonction statique QDir::convertSeparators() qui convertit les slash en sparateurs spciques la plate-forme. Ajoutons une fonction main() notre petit programme :
int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QStringList args = app.arguments(); QString path = QDir::currentPath(); if (args.count() > 1) path = args[1]; cout << "Space used by images in " << qPrintable(path) << " and its subdirectories is " << (imageSpace(path) / 1024) << " KB" << endl; return 0; }

Nous utilisons QDir::currentPath() pour initialiser le chemin vers le rpertoire courant. Nous aurions aussi pu faire appel QDir::homePath() pour linitialiser avec le rpertoire de base de lutilisateur. Si ce dernier a spci un chemin sur la ligne de commande, nous lutilisons la place. Nous appelons enn notre fonction imageSpace() pour calculer lespace occup par les images. La classe QDir fournit dautres fonctions lies aux rpertoires et aux chiers, dont entryInfoList() (qui retourne une liste dobjets QFileInfo), rename(), exists(), mkdir() et rmdir(). La classe QFile fournit des fonctions utilitaires statiques, dont remove() et exists().

302

Qt4 et C++ : Programmation dinterfaces GUI

Intgration des ressources


Jusqu prsent, nous avons tudi laccs aux donnes dans des priphriques externes, mais avec Qt, il est galement possible dintgrer du texte ou des donnes binaires dans lexcutable de lapplication. Pour ce faire, il convient dutiliser le systme de ressources de Qt. Dans les autres chapitres, nous avons utilis les chiers de ressources pour intgrer des images dans lexcutable, mais il est possible dintgrer tout type de chier. Les chiers intgrs peuvent tre lus au moyen de QFile, exactement comme les chiers normaux dun systme de chiers. Les ressources sont converties en code C++ par rcc, le compilateur de ressources de Qt. Nous pouvons demander qmake dinclure des rgles spciales dexcution de rcc en ajoutant cette ligne au chier .pro:
RESOURCES = myresourcefile.qrc

myresourcefile.qrc est un chier XML qui rpertorie les chiers intgrer dans lexcutable.
Imaginons que nous crivions une application destine rpertorier des coordonnes. Pour des raisons pratiques, nous souhaitons intgrer les indicatifs tlphoniques internationaux dans lexcutable. Si le chier se situe dans le rpertoire datafiles de lapplication, le chier de ressources serait le suivant :
<!DOCTYPE RCC><RCC version="1.0"> <qresource> <file>datafiles/phone-codes.dat</file> </qresource> </RCC>

Depuis lapplication, les ressources sont identies par le prxe de chemin :/. Dans cet exemple, le chemin du chier contenant les indicatifs tlphoniques est :/datafiles/phonecodes.dat et peut tre lu comme tout autre chier en utilisant QFile. Lintgration de donnes dans lexcutable prsente plusieurs avantages : les donnes ne peuvent tre perdues et cette opration permet la cration dexcutables vritablement autonomes (si une liaison statique est galement utilise). Les inconvnients sont les suivants : si les donnes intgres doivent tre changes, il est impratif de remplacer lexcutable entier, et la taille de ce dernier sera plus importante car il doit sadapter aux donnes intgres. Le systme de ressources de Qt fournit des fonctionnalits supplmentaires, telles que la prise en charge des alias de noms de chiers et la localisation. Ces fonctionnalits sont documentes ladresse http://doc.trolltech.com/4.1/resources.html.

Chapitre 12

Entres/Sorties

303

Communication inter-processus
La classe QProcess nous permet dexcuter des programmes externes et dinteragir avec ceux-ci. La classe fonctionne de faon asynchrone, effectuant sa tche larrire-plan de sorte que linterface utilisateur reste ractive. QProcess met des signaux pour nous indiquer quand le processus externe dtient des donnes ou a termin. Nous allons rviser le code dune petite application qui fournit une interface utilisateur pour un programme externe de conversion des images. Pour cet exemple, nous nous reposons sur le programme ImageMagick convert, qui est disponible gratuitement sur toutes les platesformes principales. (Voir Figure 12.2)
Figure 12.2 Lapplication Image Converter

Linterface utilisateur a t cre dans Qt Designer. Le chier .ui se trouve sur le site web de Pearson, www.pearson.fr, la page ddie ce livre. Ici, nous allons nous concentrer sur la sous-classe qui hrite de la classe Ui::ConvertDialog gnre par le compilateur . d'interface utilisateur. Commenons par len-tte :
#ifndef CONVERTDIALOG_H #define CONVERTDIALOG_H #include <QDialog> #include <QProcess> #include "ui_convertdialog.h" class ConvertDialog: public QDialog, public Ui::ConvertDialog { Q_OBJECT public: ConvertDialog(QWidget *parent = 0);

304

Qt4 et C++ : Programmation dinterfaces GUI

private void void void void void

slots: on_browseButton_clicked(); on_convertButton_clicked(); updateOutputTextEdit(); processFinished(int exitCode, QProcess::ExitStatus exitStatus); processError(QProcess::ProcessError error);

private: QProcess process; QString targetFile; }; #endif

Len-tte est conforme celui dune sous-classe de formulaire Qt Designer. Grce au mcanisme de connexion automatique de Qt Designer, les slots on_browseButton_clicked() et on_convertButton_clicked() sont automatiquement connects aux signaux clicked() des boutons Browse et Convert.
ConvertDialog::ConvertDialog(QWidget *parent) : QDialog(parent) { setupUi(this); connect(&process, SIGNAL(readyReadStandardError()), this, SLOT(updateOutputTextEdit())); connect(&process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(processFinished(int, QProcess::ExitStatus))); connect(&process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(processError(QProcess::ProcessError))); }

Lappel de setupUi() cre et positionne tous les widgets du formulaire, tablit les connexions signal/slot pour les slots on_objectName_signalName() et connecte le bouton Quit QDialog::accept(). Nous connectons ensuite manuellement trois signaux de lobjet QProcess trois slots privs. A chaque fois que le processus externe va dtecter des donnes sur son cerr, il les grera avec updateOutputTextEdit().
void ConvertDialog::on_browseButton_clicked() { QString initialName = sourceFileEdit->text(); if (initialName.isEmpty()) initialName = QDir::homePath(); QString fileName = QFileDialog::getOpenFileName(this, tr("Choose File"), initialName); fileName = QDir::convertSeparators(fileName); if (!fileName.isEmpty()) { sourceFileEdit->setText(fileName); convertButton->setEnabled(true); } }

Chapitre 12

Entres/Sorties

305

Le signal clicked() du bouton Browse est automatiquement connect au slot on_browseButton_clicked() par setupUi(). Si lutilisateur a pralablement slectionn un chier, nous initialisons la bote de dialogue avec le nom de ce chier. Sinon, nous prenons le rpertoire de base de lutilisateur.
void ConvertDialog::on_convertButton_clicked() { QString sourceFile = sourceFileEdit->text(); targetFile = QFileInfo(sourceFile).path() + QDir::separator() + QFileInfo(sourceFile).baseName() + "." + targetFormatComboBox->currentText().toLower(); convertButton->setEnabled(false); outputTextEdit->clear(); QStringList args; if (enhanceCheckBox->isChecked()) args << "-enhance"; if (monochromeCheckBox->isChecked()) args << "-monochrome"; args << sourceFile << targetFile; process.start("convert", args); }

Lorsque lutilisateur clique sur le bouton Convert, nous copions le nom du chier source et changeons lextension an quelle corresponde au format du chier cible. Nous utilisons le sparateur de rpertoires spcique la plate-forme (/ ou \, disponible sous la forme QDir::separator()) au lieu de coder les slash en dur, car le nom du chier sera visible pour lutilisateur. Nous dsactivons alors le bouton Convert pour viter que lutilisateur ne lance accidentellement plusieurs conversions, et nous effaons lditeur de texte qui nous permet dafcher les informations dtat. Pour lancer le processus externe, nous appelons QProcess::start() avec le nom du programme excuter (convert) et tous les arguments requis. Dans ce cas, nous transmettons les balises enhance et monochrome si lutilisateur a coch les options appropries, suivies des noms des chiers source et cible. Le programme convert dduit la conversion requise partir des extensions de chier.
void ConvertDialog::updateOutputTextEdit() { QByteArray newData = process.readAllStandardError(); QString text = outputTextEdit->toPlainText() + QString::fromLocal8Bit(newData); outputTextEdit->setPlainText(text); }

306

Qt4 et C++ : Programmation dinterfaces GUI

Lorsque le processus externe effectue une opration dcriture dans le cerr, le slot updateOutputTextEdit() est appel. Nous lisons le texte derreur et lajoutons au texte existant de QTextEdit.
void ConvertDialog::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { if (exitStatus == QProcess::CrashExit) { outputTextEdit->append(tr("Conversion program crashed")); } else if (exitCode!= 0) { outputTextEdit->append(tr("Conversion failed")); } else { outputTextEdit->append(tr("File %1 created").arg(targetFile)); } convertButton->setEnabled(true); }

Une fois le processus termin, nous faisons connatre le rsultat lutilisateur et activons le bouton Convert.
void ConvertDialog::processError(QProcess::ProcessError error) { if (error == QProcess::FailedToStart) { outputTextEdit->append(tr("Conversion program not found")); convertButton->setEnabled(true); } }

Si le processus ne peut tre lanc, QProcess met error() au lieu de finished(). Nous rapportons toute erreur et activons le bouton Click. Dans cet exemple, nous avons effectu les conversions de chier de faon asynchrone nous avons demand QProcess dexcuter le programme convert et de rendre immdiatement le contrle lapplication. Cette mthode laisse linterface utilisateur ractive puisque le processus sexcute larrire-plan. Mais dans certains cas, il est ncessaire que le processus externe soit termin avant de pouvoir poursuivre avec notre application. Dans de telles situations, QProcess doit agir de faon synchrone. Les applications qui prennent en charge ldition de texte brut dans lditeur de texte prfr de lutilisateur sont typiques de celles dont le comportement doit tre synchrone. Limplmentation sobtient facilement en utilisant QProcess. Supposons, par exemple, que le texte brut se trouve dans un QTextEdit, et que vous fournissiez un bouton Edit sur lequel lutilisateur peut cliquer, connect un slot edit().
void ExternalEditor::edit() { QTemporaryFile outFile; if (!outFile.open()) return; QString fileName = outFile.fileName();

Chapitre 12

Entres/Sorties

307

QTextStream out(&outFile); out << textEdit->toPlainText(); outFile.close(); QProcess::execute(editor, QStringList() << options << fileName); QFile inFile(fileName); if (!inFile.open(QIODevice::ReadOnly)) return; QTextStream in(&inFile); textEdit->setPlainText(in.readAll()); }

Nous utilisons QTemporaryFile pour crer un chier vide avec un nom unique. Nous ne spcions aucun argument pour QTemporaryFile::open() puisquelle est judicieusement dnie pour ouvrir en mode criture/lecture par dfaut. Nous crivons le contenu de lditeur de texte dans le chier temporaire, puis nous fermons ce dernier car certains diteurs de texte ne peuvent travailler avec des chiers dj ouverts. La fonction statique QProcess::execute() excute un processus externe et provoque un blocage jusqu ce que le processus soit termin. Largument editor est un QString contenant le nom dun excutable diteur ("gvim", par exemple). Largument options est un QStringList (contenant un lment, "-f", si nous utilisons gvim). Une fois que lutilisateur a ferm lditeur de texte, le processus se termine ainsi que lappel de execute(). Nous ouvrons alors le chier temporaire et lisons son contenu dans le QTextEdit. QTemporaryFile supprime automatiquement le chier temporaire lorsque lobjet sort de la porte. Les connexions signal/slot ne sont pas ncessaires lorsque QProcess est utilis de faon synchrone. Si un contrle plus n que celui fourni par la fonction statique execute() est requis, nous pouvons utiliser une autre approche qui implique de crer un objet QProcess et dappeler start() sur celui-ci, puis de forcer un blocage en appelant QProcess::waitForStarted(), et en cas de succs, en appelant QProcess::waitForFinished(). Reportez-vous la documentation de rfrence de QProcess pour trouver un exemple utilisant cette approche. Dans cette section, nous avons exploit QProcess pour obtenir laccs une fonctionnalit prexistante. Lutilisation dapplications dj existantes reprsente un gain de temps et vous vite davoir rsoudre des problmes trs loigns de lobjectif principal de votre application. Une autre faon daccder une fonctionnalit prexistante consiste tablir une liaison vers une bibliothque qui la fournit. Si une telle bibliothque nexiste pas, vous pouvez aussi envisager dencapsuler votre application de console dans un QProcess. QProcess peut galement servir lancer dautres applications GUI, telles quun navigateur Web ou un client email. Si, cependant, votre objectif est la communication entre applications et non la simple excution de lune partir de lautre, il serait prfrable de les faire communiquer directement en utilisant les classes de gestion de rseau de Qt ou lextension ActiveQt sur Windows.

13
Les bases de donnes
Au sommaire de ce chapitre Connexion et excution de requtes Prsenter les donnes sous une forme tabulaire Implmenter des formulaires matre/dtail

Le module QtSql fournit une interface indpendante de la plate-forme pour accder aux bases de donnes. Cette interface est prise en charge par un ensemble de classes qui utilisent larchitecture modle/vue de Qt pour intgrer la base de donnes linterface utilisateur. Ce chapitre suppose une certaine familiarit avec les classes modle/vue de Qt, traites dans le Chapitre 10.

310

Qt4 et C++ : Programmation dinterfaces GUI

Une connexion de base de donnes est reprsente par un objet QSqlDatabase. Qt utilise des pilotes pour communiquer avec les diverses API de base de donnes. Qt Desktop Edition inclut les pilotes suivants :
Pilote QDB2 QIBASE QMYSQL QOCI QODBC QPSQL QSQLITE QSQLITE2 QTDS Base de donnes IBM DB2 version 7.1 et ultrieure Borland InterBase MySQL Oracle (Oracle Call Interface) ODBC (inclut Microsoft SQL Server) PostgreSQL versions 6.x et 7.x SQLite version 3 et ultrieure SQLite version 2 Sybase Adaptive Server

Tous les pilotes ne sont pas fournis avec Qt Open Source Edition, en raison de restrictions de licence. Lors de la conguration de Qt, nous pouvons choisir entre inclure directement les pilotes SQL Qt et les crer en tant que plugin. Qt est fourni avec la base de donnes SQLite, une base de donnes in-process (qui sintgre aux applications) de domaine public. Pour les utilisateurs familiariss avec la syntaxe SQL, la classe QSqlQuery reprsente un bon moyen dexcuter directement des instructions SQL arbitraires et de grer leurs rsultats. Pour les utilisateurs qui prfrent une interface de base de donnes plus volue masquant la syntaxe SQL, QSqlTableModel et QSqlRelationalTableModel fournissent des abstractions acceptables. Ces classes reprsentent une table SQL de la mme faon que les autres classes modle de Qt (traites dans le Chapitre 10).Elles peuvent tre utilises de faon autonome pour parcourir et modier des donnes dans le code, ou encore tre associes des vues par le biais desquelles les utilisateurs naux peuvent afcher et modier les donnes.

Connexion et excution de requtes


Pour excuter des requtes SQL, nous devons tout dabord tablir une connexion avec une base de donnes. En rgle gnrale, les connexions aux bases de donnes sont dnies dans une fonction spare que nous appelons au lancement de lapplication. Par exemple :
bool createConnection() { QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL");

Chapitre 13

Les bases de donnes

311

db.setHostName("mozart.konkordia.edu"); db.setDatabaseName("musicdb"); db.setUserName("gbatstone"); db.setPassword("T17aV44"); if (!db.open()) { QMessageBox::critical(0, QObject::tr("Database Error"), db.lastError().text()); return false; } return true; }

Dans un premier temps, nous appelons QSqlDatabase::addDatabase() pour crer un objet QSqlDatabase. Le premier argument de addDatabase() spcie quel pilote doit tre utilis par Qt pour accder la base de donnes. Dans ce cas, nous utilisons MySQL. Nous dnissons ensuite le nom de lhte de la base de donnes, le nom de cette base de donnes, le nom dutilisateur et le mot de passe, puis nous ouvrons la connexion. Si open() choue, nous afchons un message derreur. Nous appelons gnralement createConnection() dans main():
int main(int argc, char *argv[]) { QApplication app(argc, argv); if (!createConnection()) return 1; return app.exec(); }

Une fois quune connexion est tablie, nous pouvons utiliser QSqlQuery pour excuter toute instruction SQL supporte par la base de donnes concerne. Par exemple, voici comment excuter une instruction SELECT:
QSqlQuery query; query.exec("SELECT title, year FROM cd WHERE year >= 1998");

Aprs lappel dexec(), nous pouvons parcourir lensemble de rsultats de la requte :


while (query.next()) { QString title = query.value(0).toString(); int year = query.value(1).toInt(); cerr << qPrintable(title) << ": " << year << endl; }

Au premier appel de next(), QSqlQuery est positionn sur le premier enregistrement de lensemble de rsultats. Les appels ultrieurs next() permettent davancer le pointeur vers les enregistrements successifs, jusqu ce que la n soit atteinte. A ce stade, next() retourne false. Si lensemble de rsultats est vide (ou si la requte a chou), le premier appel next() retourne false.

312

Qt4 et C++ : Programmation dinterfaces GUI

La fonction value() retourne la valeur dun champ en tant que QVariant. Les champs sont numrots en partant de 0 dans lordre fourni dans linstruction SELECT. La classe QVariant peut contenir de nombreux types Qt et C++, dont int et QString. Les diffrents types de donnes susceptibles dtre stocks dans une base de donnes sont transforms en types Qt et C++ correspondants et stocks en tant que QVariant. Par exemple, un VARCHAR est reprsent en tant que QString et un DATETIME en tant que QDateTime. QSqlQuery fournit quelques autres fonctions destines parcourir lensemble de rsultats : first(), last(), previous() et seek(). Ces fonctions sont pratiques, mais pour certaines bases de donnes elles peuvent savrer plus lentes et plus gourmandes en mmoire que next(). Pour optimiser facilement dans le cas de jeux de donnes de taille importante, nous pouvons appeler QSqlQuery::setForwardOnly(true) avant dappeler exec(), puis seulement utiliser next() pour parcourir lensemble de rsultats. Nous avons prcdemment prsent la requte SQL comme un argument de QSqlQuery::exec(), mais il est galement possible de la transmettre directement au constructeur, qui lexcute immdiatement :
QSqlQuery query("SELECT title, year FROM cd WHERE year >= 1998");

Nous pouvons contrler lexistence dune erreur en appelant isActive() sur la requte :
if (!query.isActive()) QMessageBox::warning(this, tr("Database Error"), query.lastError().text());

Si aucune erreur napparat, la requte devient "active" et nous pouvons utiliser next() pour parcourir lensemble de rsultats. Il est presque aussi facile de raliser un INSERT que deffectuer un SELECT:
QSqlQuery query("INSERT INTO cd (id, artistid, title, year) " "VALUES (203, 102, Living in America, 2002)");

Aprs cette opration, numRowsAffected() retourne le nombre de lignes affectes par linstruction SQL (ou -1 en cas derreur). Si nous devons insrer de nombreux enregistrements, ou si nous souhaitons viter la conversion de valeurs en chanes, nous pouvons recourir prepare() pour excuter une requte contenant des emplacements rservs puis lier les valeurs insrer. Qt prend en charge la fois la syntaxe de style ODBC et celle de style Oracle pour les espaces rservs, en utilisant le support natif lorsquil est disponible et en le simulant dans les autres cas. Voici un exemple utilisant la syntaxe de style Oracle avec des espaces rservs nomms :
QSqlQuery query; query.prepare("INSERT INTO cd (id, artistid, title, year) " "VALUES (:id,:artistid,:title,:year)"); query.bindValue(":id", 203); query.bindValue(":artistid", 102); query.bindValue(":title", "Living in America");

Chapitre 13

Les bases de donnes

313

query.bindValue(":year", 2002); query.exec();

Voici le mme exemple utilisant des espaces rservs positionnels de style ODBC :
QSqlQuery query; query.prepare("INSERT INTO cd (id, artistid, title, year) " "VALUES (?,?,?,?)"); query.addBindValue(203); query.addBindValue(102); query.addBindValue("Living in America"); query.addBindValue(2002); query.exec();

Aprs lappel exec(), nous pouvons appeler bindValue() ou addBindValue() pour lier de nouvelles valeurs, puis nous appelons de nouveau exec() pour excuter la requte avec ces nouvelles valeurs. Les espaces rservs sont souvent utiliss pour des donnes binaires contenant des caractres non ASCII ou nappartenant pas au jeu de caractres Latin-1. A larrire-plan, Qt utilise Unicode avec les bases de donnes qui prennent en charge cette norme. Pour les autres, Qt convertit de faon transparente les chanes en codage appropri. Qt supporte les transactions SQL sur les bases de donnes pour lesquelles elles sont disponibles. Pour lancer une transaction, nous appelons transaction() sur lobjet QSqlDatabase qui reprsente la connexion de base de donnes. Pour mettre n la transaction, nous appelons soit commit(), soit rollback(). Voici, par exemple, comment rechercher une cl trangre et excuter une instruction INSERT dans une transaction :
QSqlDatabase::database().transaction(); QSqlQuery query; query.exec("SELECT id FROM artist WHERE name = Gluecifer"); if (query.next()) { int artistId = query.value(0).toInt(); query.exec("INSERT INTO cd (id, artistid, title, year) " "VALUES (201, " + QString::number(artistId) + ", Riding the Tiger, 1997)"); } QSqlDatabase::database().commit();

La fonction QSqlDatabase::database() retourne un objet QSqlDatabase reprsentant la connexion cre dans createConnection(). Si une transaction ne peut tre dmarre, QSqlDatabase::transaction() retourne false. Certaines bases de donnes ne supportent pas les transactions. Dans cette situation, les fonctions transaction(), commit() et rollback() nont aucune action. Nous pouvons dterminer si une base de donnes prend en charge les transactions en excutant hasFeature() sur le QSqlDriver associ cette base :
QSqlDriver *driver = QSqlDatabase::database().driver(); if (driver->hasFeature(QSqlDriver::Transactions))

314

Qt4 et C++ : Programmation dinterfaces GUI

Plusieurs autres fonctionnalits de bases de donnes peuvent tre testes, notamment la prise en charge des BLOB (objets binaires volumineux), dUnicode et des requtes prpares. Dans les exemples fournis jusqu prsent, nous avons suppos que lapplication utilise une seule connexion de base de donnes. Pour crer plusieurs connexions, il est possible de transmettre un nom en tant que second argument addDatabase(). Par exemple :
QSqlDatabase db = QSqlDatabase::addDatabase("QPSQL", "OTHER"); db.setHostName("saturn.mcmanamy.edu"); db.setDatabaseName("starsdb"); db.setUserName("hilbert"); db.setPassword("ixtapa7");

Nous pouvons alors obtenir un pointeur vers lobjet QSqlDatabase en transmettant le nom QSqlDatabase::database():
QSqlDatabase db = QSqlDatabase::database("OTHER");

Pour excuter des requtes en utilisant lautre connexion, nous transmettons lobjet QSqlDatabase au constructeur de QSqlQuery:
QSqlQuery query(db); query.exec("SELECT id FROM artist WHERE name = Mando Diao");

Des connexions multiples peuvent savrer utiles si vous souhaitez effectuer plusieurs transactions la fois, chaque connexion ne pouvant grer quune seule transaction. Lorsque nous utilisons les connexions de base de donnes multiples, nous pouvons toujours avoir une connexion non nomme qui sera exploite par QSqlQuery si aucune connexion nest spcie. En plus de QSqlQuery, Qt fournit la classe QSqlTableModel comme interface de haut niveau, ce qui permet dviter lemploi du code SQL brut pour raliser les oprations SQL les plus courantes (SELECT, INSERT, UPDATE et DELETE). La classe peut tre utilise de faon autonome pour manipuler une base de donnes sans aucune implication GUI. Elle peut galement tre utilise comme source de donnes pour QListView ou QTableView. Voici un exemple qui utilise QSqlTableModel pour raliser un SELECT:
QSqlTableModel model; model.setTable("cd"); model.setFilter("year >= 1998"); model.select();

Ce qui est quivalent la requte


SELECT * FROM cd WHERE year >= 1998

Pour parcourir un ensemble de rsultats, nous rcuprons un enregistrement donn au moyen de QSqlTableModel::record() et nous accdons aux champs individuels laide de value():
for (int i = 0; i < model.rowCount(); ++i) {

Chapitre 13

Les bases de donnes

315

QSqlRecord record = model.record(i); QString title = record.value("title").toString(); int year = record.value("year").toInt(); cerr << qPrintable(title) << ": " << year << endl; }

La fonction QSqlRecord::value() reoit soit un nom, soit un index de champ. En travaillant sur des jeux de donnes de taille importante, il est prfrable de dsigner les champs par leurs index. Par exemple :
int titleIndex = model.record().indexOf("title"); int yearIndex = model.record().indexOf("year"); for (int i = 0; i < model.rowCount(); ++i) { QSqlRecord record = model.record(i); QString title = record.value(titleIndex).toString(); int year = record.value(yearIndex).toInt(); cerr << qPrintable(title) << ": " << year << endl; }

Pour insrer un enregistrement dans une table de base de donnes, nous utilisons la mme approche que pour une insertion dans tout modle bidimensionnel : en premier lieu, nous appelons insertRow() pour crer une nouvelle ligne (enregistrement) vide, puis nous faisons appel setData() pour dnir les valeurs de chaque colonne (champ).
QSqlTableModel model; model.setTable("cd"); int row = 0; model.insertRows(row, 1); model.setData(model.index(row, model.setData(model.index(row, model.setData(model.index(row, model.setData(model.index(row, model.submitAll();

0), 1), 2), 3),

113); "Shanghai My Heart"); 224); 2003);

Aprs lappel submitAll(), lenregistrement peut tre dplac vers un emplacement diffrent dans la ligne, selon lorganisation de la table. Lappel de cette fonction retournera false si linsertion a chou. Une diffrence importante entre un modle SQL et un modle standard est que dans le premier cas, nous devons appeler submitAll() pour valider une modication dans la base de donnes. Pour mettre jour un enregistrement, nous devons tout dabord placer le QSqlTableModel sur lenregistrement modier (en excutant select()), par exemple. Nous extrayons ensuite lenregistrement, mettons jour les champs voulus, puis rcrivons nos modications dans la base de donnes :
QSqlTableModel model; model.setTable("cd"); model.setFilter("id = 125"); model.select(); if (model.rowCount() == 1) {

316

Qt4 et C++ : Programmation dinterfaces GUI

QSqlRecord record = model.record(0); record.setValue("title", "Melody A.M."); record.setValue("year", record.value("year").toInt() + 1); model.setRecord(0, record); model.submitAll(); }

Si un enregistrement correspond au ltre spci, nous le rcuprons au moyen de QSqlTableModel::record(). Nous appliquons nos modications et remplaons lenregistrement initial par ce dernier. Comme dans le cas dun modle non SQL, il est galement possible de raliser une mise jour au moyen de setData(). Les index de modle que nous rcuprons correspondent une ligne ou une colonne donne :
model.select(); if (model.rowCount() == 1) { model.setData(model.index(0, 1), "Melody A.M."); model.setData(model.index(0, 3), model.data(model.index(0, 3)).toInt() + 1); model.submitAll(); }

La suppression dun enregistrement est similaire sa mise jour :


model.setTable("cd"); model.setFilter("id = 125"); model.select(); if (model.rowCount() == 1) { model.removeRows(0, 1); model.submitAll(); }

Lappel de removeRows() reoit le numro de ligne du premier enregistrement supprimer ainsi que le nombre denregistrements liminer. Lexemple suivant supprime tous les enregistrements correspondant au ltre :
model.setTable("cd"); model.setFilter("year < 1990"); model.select(); if (model.rowCount() > 0) { model.removeRows(0, model.rowCount()); model.submitAll(); }

Les classes QSqlQuery et QSqlTableModel fournissent une interface entre Qt et une base de donnes SQL. En utilisant ces classes, nous pouvons crer des formulaires qui prsentent les donnes aux utilisateurs et leur permettent dinsrer, de mettre jour et de supprimer des enregistrements.

Chapitre 13

Les bases de donnes

317

Prsenter les donnes sous une forme tabulaire


Dans de nombreuses situations, il est plus simple de proposer aux utilisateurs une vue tabulaire dun jeu de donnes. Dans cette section ainsi que dans la suivante, nous prsentons une application CD Collection simple qui fait appel QSqlTableModel et sa sous-classe QSqlRelationalTableModel pour permettre aux utilisateurs dafcher et dinteragir avec les donnes stockes dans une base de donnes. Le formulaire principal prsente une vue matre/dtail dun CD et les pistes du CD en cours de slection, comme illustr en Figure 13.1.
Figure 13.1 Lapplication CD Collection

Lapplication utilise trois tables, dnies comme suit :


CREATE TABLE artist ( id INTEGER PRIMARY KEY, name VARCHAR(40) NOT NULL, country VARCHAR(40)); CREATE TABLE cd ( id INTEGER PRIMARY KEY, title VARCHAR(40) NOT NULL, artistid INTEGER NOT NULL, year INTEGER NOT NULL, FOREIGN KEY (artistid) REFERENCES artist); CREATE TABLE track ( id INTEGER PRIMARY KEY,

318

Qt4 et C++ : Programmation dinterfaces GUI

title VARCHAR(40) NOT NULL, duration INTEGER NOT NULL, cdid INTEGER NOT NULL, FOREIGN KEY (cdid) REFERENCES cd);

Certaines bases de donnes ne supportent pas les cls trangres. Dans ce cas, nous devons supprimer les clauses FOREIGN KEY. Lexemple fonctionnera toujours, mais la base de donnes nappliquera pas lintgrit rfrentielle. (Voir Figure 13.2)
Figure 13.2 Les tables de lapplication CD Collection
artist id name country cd id title artistid year track id title duration cdid

1:N

1:N

Dans cette section, nous allons crire une bote de dialogue dans laquelle les utilisateurs pourront modier une liste dartistes en utilisant une forme tabulaire simple. Lutilisateur peut insrer ou supprimer des artistes au moyen des boutons du formulaire. Les mises jour peuvent tre appliques directement, simplement en modiant le texte des cellules. Les changements sont appliqus la base de donnes lorsque lutilisateur appuie sur Entre ou passe un autre enregistrement. (Voir Figure 13.3)
Figure 13.3 La bote de dialogue ArtistForm

Voici la dnition de classe pour cette bote de dialogue :


class ArtistForm: public QDialog { Q_OBJECT public: ArtistForm(const QString &name, QWidget *parent = 0); private slots: void addArtist(); void deleteArtist();

Chapitre 13

Les bases de donnes

319

void beforeInsertArtist(QSqlRecord &record); private: enum { Artist_Id = 0, Artist_Name = 1, Artist_Country = 2 }; QSqlTableModel *model; QTableView *tableView; QPushButton *addButton; QPushButton *deleteButton; QPushButton *closeButton; };

Le constructeur est trs similaire celui qui serait utilis pour crer un formulaire bas sur un modle non SQL :
ArtistForm::ArtistForm(const QString &name, QWidget *parent) : QDialog(parent) { model = new QSqlTableModel(this); model->setTable("artist"); model->setSort(Artist_Name, Qt::AscendingOrder); model->setHeaderData(Artist_Name, Qt::Horizontal, tr("Name")); model->setHeaderData(Artist_Country, Qt::Horizontal, tr("Country")); model->select(); connect(model, SIGNAL(beforeInsert(QSqlRecord &)), this, SLOT(beforeInsertArtist(QSqlRecord &))); tableView = new QTableView; tableView->setModel(model); tableView->setColumnHidden(Artist_Id, true); tableView->setSelectionBehavior(QAbstractItemView::SelectRows); tableView->resizeColumnsToContents(); for (int row = 0; row < model->rowCount(); ++row) { QSqlRecord record = model->record(row); if (record.value(Artist_Name).toString() == name) { tableView->selectRow(row); break; } } }

Nous commenons le constructeur en crant un QSqlTableModel. Nous lui transmettons this comme parent pour octroyer la proprit au formulaire. Nous avons choisi de baser le tri sur la colonne 1 (spcie par la constante Artist_Name), ce qui correspond au champ name. Si nous ne spcions pas den-tte de colonnes, ce sont les noms des champs qui sont utiliss.

320

Qt4 et C++ : Programmation dinterfaces GUI

Nous prfrons les nommer nous-mmes pour garantir une casse et une internationalisation correctes. Nous crons ensuite un QTableView pour visualiser le modle. Nous masquons le champ id et dnissons les largeurs des colonnes en fonction de leur texte an de ne pas avoir afcher de points de suspension. Le constructeur de ArtistForm reoit le nom de lartiste qui doit tre slectionn louverture de la bote de dialogue. Nous parcourons les enregistrements de la table artist et slectionnons lartiste voulu. Le reste du code du constructeur permet de crer et de connecter les boutons ainsi que de positionner les widgets enfants.
void ArtistForm::addArtist() { int row = model->rowCount(); model->insertRow(row); QModelIndex index = model->index(row, Artist_Name); tableView->setCurrentIndex(index); tableView->edit(index); }

Pour ajouter un nouvel artiste, nous insrons une ligne vierge dans le bas de QTableView. Lutilisateur peut maintenant entrer le nom et le pays du nouvel artiste. Sil conrme linsertion en appuyant sur Entre, le signal beforeInsert() est mis puis le nouvel enregistrement est insr dans la base de donnes.
void ArtistForm::beforeInsertArtist(QSqlRecord &record) { record.setValue("id", generateId("artist")); }

Dans le constructeur, nous avons connect le signal beforeInsert() du modle ce slot. Une rfrence non-const lenregistrement nous est transmise juste avant son insertion dans la base de donnes. A ce stade, nous remplissons son champ id. Comme nous aurons besoin de generateId() plusieurs reprises, nous la dnissons "en ligne" dans un chier den-tte et lincluons chaque fois que ncessaire. Voici un moyen rapide (et inefcace) de limplmenter :
inline int generateId(const QString &table) { QSqlQuery query; query.exec("SELECT MAX(id) FROM " + table); int id = 0; if (query.next()) id = query.value(0).toInt() + 1; return id; }

La fonction generateId() nest assure de fonctionner correctement que si elle est excute dans le contexte de la mme transaction que linstruction INSERT correspondante.

Chapitre 13

Les bases de donnes

321

Certaines bases de donnes supportent les champs gnrs automatiquement, et il est gnralement nettement prfrable dutiliser la prise en charge spcique chaque base de donnes pour cette opration. La dernire possibilit offerte par la bote de dialogue ArtistForm est la suppression. Au lieu deffectuer des suppressions en cascade (que nous avons abordes brivement), nous avons choisi de nautoriser que les suppressions dartistes ne possdant pas de CD dans la collection.
void ArtistForm::deleteArtist() { tableView->setFocus(); QModelIndex index = tableView->currentIndex(); if (!index.isValid()) return; QSqlRecord record = model->record(index.row()); QSqlTableModel cdModel; cdModel.setTable("cd"); cdModel.setFilter("artistid = " + record.value("id").toString()); cdModel.select(); if (cdModel.rowCount() == 0) { model->removeRow(tableView->currentIndex().row()); } else { QMessageBox::information(this, tr("Delete Artist"), tr("Cannot delete %1 because there are CDs associated " "with this artist in the collection.") .arg(record.value("name").toString())); } }

Si un enregistrement est slectionn, nous dterminons si lartiste possde un CD. Si tel nest pas le cas, nous le supprimons immdiatement. Sinon, nous afchons une bote de message expliquant pourquoi la suppression na pas eu lieu. Strictement parlant, nous aurions d utiliser une transaction, car tel que le code se prsente, il est possible que lartiste que nous supprimons soit associ un CD entre les appels de cdModel.select() et model->removeRow(). Nous prsenterons une transaction dans la prochaine section.

Implmenter des formulaires matre/dtail


A prsent, nous allons rviser le formulaire principal avec une approche matre/dtail. La vue matre est une liste de CD. La vue dtail est une liste de pistes pour le CD en cours. Ce formulaire reprsente la fentre principale de lapplication CD Collection comme illustr en Figure 13.1.
class MainForm: public QWidget { Q_OBJECT

322

Qt4 et C++ : Programmation dinterfaces GUI

public: MainForm(); private void void void void void void void void void slots: addCd(); deleteCd(); addTrack(); deleteTrack(); editArtists(); currentCdChanged(const QModelIndex &index); beforeInsertCd(QSqlRecord &record); beforeInsertTrack(QSqlRecord &record); refreshTrackViewHeader();

private: enum { Cd_Id = 0, Cd_Title = 1, Cd_ArtistId = 2, Cd_Year = 3 }; enum { Track_Id = 0, Track_Title = 1, Track_Duration = 2, Track_CdId = 3 }; QSqlRelationalTableModel *cdModel; QSqlTableModel *trackModel; QTableView *cdTableView; QTableView *trackTableView; QPushButton *addCdButton; QPushButton *deleteCdButton; QPushButton *addTrackButton; QPushButton *deleteTrackButton; QPushButton *editArtistsButton; QPushButton *quitButton; };

Au lieu dun QSqlTableModel, nous utilisons un QSqlRelationalTableModel pour la table cd, car nous devons grer les cls trangres. Nous allons maintenant revoir chaque fonction tour tour, en commenant par le constructeur que nous tudierons par segments car il est assez long.
MainForm::MainForm() { cdModel = new QSqlRelationalTableModel(this); cdModel->setTable("cd"); cdModel->setRelation(Cd_ArtistId, QSqlRelation("artist", "id", "name")); cdModel->setSort(Cd_Title, Qt::AscendingOrder);

Chapitre 13

Les bases de donnes

323

cdModel->setHeaderData(Cd_Title, Qt::Horizontal, tr("Title")); cdModel->setHeaderData(Cd_ArtistId, Qt::Horizontal, tr("Artist")); cdModel->setHeaderData(Cd_Year, Qt::Horizontal, tr("Year")); cdModel->select();

Le constructeur dnit tout dabord le QSqlRelationalTableModel qui gre la table cd. Lappel de setRelation() indique au modle que son champ artistid (dont lindex est inclus dans Cd_ArtistId) possde la cl trangre id de la table artist, et que le contenu du champ name correspondant doit tre afch la place des ID. Si lutilisateur choisit dditer ce champ (par exemple en appuyant sur F2), le modle prsentera automatiquement une zone de liste droulante avec les noms de tous les artistes, et si lutilisateur choisit un artiste diffrent, il mettra la table cd jour.
cdTableView = new QTableView; cdTableView->setModel(cdModel); cdTableView->setItemDelegate(new QSqlRelationalDelegate(this)); cdTableView->setSelectionMode(QAbstractItemView::SingleSelection); cdTableView->setSelectionBehavior(QAbstractItemView::SelectRows); cdTableView->setColumnHidden(Cd_Id, true); cdTableView->resizeColumnsToContents();

La dnition de la vue pour la table cd est similaire ce que nous avons dj vu. La seule diffrence signicative est la suivante : au lieu dutiliser le dlgu par dfaut de la vue, nous utilisons QSqlRelationalDelegate. Cest ce dlgu qui gre les cls trangres.
trackModel = new QSqlTableModel(this); trackModel->setTable("track"); trackModel->setHeaderData(Track_Title, Qt::Horizontal, tr("Title")); trackModel->setHeaderData(Track_Duration, Qt::Horizontal, tr("Duration")); trackTableView = new QTableView; trackTableView->setModel(trackModel); trackTableView->setItemDelegate( new TrackDelegate(Track_Duration, this)); trackTableView->setSelectionMode( QAbstractItemView::SingleSelection); trackTableView->setSelectionBehavior(QAbstractItemView::SelectRows);

Pour ce qui est des pistes, nous nallons montrer que leurs noms et leurs dures. Cest pourquoi QSqlTableModel est sufsant. Le seul aspect remarquable de cette partie du code est que nous utilisons le TrackDelegate dvelopp dans le Chapitre 10 pour afcher les dures des pistes sous la forme "minutes:secondes" et permettre leur dition en utilisant un QTimeEdit adapt. La cration, la connexion et la disposition des vues ainsi que des boutons ne prsente pas de surprise. Cest pourquoi la seule autre partie du constructeur que nous allons prsenter contient quelques connexions non videntes.
connect(cdTableView->selectionModel(),

324

Qt4 et C++ : Programmation dinterfaces GUI

SIGNAL(currentRowChanged(const QModelIndex &, const QModelIndex &)), this, SLOT(currentCdChanged(const QModelIndex &))); connect(cdModel, SIGNAL(beforeInsert(QSqlRecord &)), this, SLOT(beforeInsertCd(QSqlRecord &))); connect(trackModel, SIGNAL(beforeInsert(QSqlRecord &)), this, SLOT(beforeInsertTrack(QSqlRecord &))); connect(trackModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(refreshTrackViewHeader())); }

La premire connexion est inhabituelle, car au lieu de connecter un widget, nous tablissons une connexion avec un modle de slection. La classe QItemSelectionModel est utilise pour assurer le suivi des slections dans les vues. En tant connect au modle de slection de la vue table, notre slot currentCdChanged() sera appel ds que lutilisateur passe dun enregistrement lautre.
void MainForm::currentCdChanged(const QModelIndex &index) { if (index.isValid()) { QSqlRecord record = cdModel->record(index.row()); int id = record.value("id").toInt(); trackModel->setFilter(QString("cdid = %1").arg(id)); } else { trackModel->setFilter("cdid = -1"); } trackModel->select(); refreshTrackViewHeader(); }

Ce slot est appel ds que le CD en cours change, ce qui se produit lorsque lutilisateur passe un autre CD (en cliquant ou en utilisant les touches ches). Si le CD est invalide (sil nexiste pas de CD, si un nouveau CD est en cours dinsertion ou encore si celui en cours vient dtre supprim), nous dnissons le cdid de la table track en 1 (un ID invalide qui ne correspondra aucun enregistrement). Puis, en ayant dni le ltre, nous slectionnons les enregistrements de piste correspondants. La fonction refreshTrackViewHeader() sera tudie dans un moment.
void MainForm::addCd() { int row = 0; if (cdTableView->currentIndex().isValid()) row = cdTableView->currentIndex().row(); cdModel->insertRow(row); cdModel->setData(cdModel->index(row, Cd_Year), QDate::currentDate().year());

Chapitre 13

Les bases de donnes

325

QModelIndex index = cdModel->index(row, Cd_Title); cdTableView->setCurrentIndex(index); cdTableView->edit(index); }

Lorsque lutilisateur clique sur le bouton Add CD, une nouvelle ligne vierge est insre dans le cdTableView et nous entrons en mode dition. Nous dnissons galement une valeur par dfaut pour le champ year. A ce stade, lutilisateur peut modier lenregistrement en remplissant les champs vierges et en slectionnant un artiste dans la zone de liste droulante qui est automatiquement fournie par le QSqlRelationalTableModel grce lappel de setRelation(). Il peut aussi modier lanne si celle propose par dfaut savre inapproprie. Si lutilisateur conrme linsertion en appuyant sur Entre, lenregistrement est insr. Lutilisateur peut annuler en appuyant sur Echap.
void MainForm::beforeInsertCd(QSqlRecord &record) { record.setValue("id", generateId("cd")); }

Ce slot est appel lorsque le cdModel met son signal beforeInsert(). Nous lutilisons pour remplir le champ id de la mme faon que nous lavons fait pour insrer de nouveaux artistes. Les mmes rgles sappliquent : cette opration doit seffectuer dans la porte dune transaction et avec les mthodes de cration dID spciques la base de donnes (par exemple, les ID gnrs automatiquement).
void MainForm::deleteCd() { QModelIndex index = cdTableView->currentIndex(); if (!index.isValid()) return; QSqlDatabase db = QSqlDatabase::database(); db.transaction(); QSqlRecord record = cdModel->record(index.row()); int id = record.value(Cd_Id).toInt(); int tracks = 0; QSqlQuery query; query.exec(QString("SELECT COUNT(*) FROM track WHERE cdid = %1") .arg(id)); if (query.next()) tracks = query.value(0).toInt(); if (tracks > 0) { int r = QMessageBox::question(this, tr("Delete CD"), tr("Delete \"%1\" and all its tracks?") .arg(record.value(Cd_ArtistId).toString()), QMessageBox::Yes | QMessageBox::Default, QMessageBox::No | QMessageBox::Escape); if (r == QMessageBox::No) { db.rollback(); return; }

326

Qt4 et C++ : Programmation dinterfaces GUI

query.exec(QString("DELETE FROM track WHERE cdid = %1") .arg(id)); } cdModel->removeRow(index.row()); cdModel->submitAll(); db.commit(); currentCdChanged(QModelIndex()); }

Lorsque lutilisateur clique sur le bouton Delete CD, ce slot est appel. Quand un CD est en cours, nous dterminons son nombre de pistes. Si nous ne trouvons pas de piste, nous supprimons directement lenregistrement du CD. Sil existe au moins une piste, nous demandons lutilisateur de conrmer la suppression. Sil clique sur Yes, nous supprimons tous les enregistrements de piste, puis lenregistrement du CD. Toutes ces oprations sont effectues dans la porte dune transaction. Ainsi, soit la suppression en cascade choue en bloc, soit elle russit dans son ensemble en supposant que la base de donnes en question supporte les transactions. La gestion des donnes de piste est trs similaire celle des donnes de CD. Les mises jour peuvent tre effectues simplement via les cellules ddition fournies lutilisateur. Dans le cas des dures de piste, notre TrackDelegate sassure quelles sont prsentes dans le bon format et quelles sont facilement modiables au moyen de QTimeEdit.
void MainForm::addTrack() { if (!cdTableView->currentIndex().isValid()) return; int row = 0; if (trackTableView->currentIndex().isValid()) row = trackTableView->currentIndex().row(); trackModel->insertRow(row); QModelIndex index = trackModel->index(row, Track_Title); trackTableView->setCurrentIndex(index); trackTableView->edit(index); }

Le fonctionnement ici est le mme que celui de addCd(), avec une nouvelle ligne vierge insre dans la vue.
void MainForm::beforeInsertTrack(QSqlRecord &record) { QSqlRecord cdRecord = cdModel->record(cdTableView->currentIndex() .row()); record.setValue("id", generateId("track")); record.setValue("cdid", cdRecord.value(Cd_Id).toInt()); }

Chapitre 13

Les bases de donnes

327

Si lutilisateur conrme linsertion initie par addTrack(), cette fonction est appele pour remplir les champs id et cdid. Les rgles mentionnes prcdemment sappliquent bien sr toujours ici.
void MainForm::deleteTrack() { trackModel->removeRow(trackTableView->currentIndex().row()); if (trackModel->rowCount() == 0) trackTableView->horizontalHeader()->setVisible(false); }

Si lutilisateur clique sur le bouton Delete Track, nous supprimons la piste sans formalit. Il serait facile dafcher une bote de message de type Yes/No si nous envisagions de faire conrmer les suppressions.
void MainForm::refreshTrackViewHeader() { trackTableView->horizontalHeader()->setVisible( trackModel->rowCount() > 0); trackTableView->setColumnHidden(Track_Id, true); trackTableView->setColumnHidden(Track_CdId, true); trackTableView->resizeColumnsToContents(); }

Le slot refreshTrackViewHeader() est invoqu depuis plusieurs emplacements pour sassurer que len-tte horizontal de la vue de piste nest prsent que sil existe des pistes afcher. Il masque aussi les champs id et cdid et redimensionne les colonnes de table visibles en fonction du contenu courant de la table.
void MainForm::editArtists() { QSqlRecord record = cdModel->record(cdTableView->currentIndex() .row()); ArtistForm artistForm(record.value(Cd_ArtistId).toString(), this); artistForm.exec(); cdModel->select(); }

Ce slot est appel si lutilisateur clique sur le bouton Edit Artists. Il afche les donnes concernant lartiste du CD en cours, invoquant le ArtistForm trait dans la section prcdente et slectionnant lartiste appropri. Sil nexiste pas denregistrement en cours, un enregistrement vide est retourn par record(). Il ne correspond naturellement aucun artiste dans le formulaire. Voici ce qui se produit vritablement : comme nous utilisons un QSqlRelationalTableModel qui tablit une correspondance entre les ID des artistes et leurs noms, la valeur qui est retourne lorsque nous appelons record.value(Cd_ArtistId) est le nom de lartiste (qui sera une chane vide si lenregistrement est vide). Nous forons enn le cdModel slectionner de nouveau ses donnes, ce qui conduit le cdTableView rafrachir ses cellules visibles.

328

Qt4 et C++ : Programmation dinterfaces GUI

Cette opration permet de sassurer que les noms des artistes sont afchs correctement, certains dentre eux ayant pu tre modis par lutilisateur dans la bote de dialogue ArtistForm. Pour les projets qui utilisent les classes SQL, nous devons ajouter la ligne
QT += sql

aux chiers .pro, ce qui garantit la liaison de lapplication la bibliothque QtSql. Ce chapitre vous a dmontr que les classes vue/modle de Qt facilitent autant que possible lafchage et la modication de donnes dans les bases SQL. Dans les cas o les cls trangres se rfrent des tables comportant de nombreux enregistrements (des milliers, voir plus), il est probablement prfrable de crer votre propre dlgu et de lutiliser pour prsenter un formulaire de "listes de valeurs" offrant des possibilits de recherche au lieu de vous reposer sur les zones de liste droulante par dfaut de QSqlRelationalTableModel. Et si nous souhaitons prsenter des enregistrements en utilisant un mode formulaire, nous devons le grer par nousmmes : en faisant appel un QSqlQuery ou un QSqlTableModel pour grer linteraction avec la base de donnes, et en tablissant une correspondance entre le contenu des widgets de linterface utilisateur que nous souhaitons utiliser pour prsenter et modier les donnes et la base de donnes concerne dans notre propre code.

14
Gestion de rseau
Au sommaire de ce chapitre Programmer les clients FTP Programmer les clients HTTP Programmer les applications client/serveur TCP Envoyer et recevoir des datagrammes UDP

Qt fournit les classes QFtp et QHttp pour la programmation de FTP et HTTP. Ces protocoles sont faciles utiliser pour tlcharger des chiers et, dans le cas de HTTP, pour envoyer des requtes aux serveurs Web et rcuprer les rsultats. Qt fournit galement les classes de bas niveau QTcpSocket et QUdpSocket, qui implmentent les protocoles de transport TCP et UDP. TCP est un protocole orient connexion able qui agit en termes de ux de donnes transmis entre les nuds rseau, alors que UDP est un protocole fonctionnant en mode non connect non able qui permet denvoyer des paquets discrets entre des nuds rseau. Tous deux peuvent tre utiliss pour crer des applications rseau clientes et serveur. En ce qui concerne les serveurs, nous avons aussi besoin de la classe QTcpServer pour grer les connexions TCP entrantes.

330

Qt4 et C++ : Programmation dinterfaces GUI

Programmer les clients FTP


La classe QFtp implmente le ct client du protocole FTP dans Qt. Elle offre diverses fonctions destines raliser les oprations FTP les plus courantes et nous permet dexcuter des commandes FTP arbitraires. La classe QFtp fonctionne de faon asynchrone. Lorsque nous appelons une fonction telle que get() ou put(), elle se termine immdiatement et le transfert de donnes se produit quand le contrle revient la boucle dvnement de Qt. Ainsi, linterface utilisateur reste ractive pendant lexcution des commandes FTP. Nous allons commencer par un exemple qui illustre comment rcuprer un chier unique au moyen de get(). Lexemple est une application de console nomme ftpget qui tlcharge le chier distant spci sur la ligne de commande. Commenons par la fonction main():
int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QStringList args = app.arguments(); if (args.count()!= 2) { cerr << "Usage: ftpget url" << endl << "Example:" << endl << " ftpget ftp://ftp.trolltech.com/mirrors" << endl; return 1; } FtpGet getter; if (!getter.getFile(QUrl(args[1]))) return 1; QObject::connect(&getter, SIGNAL(done()), &app, SLOT(quit())); return app.exec(); }

Nous crons un QCoreApplication plutt que sa sous-classe QApplication pour viter une liaison dans la bibliothque QtGui. La fonction QCoreApplication::arguments() retourne les arguments de ligne de commande sous forme de QStringList, le premier lment tant le nom sous lequel le programme a t invoqu, et tous les arguments propres Qt (tels que -style) tant supprims. Le cur de la fonction main() est la construction de lobjet FtpGet et lappel de getFile(). Si lappel russit, nous laissons la boucle dvnement sexcuter jusqu la n du tlchargement. Tout le travail est effectu par la sous-classe FtpGet, qui est dnie comme suit :
class FtpGet: public QObject { Q_OBJECT

Chapitre 14

Gestion de rseau

331

public: FtpGet(QObject *parent = 0); bool getFile(const QUrl &url); signals: void done(); private slots: void ftpDone(bool error); private: QFtp ftp; QFile file; };

La classe possde une fonction publique, getFile(), qui rcupre le chier spci par une URL. La classe QUrl fournit une interface de haut niveau destine extraire les diffrents segments dune URL, tels que le nom du chier, le chemin daccs, le protocole et le port. FtpGet possde un slot priv, ftpDone(), qui est appel lorsque le transfert de chier est termin, et un signal done() qui est mis une fois le chier tlcharg. La classe contient galement deux variables prives : la variable ftp, de type QFtp, qui encapsule la connexion avec un serveur FTP et la variable file qui est utilise pour crire le chier tlcharg sur le disque.
FtpGet::FtpGet(QObject *parent) : QObject(parent) { connect(&ftp, SIGNAL(done(bool)), this, SLOT(ftpDone(bool))); }

Dans le constructeur, nous connectons le signal QFtp::done(bool) notre slot priv ftpDone(bool). QFtp met done(bool) une fois le traitement de toutes les requtes termin. Le paramtre bool indique si une erreur sest produite ou non.
bool FtpGet::getFile(const QUrl &url) { if (!url.isValid()) { cerr << "Error: Invalid URL" << endl; return false; } if (url.scheme()!= "ftp") { cerr << "Error: URL must start with ftp:" << endl; return false; } if (url.path().isEmpty()) { cerr << "Error: URL has no path" << endl; return false; }

332

Qt4 et C++ : Programmation dinterfaces GUI

QString localFileName = QFileInfo(url.path()).fileName(); if (localFileName.isEmpty()) localFileName = "ftpget.out"; file.setFileName(localFileName); if (!file.open(QIODevice::WriteOnly)) { cerr << "Error: Cannot open " << qPrintable(file.fileName()) << " for writing: " << qPrintable(file.errorString()) << endl; return false; } ftp.connectToHost(url.host(), url.port(21)); ftp.login(); ftp.get(url.path(), &file); ftp.close(); return true; }

La fonction getFile() commence en vriant lURL transmise. Si un problme est rencontr, elle met un message derreur vers cerr et retourne false pour indiquer que le tlchargement a chou. Au lieu dobliger lutilisateur crer un nom de chier local, nous essayons de gnrer un nom judicieux constitu de lURL elle-mme, avec ftpget.out comme solution de secours. Si nous ne parvenons pas ouvrir le chier, nous afchons un message derreur et retournons false. Nous excutons ensuite une squence de quatre commandes FTP en utilisant notre objet QFtp. Lappel de url.port(21) retourne le numro de port mentionn dans lURL, ou le port 21 si lURL nen spcie aucun. Aucun nom dutilisateur ou mot de passe ntant transmis la fonction login(), on tente une ouverture de session anonyme. Le second argument de get() spcie le priphrique de sortie. Les commandes FTP sont places en le dattente et excutes dans la boucle dvnement de Qt. Lachvement de toutes les commandes est indiqu par le signal done(bool) de QFtp, que nous avons connect ftpDone(bool) dans le constructeur.
void FtpGet::ftpDone(bool error) { if (error) { cerr << "Error: " << qPrintable(ftp.errorString()) << endl; } else { cerr << "File downloaded as " << qPrintable(file.fileName()) << endl; } file.close(); emit done(); }

Chapitre 14

Gestion de rseau

333

Les commandes FTP ayant toutes t excutes, nous fermons le chier et mettons notre propre signal done(). Il peut sembler trange de fermer le chier ici, et non aprs lappel de ftp.close() la n de la fonction getFile(), mais souvenez-vous que les commandes FTP sont excutes de faon asynchrone et peuvent trs bien tre en cours la n de lexcution de getFile(). Seule lmission du signal done() de lobjet QFtp nous permet de savoir que le tlchargement est termin et que le chier peut tre ferm en toute scurit. QFtp fournit plusieurs commandes FTP, dont connectToHost(), login(), close(), list(), cd(), get(), put(), remove(), mkdir(), rmdir() et rename(). Toutes ces fonctions programment une commande FTP et retournent un numro dID qui identie cette commande. Il est galement possible de contrler le mode et le type de transfert (loption par dfaut est un mode passif et un type binaire). Les commandes FTP arbitraires peuvent tre excutes au moyen de la commande rawCommand(). Voici, par exemple, comment excuter une commande SITE CHMOD:
ftp.rawCommand("SITE CHMOD 755 fortune");

QFtp met le signal commandStarted(int) quand il commence excuter une commande et le signal commandFinished(int, bool) une fois la commande termine. Le paramtre int est le numro dID qui identie la commande. Si nous nous intressons au sort des commandes individuelles, nous pouvons stocker les numros dID lors de la programmation des commandes. Le fait de suivre ces numros nous permet de fournir un rapport dtaill lutilisateur. Par exemple :
bool FtpGet::getFile(const QUrl &url) { ... connectId = ftp.connectToHost(url.host(), url.port(21)); loginId = ftp.login(); getId = ftp.get(url.path(), &file); closeId = ftp.close(); return true; } void FtpGet::ftpCommandStarted(int id) { if (id == connectId) { cerr << "Connecting..." << endl; } else if (id == loginId) { cerr << "Logging in..." << endl; ... }

Un autre moyen de fournir un rapport consiste tablir une connexion au signal stateChanged() de QFtp, qui est mis lorsque la connexion entre dans un nouvel tat (QFtp::Connecting, QFtp::Connected, QFtp::LoggedIn, etc.)

334

Qt4 et C++ : Programmation dinterfaces GUI

Dans la plupart des applications, nous nous intressons plus au sort de la squence de commandes dans son ensemble quaux commandes particulires. Dans ce cas, nous pouvons simplement nous connecter au signal done(bool), qui est mis ds que la le dattente de commandes est vide. Quand une erreur se produit, QFtp vide automatiquement la le dattente de commandes. Ainsi, si la connexion ou louverture de session choue, les commandes qui suivent dans la le dattente ne sont jamais excutes. Si nous programmons de nouvelles commandes au moyen de lobjet QFtp aprs que lerreur se soit produite, elles sont places en le dattente et excutes. Dans le chier .pro de lapplication, la ligne suivante est ncessaire pour tablir une liaison avec la bibliothque QtNetwork :
QT += network

Nous allons maintenant tudier un exemple plus sophistiqu. Le programme de ligne de commande spider tlcharge tous les chiers situs dans un rpertoire FTP. La logique rseau est gre dans la classe Spider:
class Spider: public QObject { Q_OBJECT public: Spider(QObject *parent = 0); bool getDirectory(const QUrl &url); signals: void done(); private slots: void ftpDone(bool error); void ftpListInfo(const QUrlInfo &urlInfo); private: void processNextDirectory(); QFtp ftp; QList<QFile *> openedFiles; QString currentDir; QString currentLocalDir; QStringList pendingDirs; };

Le rpertoire de dpart est spci en tant que QUrl et est dni au moyen de la fonction getDirectory().
Spider::Spider(QObject *parent) : QObject(parent) {

Chapitre 14

Gestion de rseau

335

connect(&ftp, SIGNAL(done(bool)), this, SLOT(ftpDone(bool))); connect(&ftp, SIGNAL(listInfo(const QUrlInfo &)), this, SLOT(ftpListInfo(const QUrlInfo &))); }

Dans le constructeur, nous tablissons deux connexions signal/slot. Le signal listInfo (const QUrlInfo &) est mis par QFtp lorsque nous demandons un listing de rpertoires (dans getDirectory()) pour chaque chier rcupr. Ce signal est connect un slot nomm ftpListInfo(), qui tlcharge le chier associ lURL qui lui est fournie.
bool Spider::getDirectory(const QUrl &url) { if (!url.isValid()) { cerr << "Error: Invalid URL" << endl; return false; } if (url.scheme()!= "ftp") { cerr << "Error: URL must start with ftp:" << endl; return false; } ftp.connectToHost(url.host(), url.port(21)); ftp.login(); QString path = url.path(); if (path.isEmpty()) path = "/"; pendingDirs.append(path); processNextDirectory(); return true; }

Lorsque la fonction getDirectory() est appele, elle commence par effectuer quelques vrifications de base, et, si tout va bien, tente dtablir une connexion FTP. Elle appelle processNextDirectory() pour lancer le tlchargement du rpertoire racine.
void Spider::processNextDirectory() { if (!pendingDirs.isEmpty()) { currentDir = pendingDirs.takeFirst(); currentLocalDir = "downloads/" + currentDir; QDir(".").mkpath(currentLocalDir); ftp.cd(currentDir); ftp.list(); } else { emit done(); } }

336

Qt4 et C++ : Programmation dinterfaces GUI

La fonction processNextDirectory() reoit le premier rpertoire distant provenant de la liste pendingDirs et cre un rpertoire correspondant dans le systme de chiers local. Elle indique ensuite lobjet QFtp de remplacer le rpertoire existant par celui reu et de rpertorier ses chiers. Pour tout chier trait par list(), un signal listInfo() provoquant lappel du slot ftpListInfo() est mis. Sil ne reste plus de rpertoire traiter, la fonction met le signal done() pour indiquer que le tlchargement est achev.
void Spider::ftpListInfo(const QUrlInfo &urlInfo) { if (urlInfo.isFile()) { if (urlInfo.isReadable()) { QFile *file = new QFile(currentLocalDir + "/" + urlInfo.name()); if (!file->open(QIODevice::WriteOnly)) { cerr << "Warning: Cannot open file " << qPrintable( QDir::convertSeparators(file->fileName())) << endl; return; } ftp.get(urlInfo.name(), file); openedFiles.append(file); } } else if (urlInfo.isDir() &&!urlInfo.isSymLink()) { pendingDirs.append(currentDir + "/" + urlInfo.name()); } }

Le paramtre urlInfo du slot ftpListInfo() fournit des informations dtailles concernant un chier distant. Sil sagit dun chier normal (et non dun rpertoire) lisible, nous appelons get() pour le tlcharger. Lobjet QFile utilis pour le tlchargement est allou au moyen de new et un pointeur dirig vers celui-ci est stock dans la liste openedFiles. Si le QUrlInfo contient les dtails dun rpertoire distant qui nest pas un lien symbolique, nous ajoutons ce rpertoire la liste pendingDirs. Nous ignorons les liens symboliques car ils peuvent aisment mener une rcurrence innie.
void Spider::ftpDone(bool error) { if (error) { cerr << "Error: " << qPrintable(ftp.errorString()) << endl; } else { cout << "Downloaded " << qPrintable(currentDir) << " to " << qPrintable(QDir::convertSeparators( QDir(currentLocalDir).canonicalPath())); }

Chapitre 14

Gestion de rseau

337

qDeleteAll(openedFiles); openedFiles.clear(); processNextDirectory(); }

Le slot ftpDone() est appel lorsque toutes les commandes FTP sont termines ou si une erreur se produit. Nous supprimons les objets QFile pour viter les fuites de mmoire et galement pour fermer chaque chier. Nous appelons enn processNextDirectory(). Sil reste des rpertoires, tout le processus recommence pour le rpertoire suivant dans la liste. Dans le cas contraire, le tlchargement est interrompu une fois done() mis. Si aucune erreur nintervient, la squence de signaux et de commandes FTP est la suivante :
connectToHost(host, port) login() cd(directory_1) list() emit listInfo(file_1_1) get(file_1_1) emit listInfo(file_1_2) get(file_1_2) ... emit done() ... cd(directory_N) list() emit listInfo(file_N_1) get(file_N_1) emit listInfo(file_N_2) get(file_N_2) ... emit done()

Si un chier est un rpertoire, il est ajout la liste pendingDirs. Une fois le dernier chier de la commande list() tlcharg, une nouvelle commande cd() est mise, suivie dune commande list() avec le rpertoire suivant en attente. Tout le processus recommence alors avec ce rpertoire. Ces oprations sont rptes jusqu ce que chaque chier ait t tlcharg. A ce moment l, la liste pendingDirs est vide. Si une erreur rseau se produit lors du tlchargement du cinquime ou, disons, du vingtime chier dun rpertoire, les chiers restants ne sont pas tlchargs. Pour tlcharger autant de chiers que possible, une solution consisterait planier les oprations GET une par une et attendre le signal done(bool) avant la planication de lopration suivante. Dans listInfo(), nous accolerions simplement le nom de chier un QStringList au lieu

338

Qt4 et C++ : Programmation dinterfaces GUI

dappeler get() directement, et dans done(bool) nous appellerions get() sur le chier suivant tlcharger dans le QStringList. La squence dexcution serait alors celle-ci :
connectToHost(host, port) login() cd(directory_1) list() ... cd(directory_N) list() emit listInfo(file_1_1) emit listInfo(file_1_2) ... emit listInfo(file_N_1) emit listInfo(file_N_2) ... emit done() get(file_1_1) emit done() get(file_1_2) emit done() ... get(file_N_1) emit done() get(file_N_2) emit done() ...

Une autre solution consisterait utiliser un objet QFtp pour chaque chier, ce qui nous permettrait de tlcharger les chiers en parallle, par le biais de connexions FTP spares.
int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QStringList args = app.arguments(); if (args.count()!= 2) { cerr << "Usage: spider url" << endl << "Example:" << endl << " spider ftp://ftp.trolltech.com/freebies/leafnode" << endl; return 1; }

Chapitre 14

Gestion de rseau

339

Spider spider; if (!spider.getDirectory(QUrl(args[1]))) return 1; QObject::connect(&spider, SIGNAL(done()), &app, SLOT(quit())); return app.exec(); }

La fonction main() achve le programme. Si lutilisateur ne spcie pas dURL sur la ligne de commande, nous gnrons un message derreur et terminons le programme. Dans les deux exemples FTP, les donnes rcupres au moyen de get() ont t crites dans un QFile. Ceci nest pas obligatoire. Si nous souhaitions enregistrer les donnes en mmoire, nous pourrions utiliser un QBuffer, la sous-classe QIODevice qui intgre un QByteArray. Par exemple :
QBuffer *buffer = new QBuffer; buffer->open(QIODevice::WriteOnly); ftp.get(urlInfo.name(), buffer);

Nous pourrions galement omettre largument de priphrique dE/S de get() ou transmettre un pointeur nul. La classe QFtp met alors un signal readyRead() ds quune nouvelle donne est disponible. Cette dernire est ensuite lue au moyen de read() ou de readAll().

Programmer les clients HTTP


La classe QHttp implmente le ct client du protocole HTTP dans Qt. Elle fournit diverses fonctions destines effectuer les oprations HTTP les plus courantes dont get() et post(), et permet denvoyer des requtes HTTP arbitraires. Si vous avez lu la section prcdente concernant QFtp, vous constaterez quil existe des similitudes entre QFtp et QHttp. La classe QHttp fonctionne de faon asynchrone. Lorsque nous appelons une fonction telle que get() ou post(), elle se termine immdiatement et le transfert de donnes se produit ultrieurement quand le contrle revient la boucle dvnement de Qt. Ainsi, linterface utilisateur de lapplication reste ractive pendant le traitement des requtes HTTP. Nous allons tudier un exemple dapplication de console nomme httpget qui illustre comment tlcharger un chier en utilisant le protocole HTTP. Il est trs similaire lexemple ftpget de la section prcdente, la fois en matire de fonctionnalit et dimplmentation. Nous ne prsenterons donc pas le chier den-tte.
HttpGet::HttpGet(QObject *parent) : QObject(parent) { connect(&http, SIGNAL(done(bool)), this, SLOT(httpDone(bool))); }

340

Qt4 et C++ : Programmation dinterfaces GUI

Dans le constructeur, nous connectons le signal done(bool) de lobjet QHttp au slot priv httpDone(bool).
bool HttpGet::getFile(const QUrl &url) { if (!url.isValid()) { cerr << "Error: Invalid URL" << endl; return false; } if (url.scheme()!= "http") { cerr << "Error: URL must start with http:" << endl; return false; } if (url.path().isEmpty()) { cerr << "Error: URL has no path" << endl; return false; } QString localFileName = QFileInfo(url.path()).fileName(); if (localFileName.isEmpty()) localFileName = "httpget.out"; file.setFileName(localFileName); if (!file.open(QIODevice::WriteOnly)) { cerr << "Error: Cannot open " << qPrintable(file.fileName()) << " for writing: " << qPrintable(file.errorString()) << endl; return false; } http.setHost(url.host(), url.port(80)); http.get(url.path(), &file); http.close(); return true; }

La fonction getFile() effectue le mme type de contrle derreur que la fonction FtpGet:: getFile() prsente prcdemment et utilise la mme approche pour attribuer au chier un nom local. Lors dune rcupration depuis un site Web, aucun nom de connexion nest ncessaire. Nous dnissons simplement lhte et le port (en utilisant le port HTTP 80 par dfaut sil nest pas spci dans lURL) et tlchargeons les donnes dans le chier, puisque le deuxime argument de QHttp::get() spcie le priphrique dE/S. Les requtes HTTP sont places en le dattente et excutes de faon asynchrone dans la boucle dvnement de Qt. Lachvement des requtes est indiqu par le signal done(bool) de QHttp, que nous avons connect httpDone(bool) dans le constructeur.
void HttpGet::httpDone(bool error) {

Chapitre 14

Gestion de rseau

341

if (error) { cerr << "Error: " << qPrintable(http.errorString()) << endl; } else { cerr << "File downloaded as " << qPrintable(file.fileName()) << endl; } file.close(); emit done(); }

Une fois les requtes HTTP termines, nous fermons le chier, en avertissant lutilisateur si une erreur sest produite. La fonction main() est trs similaire celle utilise par ftpget:
int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QStringList args = app.arguments(); if (args.count()!= 2) { cerr << "Usage: httpget url" << endl << "Example:" << endl << " httpget http://doc.trolltech.com/qq/index.html" << endl; return 1; } HttpGet getter; if (!getter.getFile(QUrl(args[1]))) return 1; QObject::connect(&getter, SIGNAL(done()), &app, SLOT(quit())); return app.exec(); }

La classe QHttp fournit de nombreuses oprations, dont setHost(), get(), post() et head(). Si un site requiert une authentication, setUser() sera utilis pour fournir un nom dutilisateur et un mot de passe. QHttp peut utiliser un socket transmis par le programmeur au lieu de son QTcpSocket interne, ce qui autorise lemploi dun QtSslSocket scuris, fourni en tant que Qt Solution par Trolltech. Pour envoyer une liste de paires "nom=valeur" un script CGI, nous faisons appel post():
http.setHost("www.example.com"); http.post("/cgi/somescript.py", "x=200&y=320", &file);

Nous pouvons transmettre les donnes soit sous la forme dune chane de 8 octets, soit en transmettant un QIODevice ouvert, tel quun QFile. Pour plus de contrle, il est possible de recourir la fonction request(), qui accepte des donnes et un en-tte HTTP arbitraire.

342

Qt4 et C++ : Programmation dinterfaces GUI

Par exemple :
QHttpRequestHeader header("POST", "/search.html"); header.setValue("Host", "www.trolltech.com"); header.setContentType("application/x-www-form-urlencoded"); http.setHost("www.trolltech.com"); http.request(header, "qt-interest=on&search=opengl");

QHttp met le signal requestStarted(int) quand il commence excuter une requte, puis le signal requestFinished(int, bool) une fois la commande termine. Le paramtre int est le numro dID qui identie une requte. Si nous nous intressons au sort des requtes individuelles, nous pouvons stocker les numros dID lors de la programmation de ces dernires. Le suivi de ces identiants nous permet de fournir un rapport dtaill lutilisateur.
Dans la plupart des applications, nous souhaitons simplement savoir si la squence de requtes dans son ensemble sest termine avec succs ou non. Dans ce cas, nous tablissons une connexion au signal done() , qui est mis lorsque la le dattente de la requte est vide. Quand une erreur se produit, la le dattente de la requte est vide automatiquement. Si nous programmons de nouvelles requtes au moyen de lobjet QHttp aprs que lerreur sest produite, elles sont places en le dattente et envoyes. Comme QFtp, QHttp fournit un signal readyRead() ainsi que les fonctions read() et readAll(), dont lemploi vite la spcication dun priphrique dE/S.

Programmer les applications client/serveur TCP


Les classes QTcpSocket et QTcpServer peuvent tre utilises pour implmenter des serveurs et des clients TCP. TCP est un protocole de transport sur lequel sont bass la plupart des protocoles Internet de niveau application, y compris FTP et HTTP. En outre, il est susceptible dtre utilis pour les protocoles personnaliss. TCP est un protocole orient ux. Pour les applications, les donnes apparaissent sous la forme dun long ux, plutt que sous la forme dun gros chier plat. Les protocoles de haut niveau bass sur TCP sont gnralement orients ligne ou bloc :

Les protocoles orients ligne transfrent les donnes sous la forme de lignes de texte, chacune tant termine par un retour la ligne. Les protocoles orients bloc transfrent les donne sous la forme de blocs de donnes binaires. Chaque bloc comprend un champ de taille suivi de la quantit de donnes spcie.

QTcpSocket hrite de QIODevice par le biais de QAbstractSocket. Il peut donc tre lu et crit au moyen dun QDataStream ou dun QTextStream. Une diffrence notable entre la lecture de donnes partir dun rseau et celle effectue depuis un chier est que nous devons veiller avoir reu sufsamment de donnes avant dutiliser loprateur >>. Dans le cas contraire, nous obtenons un comportement alatoire.

Chapitre 14

Gestion de rseau

343

Dans cette section, nous allons examiner le code dun client et dun serveur qui utilisent un protocole personnalis orient bloc. Le client se nomme Trip Planner et permet aux utilisateurs de planier leur prochain voyage ferroviaire. Le serveur se nomme Trip Server et fournit les informations concernant le voyage au client. Nous allons commencer par crire le client Trip Planner. Trip Planner fournit les champs From, To, Date et Approximate Time ainsi que deux boutons doption indiquant si lheure approximative est celle de dpart ou darrive. Lorsque lutilisateur clique sur Search, lapplication expdie une requte au serveur, qui renvoie une liste des trajets correspondant aux critres de lutilisateur. La liste est prsente sous la forme dun QTableWidget dans la fentre Trip Planner. Le bas de la fentre est occup par un QProgressBar ainsi que par un QLabel qui afche le statut de la dernire opration. (Voir Figure 14.1)
Figure 14.1 Lapplication Trip Planner

Linterface utilisateur de Trip Planner a t cre au moyen de Qt Designer dans un chier nomm tripplanner.ui. Ici, nous allons nous concentrer sur le code source de la sousclasse QDialog qui implmente la fonctionnalit de lapplication :
#include "ui_tripplanner.h" class TripPlanner: public QDialog, public Ui::TripPlanner { Q_OBJECT public: TripPlanner(QWidget *parent = 0); private void void void void void void slots: connectToServer(); sendRequest(); updateTableWidget(); stopSearch(); connectionClosedByServer(); error();

344

Qt4 et C++ : Programmation dinterfaces GUI

private: void closeConnection(); QTcpSocket tcpSocket; quint16 nextBlockSize; };

La classe TripPlanner hrite de Ui::TripPlanner (qui est gnr par le uic de tripplanner.ui) en plus de QDialog. La variable membre tcpSocket encapsule la connexion TCP. La variable nextBlockSize est utilise lors de lanalyse des blocs reus du serveur.
TripPlanner::TripPlanner(QWidget *parent) : QDialog(parent) { setupUi(this); QDateTime dateTime = QDateTime::currentDateTime(); dateEdit->setDate(dateTime.date()); timeEdit->setTime(QTime(dateTime.time().hour(), 0)); progressBar->hide(); progressBar->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Ignored); tableWidget->verticalHeader()->hide(); tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); connect(searchButton, SIGNAL(clicked()), this, SLOT(connectToServer())); connect(stopButton, SIGNAL(clicked()), this, SLOT(stopSearch())); connect(&tcpSocket, SIGNAL(connected()), this, SLOT(sendRequest())); connect(&tcpSocket, SIGNAL(disconnected()), this, SLOT(connectionClosedByServer())); connect(&tcpSocket, SIGNAL(readyRead()), this, SLOT(updateTableWidget())); connect(&tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(error())); }

Dans le constructeur, nous initialisons les diteurs de date et dheure en fonction de la date et de lheure courantes. Nous masquons galement la barre de progression, car nous souhaitons ne lafcher que lorsquune connexion est active. Dans Qt Designer, les proprits minimum et maximum de la barre de progression sont toutes deux dnies en 0, ce qui indique au QProgressBar de se comporter comme un indicateur dactivit au lieu dune barre de progression standard base sur les pourcentages. En outre, dans le constructeur, nous connectons les signaux connected(), disconnected(), readyRead() et error(QAbstractSocket::SocketError) de QTcpSocket des slots privs.

Chapitre 14

Gestion de rseau

345

void TripPlanner::connectToServer() { tcpSocket.connectToHost("tripserver.zugbahn.de", 6178); tableWidget->setRowCount(0); searchButton->setEnabled(false); stopButton->setEnabled(true); statusLabel->setText(tr("Connecting to server...")); progressBar->show(); nextBlockSize = 0; }

Le slot connectToServer() est excut lorsque lutilisateur clique sur Search pour lancer une recherche. Nous appelons connectToHost() sur lobjet QTcpSocket pour tablir une connexion avec le serveur, que nous supposons tre accessible par le biais du port 6178 sur lhte ctif tripserver.zugbahn.de. (Si vous souhaitez tester lexemple sur votre propre machine, remplacez le nom de lhte par QHostAddress::LocalHost.) Lappel de connectToHost() est asynchrone. Il rend toujours le contrle immdiatement. La connexion est gnralement tablie ultrieurement. Lobjet QTcpSocket met le signal connected() lorsque la connexion fonctionne ou error(QAbstractSocket::SocketError) en cas dchec. Nous mettons ensuite jour linterface utilisateur, en particulier en afchant la barre de progression. Nous dnissons enn la variable nextBlockSize en 0. Cette variable stocke la longueur du prochain bloc en provenance du serveur. Nous avons choisi dutiliser la valeur 0 pour indiquer que nous ne connaissons pas encore la taille du bloc venir.
void TripPlanner::sendRequest() { QByteArray block; QDataStream out(&block, QIODevice::WriteOnly); out.setVersion(QDataStream::Qt_4_1); out << quint16(0) << quint8(S) << fromComboBox->currentText() << toComboBox->currentText() << dateEdit->date() << timeEdit->time(); if (departureRadioButton->isChecked()) { out << quint8(D); } else { out << quint8(A); } out.device()->seek(0); out << quint16(block.size() - sizeof(quint16)); tcpSocket.write(block); statusLabel->setText(tr("Sending request...")); }

346

Qt4 et C++ : Programmation dinterfaces GUI

Le slot sendRequest() est excut lorsque lobjet QTcpSocket met le signal connected(), indiquant quune connexion a t tablie. La tche du slot consiste gnrer une requte destination du serveur, avec toutes les informations entres par lutilisateur. La requte est un bloc binaire au format suivant :
quint16 quint8 QString QString QDate QTime quint8 Taille du bloc en octets (excluant ce champ) Type de requte (toujours S) Ville de dpart Ville darrive Date du voyage Horaire approximatif du voyage Heure de dpart (D) ou darrive (A)

Dans un premier temps, nous crivons les donnes dans un QByteArray nomm block. Nous ne pouvons pas crire les donnes directement dans le QTcpSocket car nous ne connaissons pas la taille du bloc avant dy avoir plac toutes les donnes. Nous avons initialement indiqu 0 pour la taille du bloc, suivi du reste des donnes. Nous appelons ensuite seek(0) sur le priphrique dE/S (un QBuffer cr par QDataStream larrire-plan) pour revenir au dbut du tableau doctets, et nous remplaons le 0 initial par la taille des donnes du bloc. Elle est calcule en prenant la taille du bloc et en soustrayant sizeof(quint16) (cest--dire 2) pour exclure le champ de taille du compte doctets. Nous appelons alors write() sur le QTcpSocket pour envoyer le bloc au serveur.
void TripPlanner::updateTableWidget() { QDataStream in(&tcpSocket); in.setVersion(QDataStream::Qt_4_1); forever { int row = tableWidget->rowCount(); if (nextBlockSize == 0) { if (tcpSocket.bytesAvailable() < sizeof(quint16)) break; in >> nextBlockSize; } if (nextBlockSize == 0xFFFF) { closeConnection(); statusLabel->setText(tr("Found %1 trip(s)").arg(row)); break; }

Chapitre 14

Gestion de rseau

347

if (tcpSocket.bytesAvailable() < nextBlockSize) break; QDate date; QTime departureTime; QTime arrivalTime; quint16 duration; quint8 changes; QString trainType; in >> date >> departureTime >> duration >> changes >> trainType; arrivalTime = departureTime.addSecs(duration * 60); tableWidget->setRowCount(row + 1); QStringList fields; fields << date.toString(Qt::LocalDate) << departureTime.toString(tr("hh:mm")) << arrivalTime.toString(tr("hh:mm")) << tr("%1 hr %2 min").arg(duration / 60) .arg(duration % 60) << QString::number(changes) << trainType; for (int i = 0; i < fields.count(); ++i) tableWidget->setItem(row, i, new QTableWidgetItem(fields[i])); nextBlockSize = 0; } }

Le slot updateTableWidget() est connect au signal readyRead() de QTcpSocket, qui est mis ds que le QTcpSocket reoit de nouvelles donnes en provenance du serveur. Le serveur nous envoie une liste des trajets possibles correspondant aux critres de lutilisateur. Chaque trajet est expdi sous la forme dun bloc unique. La boucle forever est ncessaire dans la mesure o nous ne recevons pas obligatoirement un seul bloc de donnes la fois de la part du serveur. Nous pouvons recevoir un bloc entier, une partie de celui-ci, un bloc et demi ou encore tous les blocs la fois. (Voir Figure 14.2)
Figure 14.2 Les blocs de Trip Server
51
51 octets 48 octets 53 octets

data

48

data

53

data

0xFFFF

Comment fonctionne la boucle forever? Si la variable nextBlockSize a pour valeur 0, nous navons pas lu la taille du bloc suivant. Nous essayons de la lire (en prenant en compte le fait que deux octets au moins sont disponibles pour la lecture). Le serveur utilise une valeur de taille de 0xFFFF pour indiquer quil ne reste plus de donnes recevoir. Ainsi, si nous lisons cette valeur, nous savons que nous avons atteint la n.

348

Qt4 et C++ : Programmation dinterfaces GUI

Si la taille du bloc nest pas de 0xFFFF, nous essayons de lire le bloc suivant. Dans un premier temps, nous essayons de dterminer si des octets de taille de bloc sont disponibles la lecture. Si tel nest pas le cas, nous interrompons cette action un instant. Le signal readyRead() sera de nouveau mis lorsque des donnes supplmentaires seront disponibles. Nous procderons alors de nouvelles tentatives. Lorsque nous sommes certains que le bloc entier est arriv, nous pouvons utiliser loprateur >> en toute scurit sur le QDataStream pour extraire les informations relatives au voyage, et nous crons un QTableWidgetItems avec ces informations. Le format dun bloc reu du serveur est le suivant :
quint16 QDate QTime quint16 quint8 QString Taille du bloc en octets (en excluant son champ) Date de dpart Heure de dpart Dure (en minutes) Nombre de changements Type de train

A la n, nous rinitialisons la variable nextBlockSize en 0 pour indiquer que la taille du bloc suivant est inconnue et doit tre lue.
void TripPlanner::closeConnection() { tcpSocket.close(); searchButton->setEnabled(true); stopButton->setEnabled(false); progressBar->hide(); }

La fonction prive closeConnection() ferme la connexion avec le serveur TCP et met jour linterface utilisateur. Elle est appele depuis updateTableWidget() lorsque 0xFFFF est lu ainsi que depuis plusieurs autres slots sur lesquels nous reviendrons dans un instant.
void TripPlanner::stopSearch() { statusLabel->setText(tr("Search stopped")); closeConnection(); }

Le slot stopSearch() est connect au signal clicked() du bouton Stop. Il appelle simplement closeConnection().
void TripPlanner::connectionClosedByServer() {

Chapitre 14

Gestion de rseau

349

if (nextBlockSize!= 0xFFFF) statusLabel->setText(tr("Error: Connection closed by server")); closeConnection(); }

Le slot connectionClosedByServer() est connect au signal disconnected() du QTcpSocket. Si le serveur ferme la connexion sans que nous ayons encore reu le marqueur 0xFFFF de n de donnes, nous indiquons lutilisateur quune erreur sest produite. Nous appelons normalement closeConnection() pour mettre jour linterface utilisateur.
void TripPlanner::error() { statusLabel->setText(tcpSocket.errorString()); closeConnection(); }

Le slot error() est connect au signal error(QAbstractSocket::SocketError) du QTcpSocket. Nous ignorons le code derreur et nous utilisons QTcpSocket::errorString(), qui retourne un message en texte clair concernant la dernire erreur dtecte. Tout ceci concerne la classe TripPlanner. Comme nous pouvons nous y attendre, la fonction main() de lapplication TripPlanner est la suivante :
int main(int argc, char *argv[]) { QApplication app(argc, argv); TripPlanner tripPlanner; tripPlanner.show(); return app.exec(); }

Implmentons maintenant le serveur. Ce dernier est compos de deux classes : TripServer et ClientSocket. La classe TripServer hrite de QTcpServer, une classe qui nous permet daccepter des connexions TCP entrantes. ClientSocket rimplmente QTcpSocket et gre une connexion unique. Il existe chaque instant autant dobjets ClientSocket en mmoire que de clients servis.
class TripServer: public QTcpServer { Q_OBJECT public: TripServer(QObject *parent = 0); private: void incomingConnection(int socketId); };

La classe TripServer rimplmente la fonction incomingConnection() depuis QTcpServer. Cette fonction est appele ds quun client tente dtablir une connexion au port cout par le serveur.

350

Qt4 et C++ : Programmation dinterfaces GUI

TripServer::TripServer(QObject *parent) : QTcpServer(parent) { }

Le constructeur TripServer est simple.


void TripServer::incomingConnection(int socketId) { ClientSocket *socket = new ClientSocket(this); socket->setSocketDescriptor(socketId); }

Dans incomingConnection(), nous crons un objet ClientSocket qui est un enfant de lobjet TripServer, et nous attribuons son descripteur de socket le nombre qui nous a t fourni. Lobjet ClientSocket se supprimera automatiquement de lui-mme une fois la connexion termine.
class ClientSocket: public QTcpSocket { Q_OBJECT public: ClientSocket(QObject *parent = 0); private slots: void readClient(); private: void generateRandomTrip(const QString &from, const QString &to, const QDate &date, const QTime &time); quint16 nextBlockSize; };

La classe ClientSocket hrite de QTcpSocket et encapsule ltat dun client unique.


ClientSocket::ClientSocket(QObject *parent) : QTcpSocket(parent) { connect(this, SIGNAL(readyRead()), this, SLOT(readClient())); connect(this, SIGNAL(disconnected()), this, SLOT(deleteLater())); nextBlockSize = 0; }

Dans le constructeur, nous tablissons les connexions signal/slot ncessaires, et nous dnissons la variable nextBlockSize en 0, indiquant ainsi que nous ne connaissons pas encore la taille du bloc envoy par le client. Le signal disconnected() est connect deleteLater(), une fonction hrite de QObject qui supprime lobjet lorsque le contrle retourne la boucle dvnement de Qt. De cette faon, lobjet ClientSocket est supprim lorsque la connexion du socket est ferme.

Chapitre 14

Gestion de rseau

351

void ClientSocket::readClient() { QDataStream in(this); in.setVersion(QDataStream::Qt_4_1); if (nextBlockSize == 0) { if (bytesAvailable() < sizeof(quint16)) return; in >> nextBlockSize; } if (bytesAvailable() < nextBlockSize) return; quint8 requestType; QString from; QString to; QDate date; QTime time; quint8 flag; in >> requestType; if (requestType == S) { in >> from >> to >> date >> time >> flag; srand(from.length() * 3600 + to.length() * 60 + time.hour()); int numTrips = rand() % 8; for (int i = 0; i < numTrips; ++i) generateRandomTrip(from, to, date, time); QDataStream out(this); out << quint16(0xFFFF); } close(); }

Le slot readClient() est connect au signal readyRead() du QTcpSocket. Si nextBlockSize est dni en 0, nous commenons par lire la taille du bloc. Dans le cas contraire, nous lavons dj lue. Nous poursuivons donc en vriant si un bloc entier est arriv, et nous le lisons dune seule traite. Nous utilisons QDataStream directement sur le QTcpSocket (lobjet this) et lisons les champs au moyen de loprateur >>. Une fois la requte du client lue, nous sommes prts gnrer une rponse. Dans le cas dune application relle, nous rechercherions les informations dans une base de donnes dhoraires et tenterions de trouver les trajets correspondants. Ici, nous nous contenterons dune fonction nomme generateRandomTrip() qui gnrera un trajet alatoire. Nous appelons la fonction un nombre quelconque de fois, puis nous envoyons 0xFFFF pour signaler la n des donnes. Enn, nous fermons la connexion.
void ClientSocket::generateRandomTrip(const QString & /* from */, const QString & /* to */, const QDate &date, const QTime &time)

352

Qt4 et C++ : Programmation dinterfaces GUI

{ QByteArray block; QDataStream out(&block, QIODevice::WriteOnly); out.setVersion(QDataStream::Qt_4_1); quint16 duration = rand() % 200; out << quint16(0) << date << time << duration << quint8(1) << QString("InterCity"); out.device()->seek(0); out << quint16(block.size() - sizeof(quint16)); write(block); }

La fonction generateRandomTrip() illustre comment envoyer un bloc de donnes par le biais dune connexion TCP. Ce processus est trs similaire celui suivi sur le client, dans la fonction sendRequest(). Une fois encore, nous crivons le bloc dans un QByteArray en excutant write(), de faon pouvoir dterminer sa taille avant de lenvoyer.
int main(int argc, char *argv[]) { QApplication app(argc, argv); TripServer server; if (!server.listen(QHostAddress::Any, 6178)) { cerr << "Failed to bind to port" << endl; return 1; } QPushButton quitButton(QObject::tr("&Quit")); quitButton.setWindowTitle(QObject::tr("Trip Server")); QObject::connect(&quitButton, SIGNAL(clicked()), &app, SLOT(quit())); quitButton.show(); return app.exec(); }

Dans main(), nous crons un objet TripServer et un QPushButton qui permet lutilisateur darrter le serveur. Nous lanons le serveur en appelant QTcpSocket::listen(), qui reoit ladresse IP et le numro de port sur lequel nous souhaitons accepter les connexions. Ladresse spciale 0.0.0.0 (QHostAddress::Any) signie toute interface IP prsente sur lhte local. Ceci termine notre exemple client/serveur. Ici, nous avons utilis un protocole orient bloc qui nous permet de faire appel QDataStream pour la lecture et lcriture. Si nous souhaitions utiliser un protocole orient ligne, lapproche la plus simple serait de recourir aux fonctions canReadLine() et readLine() de QTcpSocket dans un slot connect au signal readyRead():
QStringList lines; while (tcpSocket.canReadLine()) lines.append(tcpSocket.readLine());

Chapitre 14

Gestion de rseau

353

Nous traiterions alors chaque ligne lue. Comme pour lenvoi des donnes, ceci pourrait tre effectu en utilisant un QTextStream sur le QTcpSocket. Limplmentation serveur que nous avons utilise nest pas adapte une situation o les connexions sont nombreuses. En effet, lorsque nous traitons une requte, nous ne grons pas les autres connexions. Une approche plus souple consisterait dmarrer un nouveau thread pour chaque connexion. Lexemple Threaded Fortune Server situ dans le rpertoire examples/ network/threadedfortuneserver illustre ce procd.

Envoi et rception de datagrammes UDP


La classe QUdpSocket peut tre utilise pour envoyer et recevoir des datagrammes UDP. UDP est un protocole orient datagramme non able. Certains protocoles de niveau application utilisent UDP car il est plus lger que TCP. Avec UDP, les donnes sont envoyes sous forme de paquets (datagrammes) dun hte un autre. Il nexiste pas de concept de connexion, et si un paquet UDP nest pas remis avec succs, aucune erreur nest signale lexpditeur. (Voir Figure 14.3)
Figure 14.3 Lapplication Weather Station

Les exemples Weather Balloon et Weather Station vous montreront comment utiliser UDP partir dune application Qt. Lapplication Weather Balloon reproduit un ballon mto qui envoie un datagramme UDP (au moyen dune connexion sans l) contenant les conditions atmosphriques courantes toutes les deux secondes. Lapplication Weather Station reoit ces datagrammes et les afche lcran. Nous allons commencer par le code du Weather Ballon.
class WeatherBalloon: public QPushButton { Q_OBJECT public: WeatherBalloon(QWidget *parent = 0); double temperature() const; double humidity() const;

354

Qt4 et C++ : Programmation dinterfaces GUI

double altitude() const; private slots: void sendDatagram(); private: QUdpSocket udpSocket; QTimer timer; };

La classe WeatherBalloon hrite de QPushButton. Elle utilise sa variable prive QUdpSocket pour communiquer avec la station mto (Weather Station).
WeatherBalloon::WeatherBalloon(QWidget *parent) : QPushButton(tr("Quit"), parent) { connect(this, SIGNAL(clicked()), this, SLOT(close())); connect(&timer, SIGNAL(timeout()), this, SLOT(sendDatagram())); timer.start(2 * 1000); setWindowTitle(tr("Weather Balloon")); }

Dans le constructeur, nous lanons un QTimer pour invoquer sendDatagram() toutes les deux secondes.
void WeatherBalloon::sendDatagram() { QByteArray datagram; QDataStream out(&datagram, QIODevice::WriteOnly); out.setVersion(QDataStream::Qt_4_1); out << QDateTime::currentDateTime() << temperature() << humidity() << altitude(); udpSocket.writeDatagram(datagram, QHostAddress::LocalHost, 5824); }

Dans sendDatagram(), nous gnrons et envoyons un datagramme contenant la date, lheure, la temprature, lhumidit et laltitude :
QDateTime double double double Date et heure de mesure Temprature (en C) Humidit (en %) Altitude (in mtres)

Chapitre 14

Gestion de rseau

355

Le datagramme est expdi au moyen de QUdpSocket::writeDatagram(). Les deuxime et troisime arguments de writeDatagram() sont ladresse IP et le numro de port de lhomologue (la Weather Station). Nous supposons ici que la Weather Station sexcute sur la mme machine que le Weather Balloon. Nous utilisons donc ladresse IP 127.0.0.1 (QHostAddress::LocalHost), une adresse spciale qui dsigne lhte local. Contrairement aux sous-classes de QAbstractSocket, QUdpSocket accepte uniquement les adresses dhte, mais pas les noms. Si nous devions convertir un nom dhte en son adresse IP, deux solutions soffriraient nous : si nous nous sommes prpars un blocage pendant la recherche, nous pouvons faire appel la fonction statique QHostInfo::fromName(). Dans le cas contraire, nous employons la fonction statique QHostInfo::lookupHost(), qui rend le contrle immdiatement et, une fois la recherche termine, appelle le slot qui lui est transmis avec un objet QHostInfo contenant les adresses correspondantes.
int main(int argc, char *argv[]) { QApplication app(argc, argv); WeatherBalloon balloon; balloon.show(); return app.exec(); }

La fonction main() cre simplement un objet WeatherBalloon, qui sert la fois dhomologue UDP et de QPushButton lcran. En cliquant sur le QPushButton, lutilisateur quitte lapplication. Revenons maintenant au code source du client Weather Station.
class WeatherStation: public QDialog { Q_OBJECT public: WeatherStation(QWidget *parent = 0); private slots: void processPendingDatagrams(); private: QUdpSocket udpSocket; QLabel *dateLabel; QLabel *timeLabel; QLineEdit *altitudeLineEdit; };

La classe WeatherStation hrite de QDialog. Elle coute un port UDP particulier, analyse tous les datagrammes entrants (en provenance du Weather Balloon) et afche leur contenu dans cinq QLineEdits en lecture seulement. La seule variable prive prsentant un intrt ici

356

Qt4 et C++ : Programmation dinterfaces GUI

est la variable udpSocket du type QUdpSocket, laquelle nous allons faire appel pour recevoir les datagrammes.
WeatherStation::WeatherStation(QWidget *parent) : QDialog(parent) { udpSocket.bind(5824); connect(&udpSocket, SIGNAL(readyRead()), this, SLOT(processPendingDatagrams())); }

Dans le constructeur, nous commenons par tablir une liaison entre le QUdpSocket et le port auquel le Weather Balloon transmet ses donnes. Comme nous navons pas spci dadresse hte, le socket accepte les datagrammes envoys nimporte quelle adresse IP appartenant la machine sur laquelle sexcute la Weather Station. Puis nous connectons le signal readyRead() du socket au processPendingDatagrams() priv qui extrait les donnes et les afche.
void WeatherStation::processPendingDatagrams() { QByteArray datagram; do { datagram.resize(udpSocket.pendingDatagramSize()); udpSocket.readDatagram(datagram.data(), datagram.size()); } while (udpSocket.hasPendingDatagrams()); QDateTime dateTime; double temperature; double humidity; double altitude; QDataStream in(&datagram, QIODevice::ReadOnly); in.setVersion(QDataStream::Qt_4_1); in >> dateTime >> temperature >> humidity >> altitude; dateLineEdit->setText(dateTime.date().toString()); timeLineEdit->setText(dateTime.time().toString()); temperatureLineEdit->setText(tr("%1 C").arg(temperature)); humidityLineEdit->setText(tr("%1%").arg(humidity)); altitudeLineEdit->setText(tr("%1 m").arg(altitude)); }

Le slot processPendingDatagrams() est appel quand un datagramme est arriv. QUdpSocket place les datagrammes entrants en le dattente et nous permet dy accder un par un. Normalement, il ne devrait y avoir quun seul datagramme, mais nous ne pouvons pas exclure la possibilit que lexpditeur en envoie plusieurs la fois avant que le signal readyRead() ne soit mis. Dans ce cas, nous les ignorons tous, lexception du dernier. Les prcdents vhiculent en effet des informations obsoltes.

Chapitre 14

Gestion de rseau

357

La fonction pendingDatagramSize() retourne la taille du premier datagramme en attente. Du point de vue de lapplication, les datagrammes sont toujours envoys et reus sous la forme dune unit de donnes unique. Ainsi, si des octets quelconques sont disponibles, le datagramme entier peut tre lu. Lappel de readDatagram() copie le contenu du premier datagramme en attente dans la mmoire tampon char* spcie (en troquant les donnes si la capacit de cette mmoire nest pas sufsante) et passe au datagramme suivant en attente. Une fois tous les datagrammes lus, nous dcomposons le dernier (celui avec les mesures atmosphriques les plus rcentes) et alimentons le QLineEdits avec les nouvelles donnes.
int main(int argc, char *argv[]) { QApplication app(argc, argv); WeatherStation station; station.show(); return app.exec(); }

Enn, nous crons et afchons la WeatherStation dans main(). Nous en avons maintenant termin avec notre metteur et destinataire UDP. Les applications sont aussi simples que possible, puisque le Weather Balloon envoie des datagrammes la Weather Station qui les reoit. Dans la plupart des cas du monde rel, les deux applications auraient besoin deffectuer des oprations de lecture et dcriture sur leur socket. Un numro de port et une adresse hte peuvent tre transmis aux fonctions QUdpSocket::writeDatagram(), de sorte que le QUdpSocket puisse raliser une lecture depuis lhte et le port auquel il est li avec bind(), et effectuer une opration de lecture vers un autre hte ou port.

15
XML
Au sommaire de ce chapitre Lire du code XML avec SAX Lire du code XML avec DOM Ecrire du code XML

XML (Extensible Markup Language) est un format de chier texte polyvalent, populaire pour lchange et le stockage des donnes. Qt fournit deux API distinctes faisant partie du module QtXml pour la lecture de documents XML : SAX (Simple API for XML) rapporte des "vnements danalyse" directement lapplication par le biais de fonctions virtuelles. DOM (Document Object Model) convertit une documentation XML en une structure arborescente, que lapplication peut parcourir. Trois facteurs principaux sont prendre en compte lors du choix entre DOM et SAX pour une application particulire. SAX est de niveau infrieur et gnralement plus rapide, ce qui le rend particulirement appropri pour des tches simples (telles que la recherche de toutes les occurrences dune balise donne dans un document XML) ou

360

Qt4 et C++ : Programmation dinterfaces GUI

pour la lecture de chiers de trs grande taille pour lesquels la mmoire sera insufsante. Mais pour de nombreuses applications, la commodit de DOM prime sur la vitesse potentielle et les avantages offerts par SAX concernant la mmoire. Pour crire des chiers XML, deux options sont disponibles : nous pouvons gnrer le code XML manuellement, ou reprsenter les donnes sous la forme dun arbre DOM en mmoire et demander ce dernier de scrire par lui-mme dans un chier.

Lire du code XML avec SAX


SAX est une API standard de domaine public destine la lecture de documents XML. Les classes SAX de Qt sont modeles sur limplmentation SAX2 de Java, avec quelques diffrences dans lattribution des noms an de sadapter aux conventions de Qt. Pour plus dinformations concernant SAX, reportez-vous ladresse http://www.saxproject.org/. Qt fournit un analyseur XML bas sur SAX nomm QXmlSimpleReader. Cet analyseur reconnat du code XML bien form et prend en charge les espaces de noms XML. Quand il parcourt le document, il appelle les fonctions virtuelles des classes gestionnaires enregistres pour signaler des vnements danalyse. (Ces "vnements danalyse" nont aucun rapport avec les vnements Qt, tels que les vnements touche et souris.) Supposons, par exemple que lanalyseur examine le document XML suivant :
<doc> <quote>Ars longa vita brevis</quote> </doc>

Il appelle les gestionnaires dvnements danalyse ci-aprs :


startDocument() startElement("doc") startElement("quote") characters("Ars longa vita brevis") endElement("quote") endElement("doc") endDocument()

Ces fonctions sont toutes dclares dans QXmlContentHandler. Pour des questions de simplicit, nous avons omis certains arguments de startElement() et endElement().

QXmlContentHandler est juste lune des nombreuses classes gestionnaires susceptible dtre utilise avec QXmlSimpleReader. Les autres sont QXmlEntityResolver, QXmlDTDHandler, QXmlErrorHandler, QXmlDeclHandler et QXmlLexicalHandler. Ces classes ne dclarent que des fonctions purement virtuelles et fournissent des informations concernant les diffrents types dvnements danalyse. Pour la plupart des applications, QXmlContentHandler et QXmlErrorHandler sont les deux seules ncessaires.

Chapitre 15

XML

361

Pour des raisons de commodit, Qt fournit aussi QXmlDefaultHandler, une classe qui hrite de toutes les classes gestionnaires et qui fournit des implmentations simples de toutes les fonctions. Une telle conception, avec de nombreuses classes gestionnaires abstraites et une sous-classe simple, est inhabituelle pour Qt. Elle a t adopte pour se conformer limplmentation de modle Java. Nous allons tudier un exemple qui illustre comment utiliser QXmlSimpleReader et QXmlDefaultHandler pour analyser un chier XML ad hoc et afcher son contenu dans un QTreeWidget. La sous-classe QXmlDefaultHandler se nomme SaxHandler, et le format gr par celle-ci est celui dun index de livre, avec les entres et les sous-entres.
Figure 15.1 Arbre dhritage pour SaxHandler
QXmlContentHandler QXmlDTDHandler QXmlLexicalHandler QXmlDeclHandler

QXmlErrorHandler

QXmlEntityResolver

QXmlDefaultHandler SaxHandler

Voici le chier dindex qui est afch dans le QTreeWidget en Figure 15.2 :
<?xml version="1.0"?> <bookindex> <entry term="sidebearings"> <page>10</page> <page>34-35</page> <page>307-308</page> </entry> <entry term="subtraction"> <entry term="of pictures"> <page>115</page> <page>244</page> </entry> <entry term="of vectors"> <page>9</page> </entry> </entry> </bookindex>

Figure 15.2 Un chier dindex afch dans un QTreeWidget

362

Qt4 et C++ : Programmation dinterfaces GUI

La premire tape dans limplmentation de lanalyseur consiste dnir la sous-classe QXmlDefaultHandler:


class SaxHandler: public QXmlDefaultHandler { public: SaxHandler(QTreeWidget *tree); bool startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &attributes); bool endElement(const QString &namespaceURI, const QString &localName, const QString &qName); bool characters(const QString &str); bool fatalError(const QXmlParseException &exception); private: QTreeWidget *treeWidget; QTreeWidgetItem *currentItem; QString currentText; };

La classe SaxHandler hrite de QXmlDefaultHandler et rimplmente quatre fonctions : startElement(), endElement(), characters() et fatalError(). Les trois premires sont dclares dans QXmlContentHandler. La dernire est dclare dans QXmlErrorHandler.
SaxHandler::SaxHandler(QTreeWidget *tree) { treeWidget = tree; currentItem = 0; }

Le constructeur SaxHandler accepte le QTreeWidget que nous souhaitons remplir avec les informations stockes dans le chier XML.
bool SaxHandler::startElement(const QString & /* namespaceURI */, const QString & /* localName */, const QString &qName, const QXmlAttributes &attributes) { if (qName == "entry") { if (currentItem) { currentItem = new QTreeWidgetItem(currentItem); } else { currentItem = new QTreeWidgetItem(treeWidget); }

Chapitre 15

XML

363

currentItem->setText(0, attributes.value("term")); } else if (qName == "page") { currentText.clear(); } return true; }

La fonction startElement() est appele ds que le lecteur rencontre une nouvelle balise douverture. Le troisime paramtre est le nom de la balise (ou plus prcisment, son "nom quali"). Le quatrime paramtre est la liste des attributs. Dans cet exemple, nous ignorons les premier et deuxime paramtres. Ils sont utiles pour les chiers XML qui utilisent le mcanisme despace de noms de XML, un sujet qui est trait en dtail dans la documentation de rfrence. Si la balise est <entry>, nous crons un nouvel lment QTreeWidget. Si elle est imbrique dans une autre balise <entry>, la nouvelle balise dnit une sous-entre dans lindex, et le nouveau QTreeWidgetItem est cr en tant quenfant du QTreeWidgetItem qui reprsente lentre principale. Dans le cas contraire, nous crons le QTreeWidgetItem avec llment treeWidget en tant que parent, en faisant de celui-ci un lment de haut niveau. Nous appelons setText() pour dnir le texte prsent en colonne 0 avec la valeur de lattribut term de la balise <entry>. Si la balise est <page>, nous dnissons le currentText en une chane vide. Le currentText sert daccumulateur pour le texte situ entre les balises <page> et </page>. Nous retournons enn true pour demander SAX de poursuivre lanalyse du chier. Si nous souhaitions signaler les balises inconnues comme des erreurs, nous retournerions false dans ces situations. Nous rimplmenterions galement errorString() partir de QXmlDefaultHandler pour retourner un message derreur appropri.
bool SaxHandler::characters(const QString &str) { currentText += str; return true; }

La fonction characters() est appele si des donnes caractres sont rencontres dans le document XML. Nous accolons simplement les caractres la variable currentText.
bool SaxHandler::endElement(const QString & /* namespaceURI */, const QString & /* localName */, const QString &qName) { if (qName == "entry") { currentItem = currentItem->parent(); } else if (qName == "page") { if (currentItem) { QString allPages = currentItem->text(1);

364

Qt4 et C++ : Programmation dinterfaces GUI

if (!allPages.isEmpty()) allPages += ", "; allPages += currentText; currentItem->setText(1, allPages); } } return true; }

La fonction endElement() est appele quand le lecteur rencontre une balise de fermeture. Comme pour startElement(), le troisime paramtre est le nom de la balise. Si la balise est </entry>, nous mettons jour la variable prive currentItem de faon la diriger vers le parent de QTreeWidgetItem en cours. De cette faon, la variable currentItem reprend la valeur qui tait la sienne avant la lecture de la balise <entry> correspondante. Si la balise est </page>, nous ajoutons le numro de page ou la plage de pages sous la forme dune liste spare par des virgules au texte de llment courant de la colonne 1.
bool SaxHandler::fatalError(const QXmlParseException &exception) { QMessageBox::warning(0, QObject::tr("SAX Handler"), QObject::tr("Parse error at line %1, column " "%2:\n%3.") .arg(exception.lineNumber()) .arg(exception.columnNumber()) .arg(exception.message())); return false; }

La fonction fatalError() est appele lorsque le lecteur ne parvient pas analyser le chier XML. Dans cette situation, nous afchons simplement une bote de message, en donnant le numro de ligne, le numro de colonne et le texte derreur de lanalyseur. Ceci termine limplmentation de la classe SaxHandler. Voyons maintenant comment lutiliser :
bool parseFile(const QString &fileName) { QStringList labels; labels << QObject::tr("Terms") << QObject::tr("Pages"); QTreeWidget *treeWidget = new QTreeWidget; treeWidget->setHeaderLabels(labels); treeWidget->setWindowTitle(QObject::tr("SAX Handler")); treeWidget->show();

Chapitre 15

XML

365

QFile file(fileName); QXmlInputSource inputSource(&file); QXmlSimpleReader reader; SaxHandler handler(treeWidget); reader.setContentHandler(&handler); reader.setErrorHandler(&handler); return reader.parse(inputSource); }

Nous dnissons un QTreeWidget avec deux colonnes. Puis nous crons un objet QFile pour le chier devant tre lu et un QXmlSimpleReader pour analyser le chier. Il nest pas ncessaire douvrir le QFile par nous-mmes. QXmlInputSource sen charge automatiquement. Nous crons enn un objet SaxHandler, nous linstallons sur le lecteur la fois en tant que gestionnaire de contenu et en tant que gestionnaire derreur, et nous appelons parse() sur le lecteur pour effectuer lanalyse. Au lieu de transmettre un simple objet de chier la fonction parse(), nous transmettons un QXmlInputSource. Cette classe ouvre le chier qui lui est fourni, le lit (en prenant en considration tout codage de caractres spci dans la dclaration <?xml?>), et fournit une interface par le biais de laquelle lanalyseur lit le chier. Dans SaxHandler, nous rimplmentons uniquement les fonctions des classes QXmlContentHandler et QXmlErrorHandler. Si nous avions implment des fonctions dautres classes gestionnaires, nous aurions galement d appeler leurs fonctions de rglage (set) sur le lecteur. Pour lier lapplication la bibliothque QtXml, nous devons ajouter cette ligne dans le chier .pro:
QT += xml

Lire du code XML avec DOM


DOM est une API standard pour lanalyse de code XML dveloppe par le W3C (World Wide Web Consortium). Qt fournit une implmentation DOM Niveau 2 destine la lecture, la manipulation et lcriture de documents XML. DOM prsente un chier XML sous la forme dun arbre en mmoire. Nous pouvons parcourir larbre DOM autant que ncessaire. Il nous est galement possible de le modier et de le renregistrer sur le disq