You are on page 1of 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 disque en tant que chier XML. Considrons le document XML suivant :
<doc> <quote>Ars longa vita brevis</quote> <translation>Art is long, life is short</translation> </doc>

366

Qt4 et C++ : Programmation dinterfaces GUI

Il correspond larbre DOM ci-aprs :


Document Element (doc) Element (quote) Text (Ars longa vita brevis) Element (translation) Text (Art is long, life is short)

Larbre DOM contient des nuds de types diffrents. Par exemple, un nud Element correspond une balise douverture et sa balise de fermeture. Le matriau situ entre les balises apparat sous la forme de nuds enfants de Element. Dans Qt, les types de nud (comme toutes les autres classes en liaison avec DOM) possdent un prxe QDom. Ainsi, QDomElement reprsente un nud Element, et QDomText reprsente un nud Text. Chaque nud peut possder diffrents types de nuds enfants. Par exemple, un nud Element peut contenir dautres nuds Element, ainsi que des nuds EntityReference, Text, CDATASection, ProcessingInstruction et Comment. La Figure 15.3 prsente les types de nuds enfants correspondant aux nuds parents. Ceux apparaissant en gris ne peuvent pas avoir de nud enfant.
Document Attr

Element

Document Type

Processing Instruction

Comment

Entity Reference

Text

Document Fragment

Element

Entity Reference

Entity

Element

Entity Reference

Text

CDATA Section

Processing Instruction

Comment

Figure 15.3 Relations parent/enfant entre les nuds DOM

Nous allons voir comment utiliser DOM pour lire des chiers XML en crant un analyseur pour le format de chier dindex dcrit dans la section prcdente.

Chapitre 15

XML

367

class DomParser { public: DomParser(QIODevice *device, QTreeWidget *tree); private: void parseEntry(const QDomElement &element, QTreeWidgetItem *parent); QTreeWidget *treeWidget; };

Nous dnissons une classe nomme DomParser qui analysera un index de livre se prsentant sous la forme dun document XML et afchera le rsultat dans un QTreeWidget. Cette classe nhrite daucune autre classe.
DomParser::DomParser(QIODevice *device, QTreeWidget *tree) { treeWidget = tree; QString errorStr; int errorLine; int errorColumn; QDomDocument doc; if (!doc.setContent(device, true, &errorStr, &errorLine, &errorColumn)) { QMessageBox::warning(0, QObject::tr("DOM Parser"), QObject::tr("Parse error at line %1, " "column %2:\n%3") .arg(errorLine) .arg(errorColumn) .arg(errorStr)); return; } QDomElement root = doc.documentElement(); if (root.tagName()!= "bookindex") return; QDomNode node = root.firstChild(); while (!node.isNull()) { if (node.toElement().tagName() == "entry") parseEntry(node.toElement(), 0); node = node.nextSibling(); } }

Dans le constructeur, nous crons un objet QDomDocument et appelons setContent() sur celui-ci pour lamener lire le document XML fourni par le QIODevice. La fonction setContent() ouvre automatiquement le priphrique si ce nest dj fait. Nous appelons ensuite documentElement() sur le QDomDocument pour obtenir son enfant QDomElement

368

Qt4 et C++ : Programmation dinterfaces GUI

unique, et nous vrions sil sagit bien dun lment <bookindex>. Nous parcourons tous les nuds enfants, et si le nud est un lment <entry>, nous appelons parseEntry() pour lanalyser. La classe QDomNode peut stocker tout type de nud. Si nous souhaitons traiter un nud de faon plus prcise, nous devons tout dabord le convertir en un type de donne correct. Dans cet exemple, nous ne nous proccupons que des nuds Element. Nous appelons donc toElement() sur le QDomNode pour le convertir en un QDomElement, puis nous appelons tagName() an de rcuprer le nom de balise de llment. Si le nud nest pas du type Element, la fonction toElement() retourne un objet QDomElement nul, avec un nom de balise vide.
void DomParser::parseEntry(const QDomElement &element, QTreeWidgetItem *parent) { QTreeWidgetItem *item; if (parent) { item = new QTreeWidgetItem(parent); } else { item = new QTreeWidgetItem(treeWidget); } item->setText(0, element.attribute("term")); QDomNode node = element.firstChild(); while (!node.isNull()) { if (node.toElement().tagName() == "entry") { parseEntry(node.toElement(), item); } else if (node.toElement().tagName() == "page") { QDomNode childNode = node.firstChild(); while (!childNode.isNull()) { if (childNode.nodeType() == QDomNode::TextNode) { QString page = childNode.toText().data(); QString allPages = item->text(1); if (!allPages.isEmpty()) allPages += ", "; allPages += page; item->setText(1, allPages); break; } childNode = childNode.nextSibling(); } } node = node.nextSibling(); } }

Dans parseEntry(), nous crons un 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

Chapitre 15

XML

369

principale. Dans le cas contraire, nous crons le QTreeWidgetItem avec 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 en la valeur de lattribut term de la balise <entry>. Une fois le QTreeWidgetItem initialis, nous parcourons les nuds enfants du QDomElement correspondant la balise <entry> courante. Si llment est <entry>, nous appelons parseEntry() avec llment courant en tant que deuxime argument. Le QTreeWidgetItem de la nouvelle entre sera alors cr avec le QTreeWidgetItem de lentre principale en tant que parent. Si llment est <page>, nous parcourons la liste enfant de llment la recherche dun nud Text. Une fois celui-ci trouv, nous appelons Text() pour le convertir en un objet QDomText, et data() pour extraire le texte en tant que QString. Puis nous ajoutons le texte la liste de numros de page dlimite par des virgules dans la colonne 1 du QTreeWidgetItem. Voyons comment utiliser la classe DomParser pour analyser un chier :
void parseFile(const QString &fileName) { QStringList labels; labels << QObject::tr("Terms") << QObject::tr("Pages"); QTreeWidget *treeWidget = new QTreeWidget; treeWidget->setHeaderLabels(labels); treeWidget->setWindowTitle(QObject::tr("DOM Parser")); treeWidget->show(); QFile file(fileName); DomParser(&file, treeWidget); }

Nous commenons par dnir un QTreeWidget. Puis nous crons un QFile et un DomParser. Une fois le DomParser construit, il analyse le chier et alimente larborescence. Comme dans lexemple prcdent, nous avons besoin de la ligne suivante dans le chier .pro de lapplication pour tablir un lien avec la bibliothque QtXml :
QT += xml

Comme le montre lexemple, lopration consistant parcourir un arbre DOM peut savrer assez lourde. La simple extraction du texte entre les balises <page> et </page> a ncessit le parcours dune liste de QDomNode au moyen de firstChild() et de nextSibling(). Les programmeurs qui utilisent beaucoup DOM crivent souvent leurs propres fonctions conteneur de haut niveau an de simplier les oprations courantes, telles que lextraction de texte entre les balises douverture et de fermeture.

370

Qt4 et C++ : Programmation dinterfaces GUI

Ecrire du code XML


Deux approches soffrent vous pour gnrer des chiers XML partir dapplications Qt :

gnrer un arbre DOM et appeler save() sur celui-ci ; gnrer le code XML manuellement.

Le choix entre ces approches est souvent indpendant du fait que nous utilisions SAX ou DOM pour la lecture des documents XML. Voici un extrait de code qui illustre comment crer un arbre DOM et lcrire au moyen dun QTextStream:
const int Indent = 4; QDomDocument doc; QDomElement root = doc.createElement("doc"); QDomElement quote = doc.createElement("quote"); QDomElement translation = doc.createElement("translation"); QDomText latin = doc.createTextNode("Ars longa vita brevis"); QDomText english = doc.createTextNode("Art is long, life is short"); doc.appendChild(root); root.appendChild(quote); root.appendChild(translation); quote.appendChild(latin); translation.appendChild(english); QTextStream out(&file); doc.save(out, Indent);

Le deuxime argument de save() est la taille du retrait utiliser. Une valeur diffrente de zro facilite la lecture du contenu du chier. Voici la sortie du chier XML :
<doc> <quote>Ars longa vita brevis</quote> <translation>Art is long, life is short</translation> </doc>

Un autre scnario se produit dans les applications qui utilisent larbre DOM comme structure de donnes primaire. Ces applications effectuent gnralement des oprations de lecture dans des documents XML en utilisant DOM, puis modient larbre DOM en mmoire et appellent nalement save() pour convertir de nouveau larbre vers XML. Par dfaut, QDomDocument::save() utilise lencodage UTF-8 pour le chier gnr. Nous pouvons utiliser un autre encodage en ajoutant au dbut de larbre DOM une dclaration XML telle que :
<?xml version="1.0" encoding="ISO-8859-1"?>

Chapitre 15

XML

371

Lextrait de code suivant vous montre comment y parvenir :


QTextStream out(&file); QDomNode xmlNode = doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"ISO-8859-1\""); doc.insertBefore(xmlNode, doc.firstChild()); doc.save(out, Indent);

La gnration manuelle de chiers XML nest pas beaucoup plus difcile quavec DOM. Nous pouvons employer QTextStream et crire les chanes comme nous le ferions avec tout autre chier texte. La partie la plus dlicate est de neutraliser linterprtation des caractres spciaux qui apparaissent dans le texte et les valeurs dattribut. La fonction Qt::escape() neutralise les caractres <, > et &. Voici un extrait de code qui fait appel cette fonction :
QTextStream out(&file); out.setCodec("UTF-8"); out << "<doc>\n" << " <quote>" << Qt::escape(quoteText) << "</quote>\n" << " <translation>" << Qt::escape(translationText) << "</translation>\n" << "</doc>\n";

Larticle "Generating XML" disponible ladresse http://doc.trolltech.com/qq/qq05-generating-xml.html prsente une classe trs simple qui facilite la gnration de chiers XML. Cette classe prend en charge les dtails tels que les caractres spciaux, le retrait et les problmes dencodage, nous permettant de nous concentrer librement sur le code XML gnrer. La classe a t conue pour fonctionner avec Qt 3, mais il nest pas difcile de ladapter Qt 4.

16
Aide en ligne
Au sommaire de ce chapitre Infobulles, informations dtat et aide "Quest-ce que cest ?" Utiliser QTextBrowser comme moteur daide simple Utiliser lAssistant Qt pour une aide en ligne puissante

La plupart des applications fournissent une aide en ligne leurs utilisateurs. Certaines indications apparaissent sous une forme brve, telles que les infobulles, les informations dtat et "Quest-ce que cest ?". Qt prend naturellement en charge toutes ces informations. Il existe galement un autre type daide, beaucoup plus approfondi, qui implique de nombreuses pages de texte. Dans ce cas, nous pouvons utiliser QTextBrowser en tant que navigateur daide simple. Il est galement possible dinvoquer lAssistant Qt ou un navigateur HTML depuis notre application.

374

Qt4 et C++ : Programmation dinterfaces GUI

Infobulles, informations dtat et aide "Quest-ce que cest ?"


Une infobulle est un petit texte qui apparat lorsque la souris survole un widget. Les infobulles sont prsentes sous la forme de texte noir sur un arrire-plan jaune. Leur objectif principal est de fournir des descriptions textuelles de boutons des barres doutils. Nous pouvons ajouter des infobulles des widgets arbitraires dans le code au moyen de QWidget::setToolTip(). Par exemple :
findButton->setToolTip(tr("Find next"));

Pour dnir linfobulle dun QAction que vous ajoutez un menu ou une barre doutils, nous appelons simplement setToolTip() sur laction. Par exemple :
newAction = new QAction(tr("&New"), this); newAction->setToolTip(tr("New document"));

Si nous ne dnissons pas explicitement une infobulle, QAction utilise automatiquement le texte de laction. Une information dtat est galement un texte descriptif bref, mais gnralement un peu plus long que celui dune infobulle. Lorsque la souris survole un bouton de barre doutils ou une option de menu, une information dtat apparat dans la barre dtat. (Voir Figure 16.1) Appelez setStatusTip() pour ajouter une information dtat une action ou un widget :
newAction->setStatusTip(tr("Create a new document"));

Figure 16.1 Une application afchant une infobulle et une information dtat

Chapitre 16

Aide en ligne

375

Dans certaines situations, il est souhaitable de fournir une information plus complte que celle offerte par les infobulles et les indicateurs dtat. Nous pouvons, par exemple, afcher une bote de dialogue complexe contenant un texte explicatif concernant chaque champ, ceci sans forcer lutilisateur invoquer une fentre daide distincte. Le mode "Quest-ce que cest ?" reprsente une solution idale dans cette situation. Quand une fentre se trouve en mode "Quest-ce que cest ?", le curseur se transforme en et lutilisateur peut cliquer sur tout composant de linterface utilisateur pour obtenir le texte daide le concernant. Pour entrer en mode "Quest-ce que cest ?", lutilisateur peut soit cliquer sur le bouton ? dans la barre de titre de la fentre (sous Windows et KDE), soit appuyer sur Maj+F1. Voici un exemple de texte "Quest-ce que cest ?" :
dialog->setWhatsThis(tr("<img src=\":/images/icon.png\">" "&nbsp;The meaning of the Source field depends " "on the Type field:" "<ul>" "<li><b>Books</b> have a Publisher" "<li><b>Articles</b> have a Journal name with " "volume and issue number" "<li><b>Theses</b> have an Institution name " "and a Department name" "</ul>"));

Nous pouvons utiliser les balises HTML pour mettre en forme le texte dinformation dun "Quest-ce que cest ?". Dans lexemple fourni, nous incluons une image (qui est rpertorie dans le chier de ressources de lapplication), une liste puces et du texte en gras. (Voir Figure 16.2.) Vous trouverez les balises et attributs pris en charge par Qt ladresse http://doc.trolltech.com/4.1/richtext-html-subset.html.
Figure 16.2 Une bote de dialogue afchant du texte daide "Quest-ce que cest ?"

376

Qt4 et C++ : Programmation dinterfaces GUI

Un texte "Quest-ce que cest ?" dni sur une action apparat quand lutilisateur clique sur llment de menu ou sur le bouton de la barre doutils, ou encore sil appuie sur la touche de raccourci alors quil se trouve en mode "Quest-ce que cest ?".Lorsque les composants de linterface utilisateur de la fentre principale dune application sont associs du texte "Quest-ce que cest ?", il est habituel de proposer une option de mme nom dans le menu Aide et un bouton correspondant dans la barre doutils. Pour ce faire, il suft de crer une action Quest-ce que cest? avec la fonction statique QWhatThis::createAction() et dajouter laction retourne un menu Aide et une barre doutils. La classe QWhatsThis fournit des fonctions statiques destines programmer lentre dans le mode "Quest-ce que cest ?" et la sortie de ce mode.

Utilisation de QTextBrowser comme moteur daide simple


Les grosses applications ncessitent souvent une aide en ligne plus riche que celle susceptible dtre offerte pas les infobulles, les informations dtat et le mode "Quest-ce que cest ?". Une solution simple consiste fournir un navigateur daide. Les applications incluant un navigateur de ce type possdent gnralement une entre Aide dans le menu Aide de la fentre principale ainsi quun bouton Aide dans chaque bote de dialogue. Dans cette section, nous prsentons le navigateur daide illustr en Figure 16.3 et expliquons comment lutiliser dans une application. La fentre fait appel QTextBrowser pour afcher les pages daide dont la syntaxe est base sur HTML. QTextBrowser tant en mesure de grer de nombreuses balises HTML, il savre idal dans cette situation. Nous commenons par le chier den-tte :
#include <QWidget> class QPushButton; class QTextBrowser; class HelpBrowser: public QWidget { Q_OBJECT public: HelpBrowser(const QString &path, const QString &page, QWidget *parent = 0); static void showPage(const QString &page); private slots: void updateWindowTitle();

Chapitre 16

Aide en ligne

377

private: QTextBrowser *textBrowser; QPushButton *homeButton; QPushButton *backButton; QPushButton *closeButton; };

HelpBrowser fournit une fonction statique pouvant tre appele nimporte o dans lapplication. Cette fonction cre une fentre HelpBrowser et afche la page donne.
Figure 16.3 Le widget HelpBrowser

Voici le dbut de limplmentation :


#include <QtGui> #include "helpbrowser.h" HelpBrowser::HelpBrowser(const QString &path, const QString &page, QWidget *parent) : QWidget(parent) { setAttribute(Qt::WA_DeleteOnClose); setAttribute(Qt::WA_GroupLeader); textBrowser = new QTextBrowser; homeButton = new QPushButton(tr("&Home")); backButton = new QPushButton(tr("&Back")); closeButton = new QPushButton(tr("Close")); closeButton->setShortcut(tr("Esc"));

378

Qt4 et C++ : Programmation dinterfaces GUI

QHBoxLayout *buttonLayout = new QHBoxLayout; buttonLayout->addWidget(homeButton); buttonLayout->addWidget(backButton); buttonLayout->addStretch(); buttonLayout->addWidget(closeButton); QVBoxLayout *mainLayout = new QVBoxLayout; mainLayout->addLayout(buttonLayout); mainLayout->addWidget(textBrowser); setLayout(mainLayout); connect(homeButton, SIGNAL(clicked()), textBrowser, SLOT(home())); connect(backButton, SIGNAL(clicked()), textBrowser, SLOT(backward())); connect(closeButton, SIGNAL(clicked()), this, SLOT(close())); connect(textBrowser, SIGNAL(sourceChanged(const QUrl &)), this, SLOT(updateWindowTitle())); textBrowser->setSearchPaths(QStringList() << path << ":/images"); textBrowser->setSource(page); }

Nous dnissons lattribut Qt::WA_GroupLeader car nous souhaitons faire apparatre les fentres HelpBrowser depuis des botes de dialogue modales en complment de la fentre principale. Ces botes empchent normalement lutilisateur dinteragir avec toute autre fentre de lapplication. Cependant, aprs avoir demand de laide, cet utilisateur doit de toute vidence tre autoris interagir la fois avec la bote de dialogue modale et le navigateur daide. La dnition de lattribut Qt::WA_GroupLeader autorise cette interaction. Nous fournissons deux accs pour la recherche, le premier tant le systme de chiers contenant la documentation de lapplication et le second tant lemplacement des ressources image. Le code HTML peut inclure des rfrences aux images dans le systme de chiers de faon classique, mais il peut galement faire rfrence aux ressources image en utilisant un chemin daccs commenant par :/ (deux points, slash). Le paramtre page est le nom du chier de documentation, avec une ancre HTML facultative (une ancre HTML est la cible dun lien).
void HelpBrowser::updateWindowTitle() { setWindowTitle(tr("Help: %1").arg(textBrowser->documentTitle())); }

Ds que la page source change, le slot updateWindowTitle() est appel. La fonction documentTitle() retourne le texte spci dans la balise <title> de la page.
void HelpBrowser::showPage(const QString &page) { QString path = QApplication::applicationDirPath() + "/doc"; HelpBrowser *browser = new HelpBrowser(path, page);

Chapitre 16

Aide en ligne

379

browser->resize(500, 400); browser->show(); }

Dans la fonction statique showPage(), nous crons la fentre HelpBrowser, puis nous lafchons. Comme nous avons dni lattribut Qt::WA_DeleteOnClose dans le constructeur de HelpBrowser, la fentre sera dtruite automatiquement lors de sa fermeture par lutilisateur. Pour cet exemple, nous supposons que la documentation est stocke dans le sous-rpertoire doc du rpertoire contenant lexcutable de lapplication. Toutes les pages transmises la fonction showPage() seront extraites de ce sous-rpertoire. Vous tes maintenant prt invoquer le navigateur daide depuis lapplication. Dans la fentre principale de lapplication, crez une action Aide et connectez-la un slot help() sur le modle suivant :
void MainWindow::help() { HelpBrowser::showPage("index.html"); }

Nous supposons ici que le chier daide principal se nomme index.html. Dans le cas de botes de dialogue, vous connecteriez le bouton Aide un slot help() similaire celui-ci :
void EntryDialog::help() { HelpBrowser::showPage("forms.html#editing"); }

Ici, nous effectuons la recherche dans un chier daide diffrent, forms.html et faisons dler le QTextBrowser jusqu lancre editing.

Utilisation de lassistant pour une aide en ligne puissante


LAssistant Qt est une application daide en ligne redistribuable fournie par Trolltech. Elle prsente lavantage de prendre en charge lindexation et la recherche de texte intgral et dtre capable de grer plusieurs jeux de documentation distincts correspondant diffrentes applications. Pour utiliser lAssistant Qt, nous devons incorporer le code ncessaire dans notre application et faire connatre lexistence de notre documentation cet assistant. La communication entre une application Qt et lAssistant Qt est gre par la classe QAssistantClient, qui est situe dans une bibliothque distincte. Pour tablir une liaison entre cette bibliothque et une application, vous devez ajouter cette ligne de code au chier .pro de lapplication :
CONFIG += assistant

380

Qt4 et C++ : Programmation dinterfaces GUI

Nous allons maintenant examiner le code dune nouvelle classe HelpBrowser qui utilise lAssistant Qt.
#ifndef HELPBROWSER_H #define HELPBROWSER_H class QAssistantClient; class QString; class HelpBrowser { public: static void showPage(const QString &page); private: static QAssistantClient *assistant; }; #endif Voici le nouveau fichier helpbrowser.cpp: #include <QApplication> #include <QAssistantClient> #include "helpbrowser.h" QAssistantClient *HelpBrowser::assistant = 0; void HelpBrowser::showPage(const QString &page) { QString path = QApplication::applicationDirPath() + "/doc/" + page; if (!assistant) assistant = new QAssistantClient(""); assistant->showPage(path); }

Le constructeur de QAssistantClient accepte comme premier argument un chemin daccs quil utilise pour situer lexcutable de Assistant Qt. En transmettant un chemin daccs vide, nous indiquons QAssistantClient de rechercher lexcutable dans la variable denvironnement PATH. QAssistantClient possde une fonction showPage() qui accepte un nom de page avec une ancre HTML en option. La prochaine tape consiste prparer une table des matires et un index pour la documentation. Pour ce faire, nous crons un prole Assistant Qt et crivons un chier .dcf qui fournit des informations concernant la documentation. Tout ceci est expliqu dans la documentation en ligne de lAssistant Qt. Nous ne rpterons donc pas ces indications ici. Une solution alternative lemploi de QTextBrowser ou celui de lAssistant Qt consiste sorienter vers des approches spciques la plate-forme. Pour les applications Windows, il peut tre souhaitable de crer des chiers daide HTML Windows et doffrir un accs ceux-ci par le biais de Microsoft Internet Explorer. Pour ce faire, vous pouvez recourir la classe

Chapitre 16

Aide en ligne

381

QProcess de Qt ou linfrastructure ActiveQt. Lapproche la plus judicieuse pour les applications X11 serait de fournir des chiers HTML et de lancer un navigateur Web au moyen de QProcess. Sous Mac OS X, lAide Apple fournit une fonctionnalit similaire lAssistant Qt. Nous sommes prsent la n de la Partie II. Les chapitres de la Partie III traitent des fonctionnalits avances et spcialises de Qt. Le code C++ et Qt prsent nest pas plus difcile que celui de la Partie II, mais certains concepts et ides vous paratront peut-tre plus ardus, car ces domaines sont nouveaux pour vous.

III
Qt : tude avance
17 18 19 20 21

Internationalisation Environnement multithread Crer des plug-in Fonctionnalits spciques la plate-forme Programmation embarque

17
Internationalisation
Au sommaire de ce chapitre Travailler avec Unicode Crer des applications ouvertes aux traductions Passer dynamiquement dune langue une autre Traduire les applications

En complment de lalphabet latin utilis pour le franais et de nombreuses langues europennes, Qt offre une large prise en charge des systmes dcriture du reste du monde. Qt utilise Unicode en interne et par le biais de lAPI. Ainsi, toutes les langues utilises par linterface utilisateur sont prises en charge de faon identique. Le moteur de texte de Qt est en mesure de grer tous les systmes dcriture nonlatins majeurs, dont larabe, le chinois, le cyrillique, lhbreu, le japonais, le coren, le tha et les langues Hindi. Le moteur de disposition de Qt prend en charge lcriture de la droite vers la gauche pour les langues telles que larabe et lhbreu. Certaines langues ncessitent des mthodes spciales dentre de texte. Les widgets ddition tels que QLineEdit et QTextEdit fonctionnent correctement avec toute mthode dentre installe sur le systme de lutilisateur.

386

Qt4 et C++ : Programmation dinterfaces GUI

Il faut souvent fournir plus quune simple adaptation du texte saisi par les utilisateurs dans leur langue native. Linterface utilisateur entire doit galement tre traduite. Qt facilite cette tche : il suft de traiter toutes les chanes visibles par lutilisateur avec la fonction tr() (comme nous lavons fait dans les chapitres prcdents) et dutiliser les outils de prise en charge de Qt pour prparer la traduction des chiers dans les langues requises. Qt fournit un outil GUI nomm Qt Linguist destin tre utilis par les traducteurs. Qt Linguist est complt par deux programmes de ligne de commande, lupdate et lrelease, qui sont gnralement excuts par les dveloppeurs de lapplication. Pour la plupart des applications, un chier de traduction est charg au dmarrage qui tient compte des paramtres locaux de lutilisateur. Mais dans certains cas, il est galement ncessaire de pouvoir basculer dune langue lautre au moment de lexcution. Ceci est parfaitement possible avec Qt, bien que cette opration implique un travail supplmentaire. Et grce au systme de disposition de Qt, les divers composants de linterface utilisateur sont automatiquement ajusts pour faire de la place aux textes traduits quand ils sont plus longs que les originaux.

Travailler avec Unicode


Unicode est un systme de codage de caractres qui prend en charge la plupart des systmes dcriture mondiaux. Lide lorigine du dveloppement dUnicode est quen utilisant 16 bits au lieu de 8 pour stocker les caractres, il devient possible de coder environ 65 000 caractres au lieu de 2561. Unicode comprend les systmes ASCII et ISO 8859-1 (Latin-1) et ces deux sous-ensembles se trouvent sur les mmes positions de code. La valeur du caractre "A", par exemple, est de 0x41 dans les systmes ASCII, Latin-1 et Unicode et celle de "" est de 0xD1 dans les systmes Latin-1 et Unicode. La classe QString de Qt stocke les chanes utilisant le systme Unicode. Chaque caractre dun QString est un QChar de 16 bits et non un char de 8 bits. Voici deux mthodes pour dnir le premier caractre dune chane en "A" :
str[0] = A; str[0] = QChar(0x41);

Si le chier source est cod en Latin-1, il est ais de spcier des caractres en Latin-1 :
str[0] = ;

Et si le codage du chier source est diffrent, la valeur numrique fonctionne bien :


str[0] = QChar(0xD1);

1. Les versions rcentes dUnicode affectent des valeurs de caractres au-dessus de 65 535. Ces caractres peuvent tre reprsents avec des squences de deux valeurs de 16 bits nommes "paires de substitution".

Chapitre 17

Internationalisation

387

Nous pouvons dsigner tout caractre Unicode par sa valeur numrique. Voici, par exemple, comment spcier la lettre grecque majuscule sigma ("") et le caractre montaire euro ("").
str[0] = QChar(0x3A3); str[0] = QChar(0x20AC);

Les valeurs numriques de tous les caractres pris en charge par Unicode sont rpertories ladresse http://www.unicode.org/standard/. Si votre besoin en caractres Unicode nonLatin-1 est rare et ponctuel, la recherche en ligne est la solution approprie. Qt fournit cependant des moyens plus pratiques dentrer des chanes Unicode dans un programme, comme nous le verrons ultrieurement dans cette section. Le moteur de texte de Qt 4 prend en charge les systmes dcriture suivants sur toutes les plates-formes : arabe, chinois, cyrillique, grec, hbreu, japonais, coren, lao, latin, tha et vietnamien. Il prend aussi en charge tous les scripts Unicode 4.1 ne ncessitant pas de traitement spcial. En outre, les systmes dcriture suivants sont pris en charge sur X11 avec Foncong et sur les versions rcentes de Windows : bengali, devanagari, gujarati, gurmukhi, kannada, khmer, malayalam, syriac, tamil, telugu, thaana (dhivehi) et tibtain. Loriya, enn, est pris en charge sur X11 et le mongolien ainsi que le sinhala sont pris en charge par Windows XP. En supposant que les polices correctes sont installes sur le systme, Qt peut afcher le texte au moyen de ces systmes dcriture. Et en supposant que les mthodes dentre correctes sont installes, les utilisateurs pourront entrer du texte correspondant ces systmes dcriture dans leurs applications Qt. La programmation avec QChar diffre lgrement de celle avec char. Pour obtenir la valeur numrique dun QChar, vous devez appeler unicode() sur celui-ci. Pour obtenir la valeur ASCII ou Latin-1 dun QChar, il vous faut appeler toLatin1(). Pour les caractres nonLatin-1, toLatin1() retourne "\0". Si nous savons que toutes les chanes dun programme appartiennent au systme ASCII, nous pouvons utiliser des fonctions <cctype> standard telles que isalpha(), isdigit() et isspace() sur la valeur de retour de toLatin1(). Il est cependant gnralement prfrable de faire appel aux fonctions membre de QChar pour raliser ces oprations, car elles fonctionneront pour tout caractre Unicode. Les fonctions fournies par QChar incluent isPrint(), isPunct(), isSpace(), isMark(), isLetter(), isNumber(), isLetterOrNumber(), isDigit(), isSymbol(), isLower() et isUpper(). Voici, par exemple, un moyen de tester si un caractre est un chiffre ou une lettre majuscule :
if (ch.isDigit() || ch.isUpper()) ...

Lextrait de code fonctionne pour tout alphabet qui distingue les majuscules des minuscules, dont lalphabet latin, grec et cyrillique. Lorsque vous avez une chane Unicode, vous pouvez lutiliser tout emplacement de lAPI de Qt o est attendu un QString. Qt prend alors la responsabilit de lafcher correctement et de la convertir en codages adquats pour le systme dexploitation.

388

Qt4 et C++ : Programmation dinterfaces GUI

Nous devons tre particulirement attentifs lorsque nous lisons ou crivons des chiers texte. Ces derniers peuvent utiliser plusieurs codages, et il est souvent impossible de deviner le codage dun chier de ce type partir de son contenu. Par dfaut, QTextStream utilise le codage 8 bits local du systme pour la lecture et lcriture. Pour les Etats-Unis et lEurope de louest, il sagit habituellement de Latin-1. Si nous concevons notre propre format de chier et souhaitons tre en mesure de lire et dcrire des caractres Unicode arbitraires, nous pouvons enregistrer les donnes sous la forme Unicode en appelant
stream.setCodec("UTF-16"); stream.setGenerateByteOrderMark(true);

avant de commencer lcriture dans le QTextStream. Les donnes seront alors enregistres au format UTF-16, format qui ncessite deux octets par caractre, et seront prxes par une valeur spciale de 16 bits (la marque dordre doctet Unicode, 0xFFFE) indiquant si ce chier est en Unicode et si les octets se trouvent dans lordre little-endian ou big-endian. Le format UTF-16 tant identique la reprsentation mmoire dun QString, la lecture et lcriture de chanes Unicode dans ce format peut tre trs rapide. Il se produit cependant une surcharge lors de lenregistrement de donnes ASCII pures au format UTF-16, car deux octets sont stocks pour chaque caractre au lieu dun seul. Il est possible de mentionner dautres codage en appelant setCodec() avec un QTextCodec appropri. Un QTextCodec est un objet qui effectue une conversion entre Unicode et un codage donn. Les QTextCodec sont employs dans diffrents contextes par Qt. En interne, ils sont utiliss pour la prise en charge des polices, des mthodes dentre, du presse-papiers, des oprations de glisser-dposer et des noms de chiers. Mais ils sont galement utiles pour lcriture dapplications Qt. Lors de la lecture dun chier texte, QTextStream dtecte Unicode automatiquement si le chier dbute par une marque dordre doctet. Ce comportement peut tre dsactiv en appelant setAutoDetectUnicode(false). Si les donnes ne sont pas supposes commencer par la marque dordre doctet, il est prfrable dappeler setCodec() avec "UTF-16" avant la lecture. UTF-8 est un autre codage qui prend en charge la totalit du systme Unicode. Son principal avantage par rapport UTF-16 est quil sagit dun super ensemble ASCII. Tout caractre se situant dans la plage 0x00 0x7F est reprsent par un seul octet. Les autres caractres, dont les caractres Latin-1 au-dessus de 0x7F, sont reprsents par des squences multi-octets. Pour ce qui est du texte en majorit ASCII, UTF-8 occupe environ la moiti de lespace consomm par UTF-16. Pour utiliser UTF-8 avec QTextStream, appelez setCodec() avec "UTF-8" comme nom de codec avant les oprations de lecture et dcriture. Si nous souhaitons toujours lire et crire en Latin-1 sans tenir compte du systme de codage local de lutilisateur, nous pouvons dnir le codec "ISO 8859-1" sur QTextStream. Par exemple :
QTextStream in(&file); in.setCodec("ISO 8859-1");

Chapitre 17

Internationalisation

389

Certains formats de chiers spcient leur codage dans leur en-tte. Len-tte est gnralement en ASCII brut pour assurer une lecture correcte quel que soit le codage utilis. Le format de chier XML en est un exemple intressant. Les chiers XML sont normalement encods sous la forme UTF-8 ou UTF-16. Pour les lire correctement, il faut appeler setCodec() avec "UTF-8".Si le format est UTF-16, QTextStream le dtectera automatiquement et sadaptera. Len-tte <?xml?> dun chier XML contient quelquefois un argument encoding. Par exemple :
<?xml version="1.0" encoding="EUC-KR"?>

Comme QTextStream ne vous permet pas de changer le codage une fois la lecture commence, la meilleure faon dappliquer un codage explicite consiste recommencer lire le chier, en utilisant le codec correct (obtenu laide de QTextCodec::codecForName()). Dans le cas de XML, nous pouvons viter davoir grer le codage nous-mmes en utilisant les classes XML de Qt dcrites dans le Chapitre 15. Les QTextCodec peuvent galement tre employs pour spcier le codage de chanes dans le code source. Considrons, par exemple, une quipe de programmeurs japonais crivant une application destine principalement au march des particuliers. Il est probable que ces programmeurs crivent leur code source dans un diteur de texte qui utilise un codage tel que EUC-JP ou Shift-JIS. Un diteur de ce type leur permet de saisir des caractres japonais sans problme. Ils peuvent donc crire le type de code suivant :
QPushButton *button = new QPushButton(tr(" "));

Par dfaut, Qt interprte les arguments de tr() comme Latin-1. Pour les autres cas, nous appelons la fonction statique QTextCodec::setCodecForTr(). Par exemple :
QTextCodec::setCodecForTr(QTextCodec::codecForName("EUC-JP"));

Cette opration doit tre effectue avant le premier appel tr(). En gnral, elle est ralise dans main(), immdiatement aprs la cration de lobjet QApplication. Les autres chanes spcies dans le programme seront interprtes comme des chanes Latin1. Si les programmeurs souhaitent y entrer galement des caractres japonais, ils peuvent les convertir explicitement en Unicode au moyen dun QTextCodec:
QString text = japaneseCodec->toUnicode(" ");

Ils peuvent alternativement demander Qt de faire appel un codec spcique lors de la conversion entre const char* et QString en appelant QTextCodec::setCodecForCStrings():
QTextCodec::setCodecForCStrings(QTextCodec::codecForName("EUC-JP"));

Les techniques dcrites ci-dessus peuvent tre appliques toute langue non Latin-1, dont le chinois, le grec, le coren et le russe.

390

Qt4 et C++ : Programmation dinterfaces GUI

Voici une liste des codages pris en charge par Qt 4 :


cApple Roman cBig5 cBig5-HKSCS cEUC-JP cEUC-KR cGB18030-0 cIBM 850 cIBM 866 cIBM 874 cISO 2022-JP cISO 8859-1 cISO 8859-2 cISO 8859-3 cISO 8859-4 cISO 8859-5 cISO 8859-6 cISO 8859-7 cISO 8859-8 cISO 8859-9 cISO 8859-10 cISO 8859-13 cISO 8859-14 cISO 8859-15 cISO 8859-16 cIscii-Bng cIscii-Dev cIscii-Gjr cIscii-Knd cIscii-Mlm cIscii-Ori cIscii-Pnj cIscii-Tlg cIscii-Tml cJIS X 0201 cJIS X 0208 cKOI8-R cKOI8-U cMuleLao-1 cROMAN8 cShift-JIS cTIS-620 cTSCII cUTF-8 cUTF-16 cUTF-16BE cUTF-16LE cWindows-1250 cWindows-1251 cWindows-1252 cWindows-1253 cWindows-1254 cWindows-1255 cWindows-1256 cWindows-1257 cWindows-1258 cWINSAMI2

Pour tous ces codages, QTextCodex::codecForName() retournera toujours un pointeur valide. Les autres codages peuvent tre pris en charge en drivant QTextCodec.

Crer des applications ouvertes aux traductions


Pour que nos applications soient disponibles dans plusieurs langues, il convient de veiller deux points :

Sassurer que chaque chane visible par lutilisateur passe par tr(). Charger un chier de traduction (.qm) au dmarrage.

Aucune de ces oprations nest ncessaire pour les applications qui ne seront jamais traduites. Mais lemploi de tr() ne ncessite pratiquement aucun effort et laisse la porte ouverte toute traduction ultrieure.

tr() est une fonction statique dnie dans QObject et remplace dans chaque sous-classe dnie avec la macro Q_OBJECT. Lorsque nous crivons du code dans une sous-classe QObject, nous pouvons appeler tr() sans formalit. Un appel tr() retourne une traduction si elle est disponible. Dans le cas contraire, le texte original est retourn.

Chapitre 17

Internationalisation

391

Pour prparer les chiers de traduction, nous devons excuter loutil lupdate de Qt. Cet outil extrait tous les littraux de chane qui apparaissent dans les appels de tr() et produit des chiers de traduction qui contiennent toutes les chanes prtes tre traduites. Les chiers peuvent alors tre expdis un traducteur an quil y ajoute les traductions. Ce processus est expliqu dans la section "Traduction dapplication" un peu plus loin dans ce chapitre. La syntaxe dun appel de tr() est la suivante :
Context::tr(sourceText, comment)

La partie Context est le nom dune sous-classe QObject dnie avec la macro Q_OBJECT. Il nest pas ncessaire de lui prciser si nous appelons tr() depuis une fonction membre de la classe en question. La partie sourceText est le littral chane traduire. La partie comment est facultative. Elle permet de fournir des informations supplmentaires au traducteur. Voici quelques exemples :
RockyWidget::RockyWidget(QWidget *parent) : QWidget(parent) { QString str1 = tr("Letter"); QString str2 = RockyWidget::tr("Letter"); QString str3 = SnazzyDialog::tr("Letter"); QString str4 = SnazzyDialog::tr("Letter", "US paper size"); }

Le contexte des deux premiers appels tr() est "RockyWidget" et celui des deux derniers appels est "SnazzyDialog"."Letter" est le texte source des quatre appels. Le dernier dentre eux comporte galement un commentaire destin aider le traducteur comprendre le sens du texte source. Dans des contextes diffrents, les chanes sont traduites indpendamment les unes des autres. Les traducteurs travaillent gnralement sur un seul contexte la fois, souvent avec lapplication en cours dexcution et en afchant la bote de dialogue ou le widget soumis la traduction. Lorsque nous appelons tr() depuis une fonction globale, nous devons spcier le contexte explicitement. Toute sous-classe QObject de lapplication peut tre employe en tant que contexte. Si aucun contexte nest appropri, il est toujours possible de recourir QObject luimme. Par exemple :
int main(int argc, char *argv[]) { QApplication app(argc, argv); QPushButton button(QObject::tr("Hello Qt!")); button.show(); return app.exec(); }

Dans tous les exemples tudis jusqu prsent, le contexte tait celui dun nom de classe. Cest pratique, car nous pouvons presque toujours lomettre, mais ce nest pas une obligation.

392

Qt4 et C++ : Programmation dinterfaces GUI

La faon la plus courante de traduire une chane en Qt consiste utiliser la fonction QApplication::translate(), qui accepte jusqu trois arguments : le contexte, le texte source et le commentaire facultatif. Voici, par exemple, une autre faon de traduire "Hello Qt !":
QApplication::translate("Global Stuff", "Hello Qt!")

Ici, nous plaons le texte dans le contexte "Global Stuff". Lusage des fonctions tr() et translate() est double : elles remplissent la fois le rle des marqueurs utiliss par lupdate pour trouver les chanes visibles par lutilisateur, et elles agissent en tant que fonctions C++ qui traduisent du texte. Cette caractristique a un impact sur la faon dont nous crivons le code. Les lignes suivantes, par exemple, ne fonctionneront pas :
// INCORRECT const char *appName = "OpenDrawer 2D"; QString translated = tr(appName);

Le problme ici est que lupdate ne sera pas en mesure dextraire le littral chane "OpenDrawer 2D", car il napparat pas lintrieur dun appel tr(). Le traducteur naura donc pas la possibilit de traduire la chane. Ce problme se pose souvent avec les chanes dynamiques :
// INCORRECT statusBar()->showMessage(tr("Host " + hostName + " found"));

Ici, la chane que nous transmettons tr() varie en fonction de la valeur de hostName, de sorte que nous ne pouvons pas raisonnablement nous attendre ce que tr() la traduise correctement. La solution consiste excuter QString::arg():
statusBar()->showMessage(tr("Host %1 found").arg(hostName));

Ce code repose sur le principe suivant : le littral chane "Host %1 found" est transmis tr(). En supposant quun chier de traduction en franais est charg, tr() retournera quelque chose comme "Hte %1 trouv".Puis le paramtre "%1" est remplac par le contenu de la variable hostName. Bien quil soit gnralement dconseill dappeler tr() sur une variable, il est possible de faire fonctionner cette technique correctement. Nous devons utiliser la macro QT_TR_NOOP() an de marquer les littraux chane traduire avant de les affecter une variable. Cette mthode se rvle particulirement intressante pour les tableaux statiques de chanes. Par exemple :
void OrderForm::init() { static const char * const flowers[] = { QT_TR_NOOP("Medium Stem Pink Roses"), QT_TR_NOOP("One Dozen Boxed Roses"), QT_TR_NOOP("Calypso Orchid"), QT_TR_NOOP("Dried Red Rose Bouquet"), QT_TR_NOOP("Mixed Peonies Bouquet"), 0

Chapitre 17

Internationalisation

393

}; for (int i = 0; flowers[i]; ++i) comboBox->addItem(tr(flowers[i])); }

La macro QT_TR_NOOP() retourne simplement son argument. Mais lupdate extraira toutes les chanes encadres par cette dernire an quelles puissent tre traduites. Par la suite, au moment dutiliser la variable, nous appellerons normalement tr(). Mme si elle reoit une variable, cette fonction remplit correctement son rle de traduction. La macro QT_TRANSLATE_NOOP() fonctionne comme QT_TR_NOOP() la diffrence quelle reoit aussi un contexte. Cette macro est pratique pour initialiser des variables lextrieur dune classe :
static const char * const flowers[] = { QT_TRANSLATE_NOOP("OrderForm", "Medium Stem Pink Roses"), QT_TRANSLATE_NOOP("OrderForm", "One Dozen Boxed Roses"), QT_TRANSLATE_NOOP("OrderForm", "Calypso Orchid"), QT_TRANSLATE_NOOP("OrderForm", "Dried Red Rose Bouquet"), QT_TRANSLATE_NOOP("OrderForm", "Mixed Peonies Bouquet"), 0 };

Largument de contexte doit tre identique au contexte fourni ultrieurement tr() ou translate(). Lorsque nous commenons utiliser tr() dans une application, le risque est grand doublier dinsrer des chanes visibles par lutilisateur dans un appel de cette fonction. Si ces appels manquants ne sont pas dtects par le traducteur, les utilisateurs de lapplication vont voir apparatre certaines chanes dans la langue originale. Pour viter ce problme, nous pouvons demander Qt dinterdire les conversions implicites de const char* en QString. Pour ce faire, nous dnissons le symbole de prprocesseur QT_NO_CAST_FROM_ASCII avant dinclure tout en-tte Qt. Le moyen le plus facile de sassurer que ce symbole est dni consiste ajouter la ligne suivante au chier .pro de lapplication :
DEFINES += QT_NO_CAST_FROM_ASCII

Chaque littral chane devra ainsi tre obligatoirement trait par tr() ou QLatin1String(), selon quil devra tre traduit ou non. Les chanes qui ne seront pas encadres par ces fonctions vont ainsi gnrer une erreur la compilation, et il ne vous restera plus qu ajouter les appels de tr() ou QLatin1String() manquants. Une fois chaque chane visible par lutilisateur insre dans un appel de tr(), il ne reste plus qu charger un chier de traduction. Lopration se droule gnralement dans la fonction main() de lapplication. Voici, par exemple, comment nous chargerions un chier de traduction bas sur les paramtres locaux de lutilisateur :
int main(int argc, char *argv[]) {

394

Qt4 et C++ : Programmation dinterfaces GUI

QApplication app(argc, argv); QTranslator appTranslator; appTranslator.load("myapp_" + QLocale::system().name(), qApp->applicationDirPath()); app.installTranslator(&appTranslator); return app.exec(); }

La fonction QLocale::System() retourne un objet QLocale qui fournit des informations concernant les paramtres locaux de lutilisateur. Par convention, nous intgrons le nom des paramtres locaux au nom du chier .qm. Ces noms peuvent tre plus ou moins prcis : fr, par exemple, indique des paramtres locaux en langue franaise, fr_CA reprsente des paramtres locaux en franais canadien et fr_CA.ISO8859-15 en franais canadien avec du codage ISO 8859-15 (un codage qui prend en charge "_", "" et ""). En supposant que les paramtres locaux soient en fr_CA.ISO8859-15, la fonction QTranslator::load() essaie tout dabord de charger le chier myapp_fr_CA.ISO8859-15.qm. Si le chier nexiste pas, load() essaie ensuite myapp_fr_CA.qm, puis myapp_fr.qm et enn myapp.qm avant dabandonner. Nous ne fournissons normalement quun chier myapp_fr.qm, contenant une traduction en franais standard, mais si nous souhaitons un chier diffrent pour le franais canadien, nous pouvons aussi fournir un chier myapp_fr_CA.qm qui sera utilis pour les paramtres locaux fr_CA. Le deuxime argument de QTranslator::load() est le rpertoire o nous souhaitons que load() recherche le chier de traduction. Dans ce cas, nous supposons que les chiers de traduction sont situs dans le mme rpertoire que lexcutable. Les bibliothques Qt contiennent quelques chanes ncessitant une traduction. Trolltech fournit des traductions en franais, en allemand et en chinois simpli dans le rpertoire translations de Qt. Quelques autres langues sont galement fournies, mais par les utilisateurs Qt. Ils ne sont pas ofciellement pris en charge. Le chier de traduction des bibliothques Qt doit galement tre charg.
QTranslator qtTranslator; qtTranslator.load("qt_" + QLocale::system().name(), qApp->applicationDirPath()); app.installTranslator(&qtTranslator);

Un objet QTranslator ne peut contenir quun seul chier de traduction la fois. Cest pourquoi nous utilisons un QTranslator distinct pour la traduction de Qt. Cela ne prsente aucun problme puisque nous pouvons installer autant de traducteurs que ncessaire. QApplication les utilisera tous lors de la recherche dune traduction. Certaines langues, telles que larabe et lhbreu, sont crites de droite gauche au lieu de gauche droite. Dans cette situation, la mise en forme complte de lapplication doit tre inverse en appelant QApplication::setLayoutDirection( Qt::RightToLeft). Les chiers de traduction de Qt contiennent un marqueur spcial nomm "LTR" qui indique si la

Chapitre 17

Internationalisation

395

langue scrit de gauche droite ou de droite gauche. Nous navons donc gnralement pas besoin dappeler setLayoutDirection(). Il serait plus pratique pour les utilisateurs de fournir les applications avec les chiers de traduction intgrs lexcutable, en utilisant le systme de ressource de Qt. Cette technique permettrait non seulement de rduire le nombre de chiers distribus pour constituer le produit, mais aussi dviter le risque de perte ou de suppression accidentelle de chiers de traduction. En supposant que les chiers .qm sont situs dans un sous-rpertoire translations se trouvant dans larbre source, nous aurions alors un chier myapp.qrc avec le contenu suivant :
<!DOCTYPE RCC><RCC version="1.0"> <qresource> <file>translations/myapp_de.qm</file> <file>translations/myapp_fr.qm</file> <file>translations/myapp_zh.qm</file> <file>translations/qt_de.qm</file> <file>translations/qt_fr.qm</file> <file>translations/qt_zh.qm</file> </qresource> </RCC>

Le chier .pro contiendrait lentre suivante :


RESOURCES = myapp.qrc

Nous devons enn spcier :/translations comme chemin daccs aux chiers de traduction dans main(). Les deux points qui apparaissent en premire position indiquent que le chemin daccs fait rfrence une ressource et non un chier situ dans le systme de chiers. Nous avons maintenant tudi tous les points ncessaires pour permettre une application de fonctionner en utilisant des traductions dans dautres langues. Mais la langue et la direction du systme dcriture ne sont pas les seuls points variables entre diffrents pays et cultures. Un programme internationalis doit galement prendre en compte les formats de date, dheure, montaire, numrique et lordre de classement des chanes. Qt inclut une classe QLocale qui fournit des formats de date/dheure et numrique localiss. Pour obtenir dautres informations locales, nous pouvons faire appel aux fonctions C++ setlocale() et localeconv(). Certaines classes et fonctions de Qt adaptent leur comportement en fonction des paramtres locaux : QString::localeAwareCompare() compare deux chanes en prenant en compte les paramtres locaux. Elle permet de trier les lments visibles par lutilisateur. La fonction toString() fournie par QDate, QTime et QDateTime retourne une chane dans un format local quand elle est appele avec Qt::LocalDate comme argument. Par dfaut, les widgets QDateEdit et QDateTimeEdit prsentent les dates dans le format local. Enn, il est possible quune application traduite ait besoin dutiliser des icnes diffrentes de celles fournies initialement. Par exemple, les ches gauche et droite apparaissant sur les

396

Qt4 et C++ : Programmation dinterfaces GUI

boutons Prcdente et Suivante dun navigateur Web doivent tre inverses dans le cas dune langue scrivant de droite gauche. Voici comment procder :
if (QApplication::isRightToLeft()) { backAction->setIcon(forwardIcon); forwardAction->setIcon(backIcon); } else { backAction->setIcon(backIcon); forwardAction->setIcon(forwardIcon); }

Les icnes contenant des caractres alphabtiques doivent trs souvent tre traduites. Par exemple, la lettre "I" qui apparat sur un bouton de barre doutils associ une option Italique dun traitement de texte doit tre remplace par un "C" en espagnol (Cursivo) et par un "K" en danois, nerlandais, allemand, norvgien et sudois (Kursiv). Voici un moyen simple dy parvenir :
if (tr("Italic")[0] == C) { italicAction->setIcon(iconC); } else if (tr("Italic")[0] == K) { italicAction->setIcon(iconK); } else { italicAction->setIcon(iconI); }

Une alternative consiste utiliser la prise en charge de multiples paramtres locaux de la part du systme de ressource. Dans le chier .qrc, il est possible de spcier un paramtre rgional pour une ressource au moyen de lattribut lang. Par exemple :
<qresource> <file>italic.png</file> </qresource> <qresource lang="es"> <file alias="italic.png">cursivo.png</file> </qresource> <qresource lang="sv"> <file alias="italic.png">kursiv.png</file> </qresource>

Si le paramtre local de lutilisateur est es (Espaol), :/italic.png fait alors rfrence limage cursivo.png. Si le paramtre local est sv (Svenska), cest limage kursiv.png qui est employe. Pour dautres paramtres locaux, italic.png est utilis.

Passer dynamiquement dune langue une autre


Pour la plupart des applications, la dtection de la langue prfre de lutilisateur dans main() et le chargement des chiers .qm appropris donne un rsultat satisfaisant. Mais il existe des situations dans lesquelles les utilisateurs doivent pouvoir basculer dynamiquement dune

Chapitre 17

Internationalisation

397

langue lautre. Un changement de langue sans redmarrage peut tre ncessaire pour une application employe en permanence par diffrentes personnes. Par exemple, les applications employes par les oprateurs de centres dappels, par des traducteurs simultans et par des oprateurs de caisses enregistreuses informatises requirent souvent cette possibilit. Le passage dynamique dune langue une autre est un peu plus complexe programmer que le chargement dune traduction unique au lancement de lapplication, mais ce nest pas trs difcile. Voici comment procder : Vous devez offrir lutilisateur le moyen de passer une autre langue. Pour chaque widget ou bote de dialogue, vous devez dnir toutes les chanes susceptibles dtre traduites dans une fonction distincte (souvent nomme retranslateUi()) et appeler cette fonction lors des changements de langue. Examinons les segments les plus importants du code source dune application "centre dappel".Lapplication fournit un menu Language permettant lutilisateur de dnir la langue au moment de lexcution. La langue par dfaut est langlais. (Voir Figure 17.1)
Figure 17.1 Un menu Language dynamique

Comme nous ne savons pas quelle langue lutilisateur souhaite employer au lancement de lapplication, nous ne chargeons pas de traduction dans la fonction main(). Nous les chargeons plutt dynamiquement quand elles savrent ncessaires. Ainsi, tout le code dont nous avons besoin pour grer les traductions doit tre plac dans les classes des botes de dialogue et de la fentre principale. Examinons la sous-classe QMainWindow de lapplication.
MainWindow::MainWindow() { journalView = new JournalView; setCentralWidget(journalView); qApp->installTranslator(&appTranslator); qApp->installTranslator(&qtTranslator); qmPath = qApp->applicationDirPath() + "/translations"; createActions(); createMenus(); retranslateUi(); }

398

Qt4 et C++ : Programmation dinterfaces GUI

Dans le constructeur, nous dnissons le widget central en tant que JournalView, une sousclasse de QTableWidget. Puis nous dnissons quelques variables membre prives en liaison avec la traduction : La variable appTranslator est un objet QTranslator utilis pour stocker la traduction de lapplication en cours. La variable qtTranslator est un objet QTranslator utilis pour stocker la traduction de Qt. La variable qmPath est un QString qui spcie le chemin daccs au rpertoire contenant les chiers de traduction de lapplication. Nous appelons enn les fonctions prives createActions() et createMenus() pour crer le systme de menus, et nous appelons retranslateUi(), galement une fonction prive, pour dnir les chanes initialement visibles par lutilisateur :
void MainWindow::createActions() { newAction = new QAction(this); connect(newAction, SIGNAL(triggered()), this, SLOT(newFile())); aboutQtAction = new QAction(this); connect(aboutQtAction, SIGNAL(triggered()), qApp, SLOT(aboutQt())); }

La fonction createActions() cre normalement ses objets QAction, mais sans dnir aucun texte ou touche de raccourci. Ces oprations seront effectues dans retranslateUi().
void MainWindow::createMenus() { fileMenu = new QMenu(this); fileMenu->addAction(newAction); fileMenu->addAction(openAction); fileMenu->addAction(saveAction); fileMenu->addAction(exitAction); createLanguageMenu(); helpMenu = new QMenu(this); helpMenu->addAction(aboutAction); helpMenu->addAction(aboutQtAction); menuBar()->addMenu(fileMenu); menuBar()->addMenu(editMenu); menuBar()->addMenu(reportsMenu); menuBar()->addMenu(languageMenu); menuBar()->addMenu(helpMenu); }

La fonction createMenus() cre des menus, mais ne leur affecte aucun titre. Une fois encore, cette opration sera effectue dans retranslateUi().

Chapitre 17

Internationalisation

399

Au milieu de la fonction, nous appelons createLanguageMenu() pour remplir le menu Language avec la liste des langues prises en charge. Nous reviendrons sur son code source dans un moment. Examinons tout dabord retranslateUi():
void MainWindow::retranslateUi() { newAction->setText(tr("&New")); newAction->setShortcut(tr("Ctrl+N")); newAction->setStatusTip(tr("Create a new journal")); aboutQtAction->setText(tr("About &Qt")); aboutQtAction->setStatusTip(tr("Show the Qt librarys About box")); fileMenu->setTitle(tr("&File")); editMenu->setTitle(tr("&Edit")); reportsMenu->setTitle(tr("&Reports")); languageMenu->setTitle(tr("&Language")); helpMenu->setTitle(tr("&Help")); setWindowTitle(tr("Call Center")); }

Cest dans la fonction retranslateUi() que se produisent tous les appels de tr() pour la classe MainWindow. Ces appels ont lieu la n du constructeur de MainWindow et chaque fois quun utilisateur change la langue de lapplication par le biais du menu Language. Nous dnissons chaque texte, touche de raccourci et information dtat de QAction. Nous dnissons galement chaque titre de QMenu ainsi que le titre de fentre. La fonction createMenus() prsente prcdemment a appel createLanguageMenu() pour remplir le menu Language avec une liste de langues :
void MainWindow::createLanguageMenu() { languageMenu = new QMenu(this); languageActionGroup = new QActionGroup(this); connect(languageActionGroup, SIGNAL(triggered(QAction *)), this, SLOT(switchLanguage(QAction *))); QDir dir(qmPath); QStringList fileNames = dir.entryList(QStringList("callcenter_*.qm")); for (int i = 0; i < fileNames.size(); ++i) { QString locale = fileNames[i]; locale.remove(0, locale.indexOf(_) + 1); locale.truncate(locale.lastIndexOf(.)); QTranslator translator; translator.load(fileNames[i], qmPath); QString language = translator.translate("MainWindow", "English");

400

Qt4 et C++ : Programmation dinterfaces GUI

QAction *action = new QAction(tr("&%1%2") .arg(i + 1).arg(language), this); action->setCheckable(true); action->setData(locale); languageMenu->addAction(action); languageActionGroup->addAction(action); if (language == "English") action->setChecked(true); } }

Au lieu de coder en dur les langues prises en charge par lapplication, nous crons une entre de menu pour chaque chier .qm situ dans le rpertoire translations de lapplication. Pour des raisons de simplicit, nous supposons que la langue anglaise (English) possde aussi un chier .qm. Une alternative consisterait appeler clear() sur les objets QTranslator lorsque lutilisateur choisit English. La difcult consiste trouver un nom adquat pour la langue fournie par chaque chier .qm. Le fait dafcher simplement "en" pour "English" ou "de" pour "Deutsch" semble primaire et risque de semer la confusion dans lesprit de certains utilisateurs. La solution retenue dans createLanguageMenu() est de contrler la traduction de la chane "English" dans le contexte "MainWindow".Cette chane doit tre transforme en "Deutsch" pour une traduction allemande et en " " pour une traduction japonaise.

Nous crons un QAction cocher pour chaque langue et stockons le nom local dans llment "data" de laction. Nous les ajoutons un objet QActionGroup an de nous assurer quun seul lment du menu Language est coch la fois. Quand une action du groupe est choisie par lutilisateur, le QActionGroup met le signal triggered(QAction *), qui est connect switchLanguage().
void MainWindow::switchLanguage(QAction *action) { QString locale = action->data().toString(); appTranslator.load("callcenter_" + locale, qmPath); qtTranslator.load("qt_" + locale, qmPath); retranslateUi(); }

Le slot switchLanguage() est appel lorsque lutilisateur choisit une langue dans le menu Language. Nous chargeons les chiers de traduction de lapplication et de Qt, et nous appelons retranslateUi() pour traduire toutes les chanes de la fentre principale. Sur Windows, une solution alternative au menu Language consisterait rpondre des vnements LocaleChange, un type dvnement mis par Qt quand un changement dans les paramtres locaux de lenvironnement est dtect. Ce type existe sur toutes les plates-formes prises en charge par Qt, mais il nest en fait gnr que sur Windows, lorsque lutilisateur change les paramtres locaux du systme (dans les Options rgionales et linguistiques du Panneau de

Chapitre 17

Internationalisation

401

conguration). Pour grer les vnements LocaleChange, nous pouvons rimplmenter QWidget::changeEvent() comme suit :
void MainWindow::changeEvent(QEvent *event) { if (event->type() == QEvent::LocaleChange) { appTranslator.load("callcenter_" + QLocale::system().name(), qmPath); qtTranslator.load("qt_" + QLocale::system().name(), qmPath); retranslateUi(); } QMainWindow::changeEvent(event); }

Si lutilisateur change les paramtres locaux pendant que lapplication est en cours dexcution, nous tentons de charger les chiers de traduction corrects pour les nouveaux paramtres et appelons retranslateUi() pour mettre jour linterface utilisateur. Dans tous les cas, nous transmettons lvnement la fonction changeEvent() de la classe de base, cette dernire pouvant aussi tre intresse par LocaleChange ou dautres vnements de changement. Nous en avons maintenant termin avec lexamen du code de MainWindow. Nous allons prsent observer le code de lune des classes de widget de lapplication, la classe JournalView, an de dterminer quelles modications sont ncessaires pour lui permettre de prendre en charge la traduction dynamique.
JournalView::JournalView(QWidget *parent) : QTableWidget(parent) { retranslateUi(); }

La classe JournalView est une sous-classe de QTableWidget. A la n du constructeur, nous appelons la fonction prive retranslateUi() pour dnir les chanes des widgets. Cette opration est similaire celle que nous avons effectue pour MainWindows.
void JournalView::changeEvent(QEvent *event) { if (event->type() == QEvent::LanguageChange) retranslateUi(); QTableWidget::changeEvent(event); }

Nous rimplmentons galement la fonction changeEvent() pour appeler retranslateUi() sur les vnements LanguageChange. Qt gnre un vnement LanguageChange lorsque le contenu dun QTranslator install sur QApplication change. Dans notre application, ceci se produit lorsque nous appelons load() sur appTranslator ou qtTranslator depuis MainWindow::switchLanguage() ou MainWindow::changeEvent().

402

Qt4 et C++ : Programmation dinterfaces GUI

Les vnements LanguageChange ne doivent pas tre confondus avec les vnements LocaleChange. Ces derniers sont gnrs par le systme et demandent lapplication de charger une nouvelle traduction. Les vnements LanguageChange sont gnrs par Qt et demandent aux widgets de lapplication de traduire toutes leurs chanes. Lorsque nous avons implment MainWindow, il ne nous a pas t ncessaire de rpondre LanguageChange. Nous avons simplement appel retranslateUi() chaque appel de load() sur un QTranslator.
void JournalView::retranslateUi() { QStringList labels; labels << tr("Time") << tr("Priority") << tr("Phone Number") << tr("Subject"); setHorizontalHeaderLabels(labels); }

La fonction retranslateUi() met jour les en-ttes de colonnes avec les textes nouvellement traduits, compltant ainsi la partie traduction du code dun widget crit la main. Pour ce qui est des widgets et des botes de dialogue dvelopps avec Qt Designer, loutil uic gnre automatiquement une fonction similaire retranslateUi(). Celle-ci est automatiquement appele en rponse aux vnements LanguageChange.

Traduire les applications


La traduction dune application Qt qui contient des appels tr() est un processus en trois tapes : 1. Excution de lupdate pour extraire toutes les chanes visibles par lutilisateur du code source de lapplication. 2. Traduction de lapplication au moyen de Qt Linguist. 3. Excution de lrelease pour gnrer des chiers binaires .qm que lapplication peut charger au moyen de QTranslator. La responsabilit des tapes 1 et 3 revient aux dveloppeurs. Ltape 2 est gre par les traducteurs. Ce cycle peut tre rpt aussi souvent que ncessaire pendant le dveloppement et la dure de vie de lapplication. Nous allons, par exemple, vous montrer comment traduire lapplication Spreadsheet du Chapitre 3. Cette application contient dj des appels de tr() encadrant chaque chane visible par lutilisateur. Dans un premier temps, nous devons modier le chier .pro de lapplication pour prciser quelles langues nous souhaitons prendre en charge. Si, par exemple, nous voulons prendre en charge lallemand et le franais en plus de langlais, nous ajoutons lentre TRANSLATIONS suivante spreadsheet.pro:
TRANSLATIONS = spreadsheet_de.ts \ spreadsheet_fr.ts

Chapitre 17

Internationalisation

403

Ici, nous mentionnons deux chiers de traduction : un pour lallemand et un pour le franais. Ces chiers seront crs lors de la premire excution de lupdate et seront chargs chacune de ses excutions ultrieures. Ces chiers possdent normalement une extension .ts. Ils se trouvent dans un format XML simple et ne sont pas aussi compacts que les chiers binaires .qm compris par QTranslator. La tche de conversion des chiers .ts lisibles par lhomme en chiers .qm efcaces pour la machine revient lrelease. Pour les esprits curieux, .ts et .qm signient chier "translation source" et chier "Qt message", respectivement. En supposant que nous nous trouvions dans le rpertoire contenant le code source de lapplication Spreadsheet, nous pouvons excuter lupdate sur spreadsheet partir de la ligne de commande comme suit :
lupdate -verbose spreadsheet.pro

Loption verbose invite lupdate fournir plus de commentaires que dhabitude. Voici la sortie attendue :
Updating Found Updating Found spreadsheet_de.ts... 98 source texts (98 new and 0 already existing) spreadsheet_fr.ts... 98 source texts (98 new and 0 already existing)

Chaque chane qui apparat dans un appel de tr() dans le code source de lapplication est stocke dans les chiers .ts, avec une traduction vide. Les chanes qui apparaissent dans les chiers .ui de lapplication sont galement incluses. Loutil lupdate suppose par dfaut que les arguments de tr() sont des chanes Latin-1. Si tel nest pas le cas, nous devons ajouter une entre CODECFORTR au chier .pro. Par exemple :
CODECFORTR = EUC-JP

Cette opration doit tre ralise en plus de lappel QTextCodec::setCodecForTr() depuis la fonction main() de lapplication. Les traductions sont alors ajoutes aux chiers spreadsheet_de.ts et spreadsheet_fr.ts au moyen de Qt Linguist. Pour excuter Qt Linguist, cliquez sur Qt by Trolltech v4.x.y/Linguist dans le menu Dmarrer sous Windows, saisissez linguist sur la ligne de commande sous Unix ou double-cliquez sur Linguist dans le Finder de Mac OS X. Pour commencer ajouter des traductions un chier .ts, cliquez sur File/Open et slectionnez le chier traduire. La partie gauche de la fentre principale de Qt Linguist afche la liste des contextes pour lapplication en cours de traduction. En ce qui concerne lapplication Spreadsheet, les contextes sont "FindDialog", "GoToCellDialog", "MainWindows", "SortDialog" et "Spreadsheet".La zone suprieure droite est la liste des textes sources pour le contexte en cours. Chaque texte source est prsent avec une traduction et un indicateur Done. La zone du milieu nous permet

404

Qt4 et C++ : Programmation dinterfaces GUI

dentrer une traduction pour llment source activ. Dans la zone infrieure droite apparat une liste de suggestions fournies automatiquement par Qt Linguist. Lorsque nous disposons dun chier .ts traduit, nous devons le convertir en un chier binaire .qm an quil puisse tre exploit par QTranslator. Pour effectuer cette opration depuis Qt Linguist, cliquez sur File/Release. Nous commenons gnralement par traduire quelques chanes et nous excutons lapplication avec le chier .qm pour nous assurer que tout fonctionne correctement. (Voir Figure 17.2)
Figure 17.2 Qt Linguist en action

Si vous souhaitez gnrer les chiers .qm pour tous les chiers .ts, vous pouvez utiliser loutil lrelease comme suit :
lrelease -verbose spreadsheet.pro

En supposant que vous avez traduit dix-neuf chanes en franais et cliqu sur lindicateur Done pour dix-sept dentre elles, lrelease produit la sortie suivante :
Updating spreadsheet_de.qm... Generated 0 translations (0 finished and 0 unfinished) Ignored 98 untranslated source texts Updating spreadsheet_fr.qm... Generated 19 translations (17 finished and 2 unfinished) Ignored 79 untranslated source texts

Les chanes non traduites sont prsentes dans les langues initiales lors de lexcution de lapplication. Lindicateur Done est ignor par lrelease. Il peut tre utilis par les traducteurs pour identier les traductions termines et celles qui ont encore besoin dtre rvises.

Chapitre 17

Internationalisation

405

Lorsque nous modions le code source de lapplication, les chiers de traduction deviennent obsoltes. La solution consiste excuter de nouveau lupdate, fournir les traductions pour les nouvelles chanes et regnrer les chiers .qm. Certaines quipes de dveloppement considrent quil faut rgulirement excuter lupdate, alors que dautres prfrent attendre que lapplication soit prte tre commercialise. Les outils lupdate et Qt Linguist sont relativement intelligents. Les traductions qui ne sont plus utilises sont conserves dans les chiers .ts au cas o elles seraient ncessaires dans des versions ultrieures. Lors de la mise jour des chiers .ts, lupdate utilise un algorithme de fusion intelligent qui permet aux traducteurs dconomiser un temps considrable dans le cas de texte identique ou similaire utilis dans des contextes diffrents. Pour plus dinformations concernant Qt Linguist, lupdate et lrelease, rfrez-vous au manuel Qt Linguist ladresse http://doc.trolltech.com/4.1/linguist-manual.html. Ce manuel contient une tude dtaille de linterface utilisateur de Qt Linguist et un didactitiel pour les programmeurs.

18
Environnement multithread
Au sommaire de ce chapitre Crer des threads Synchroniser des threads Communiquer avec le thread principal Utiliser les classes Qt dans les threads secondaires

Les applications GUI conventionnelles possdent un thread dexcution et ralisent, en rgle gnrale, une seule opration la fois. Si lutilisateur invoque une opration longue depuis linterface utilisateur, cette dernire se ge pendant la progression de lopration. Le Chapitre 7 prsente des solutions ce problme. Lenvironnement multithread en est une autre. Dans une application multithread, linterface utilisateur graphique sexcute dans son propre thread et le traitement a lieu dans un ou plusieurs threads distincts. De cette faon, linterface utilisateur graphique des applications reste ractive, mme pendant un traitement intensif. Un autre avantage de lenvironnement multitread est le suivant : les systmes multiprocesseurs peuvent excuter plusieurs threads simultanment sur diffrents processeurs, offrant ainsi de meilleures performances. Dans ce chapitre, nous allons commencer par vous montrer comment driver QThread et comment utiliser QMutex, QSemaphore ainsi que QWaitCondition pour synchroniser

408

Qt4 et C++ : Programmation dinterfaces GUI

les threads. Puis nous vous expliquerons comment communiquer avec le thread principal depuis les threads secondaires pendant que la boucle dvnement est en cours dexcution. Nous conclurons enn par une tude des classes Qt susceptibles ou non dtre utilises dans des threads secondaires. Lenvironnement multithread est un sujet vaste, auquel de nombreux livres sont exclusivement consacrs. Ici, nous supposons que vous matrisez les bases de la programmation multithread. Nous insisterons davantage sur la faon de dvelopper des applications Qt multithread plutt que sur le thme du dcoupage en threads lui-mme.

Crer des threads


Il nest pas difcile de fournir plusieurs threads dans une application Qt : il suft de driver QThread et de rimplmenter sa fonction run(). Pour illustrer cette opration, nous allons commencer par examiner le code dune sous-classe QThread trs simple qui afche de faon rpte une chane donne sur une console.
class Thread: public QThread { Q_OBJECT public: Thread(); void setMessage(const QString &message); void stop(); protected: void run(); private: QString messageStr; volatile bool stopped; };

La classe Thread hrite de QThread et rimplmente la fonction run(). Elle fournit deux fonctions supplmentaires : setMessage() et stop(). La variable stopped est dclare volatile car il est possible dy accder depuis plusieurs threads et nous souhaitons garantir une lecture de sa version actualise chaque fois quelle est ncessaire. Si nous omettons le mot-cl volatile, le compilateur peut optimiser laccs la variable, ce qui risque daboutir des rsultats incorrects.
Thread::Thread() { stopped = false; }

Nous avons dni stopped en false dans le constructeur.

Chapitre 18

Environnement multithread

409

void Thread::run() { while (!stopped) cerr << qPrintable(messageStr); stopped = false; cerr << endl; }

La fonction run() est appele pour lancer lexcution du thread. Tant que la variable stopped est dnie en false, la fonction continue afcher le message donn sur la console. Le thread se termine lorsque la fonction run() rend le contrle.
void Thread::stop() { stopped = true; }

La fonction stop() dnit la variable stopped en true, indiquant ainsi run() dinterrompre lafchage du texte sur la console. Cette fonction peut tre appele tout moment depuis un thread quelconque. Dans le cadre de cet exemple, nous supposons que laffectation un bool est une opration atomique. Cest une supposition raisonnable, si lon considre quun bool ne peut avoir que deux tats. Nous verrons ultrieurement comment utiliser QMutex pour garantir latomicit de laffectation une variable. QThread fournit une fonction terminate() qui met n lexcution dun thread pendant quil est toujours en cours dexcution. Lemploi de terminate() nest pas conseill, car elle peut mettre n au thread tout moment et ne donne ce dernier aucune chance de lancer les oprations de rcupration de la mmoire. Il est toujours prfrable de faire appel une variable stopped et une fonction stop() comme cest le cas ici.
Figure 18.1 Lapplication Threads

A prsent, nous allons voir comment utiliser la classe Thread dans une petite application Qt qui utilise deux threads, A et B, en plus du thread principal.
class ThreadDialog: public QDialog { Q_OBJECT public: ThreadDialog(QWidget *parent = 0); protected: void closeEvent(QCloseEvent *event); private slots: void startOrStopThreadA(); void startOrStopThreadB();

410

Qt4 et C++ : Programmation dinterfaces GUI

private: Thread threadA; Thread threadB; QPushButton *threadAButton; QPushButton *threadBButton; QPushButton *quitButton; };

La classe ThreadDialog dclare deux variables de type Thread et quelques boutons pour fournir une interface utilisateur de base.
ThreadDialog::ThreadDialog(QWidget *parent) : QDialog(parent) { threadA.setMessage("A"); threadB.setMessage("B"); threadAButton = new QPushButton(tr("Start A")); threadBButton = new QPushButton(tr("Start B")); quitButton = new QPushButton(tr("Quit")); quitButton->setDefault(true); connect(threadAButton, SIGNAL(clicked()), this, SLOT(startOrStopThreadA())); connect(threadBButton, SIGNAL(clicked()), this, SLOT(startOrStopThreadB())); }

Dans le constructeur, nous appelons setMessage() pour amener le premier et le second thread afcher de faon rpte des A et des B, respectivement.
void ThreadDialog::startOrStopThreadA() { if (threadA.isRunning()) { threadA.stop(); threadAButton->setText(tr("Start A")); } else { threadA.start(); threadAButton->setText(tr("Stop A")); } }

Lorsque lutilisateur clique sur le bouton correspondant au thread A, startOrStopThreadA() stoppe ce dernier sil tait en cours dexcution ou le lance dans le cas contraire. Elle met galement jour le texte du bouton.
void ThreadDialog::startOrStopThreadB() { if (threadB.isRunning()) {

Chapitre 18

Environnement multithread

411

threadB.stop(); threadBButton->setText(tr("Start B")); } else { threadB.start(); threadBButton->setText(tr("Stop B")); } }

Le code de startOrStopThreadB() est trs similaire.


void ThreadDialog::closeEvent(QCloseEvent *event) { threadA.stop(); threadB.stop(); threadA.wait(); threadB.wait(); event->accept(); }

Si lutilisateur clique sur Quit ou ferme la fentre, nous arrtons tous les threads en cours dexcution et attendons leur n (en excutant QThread::wait()) avant dappeler QCloseEvent::accept(). Ce processus assure une fermeture correcte de lapplication, bien quil ne prsente pas vraiment dimportance dans le cadre de cet exemple. Si vous excutez lapplication et cliquez sur Start A, la console se remplit de A. Si vous cliquez sur Start B, elle se remplit maintenant de squences alternatives de A et de B. Cliquez sur Stop A, et elle nafche alors que les B.

Synchroniser des threads


La synchronisation de plusieurs threads reprsente une ncessit pour la plupart des applications multithread. Qt fournit les classes de synchronisation suivantes : QMutex, QReadWriteLock, QSemaphore et QWaitCondition. La classe QMutex offre un moyen de protger une variable ou une partie de code de sorte quun seul thread puisse y accder la fois. Elle fournit une fonction lock() qui verrouille le mutex. Si ce dernier est dverrouill, le thread en cours sen empare immdiatement et le verrouille. Dans le cas contraire, le thread en cours est bloqu jusqu ce que celui qui le contient le dverrouille. Dans tous les cas, lorsque lappel lock() se termine, le thread en cours conserve le mutex jusqu ce que unlock() soit appel. La classe QMutex fournit galement une fonction tryLock() qui se termine immdiatement si le mutex est dj verrouill. Supposons, par exemple que nous souhaitions protger la variable stopped de la classe Thread de la section prcdente avec un QMutex. Nous ajouterions alors la donne membre suivante Thread:
private:

412

Qt4 et C++ : Programmation dinterfaces GUI

QMutex mutex; };

La fonction run() prendrait la forme suivante :


void Thread::run() { forever { mutex.lock(); if (stopped) { stopped = false; mutex.unlock(); break; } mutex.unlock(); cerr << qPrintable(messageStr); } cerr << endl; }

La fonction stop() prendrait la forme suivante :


void Thread::stop() { mutex.lock(); stopped = true; mutex.unlock(); }

Les oprations de verrouillage et de dverrouillage dun mutex dans les fonctions complexes, ou celles utilisant des exceptions C++, peuvent tre sujettes erreur. Qt offre la classe utilitaire QMutexLocker pour simplier la gestion des mutex. Le constructeur de QMutexLocker accepte QMutex comme argument et le verrouille. Le destructeur de QMutexLocker dverrouille le mutex. Nous pourrions, par exemple, rcrire les fonctions run() et stop() comme suit :
void Thread::run() { forever { { QMutexLocker locker(&mutex); if (stopped) { stopped = false; break; } } cerr << qPrintable(messageStr); } cerr << endl; }

Chapitre 18

Environnement multithread

413

void Thread::stop() { QMutexLocker locker(&mutex); stopped = true; }

Le problme de lemploi des mutex est que seul un thread peut accder la mme variable la fois. Dans les programmes avec de nombreux threads essayant de lire la mme variable simultanment (sans la modier), le mutex risque de compromettre srieusement les performances. Dans ces situations, nous pouvons faire appel QReadWriteLock, une classe de synchronisation qui autorise des accs simultans en lecture seulement sans altrer les performances. Dans la classe Thread, il serait illogique de remplacer QMutex par QReadWriteLock pour protger la variable stopped, car un thread au maximum tentera de lire la variable un moment donn. Un exemple plus appropri impliquerait un ou plusieurs threads de lecture accdant des donnes partages et un ou plusieurs threads dcriture modiant les donnes. Par exemple :
MyData data; QReadWriteLock lock; void ReaderThread::run() { ... lock.lockForRead(); access_data_without_modifying_it(&data); lock.unlock(); ... } void WriterThread::run() { ... lock.lockForWrite(); modify_data(&data); lock.unlock(); ... }

Pour des raisons de commodit, il est possible dutiliser les classes QReadLocker et QWriteLocker pour verrouiller et dverrouiller un QReadWriteLock. QSemaphore est une autre gnralisation des mutex, mais contrairement au verrouillage en lecture/criture, les smaphores peuvent tre employs pour protger un certain nombre de ressources identiques. Les deux extraits de code suivants montrent la correspondance entre QSemaphore et QMutex:
QSemaphore semaphore(1); QMutex mutex; semaphore.acquire(); mutex.lock(); semaphore.release(); mutex.unlock();

414

Qt4 et C++ : Programmation dinterfaces GUI

En transmettant 1 au constructeur, nous indiquons au smaphore quil ne contrle quune seule ressource. Lavantage de lemploi dun smaphore est que nous pouvons transmettre un nombre suprieur 1 au constructeur, puis appeler acquire() plusieurs reprise pour acqurir de nombreuses ressources. Une application typique des smaphores est le transfert dune certaine quantit de donnes (DataSize) entre deux threads laide dune mmoire tampon circulaire partag dune certaine taille (BufferSize) :
const int DataSize = 100000; const int BufferSize = 4096; char buffer[BufferSize];

Le thread producteur crit les donnes dans la mmoire tampon jusqu ce quil atteigne la n, puis recommence au dbut, crasant les donnes existantes. Le thread consommateur lit les donnes lors de sa gnration. La Figure 18.2 illustre ce processus, avec une mmoire tampon minuscule de 16 octets.
Figure 18.2 Le modle producteur/ consommateur
usedSpace(5)
0 1 2 3 4 5 6 7 8 9

freeSpace(11)
10 11 12 13 14 15

A A G C C T
consommateur

A C

producteur

Le besoin de synchronisation dans lexemple producteur/consommateur est double : si le producteur gnre les donnes trop rapidement, il crasera celles que le consommateur naura pas encore lues et si le consommateur lit trop rapidement, il dpassera le producteur et obtiendra des donnes incohrentes. Un moyen radical de rsoudre ce problme consiste amener le producteur remplir la mmoire tampon, puis attendre que le consommateur ait lu cette dernire en entier, et ainsi de suite. Mais sur les machines multiprocesseurs, ce processus nest pas aussi rapide que celui consistant laisser les threads producteur et consommateur agir sur diffrentes parties de la mmoire tampon en mme temps. Pour rsoudre efcacement ce problme, il est possible de recourir deux smaphores :
QSemaphore freeSpace(BufferSize); QSemaphore usedSpace(0);

Le smaphore freeSpace gouverne la partie de la mmoire tampon que le producteur remplit de donnes. Le smaphore usedSpace gouverne la zone susceptible dtre lue par le consommateur. Ces deux rgions sont complmentaires. Le smaphore freeSpace est initialis avec BufferSize (4096), ce qui signie quil possde autant de ressources quil est possible den acqurir. Lorsque lapplication dmarre, le thread lecteur commence par acqurir des octets

Chapitre 18

Environnement multithread

415

"libres" et les convertit en octets "utiliss".Le smaphore usedSpace est initialis avec la valeur 0 pour sassurer que le consommateur ne lit pas des donnes incohrentes au dmarrage. Dans cet exemple, chaque octet est considr comme une ressource. Dans une application du monde rel, nous agirions probablement sur des units de taille plus importante (64 ou 256 octets la fois, par exemple) pour rduire la surcharge associe lemploi des smaphores.
void Producer::run() { for (int i = 0; i < DataSize; ++i) { freeSpace.acquire(); buffer[i % BufferSize] = "ACGT"[uint(rand()) % 4]; usedSpace.release(); }

Dans le producteur, chaque itration dbute par lacquisition dun octet "libre".Si la mmoire tampon est remplie de donnes nayant pas encore t lues par le consommateur, lappel acquire() bloquera le processus jusqu ce que le consommateur ait commenc consommer les donnes. Une fois loctet acquis, nous le remplissons de donnes alatoires (A, C, G ou T) et le librons avec le statut "utilis", de sorte quil puisse tre lu par le thread consommateur.
void Consumer::run() { for (int i = 0; i < DataSize; ++i) { usedSpace.acquire(); cerr << buffer[i % BufferSize]; freeSpace.release(); } cerr << endl; }

Dans le consommateur, nous commenons par acqurir un octet "utilis".Si la mmoire tampon ne contient aucune donne lire, lappel acquire() bloquera le processus jusqu ce que le producteur en ait produites. Une fois loctet acquis, nous lafchons et le librons avec le statut "libre", en permettant ainsi au producteur de le remplir de nouveau avec des donnes.
int main() { Producer producer; Consumer consumer; producer.start(); consumer.start(); producer.wait(); consumer.wait(); return 0; }

416

Qt4 et C++ : Programmation dinterfaces GUI

Enn, nous lanons les threads producteur et consommateur dans main(). Le producteur convertit alors de lespace "libre" en espace "utilis", puis le consommateur le reconvertit en espace "libre". Lorsque nous excutons le programme, il crit une squence alatoire de 100 000 A, C, G et T sur la console et se termine. Pour vraiment comprendre ce qui se passe, nous pouvons dsactiver lcriture de la sortie et crire la place un P chaque fois que le producteur gnre un octet et c chaque fois que le consommateur en lit un. Pour simplier les choses, nous pouvons utiliser des valeurs plus petites pour DataSize et BufferSize. Voici, par exemple, une excution possible avec un DataSize de 10 et un BufferSize de 4: "PcPcPcPcPcPcPcPcPcPc". Dans ce cas, le consommateur lit les octets ds quils sont gnrs par le producteur. Les deux threads sont excuts la mme vitesse. Il est galement possible que le producteur remplisse toute la mmoire tampon avant que le consommateur ne commence sa lecture : "PPPPccccPPPPccccPPcc". Il existe de nombreuses autres possibilits. Les smaphores offrent une grande libert au planicateur de thread spcique au systme, qui peut tudier le comportement des threads et choisir une stratgie de planication approprie. Une approche diffrente au problme de synchronisation dun producteur et dun consommateur consiste employer QWaitCondition et QMutex. Un QWaitCondition permet un thread den rveiller dautres lorsque les conditions requises sont remplies. Cette approche autorise un contrle plus prcis quavec les mutex seuls. Pour illustrer le fonctionnement de ce processus, nous allons reprendre lexemple producteur/consommateur en dclarant des conditions dattente.
const int DataSize = 100000; const int BufferSize = 4096; char buffer[BufferSize]; QWaitCondition bufferIsNotFull; QWaitCondition bufferIsNotEmpty; QMutex mutex; int usedSpace = 0;

En plus du tampon, nous dclarons deux QWaitConditions, un QMutex et une variable qui stocke le nombre doctets "utiliss" dans la mmoire tampon.
void Producer::run() { for (int i = 0; i < DataSize; ++i) { mutex.lock(); while (usedSpace == BufferSize) bufferIsNotFull.wait(&mutex); buffer[i % BufferSize] = "ACGT"[uint(rand()) % 4]; ++usedSpace; bufferIsNotEmpty.wakeAll(); mutex.unlock(); } }

Chapitre 18

Environnement multithread

417

Dans le producteur, nous commenons par vrier si la mmoire tampon est remplie. Si tel est le cas, nous attendons la condition "buffer is not full".Une fois cette condition remplie, nous crivons un octet dans la mmoire tampon, nous incrmentons usedSpace, puis nous rveillons tout thread en attente de la condition "buffer is not full". Nous faisons appel un mutex pour protger tous les accs la variable usedSpace. La fonction QWaitCondition::wait() peut recevoir un mutex verrouill comme premier argument, quelle dverrouille avant de bloquer le thread en cours puis verrouille avant de rendre le contrle. Dans cet exemple, nous aurions pu remplacer la boucle while
while (usedSpace == BufferSize) bufferIsNotFull.wait(&mutex);

par cette instruction if:


if (usedSpace == BufferSize) { mutex.unlock(); bufferIsNotFull.wait(); mutex.lock(); }

Ce processus risque dtre perturb si nous autorisons plusieurs threads producteurs, car un autre producteur pourrait se saisir du mutex immdiatement aprs lappel de wait() et annuler la condition "buffer is not full".
void Consumer::run() { for (int i = 0; i < DataSize; ++i) { mutex.lock(); while (usedSpace == 0) bufferIsNotEmpty.wait(&mutex); cerr << buffer[i % BufferSize]; --usedSpace; bufferIsNotFull.wakeAll(); mutex.unlock(); } cerr << endl; }

Laction du consommateur est trs exactement oppose celle du producteur : il attend la condition "buffer is not full" et rveille tout thread en attente de la vrication de cette condition. Dans tous les exemples tudis jusqu prsent, nos threads ont accd aux mmes variables globales. Mais dans certaines applications, une variable globale doit contenir diffrentes valeurs dans diffrents threads. Ce procd se nomme souvent stockage local de thread ou donne spcique aux threads. Nous pouvons le reproduire en utilisant un map index sur les ID de thread (retourns par QThread::currentThread()), mais une approche plus lgante consiste faire appel la classe QThreadStorage<T>.

418

Qt4 et C++ : Programmation dinterfaces GUI

Cette classe est gnralement utilise pour les caches. En ayant un cache distinct dans les diffrents threads, nous vitons la surcharge du verrouillage, dverrouillage et dattente dun mutex. Par exemple :
QThreadStorage<QHash<int, double> *> cache; void insertIntoCache(int id, double value) { if (!cache.hasLocalData()) cache.setLocalData(new QHash<int, double>); cache.localData()->insert(id, value); } void removeFromCache(int id) { if (cache.hasLocalData()) cache.localData()->remove(id); }

La variable cache contient un pointeur vers un QMap<int,double> par thread. Lorsque nous utilisons le cache pour la premire fois dans un thread particulier, hasLocalData() retourne false et nous crons lobjet QHash<int,double>.

QThreadStorage<T> peut galement tre utilis pour les variables dtat derreur globales (exactement comme errno) an de sassurer que les modications apportes un thread naffectent pas les autres.

Communiquer avec le thread principal


Quand une application Qt dmarre, un seul thread sexcute le thread principal. Cest le seul thread qui est autoris crer lobjet QApplication ou QCoreApplication et appeler exec() sur celui-ci. Aprs lappel exec(), ce thread est soit en attente dun vnement, soit en cours de traitement dun vnement. Le thread principal peut dmarrer de nouveaux threads en crant les objets dune sous-classe QThread, comme ce fut le cas dans la section prcdente. Si ces nouveaux threads doivent communiquer entre eux, ils peuvent utiliser des variables partages avec des mutex, des verrouillages en lecture/criture, des smaphores ou des conditions dattente. Mais aucune de ces techniques ne peut tre exploite pour communiquer avec le thread principal, car elles verrouilleraient la boucle de lvnement et geraient linterface utilisateur. La solution pour communiquer avec le thread principal depuis un thread secondaire consiste faire appel des connexions signal/slot entre threads. En rgle gnrale, le mcanisme des signaux et des slots opre de faon synchrone. Les slots sont donc connects un signal et invoqus immdiatement une fois le signal mis, par le biais dun appel de fonction direct.

Chapitre 18

Environnement multithread

419

Cependant, lorsque nous connectons des objets "actifs" dans des threads diffrents, le mcanisme devient asynchrone. (Ce comportement peut tre modi par le biais dun cinquime paramtre facultatif de QObject::connect().) Ces connexions sont implmentes larrire-plan en publiant un vnement. Le slot est alors appel par la boucle dvnement du thread dans laquelle se trouve lobjet destinataire. Par dfaut, un QObject se situe dans le thread o il a t cr. Ceci peut tre chang tout moment en appelant QObject::moveToThread().
Figure 18.3 Lapplication Image Pro

Pour illustrer le fonctionnement des connexions signal/slot entre threads, nous allons tudier le code de lapplication Image Pro, une application de retouche dimages de base qui permet lutilisateur de faire pivoter, de redimensionner et de changer la prcision des couleurs dune image. Lapplication utilise un thread secondaire pour pouvoir raliser des oprations sur les images sans verrouiller la boucle dvnement. La diffrence est signicative lors du traitement de trs grosses images. Le thread secondaire possde une liste de tches, ou "transactions", accomplir et envoie les vnements la fentre principale pour indiquer la progression.
ImageWindow::ImageWindow() { imageLabel = new QLabel; imageLabel->setBackgroundRole(QPalette::Dark); imageLabel->setAutoFillBackground(true); imageLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); setCentralWidget(imageLabel); createActions(); createMenus(); statusBar()->showMessage(tr("Ready"), 2000); connect(&thread, SIGNAL(transactionStarted(const QString &)), statusBar(), SLOT(showMessage(const QString &))); connect(&thread, SIGNAL(finished()),

420

Qt4 et C++ : Programmation dinterfaces GUI

this, SLOT(allTransactionsDone())); setCurrentFile(""); }

La partie la plus intressante du constructeur de ImageWindow contient les deux connexions signal/slot. Toutes deux impliquent des signaux mis par lobjet TransactionThread, sur lequel nous reviendrons dans un instant.
void ImageWindow::flipHorizontally() { addTransaction(new FlipTransaction(Qt::Horizontal)); }

Le slot flipHorizontally() cre une transaction "de rotation" et lenregistre au moyen de la fonction prive addTransaction(). Les fonctions flipVertically(), resizeImage(), convertTo32Bit(), convertTo8Bit() et convertTo1Bit() sont similaires.
void ImageWindow::addTransaction(Transaction *transact) { thread.addTransaction(transact); openAction->setEnabled(false); saveAction->setEnabled(false); saveAsAction->setEnabled(false); }

La fonction addTransaction() ajoute une transaction la le dattente de transactions du thread secondaire et dsactive les actions Open, Save et Save As pendant que les transactions sont en cours de traitement.
void ImageWindow::allTransactionsDone() { openAction->setEnabled(true); saveAction->setEnabled(true); saveAsAction->setEnabled(true); imageLabel->setPixmap(QPixmap::fromImage(thread.image())); setWindowModified(true); statusBar()->showMessage(tr("Ready"), 2000); }

Le slot allTransactionsDone() est appel quand la le dattente de transactions de TransactionThread se vide. Passons maintenant la classe TransactionThread:
class TransactionThread: public QThread { Q_OBJECT public: void addTransaction(Transaction *transact); void setImage(const QImage &image); QImage image();

Chapitre 18

Environnement multithread

421

signals: void transactionStarted(const QString &message); protected: void run(); private: QMutex mutex; QImage currentImage; QQueue<Transaction *> transactions; };

La classe TransactionThread conserve une liste des transactions grer et les excute les unes aprs les autres larrire-plan.
void TransactionThread::addTransaction(Transaction *transact) { QMutexLocker locker(&mutex); transactions.enqueue(transact); if (!isRunning()) start(); }

La fonction addTransaction() ajoute une transaction la le dattente des transactions et lance le thread de cette transaction sil nest pas dj en cours dexcution. Tous les accs la variable membre transactions sont protgs par un mutex, le thread principal risquant de les modier par lintermdiaire de addTransaction() pendant que le thread secondaire parcourt transactions.
void TransactionThread::setImage(const QImage &image) { QMutexLocker locker(&mutex); currentImage = image; } QImage TransactionThread::image() { QMutexLocker locker(&mutex); return currentImage; }

Les fonctions setImage() et image() permettent au thread principal de dnir limage sur laquelle les transactions doivent tre effectues et de rcuprer limage rsultante une fois toutes les transactions termines. Nous protgeons de nouveau les accs une variable membre en utilisant un mutex.
void TransactionThread::run() { Transaction *transact;

422

Qt4 et C++ : Programmation dinterfaces GUI

forever { mutex.lock(); if (transactions.isEmpty()) { mutex.unlock(); break; } QImage oldImage = currentImage; transact = transactions.dequeue(); mutex.unlock(); emit transactionStarted(transact->message()); QImage newImage = transact->apply(oldImage); delete transact; mutex.lock(); currentImage = newImage; mutex.unlock(); } }

La fonction run() parcourt la le dattente des transactions et les excute chacune tour tour en appelant apply() sur celles-ci. Quand une transaction est lance, nous mettons le signal transactionStarted() avec un message afcher dans la barre dtat de lapplication. Une fois le traitement de toutes les transactions termin, la fonction run() se termine et QThread met le signal finished().
class Transaction { public: virtual ~Transaction() { } virtual QImage apply(const QImage &image) = 0; virtual QString message() = 0; };

La classe Transaction est une classe de base abstraite pour les oprations susceptibles dtre effectues par lutilisateur sur une image. Le destructeur virtuel est ncessaire, car nous devons supprimer les instances des sous-classes Transaction par le biais dun pointeur Transaction. (Si nous lomettons, certains compilateurs mettent un avertissement.) Transaction possde trois sous-classes concrtes : FlipTransaction, ResizeTransaction et ConvertDepthTransaction. Nous ntudierons que FlipTransaction, les deux autres classes tant similaires.
class FlipTransaction: public Transaction { public: FlipTransaction(Qt::Orientation orientation); QImage apply(const QImage &image); QString message();

Chapitre 18

Environnement multithread

423

private: Qt::Orientation orientation; };

Le constructeur FlipTransaction reoit un paramtre qui prcise lorientation de la rotation (horizontale ou verticale).
QImage FlipTransaction::apply(const QImage &image) { return image.mirrored(orientation == Qt::Horizontal, orientation == Qt::Vertical); }

La fonction apply() appelle QImage::mirrored() sur le QImage reu en tant que paramtre et retourne le QImage rsultant.
QString FlipTransaction::message() { if (orientation == Qt::Horizontal) { return QObject::tr("Flipping image horizontally..."); } else { return QObject::tr("Flipping image vertically..."); } }

La fonction message() retourne le message afcher dans la barre dtat pendant la progression de lopration. Cette fonction est appele dans TransactionThread::run() lors de lmission du signal transactionStarted().

Utiliser les classes Qt dans les threads secondaires


Une fonction est dite thread-safe quand elle peut tre appele sans problme depuis plusieurs threads simultanment. Si deux fonctions thread-safe sont appeles depuis des threads diffrents sur la mme donne partage, le rsultat est toujours dni. Par extension, une classe est dite thread-safe quand toutes ses fonctions peuvent tre appeles simultanment depuis des threads diffrents sans quil y ait dinterfrences entre elles. Les classes thread-safe de Qt sont QMutex, QMutexLocker, QReadWriteLock, QReadLocker, QWriteLocker, QSemaphore, QThreadStorage<T>, QWaitCondition et certaines parties de lAPI de QThread. En outre, plusieurs fonctions sont thread-safe, dont QObject::connect(), QObject::disconnect(), QCoreApplication::postEvent(), QCoreApplication::removePostedEvent() et QCoreApplication::removePostedEvents(). La plupart des classes non GUI de Qt rpondent une exigence moins stricte : elles sont rentrantes. Une classe est rentrante si diffrentes instances de la classe peuvent tre utilises simultanment dans des threads diffrents. Cependant, laccs simultan au mme objet rentrant dans plusieurs threads nest pas scuris, et un tel accs doit tre protg avec un mutex. Les classes

424

Qt4 et C++ : Programmation dinterfaces GUI

rentrantes sont marques comme telles dans la documentation de rfrence Qt. En rgle gnrale, toute classe C++ ne faisant pas rfrence une donne globale ou partage est rentrante. QObject est rentrant, mais trois contraintes doivent tre prises en considration : Les QObject enfants doivent tre crs dans le thread de leur parent. Ceci signie que les objets crs dans un thread secondaire ne doivent jamais tre crs avec le mme objet QThread que leur parent, car cet objet a t cr dans un autre thread (soit dans le thread principal, soit dans un thread secondaire diffrent). Nous devons supprimer tous les QObject crs dans un thread secondaire avant denvisager la suppression de lobjet QThread correspondant. Pour ce faire, nous pouvons crer les objets sur la pile dans QThread::run(). Les QObject doivent tre supprims dans le thread les ayant crs. Si nous devons supprimer un QObject situ dans un thread diffrent, nous devons appeler la fonction QObject::deleteLater(), qui publie un vnement "deferred delete" (suppression diffre). Les sous-classes QObject non GUI, telles que QTimer, QProcess et les classes de rseau, sont rentrantes. Nous pouvons les utiliser dans tout thread, ceci tant que ce thread possde une boucle dvnement. En ce qui concerne les threads secondaires, la boucle dvnement est dmarre en appelant QThread::exec() ou par des fonctions utilitaires telles que QProcess::waitForFinished() et QAbstractSocket::waitForDisconnected(). QWidget et ses sous-classes ne sont pas rentrantes, cause des limites hrites des bibliothques sur lesquelles repose la prise en charge GUI de Qt. Par consquent, nous ne pouvons pas appeler directement des fonctions depuis un thread secondaire sur un widget. Pour changer, par exemple, le texte dun QLabel depuis un thread secondaire, nous pouvons mettre un signal connect QLabel::setText() ou appeler QMetaObject::invokeMethod() depuis ce thread. Par exemple :
void MyThread::run() { ... QMetaObject::invokeMethod(label, SLOT(setText(const QString &)), Q_ARG(QString, "Hello")); ... }

De nombreuses classes non GUI de Qt, dont QImage, QString et les classes conteneur, utilisent le partage implicite comme technique doptimisation. Bien que cette optimisation ne rende gnralement pas une classe rentrante, ceci ne pose pas de problme dans Qt. En effet, des instructions de langage assembleur atomiques sont employes pour implmenter un dcompte de rfrences thread-safe, qui rend les classes implicitement partages de Qt rentrantes. Le module QtSql de Qt peut galement tre employ dans les applications multithread, mais il prsente des restrictions qui varient dune base de donnes une autre. Vous trouverez plus de dtails ladresse http://doc.trolltech.com/4.1/sql-driver.html.

19
Crer des plug-in
Au sommaire de ce chapitre Dvelopper Qt avec les plug-in Crer des applications capables de grer les plug-in Ecrire des plug-in pour des applications

Les bibliothques dynamiques (galement nommes bibliothques partages ou DLL) sont des modules indpendants stocks dans un chier spar sur le disque et auxquels de multiples applications peuvent accder. Les programmes spcient gnralement les bibliothques dynamiques qui leur sont ncessaires au moment de la liaison, auquel cas ces bibliothques sont automatiquement charges lors du dmarrage de lapplication. Cette approche implique gnralement lajout de la bibliothque ainsi que de son chemin daccs dinclusion au chier .pro de lapplication. En outre, les en-ttes adquats sont inclus dans les chiers sources. Par exemple :
LIBS INCLUDEPATH += -ldb_cxx += /usr/local/BerkeleyDB.4.2/include

Lalternative consiste charger dynamiquement la bibliothque quand elle est requise, puis rsoudre les symboles que nous souhaitons utiliser. Qt fournit la classe QLibrary pour y parvenir dune manire indpendante de la plate-forme. A partir dun nom de bibliothque,

426

Qt4 et C++ : Programmation dinterfaces GUI

QLibrary examine les emplacements standard sur la plate-forme en question la recherche dun chier appropri. Pour le nom mimetype, par exemple, elle recherchera mimetype.dll sous Windows, mimetype.so sous Linux et mimetype.dylib sous Mac OS X. Les applications GUI modernes peuvent souvent tre dveloppes par lemploi des plug-in. Un plug-in est une bibliothque dynamique qui implmente une interface particulire pour fournir une fonctionnalit supplmentaire facultative. Par exemple, dans le Chapitre 5, nous avons cr un plug-in pour intgrer un widget personnalis Qt Designer. Qt reconnat son propre ensemble dinterfaces de plugin pour divers domaines, dont les formats dimage, les pilotes de bases de donnes, les styles de widgets, le codage du texte et laccessibilit. La premire section de ce chapitre explique comment dvelopper Qt avec un plug-in. Il est galement possible de crer des plug-in spciques des applications Qt particulires. Qt facilite lcriture de tels lments par le biais de sa structure de plug-in, qui scurise galement QLibrary contre les pannes et rend son utilisation plus aise. Dans les deux dernires sections de ce chapitre, nous vous montrerons comment crer une application qui prend en charge les plug-in et comment gnrer un plug-in personnalis pour une application.

Dvelopper Qt avec les plug-in


Qt peut tre dvelopp avec plusieurs types de plug-in, les plus courants tant les pilotes de base de donnes, les formats dimages, les styles et les codecs de texte. Pour chaque type de plug-in, nous avons gnralement besoin dun minimum de deux classes : une classe wrapper (conteneur) de plug-in qui implmente les fonctions API gnriques, et une ou plusieurs classes gestionnaires qui implmentent chacune lAPI dun type de plug-in particulier. Les gestionnaires sont accessibles par le biais de la classe wrapper. (Voir Figure 19.1).
Classe de plug-in QAccessibleBridgePlugin QAccessiblePlugin QIconEnginePlugin QImageIOPlugin QInputContextPlugin QPictureFormatPlugin QSqlDriverPlugin QStylePlugin QTextCodecPlugin Classe de base gestionnaire QAccessibleBridge QAccessibleInterface QIconEngine QImageIOHandler QInputContext N/A QSqlDriver QStyle QTextCodec

Figure 19.1 Les classes gestionnaire et de plug-in de Qt ( lexception de Qtopia Core)

Chapitre 19

Crer des plug-in

427

Pour illustrer ceci, nous allons implmenter un plug-in capable de lire des chiers curseur (chiers .cur) Windows monochromes. Ces chiers peuvent contenir plusieurs images du mme curseur des tailles diffrentes. Une fois le plug-in de curseur cr et install, Qt sera en mesure de lire les chiers .cur et daccder aux curseurs individuels (par le biais de QImage, QImageReader ou QMovie, par exemple). Il pourra galement convertir les curseurs en tout autre format de chier dimage, tel que BMP, JPEG et PNG. Le plug-in pourrait aussi tre dploy avec les applications Qt, car elles contrlent automatiquement les emplacements standard des plug-in Qt et chargent ceux quelles trouvent. Les nouveaux conteneurs de plug-in de format dimage doivent sous-classer QImageIOPlugin et rimplmenter quelques fonctions virtuelles :
class CursorPlugin: public QImageIOPlugin { public: QStringList keys() const; Capabilities capabilities(QIODevice *device, const QByteArray &format) const; QImageIOHandler *create(QIODevice *device, const QByteArray &format) const; };

La fonction keys() retourne une liste de formats dimage pris en charge par le plug-in. Le paramtre format des fonctions capabilities() et create() est suppos avoir une valeur qui provient de cette liste.
QStringList CursorPlugin::keys() const { return QStringList() << "cur"; }

Notre plug-in ne prend en charge quun seul format dimage. Il retourne donc une liste avec un nom unique. Idalement, le nom doit tre lextension de chier utilise pour ce format. Dans le cas de formats avec plusieurs extensions (telles que .jpg et .jpeg pour JPEG), nous pouvons retourner une liste avec plusieurs entres, chacune correspondant une extension.
QImageIOPlugin::Capabilities CursorPlugin::capabilities(QIODevice *device, const QByteArray &format) const { if (format == "cur") return CanRead; if (format.isEmpty()) { CursorHandler handler; handler.setDevice(device); if (handler.canRead()) return CanRead; } return 0; }

428

Qt4 et C++ : Programmation dinterfaces GUI

La fonction capabilities() retourne ce que le gestionnaire dimage est en mesure daccomplir avec le format dimage donn. Il existe trois possibilits (CanRead, CanWrite et CanReadIncremental), et la valeur de retour est un oprateur de bits OR des possibilits qui sappliquent. Si le format est "cur", notre implmentation retourne CanRead. Si aucun format nest fourni, nous crons un gestionnaire de curseur et vrions sil est capable de lire les donnes depuis le priphrique en question. La fonction canRead() lit uniquement les donnes, essayant de dterminer si elle reconnat le chier, sans en changer le pointeur. Une possibilit de 0 indique que ce gestionnaire ne peut ni lire ce chier, ni y crire des donnes.
QImageIOHandler *CursorPlugin::create(QIODevice *device, const QByteArray &format) const { CursorHandler *handler = new CursorHandler; handler->setDevice(device); handler->setFormat(format); return handler; }

Quand un chier curseur est ouvert (par QImageReader, par exemple), la fonction create() du conteneur de plug-in est appele avec le pointeur de priphrique et avec le format "cur". Nous crons une instance de CursorHandler et la congurons avec le priphrique et le format spcis. Lappelant prend la proprit du gestionnaire et le supprimera quand il ne sera plus utile. Si plusieurs chiers doivent tre lus, un nouveau gestionnaire peut tre cr pour chacun.
Q_EXPORT_PLUGIN2(cursorplugin, CursorPlugin)

A la n du chier .cpp, nous utilisons la macro Q_EXPORT_PLUGIN2() pour sassurer que le plug-in est reconnu par Qt. Le premier paramtre est un nom arbitraire que nous souhaitons attribuer au plug-in. Le second paramtre est le nom de classe du plug-in. Il est facile de sous-classer QImageIOPlugin. Le travail vritable du plug-in est effectu dans le gestionnaire. Les gestionnaires de format dimage doivent sous-classer QImageIOHandler et rimplmenter ses fonctions publiques en totalit ou en partie. Commenons par len-tte :
class CursorHandler: public QImageIOHandler { public: CursorHandler(); bool canRead() const; bool read(QImage *image); bool jumpToNextImage(); int currentImageNumber() const; int imageCount() const; private: enum State { BeforeHeader, BeforeImage, AfterLastImage, Error }; void readHeaderIfNecessary() const;

Chapitre 19

Crer des plug-in

429

QBitArray readBitmap(int width, int height, QDataStream &in) const; void enterErrorState() const; mutable State state; mutable int currentImageNo; mutable int numImages; };

Les signatures de toutes les fonctions publiques sont xes. Nous avons omis quelques fonctions dont la rimplmentation est inutile pour un gestionnaire en lecture seulement, en particulier write(). Les variables de membre sont dclares avec le mot-cl mutable, car elles sont modies lintrieur des fonctions const.
CursorHandler::CursorHandler() { state = BeforeHeader; currentImageNo = 0; numImages = 0; }

Lors de la construction du gestionnaire, nous commenons par dnir son tat. Nous appliquons au premier curseur le numro dimage du curseur en cours. Ici, numImage tant dni en 0, il est vident que nous navons pas encore dimage.
bool CursorHandler::canRead() const { if (state == BeforeHeader) { return device()->peek(4) == QByteArray("\0\0\2\0", 4); } else { return state!= Error; } }

La fonction canRead() peut tre appele tout moment pour dterminer si le gestionnaire dimages peut lire des donnes supplmentaires depuis le priphrique. Si la fonction est appele avant que nous ayons lu toute donne et alors que nous nous trouvons toujours dans ltat BeforeHeader, nous vrions la signature particulire qui identie les chiers curseur Windows. Lappel QIODevice::peek() lit les quatre premiers octets sans changer le pointeur du chier du priphrique. Si canRead() est appele ultrieurement, nous retournons true quand aucune erreur ne sest produite.
int CursorHandler::currentImageNumber() const { return currentImageNo; }

Cette fonction retourne le numro du curseur au niveau duquel le pointeur de chier du priphrique est positionn. Une fois le gestionnaire construit, il est possible pour lutilisateur dappeler toutes ses fonctions publiques, dans un ordre quelconque. Cest un problme potentiel, car nous devons partir du principe que nous ne pouvons effectuer quune lecture squentielle.

430

Qt4 et C++ : Programmation dinterfaces GUI

Nous devons donc lire len-tte de chier au moins une fois avant tout autre lment. Nous rsolvons le problme en appelant la fonction readHeaderIfNecessary().
int CursorHandler::imageCount() const { readHeaderIfNecessary(); return numImages; }

Cette fonction retourne le nombre dimages dans le chier. Pour un chier valide o aucune erreur de lecture ne sest produite, elle retournera un dcompte dau moins 1 (voir Figure 19.2).
Figure 19.2 Le format de chier .cur
Header quint16 reserved (0x0000) quint16 type (0x0002) quint16 image count Image 1 quint32 size quint32 width quint32 height quint16 planes quint16 bits per pixel quint32 compression (size -- 20) + 8 bytes color table + Image n quint32 quint32 quint32 quint16

width height bytes XOR bitmap width height bytes AND bitmap

La fonction suivante est importante. Nous ltudierons donc par fragments :


bool CursorHandler::read(QImage *image) { readHeaderIfNecessary(); if (state!= BeforeImage) return false;

La fonction read() lit les donnes de toute image, en commenant lemplacement du pointeur de priphrique courant. Nous pouvons lire limage suivante si len-tte du chier est lu avec succs, ou aprs quune image ait t lue et que le pointeur de priphrique se dplace au dbut dune autre image.
quint32 size; quint32 width; quint32 height; quint16 numPlanes; quint16bitsPerPixel; quint32 compression; QDataStream in(device());

Chapitre 19

Crer des plug-in

431

in.setByteOrder(QDataStream::LittleEndian); in >> size; if (size!= 40) { enterErrorState(); return false; } in >> width >> height >> numPlanes >> bitsPerPixel >> compression; height /= 2; if (numPlanes!= 1 || bitsPerPixel!= 1 || compression!= 0) { enterErrorState(); return false; } in.skipRawData((size - 20) + 8);

Nous crons un QDataStream pour lire le priphrique. Nous devons dnir un ordre doctets correspondant celui mentionn par la spcication de format de chier .cur. Il nest pas besoin de dnir un numro de version QDataStream, car le format de nombres entiers et de nombres virgule ottante ne varie pas entre les versions de ux de donnes. Nous lisons ensuite les divers lments de len-tte du curseur. Nous passons les parties inutiles de len-tte et la table des couleurs de 8 octets au moyen de QDataStream::skipRawData(). Nous devons revoir toutes les caractristiques du format par exemple en diminuant de moiti la hauteur car le format .cur fournit une hauteur qui est deux fois suprieure celle de limage vritable. Les valeurs bitsPerPixel et compression sont toujours de 1 et de 0 dans un chier .cur monochrome. Si nous rencontrons des problmes, nous pouvons appeler enterErrorState() et retourner false.
QBitArray xorBitmap = readBitmap(width, height, in); QBitArray andBitmap = readBitmap(width, height, in); if (in.status()!= QDataStream::Ok) { enterErrorState(); return false; }

Les lments suivants du chier sont deux bitmaps, lune tant un masque XOR et lautre un masque AND. Nous les lisons dans les QBitArray plutt que dans les QBitmap. Un QBitmap est une classe conue pour tre dessine lcran, mais ici, nous avons besoin dun tableau doctets ordinaire. Lorsque nous en avons ni avec la lecture du chier, nous vrions ltat du QDataStream. Cette opration fonctionne correctement, car si un QDataStream entre dans un tat derreur, il y reste et ne peut retourner que des zros. Si, par exemple, la lecture choue au niveau du premier tableau doctets, la tentative de lecture du deuxime rsulte en un QBitArray vide.
*image = QImage(width, height, QImage::Format_ARGB32); for (int i = 0; i < int(height); ++i) { for (int j = 0; j < int(width); ++j) { QRgb color;

432

Qt4 et C++ : Programmation dinterfaces GUI

int bit = (i * width) + j; if (andBitmap.testBit(bit)) { if (xorBitmap.testBit(bit)) { color = 0x7F7F7F7F; } else { color = 0x00FFFFFF; } } else { if (xorBitmap.testBit(bit)) { color = 0xFFFFFFFF; } else { color = 0xFF000000; } } image->setPixel(j, i, color); } }

Nous construisons un nouveau QImage de la taille correcte et dnissons image de faon quil pointe ver celui-ci. Puis nous parcourons chaque pixel des tableaux doctets XOR et AND et nous les convertissons en spcications de couleur ARGB de 32 bits. Les tableaux doctets AND et XOR sont utiliss comme prsent dans le tableau suivant pour obtenir la couleur de chaque pixel du curseur :
AND 1 1 0 0 XOR 1 0 1 0 Rsultat Pixel darrire-plan invers Pixel transparent Pixel blanc Pixel noir

Les pixels noirs, blancs et transparents ne prsentent pas de problme, mais il nexiste aucun moyen dobtenir un pixel darrire-plan invers au moyen dune spcication de couleur ARGB sans connatre la couleur du pixel darrire-plan initial. En remplacement, nous utilisons une couleur grise semi-transparente (0x7F7F7F7F).
++currentImageNo; if (currentImageNo == numImages) state = AfterLastImage; return true; }

Une fois la lecture de limage termine, nous mettons jour le numro de limage courante ainsi que ltat si nous avons atteint la dernire image. A la n de la fonction, le priphrique sera positionn au niveau de limage suivante ou la n du chier.

Chapitre 19

Crer des plug-in

433

bool CursorHandler::jumpToNextImage() { QImage image; return read(&image); }

La fonction jumpToNextImage() permet de passer une image. Pour des raisons de simplicit, nous appelons simplement read() et ignorons le QImage rsultant. Une implmentation plus efcace consisterait utiliser linformation stocke dans len-tte du chier .cur pour passer directement loffset appropri dans le chier.
void CursorHandler::readHeaderIfNecessary() const { if (state!= BeforeHeader) return; quint16 reserved; quint16 type; quint16 count; QDataStream in(device()); in.setByteOrder(QDataStream::LittleEndian); in >> reserved >> type >> count; in.skipRawData(16 * count); if (in.status()!= QDataStream::Ok || reserved!= 0 || type!= 2 || count == 0) { enterErrorState(); return; } state = BeforeImage; currentImageNo = 0; numImages = int(count); }

La fonction prive readHeaderIfNecessary() est appele depuis imageCount() et read(). Si len-tte du chier a dj t lu, ltat nest pas BeforeHeader et nous rendons immdiatement le contrle. Dans le cas contraire, nous ouvrons un ux de donnes sur le priphrique, lisons quelques donnes gnriques (dont le nombre de curseurs dans le chier) et dnissons ltat en BeforeImage. Le pointeur de chier du priphrique est positionn avant la premire image.
void CursorHandler::enterErrorState() const { state = Error; currentImageNo = 0; numImages = 0; }

434

Qt4 et C++ : Programmation dinterfaces GUI

Si une erreur se produit, nous supposons quil nexiste pas dimages valides et dnissons ltat en Error. Ltat du gestionnaire ne peut alors plus tre modi.
QBitArray CursorHandler::readBitmap(int width, int height, QDataStream &in) const { QBitArray bitmap(width * height); quint8 byte; quint32 word; for (int i = 0; i < height; ++i) { for (int j = 0; j < width; ++j) { if ((j % 32) == 0) { word = 0; for (int k = 0; k < 4; ++k) { in >> byte; word = (word << 8) | byte; } } bitmap.setBit(((height - i - 1) * width) + j, word & 0x80000000); word <<= 1; } } return bitmap; }

La fonction readBitmap() permet de lire les masques AND et XOR dun curseur. Ces masques ont deux fonctions inhabituelles. En premier lieu, ils stockent les lignes de bas en haut, au lieu de lapproche courante de haut en bas. En second lieu, lordre des donnes apparat invers par rapport celui utilis tout autre emplacement dans les chiers .cur. Nous devons donc inverser la coordonne y dans lappel de setBit(), et nous lisons les valeurs bit par bit dans le masque. Nous terminons ici limplmentation du plug-in de format dimage CursorHandler. Les plug-in correspondant dautres formats dimage doivent suivre le mme schma, bien que certains puissent implmenter une part plus importante de lAPI QImageIOHandler, en particulier les fonctions utilises pour crire les images. Les plug-in dautres types, tels que les codecs de texte ou les pilotes de base de donnes, suivent le mme schma : un conteneur de plug-in fournit une API gnrique que les applications peuvent utiliser, ainsi quun gestionnaire fournissant les fonctionnalits sous-jacentes. Le chier .pro diffre entre les plug-in et les applications. Nous terminerons avec ceci :
TEMPLATE CONFIG HEADERS SOURCES DESTDIR = lib += plugin = cursorhandler.h \ cursorplugin.h = cursorhandler.cpp \ cursorplugin.cpp = $(QTDIR)/plugins/imageformats

Chapitre 19

Crer des plug-in

435

Par dfaut, les chiers .pro font appel au modle app, mais ici, nous devons utiliser le modle lib car un plug-in est une bibliothque, et non une application autonome. La ligne CONFIG indique Qt que la bibliothque nest pas classique, mais quil sagit dune bibliothque de plug-in. DESTDIR mentionne le rpertoire o doit tre plac le plug-in. Tous les plug-in Qt doivent se trouver dans le sous-rpertoire plugins appropri o a t install Qt, et comme notre plug-in fournit un nouveau format dimage, nous le plaons dans le sous-rpertoire plugins/imageformats. La liste des noms de rpertoire et des types de plug-in est fournie ladresse http://doc.trolltech.com/4.1/plugins-howto.html. Pour cet exemple, nous supposons que la variable denvironnement QTDIR est dnie dans le rpertoire o est install Qt. Les plug-in crs pour Qt en mode release et en mode debug sont diffrents. Ainsi, si les deux versions de Qt sont installes, il est conseill de prciser laquelle doit tre utilise dans le chier .pro, en ajoutant par exemple la ligne suivante :
CONFIG += release

Les applications qui utilisent les plug-in Qt doivent tre dployes avec ceux quelles sont senses exploiter. Les plug-in Qt doivent tre placs dans des sous-rpertoires spciques (imageformats, par exemple, pour les formats dimage). Les applications Qt recherchent les plug-in dans le sous-rpertoire plugins situ dans le dossier o rside leur excutable. Ainsi, pour les plug-in dimage elles recherchent dans application_dir/plugins/imageformats. Si nous souhaitons dployer les plug-in Qt dans un rpertoire diffrent, les directions de recherche des plug-in peuvent tre multiplies en utilisant QCoreApplication::addLibraryPath().

Crer des applications capables de grer les plug-in


Un plug-in dapplication est une bibliothque dynamique qui implmente une ou plusieurs interfaces. Une interface est une classe qui est constitue exclusivement de fonctions purement virtuelles. La communication entre lapplication et les plug-in seffectue par le biais de la table virtuelle de linterface. Dans cette section, nous nous pencherons sur lemploi dun plug-in dans une application Qt par lintermdiaire de ses interfaces, et dans la section suivante nous verrons comment implmenter un plug-in. Pour fournir un exemple concret, nous allons crer lapplication Text Art simple prsente en Figure 19.3. Les effets de texte sont fournis par les plug-in. Lapplication rcupre la liste des effets de texte fournis par chaque plug-in et les parcourt pour afcher chacun deux en tant qulment dans un QListWidget (voir Figure 19.3). Lapplication Text Art dnit une interface :
class TextArtInterface {

436

Qt4 et C++ : Programmation dinterfaces GUI

public: virtual ~TextArtInterface() { } virtual QStringList effects() const = 0; virtual QPixmap applyEffect(const QString &effect, const QString &text, const QFont &font, const QSize &size, const QPen &pen, const QBrush &brush) = 0; }; Q_DECLARE_INTERFACE(TextArtInterface, "com.software-inc.TextArt.TextArtInterface/1.0")

Figure 19.3 Lapplication Text Art

Une classe dinterface dclare normalement un destructeur virtuel, une fonction virtuelle qui retourne un QStringList ainsi quune ou plusieurs autres fonctions virtuelles. Le destructeur est avant tout destin satisfaire le compilateur, qui, sans sa prsence se plaindrait du manque de destructeur virtuel dans une classe possdant des fonctions virtuelles. Dans cet exemple, la fonction effects() retourne une liste deffets de texte fournis par le plug-in. Nous pouvons envisager cette liste comme une liste de cls. A chaque fois que nous appelons une autre fonction, nous transmettons lune de ces cls en tant que premier argument, permettant ainsi limplmentation deffets multiples dans un plug-in. Nous faisons nalement appel la macro Q_DECLARE_INTERFACE() pour associer un identicateur linterface. Cet identicateur comprend gnralement quatre composants : un nom de domaine inverse spciant le crateur de linterface, le nom de lapplication, le nom de linterface et un numro de version. Ds que nous modions linterface (en ajoutant une fonction virtuelle ou en changeant la signature dune fonction existante), nous devons incrmenter le numro de version. Dans le cas contraire, lapplication risque de tomber brusquement en panne en essayant daccder un plug-in obsolte.

Chapitre 19

Crer des plug-in

437

Lapplication est implmente dans une classe nomme TextArtDialog. Nous ntudierons ici que le code prsentant un intrt. Commenons par le constructeur :
TextArtDialog::TextArtDialog(const QString &text, QWidget *parent) : QDialog(parent) { listWidget = new QListWidget; listWidget->setViewMode(QListWidget::IconMode); listWidget->setMovement(QListWidget::Static); listWidget->setIconSize(QSize(260, 80)); loadPlugins(); populateListWidget(text); }

Le constructeur cre un QListWidget pour rpertorier les effets disponibles. Il appelle la fonction prive loadPlugins() pour rechercher et charger tout plug-in implmentant le TextArtInterface et remplit le QListWidget en consquence, en appelant une autre fonction prive, populateListWidget().
void TextArtDialog::loadPlugins() { QDir pluginDir(QApplication::applicationDirPath()); #if defined(Q_OS_WIN) if (pluginDir.dirName().toLower() == "debug" || pluginDir.dirName().toLower() == "release") pluginDir.cdUp(); #elif defined(Q_OS_MAC) if (pluginDir.dirName() == "MacOS") { pluginDir.cdUp(); pluginDir.cdUp(); pluginDir.cdUp(); } #endif if (!pluginDir.cd("plugins")) return; foreach (QString fileName, pluginDir.entryList(QDir::Files)) { QPluginLoader loader(pluginDir.absoluteFilePath(fileName)); if (TextArtInterface *interface = qobject_cast<TextArtInterface *>(loader.instance())) interfaces.append(interface); } }

Dans loadPlugins(), nous tentons de charger tous les chiers du rpertoire plugins de lapplication. (Sous Windows, lexcutable de lapplication rside gnralement dans un sousrpertoire debug ou release. Nous remontons donc dun niveau. Sous Mac OS X, nous prenons en compte la structure de rpertoire du paquet.)

438

Qt4 et C++ : Programmation dinterfaces GUI

Si le chier que nous essayons de charger est un plug-in qui utilise la mme version de Qt que lapplication, QPluginLoader::instance() retournera un QObject* qui pointe vers un plug-in Qt. Nous excutons qobject_cast<T>() pour vrier si le plug-in implmente le TextArtInterface. A chaque fois que la conversion est russie, nous ajoutons linterface la liste dinterfaces (de type QList<TextArtInterface*>) de TextArtDialog. Certaines applications peuvent vouloir charger plusieurs interfaces diffrentes, auquel cas le code permettant dobtenir les interfaces doit tre similaire celui-ci :
QObject *plugin = loader.instance(); if (TextArtInterface *i = qobject_cast<TextArtInterface *>(plugin)) textArtInterfaces.append(i); if (BorderArtInterface *i = qobject_cast<BorderArtInterface *>(plugin)) borderArtInterfaces.append(i); if (TextureInterface *i = qobject_cast<TextureInterface *>(plugin)) textureInterfaces.append(i);

Les mmes plug-in peuvent tre convertis avec succs en plusieurs pointeurs dinterface, car il est possible pour les plug-in de fournir plusieurs interfaces avec lhritage multiple.
void TextArtDialog::populateListWidget(const QString &text) { QSize iconSize = listWidget->iconSize(); QPen pen(QColor("darkseagreen")); QLinearGradient gradient(0, 0, iconSize.width() / 2, iconSize.height() / 2); gradient.setColorAt(0.0, QColor("darkolivegreen")); gradient.setColorAt(0.8, QColor("darkgreen")); gradient.setColorAt(1.0, QColor("lightgreen")); QFont font("Helvetica", iconSize.height(), QFont::Bold); foreach (TextArtInterface *interface, interfaces) { foreach (QString effect, interface->effects()) { QListWidgetItem *item = new QListWidgetItem(effect, listWidget); QPixmap pixmap = interface->applyEffect(effect, text, font, iconSize, pen, gradient); item->setData(Qt::DecorationRole, pixmap); } } listWidget->setCurrentRow(0); }

La fonction populateListWidget() commence en crant quelques variables transmette la fonction applyEffect(), et en particulier un stylet, un dgrad linaire et une police. Elle parcourt ensuit le TextArtInterface ayant t trouv par loadPlugins(). Pour tout effet

Chapitre 19

Crer des plug-in

439

fourni par chaque interface, un nouveau QListWidgetItem est cr avec le nom de leffet quil reprsente, et un QPixmap est gnr au moyen de applyEffect(). Dans cette section, nous avons vu comment charger des plug-in en appelant loadPlugins() dans le constructeur, et comment les exploiter dans populateListWidget(). Le code dtermine lgamment combien il existe de plug-in fournissant TextArtInterface. En outre, des plug-in supplmentaires peuvent tre ajouts ultrieurement : chaque nouvelle ouverture de lapplication, celle-ci charge tous les plug-in fournissant les interfaces souhaites. Il est ainsi possible de dvelopper la fonctionnalit de lapplication sans pour autant la modier.

Ecrire des plug-in dapplication


Un plug-in dapplication est une sous-classe de QObject et des interfaces quil souhaite fournir. Le CD daccompagnement de ce livre inclut deux plug-in pour lapplication Text Art prsente dans la section prcdente. Ils visent dmontrer que lapplication gre correctement plusieurs plug-in. Dans cet ouvrage, nous ntudierons le code que de lun dentre eux, le plug-in Basic Effects. Nous supposerons que le code source du plug-in est situ dans un rpertoire nomm basiceffectsplugin et que lapplication Text Art se trouve dans un rpertoire parallle nomm textart. Voici la dclaration de la classe de plug-in :
class BasicEffectsPlugin: public QObject, public TextArtInterface { Q_OBJECT Q_INTERFACES(TextArtInterface) public: QStringList effects() const; QPixmap applyEffect(const QString &effect, const QString &text, const QFont &font, const QSize &size, const QPen &pen, const QBrush &brush); };

Le plug-in nimplmente quune seule interface, TextArtInterface. En plus de Q_OBJECT, nous devons utiliser la macro Q_INTERFACES() pour chaque interface sous-classe an de nous assurer dune coopration sans problmes entre moc et qobject_cast<T>().
QStringList BasicEffectsPlugin::effects() const { return QStringList() << "Plain" << "Outline" << "Shadow"; }

La fonction effects() retourne une liste deffets de texte pris en charge par le plug-in. Celuici supportant trois effets, nous retournons simplement une liste contenant le nom de chacun deux.

440

Qt4 et C++ : Programmation dinterfaces GUI

La fonction applyEffect() fournit la fonctionnalit du plug-in et savre assez importante. Cest pourquoi nous ltudierons par fragments.
QPixmap BasicEffectsPlugin::applyEffect(const QString &effect, const QString &text, const QFont &font, const QSize &size, const QPen &pen, const QBrush &brush) { QFont myFont = font; QFontMetrics metrics(myFont); while ((metrics.width(text) > size.width() || metrics.height() > size.height()) && myFont.pointSize() > 9) { myFont.setPointSize(myFont.pointSize() - 1); metrics = QFontMetrics(myFont); }

Nous souhaitons nous assurer que le texte donn sadaptera si possible la taille spcie. Cest la raison pour laquelle nous utilisons les mtriques de la police an de dterminer si texte est trop gros pour sadapter. Si tel est le cas, nous excutons une boucle o nous rduisons la taille en points jusqu parvenir une dimension correcte, ou jusqu parvenir 9 points, notre taille minimale xe.
QPixmap pixmap(size); QPainter painter(&pixmap); painter.setFont(myFont); painter.setPen(pen); painter.setBrush(brush); painter.setRenderHint(QPainter::Antialiasing, true); painter.setRenderHint(QPainter::TextAntialiasing, true); painter.setRenderHint(QPainter::SmoothPixmapTransform, true); painter.eraseRect(pixmap.rect());

Nous crons un pixmap de la taille requise et un painter pour peindre sur le pixmap. Nous dnissons aussi quelques conseils de rendu pour assurer les meilleurs rsultats possible. Lappel eraseRect() efface le pixmap avec la couleur darrire-plan.
if (effect == "Plain") { painter.setPen(Qt::NoPen); } else if (effect == "Outline") { QPen pen(Qt::black); pen.setWidthF(2.5); painter.setPen(pen); } else if (effect == "Shadow") { QPainterPath path; painter.setBrush(Qt::darkGray); path.addText(((size.width() - metrics.width(text)) / 2) + 3, (size.height() - metrics.descent()) + 3, myFont, text); painter.drawPath(path); painter.setBrush(brush); }

Chapitre 19

Crer des plug-in

441

Pour leffet "Plain" aucun relief nest requis. En ce qui concerne leffet "Outline", nous ignorons le stylet initial et en crons un de couleur noire avec une largeur de 2,5 pixels. Quant leffet "Shadow", il nous faut dabord dessiner lombre, de faon pouvoir inscrire le texte par-dessus.
QPainterPath path; path.addText((size.width() - metrics.width(text)) / 2, size.height() - metrics.descent(), myFont, text); painter.drawPath(path); return pixmap; }

A prsent, nous disposons du stylet et de lensemble des pinceaux adapts chaque effet de texte. Nous sommes maintenant prts afcher le texte. Ce dernier est centr horizontalement et sufsamment loign du bas du pixmap pour laisser de la place aux hampes infrieures.
Q_EXPORT_PLUGIN2(basiceffectsplugin, BasicEffectsPlugin)

A la n du chier .cpp, nous excutons la macro Q_EXPORT_PLUGIN2() an de rendre le plug-in disponible pour Qt. Le chier .pro est similaire celui que nous avons utilis pour le plug-in de curseur Windows prcdemment dans ce chapitre.
TEMPLATE CONFIG HEADERS SOURCES DESTDIR = += = = = lib plugin ../textart/textartinterface.h \<RC>basiceffectsplugin.h basiceffectsplugin.cpp ../textart/plugins

Si ce chapitre vous a donn lenvie den savoir plus sur les plug-in, vous pouvez tudier lexemple plus avanc Plug & Paint fourni avec Qt. Lapplication prend en charge trois interfaces diffrentes et inclut une bote de dialogue Plugin Information rpertoriant les plug-in et interfaces disponibles pour lapplication.

20
Fonctionnalits spciques la plate-forme
Au sommaire de ce chapitre Construire une interface avec les API natives ActiveX sous Windows Prendre en charge la gestion de session de X11

Dans ce chapitre, nous allons tudier quelques options spciques la plate-forme disponibles pour les programmeurs Qt. Nous commencerons par examiner comment accder aux API natives telles que lAPI Win32 de Windows, Carbon sous Mac OS X et Xlib sous X11. Nous tudierons ensuite lextension ActiveQt, pour apprendre utiliser les contrles ActiveX sous Windows. Vous dcouvrirez aussi que cette extension permet

444

Qt4 et C++ : Programmation dinterfaces GUI

de crer des applications qui se comportent comme des serveurs ActiveX. Dans la dernire section, nous expliquerons comment amener des applications Qt cooprer avec le gestionnaire de session sous X11. Pour complter les fonctionnalits prsentes ci-dessus, Trolltech offre plusieurs solutions Qt spciques la plate-forme, notamment les frameworks de migration Qt/Motif et Qt/MFC qui simplient la migration des applications Motif/Xt et MFC vers Qt. Une extension analogue est fournie pour les applications Tcl/Tk par froglogic, et un convertisseur de ressource Microsoft Windows est disponible auprs de Klarlvdalens Datakonsult. Consultez les pages Web suivantes pour obtenir des dtails complmentaires : http://www.trolltech.com/products/solutions/catalog/ http://www.froglogic.com/tq/ http://www.kdab.net/knut/ Pour tout ce qui concerne le dveloppement intgr, Trolltech propose la plate-forme dapplication Qtopia. Cette plate-forme est dtaille au Chapitre 21.

Construire une interface avec les API natives


LAPI trs dveloppe de Qt rpond la plupart des besoins sur toutes les plates-formes, mais dans certaines circonstances, vous pourriez avoir besoin demployer les API locales. Dans cette section, nous allons montrer comment utiliser ces API pour les diffrentes plates-formes prises en charge par Qt an daccomplir des tches particulires.
Figure 20.1 Une fentre doutil Mac OS X avec la barre de titre sur le ct

Sur toutes les plates-formes, QWidget fournit une fonction winId() qui renvoie lidentiant de la fentre ou le handle (descripteur). QWidget fournit aussi une fonction statique nomme find() qui renvoie le QWidget avec un ID de fentre particulier. Nous pouvons transmettre cet identiant aux fonctions dAPI natives pour obtenir des effets spciques la plate-forme.

Chapitre 20

Fonctionnalits spciques la plate-forme

445

Par exemple, le code suivant sappuie sur winId() pour dplacer la barre de titre dune fentre doutil vers la gauche laide des fonctions natives de Mac OS X :
#ifdef Q_WS_MAC ChangeWindowAttributes(HIViewGetWindow(HIViewRef(toolWin.winId())), kWindowSideTitlebarAttribute, kWindowNoAttributes); #endif

Sous X11, voici comment nous pourrions modier une proprit de fentre :
#ifdef Q_WS_X11 Atom atom = XInternAtom(QX11Info::display(), "MY_PROPERTY", False); long data = 1; XChangeProperty(QX11Info::display(), window->winId(), atom, atom, 32, PropModeReplace, reinterpret_cast<uchar *>(&data), 1); #endif

Les directives #ifdef et #endif qui encadrent le code spcique la plate-forme garantissent la compilation de lapplication sur les autres plates-formes. Pour une application uniquement destine Windows, voici un exemple dutilisation des appels de GDI pour afcher un widget Qt :
void GdiControl::paintEvent(QPaintEvent * /* event */) { RECT rect; GetClientRect(winId(), &rect); HDC hdc = GetDC(winId()); FillRect(hdc, &rect, HBRUSH(COLOR_WINDOW + 1)); SetTextAlign(hdc, TA_CENTER | TA_BASELINE); TextOutW(hdc, width() / 2, height() / 2, text.utf16(), text.size()); ReleaseDC(winId(), hdc); }

Pour que ce code sexcute, nous devons aussi rimplmenter QPaintDevice::paintEngine() de sorte quelle renvoie un pointeur nul et dnisse lattribut Qt::WA_PaintOnScreen dans le constructeur du widget. Lexemple suivant montre comment combiner QPainter et des appels GDI dans le gestionnaire dvnement paint en excutant les fonctions getDC() et releaseDC():
void MyWidget::paintEvent(QPaintEvent * /* event */) { QPainter painter(this); painter.fillRect(rect().adjusted(20, 20, -20, -20), Qt::red); #ifdef Q_WS_WIN HDC hdc = painter.paintEngine()->getDC(); Rectangle(hdc, 40, 40, width() - 40, height() - 40); painter.paintEngine()->releaseDC(); #endif }

446

Qt4 et C++ : Programmation dinterfaces GUI

Le mlange des appels de QPainter avec les appels GDI comme dans ce code conduit quelquefois des rsultats tranges, en particulier quand les appels de QPainter se produisent aprs les appels GDI. En effet, QPainter sappuie dans ce cas sur certaines hypothses concernant ltat de la couche de dessin sous-jacente. Qt dnit un des quatre symboles de systme de fentre suivant : Q_WS_WIN, Q_WS_ X11, Q_WS_MAC, et Q_WS_QWS (Qtopia). Nous devons inclure au moins un en-tte Qt pour tre en mesure de les utiliser dans les applications. Qt fournit aussi des symboles de prprocesseur destins identier le systme dexploitation :
c Q_OS_AIX c Q_OS_BSD4 c Q_OS_BSDI c Q_OS_CYGWIN c Q_OS_DGUX c Q_OS_DYNIX c Q_OS_FREEBSD c Q_OS_HPUX c Q_OS_HURD c Q_OS_IRIX c Q_OS_LINUX c Q_OS_LYNX c Q_OS_MAC c Q_OS_NETBSD c Q_OS_OPENBSD c Q_OS_OS2EMX c Q_OS_OSF c Q_OS_QNX6 c Q_OS_QNX c Q_OS_RELIANT c Q_OS_SCO c Q_OS_SOLARIS c Q_OS_ULTRIX c Q_OS_UNIXWARE c Q_OS_WIN32 c Q_OS_WIN64

Nous pouvons supposer quau moins un de ces symboles sera dni. Pour des raisons pratiques, Qt dnit aussi Q_OS_WIN ds que Win32 ou Win64 est dtect, et Q_OS_UNIX lorsquun systme dexploitation de type UNIX (y compris Linux et Mac OS X), est reconnu. Au moment de lexcution, nous pouvons contrler QSysInfo::WindowsVersion ou QSysInfo::MacintoshVersion pour identier les diffrentes versions de Windows (2000, XP, etc.) ou de Mac OS X (10.2, 10.3, etc.). Des macros de compilateur viennent complter les macros du systme de fentrage et du systme dexploitation. Par exemple, Q_CC_MSVC est dnie si le compilateur est Microsoft Visual C++. Elles peuvent se rvler trs pratiques pour rsoudre les erreurs de compilateur. Plusieurs classes de Qt lies linterface graphique fournissent des fonctions spciques la plate-forme qui renvoient des handles de bas niveau vers lobjet sous-jacent. Ces descripteurs sont numrs en Figure 20.2. Sous X11, QPixmap::x11Info() et QWidget::x11Info() renvoient un objet QX11Info qui fournit divers pointeurs ou handles, tels que display(), screen(), colormap() et visual(). Nous pouvons les utiliser pour congurer un contexte graphique X11 sur un QPixmap ou un QWidget, par exemple. Les applications Qt qui doivent collaborer avec dautres toolkits ou bibliothques ont souvent besoin daccder aux vnements de bas niveau sous X11, MSGs sous Windows, EventRef sous Mac OS X, QWSEvents sous Qtopia) avant quils ne soient convertis en QEvents. Nous pouvons procder en drivant QApplication et en rimplmentant le ltre dvnement spcique la plate-forme appropri, cest--dire x11EventFilter(), winEventFilter(),

Chapitre 20

Fonctionnalits spciques la plate-forme

447

Mac OS X

ATSFontFormatRef CGImageRef GWorldPtr GWorldPtr RgnHandle HIViewRef


Windows HCURSOR HDC HDC HFONT HPALETTE HRGN HWND X11 Cursor Font Picture Picture Pixmap QX11Info QX11Info Region Screen SmcConn Window Window

QFont::handle() QPixmap::macCGHandle() QPixmap::macQDAlphaHandle() QPixmap::macQDHandle() QRegion::handle() QWidget::winId()

QCursor::handle() QPaintEngine::getDC() QPrintEngine::getPrinterDC() QFont::handle() QColormap::hPal() QRegion::handle() QWidget::winId()

QCursor::handle() QFont::handle() QPixmap::x11PictureHandle() QWidget::x11PictureHandle() QPixmap::handle() QPixmap::x11Info() QWidget::x11Info() QRegion::handle() QCursor::x11Screen() QSessionManager::handle() QWidget::handle() QWidget::winId()

Figure 20.2 Fonctions spciques la plate-forme pour accder aux handles de bas niveau

448

Qt4 et C++ : Programmation dinterfaces GUI

macEventFilter(), ou qwsEventFilter(). Nous pouvons aussi accder aux vnements de plate-forme qui sont envoys un widget donn en rimplmentant un des ltres x11Event(), winEvent(), macEvent(), et qwsEvent(). Cela pourrait prsenter de lintrt pour la gestion de certains types dvnement qui seraient normalement ignors dans QT, comme les vnements de manette de jeu. Vous trouverez des informations complmentaires concernant les dtails spciques chaque plate-forme, notamment comment dployer les applications Qt sur diffrentes plates-formes, ladresse http://doc.trolltech.com/4.1/win-system.html.

ActiveX sous Windows


La technologie ActiveX de Microsoft permet aux applications dincorporer des composants dinterface utilisateur fournis par dautres applications ou bibliothques. Elle est base sur Microsoft COM et dnit un jeu dinterfaces pour application qui emploie des composants et un autre jeu dinterfaces pour application et bibliothque qui fournit les composants. La version Qt/Windows Desktop Edition fournit le Framework ActiveQt et combine de faon homogne ActiveX et Qt. ActiveQt est constitu de deux modules : Le module QAxContainer nous permet dutiliser les objets COM et dintgrer des contrles ActiveX dans les applications Qt. Le module QAxServer nous permet dexporter des objets COM personnaliss et des contrles ActiveX crits avec Qt. Notre premier exemple va intgrer lapplication Windows Media Player dans une application Qt laide du module QAxContainer. Lapplication Qt ajoute un bouton Open, un bouton Play/Pause, un bouton Stop et un curseur au contrle ActiveX de Windows Media Player.
Figure 20.3 Lapplication Media Player

La fentre principale de lapplication est de type PlayerWindow:

Chapitre 20

Fonctionnalits spciques la plate-forme

449

class PlayerWindow: public QWidget { Q_OBJECT Q_ENUMS(ReadyStateConstants) public: enum PlayStateConstants { Stopped = 0, Paused = 1, Playing = 2 }; enum ReadyStateConstants { Uninitialized = 0, Loading = 1, Interactive = 3, Complete = 4 }; PlayerWindow(); protected: void timerEvent(QTimerEvent *event); private void void void void void slots: onPlayStateChange(int oldState, int newState); onReadyStateChange(ReadyStateConstants readyState); onPositionChange(double oldPos, double newPos); sliderValueChanged(int newValue); openFile();

private: QAxWidget *wmp; QToolButton *openButton; QToolButton *playPauseButton; QToolButton *stopButton; QSlider *seekSlider; QString fileFilters; int updateTimer; };

La classe PlayerWindow hrite de QWidget. La macro Q_ENUMS() (directement sous Q_OBJECT) est indispensable pour signaler moc que le type ReadyStateConstants utilis dans le slot onReadyStateChange() est un type enum. Dans la section prive, nous dclarons la donne membre QAxWidget*.
PlayerWindow::PlayerWindow() { wmp = new QAxWidget; wmp->setControl("{22D6F312-B0F6-11D0-94AB-0080C74C7E95}");

Dans le constructeur, nous commenons par crer un objet QAxWidget an dencapsuler le contrle ActiveX Windows Media Player. Le module QAxContainer se compose de trois classes : QAxObject englobe un objet COM, QAxWidget englobe un contrle ActiveX, et QAxBase implmente la fonctionnalit COM pour QAxObject et QAxWidget. Nous appelons setControl() sur QAxWidget avec lID de la classe du contrle Windows Media Player 6.4. Une instance du composant requis va ainsi tre cre. A partir de l, toutes

450

Qt4 et C++ : Programmation dinterfaces GUI

les proprits, vnements, et mthodes du contrle ActiveX vont tre disponibles sous la forme de proprits, signaux et slots Qt, par le biais de lobjet QAxWidget.
Figure 20.4 Schma dhritage du module QAxContainer
QObject QAxBase QAxObject QWidget QAxWidget

Les types de donnes COM sont automatiquement convertis vers les types Qt correspondants, comme illustr en Figure 20.5. Par exemple, un paramtre dentre de type VARIANT_BOOL prend le type bool, et un paramtre de sortie de type VARIANT_BOOL devient un type bool &. Si le type obtenu est une classe Qt (QString, QDateTime, etc.), le paramtre dentre est une rfrence const (par exemple, const QString &).
Types COM VARIANT_BOOL char, short, int, long unsigned char, unsigned short, unsigned int, unsigned long float, double CY BSTR DATE OLE_COLOR SAFEARRAY(VARIANT) SAFEARRAY(BSTR) SAFEARRAY(BYTE) VARIANT IFontDisp * IPictureDisp * User defined type Figure 20.5 Relations entre types COM et types Qt double qlonglong, qulonglong QString QDateTime, QDate, QTime QColor QList<QVariant> QStringList QByteArray QVariant QFont QPixmap QRect, QSize, QPoint Types Qt bool int uint

Chapitre 20

Fonctionnalits spciques la plate-forme

451

Pour obtenir la liste de toutes les proprits, signaux et slots disponibles pour un QAxObject ou un QAxWidget avec leurs types de donnes Qt, appelez QAxBase::generateDocumentation() ou faites appel loutil de ligne de commande de Qt dumpdoc, que vous trouverez dans le rpertoire tools\activeqt\dumpdoc de Qt. Examinons maintenant le constructeur de PlayerWindow:
wmp->setProperty("ShowControls", false); wmp->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); connect(wmp, SIGNAL(PlayStateChange(int, int)), this, SLOT(onPlayStateChange(int, int))); connect(wmp, SIGNAL(ReadyStateChange(ReadyStateConstants)), this, SLOT(onReadyStateChange(ReadyStateConstants))); connect(wmp, SIGNAL(PositionChange(double, double)), this, SLOT(onPositionChange(double, double)));

Aprs lappel de QAxWidget::setControl(), nous appelons QObject::setProperty() pour dnir la proprit ShowControls du Windows Media Player en false, puisque nous fournissons nos propres boutons pour manipuler le composant. QObject::setProperty() peut tre utilise la fois pour les proprits COM et pour les proprits Qt normales. Son second paramtre est de type QVariant. Nous appelons ensuite setSizePolicy() de sorte que le contrle ActiveX occupe toute la place disponible dans la disposition, et nous connectons trois vnements ActiveX depuis le composant COM vers les trois slots.
stopButton = new QToolButton; stopButton->setText(tr("&Stop")); stopButton->setEnabled(false); connect(stopButton, SIGNAL(clicked()), wmp, SLOT(Stop())); }

La n du constructeur PlayerWindow suit le modle habituel, sauf que nous connectons quelques signaux Qt aux slots fournis par lobjet COM (Play(), Pause(), et Stop()). Les boutons tant analogues, nous ne prsentons ici que limplmentation du bouton Stop. Il est temps daborder la fonction timerEvent():
void PlayerWindow::timerEvent(QTimerEvent *event) { if (event->timerId() == updateTimer) { double curPos = wmp->property("CurrentPosition").toDouble(); onPositionChange(-1, curPos); } else { QWidget::timerEvent(event); } }

452

Qt4 et C++ : Programmation dinterfaces GUI

La fonction timerEvent() est appele intervalles rguliers pendant la diffusion dun clip. Nous lemployons pour faire avancer le curseur. Pour ce faire, nous appelons property() sur le contrle ActiveX an dobtenir la valeur de la proprit CurrentPosition en tant que QVariant et nous appelons toDouble() pour le convertir en double. Nous appelons alors onPositionChange() pour raliser la mise jour. La suite du code prsente peu dintrt parce quelle ne concerne pas directement ActiveX et quelle ne comporte rien qui nait dj t abord. Le code complet est inclus sur le CD-ROM. Dans le chier .pro, nous devons introduire lentre suivante pour tablir une liaison avec le module QAxContainer :
CONFIG += qaxcontainer

Lorsquon gre des objets COM, on a souvent besoin dappeler directement une mthode COM (et pas seulement de la connecter un signal Qt). La mthode la plus simple de procder est dinvoquer QAxBase::dynamicCall() avec le nom et la signature de la mthode en premier paramtre et les arguments de cette dernire comme paramtres supplmentaires. Saisissez par exemple :
wmp->dynamicCall("TitlePlay(uint)", 6);

La fonction dynamicCall() reoit jusqu huit paramtres de type QVariant et renvoie un QVariant. Si nous avons besoin de transmettre un IDispatch* ou un IUnknown* de cette faon, nous pouvons encapsuler le composant dans un QAxObject et appeler asVariant() sur ce dernier pour le convertir en QVariant. Si nous avons besoin dappeler une mthode COM qui renvoie un IDispatch* ou un IUnknown*, ou si nous avons besoin daccder une proprit COM appartenant lun de ces types, nous pouvons opter plutt pour querySubObject():
QAxObject *session = outlook.querySubObject("Session"); QAxObject *defaultContacts = session->querySubObject("GetDefaultFolder(OlDefaultFolders)", "olFolderContacts");

Si nous dsirons appeler des mthodes dont la liste de paramtres contient des types de donnes non pris en charge, nous pouvons utiliser QAxBase::queryInterface()pour rcuprer linterface COM et appeler directement la mthode. Comme toujours avec COM, nous devons appeler Release() quand nous navons plus besoin de linterface. Sil devient trs frquent davoir appeler de telles mthodes, nous pouvons driver QAxObject ou QAxWidget et fournir des fonctions membres qui encapsulent les appels de linterface COM. Vous ne devez pas ignorer que les sous classes QAxObject et QAxWidget ne peuvent pas dnir leurs propres proprits, signaux, ou slots. Nous allons maintenant dtailler le module QAxServer. Il nous permet de transformer un programme Qt standard en serveur ActiveX. Le serveur peut tre une bibliothque partage ou une application autonome. Les serveurs dnis en tant que bibliothques partages sont souvent appels serveurs in-process (in-process signie "qui sexcute dans lespace de traitement dun client") ; les applications autonomes sont nommes serveurs hors processus.

Chapitre 20

Fonctionnalits spciques la plate-forme

453

Notre premier exemple de QAxServer est un serveur in-process qui fournit un widget afchant une bille qui se balance de gauche droite. Nous allons aussi expliquer comment intgrer le widget dans Internet Explorer. Voici le dbut de la dnition de classe du widget AxBouncer:
class AxBouncer: public QWidget, public QAxBindable { Q_OBJECT Q_ENUMS(SpeedValue) Q_PROPERTY(QColor color READ color WRITE setColor) Q_PROPERTY(SpeedValue speed READ speed WRITE setSpeed) Q_PROPERTY(int radius READ radius WRITE setRadius) Q_PROPERTY(bool running READ isRunning)

AxBouncer hrite la fois de QWidget et QAxBindable. La classe QAxBindable fournit une interface entre le widget et un client ActiveX. Tout QWidget peut tre export sous forme de contrle ActiveX, mais en drivant QAxBindable nous pouvons informer le client des changements de valeur dune proprit, et nous pouvons implmenter des interfaces COM pour complter celles dj implmentes par QAxServer.
Figure 20.6 Le widget AxBouncer dans Internet Explorer

En prsence dhritage multiple impliquant une classe drive de QObject, nous devons toujours positionner la classe drive de QObject en premier de sorte que moc puisque la rcuprer. Nous dclarons trois proprits de lecture-criture et une proprit en lecture seulement. La macro Q_ ENUMS() est ncessaire pour signaler moc que le type SpeedValue est un type enum. Lnumration est dclare dans la section publique de la classe :
public: enum SpeedValue { Slow, Normal, Fast };

454

Qt4 et C++ : Programmation dinterfaces GUI

AxBouncer(QWidget *parent = 0); void setSpeed(SpeedValue newSpeed); SpeedValue speed() const { return ballSpeed; } void setRadius(int newRadius); int radius() const { return ballRadius; } void setColor(const QColor &newColor); QColor color() const { return ballColor; } bool isRunning() const { return myTimerId!= 0; } QSize sizeHint() const; QAxAggregated *createAggregate(); public slots: void start(); void stop(); signals: void bouncing();

Le constructeur de AxBouncer est un constructeur standard pour un widget, avec un paramtre parent. La macro QAXFACTORY_DEFAULT(), que nous utiliserons pour exporter le composant, doit recevoir un constructeur avec sa signature. La fonction createAggregate() est rimplmente dans QAxBindable. Nous ltudierons un peu plus loin.
protected: void paintEvent(QPaintEvent *event); void timerEvent(QTimerEvent *event); private: int intervalInMilliseconds() const; QColor ballColor; SpeedValue ballSpeed; int ballRadius; int myTimerId; int x; int delta; };

Les sections protges et prives de la classe sont identiques celles que nous aurions sil sagissait dun widget Qt standard.
AxBouncer::AxBouncer(QWidget *parent) : QWidget(parent) { ballColor = Qt::blue; ballSpeed = Normal; ballRadius = 15; myTimerId = 0; x = 20; delta = 2; }

Chapitre 20

Fonctionnalits spciques la plate-forme

455

Le constructeur de AxBouncer initialise les variables prives de la classe.


void AxBouncer::setColor(const QColor &newColor) { if (newColor!= ballColor && requestPropertyChange("color")) { ballColor = newColor; update(); propertyChanged("color"); } }

La fonction setColor() dnie la valeur de la proprit color. Elle appelle update() pour redessiner le widget. Les appels de requestPropertyChange() et propertyChanged() sont plus inhabituels. Ces fonctions drivent de QAxBindable et devraient normalement tre appeles chaque fois que nous modions une proprit. requestPropertyChange() demande au client la permission de changer une proprit, et renvoie true si le client accepte. La fonction propertyChanged() signale au client que la proprit a chang. Les mthodes daccs de proprit setSpeed() et setRadius() suivent galement ce modle, ainsi que les slots start() et stop(), puisquils modient la valeur de la proprit running. La fonction membre AxBouncer ne doit pas tre oublie :
QAxAggregated *AxBouncer::createAggregate() { return new ObjectSafetyImpl; }

La fonction createAggregate() est rimplmente dans QAxBindable. Elle nous permet dimplmenter les interfaces COM que le module QAxServer nimplmente pas dj ou de remplacer les interfaces COM par dfaut de QAxServer. Ici, nous le faisons pour fournir linterface IObjectSafety, qui est celle partir de laquelle Internet Explorer accde aux options de scurit du composant. Voici lastuce standard pour se dbarrasser du fameux message derreur "Objet non scuris pour le script" dInternet Explorer. Voici la dnition de la classe qui implmente linterface IObjectSafety:
class ObjectSafetyImpl: public QAxAggregated, public IObjectSafety { public: long queryInterface(const QUuid &iid, void **iface); QAXAGG_IUNKNOWN HRESULT WINAPI GetInterfaceSafetyOptions(REFIID riid, DWORD *pdwSupportedOptions, DWORD *pdwEnabledOptions);

456

Qt4 et C++ : Programmation dinterfaces GUI

HRESULT WINAPI SetInterfaceSafetyOptions(REFIID riid, DWORD pdwSupportedOptions, DWORD pdwEnabledOptions); };

La classe ObjectSafetyImpl hrite la fois de QAxAggregated et de IObjectSafety. La classe QAxAggregated est une classe de base abstraite pour limplmentation dinterfaces COM complmentaires. Lobjet COM tendu par QAxAggregated est accessible par le biais de controllingUnknown(). Cet objet est cr en arrire-plan par le module QAxServer. La macro QAXAGG_IUNKNOWN fournit les implmentations standard de QueryInterface(), AddRef(), et Release(). Ces implmentations appellent simplement la mme fonction sur lobjet COM contrleur.
long ObjectSafetyImpl::queryInterface(const QUuid &iid, void **iface) { *iface = 0; if (iid == IID_IObjectSafety) { *iface = static_cast<IObjectSafety *>(this); } else { return E_NOINTERFACE; } AddRef(); return S_OK; }

La fonction queryInterface() est une fonction virtuelle pure de QAxAggregated. Elle est appele par lobjet COM contrleur an doffrir laccs aux interfaces fournies par la sous classe QAxAggregated. Nous devons renvoyer E_NOINTERFACE pour les interfaces que nous nimplmentons pas et pour IUnknown.
HRESULT WINAPI ObjectSafetyImpl::GetInterfaceSafetyOptions( REFIID /* riid */, DWORD *pdwSupportedOptions, DWORD *pdwEnabledOptions) { *pdwSupportedOptions = INTERFACESAFE_FOR_UNTRUSTED_DATA | INTERFACESAFE_FOR_UNTRUSTED_CALLER; *pdwEnabledOptions = *pdwSupportedOptions; return S_OK; } HRESULT WINAPI ObjectSafetyImpl::SetInterfaceSafetyOptions( REFIID /* riid */, DWORD /* pdwSupportedOptions */, DWORD /* pdwEnabledOptions */) { return S_OK; }

Les fonctions GetInterfaceSafetyOptions() et SetInterfaceSafetyOptions() sont dclares dans IObjectSafety. Nous les implmentons pour annoncer tous que notre objet est bien scuris pour les scripts.

Chapitre 20

Fonctionnalits spciques la plate-forme

457

Examinons maintenant main.cpp:


#include <QAxFactory> #include "axbouncer.h" QAXFACTORY_DEFAULT(AxBouncer, "{5e2461aa-a3e8-4f7a-8b04-307459a4c08c}", "{533af11f-4899-43de-8b7f-2ddf588d1015}", "{772c14a5-a840-4023-b79d-19549ece0cd9}", "{dbce1e56-70dd-4f74-85e0-95c65d86254d}", "{3f3db5e0-78ff-4e35-8a5d-3d3b96c83e09}")

La macro QAXFACTORY_DEFAULT() exporte un contrle ActiveX. Nous pouvons lutiliser pour les serveurs ActiveX qui exportent un seul contrle. Lexemple suivant de cette section montre comment exporter plusieurs contrles ActiveX. Le premier argument destin QAXFACTORY_DEFAULT() est le nom de la classe Qt exporter. Cest aussi le nom sous lequel le contrle est export. Les cinq autres arguments sont lID de la classe, de linterface, de linterface de lvnement, de la bibliothque des types, et de lapplication. Nous pouvons gnrer ces identicateurs laide dun outil standard tel que guidgen ou uuidgen. Le serveur tant une bibliothque, nous navons pas besoin de la fonction main(). Voici le chier .pro pour notre serveur ActiveX in-process :
TEMPLATE CONFIG HEADERS SOURCES RC_FILE DEF_FILE = lib += dll qaxserver = axbouncer.h \ objectsafetyimpl.h = axbouncer.cpp \ main.cpp \ objectsafetyimpl.cpp = qaxserver.rc = qaxserver.def

Les chiers qaxserver.rc et qaxserver.def auxquels il est fait rfrence dans le chier .pro sont des chiers standards que lon peut copier dans le rpertoire src\activeqt\control de Qt. Le make?le ou le chier de projet Visual C++ gnr par qmake contient les rgles qui rgissent lenregistrement du serveur dans le registre de Windows. Pour enregistrer le serveur sur les machines utilisateur, nous pouvons faire appel loutil regsvr32 disponible sur tous les systmes Windows. Nous incluons alors le composant Bouncer dans une page HTML via la balise <object>:
<object id="AxBouncer" classid="clsid:5e2461aa-a3e8-4f7a-8b04-307459a4c08c"> <b>The ActiveX control is not available. Make sure you have built and registered the component server.</b> </object>

458

Qt4 et C++ : Programmation dinterfaces GUI

Il est possible de crer des boutons qui invoquent des slots :


<input type="button" value="Start" onClick="AxBouncer.start()"> <input type="button" value="Stop" onClick="AxBouncer.stop()">

Nous pouvons manipuler le widget laide de JavaScript ou VBScript comme nimporte quel autre contrle ActiveX. Le chier demo.html propos sur le CD prsente une page rudimentaire qui utilise le serveur ActiveX. Notre dernier exemple est une application Carnet dadresse. Elle peut se comporter comme une application standard Qt/Windows ou comme un serveur ActiveX hors processus. Cette dernire option nous permet de crer le script de lapplication en Visual Basic, par exemple.
class AddressBook: public QMainWindow { Q_OBJECT Q_PROPERTY(int count READ count) Q_CLASSINFO("ClassID", "{588141ef-110d-4beb-95ab-ee6a478b576d}") Q_CLASSINFO("InterfaceID", "{718780ec-b30c-4d88-83b3-79b3d9e78502}") Q_CLASSINFO("ToSuperClass", "AddressBook") public: AddressBook(QWidget *parent = 0); ~AddressBook(); int count() const; public slots: ABItem *createEntry(const QString &contact); ABItem *findEntry(const QString &contact) const; ABItem *entryAt(int index) const; private slots: void addEntry(); void editEntry(); void deleteEntry(); private: void createActions(); void createMenus(); QTreeWidget *treeWidget; QMenu *fileMenu; QMenu *editMenu; QAction *exitAction; QAction *addEntryAction; QAction *editEntryAction; QAction *deleteEntryAction; };

Chapitre 20

Fonctionnalits spciques la plate-forme

459

Le widget AddressBook correspond la fentre principale de lapplication. La proprit et les slots fournis seront disponibles via le script. La macro Q_CLASSINFO() permet de spcier la classe et les identiants dinterface associs cette dernire. Ceux-ci ont t gnrs avec un outil tel que guid ou uuid. Dans lexemple prcdent, nous avions spci la classe et les identiants dinterface quand nous avions export la classe QAxBouncer laide de la macro QAXFACTORY_DEFAULT(). Dans cet exemple, nous allons exporter plusieurs classes, nous ne pouvons donc pas excuter QAXFACTORY_DEFAULT(). Nous avons deux options : Driver QAxFactory, rimplmenter ses fonctions virtuelles pour fournir des informations concernant les types exporter, et excuter la macro QAXFACTORY_EXPORT() pour enregistrer le composant fabricant. Excuter les macros QAXFACTORY_BEGIN(), QAXFACTORY_END(), QAXCLASS(), et QAXTYPE() pour dclarer et enregistrer le composant fabricant. Cette approche exige de spcier la classe et lidentiant dinterface laide de Q_CLASSINFO(). Voici la dnition de la classe AddressBook: La troisime occurrence de Q_CLASSINFO() pourrait vous sembler un peu bizarre. Par dfaut, les contrles ActiveX exposent non seulement leurs propres proprits, signaux, et slots leurs clients, mais aussi ceux de leurs superclasses jusqu QWidget. Lattribut ToSuperClass permet de spcier la superclasse de niveau suprieur (dans larbre dhritage) que nous dsirons exposer. Nous spcions ici le nom de la classe du composant (AddressBook) en tant que classe de niveau le plus haut exporter, ce qui signie que les proprits, signaux, et slots dnis dans les superclasses dAddressBook ne seront pas exports.
class ABItem: public QObject, public QTreeWidgetItem { Q_OBJECT Q_PROPERTY(QString contact READ contact WRITE setContact) Q_PROPERTY(QString address READ address WRITE setAddress) Q_PROPERTY(QString phoneNumber READ phoneNumber WRITE setPhoneNumber) Q_CLASSINFO("ClassID", "{bc82730e-5f39-4e5c-96be-461c2cd0d282}") Q_CLASSINFO("InterfaceID", "{c8bc1656-870e-48a9-9937-fbe1ceff8b2e}") Q_CLASSINFO("ToSuperClass", "ABItem") public: ABItem(QTreeWidget *treeWidget); void setContact(const QString &contact); QString contact() const { return text(0); } void setAddress(const QString &address); QString address() const { return text(1); } void setPhoneNumber(const QString &number); QString phoneNumber() const { return text(2); } public slots: void remove(); };

460

Qt4 et C++ : Programmation dinterfaces GUI

La classe ABItem reprsente une entre dans le carnet dadresses. Elle hrite de QTreeWidgetItem pour pouvoir tre afche dans un QTreeWidget et de QObject pour pouvoir tre exporte sous forme dobjet COM.
int main(int argc, char *argv[]) { QApplication app(argc, argv); if (!QAxFactory::isServer()) { AddressBook addressBook; addressBook.show(); return app.exec(); } return app.exec(); }

Dans main(), nous vrions si lapplication sexcute en autonome ou en tant que serveur. Loption de ligne de commande -activex est reconnue par QApplication et excute lapplication en tant que serveur. Si lapplication nest pas excute de cette faon, nous crons le widget principal et nous lafchons comme nous le ferions normalement pour une application Qt autonome. En complment de -activex, les serveurs ActiveX comprennent les options de ligne de commande suivantes : -regserver enregistre le serveur dans le registre systme. -unregserver annule lenregistrement du serveur dans le registre systme. -dumpidl file inscrit lIDL (Interface Denition Langage) du serveur dans le chier spci. Lorsque lapplication sexcute en tant que serveur, nous devons exporter les classes AddressBook et ABItem en tant que composants COM :
QAXFACTORY_BEGIN("{2b2b6f3e-86cf-4c49-9df5-80483b47f17b}", "{8e827b25-148b-4307-ba7d-23f275244818}") QAXCLASS(AddressBook) QAXTYPE(ABItem) QAXFACTORY_END()

Les macros prcdentes exportent un composant fabricant dobjets COM. Puisque nous devons exporter deux types dobjets COM, nous ne pouvons pas nous contenter dexcuter QAXFACTORY_DEFAULT() comme nous lavions fait dans lexemple prcdent. Le premier argument de QAXFACTORY_BEGIN() correspond lID de bibliothque de types ; le second est lID de lapplication. Entre QAXFACTORY_BEGIN() et QAXFACTORY_ END(), nous spcions toutes les classes pouvant tre instancies et tous les types de donnes qui ont besoin dtre accessibles sous forme dobjets COM. Voici le chier .pro pour notre serveur ActiveX hors processus :
TEMPLATE CONFIG = app += qaxserver

Chapitre 20

Fonctionnalits spciques la plate-forme

461

HEADERS

SOURCES

FORMS RC_FILE

= abitem.h \ addressbook.h \ editdialog.h = abitem.cpp \ addressbook.cpp \ editdialog.cpp \ main.cpp = editdialog.ui = qaxserver.rc

Le chier qaxserver.rc auquel il est fait rfrence dans le chier .pro est un chier standard que lon peut copier dans le rpertoire src\activeqt\control de Qt. Cherchez dans le rpertoire vb de lexemple un projet Visual Basic qui utilise le serveur de carnet dadresses. Nous en avons termin avec la prsentation du framework ActiveQt. La distribution de Qt propose des exemples supplmentaires, et la documentation contient des informations concernant la faon de crer les modules QAxContainer et QAxServer et comment rsoudre les problmes dinteroprabilit courants.

Prendre en charge la gestion de session X11


Lorsque nous quittons X11, certains gestionnaires de fentre nous demandent si nous dsirons enregistrer la session. Si nous rpondons oui, les applications en cours dexcution seront automatiquement redmarres lors de la prochaine ouverture de session, au mme emplacement sur lcran et, cerise sur le gteau, dans le mme tat. Le composant X11 charg de lenregistrement et de la restauration de la session est le gestionnaire de session. Pour quune application Qt/X11 puisse tre prise en charge par ce gestionnaire, nous devons rimplmenter QApplication::saveState() an denregistrer ltat de lapplication.
Figure 20.7 Fermeture de session sous KDE

462

Qt4 et C++ : Programmation dinterfaces GUI

Windows 2000 et XP, et certains systmes Unix, proposent un autre mcanisme nomm Mise en veille. Ds que lutilisateur active la mise en veille, le systme dexploitation sauvegarde simplement la mmoire de lordinateur sur disque puis la recharge au moment de la ractivation. Les applications ne sont pas sollicites et nont pas besoin dtre averties de lopration. Lorsque lutilisateur demande larrt de lordinateur, nous pouvons prendre le contrle juste avant lexcution de cette opration en rimplmentant QApplication::commitData(). Nous avons ainsi la possibilit denregistrer toute donne non sauvegarde et de dialoguer avec lutilisateur si ncessaire. Cette partie de la gestion de session est prise en charge sur les deux plates-formes X11 et Windows. Notre tude de la gestion de session va seffectuer en analysant le code dune application Tic-Tac-Toe compatible avec cette fonction. Commenons par examiner la fonction main():
int main(int argc, char *argv[]) { Application app(argc, argv); TicTacToe toe; toe.setObjectName("toe"); app.setTicTacToe(&toe); toe.show(); return app.exec(); }

Nous crons un objet Application. La classe Application hrite de QApplication et rimplmente la fois commitData() et saveState() an de prendre en charge la gestion de session. Nous crons ensuite un widget TicTacToe, que nous associons lobjet Application, puis nous lafchons. Nous avons appel le widget TicTacToe. Nous devons attribuer des noms dobjet uniques aux widgets de niveau suprieur si nous voulons que le gestionnaire de session soit en mesure de restaurer les tailles et positions des fentres.
Figure 20.8 Lapplication Tic-Tac-Toe

Chapitre 20

Fonctionnalits spciques la plate-forme

463

Voici la dnition de la classe Application:


class Application: public QApplication { Q_OBJECT public: Application(int &argc, char *argv[]); void setTicTacToe(TicTacToe *tic); void saveState(QSessionManager &sessionManager); void commitData(QSessionManager &sessionManager); private: TicTacToe *ticTacToe; };

La classe Application stocke un pointeur vers le widget TicTacToe dans une variable prive.
void Application::saveState(QSessionManager &sessionManager) { QString fileName = ticTacToe->saveState(); QStringList discardCommand; discardCommand << "rm" << fileName; sessionManager.setDiscardCommand(discardCommand); }

Sous X11, la fonction saveState() est appele au moment o le gestionnaire de session veux que lapplication enregistre son tat. La fonction est aussi disponible sur dautres platesformes, mais elle nest jamais appele. Le paramtre QSessionManager nous permet de communiquer avec le gestionnaire de session. Nous commenons par demander au widget TicTacToe denregistrer son tat dans un chier. Nous affectons ensuite une valeur la commande dannulation du gestionnaire. Cette commande est celle que le gestionnaire de session doit excuter pour supprimer toute information stocke concernant ltat courant. Pour cet exemple, nous la dnissons en
rm sessionfile

o sessionfile est le nom du chier qui contient ltat enregistr pour la session, et rm est la commande Unix standard pour supprimer des chiers. Le gestionnaire de session comporte aussi une commande de redmarrage. Il sagit de celle que le gestionnaire excute pour redmarrer lapplication. Par dfaut, Qt fournit la commande de redmarrage suivante :
appname -session id_key

464

Qt4 et C++ : Programmation dinterfaces GUI

La premire partie, appname, est drive de argv[0]. Le composant id correspond lidentiant de session fourni par le gestionnaire de session; dont lunicit est garantie au sein de plusieurs applications et de diffrentes excutions de la mme application. La partie key est ajoute an didentier de faon unique lheure laquelle ltat a t enregistr. Pour diverses raisons, le gestionnaire de session peut appeler plusieurs fois saveState() au cours dune mme session, et les diffrents tats doivent pouvoir tre distingus. Etant donn les limites des gestionnaires de session existants, nous devons nous assurer que le rpertoire de lapplication se trouve dans la variable denvironnement PATH si nous voulons que lapplication puisse redmarrer correctement. Si vous dsirez en particulier tester lexemple Tic-Tac-Toe, vous devez linstaller dans le rpertoire /usr/bin par exemple et linvoquer en tapant tictactoe. Pour des applications simples, comme Tic-Tac-Toe, nous pourrions enregistrer ltat sous forme dargument de ligne de commande supplmentaire de la commande de redmarrage. Par exemple :
tictactoe -state OX-XO-X-O

Ceci nous viterait davoir stocker les donnes dans un chier puis fournir une commande dannulation pour supprimer le chier.
void Application::commitData(QSessionManager &sessionManager) { if (ticTacToe->gameInProgress() && sessionManager.allowsInteraction()) { int r = QMessageBox::warning(ticTacToe, tr("Tic-Tac-Toe"), tr("The game hasnt finished.\n" "Do you really want to quit?"), QMessageBox::Yes | QMessageBox::Default, QMessageBox::No | QMessageBox::Escape); if (r == QMessageBox::Yes) { sessionManager.release(); } else { sessionManager.cancel(); } } }

La fonction commitData() est appele quand lutilisateur ferme la session. Nous pouvons la rimplmenter de sorte dafcher un message davertissement qui signale lutilisateur le risque de perte de donnes. Limplmentation par dfaut ferme tous les widgets de niveau le plus haut, ce qui donne le mme rsultat que lorsque lutilisateur ferme les fentres lune aprs lautre en cliquant sur le bouton de fermeture de leur barre de titre. Au Chapitre 3, nous avons vu comment rimplmenter closeEvent() pour dtecter cette situation et afcher un message. Pour cet exemple, nous allons rimplmenter commitData() et afcher un message demandant lutilisateur de conrmer la fermeture de session si un jeu est en cours dexcution et si le gestionnaire de session nous permet de dialoguer avec lutilisateur. Si lutilisateur clique sur

Chapitre 20

Fonctionnalits spciques la plate-forme

465

Yes, nous appelons release() pour ordonner au gestionnaire de poursuivre la fermeture de session ; sil clique sur No, nous appelons cancel() pour annuler lopration.
Figure 20.9 "Vous dsirez vraiment quitter ?"

Examinons maintenant la classe TicTacToe:


class TicTacToe: public QWidget { Q_OBJECT public: TicTacToe(QWidget *parent = 0); bool gameInProgress() const; QString saveState() const; QSize sizeHint() const; protected: void paintEvent(QPaintEvent *event); void mousePressEvent(QMouseEvent *event); private: enum { Empty = -, Cross = X, Nought = O }; void clearBoard(); void restoreState(); QString sessionFileName() const; QRect cellRect(int row, int column) const; int cellWidth() const { return width() / 3; } int cellHeight() const { return height() / 3; } bool threeInARow(int row1, int col1, int row3, int col3) const; char board[3][3]; int turnNumber; };

La classe TicTacToe hrite de QWidget et rimplmente sizeHint(), paintEvent(), et mousePressEvent(). Elle fournit aussi les fonctions gameInProgress() et saveState() que nous avions utilises dans notre classe Application.
TicTacToe::TicTacToe(QWidget *parent) : QWidget(parent) {

466

Qt4 et C++ : Programmation dinterfaces GUI

clearBoard(); if (qApp->isSessionRestored()) restoreState(); setWindowTitle(tr("Tic-Tac-Toe")); }

Dans le constructeur, nous effaons le tableau et si lapplication avait t invoque avec loption -session, nous appelons la fonction prive restoreState() pour recharger lancienne session.
void TicTacToe::clearBoard() { for (int row = 0; row < 3; ++row) { for (int column = 0; column < 3; ++column) { board[row][column] = Empty; } } turnNumber = 0; }

Dans clearBoard(), nous effaons toutes les cellules et nous dnissons turnNumber 0.
QString TicTacToe::saveState() const { QFile file(sessionFileName()); if (file.open(QIODevice::WriteOnly)) { QTextStream out(&file); for (int row = 0; row < 3; ++row) { for (int column = 0; column < 3; ++column) out << board[row][column]; } } return file.fileName(); }

Dans saveState(), nous enregistrons ltat du tableau sur disque. Le format est simple, avec X pour les croix, O pour les ronds, et -+- pour les cellules vides.
QString TicTacToe::sessionFileName() const { return QDir::homePath() + "/.tictactoe_" + qApp->sessionId() + "_" + qApp->sessionKey(); }

La fonction prive sessionFileName() renvoie le nom de chier pour lID et la cl de session en cours. Cette fonction est exploite la fois par saveState() et par restoreState(). Le nom du chier est constitu partir de ces ID et cl de session.
void TicTacToe::restoreState() { QFile file(sessionFileName());

Chapitre 20

Fonctionnalits spciques la plate-forme

467

if (file.open(QIODevice::ReadOnly)) { QTextStream in(&file); for (int row = 0; row < 3; ++row) { for (int column = 0; column < 3; ++column) { in >> board[row][column]; if (board[row][column]!= Empty) ++turnNumber; } } } update(); }

Dans restoreState(), nous chargeons le chier qui correspond la session restaure et nous remplissons le tableau avec ces informations. Nous dduisons la valeur de turnNumber partir du nombre de X et O sur le tableau. Dans le constructeur de TicTacToe, nous appelons restoreState() lorsque QApplication::isSessionRestored() renvoie true. Dans ce cas, sessionId() et sessionKey() renvoient les mmes valeurs que lorsque ltat de lapplication tait enregistr, et sessionFileName() renvoie le nom de chier pour cette session. Les tests et le dbogage de la gestion de session peuvent tre pnibles, parce que vous devez continuellement vous connecter puis vous dconnecter. Un moyen dviter ces oprations consiste utiliser lutilitaire standard xsm fourni avec X11. Le premier appel de xsm ouvre une fentre du gestionnaire de session et un terminal. Les applications dmarres dans ce terminal vont toutes utiliser xsm comme gestionnaire de session plutt que celui du systme. Nous pouvons alors nous servir de la fentre de xsm pour terminer, redmarrer, ou supprimer une session, et voir si notre application se comporte normalement. Vous trouverez tous les dtails de cette procdure ladresse http://doc.trolltech.com/4.1/session.html.

21
Programmation embarque
Au sommaire de ce chapitre Dmarrer avec Qtopia Personnaliser Qtopia Core

Le dveloppement des logiciels destins sexcuter sur des priphriques mobiles tels que les PDA et les tlphones portables prsente des difcults bien spciques parce que les systmes embarqus possdent gnralement des processeurs plus lents, une capacit de mmoire permanente rduites (mmoire ash ou disque dur), moins de mmoire et un cran plus petit que les ordinateurs de bureau. Qtopia Core (prcdemment nomm Qt/Embedded) est une version de Qt optimise pour le systme dexploitation Linux embarqu. Qtopia Core fournit les mmes outils et la mme API que les versions de bureau de Qt (Qt/Windows, Qt/X 11 et Qt/Mac) complts des classes et outils requis pour la programmation embarque. Par le biais dune double licence, ce systme est disponible la fois pour le dveloppement open source et le dveloppement commercial.

470

Qt4 et C++ : Programmation dinterfaces GUI

Qtopia Core peut sexcuter sur nimporte quel matriel quip de Linux (notamment les architectures Intel x86, MIPS, ARM, StrongARM, Motorola 68000, et PowerPC). Il comporte une mmoire dimage et prend en charge un compilateur C++. Contrairement Qt/X11, il na pas besoin du systme XWindow ; en fait, il implmente son propre systme de fentrage (QWS) ce qui permet doptimiser au maximum la gestion des mmoires. Pour rduire encore ses besoins en mmoire, Qtopia Core peut tre recompil en excluant les fonctions non utilises. Si les applications et composants exploits sur un priphrique sont connus par avance, ils peuvent tre compils ensemble pour fournir un seul excutable avec des liens statiques vers les bibliothques de Qtopia Core. Qtopia Core bncie galement de diverses fonctionnalits qui existent aussi dans les versions bureau de Qt, notamment lusage extensif du partage de donnes implicite ("copie lors de lcriture") pour ce qui concerne la technique doptimisation de la mmoire, la prise en charge des styles de widget personnaliss via QStyle et un systme de disposition qui sadapte pour optimiser lespace cran disponible. Qtopia Core est au cur de loffre embarque de Trolltech, qui comprend galement Qtopia Platform, Qtopia PDA, et Qtopia Phone. Ces versions fournissent les classes et applications conues spciquement pour les priphriques portables et elles peuvent tre intgres avec plusieurs machines virtuelles Java tiers.

Dmarrer avec Qtopia


Les applications de Qtopia Core peuvent tre dveloppes sur toute plate-forme quipe dune chane doutils multiplate-forme. Loption la plus courante consiste installer un compilateur crois GNU C++ sur un systme UNIX. Ce processus et simpli par un script et un ensemble de correctifs fournis par Dan Kegel ladresse http://kegel.com/crosstool/. Puisque Qtopia Core contient lAPI de Qt, il est gnralement possible de travailler avec une version bureau de Qt, telle que Qt/X11 ou Qt/Windows pour la plupart des dveloppements. Le systme de conguration de Qtopia Core prend en charge les compilateurs croiss, via loption -embedded du script configure. Par exemple, pour obtenir une gnration destine larchitecture ARM, vous devriez saisir
./configure -embedded arm

Nous avons la possibilit de crer des congurations personnalises en ajoutant de nouveaux chiers dans le rpertoire mkspecs/ qws de Qt. Qtopia Core dessine directement dans la mmoire dimage de Linux (la zone de mmoire associe avec lafchage vido). Pour accder cette mmoire dimage, vous devrez accorder des permissions en criture au priphrique /dev/fb0. Pour excuter les applications de Qtopia Core, nous devons commencer par dmarrer un processus qui joue le rle de serveur. Celui-ci est charg dallouer des zones dcran aux clients et de gnrer les vnements de souris et de clavier. Toute application Qtopia Core peut devenir

Chapitre 21

Programmation embarque

471

serveur si vous spciez -qws sur sa ligne de commande ou si vous transmettez QApplication::GuiServer comme troisime paramtre du constructeur de QApplication. Les applications client communiquent avec le serveur Qtopia Core par le biais de la mmoire partage. En arrire plan, les clients se dessinent eux-mmes dans cette mmoire partage et sont chargs dafcher leurs propres dcorations de fentre. Cela permet dobtenir un niveau de communication minimum entre les clients et le serveur tout en proposant une interface utilisateur soigne. Les applications de Qtopia Core sappuient normalement sur QPainter pour se dessiner elles-mmes mais elles peuvent aussi accder au matriel vido directement laide de QDirectPainter. Les clients ont la possibilit de communiquer via le protocole QCOP. Un client peut couter sur un canal nomm en crant un objet QCopChannel et en se connectant son signal received(). Par exemple :
QCopChannel *channel = new QCopChannel("System", this); connect(channel, SIGNAL(received(const QString &, const QByteArray &)), this, SLOT(received(const QString &, const QByteArray &)));

Un message QCOP est constitu dun nom et ventuellement dun QByteArray. La fonction QCopChannel::send() statique diffuse un message sur le canal. Par exemple :
QByteArray data; QDataStream out(&data, QIODevice::WriteOnly); out << QDateTime::currentDateTime(); QCopChannel::send("System", "clockSkew(QDateTime)", data);

Lexemple prcdent illustre un idiome connu : nous nous servons de QDataStream pour coder les donnes, et pour garantir que le QByteArray sera correctement interprt par le destinataire, nous joignons le format de donnes dans le nom du message comme sil sagissait dune fonction C++. Plusieurs variables denvironnement affectent les applications de Qtopia Core. Les plus importantes sont QWS_MOUSE_PROTO et QWS_KEYBOARD, qui spcient le priphrique souris et le type de clavier. Vous trouverez une liste complte des variables denvironnement sur la page http://doc.trolltech.com/4.1/emb-envvars. html. Si UNIX est la plate-forme de dveloppement, nous pouvons tester lapplication en utilisant la mmoire dimage virtuelle de Qtopia (qvfb), une application X11 qui simule pixel par pixel la mmoire dimage relle. Cela acclre considrablement le cycle de dveloppement. Pour activer la prise en charge de la mmoire virtuelle dans Qtopia Core, vous transmettez loption -qvfb au script configure. Noubliez pas que cette option nest pas destine un usage en production. Lapplication de mmoire dimage virtuelle se trouve dans le rpertoire tools/ qvfb et peut tre invoque de la faon suivante :
qvfb -width 320 -height 480 -depth 32

Une autre option qui fonctionne sur la plupart des plates-formes consiste utiliser VNC (Virtual Network Computing) pour excuter des applications distance. Pour activer la prise en

472

Qt4 et C++ : Programmation dinterfaces GUI

charge de VNC dans Qtopia Core, vous transmettez loption -qt-gfx-vnc configure. Lancez ensuite vos applications Qtopia Core avec loption de ligne de commande -display VNC:0 et excutez un client VNC qui pointe sur lhte sur lequel vos applications sexcutent. La taille et la rsolution de lcran peuvent tre spcis en dnissant les variables denvironnement QWS_SIZE et QWS_DEPTH sur lhte qui excute les applications Qtopia Core (par exemple, QWS_SIZE=320x480 et QWS_DEPTH=32).

Personnaliser Qtopia Core


A linstallation de Qtopia Core, nous pouvons spcier les fonctionnalits dont nous navons pas besoin an de rduire loccupation mmoire. Qtopia Core comprend plus dune centaine de fonctionnalits congurables, chacune tant associe un symbole de prprocesseur. QT_NO_FILEDIALOG, par exemple, exclut QFileDialog de la bibliothque QtGui, et QT_NO_I18N renonce la prise en charge de linternationalisation. Les fonctionnalits sont numres dans le chier src/ corelib/qfeatures.txt. Qtopia Core propose cinq congurations type (minimum, small, medium, large, et dist) qui sont stockes dans les chiers src/corelib/qconfig_xxx.h. Vous spciez ces congurations via loption -qconfig xxx de configure, par exemple :
./configure -qconfig small

Pour crer des congurations personnalises, nous pouvons fournir manuellement un chier qconfig-xxx.h et lutiliser comme sil sagissait dune conguration standard. Nous pourrions aussi nous servir de loutil graphique qconfig, disponible dans le sous-rpertoire tools de Qt. Qtopia Core propose les classes suivantes pour le dialogue avec les priphriques dentre et de sortie et pour personnaliser laspect et le comportement du systme de fentrage :
Classe QScreen QScreenDriverPlugin QWSMouseHandler QMouseDriverPlugin QWSKeyboardHandler QKbdDriverPlugin QWSInputMethod QDecoration QDecorationPlugin Classe de base pour Pilotes dcran plug-in de pilote dcran Pilotes de souris Plug-in de pilotes de souris Pilotes de clavier Plug-in de pilote de clavier Mthodes dentre Styles de dcoration de fentre Plug-in fournissant des styles de dcoration de fentre

Chapitre 21

Programmation embarque

473

Vous obtenez la liste des pilotes prdnis, des mthodes dentre, et des styles de dcoration de fentre en excutant le script configure avec loption -help. Vous spciez le pilote vido laide de loption de ligne de commande -display au dmarrage du serveur Qtopia Core, comme expliqu dans la section prcdente, ou en dnissant la variable denvironnement QWS_DISPLAY. Vous spciez le pilote de souris et le priphrique associ via la variable denvironnement QWS_MOUSE_PROTO, dont la valeur suit la syntaxe type : device, o type est un des pilotes pris en charge et device le chemin daccs au priphrique (par exemple, QWS_MOUSE_PRO-TO=IntelliMouse:/dev/mouse). Les claviers sont grs dune faon analogue dans la variable denvironnement QWS_ KEYBOARD. Les mthodes dentre et dcorations de fentre sont dnies par programme dans le serveur en excutant QWSServer::setCurrentInputMethod() et QApplication::qwsSetDecoration(). Les styles de dcoration de fentre sont dnis indpendamment du style de widget, qui hrite de QStyle. Il est tout fait possible, par exemple, de dnir Windows comme style de dcoration de fentre et Plastique comme style de widget. Si vous en avez envie, les dcorations peuvent tre rgles fentre par fentre. La classe QWSServer fournit diverses fonctions pour personnaliser le systme de fentrage. Les applications qui sexcutent en tant que serveurs Qtopia Core peuvent accder linstance unique QWSServer via la variable globale qwsServer, initialise dans le constructeur de QApplication. Qtopia Core prend en charge les formats de police suivants : TrueType (TTF), PostScript Type 1, Bitmap Distribution Format (BDF), et Qt Pre-rendered Fonts (QPF). QPF tant un format brut, il est plus rapide et gnralement plus compact que des formats vectoriels tels que TTF et Type 1 si le besoin se limite une ou deux tailles diffrentes. Loutil makeqpf permet de crer des chiers QPF partir de chiers TTF ou Type 1. Une autre solution consiste excuter nos applications avec loption de ligne de commande -savefonts. Au moment dcrire ces lignes, Trolltech dveloppe une couche supplmentaire au-dessus de Qtopia Core pour rendre le dveloppement des applications embarques encore plus rapide et efcace. Une prochaine dition de cet ouvrage devrait contenir de plus amples informations ce propos.

Annexes
A B

Installer Qt
Introduction au langage C++ pour les programmeurs Java et C#

A
Installer Qt
Au sommaire de ce chapitre A propos des licences Installer Qt/Windows Installer Qt/Mac Installer Qt/X11 Cette annexe explique comment installer Qt sur votre systme partir du CD qui accompagne cet ouvrage. Ce CD comporte les ditions de Qt 4.1.1 pour Windows, Mac OS X, et X11 (pour Linux et la plupart des versions dUnix). Elles intgrent toutes SQLite, une base de donnes du domaine public, ainsi quun pilote SQLite. Les ditions de Qt fournies sur le CD vous sont proposes pour des raisons pratiques. Si vous envisagez srieusement le dveloppement logiciel, il est prfrable de tlcharger la dernire version de Qt partir de http://www.trolltech.com/download/ ou dacheter une version commercialise. Trolltech fournit aussi Qtopia Core pour la cration dapplications destines aux priphriques embarqus quips de Linux tels que les PDA et les tlphones portables. Si vous envisagez de crer des applications embarques, vous pouvez obtenir Qtopia Core depuis la page Web de tlchargement de Trolltech. Les exemples dapplication tudis dans cet ouvrage se trouvent dans le rpertoire examples du CD. De plus, Qt propose de nombreux petits exemples dapplication dans le sous rpertoire examples.

478

Qt4 et C++ : Programmation dinterfaces GUI

A propos des licences


Qt est propos sous deux formes : open source et commerciale. Les ditions open source sont disponibles gratuitement, alors que vous devez payer pour les ditions commerciales. Le logiciel du CD convient pour crer des applications destines votre usage personnel ou pour votre formation. Si vous dsirez distribuer les applications que vous allez crer avec la version Open source de Qt, vous devez respecter les termes et conditions de licence spciques au logiciel que vous utilisez pour crer ces applications. Pour les ditions Open source, ces termes et conditions impliquent lutilisation de la licence GNU General Public License (GPL). Les licences libres telles que la licence GPL accordent des droits aux utilisateurs des applications, notamment celui de visualiser et de modier le code source et de distribuer les applications (sous les mmes termes). Si vous dsirez distribuer vos applications sans le code source ou si vous voulez appliquer vos propres conditions commerciales, vous devez acheter les ditions commerciales du logiciel qui sert crer vos applications. Ces ditions vous permettent en effet de vendre et de distribuer vos applications sous vos propres termes. Le CD contient les versions GPL de Qt pour Windows, Mac OS X, et X11. Le texte lgal complet des licences est inclus avec les packages sur le CD, ainsi que les informations concernant la faon dobtenir les versions commerciales.

Installer Qt/Windows
Lorsque vous insrez le CD sur un ordinateur quip de Windows, le programme dinstallation devrait dmarrer automatiquement. Sinon, ouvrez lexplorateur de chiers pour localiser le rpertoire racine du CD et double-cliquez sur install.exe. (Il est possible que ce programme se nomme install selon la faon dont votre systme est congur.) Si vous disposez dj du compilateur MinGW C++ vous devez prciser le rpertoire dans lequel il est install ; sinon cochez la case an dinstaller aussi ce programme. La version GPL de Qt fournie sur le CD ne fonctionnera pas avec Visual C++, vous devez donc absolument installer MinGW si vous ne lavez pas encore fait. Le programme dinstallation vous propose galement dinstaller les exemples qui accompagnent cet ouvrage. Les exemples standard de Qt sont eux automatiquement installs ainsi que la documentation. Si vous choisissez dinstaller le compilateur MinGW, vous constaterez certainement un dlai entre la n de linstallation de ce dernier et le dbut de linstallation de Qt. Aprs linstallation, un nouveau dossier apparatra dans le menu Dmarrer intitul "Qt by Trolltech v4.1.1 (OpenSource)". Ce dossier propose des raccourcis vers Qt Assistant et Qt Designer, et un troisime nomm "Qt 4.1.1 Command Prompt" qui dmarre une fentre de

Annexe A

Installer Qt

479

console. Ds que vous ouvrez cette fentre, elle va dnir les variables denvironnement pour la compilation des programmes Qt avec MinGW. Cest dans cette fentre que vous allez excuter qmake et make an de gnrer vos applications Qt.

Installer Qt/Mac
Avant dinstaller Qt sur Mac OS X, vous devez dabord avoir install le jeu doutils Xcode Tools dApple. Le CD (ou le DVD) contenant ces outils est gnralement fourni avec Mac OS X ; vous pouvez aussi les tlcharger partir du site Apple Developer Connection, ladresse http://developer.apple.com. Si vous travaillez avec Mac OS X 10.4 (Tiger) et Xcode Tools 2.x (avec GCC 4.0.x), vous pouvez excuter le programme dinstallation dcrit prcdemment. Si vous possdez une version plus ancienne de Mac OS X, ou une version plus ancienne de GCC, vous devrez installer manuellement le package source. Celui-ci se nomme qt-mac-opensource4.1.1.tar.gz et il est stock dans le dossier mac sur le CD. Si vous linstallez, suivez les instructions de la section suivante qui concernent linstallation de Qt sous X11. Pour excuter le programme dinstallation, insrez le CD et double-cliquez sur le package nomm Qt.mpkg. Le programme dinstallation, Installer.app, va se lancer et Qt sera install avec les exemples standard, la documentation, et les exemples associs cet ouvrage. Cette installation seffectue dans le rpertoire /Developer, et les exemples du livre sont enregistrs dans le rpertoire /Developer/Examples/Qt4Book. Pour excuter des commandes telles que qmake et make, vous devrez avoir recours une fentre terminal, par exemple, Terminal.app dans /Applications/Utilities. Vous avez aussi la possibilit de gnrer des projets Xcode laide de qmake. Pour gnrer, par exemple, un projet Xcode pour lexemple hello, dmarrez une console telle que Terminal.app, placezvous sur le rpertoire /Developer/Examples/Qt4Book/chap01/hello, puis saisissez la commande suivante :
qmake -spec macx-xcode hello.pro

Installer Qt/X11
Pour installer Qt sur son emplacement par dfaut sous X11, vous devez tre connect en tant que root. Si vous navez pas ce niveau daccs, spciez largument -prefix de configure pour indiquer un rpertoire dans lequel vous avez lautorisation dcrire. 1. Placez-vous sur un rpertoire temporaire. Par exemple :
cd /tmp

2. Dcompressez le chier archive du CD :

480

Qt4 et C++ : Programmation dinterfaces GUI

cp /cdrom/x11/qt-x11-opensource-src-4.1.1.tgz . gunzip qt-x11-opensource-src-4.1.1.tgz tar xvf qt-x11-opensource-src-4.1.1.tar

Vous allez ainsi crer le rpertoire /tmp/qt-x11-opensource-src-4.1.1, en supposant que votre CD-ROM soit mont en /cdrom. Qt exige le logiciel GNU tar; qui se nomme gtar sur certains systmes. 3. Excutez loutil configure avec vos options favorites an de gnrer la bibliothque de Qt et les outils qui accompagnent ce framework :
cd /tmp/qt-x11-opensource-src-4.1.1 ./configure

Vous pouvez excuter ./configure -help pour obtenir une liste des options de conguration. 4. Pour gnrer Qt, saisissez
make

Cette commande va crer la bibliothque et compiler toutes les dmos, les exemples et les outils. Sur certains systmes, make se nomme gmake. 5. Pour installer Qt, saisissez
su -c "make install"

puis saisissez le mot de passe root. Ceci va installer Qt dans /usr/local/Troll-tech/ Qt-4.1.1. Vous pouvez choisir un autre rpertoire de destination via loption -prefix de configure, et si vous disposez dun accs en criture sur ce rpertoire il vous suft de saisir :
make install

6. Dnissez certaines variables denvironnement pour Qt. Si vous travailler avec le shell bash, ksh, zsh, ou sh, ajoutez les lignes suivantes dans votre chier .profile:
PATH=/usr/local/Trolltech/Qt-4.1.1/bin:$PATH export PATH

Si vous travaillez avec le shell csh ou tcsh, ajoutez la ligne suivante dans votre chier .login:
setenv PATH /usr/local/Trolltech/Qt-4.1.1/bin:$PATH

Si vous aviez prcis -prefix avec configure, utilisez le chemin indiqu plutt que le chemin par dfaut de la ligne prcdente.

Annexe A

Installer Qt

481

Si votre compilateur ne prend pas en charge rpath, vous devez aussi tendre la variable denvironnement LD_LIBRARY_PATH pour inclure /usr/local/ Trolltech/Qt-4.1.1/lib. Ce nest pas ncessaire sous Linux avec GCC. Qt est distribu avec une application de dmonstration, qtdemo, qui exploite de nombreuses fonctionnalits de la bibliothque. Elle reprsente un bon point de dpart pour tester les possibilits de Qt. Vous pouvez consulter la documentation de Qt soit en visitant le site http://doc.trolltech.com, soit en excutant Qt Assistant, lapplication daide de Qt, que vous obtenez en tapant assistant dans une fentre console.

B
Introduction au langage C++ pour les programmeurs Java et C#
Au sommaire de ce chapitre Dmarrer avec C++ Principales diffrences de langage La bibliothque C++ standard Cette annexe fournit une courte introduction au langage C++ pour les dveloppeurs qui connaissent dj Java ou C#. Nous supposons que vous matrisez les concepts de lorient objet tels que lhritage et le polymorphisme, et que vous dsirez tudier le C++. Pour ne pas transformer cet ouvrage en bible de 1500 pages qui couvrirait la totalit du langage C++, cette annexe vous livre uniquement lessentiel. Elle prsente les techniques et connaissances de base requises pour comprendre les programmes prsents dans le reste de cet ouvrage, avec sufsamment dinformations pour dvelopper des applications graphiques C++ multiplateforme laide de Qt. Au moment dcrire ces lignes, C++ est la seule option raliste pour crire des applications graphiques orientes objet performantes, multiplateforme. Les dtracteurs de ce langage soulignent gnralement que Java et C#, qui ont abandonn la compatibilit avec le langage C, sont plus pratiques utiliser ; en fait, Bjarne Stroustrup, linventeur

484

Qt4 et C++ : Programmation dinterfaces GUI

du C++, signale dans son ouvrage The Design and Evolution of C++ que "dans le langage C++, il y a beaucoup moins de difcults de langage viter". Heureusement, lorsque nous programmons avec Qt, nous exploitons gnralement un sousensemble de C++ qui est trs proche du langage utopique envisag par Stroustrup, ce qui nous laisse libre de nous concentrer sur le problme immdiat. De plus, Qt tend C++ sur plusieurs points de vue, par le biais de son mcanisme "signal et slot" innovant, de sa prise en charge dUnicode et de son mot-cl foreach. Dans la premire section de cette annexe, nous allons voir comment combiner des chiers de code source C++ an dobtenir un programme excutable. Nous serons ainsi amens tudier les concepts de base de C++ tels que les units de compilation, les chiers den-tte, les chiers dobjets, les bibliothques et nous apprendrons devenir familier avec le prprocesseur, le compilateur et lditeur de liens C++. Nous examinerons ensuite les diffrences de langage les plus importantes entre C++, Java et C# : comment dnir des classes, comment utiliser les pointeurs et rfrences, comment surcharger les oprateurs, comment utiliser le prprocesseur et ainsi de suite. Mme si la syntaxe de C++ semble analogue celle de Java ou C# au premier abord, les concepts sous-jacents diffrent de faon subtile. Dautre part, en tant que source dinspiration pour Java et C#, le langage C++ comporte de nombreux points communs avec ces deux langages, notamment les types de donnes similaires, les mmes oprateurs arithmtiques et les mmes instructions de contrle de ux de base. La dernire section est ddie la bibliothque C++ standard qui fournit une fonctionnalit prt lemploi que vous pouvez exploiter dans tout programme C++. Cette bibliothque rsulte de plus de 30 annes de dveloppement, et en tant que telle fournit une large gamme dapproches comprenant les styles de programmation procdural, orient objet, et fonctionnel, ainsi que des macros et des modles. Compare aux bibliothques Java et C#, la porte de la bibliothque C++ standard est relativement limite ; elle ne fournit aucun support au niveau de linterface graphique GUI, de la programmation multithread, des bases de donnes, de linternationalisation, de la gestion de rseau, XML et Unicode. Pour tendre la porte de C++ dans ces domaines, les dveloppeurs C++ doivent avoir recours diverses bibliothques (souvent spciques la plate-forme). Cest ce niveau que Qt apporte son bonus. Qt a dmarr comme "bote outils" GUI multiplateforme (un ensemble de classes qui permettent dcrire des applications interface utilisateur graphique portables) mais qui a rapidement volu en framework part entire qui tend et remplace partiellement la bibliothque C++ standard. Bien que cet ouvrage traite de Qt, il est intressant de connatre ce que la bibliothque C++ standard a offrir, puisque vous risquez davoir besoin de travailler avec du code qui lutilise.

Dmarrer avec C++


Un programme C++ est constitu dune ou plusieurs units de compilation. Chacune de ces units est un chier de code source distinct, typiquement avec une extension .cpp (les autres extensions courantes sont .cc et .cxx) que le compilateur traite dans le mme cycle dexcution.

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

485

Pour chaque unit de compilation, le compilateur gnre un chier objet, avec lextension .obj (sous Windows) ou .o (sous Unix et Mac OS X). Le chier objet est un chier binaire qui contient le code machine destin larchitecture sur laquelle le programme va sexcuter. Une fois que les chiers .cpp ont t compils, nous pouvons combiner les chiers objet n de crer un excutable laide dun programme particulier nomm diteur de liens. Cet diteur de liens concatne les chiers objet et rsout les adresses mmoire des fonctions et autres symboles auxquels font rfrence les units de compilation.
Figure B.1 Le processus de compilation de C++ (sous Windows)
unit1.cpp unit1.obj unit2.cpp unit2.obj program.exe unit3.cpp

Compilation
unit3.obj

Edition des liens

Lorsque vous gnrez un programme, une seule unit de compilation doit contenir la fonction main() qui joue le rle de point dentre dans le programme. Cette fonction nappartient aucune classe; il sagit dune fonction globale. Contrairement Java, pour lequel chaque chier source doit contenir une classe exactement, C++ nous laisse libre dorganiser nos units de compilation. Nous avons la possibilit dimplmenter plusieurs classes dans le mme chier .cpp, ou de rpartir limplmentation dune classe dans plusieurs chiers .cpp. Nous pouvons aussi choisir nimporte quel nom pour nos chiers source. Quand nous effectuons une modication dans un chier .cpp particulier, il suft de recompiler uniquement ce chier puis dexcuter de nouveau lditeur de liens sur application pour crer un nouvel excutable. Avant de poursuivre, examinons rapidement le code source dun programme C++ trs simple qui calcule le carr dun entier. Le programme est constitu de deux units de compilation : main.cpp et square.cpp. Voici square.cpp:
1 2 3 4

double square(double n) { return n * n; }

Ce chier contient simplement une fonction globale nomme square() qui renvoie le carr de son paramtre. Voici main.cpp:
1 2

#include <cstdlib> #include <iostream>

486

Qt4 et C++ : Programmation dinterfaces GUI

3 4 5 6 7 8 9 10 11 12 13 14

using namespace std; double square(double); int main(int argc, char *argv[]) { if (argc!= 2) { cerr << "Usage: square <number>" << endl; return 1; } double n = strtod(argv[1], 0); cout << "The square of " << argv[1] << " is " << square(n) << endl; return 0; }

Le chier source de main.cpp contient la dnition de la fonction main(). En C++, cette fonction reoit en paramtres un int et un tableau de char* (un tableau de chanes de caractres). Le nom du programme est disponible en tant que argv[0] et les arguments de ligne de commande en tant que argv[1], argv[2], , argv[argc -1]. Les noms de paramtre argc ("nombre darguments") et argv ("valeurs des arguments") sont conventionnels. Si le programme na pas accs aux arguments de ligne de commande, nous pouvons dnir main() sans paramtre. La fonction main() utilise strtod() ("string to double"), cout (ux de sortie standard de C++), et cerr (ux derreur standard du C++) de la bibliothque Standard C++ pour convertir largument de ligne de commande en double puis pour afcher le texte sur la console. Les chanes, les nombres, et les marqueurs de n de ligne (endl) sont transmis en sortie laide de loprateur <<, qui est aussi utilis pour les oprations avec dcalage des bits. Pour obtenir cette fonctionnalit standard, il faut coder les deux directives #include des lignes 1 et 2. La directive using namespace de la ligne 3 indique au compilateur que nous dsirons importer tous les identicateurs dclars dans lespace de noms std da528ns lespace de noms global. Cela nous permet dcrire strtod(), cout, cerr, et endl plutt que les versions compltement qualies std::strtod(), std::cout, std::cerr, et std::endl. En C++, loprateur :: spare les composants dun nom complexe. La dclaration de la ligne 4 est un prototype de fonction. Elle signale au compilateur quune fonction existe avec les paramtres et la valeur de retour indiqus. La fonction relle peut se trouver dans la mme unit de compilation ou dans une autre. En labsence de ce prototype, le compilateur aurait refus lappel de la ligne 12. Les noms de paramtre dans les prototypes de fonction sont optionnels. La procdure de compilation du programme varie dune plate-forme lautre. Pour compiler sur Solaris, par exemple, avec le compilateur C++ de Sun, il faudrait taper les commandes suivantes :
CC -c main.cpp CC -c square.cpp ld main.o square.o -o square

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

487

Les deux premires lignes invoquent le compilateur an de gnrer les chiers .o pour les chiers .cpp. La troisime ligne invoque lditeur de liens et gnre un excutable nomm square, que nous pouvons ensuite appeler de la faon suivante :
./square 64

Le programme afche le message suivant sur la fentre de console :


The square of 64 is 4096

Pour compiler le programme, vous prfrez peut -tre obtenir laide de votre gourou C++ local. Si vous ny parvenez pas, lisez quand mme la suite de cette annexe sans rien compiler et suivez les instructions du Chapitre 1 pour compiler votre premire application C++/Qt. Qt fournit des outils partir desquels il nest pas difcile de gnrer des applications pour nimporte quelle plate-forme. Revenons notre programme. Dans une application du monde rel, nous devrions normalement placer le prototype de la fonction square() dans un chier spar puis inclure ce chier dans toutes les units de compilation dans lesquelles nous avons besoin dappeler la fonction. Un tel chier est nomm chier den-tte et il comporte gnralement lextension .h (.hh, .hpp, et .hxx sont galement courantes). Si nous reprenons notre exemple avec la mthode du chier den-tte, il faudrait crer le chier square.h avec le contenu suivant :
1 2 3 4 #ifndef SQUARE_H #define SQUARE_H double square(double); #endif

Le chier den-tte runit trois directives de prprocesseur (#ifndef, #define, et #endif). Elles garantissent que le chier sera trait une seule fois, mme si le chier den-tte est inclus plusieurs fois dans la mme unit de compilation (une situation que lon retrouve lorsque des chiers den-tte incluent dautres chiers den-tte). Par convention, le symbole de prprocesseur utilis pour obtenir ce rsultat est driv du nom de chier (dans notre exemple, SQUARE_H). Nous reviendrons au prprocesseur un peu plus loin dans cette annexe. Voici la syntaxe du nouveau chier main.cpp:
1 #include <cstdlib> 2 #include <iostream> 3 #include "square.h" 4 using namespace std; 5 int main(int argc, char *argv[]) 6 { 7 if (argc!= 2) { 8 cerr << "Usage: square <number>" << endl; 9 return 1; 10 }

488

Qt4 et C++ : Programmation dinterfaces GUI

11 double n = strtod(argv[1], 0); 12 cout << "The square of " << argv[1] << " is " << square(n) << endl; 13 return 0; 14 }

La directive #include en ligne 3 insre cet endroit le contenu du chier square.h. Les directives qui dbutent par un # sont prises en charge par le prprocesseur C++ avant mme le dbut de la compilation. Dans le temps, le prprocesseur tait un programme spar que le programmeur invoquait manuellement avant dexcuter le compilateur. Les compilateurs modernes grent dsormais implicitement ltape du prprocesseur. Les directives #include en lignes 1 et 2 insrent le contenu des chiers den-tte cstdlib et iostream, qui font partie de la bibliothque C++ standard. Les chiers den-tte standard nont pas de sufxe .h. Les crochets (< >) autour des noms de chier indiquent que les chiers den-tte se situent un emplacement standard sur le systme, alors que les guillemets doubles indiquent au compilateur quil doit examiner le rpertoire courant. Les inclusions sont normalement insres au dbut du chier .cpp. Contrairement aux chiers .cpp, les chiers den-tte ne sont pas des units de compilation proprement parler et ne produisent pas le moindre chier objet. Ils ne peuvent contenir que des dclarations qui permettent diverses units de compilation de communiquer entre elles. Il ne serait donc pas appropri dintroduire limplmentation de la fonction square() dans un tel chier. Si nous lavions fait dans cet exemple, cela naurait dclench aucune erreur, parce que nous incluons square.h une seule fois, mais si nous avions inclus square.h dans plusieurs chiers .cpp, nous aurions obtenu de multiples implmentations de square() (une par chier .cpp dans lequel elle est incluse). Lditeur signalerait alors la prsence de dnitions multiples (identiques) de square() et refuserait de gnrer lexcutable. Inversement, si nous dclarons une fonction mais que nous oublions de limplmenter, lditeur de liens signale un "symbole non rsolu". Jusqu prsent, nous avons suppos quun excutable tait uniquement constitu de chiers objet. En pratique, ils comportent aussi souvent des liaisons vers des bibliothques qui implmentent une fonctionnalit prte lemploi. Il existe deux principaux types de bibliothque :

Les bibliothques statiques qui sont directement insres dans lexcutable, comme sil sagissait de chiers objet. Cela annule les risques de perte de la bibliothque mais cela augmente la taille de lexcutable. Les bibliothques dynamiques (galement nommes bibliothques partages ou DLL) qui sont stockes sur un emplacement standard sur lordinateur de lutilisateur et qui sont automatiquement charges au dmarrage de lapplication.

Pour le programme square, nous tablissons une liaison avec la bibliothque C++ standard, qui est implmente en tant que bibliothque dynamique sur la plupart des plates-formes. Qt lui-mme est une collection de bibliothques qui peuvent tre gnres en tant que bibliothques statiques ou dynamiques (dynamique est loption par dfaut).

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

489

Principales diffrences de langage


Nous allons maintenant examiner de prs les domaines dans lesquels C++ diffre de Java et C#. De nombreuses diffrences de langage sont dues la nature compile du C++ et ses obligations en termes de performances. Ainsi, le C++ ne contrle pas les limites de tableau au moment de lexcution, et aucun programme de rcupration de la mmoire ne raffecte la mmoire alloue dynamiquement aprs usage. Pour rester concis, nous naborderons pas dans cette annexe les constructions C++ qui sont pratiquement identiques leurs quivalents Java et C#. Certains sujets C++ ne sont pas non plus couverts ici parce quils ne sont pas indispensables pour la programmation avec Qt. Nous ne traitons pas, en particulier, la dnition des fonctions et classes modles, celle des types union, et les exceptions. Vous trouverez une description dtaille de lensemble des fonctionnalits du langage dans un ouvrage tel que Le langage C++ de Bjarne Stroustrup ou C++ pour les programmeurs Java de Mark Allen Weiss.

Les types de donnes primitifs


Les types de donnes primitifs du C++ sont analogues ceux de Java ou C#. La Figure B.2 rpertorie ces types avec leur dnition sur les plates-formes prises en charge par Qt 4.
Type C++ bool char short int long long long_ float double Figure B.2 Les types primitifs de C++ Description Valeur boolenne Entier 8 bits Entier 16 bits Entier 32 bits Entier 32 bits ou 64 bits Entier 64 bits Valeur virgule ottante 32 bits (IEEE 754) Valeur virgule ottante 64 bits (IEEE 754)

Par dfaut, les types short, int, long, et long long sont signs, ce qui signie quils peuvent stocker aussi bien des valeurs ngatives que des valeurs positives. Si nous navons besoin de stocker que des nombres positifs, nous pouvons coder le mot-cl unsigned devant le type. Alors quun short est capable de stocker nimporte quelle valeur entre 32 768 et

490

Qt4 et C++ : Programmation dinterfaces GUI

+32 767, un unsigned short stocke une valeur entre 0 et 65 535. Loprateur de dcalage droite >> ("remplit avec des 0") a une smantique non signe si un des oprandes est non sign.Le type bool peut avoir les valeurs true et false. De plus, les types numriques peuvent tre utiliss partout o un bool est attendu, 0 tant interprt comme false et toute valeur non nulle par true. Le type char sert la fois pour le stockage des caractres ASCII et pour celui des entiers 8 bits (octets). Lorsquil est utilis pour un entier, celui-ci peut tre sign ou non sign, selon la plateforme. Les types signed char et unsigned char sont des alternatives non ambiges au char. Qt fournit un type QChar qui stocke des caractres Unicode 16 bits. Les instances des types intgrs ne sont pas initialises par dfaut. Lorsque nous crons une variable int, sa valeur pourrait tout aussi bien tre de 0 que de 209 486 515. Heureusement, les compilateurs nous avertissent pour la plupart lorsque nous tentons de lire le contenu dune variable non initialise. Nous pouvons faire appel des outils tels que Rational PurifyPlus et Valgrind pour dtecter les accs de la mmoire non initialise et les autres problmes lis la mmoire lors de lexcution. En mmoire, les types numriques ( lexception des long) ont des tailles identiques sur les diffrentes plates-formes prises en charge par Qt, mais leur reprsentation varie selon larchitecture du systme. Sur les architectures big-endian (telles que PowerPC et SPARC), la valeur 32 bits 0x12345678 est stocke sous la forme des quatre octets 0x12 0x34 0x56 0x78, alors que sur les architectures little-endian (telles que Intel x86), la squence doctets est inverse. La diffrence apparat dans les programmes qui copient des zones de mmoire sur disque ou qui envoient des donnes binaires sur le rseau. La classe QDataStream de Qt, prsente au Chapitre 12 (Entres/Sorties), peut tre utilise pour stocker des donnes binaires en restant indpendant de la plate-forme. Chez Microsoft, le type long long sappelle __int64. Dans les programmes Qt, qlonglong est propos comme une alternative compatible avec toutes les plates-formes de Qt.

Dnitions de classe
Les dnitions de classe en C++ sont analogues celles de Java et C#, mais vous devez connatre plusieurs diffrences. Nous allons les tudier avec une srie dexemples. Commenons par une classe qui reprsente une paire de coordonnes (x, y) :
#ifndef POINT2D_H #define POINT2D_H class Point2D { Public Point2D() { xVal = 0; yVal = 0; }

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

491

Point2D(double x, double y) { xVal = x; yVal = y; } void setX(double void setY(double double x() const double y() const Private double xVal; double yVal; }; #endif x) { xVal = x; y) { yVal = y; { return xVal; { return yVal; } } } }

La dnition de classe prcdente devrait apparatre dans un chier den-tte, typiquement nomm point2d.h. Cet exemple illustre les caractristiques C++ suivantes : Une dnition de classe est divise en sections publique, protge et prive, et se termine par un point-virgule. Si aucune section nest spcie, la section par dfaut est prive. (Pour des raisons de compatibilit avec le C, C++ fournit le mot-cl struct identique class sauf que la section par dfaut est publique si aucune section nest spcie.) La classe comporte deux constructeurs (le premier sans paramtre et le second avec deux paramtres). Si nous navions pas dclar de constructeur, C++ en aurait automatiquement fourni un sans paramtre avec un corps vide. Les fonctions daccs x() et y() sont dclares comme const. Cela signie quelles ne peuvent ni modier les variables membres ni appeler des fonctions membres non-const (telles que setX() et setY()). Les fonctions ci-dessus ont t implmentes inline, dans la dnition de classe. Vous avez aussi la possibilit de ne fournir que les prototypes de fonction dans le chier den-tte et dimplmenter les fonctions dans un chier .cpp. Dans ce cas, le chier den-tte aurait la syntaxe suivante :
#ifndef POINT2D_H #define POINT2D_H class Point2D { Public Point2D(); Point2D(double x, double y); void setX(double x); void setY(double y); double x() const; double y() const;

492

Qt4 et C++ : Programmation dinterfaces GUI

Private double xVal; double yVal; }; #endif

Les fonctions seraient alors implmentes dans point2d.cpp:


#include "point2d.h" Point2D::Point2D() { xVal = 0.0; yVal = 0.0; } Point2D::Point2D(double x, double y) { xVal = x; yVal = y; } void Point2D::setX(double x) { xVal = x; } void Point2D::setY(double y) { yVal = y; } double Point2D::x() const { return xVal; } double Point2D::y() const { return yVal; }

Nous commenons par inclure point2d.h parce que le compilateur a besoin de la dnition de classe pour tre en mesure danalyser les implmentations de fonction membre. Nous implmentons ensuite les fonctions, en prxant leurs noms de celui de la classe suivi de loprateur ::. Nous avons dj expliqu comment implmenter une fonction inline et maintenant comment limplmenter dans un chier .cpp. Les deux mthodes sont quivalentes dun point de vue smantique, mais lorsque nous appelons une fonction qui est dclare inline, la plupart des

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

493

compilateurs se contentent dinsrer le corps de la fonction plutt que de gnrer un vritable appel de fonction. Vous obtenez gnralement un code plus performant, mais la taille de lapplication est suprieure. Cest pourquoi seules les fonctions trs courtes devraient tre implmentes de cette faon ; les autres devront plutt tre implmentes dans un chier .cpp. De plus, si nous oublions dimplmenter une fonction et que nous tentons de lappeler, lditeur de liens va signaler un symbole non rsolu. Essayons maintenant dutiliser la classe.
#include "point2d.h" int main() { Point2D alpha; Point2D beta(0.666, 0.875); alpha.setX(beta.y()); beta.setY(alpha.x()); return 0; }

En C++, les variables de tout type peuvent tre dclares directement sans coder new. La premire variable est initialise laide du constructeur par dfaut Point2D (celui qui ne reoit aucun paramtre). La seconde variable est initialise laide du second constructeur. Laccs au membre de lobjet seffectue via loprateur "." (point). Les variables dclares de cette faon se comportent comme les types primitifs de Java/C# tels que int et double. Lorsque nous codons loprateur daffectation, par exemple, cest le contenu de la variable qui est copi et non une simple rfrence lobjet. Si nous modions une variable par la suite, cette modication ne sera pas rpercute dans les variables qui on a pu affecter la valeur de la premire. En tant que langage orient objet, C++ prend en charge lhritage et le polymorphisme. Nous allons tudier ces deux proprits avec lexemple de classe de base abstraite Shape et une sous-classe nomme Circle. Commenons par la classe de base :
#ifndef SHAPE_H #define SHAPE_H #include "point2d.h" class Shape { Public Shape(Point2D center) { myCenter = center; } virtual void draw() = 0;

494

Qt4 et C++ : Programmation dinterfaces GUI

Protected Point2D myCenter; }; #endif

La dnition se trouve dans un chier den-tte nomm shape.h. Cette dnition faisant rfrence la classe Point2D, nous incluons point2d.h. La classe Shape ne possde pas de classe de base. Contrairement Java et C#, C++ ne fournit pas de classe Object gnrique partir de laquelle toutes les classes hritent. Qt fournit QObject comme classe de base naturelle pour toutes sortes dobjets. La dclaration de la fonction draw() prsente deux caractristiques intressantes : elle comporte le mot-cl virtual, et elle se termine par = 0. Ce mot-cl signale que la fonction pourrait tre rimplmente dans les sous-classes. Comme en C#, les fonctions membres C++ ne sont pas rimplmentables par dfaut. La syntaxe bizarre =0 indique que la fonction est une fonction virtuelle pure, cest--dire une fonction qui ne possde aucune implmentation par dfaut et qui doit obligatoirement tre implmente dans les sous-classes. Le concept "dinterface" en Java et C# correspond une classe constitue uniquement de fonctions virtuelles pures en C++. Voici la dnition de la sous-classe Circle :
#ifndef CIRCLE_H #define CIRCLE_H #include "shape.h" class Circle: public Shape { Public Circle(Point2D center, double radius = 0.5) : Shape(center) { myRadius = radius; } void draw() { //excuter ici une action } Private double myRadius; }; #endif

La classe Circle hrite publiquement de Shape, ce qui signie que tous les membres publics de cette dernire restent publics dans Circle. C++ prend aussi en charge lhritage priv et protg, qui limite laccs aux membres publics et protgs de la classe de base. Le constructeur reoit deux paramtres : le second est facultatif et il prend la valeur 0.5 lorsquil nest pas spci. Le constructeur transmet le paramtre correspondant au centre au

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

495

constructeur de la classe de base en appliquant une syntaxe particulire entre la signature et le corps de la fonction. Dans le corps, nous initialisons la variable membre myRadius. Nous aurions pu aussi grouper linitialisation de la variable avec celle du constructeur de la classe de base sur la mme ligne :
Circle(Point2D center, double radius = 0.5) : Shape(center), myRadius(radius) { }

Dautre part, C++ nautorise pas linitialisation dune variable membre dans la dnition de classe, le code suivant est donc incorrect :
// ne compilera pas Private double myRadius = 0.5; };

La fonction draw() possde la mme signature que la fonction virtuelle draw() dclare dans Shape. Il sagit dune rimplmentation et elle sera invoque de faon polymorphique au moment o draw() sera appele pour une instance de Circle par le biais dun pointeur ou dune rfrence de Shape. C++ ne fournit pas de mot-cl pour une rednition de fonction comme en C#. Ce langage ne comporte pas non plus de mot-cl super ou base qui fasse rfrence la classe de base. Si nous avons besoin dappeler limplmentation de base dune fonction, nous pouvons prxer son nom avec celui de la classe de base suivi de loprateur ::. Par exemple :
class LabeledCircle: public Circle { Public void draw() { Circle::draw(); drawLabel(); } ... };

C++ prend en charge lhritage multiple, ce qui signie quune classe peut tre drive de plusieurs classes la fois. Voici la syntaxe :
class DerivedClass: public BaseClass1, public BaseClass2, ..., public BaseClassN { ... };

Par dfaut, fonctions et variables dclares dans une classe sont associes avec les instances de cette classe. Nous avons aussi la possibilit de dclarer des fonctions membres et des variables membres statiques, que vous utilisez ensuite sans instance.

496

Qt4 et C++ : Programmation dinterfaces GUI

Par exemple :
#ifndef TRUCK_H #define TRUCK_H class Truck { Public Truck() { ++counter; } ~Truck() { --counter; } static int instanceCount() { return counter; } Private static int counter; }; #endif

La variable membre statique counter assure le suivi du nombre dinstances de Truck un instant donn. Cest le constructeur de Truck qui lincrmente. Le destructeur, que vous reconnaissez au prxe ~, dcrmente cette valeur. En C++, le destructeur est automatiquement invoqu lorsquune variable alloue de faon statique sort de la porte ou lorsquune variable alloue avec new est supprime. Ce comportement est analogue celui de la mthode finalize() en Java, sauf que nous pouvons lobtenir en y faisant appel un instant spcique. Une variable membre statique existe uniquement dans sa classe : il sagit de "variables de classe" plutt que de "variables dinstance". Chaque variable membre statique doit tre dnie dans un chier .cpp (mais sans rpter le mot-cl static). Par exemple :
#include "truck.h" int Truck::counter = 0;

Si vous ne suivez pas cette rgle, vous obtiendrez une erreur de "symbole non rsolu" au moment de ldition des liens. La fonction statique instanceCount() est accessible depuis lextrieur de la classe, en la prxant avec le nom de cette dernire. Par exemple :
#include <iostream> #include "truck.h" using namespace std; int main() { Truck truck1; Truck truck2; cout << Truck::instanceCount() << " equals 2" << endl; return 0; }

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

497

Les pointeurs
Un pointeur en C++ est une variable qui stocke ladresse mmoire dun objet (plutt que lobjet lui-mme). Java et C# ont un concept analogue, la "rfrence," mais avec une syntaxe diffrente. Nous allons commencer par tudier un exemple (peu raliste) pour observer les pointeurs en action :
1 #include "point2d.h" 2 int main() 3 { 4 Point2D alpha; 5 Point2D beta; 6 7 8 9 10 11 12 13 14 15 } Point2D *ptr; ptr = &alpha; ptr->setX(1.0); ptr->setY(2.5); ptr = &beta; ptr->setX(4.0); ptr->setY(4.5); ptr = 0; return 0;

Cet exemple sappuie sur la classe Point2D de la sous-section prcdente. Les lignes 4 et 5 dnissent deux objets de type Point2D. Ces objets sont initialiss (0, 0) par le constructeur de Point2D par dfaut. La ligne 6 dnit un pointeur vers un objet Point2D. La syntaxe des pointeurs place un astrisque devant le nom de la variable. Ce pointeur nayant pas t initialis, il contient une adresse mmoire alatoire. Ce problme est rgl ligne 7 par laffectation de ladresse dalpha ce pointeur. Loprateur unaire & renvoie ladresse mmoire dun objet. Une adresse est typiquement une valeur entire sur 32 ou 64 bits qui spcie le dcalage dun objet en mmoire. En lignes 8 et 9, nous accdons lobjet alpha via le pointeur ptr. ptr tant un pointeur et non un objet, nous devons coder loprateur -> (che) plutt que loprateur . (point). Ligne 10, nous affectons ladresse de beta au pointeur. A partir de l, toute opration sur le pointeur va affecter lobjet beta. Ligne 13, le pointeur est dni en pointeur nul. C++ ne fournit pas de mot-cl pour reprsenter un pointeur qui ne pointe pas vers un objet; cest pourquoi nous affectons la valeur 0 (ou la constante symbolique NULL, qui reprsente 0). Lemploi dun pointeur nul entrane aussitt une panne accompagne dun message derreur du type "Erreur de segmentation", "Erreur de

498

Qt4 et C++ : Programmation dinterfaces GUI

protection gnrale", ou "Erreur de bus". Avec laide dun dbogueur, nous pouvons retrouver la ligne de code lorigine de lerreur. A la n de la fonction, lobjet alpha contient la paire de coordonnes (1.0, 2.5), alors que beta contient (4.0, 4.5). Les pointeurs sont souvent employs pour stocker des objets allous dynamiquement laide de new. En jargon C++, nous disons que ces objets sont allous sur le "tas", alors que les variables locales (les variables dnies dans une fonction) sont stockes sur la "pile". Voici un extrait de code qui illustre lallocation de mmoire dynamique laide de new:
#include "point2d.h" int main() { Point2D *point = new Point2D; point->setX(1.0); point->setY(2.5); delete point; return 0; }

Loprateur new renvoie ladresse mmoire de lobjet qui vient dtre allou. Nous stockons ladresse dans une variable pointeur et laccs lobjet seffectue via le pointeur. Quand nous en avons termin avec lobjet, nous librons la mmoire associe laide de loprateur delete. Contrairement Java et C#, C++ ne possde pas de programme de libration de la mmoire (garbage collector) ; les objets allous dynamiquement doivent tre explicitement librs laide de delete quand nous nen navons plus besoin. Le Chapitre 2 dcrit le mcanisme parentenfant de Qt, qui simplie largement la gestion de mmoire dans les programmes C++. Si nous oublions dappeler delete, la mmoire reste occupe jusqu ce que le programme se termine. Cela ne poserait aucun problme dans lexemple prcdent, parce que nous nallouons quun seul objet, mais dans le cas dun programme qui allouerait constamment de nouveaux objets, les allocations de mmoire associes se produiraient jusqu saturation de la mmoire de lordinateur. Une fois quun objet est supprim, la variable pointeur contient toujours ladresse de cet objet. Ce pointeur devient un "pointeur dans le vide" et ne doit plus tre utilis. Qt fournit un pointeur "intelligent," QPointer<T>, qui se dnit automatiquement 0 si le QObject sur lequel il pointe est supprim. Dans lexemple ci-dessus, nous avons invoqu le constructeur par dfaut et nous avons appel setX() et setY() pour initialiser lobjet. Nous aurions pu faire plutt appel au constructeur deux paramtres :q
Point2D *point = new Point2D(1.0, 2.5);

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

499

Lexemple nexigeait pas lemploi de new et de delete. Nous aurions trs bien pu allouer lobjet sur la pile de la faon suivante :
Point2D point; point.setX(1.0); point.setY(2.5);

La mmoire occupe par des objets allous de cette faon est automatiquement rcupre la n du bloc dans lequel ils apparaissent. Si nous navons pas lintention de modier lobjet par lintermdiaire du pointeur, nous pouvons dclarer le pointeur const. Par exemple :
const Point2D *ptr = new Point2D(1.0, 2.5); double x = ptr->x(); double y = ptr->y(); // la compilation va chouer ptr->setX(4.0); *ptr = Point2D(4.0, 4.5);

Le pointeur ptr const sert uniquement appeler des fonctions membres const telles que x() et y(). Prenez lhabitude de dclarer vos pointeurs comme const quand vous ne prvoyez pas de modier lobjet par leur intermdiaire. De plus, si lobjet lui-mme est const, nous navons de toute faon pas dautre choix que dutiliser un pointeur const pour stocker son adresse. Le mot-cl const apporte des informations au compilateur qui est ainsi en mesure de dtecter trs tt des erreurs et damliorer les performances. C# comporte un mot-cl const trs similaire celui de C++. Lquivalent Java le plus proche est final, mais il protge les variables uniquement contre les affectations, pas contre les appels de fonctions membres "nonconst" sur ces dernires. Les pointeurs fonctionnent avec les types intgrs ainsi quavec les classes. Dans une expression, loprateur unaire * renvoie la valeur de lobjet associ au pointeur. Par exemple :
int i = 10; int j = 20; int *p = &i; int *q = &j; cout << *p << " equals 10" << endl; cout << *q << " equals 20" << endl; *p = 40; cout << i << " equals 40" << endl; p = q; *p = 100; cout << i << " equals 40" << endl; cout << j << " equals 100" << endl;

500

Qt4 et C++ : Programmation dinterfaces GUI

Loprateur ->, partir duquel vous avez accs aux membres dun objet via un pointeur, a une syntaxe trs particulire. Plutt que de coder ptr->membre, nous pouvons aussi crire (*ptr).membre. Les parenthses sont ncessaires parce que loprateur "." (point) est prioritaire sur loprateur unaire *. Les pointeurs avaient mauvaise rputation en C et C++, tel point que la promotion de Java insiste souvent sur labsence de pointeur dans ce langage. En ralit, les pointeurs C++ sont analogues dun point de vue conceptuel aux rfrences de Java et C# sauf que nous pouvons nous servir de pointeurs pour parcourir la mmoire, comme vous le dcouvrirez un peu plus loin dans cette section. De plus, linclusion des classes conteneur en "copie lcriture" dans Qt, ainsi que la possibilit de C++ dinstancier une classe sur la pile, signie que nous pouvons souvent viter davoir recours aux pointeurs.

Les rfrences
En complment des pointeurs, C++ prend aussi en charge le concept de "rfrence".Comme un pointeur, une rfrence C++ stocke ladresse dun objet. Voici les principales diffrences :

Les rfrences sont dclares laide de & plutt que *. Les rfrences doivent tre initialises et ne peuvent tre raffectes par la suite. Lobjet associ une rfrence est directement accessible ; il ny a pas de syntaxe particulire telle que * ou ->. Une rfrence ne peut tre nulle.

Les rfrences sont surtout utilises pour dclarer des paramtres. Par dfaut, C++ applique le mcanisme de transmission par valeur lors de la transmission des paramtres, ce qui signie que lorsquun argument est transmis une fonction, celle-ci reoit une copie entirement nouvelle de lobjet. Voici la dnition dune fonction qui reoit ses paramtres par le biais dun appel par valeur :
#include <cstdlib> using namespace std; double manhattanDistance(Point2D a, Point2D b) { return abs(b.x() - a.x()) + abs(b.y() - a.y()); }

Nous pourrions alors invoquer la fonction comme suit :


Point2D broadway(12.5, 40.0); Point2D harlem(77.5, 50.0); double distance = manhattanDistance(broadway, harlem);

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

501

Les programmeurs C convertis vitent les oprations de copie inutiles en dclarant leurs paramtres sous forme de pointeurs plutt que sous forme de valeurs :
double manhattanDistance(const Point2D *ap, const Point2D *bp) { return abs(bp->x() - ap->x()) + abs(bp->y() - ap->y()); }

Ils doivent ensuite transmettre les adresses plutt que les valeurs lors de lappel de la fonction :
Point2D broadway(12.5, 40.0); Point2D harlem(77.5, 50.0); double distance = manhattanDistance(&broadway, &harlem);

C++ a introduit les rfrences an de simplier la syntaxe et dviter que lappelant ne transmette un pointeur nul. Si nous utilisons des rfrences plutt que des pointeurs, voici ce que devient la fonction :
double manhattanDistance(const Point2D &a, const Point2D &b) { return abs(b.x() - a.x()) + abs(b.y() - a.y()); }

La dclaration dune rfrence est analogue celle dun pointeur, & remplaant *. Mais lorsque nous utilisons effectivement la rfrence, nous pouvons oublier quil sagit dune adresse mmoire et la traiter comme une variable ordinaire. De plus, lappel dune fonction qui reoit des rfrences en arguments nexige aucune attention particulire (pas doprateur &). Dans lensemble, en remplaant Point2D par const Point2D & dans la liste de paramtres, nous avons rduit la surcharge de traitement de lappel de fonction : plutt que de copier 256 bits (la taille de quatre double), nous copions seulement 64 ou 128 bits, selon la taille du pointeur de la plate-forme cible. Lexemple prcdent sappuyait sur les rfrences const, ce qui empchait la fonction de modier les objets associs aux rfrences. Lorsquon veut au contraire autoriser ce comportement, nous pouvons transmettre une rfrence ou un pointeur non-const. Par exemple :
void transpose(Point2D &point) { double oldX = point.x(); point.setX(point.y()); point.setY(oldX); }

Dans certains cas, nous disposons dune rfrence et nous avons besoin dappeler une fonction qui reoit un pointeur, ou vice versa. Pour convertir une rfrence en pointeur, nous pouvons tout simplement utiliser loprateur unaire &:
Point2D point; Point2D &ref = point; Point2D *ptr = &ref;

502

Qt4 et C++ : Programmation dinterfaces GUI

Pour convertir un pointeur en rfrence, il y a loprateur unaire *:


Point2D point; Point2D *ptr = &point; Point2D &ref = *ptr;

Rfrences et pointeurs sont reprsents de la mme faon en mmoire, et ils peuvent souvent tre utiliss de faon interchangeable, ce qui soulve la question de quel lment utiliser et quand. Dun ct, la syntaxe des rfrences est plus pratique. Dun autre ct, tout moment vous pouvez raffecter les pointeurs pour pointer sur un autre objet, ils peuvent stocker une valeur nulle, et leur syntaxe plus explicite est plutt une bonne chose. Cest pour cette raison que les pointeurs ont tendance lemporter, les rfrences tant plutt presque exclusivement employe pour dclarer les paramtres de fonction, en conjonction avec const.

Les tableaux
Les tableaux en C++ sont dclars en spciant leur nombre dlments entre crochets dans la dclaration de variable aprs le nom de variable. Il est possible de crer des tableaux deux dimensions laide dun tableau de tableau. Voici la dnition dun tableau une dimension contenant 10 lments de type int:
int fibonacci[10];

On accde aux lments via la syntaxe fibonacci[0], fibonacci[1], , fibonacci[9]. Nous avons souvent besoin dinitialiser le tableau lors de sa dnition :
int fibonacci[10] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 };

Dans ce cas, nous pouvons alors omettre la taille de tableau puisque le compilateur peut la dduire partir du nombre dinitialisations :
int fibonacci[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 };

Linitialisation statique fonctionne aussi pour les types complexes, tels que Point2D:
Point2D triangle[] = { Point2D(0.0, 0.0), Point2D(1.0, 0.0), Point2D(0.5, 0.866) };

Si nous navons pas lintention de modier le tableau par la suite, nous pouvons le rendre const:
const int fibonacci[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 };

Pour trouver le nombre dlments dun tableau, il suft dexcuter loprateur sizeof() comme suit :
int n = sizeof(fibonacci) / sizeof(fibonacci[0]);

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

503

Cet oprateur renvoie la taille de ses arguments en octets. Le nombre dlments dans un tableau correspond la taille de ce dernier en octets divise par la taille dun de ses lments. Comme cest un peu fatiguant taper, une solution courante consiste dclarer une constante et lutiliser pour dnir le tableau :
enum { NFibonacci = 10 }; const int fibonacci[NFibonacci] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 };

Il aurait t tentant de dclarer la constante sous forme de variable const int. Malheureusement, certains compilateurs acceptent mal les variables const pour spcier les tailles de tableau. Le mot-cl enum sera expliqu plus tard dans cette annexe. Le parcours dun tableau est normalement ralis laide dun entier. Par exemple :
for (int i = 0; i < NFibonacci; ++i) cout << fibonacci[i] << endl;

Il est aussi possible de parcourir le tableau laide dun pointeur :


const int *ptr = &fibonacci[0]; while (ptr!= &fibonacci[10]) { cout << *ptr << endl; ++ptr; }

Nous initialisons le pointeur avec ladresse du premier lment et nous bouclons jusqu ce que nous atteignons llment "qui suit le dernier" (le "onzime" lment, fibonacci[10]). A chaque itration, loprateur ++ positionne le pointeur sur llment suivant. Au lieu de &fibonacci[0], nous aurions aussi pu crire fibonacci. En effet, le nom dun tableau utilis seul est automatiquement converti en pointeur vers le premier lment du tableau. De la mme faon, nous pourrions remplacer fibonacci + 10 par &fibonacci[10]. Cela marche aussi en sens inverse : nous pouvons rcuprer le contenu de llment courant en codant soit *ptr soit ptr[0] et nous pourrions accder llment suivant laide de *(ptr + 1) ou ptr[1]. On fait quelquefois rfrence ce principe en termes "dquivalence entre pointeurs et tableaux". Pour empcher ce quil considre comme une perte defcacit gratuite, C++ nous interdit de transmettre des tableaux des fonctions par valeur. Il faut plutt transmettre leur adresse. Par exemple :
#include <iostream> using namespace std; void printIntegerTable(const int *table, int size) { for (int i = 0; i < size; ++i) cout << table[i] << endl; }

504

Qt4 et C++ : Programmation dinterfaces GUI

int main() { const int fibonacci[10] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 }; printIntegerTable(fibonacci, 10); return 0; }

Ironiquement, alors que le C++ ne nous donne aucun choix concernant le mode de transmission dun tableau, par adresse ou par valeur, il nous accorde quelques liberts dans la syntaxe employe pour dclarer le type de paramtre. Pour remplacer const int *table, nous aurions pu coder const int table[] pour dclarer un paramtre pointeur-vers-constanteint. Dune faon analogue, le paramtre argv de main() peut tre dclar soit en tant que char *argv[] soit en tant que char **argv. Pour copier un tableau dans un autre tableau, une solution consiste boucler dans le tableau :
const int fibonacci[NFibonacci] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 }; int temp[NFibonacci]; for (int i = 0; i < NFibonacci; ++i) temp[i] = fibonacci[i];

Pour les types de base tel que int, nous pouvons aussi utiliser std::memcpy(), qui copie un bloc de mmoire. Par exemple :
memcpy(temp, fibonacci, sizeof(fibonacci));

Lorsque nous dclarons un tableau C++, la taille doit tre une constante. Si nous dsirons crer un tableau de taille variable, nous avons plusieurs solutions. Nous pouvons allouer dynamiquement ce tableau :
int *fibonacci = new int[n];

Certains compilateurs autorisent les variables dans ce contexte, mais il ne faut pas exploiter ce type de fonctionnalit pour crer un programme portable. Loprateur new[] alloue un certain nombre dlments dans des emplacements mmoire conscutifs et renvoie un pointeur vers le premier lment. Grce au principe de "lquivalence des pointeurs et des tableaux," on peut accder aux lments par lintermdiaire dun pointeur avec fibonacci[0], fibonacci[1], , fibonacci[n -1]. Ds que le tableau nest plus ncessaire, nous devons librer la mmoire quil occupe en excutant loprateur delete[]:
delete [] fibonacci;

Nous pouvons appliquer la syntaxe standard std::vector<T> class:


#include <vector> using namespace std; vector<int> fibonacci(n);

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

505

Vous accdez aux lments laide de loprateur [], exactement comme dans un tableau C++ ordinaire. Avec std::vector<T> (o T est le type des lments stocks dans le vecteur), nous pouvons redimensionner le tableau tout moment en excutant resize() et nous le copions avec loprateur daffectation. Les classes qui contiennent des crochets (<>) dans leur nom sont appeles classes modle. Nous pouvons utiliser la syntaxe QVector<T> class de Qt :
#include <QVector> QVector<int> fibonacci(n);

LAPI de QVector<T> est trs proche de celle de std::vector<T>, mais elle prend aussi en charge litration via le mot-cl foreach de Qt et elle utilise le partage de donnes ("copie lcriture") an doptimiser la mmoire et les temps de rponse. Le Chapitre 11 prsente les classes conteneur de Qt et expose leurs relations avec les conteneurs standard du C++. Vous pourriez tre tent dviter au maximum les tableaux intgrs et de coder plutt std:: vector<T> ou QVector<T>. Il est pourtant essentiel de bien comprendre le fonctionnement des tableaux intgrs parce que tt ou tard vous en aurez besoin dans du code fortement optimis ou pour servir dinterface avec les bibliothques C existantes.

Les chanes de caractres


La faon la plus simple de reprsenter des chanes de caractres en C++ consiste utiliser un tableau de caractres termin par un octet nul (0). Les quatre fonctions suivantes illustrent le comportement de ce type de chanes :
void hello1() { const char str[] = { H, e, l, l, o, , w, o, r, l, d, \0 }; cout << str << endl; } void hello2() { const char str[] = "Hello world!"; cout << str << endl; } void hello3() { cout << "Hello world!" << endl; }

506

Qt4 et C++ : Programmation dinterfaces GUI

void hello4() { const char *str = "Hello world!"; cout << str << endl; }

Dans la premire fonction, nous dclarons la chane en tant que tableau et nous linitialisons de faon basique. Notez le caractre 0 (zro) de n, qui signale la n de la chane. La deuxime fonction comporte une dnition de tableau similaire mais nous faisons cette fois appel un littral chane pour initialiser le tableau. En C++, les littraux chane sont de simples tableaux de const char avec un caractre 0 de n implicite. La troisime fonction emploie directement un littral chane, sans lui donner de nom. Une fois traduit en instructions de langage machine, ce code est identique celui des deux fonctions prcdentes. La quatrime fonction est un peu diffrente parce quelle cre non seulement un tableau (anonyme) mais aussi une variable pointeur appele str qui stocke ladresse du premier lment du tableau. Malgr cela, la smantique de la fonction est identique aux trois fonctions prcdentes, et un compilateur performant devrait liminer la variable str superue. Les fonctions qui reoivent des chanes C++ en arguments reoivent gnralement un char* ou un const char*. Voici un programme court qui illustre lemploi de chacune delles :
#include <cctype> #include <iostream> using namespace std; void makeUppercase(char *str) { for (int i = 0; str[i]!= \0; ++i) str[i] = toupper(str[i]); } void writeLine(const char *str) { cout << str << endl; } int main(int argc, char *argv[]) { for (int i = 1; i < argc; ++i) { makeUppercase(argv[i]); writeLine(argv[i]); } return 0; }

En C++, le type char stocke normalement une valeur 8 bits. Cela signie que nous pouvons facilement stocker des chanes ASCII, ISO 8859-1 (Latin-1), et autres codages 8 bits dans un tableau de caractres, mais que nous ne pouvons pas stocker des caractres Unicode arbitraires sans faire appel des squences multioctet. Qt fournit la puissante classe QString, qui stocke

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

507

des chanes Unicode sous forme de squences de QChars 16 bits et qui exploite en interne loptimisation du partage de donnes implicite ("copie lcriture"). Cette classe est dtaille dans les Chapitres 11 (Classes conteneur) et 17 (Internationalisation).

Les numrations
C++ comprend une fonctionnalit dnumration pour la dclaration dun jeu de constantes nommes analogue celle fournie par C#. Supposons que nous voulions stocker les jours de la semaine dans un programme :
enum DayOfWeek { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday };

Normalement, nous devrions placer cette dclaration dans un chier den-tte ou mme dans une classe. La dclaration prcdente est quivalente en apparence aux dnitions de constantes suivantes :
const const const const const const const int int int int int int int Sunday = 0; Monday = 1; Tuesday = 2; Wednesday = 3; Thursday = 4; Friday = 5; Saturday = 6;

En exploitant la construction de lnumration, nous pouvons dclarer par la suite des variables ou paramtres de type DayOfWeek et le compilateur garantira que seules les valeurs de lnumration DayOfWeek leur seront affectes. Par exemple :
DayOfWeek day = Sunday;

Si la scurit des types ne nous concerne pas, nous pouvons aussi coder
int day = Sunday;

Notez que pour faire rfrence la constante Sunday dans lnumration DayOfWeek, nous crivons simplement Sunday, et non DayOfWeek::Sunday. Par dfaut, le compilateur affecte des valeurs dentiers conscutifs aux constantes dune numration, en commenant 0. Nous pouvons spcier dautres valeurs si ncessaire :
enum DayOfWeek { Sunday = 628, Monday = 616, Tuesday = 735, Wednesday = 932, Thursday = 852, Friday = 607, Saturday = 845 };

508

Qt4 et C++ : Programmation dinterfaces GUI

Si nous ne spcions pas la valeur dun lment de lnumration, celui-ci prend la valeur de llment prcdent, plus 1. Les numrations sont quelquefois employes pour dclarer des constantes entires, auquel cas nous omettons normalement le nom de lnumration :
Enum FirstPort = 1024, MaxPorts = 32767 };

Un autre emploi frquent des numrations concerne la reprsentation des ensembles doptions. Prenons lexemple dune bote de dialogue Find, avec quatre cases cocher contrlant lalgorithme de recherche (Wildcard syntax, Case sensitive, Search backward, et Wrap around). Nous pouvons reprsenter ces cases laide dune numration dans laquelle les constantes sont des puissances de 2 :
enum FindOption { NoOptions = 0x00000000, WildcardSyntax = 0x00000001, CaseSensitive = 0x00000002, SearchBackward = 0x00000004, WrapAround = 0x00000008 };

Chaque option est souvent nomme "indicateur". Nous pouvons combiner ces indicateurs laide des oprateurs bit bit | ou |=:
int options = NoOptions; if (wilcardSyntaxCheckBox->isChecked()) options |= WildcardSyntax; if (caseSensitiveCheckBox->isChecked()) options |= CaseSensitive; if (searchBackwardCheckBox->isChecked()) options |= SearchBackwardSyntax; if (wrapAroundCheckBox->isChecked()) options |= WrapAround; Nous pouvons tester si un indicateur existe laide de loprateur if (options & CaseSensitive) { // rechercher avec casse significative }

&

bit bit:

Une variable de type FindOption ne peut contenir quun seul indicateur la fois. La combinaison de plusieurs indicateurs laide de | donne un entier ordinaire. Dans ce cas, les types ne sont malheureusement pas scuriss : le compilateur ne va rien signaler si une fonction qui doit recevoir une combinaison de FindOptions dans un paramtre int reoit plutt Saturday. Qt utilise QFlags<T> pour scuriser les types de ses propres types dindicateur. La classe est galement disponible lorsque nous dnissons des types dindicateur personnaliss. Consultez la documentation en ligne de QFlags<T> pour connatre tous les dtails.

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

509

TypeDef
Avec C++, nous pouvons dnir un alias pour un type de donnes laide du mot-cl typedef. Si nous codons souvent par exemple QVector<Point2D> et que nous dsirons conomiser les oprations de frappe, nous pouvons placer la dclaration de typedef suivante dans un de nos chiers den-tte :
typedef QVector<Point2D> PointVector;

A partir de l, PointVector peut tre cod en lieu et place de QVector<Point2D>. Notez que le nouveau nom du type apparat aprs lancien. La syntaxe de typedef suit dlibrment celle des dclarations de variable. Avec Qt, les typedef sont surtout employs pour les raisons suivantes : Laspect pratique : Qt dclare uint et QWidgetList en tant que typedef pour unsigned int et QList<QWidget *> an de simplier la frappe. Les diffrences entre plates-formes : certains types ont des dnitions diffrentes sur des plates-formes diffrentes. Par exemple, qlonglong est dni comme __int64 sous Windows et comme long long sur dautres plates-formes. La compatibilit : la classe QIconSet de Qt 3 a t renomme en QIcon dans Qt 4. Pour aider les utilisateurs de Qt 3 porter leurs applications vers Qt 4, QIconSet est fourni comme typedef pour QIcon lorsque la compatibilit Qt 3 est active.

Conversions de type
C++ fournit plusieurs syntaxes pour la conversion des valeurs dun type lautre. La syntaxe traditionnelle, hrite de C, implique de placer le type obtenu entre parenthses avant la valeur convertir :
const double Pi = 3.14159265359; int x = (int)(Pi * 100); cout << x << " equals 314" << endl;

Cette syntaxe est trs puissante. Elle permet de changer le type des pointeurs, de supprimer const, et plus encore. Par exemple :
short j = 0x1234; if (*(char *)&j == 0x12) cout << "The byte order is big-endian" << endl;

Dans lexemple prcdent, nous convertissons un short* en char* et nous utilisons loprateur unaire * pour accder loctet situ lemplacement de mmoire donn. Sur les systmes bigendian, cet octet se trouve en 0x12; sur les systmes little-endian, il se trouve lemplacement 0x34. Les pointeurs et rfrences tant reprsents de la mme faon, vous ne serez pas surpris dapprendre que le code prcdent peut tre rcrit laide dune conversion de rfrence :

510

Qt4 et C++ : Programmation dinterfaces GUI

short j = 0x1234; if ((char &)j == 0x12) cout << "The byte order is big-endian" << endl;

Si le type de donnes est un nom de classe, un typedef, ou un type primitif qui peut sexprimer sous la forme dun unique jeton alphanumrique, nous pouvons nous servir de la syntaxe du constructeur comme dune conversion :
int x = int(Pi * 100);

La conversion des pointeurs et rfrences laide des conversions traditionnelles de style C est assez prilleuse parce que le compilateur nous autorise convertir nimporte quel type de pointeur (ou rfrence) vers nimporte quel autre type de pointeur (ou rfrence). Cest pourquoi C++ a introduit quatre conversions de style nouveau avec une smantique plus prcise. Pour les pointeurs et rfrences, les nouvelles conversions sont prfrables aux conversions de style C plus risques et sont utilises dans cet ouvrage. static_cast<T>() convertit un pointeur-vers-A en pointeur-vers-B, en imposant que la classe B hrite de la classe A. Par exemple :
A *obj = new B; B *b = static_cast<B *>(obj); b->someFunctionDeclaredInB();

Si lobjet nest pas une instance de B (mais quil hrite de A), lusage du pointeur obtenu peut produire des pannes obscures. dynamic_cast<T>() est analogue static_cast<T>(), sauf que les informations de type lexcution (RTTI) servent contrler si lobjet associ au pointeur est bien une instance de la classe B. Si ce nest pas le cas, la conversion renvoie un pointeur nul. Par exemple :
A *obj = new B; B *b = dynamic_cast<B *>(obj); if (b) b-> someFunctionDeclaredInB();

Avec certains compilateurs, dynamic_cast<T>() ne fonctionne pas au-del des limites de la bibliothque dynamique. Cet lment sappuie galement sur la prise en charge de RTTI par le compilateur, une fonctionnalit que tout bon programmeur doit dsactiver pour rduire la taille des excutables. Qt rsout ces problmes en fournissant qobject_cast<T>() pour les sous-classes de QObject. const_cast<T>() ajoute ou supprime un qualicateur const sur un pointeur ou une rfrence. Par exemple :
int MyClass::someConstFunction() const { if (isDirty()) { MyClass *that = const_cast<MyClass *>(this);

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

511

that->recomputeInternalData(); } ... }

Dans lexemple prcdent, nous convertissons le qualicateur const du pointeur this an dappeler la fonction membre non-const recomputeInternalData(). Il nest pas recommand de procder de cette faon et vous pouvez lviter en codant mutable, comme expliqu au Chapitre 4. reinterpret_cast<T>() convertit tout type pointeur ou rfrence vers un autre type de ce genre. Par exemple :
short j = 0x1234; if (reinterpret_cast<char &>(j) == 0x12) cout << "The byte order is big-endian" << endl;

En Java et C#, il est possible de stocker toute rfrence en tant que rfrence dObject. C++ ne comporte pas une telle classe de base universelle, mais il fournit un type de donnes particulier, void*, qui stocke ladresse dune instance de nimporte quel type. Un void* doit tre reconvertit vers un autre type ( laide de static_cast<T>()) avant dtre exploit. C++ offre de nombreuses mthodes de conversion entre types, mais nous avons rarement besoin deffectuer une conversion dans ce langage. Lorsque nous utilisons des classes conteneurs telles que std::vector<T> ou QVector<T>, nous spcions le type T et nous rcuprons les lments sans convertir. De plus, pour les types primitifs, certaines conversions seffectuent implicitement (de char vers int par exemple), et pour les types personnaliss nous pouvons dnir des conversions implicites en fournissant un constructeur un paramtre. Par exemple :
class MyInteger { Public MyInteger(); MyInteger(int i); ... }; int main() { MyInteger n; n = 5; ... }

Pour certains constructeurs un paramtre, la conversion automatique nest pas justie. Il est possible de la dsactiver en dclarant le constructeur avec le mot-cl explicit:
class MyVector {

512

Qt4 et C++ : Programmation dinterfaces GUI

Public explicit MyVector(int size); ... };

Surcharge doprateur
C++ nous permet de surcharger des fonctions, ce qui signie que nous pouvons dclarer plusieurs fonctions avec le mme nom dans la mme porte, partir du moment o leurs listes de paramtres sont diffrentes. C++ prend aussi en charge la surcharge des oprateurs. Il sagit de la possibilit daffecter une smantique particulire des oprateurs intgrs (tels que +, <<, et []) lorsquils sont employs avec des types personnaliss. Nous avons dj tudi quelques exemples doprateurs surchargs. Lorsque nous avions utilis << pour transmettre du texte en sortie vers cout ou cerr, nous navions pas excut loprateur de dcalage de C++, mais plutt une version particulire de cet oprateur qui recevait un objet ostream (tel que cout et cerr) gauche et une chane (ou bien un nombre ou un manipulateur de ux tel que endl) droite et qui renvoyait lobjet ostream, ce qui permettait deffectuer plusieurs appels par ligne. Lintrt de la surcharge des oprateurs est que nous pouvons obtenir un comportement pour nos types personnaliss identique celui des types intgrs. Pour illustrer ce point, nous allons surcharger +=, -=, +, et - pour travailler avec des objets Point2D:
#ifndef POINT2D_H #define POINT2D_H class Point2D { Public Point2D(); Point2D(double x, double y); void setX(double x); void setY(double y); double x() const; double y() const; Point2D &operator+=(const Point2D &other) { xVal += other.xVal; yVal += other.yVal; return *this; } Point2D &operator-=(const Point2D &other) { xVal -= other.xVal; yVal -= other.yVal; return *this; } Private

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

513

double xVal; double yVal; }; inline Point2D operator+(const Point2D &a, const Point2D &b) { return Point2D(a.x() + b.x(), a.y() + b.y()); } inline Point2D operator-(const Point2D &a, const Point2D &b) { return Point2D(a.x() - b.x(), a.y() - b.y()); } #endif

Les oprateurs peuvent tre implments soit en tant que fonctions membres soit en tant que fonctions globales. Dans notre exemple, nous avons implment += et -= en tant que fonctions membres, et + et - en tant que fonctions globales. Les oprateurs += et -= reoivent la rfrence dun autre objet Point2D et incrmentent ou dcrmentent les coordonnes x et y de lobjet courant en fonction de lautre objet. Ils renvoient *this, qui reprsente une rfrence lobjet courant (de type Point2D*). Puisquon renvoie une rfrence, nous pouvons crire du code un peu exotique comme ci-aprs :
a += b += c;

Les oprateurs + et - reoivent deux paramtres et renvoient un objet Point2D par valeur (et non la rfrence dun objet existant). Grce au mot-cl inline, nous avons la possibilit de placer ces dnitions de fonction dans le chier den-tte. Si le corps de la fonction avait t plus long, nous aurions insr le prototype de fonction dans le chier den-tte et la dnition de fonction (sans le mot-cl inline) dans le chier .cpp. Lextrait de code suivant prsente les quatre oprateurs surchargs en action :
Point2D alpha(12.5, 40.0); Point2D beta(77.5, 50.0); alpha += beta; beta -= alpha; Point2D gamma = alpha + beta; Point2D delta = beta - alpha;

Nous invoquons les fonctions oprateur comme nimporte quelle autre fonction :
Point2D alpha(12.5, 40.0); Point2D beta(77.5, 50.0); alpha.operator+=(beta); beta.operator-=(alpha);

514

Qt4 et C++ : Programmation dinterfaces GUI

Point2D gamma = operator+(alpha, beta); Point2D delta = operator-(beta, alpha);

La surcharge des oprateurs en C++ est un sujet complexe, mais vous navez pas forcment besoin den connatre tous les dtails. Il est surtout important de comprendre les principes de base de la surcharge des oprateurs parce que plusieurs classes Qt (notamment QString et QVector<T>) sappuient sur cette fonctionnalit pour fournir une syntaxe plus simple et plus naturelle pour des oprations telles que la concatnation et lajout dlment.

Les types valeur


Java et C# diffrencient les types valeur et les types rfrence. Types valeur : il sagit des types primitifs tels que char, int, et float, ainsi que les structures C#. Ils sont caractriss par le fait quils ne sont pas crs laide de new et que loprateur daffectation copie la valeur stocke dans la variable. Par exemple :
int i = 5; int j = 10; i = j;

Types rfrence : Il sagit de classes telles que Integer (en Java), String, et MaClasseAmoi. Les instances sont cres en codant new. Loprateur daffectation copie uniquement une rfrence de lobjet ; pour obtenir une copie intgrale, il faut appeler clone() (en Java) ou Clone() (en C#). Par exemple :
Integer i = new Integer(5); Integer j = new Integer(10); i = j.clone();

En C++, tous les types peuvent tre employs en tant que "types rfrence," et ceux qui peuvent tre copis peuvent aussi tre employs en tant que "types valeur".C++na pas besoin dun classe Integer, par exemple, parce que vous pouvez vous servir des pointeurs et de new comme dans les lignes suivantes :
int *i = new int(5); int *j = new int(10); *i = *j;

Contrairement Java et C#, C++ traite les classes dnies par lutilisateur exactement comme des types intgrs :
Point2D *i = new Point2D(5, 5); Point2D *j = new Point2D(10, 10); *i = *j;

Si nous dsirons rendre une classe C++ copiable, nous devons prvoir un constructeur de copie et un oprateur daffectation pour cette dernire. Le constructeur de copie est invoqu lorsque

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

515

nous initialisons lobjet avec un autre objet de mme type. C++ fournit deux syntaxes quivalentes pour cette situation :
Point2D i(20, 20); Point2D j(i); // premire syntaxe Point2D k = i; // deuxime syntaxe

Loprateur daffectation est invoqu lorsque nous utilisons cet oprateur sur une variable existante :
Point2D i(5, 5); Point2D j(10, 10); j = i;

Lorsque nous dnissons une classe, le compilateur C++ fournit automatiquement un constructeur de copie et un oprateur daffectation qui effectue une copie membre membre. Pour la classe Point2D, cest comme si nous avions crit le code suivant dans la dnition de classe :
class Point2D { Public ... Point2D(const Point2D &other) : xVal(other.xVal), yVal(other.yVal) { } Point2D &operator=(const Point2D &other) { xVal = other.xVal; yVal = other.yVal; return *this; } ... Private double xVal; double yVal; };

Pour certaines classes, le constructeur de copie par dfaut et loprateur daffectation ne conviennent pas. Cela se produit en particulier lorsque la classe travaille en mmoire dynamique. Pour la rendre copiable, nous devons alors implmenter nous-mmes le constructeur de copie et loprateur daffectation. Pour les classes qui nont pas besoin dtre copiables, nous avons la possibilit de dsactiver le constructeur de copie et loprateur daffectation en les rendant privs. Si nous tentons accidentellement de copier des instances de cette classe, le compilateur signale une erreur. Par exemple :
class BankAccount { Public

516

Qt4 et C++ : Programmation dinterfaces GUI

... Private BankAccount(const BankAccount &other); BankAccount &operator=(const BankAccount &other); };

En Qt, beaucoup de classes sont conues pour tre utilises en tant que classes de valeur. Elles disposent dun constructeur de copie et dun oprateur daffectation, et elles sont normalement instancies sur la pile sans new. Cest le cas en particulier pour QDateTime, QImage, QString, et les classes conteneur telles que QList<T>, QVector<T>, et QMap<K,T>. Les autres classes appartiennent la catgorie "type rfrence", notamment QObject et ses sous-classes (QWidget, QTimer, QTcpSocket, etc.). Celles-ci possdent des fonctions virtuelles et ne peuvent pas tre copies. Un QWidget reprsente par exemple une fentre spcique ou un contrle sur lcran. Sil y a 75 instances de QWidget en mmoire, il y a aussi 75 fentres et contrles sur cet cran. Ces classes sont typiquement instancies laide de loprateur new.

Variables globales et fonctions


C++ autorise la dclaration de fonctions et de variables qui nappartiennent aucune classe et qui sont accessibles dans nimporte quelle autre fonction. Nous avons tudi plusieurs exemples de fonctions globales, dont main(), le point dentre du programme. Les variables globales sont plus rares, parce quelles compromettent la modularit et la rentrance du thread. Il est important de bien les comprendre parce que vous pourriez les retrouver dans du code crit par danciens programmeurs C ou dautres utilisateurs de C++. Nous allons illustrer le fonctionnement des fonctions et variables globales en tudiant un petit programme qui afche une liste de 128 nombres pseudo-alatoires laide dun algorithme rapide et simple. Le code source du programme est rparti dans deux chiers .cpp. Le premier est random.cpp:
int randomNumbers[128]; static int seed = 42; static int nextRandomNumber() { seed = 1009 + (seed * 2011); return seed; } void populateRandomArray() { for (int i = 0; i < 128; ++i) randomNumbers[i] = nextRandomNumber(); }

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

517

Dans ce chier, deux variables globales (randomNumbers et seed) et deux fonctions globales (nextRandomNumber() et populateRandomArray()) sont dclares. Deux dclarations contiennent le mot-cl static; elles ne sont visibles que dans lunit de compilation courante (random.cpp) et on dit quelles ont une liaison statique. Les deux autres sont disponibles dans nimporte quelle unit de compilation du programme ; elles ont une liaison externe. La liaison statique est particulirement indique pour les fonctions assistantes et les variables internes qui ne doivent pas tre utilises dans dautres units de compilation. Elle rduit le risque de collision didenticateurs (variables globales de mme nom ou fonctions globales avec la mme signature dans des units de compilation diffrentes) et empche des utilisateurs malveillants ou trs maladroits daccder au code dune unit de compilation. Examinons maintenant le second chier, main.cpp, qui utilise les deux variables globales dclares avec une liaison externe dans random.cpp:
#include <iostream> using namespace std; extern int randomNumbers[128]; void populateRandomArray(); int main() { populateRandomArray(); for (int i = 0; i < 128; ++i) cout << randomNumbers[i] << endl; return 0; }

Nous dclarons les variables et fonctions externes avant de les appeler. La dclaration de variable externe (qui rend la variable visible dans lunit de compilation courante) pour randomNumbers dbute avec le mot-cl extern. Sans ce mot-cl, le compilateur pourrait penser quil sagit dune dnition de variable, et lditeur de liens rapporterait une erreur pour la mme variable dnie dans deux units de compilation diffrentes (random.cpp et main.cpp). Vous pouvez dclarer des variables autant de fois que vous voulez, mais elles ne doivent tre dnies quune seule fois. Cest partir de la dnition que le compilateur rserve lespace requis pour la variable. La fonction populateRandomArray() est dclare avec un prototype de fonction. Le mot-cl extern est facultatif pour les fonctions. Nous pourrions typiquement placer les dclarations de fonction et de variable externes dans un chier den-tte et inclure ce dernier dans tous les chiers qui en ont besoin :
#ifndef RANDOM_H #define RANDOM_H

518

Qt4 et C++ : Programmation dinterfaces GUI

extern int randomNumbers[128]; void populateRandomArray(); #endif

Nous avions dj vu comment coder static pour dclarer des fonctions et variables membres qui ne sont pas associes une instance spcique de la classe, et nous venons maintenant de voir comme utiliser ce mot-cl pour dclarer des fonctions et variables avec une liaison statique. Notez quil existe un autre usage de static. En C++, nous pouvons dclarer une variable locale avec ce mot-cl. De telles variables sont initialises la premire fois que la fonction est appele et leur valeur est garantie entre plusieurs appels de fonction. Par exemple :
void nextPrime() { static int n = 1; do { ++n; } while (!isPrime(n)); return n; }

Les variables locales statiques sont similaires aux variables globales, mais elles ne sont visibles qu lintrieur de la fonction dans laquelle elles sont dnies.

Espaces de noms
Les espaces de noms sont un mcanisme destin rduire les risques de collision de noms dans les programmes C++. Ces collisions sont frquentes dans les gros programmes qui exploitent plusieurs bibliothques tiers. Dans vos propres programmes, vous avez le choix de les utiliser ou non. Nous encadrons typiquement dans un espace de noms toutes les dclarations dun chier dentte an de ne pas "polluer" lespace de noms global avec les identicateurs quil contient. Par exemple :
#ifndef SOFTWAREINC_RANDOM_H #define SOFTWAREINC_RANDOM_H namespace SoftwareInc { extern int randomNumbers[128]; void populateRandomArray(); } #endif

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

519

(Notez que nous avons aussi renomm la macro de prprocesseur utilise pour viter les inclusions multiples, rduisant ainsi le risque de collision de nom avec un chier den-tte de mme nom mais situ dans un rpertoire diffrent.) La syntaxe dun espace de noms est similaire celle dune classe, mais la ligne ne se termine pas par un point-virgule. Voici le nouveau chier random.cpp:
#include "random.h" int SoftwareInc::randomNumbers[128]; static int seed = 42; static int nextRandomNumber() { seed = 1009 + (seed * 2011); return seed; } void SoftwareInc::populateRandomArray() { for (int i = 0; i < 128; ++i) randomNumbers[i] = nextRandomNumber(); }

Contrairement aux classes, les espaces de noms peuvent tre "rouverts" tout moment. Par exemple :
namespace Alpha { void alpha1(); void alpha2(); } namespace Beta { void beta1(); } namespace Alpha { void alpha3(); }

Vous avez ainsi la possibilit de dnir des centaines de classes, situes dans autant de chiers den-tte, dans un seul espace de noms. La bibliothque standard C++ englobe ainsi tous ses identicateurs dans lespace de noms std. Dans Qt, les espaces de noms servent pour des identicateurs de type global tels que Qt::AlignBottom et Qt::yellow. Pour des raisons historiques, les classes de Qt nappartiennent aucun espace de noms mais sont prxes de la lettre "Q".

520

Qt4 et C++ : Programmation dinterfaces GUI

Pour se rfrer un identicateur dclar dans un espace de noms depuis lextrieur de cet espace, nous le prxons avec le nom de lespace de noms (et ::). Nous pouvons aussi faire appel lun des trois mcanismes suivants, qui visent rduire la frappe.

Dnir un alias despace de noms :


namespace ElPuebloDeLaReinaDeLosAngeles { void beverlyHills(); void culverCity(); void malibu(); void santaMonica(); } namespace LA = ElPuebloDeLaReinaDeLosAngeles;

Aprs la dnition de lalias, celui-ci peut tre employ la place du nom original.

Importer un identicateur unique depuis un espace de noms :


int main() { using ElPuebloDeLaReinaDeLosAngeles::beverlyHills; beverlyHills(); ... }

La dclaration using nous permet daccder un identicateur donn dans un espace de noms sans avoir besoin de le prxer avec le nom de lespace de noms.

Importer un espace de noms complet via une unique directive :


int main() { using namespace ElPuebloDeLaReinaDeLosAngeles; santaMonica(); malibu(); ... }

Cette approche augmente les risques de collision de noms. Ds que le compilateur signale un nom ambigu (deux classes de mme nom, par exemple, dnies dans deux espaces de noms diffrents), nous sommes toujours en mesure de qualier lidenticateur avec le nom de lespace de noms lorsque nous avons besoin dy faire rfrence.

Le prprocesseur
Le prprocesseur C++ est un programme qui convertit un chier source .cpp contenant des directives #- (telles que #include, #ifndef, et #endif) en chier source ne contenant aucune directive de cette sorte. Ces directives effectuent des oprations de texte simples sur le

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

521

chier source, comme une compilation conditionnelle, une inclusion de chier et une expansion de macro. Le prprocesseur est normalement invoqu automatiquement par le compilateur, mais dans la plupart des systmes vous avez encore la possibilit de linvoquer seul (souvent par lintermdiaire dune option de compilateur -E ou /E).

La directive #include insre localement le contenu du chier indiqu entre crochets (<>) ou guillemets doubles (""), selon que le chier den-tte est install sur un emplacement standard ou avec les chiers du projet courant. Le nom de chier peut contenir .. et / (que les compilateurs Windows interprtent correctement comme tant des sparateurs de rpertoire). Par exemple :
#include "../shared/globaldefs.h"

La directive #define dnit une macro. Les occurrences de la macro qui apparaissent aprs la directive #define sont remplaces par la dnition de la macro. Par exemple, la directive
#define PI 3.14159265359

demande au prprocesseur de remplacer toutes les futures occurrences du jeton PI dans lunit de compilation courante par le jeton 3.14159265359. Pour viter les collisions entre noms de variable et de classe, on affecte gnralement aux macros des noms en majuscules. Nous pouvons trs bien dnir des macros qui reoivent des arguments :
#define SQUARE(x) ((x) * (x))

Dans le corps de la macro, il est conseill dencadrer toutes les occurrences des paramtres par des parenthses, ainsi que le corps complet, pour viter les problmes de priorit des oprateurs. Aprs tout, nous voulons que 7*SQUARE(2+3) soit dvelopp en 7*((2+3)*(2+3)), et non en 7*2+3*2+3. Les compilateurs C++ nous permettent normalement de dnir des macros sur la ligne de commande, via loption -D ou /D. Par exemple :
CC -DPI=3.14159265359 -c main.cpp

Les macros taient trs populaires avant larrive des typedef, numrations, constantes, fonctions inline et modles. Leur rle aujourdhui consiste surtout protger les chiers den-tte contre les inclusions multiple.

Vous annulez la dnition dune macro tout moment laide de #undef:


#undef PI

Cest pratique si nous avons lintention de rednir une macro, puisque le prprocesseur interdit de dnir deux fois la mme macro. a sert aussi contrler la compilation conditionnelle.

522

Qt4 et C++ : Programmation dinterfaces GUI

Vous traitez des parties du code ou vous les sautez en codant #if, #elif, #else, et #endif, en fonction de la valeur numrique des macros. Par exemple :
#define NO_OPTIM 0 #define OPTIM_FOR_SPEED 1 #define OPTIM_FOR_MEMORY 2 #define OPTIMIZATION OPTIM_FOR_MEMORY ... #if OPTIMIZATION == OPTIM_FOR_SPEED typedef int MyInt; #elif OPTIMIZATION == OPTIM_FOR_MEMORY typedef short MyInt; #else typedef long long MyInt; #endif

Dans lexemple ci-dessus, seule la deuxime dclaration de typedef devra tre traite par le compilateur, MyInt tant ainsi dni comme un synonyme de short. En changeant la dnition de la macro OPTIMIZATION, nous obtenons des programmes diffrents. Lorsquune macro nest pas dnie, sa valeur est considre comme tant 0. Une autre approche de la compilation conditionnelle consiste tester si une macro est dnie ou non. Vous procdez en codant loprateur defined() comme suit :
#define OPTIM_FOR_MEMORY ... #if defined(OPTIM_FOR_SPEED) typedef int MyInt; #elif defined(OPTIM_FOR_MEMORY) typedef short MyInt; #else typedef long long MyInt; #endif

Pour des raisons pratiques, le prprocesseur reconnat #ifdef X et #ifndef X comme des synonymes de #if defined(X) et #if!defined(X). Vous protgez un chier den-tte des inclusions multiple en encadrant son contenu comme ci-aprs :
#ifndef MYHEADERFILE_H #define MYHEADERFILE_H ... #endif

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

523

La premire fois que le chier den-tte est inclus, le symbole MYHEADERFILE_H nest pas dni, donc le compilateur traite le code entre #ifndef et #endif. La seconde et toutes les fois suivantes o le chier den-tte est inclus, MYHEADERFILE_ H est dj dni, donc le bloc #ifndef #endif complet est ignor.

La directive #error met un message derreur dni par lutilisateur au moment de la compilation. Elle est souvent associe une compilation conditionnelle pour signaler un cas impossible. Par exemple :
class UniChar { Public #if BYTE_ORDER == BIG_ENDIAN uchar row; uchar cell; #elif BYTE_ORDER == LITTLE_ENDIAN uchar cell; uchar row; #else #error "BYTE_ORDER must be BIG_ENDIAN or LITTLE_ENDIAN" #endif };

Contrairement la plupart des autres constructions C++, pour lesquelles les espaces ntaient pas signicatifs, les directives de prprocesseur doivent apparatre sur leur propre ligne, sans point-virgule la n. Les directives trs longues peuvent occuper plusieurs lignes en terminant chacune delles sauf la dernire par un slash invers (\).

La bibliothque C++ standard


Dans cette section, nous allons rapidement passer en revue la bibliothque C++ standard. La Figure B.3 rpertorie les principaux chiers den-tte C++. Les en-ttes <exception>, <limits>, <new>, et <typeinfo> prennent en charge le langage C++ ; par exemple, <limits> nous permet de tester les proprits du support arithmtique des types entier et virgule ottante sur le compilateur, et <typeinfo> offre une introspection de base. Les autres en-ttes fournissent gnralement des classes pratiques, notamment une classe chane et un type numrique complexe. La fonctionnalit offerte par <bitset>, <locale>, <string>, et <typeinfo> reprend largement celle des classes QBitArray, QLocale, QString, et QMetaObject de Qt. Le C++ standard inclut aussi un ensemble de chiers den-tte qui traitent les E/S, numrs en Figure B.4. La conception des classes dentres/sorties standard datant des annes 80, elles sont assez complexes et donc difciles tendre. Lopration est dailleurs tellement complexe que des livres complets ont t crits sur le sujet. Le programmeur hrite dailleurs avec ces classes dun bon nombre de problmes non rsolus lis au codage des caractres et aux reprsentations binaires dpendantes de la plate-forme des types de donnes primitifs.

524

Qt4 et C++ : Programmation dinterfaces GUI

Fichier den-tte

Description Classe modle pour reprsenter des squences de bits de longueur xe Classe modle pour reprsenter des nombres complexes Types et fonctions lis la gestion des exceptions Classe modle qui spcie les proprits des types numriques Classes et fonctions lies la localisation Fonctions qui grent lallocation de mmoire dynamique Types dexception prdnis pour signaler les erreurs Conteneur de chanes modle et caractristiques des caractres Classe qui fournit des informations de base concernant un type Classes modle pour reprsenter des tableaux de valeurs

<bitset> <complex> <exception> <limits> <locale> <new> <stdexcept> <string> <typeinfo> <valarray>

Figure B.3 Principaux chiers den-tte de la bibliothque C++ Fichier den-tte Description Classes modle qui manipulent des chiers externes Manipulateur de ux dE/S qui reoit un argument Classe de base modle pour les ux dE/S Dclarations anticipes pour plusieurs classes modles de ux dE/S Flux dE/S standard (cin, cout, cerr, clog) Classe modle qui contrle les entres en provenance dune mmoire tampon de ux Classe modle qui contrle les sorties vers une mmoire tampon de ux Classes modle qui associent des mmoires tampon de ux avec des chanes Classes modle qui placent en mmoire tampon les oprations dE/S Classes pour effectuer des oprations de ux dE/S sur des tableaux de caractres

<fstream> <iomanip> <ios> <iosfwd> <iostream> <istream> <ostream> <sstream> <streambuf> <strstream>

Figure B.4 Fichiers den-tte de la bibliothque dE/S C++

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

525

Le Chapitre 12 consacr aux entres/sorties dtaille les classes Qt correspondantes, qui prsentent les entres/sorties Unicode ainsi quun jeu important de codages de caractres nationaux et une abstraction indpendante de la plate-forme pour stocker les donnes binaires. Les classes dE/S de Qt forment la base des communications interprocessus, de la gestion de rseau, et du support XML de Qt. Les classes de ux de texte et binaire de Qt sont trs faciles tendre pour prendre en charge les types de donnes personnaliss. La bibliothque STL (Standard Template Library) existe depuis les annes 90 et propose un jeu de classes conteneur, itrateurs et algorithmes bass sur des modles, qui font maintenant partie de la norme standard ISO C++. La Figure B.5 numre les chiers den-tte de la STL. La conception de cette bibliothque est rigoureuse, presque mathmatique, et elle fournit une fonctionnalit gnrique scurise au niveau des types. Qt fournit ses propres classes conteneur, dont la conception sinspire de celle de la STL. Ces classes sont dtailles au Chapitre 11.
Fichier den-tte Description Fonctions modle usage gnral Conteneur modle de le dattente double accs Modles daide la construction et manipulation des functeurs (objets fonction) Modles daide la construction et manipulation des itrateurs Conteneur modle de listes doublement chanes Conteneurs modle de map valeur unique ou multiple Utilitaires pour simplier la gestion de mmoire Oprations numriques modles Conteneur modle de les dattente Conteneurs modle densembles valeur unique ou multiple Conteneur modle de piles Fonctions modle de base Conteneur modle de vecteurs

<algorithm> <deque> <functional> <iterator> <list> <map> <memory> <numeric> <queue> <set> <stack> <utility> <vector>

Figure B.5 chiers den-tte de la STL

C++ tant surtout une version amliore du langage de programmation C, les programmeurs C++ disposent aussi de la bibliothque C complte. Les chiers den-tte C sont disponibles

526

Qt4 et C++ : Programmation dinterfaces GUI

soit sous leurs noms traditionnels (par exemple <stdio.h>) soit sous leur forme moderne avec le prxe c- et sans lextension .h (<cstdio>, par exemple). Lorsque nous codons avec le nom moderne, les fonctions et types de donnes sont dclars dans lespace de noms std. (Cela ne sapplique pas des macros telles que ASSERT(), parce que le prprocesseur ne connat pas les espaces de noms.) La nouvelle syntaxe est recommande si votre compilateur la prend en charge. La Figure B.6 rpertorie les chiers den-tte C. La plupart proposent une fonctionnalit qui recouvre celle de chiers den-tte C++ ou de Qt plus rcents. Notez que <cmath> est une exception, ce chier dclare des fonctions mathmatiques telles que sin(), sqrt(), et pow().
Fichier den-tte Description La macro ASSERT() Fonctions pour classer et faire correspondre les caractres Macros lies au signalement des conditions derreur Macros spciant les proprits des types virgule ottante primitifs Autres orthographes pour les utilisateurs du jeu de caractres ISO 646 Macros spciant les proprits des types entiers primitifs Fonctions et types lis la localisation Fonctions et constantes mathmatiques Fonctions pour excuter des branchements non locaux Fonctions pour grer les signaux du systme Macros pour implmenter les fonctions liste dargument variable Dnitions courantes de plusieurs en-ttes standard Fonctions pour effectuer les E/S Fonctions utilitaires gnrales Fonctions pour manipuler les tableaux de caractres Types et fonctions pour manipuler le temps Fonctions de manipulation de chanes de caractres tendus Fonctions pour classer et faire correspondre les caractres tendus

<cassert> <cctype> <cerrno> <cfloat> <ciso646> <climits> <clocale> <cmath> <csetjmp> <csignal> <cstdarg> <cstddef> <cstdio> <cstdlib> <cstring> <ctime> <cwchar> <cwctype>

Figure B.6 Fichiers den-tte C++ pour les utilitaires de bibliothque C

Annexe B

Introduction au langage C++ pour les programmeurs Java et C#

527

Cette rapide prsentation de la bibliothque C++ standard sachve prsent. Dinkumware propose sur Internet une documentation de rfrence complte concernant la bibliothque C++ standard ladresse http://www.dinkumware.com/refxcpp.html, et SGI offre un guide du programmeur STL ladresse http://www.sgi.com/tech/stl/. La dnition ofcielle de cette bibliothque se trouve avec les standard C et C++, disponibles sous forme de chiers PDF ou de copies papier demander auprs de lorganisation ISO (International Organization for Standardization). Cette annexe traite en trs peu de pages un sujet normalement fort tendu. Quand vous allez commencer tudier Qt partir du Chapitre 1, vous allez dcouvrir que la syntaxe est beaucoup plus simple que cette annexe ne pouvait le laisser supposer. Vous pouvez trs bien programmer en Qt en faisant uniquement appel un sous-ensemble de C++ et sans avoir besoin de la syntaxe plus complexe que vous pouvez retrouver dans ce langage. Ds que vous aurez commenc saisir du code et crer et lancer vos excutables, vous ne pourrez que constater quel point lapproche de Qt est simple et claire. Et ds que vous aborderez des programmes plus ambitieux, en particulier ceux qui ont besoin dun graphisme performant et labor, la combinaison C++/Qt continuera vous donner toute satisfaction.

Index

Symboles
#define 487, 521 #endif 445, 487 #ifdef 445 #ifndef 487 #include 486, 521 #undef 521 & 18, 497 () 94 <algorithm> 276 <cmath> 125 <iostream> 86 <QtAlgorithms> 276 <QtGui> 18, 48

A
About Qt 53 about() (QMessageBox) 70 accept() 411 QDialog 31, 36 Acceptable (QSpinBox) 107 acceptProposedAction() 215 acquire() 414, 415 Action 51 About Qt 53 Auto-Recalculate 52 exitAction 53 New 51

Open 52 Save 52 Save As 52 Select All 52 Show Grid 52 activateWindow() 65 Active (groupe de couleurs) 115 activeEditor() 162 ActiveQt 448 ActiveX 460 sous Windows 448 addAction() (QMenu) 54 addBindValue() 313 addCd() 326 addChildSettings() 235 addDatabase() 311, 314 addItem() (QComboBox) 241 addLibraryPath() 435 addMenu() (QMenu) 54 AddRef() 456 addRow() 233 addStretch() 148 addTrack() 327 addTransaction() 420-421 addWidget() 147, 151 QStatusBar 57 adjust() 139 PlotSettings 134 adjustAxis() 139 adjusted() 130

adjustSize() 127 Aide en ligne 373 Assistant Qt 379 avec lassistant 379 infobulle 374 informations dtat 374 QTextBrowser 376 Quest-ce que cest ? 374 Algorithme gnrique 276 qBinaryFind 277 qCopy 277 QCopyBackward() 285 qDeleteAll 278 qEqual() 285 qFill 277 qFind 276 qSort 277 qStableSort 278 qSwap 278 AlignHCenter 57 alignment 137 Anticrnelage 186, 189 bas sur X Render 197 AnyKeyPressed (QAbstractItemView) 231 API natives 444 append() 265 QString 279 Apple Developer Connection 479

530

Qt4 et C++ : Programmation dinterfaces GUI

Application Age 7 capable de grer les plug-in 435 Carnet dadresse 458 CD Collection 317 Cities 246 Color Names 240 Coordinate Setter 232 Currencies 243 Directory Viewer 238 crire des plug-in 439 Flowchart Symbol Picker 230 Image Converter 303, 353 Image Pro 419 implmenter la fonctionnalit 77 infobulle et information dtat 374 Mail Client 154 MDI Editor 160 ouverte aux traductions 390 Project Chooser 216 Quit 7 Regexp Parser 251 Settings Viewer 234 Splitter 152 Spreadsheet 46, 92 Team Leaders 236 Tetrahedron 207-208 Threads 409 Tic-Tac-Toe 462 traduire 402 Trip Planner 343 Windows Media Player 448 apply() 422-423 applyEffect() 438-440 appTranslator 398, 401 Architecture modle/vue 228 arg() 279, 392 QString 62 ARGB32 197

arguments() 330 QApplication 168 Assistant Qt 10, 379 asVariant() 452 at() 271 Attribut WA_DeleteOnClose 74, 162 WA_StaticContents 117 Auto-Recalculate 52, 71, 75, 79 avg() 104

B
BackgroundColorRole 242, 256 Barre dtat, configurer 56 Barre doutils ancrable 157 crer 51 Base de donnes 309 connexion et excution de requtes 310 formulaires 321 prsenter les donnes 317 beforeInsert() 320, 325 begin() 269 beginGroup() 71 Bibliothque C++ standard 523 dynamique 425, 488 E/S C++ 524 OpenGL 207 statique 488 STL 63 bindValue() 313 Bote de dialogue About 69 conception rapide 25 crer 15 de feedback 44 dynamique 39 tapes de cration 26 extensible 33 Find 16

Go-to-Cell 26 intgre 40 multiforme 32 multipage 39 QDialog (classe de base) 16 Sort 33 utiliser 64 bool 489 Boucle dvnement 4, 178 foreach 271 while 417 boundingRect() (QPainter) 204 break 273 BufferSize 416 buttons() (QMouseEvent) 117

C
C# diffrences avec C++ 489 diffrences avec Java 483 C++ chanes de caractres 505 dfinitions de classe 490 destructeur 496 diffrences avec Java et C# 489 numrations 507 espaces de noms 518 hritage 493 pointeurs 497 polymorphisme 493 prprocesseur 520 prsentation 483 surcharge doprateur 512 tableaux 502 typeDef 509 types valeur 514 cachedValue 97, 100 cacheIsDirty 97, 98 cacheIsValid 100 canRead() 428, 429

Index

531

canReadLine() 352 capabilities() 427 CanRead 428 CanReadIncremental 428 CanWrite 428 Capuchon 185 cascade() 164 CaseSensitivity 17 cd() 333 cdModel 325 cdTableView 325, 327 Cell 79, 81, 90, 96 arbre dhritage 79 cell() 83, 90 cerr 486 Chanes de caractres 505 changeEvent() 401 char 386, 489 characters() 362-363 Chargement 85 charset 224 checkable 33 Classe accs alatoire QBuffer 288 QFile 288 QTemporaryFile 288 conteneur 263 itrateurs 267 QBrush 271 QByteArray 266, 271 QCache 275 QDateTime 266 QFont 271 QHash 264, 273 QImage 271 QLinkedList 264-266 QList 264 QList<T> 266 QMap 264, 273 QPair 285 QPixmap 271 QRegExp 266 QSet 275 QString 266, 271

QVariant 266 QVarLengthArray <T,Prealloc> 285 QVector 264, 266 daffichage dlments 227 QListWidget 228 QTableWidget 228 QTreeWidget 228 utiliser 229 dlments 82 de modle 244 QAbstractItemModel 244 QAbstractListModel 244 QAbstractTableModel 244 de widgets 40 QByteArray 278 QString 278 QVariant 278 squentielle QProcess 288 QTcpSocket 288 QUdpSocket 288 TextArtDialog 437 wrapper de plug-in 426 Cl 71 clear() 81, 82, 252, 278, 400 Spreadsheet 58 clearCurve() 129 clicked() 7, 170, 304 Client FTP 330 HTTP 339 TCP 342 ClientSocket 349-350 clipboard() 88, 224 clone() 98, 514 close() 19, 61, 74, 164, 333 closeActiveWindow() 164 closeAllWindows() 74, 164 closeConnection(), TCP 348 closeEditor() 260 closeEvent() 70, 164, 167

MainWindow 74 QWidget 47, 61 codecForName() 389 CodeEditor 171 ColorGroup 115 ColorNamesDialog 240 column() (QModelIndex) 242 columnCount() 82, 244 columnSpan 148 COM, objets 448 commit() 313 commitAndCloseEditor() 259 commitData() 464 Communication inter-processus 303 Compagnon 18 compare() 68, 94-95 Compilateur excution 486 macros 446 MinGW C++ 478 moc 22 uic 29 units 484 Composition (mode) 188, 198 CompositionMode_SourceOver 198 CompositionMode_Xor 199 connect() 19, 192, 419, 423 QObject 9, 23 connected() 344, 345 connectionClosedByServer() 349 connectToHost() 333, 345 connectToServer() 345 Connexion aux bases de donnes 310 de requtes 310 tablir 6 signal/slot 304 const 499 const_cast<T>() 510 constData() 283 Constructeur de copie 514 contains() (QRect) 117

532

Qt4 et C++ : Programmation dinterfaces GUI

Conteneur associatif 264, 273 QCache 275 QHash<K,T> 264, 273 QMap<K,T> 264, 273 QSet 275 copie 270 squentiel 264 QLinkedList 266 QLinkedList<T> 264265 QList<T> 264, 266 QQueue<T> 266 QStack<T> 266 QVector 264 QVector<T> 264, 266 contentsChanged() 166-167 contextMenuEvent() (QWidget) 55, 67 ContiguousSelection 81, 89 continue 273 Contrle ActiveX 448 controllingUnknown() 456 Conversions de type 509 ConvertDepthTransaction 422 convertTo1Bit() 420 convertTo32Bit() 420 convertTo8Bit() 420 convertToFormat() 112 Coordonnes conversion logiquesphysiques 190 systme par dfaut 188 copy() 88 CopyAction 218 copyAvailable() 162 count() 265 Courbe de Bzier 186 cout 486 Crayon 185 create() 121, 427-428 createActions() 49, 376, 398 createConnection() 311, 313 createContextMenu() 49 createEditor() 161, 258, 260

createIndex() 249 createLanguageMenu() 399-400 createMenus() 49, 164, 398 createStatusBar() 49, 56 createToolBars() 49 critical() (QMessageBox) 59 curFile 62, 167 currencyAt() 246 CurrencyModel 243 currentCdChanged() 324 currentCellChanged() 57 currentDateTime() (QDateTime) 193 currentFormula() 84 currentIndex() (QComboBox) 68 currentItem 364 currentLocation() 84 currentRow 151 currentRowChanged() 151 currentText 363 currentThread() 417 Curseur 452 CursorHandler 428, 434 curveMap 129 curZoom 127 CustomerInfoDialog 176 cut() 88 Editor 163

D
data() 99, 244-245, 283 QMimeData 219 QTableWidgetItem 96 dataChanged() 249 QClipboard 225 Datagramme UDP 353 DataSize 416 debug 5 Dclaration pralable 16 default 33 defined() 522 Dfinition de classe 490

Dgrad circulaire 188 conique 188 linaire 187 del() 90 Dlgu 228 personnalis 256 delete 90, 252 deleteLater() 350 delta() 135 Drivation QDialog 16 QMainWindow 46 QMimeData 224 QTableWidget 78 QTableWidgetItem 96 QWidget 108 Dessin courbe de Bzier 186 crayon 185 dgrads circulaires 188 coniques 188 linaires 187 formes gomtriques 186 pinceau 185 police 185 QPainter 184 rectangle sans anticrnelage 189 styles de capuchon et jointure 185 de crayon 185 trac 187 viewport 189 DiagCrossPattern 187 Dinkumware 527 Directives 445 #include 486 de prprocesseur 487 Disabled (groupe de couleurs) 115 disconnect() 423

Index

533

disconnected() 344, 349 DisplayRole 97, 99, 242, 245 Disposition 7, 28, 144 empile 150 grer 143 gestionnaires 9, 146 manuelle 145 positionnement absolu 144 distances 248 DLL 425, 488 Document multipage 201 multiple 72 Documentation de rfrence 10 documentElement() 367 documentTitle() 378 documentWasModified() 166 DOM (Document Object Model) 359 DomParser 367 done() 231, 331 Donne binaire criture 288 lecture 288 les stocker en tant qulments 82 prsentation sous forme tabulaire 317 types primitifs 489 DontConfirmOverwrite (QFileDialog) 61 double 489 mise en mmoire tampon 122 dragEnterEvent() 214-215 dragMoveEvent() 218 draw() 184, 194, 209 dessin dun ttradre 210 drawCurves() 136, 138 drawDisplay() 259 drawFocus() 259 drawGrid() 136 drawLine() 196 QPainter 114

drawPie() 186 drawPolygon() 195 drawPolyline() 138 drawPrimitive() 130 QStylePainter 130 drawRect() 189 drawText() 137, 196 conversion des coordonnes 190 dropEvent() 214 glisser-dposer 215 dumpdoc 451 duration() 193 dynamic_cast<T>() 510 dynamicCall() 452

E
E_NOINTERFACE 456 Echelon 140 Ecriture code XML 370 donnes binaires 288 texte 294 Editeur de connexion (Qt Designer) 37 de liens 485 editingFinished() 259 Editor 164 cut() 163 fonctions 165 save() 162 EditRole 97-99, 242 effects() 436, 439 Ellipse (dessiner) 186 emit 21 enabled 27 enableFindButton() 19, 21 end() 269 conteneur non const 271 endElement() 360, 362, 364 endGroup() 71 endl 486

endsWith() 281 enterErrorState() 431 Entres/sorties 287 lire et crire des donnes binaires 288 lire et crire du texte 294 parcourir les rpertoires 300 entryHeight() 203, 204 entryList() 301 Enumrations 507 Equivalence pointeurs/tableaux 503 eraseRect() 440 error() 306, 344, 349 errorString() 363 escape() 221 Espace de noms 518 std 125 Espacement 20 evalExpression() 100-101, 104 evalFactor() 101, 104 evalTerm() 101-104 Evnement 4 boucle 178 close 164 drop 214 filtre 175, 177 gestionnaires, rimplmenter 170 key 170 paint 113, 118 propagation 178 resize 131 timer 172 traiter 169, 177 versus signal 170 wheel 135 event() 170 eventFilter() 176 exec() 66, 312, 418 QApplication 178 QDialog 66 QPrintDialog 200 requte SQL 311

534

Qt4 et C++ : Programmation dinterfaces GUI

Excutable 485 intgrer des donnes 302 execute() 307 QProcess 307 exitAction 53 expand() 239 Expanding 149 explicit 511 Expression 101 rgulire 250 syntaxe 101 extern 517

F
faceAtPosition() 211-212 fatalError() 362, 364 Fentre dapplication QDialog 4 QMainWindow 4 dessin 188 modale 66 non modale 64 principale configurer 49 crer 45 Fichier den-tte 18, 48 <QtAlgorithms> 276 <QtGui> 18 dfinition 487 Fichier objet 485 fill() (QPixmap) 136 fillRect() (QPainter) 116 Filtre cin en cout 299 dvnement 446 installer 169, 175 entres du systme de fichiers 238 find() 444 findChild<T>() (QObject) 40 findClicked() 19, 21

findNext() 17, 21, 65, 91 findPrevious() 17, 21, 65, 92 finished() 306, 422 firstChild() 369 flags() 247, 249 flipHorizontally() 420 FlipTransaction 422 flipVertically() 420 float 489 flush() 289 focusNextChild() 175, 177 Fonction globale 485, 516 prototype 486 virtuelle pure 494 fontMetrics() (QWidget) 174 FontRole 242, 256 foreach 75, 505 foreground() (QPalette) 115 FOREIGN KEY 318 forever 347 formats() 222-223 ARGB32 prmultipli 197 formula() 83 Formulaire crer laide de QWidget 108 dveloppement 26 disposition des widgets 28 insrer un HexSpinBox 118 matre/dtail 321 modifier la conception 32 nommer les widgets 35 Frameworks de migration Qt/Motif et Qt/MFC 444 Froglogic 444 fromAscii() 283 fromLatin1() 283 fromPage() (QPrinter) 205 ftpDone() 331 FtpGet 330 ftpDone() 331 ftpListInfo(), urlInfo 336

G
generateDocumentation() 451 generateId() 320 generateRandomTrip() 351 geometry() (QWidget) 72 Gestion de session tests et dbogage 467 X11 461 Gestionnaire dvnements endElement() 360 rimplmenter 170 startElement() 360 de disposition 9, 146 QGridLayout 9, 146 QHBoxLayout 9, 146 QVBoxLayout 9, 146 get() 330, 332-333, 337 oprations HTTP 339 getColor() (QColorDialog) 211 getDC() 445 getDirectory() 335 getFile() 330, 332 oprations HTTP 340 GetInterfaceSafetyOptions() 456 getOpenFileName() (QFileDialog) 59 getSaveFileName() (QFileDialog) 61 GL_SELECT 212 glClearColor() 209 glClearIndex() 209 glColor3d() 210 glIndex() 210 Glisser, types personnaliss 219 Glisser-dposer 213 activer 214 QTableWidget 219 GNU (General Public License) 478 Go to Cell 27 GoToCellDialog 27

Index

535

Graphique 2D 183 3D 183 OpenGL 207 group() 120 Groupe de couleurs Active 115 Disabled 115 Inactive 115 guidgen 457 GuiServer 471

H
Handle 444, 446 hasAcceptableInput() (QLineEdit) 32 hasFeature() 313 hasLocalData() 418 hasNext() 267 hasPendingEvents() 181 hasPrevious() 268 head() 341 headerData() 244-245, 248, 256 Hello Qt 4 HelpBrowser 377, 380 Hritage 493 HexSpinBox 106, 108, 118 hide() 128 hideEvent() 173, 175 hostName 392

I
IANA (Internet Assigned Numbers Authority) 215 icon() 120 IconEditor 109, 115, 120, 122 pixelRect() 115 IconEditorPlugin 120 iconForSymbol() 231 iconImage() 109-110, 113 Identifiant de la fentre 444

IgnoreAction 218 image() 110, 421 presse-papiers 224 imageCount() 433 imageSpace() 300-301 ImageWindow 420 Impression 199 QPainter 205 QTextDocument 202 Inactive (groupe de couleurs) 115 includeFile() 120 incomingConnection() 349-350 index() 253 de modle 249 indexOf() 150, 280 Infobulle 374 information() (QMessageBox) 59 initFrom() 130, 136, 198 initializeGL() 208-209 insert() 237, 281 avec itrateur 269 dans map 273 insertMulti() 274-275 insertRow() 233, 315 installEventFilter() 176-177 instance() (QPluginLoader) 438 Instruction DELETE 314 GET 337 if 417 INSERT 313-314 SELECT 312, 314 UPDATE 314 int 312, 489 Interface avec les API natives 444 Intermediate 107 QSpinBox 107 Internationalisation 385 passer dynamiquement dune langue lautre 396 traduire les applications 390, 402 Unicode 386

Internet Explorer accs laide 380 intgrer un widget 453 options de scurit du composant 455 Introspection 25 Invalid 107 QSpinBox 107 invokeMethod() 424 IObjectSafety 455 isActive() 312 isalpha() 387 isContainer() 121 isdigit() 387 isEmpty() 282 conteneur 269 isLetter() 387 isLetterOrNumber() 387 isLower() 387 isMark() 387 isNumber() 387 isPrint() 387 isPunct() 387 isSessionRestored() 467 isSpace() 387 isSymbol() 387 isUntitled 165, 167 isUpper() 387 item->text() 231 itemChanged() 81 ItemIsEditable 249 Itrateur de style Java 267 de style STL 269 mutable 268 pour liste chane 265

J
Java diffrences avec C# 483 diffrences avec C++ 489 machines virtuelles 470

536

Qt4 et C++ : Programmation dinterfaces GUI

join() 282 Jointure 185 JournalView 401 jumpToNextImage() 433

K
KDE XIV fermeture de session 461 projet XVIII key() 138, 169, 246, 275-276 map 275 keyPressEvent() 135, 170-171, 175 Plotter 139 keyReleaseEvent() 170 keys() 274, 427 itrateur STL 276 killTimer() (QObject) 175 Klarlvdalens Datakonsult 444

localeAwareCompare() 281, 395 LocaleChange 401-402 localeconv() 395 lock() 411 login() 332, 333 long 489 long long 489 lrelease 402 lupdate 402, 403

M
Mac OS X identifier la version 446 installer Qt 479 macEvent() 448 macEventFilter() 448 Macro de compilateur 446 Q_ OBJECT 47 Q_DECLARE_INTERFACE () 436 Q_DECLARE_METATYP E() 284 Q_EXPORT_PLUGIN2() 121, 428, 441 Q_INTERFACES() 120, 439 Q_OBJECT 17-18, 21, 110, 390, 439 Q_PROPERTY() 109 qPrintable() 283, 289 QT_TR_NOOP() 392 QT_TRANSLATE_NOOP() 393 SIGNAL() 7, 23 signals 17 SLOT() 7, 23 slots 17 MagicNumber 86, 292 mainSplitter 154 MainWindow 399 changeEvent() 401 closeEvent() 74

L
LanguageChange 402 LargeGap 203 Latin-1 386 LD_LIBRARY_PATH 481 leaders() 238 Lecture code XML 360, 365 donnes binaires 288 texte 294 left() 280, 283 LeftDockWidgetArea 158 length() 279, 282 LinkAction 218 list() 333 Liste chane 265 load() 394, 401 loadFile() 59, 64 loadPlugins() 437-438

drivation de QMainWindow 47 glisser-dposer 214 newFile() 82 sort() 69 switchLanguage() 401 updateStatusBar() 84 make 5, 479 Makefile 5, 22, 29 Manipulateur de flux 295 Masque AND 431 XOR 431 Matrice world 190 dessin 188 MDI (Multiple Document Interface) 75, 159 Mcanisme des ressources 50 fentre-viewport 189 parent-enfant 31 MediumGap 204 Mmoire dimage 470 Menu Bar() (QMainWindow) 53 crer 51 Edit 88 File 57 Options 92 Tools 92 Window 160 message() 423 metaObject(), dclaration avec Q_OBJECT 25 Mta-objets 25 mid() 280, 283 QString 66 Migration de Motif/Xt et MFC vers Qt 444 MIME (type) 215 mimeData() 224 MinGW C++ 478 minimumSizeHint() 124, 129, 149

Index

537

mirrored() 423 Mise en veille 462 mkdir() 239, 333 moc 22, 25 Mode de composition 188, 198 Modle arborescence 242 index 249 liste 242 personnalis 241 prdfini 236 tableau 242 modified() 80, 85 modifiers() (QKeyEvent) 135 Module QtCore 18 QtGui 18 QtNetwork 18 QtOpenGL 18, 207 QtSql 18 QtSvg 18 QtXml 18 mouse 169, 208 MouseButtonPress 170 mouseMoveEvent() 117, 136, 210 mousePressEvent() 117, 136, 170, 192, 210, 465 QListWidget 217 QWidget 116 mouseReleaseEvent() 136, 139 collage avec le bouton du milieu de la souris 225 move() (QWidget) 72 MoveAction 218 moveToThread() 419 Multithread 407 mutable 97, 101 Mutex 411 emploi 413 MVC (Modle-Vue-Contrleur) 228

N
name() 120 New 51 newFile() 51, 58, 161, 166 MainWindow 73, 82 newPage() 199, 200, 205 next() 267, 268 map 275 nextBlockSize() 344-345, 347, 350 nextSibling() 369 nmake 5 NoBrush 187 Node 250, 254 nodeFromIndex() 253-254 Nud 251 normalized() (QRect) 130, 133 notify() (QApplication) 178 number() (QString) 107 numCopies() (QPrinter) 205 numRowsAffected() 312 numTicks 140 numXTicks 125 numYTicks 125

OpenGL 183, 207 bibliothque 207 openRecentFile() 52, 64 Oprateur () 94 + 279 += 279 . (point) 493 : 486, 495 << 486 -> (flche) 497 bit bit | 508 bit bit|= 508 daffectation 514 sizeof() 502 unaire & 497 unaire * 499 operator 291 operator()(int) 95 operator==() 274 operator>>() 291 Ordre de tabulation 22, 28 OvenTimer 191

P O
objectName 27 Objet COM 448 compare 68, 94 QSettings 72 SpreadsheetCompare 68 offset 173, 174 offsetOf() 250 okToContinue() 58, 165, 167 on_browseButton_clicked() 304 on_convertButton_clicked() 304 on_lineEdit_textChanged() 32 Open 52 Open source 478 open() 59, 162, 166 QTemporaryFile 307 Page daccueil 75 paginate() 202-203 paint 113 paintEngine() 445 Painter, transformations 188 paintEvent() anticrnelage 197 copie de pixmap 136 dfinition du viewport 194 IconEditor 113 implmentation de event() 170 OvenTimer 192 rimplmentation TicTacToe 465 widget Ticker 174 paintGL() 208-209

538

Qt4 et C++ : Programmation dinterfaces GUI

palette() (QWidget) 115 Paramtre charset 224 parent 244, 247 preferredType 223 section 245 stocker 70 parent 244, 247 parent() (QModelIndex) 242 parse() 365 parseEntry() 368 Partage implicite 270 principe 272 paste() 90 PATH 5, 380 PdfFormat 199 peek() 294, 429 penColor() 109, 111, 113 pendingDatagramSize() 357 Pile 498 Pile de zoom curZoom 127 zoomStack 127 Pilote QODBC 310 QPSQL 310 QSQLITE 310 QSQLITE2 310 QTDS 310 Pinceau 185 de fond 188 origine 188 pixelRect() 116 IconEditor 115 pixmap() 224, 440 Plastique 12 PlotSettings 123, 125, 127, 139 adjust() 134 Plotter 122, 124 keyPressEvent() 139 Plug-in 119 cration 425 dapplication 435, 439 dveloppement de Qt 426

plugins 122 Pointeur rticule 132 C++ 497 conteneur 252 dattente standard 86 de fichier 288 de nud 256 glisser-dposer 218 nul 17 QObject 64 QPointer 498 toupie 7 hexadcimal 106 vers la barre dtat 56 void 83 Police 185 Polymorphisme 493 pop() 266 populateListWidget() 437-438 pos 100 post() 339, 341 postEvent() 423 Preferred 149 preferredType 223 prepare() 312 prepend() 62 Prprocesseur 488, 520 Presse-papiers de slection 224 grer 224 previous() 268 map 275 printBox() 206 printFlowerGuide() 202 printHtml() 201 printImage() 200 printPages() 202, 205 PrintWindow 200 pro (fichier) 122 processEvents() (QApplication) 179 processNextDirectory() 335, 337 processPendingDatagrams() 356

Programmation embarque 469 ProjectListWidget 216, 218 Promotion 118 propertyChanged() 455 Protocole de transport TCP 329 UDP 329 QCOP 471 Prototype de fonction 486 push() 266 put() 330, 333

Q
Q_ OBJECT 47 Q_CLASSINFO() 459 Q_DECLARE_INTERFACE() 436 Q_ENUMS() 449 Q_EXPORT_PLUGIN2() 121, 428 Q_INTERFACES() 120, 439 Q_OBJECT 17-18, 21, 110, 390, 439 Q_OS_UNIX 446 Q_OS_WIN 446 Q_PROPERTY() 109 Q_WS_ X11 446 Q_WS_MAC 446 Q_WS_QWS 446 Q_WS_WIN 446 qAbs() 278 QAbstractItemDelegate 258 QAbstractItemModel 244, 252 reset() 246, 250 QAbstractItemView 52, 156, 218 AnyKeyPressed 231 ContiguousSelection 81, 89 NoEditTriggers 233 selectAll() 91 setEditTriggers() 231 QAbstractListModel 244

Index

539

QAbstractScrollArea 42, 82, 156 QAbstractSocket 342 waitForDisconnected() 424 QAbstractTableModel 244 QAction 55, 162, 165, 171-172, 374, 399, 400 conversion dun pointeur QObject 64 setShortcutContext() 172 QActionGroup 52, 162-163, 400 triggered() 400 qApp 53 QApplication 4, 394, 418 arguments() 168 clipboard() 88, 224 commitData() 462 exec() 178 filtre dvnement 177 GuiServer 471 isSessionRestored() 467 notify() 178 processEvents() 179 quit() 61 setLayoutDirection() 394 setOverrideCursor() 132 translate() 392 QAssistantClient 379 showPage() 380 QAXAGG_IUNKNOWN 456 QAxAggregated 456 QAxBase 449 dynamicCall() 452 generateDocumentation() 451 queryInterface() 452 QAxBindable 453 QAXCLASS() 459 QAxContainer 448 schma dhritage 450 QAXFACTORY_BEGIN() 459 QAXFACTORY_DEFAULT() 454, 457 QAXFACTORY_END() 459

QAXFACTORY_EXPORT() 459 QAxObject 449 QAxServer 448, 453 qaxserver.def 457 qaxserver.rc 457 QAXTYPE() 459 QAxWidget 449 setControl() 451 qBinaryFind() 264, 277 QBitArray 431 QBrush 116, 186 QBuffer 86, 288 fichiers tlcharger 339 QByteArray 218-219, 222, 266, 278, 288, 346, 352 QCDEStyle 130 QChar 386 isDigit() 387 isLetter() 387 isLetterOrNumber() 387 isLower() 387 isMark() 387 isNumber() 387 isPrint() 387 isPunct() 387 isSpace() 387 isSymbol() 387 isUpper() 387 QCheckBox 40, 147, 178 QClipboard dataChanged() 225 Selection 224 setText() 88 QCloseEvent, accept() 411 QColor 111, 116 QColorDialog 208 getColor() 211 QComboBox 158 addItem() 241 currentIndex() 68 QComboBoxes 176 qCompress() 294 qconfig 472

QCOP 471 QCopChannel 471 qCopy() 277 QCoreApplication 330, 418 addLibraryPath() 435 arguments() 330 postEvent() 423 removePostedEvent() 423 QDataStream 85-86, 288, 290, 351, 431, 490 numro de version 292 oprations TCP 342 Qt_4_1 86 skipRawData() 431 QDate (toString) 395 QDateEdit 395 QDateTime 266 currentDateTime() 193 toString 395 QDateTimeEdit 395 qDeleteAll() 252, 278 QDesignerCustomWidgetCollectionInterface 122 QDesignerCustomWidgetInterface 119 IconEditorPlugin 120 QDialog 4, 16, 31, 178 driver 16 exec() 66 Rejected 66 QDir 300, 301 entryInfoList() 301 entyList() 301 exists() 301 imageSpace() 301 mkdir() 301 rename() 301 rmdir() 301 QDirectPainter 471 QDirModel 236, 238, 240 mkdir() 239 QDockWidget 157 setFeatures() 157 QDockWindow 157

540

Qt4 et C++ : Programmation dinterfaces GUI

QDomDocument documentElement() 367 save() 370 setContent() 367 QDomElement 369 QDomNode 368 firstChild() 369 nextSibling() 369 tagName() 368 toElement 368 QDoubleValidator 31 QDrag 217 start() 218 QDragEnterEvent 218 QEvent 170 MouseButtonPress 170 type() 170 QFile 85-86, 288, 301, 336 exists() 301 remove() 301 QFileDialog DontConfirmOverwrite 61 getOpenFileName() 59 getSaveFileName() 61 QFileInfo 301 qFill() 277 qFind() 276 QFont 186, 288, 292 QFontMetrics 174 size() 174 QFrame 41, 121 QFtp 329 cd() 333 close() 333 commandFinished() 333 commandStarted() 333 ConnectToHost 333 done() 332 ftpListInfo() 335 get() 330, 333 list() 333 listInfo() 335 login() 333 mkdir() 333

put() 330, 333 rawCommand() 333 readyRead() 339 remove() 333 rename() 333 rmdir() 333 stateChanged() 333 qglClearColor() 209 qglColor() 210 QGLWidget dessin avec OpenGL 207 qglClearColor() 209 setFormat() 209 QGridLayout 9, 19, 143, 146147 QGroupBox 178 QHash 274 QHash() 275 QHBoxLayout 9, 19, 143, 146147 QHeaderView 82 QHostInfo fromName() 355 lookupHost() 355 QHttp 329 done() 340, 342 get() 339, 341 getFile() 340 head() 341 httpDone() 340 post() 339, 341 QTcpSocket 341 QtSslSocket 341 read() 342 readAll() 342 readyRead() 342 request() 341 requestFinished() 342 requestStarted() 342 setHost() 341 setUser() 341 QIcon 231

QImage 111, 288, 427 affichage de haute qualit 197 CompositionMode_SourceOver 198 CompositionMode_Xor 199 format ARGB32 prmultipli 197 imprimer 199 mirrored() 423 rect() 117 setPixel() 117 QImageIOHandler 428 QImageIOPlugin 427-428 QImageReader 427-428 QInputDialog 42 QIntValidator 31 QIODevice 287, 293 crire dedans 300 oprations HTTP 341 peek() 294, 429 QBuffer 287 QFile 287 QProcess 287 QTcpSocket 287 QTemporaryFile 287 QUdpSocket 287 ReadOnly 87 seek() 294 unget-Char() 294 WriteOnly 87 QItemDelegate 258 drawDisplay() 259 drawFocus() 259 QItemSelectionModel 324 QKeyEvent 171, 177 modifiers() 135 QLabel 42 bote de dialogue Find File 147 fentre dapplication 4 Hello Qt 4 implmentation 108 indicateurs dtat 56

Index

541

setText() 424 statut de la dernire opration 343 QLatin1String() 393 QLayout 146 SetFixedSize 38 setMargin() 147 setSpacing() 147 QLibrary 425 QLineEdit activer avec barre despace 175 bote de dialogue Find File 147 entres de donnes 42 hasAcceptableInput() 32 stocker une chane de filtre 240 text() 66 QLineEdits 355 QList 274 QListView 229, 237, 314 QListWidget 39, 151, 228, 231, 435 glisser-dposer 215 QListWidgetItem 231, 439 QLocale 395 System() 394 qlonglong 490 QMacStyle 130 QMainWindow 4, 78, 144, 157 driver 46 menuBar() 53 statusBar() 56 qmake 5, 21-22, 29, 479 QMatrix 191 qMax() 278 QMenu 53 addAction() 54 addMenu() 54 QMenuBar 4 QMessageBox about() 70 critical() 59

Default 58 Escape 58 information() 59 question() 59 warning() 58, 70 QMetaObject 25 invokeMethod() 424 QMimeData 213, 217 conversion en TableMimeData 224 data() 219 driver 224 glisser-dposer 219 setData() 219 text() 219 urls() 215 qMin() 194, 278 QModelIndex 237, 240, 245, 253 column() 242 parent() 242 row() 242 QMotifStyle 130 QMouseEvent 170 buttons() 117 QMovie 427 qmPath 398 QMultiMap 274 QMutex 407, 409, 411, 416, 423 QMutexLocker 412, 423 QObject 24, 266, 424, 439 connect() 9, 419, 423 deleteLater() 424 disconnect() 423 event() 170 filtre dvnement 177 findChild() 40 IconEditorPlugin 120 killTimer() 175 mcanisme parent-enfant 31 moveToThread() 419 QProcess 424 QTimer 424

sender() 64 setProperty() 451 startTimer() 174 tr() 18, 390 qobject_cast() 64, 218, 224, 510 QPaintDevice 445 QPainter 114, 136, 183 boundingRect() 204 dessiner 184 sur QPrinter 200 draw...() 184 drawLine() 114 fillRect() 116 imprimer 205 programmation embarque 471 rotate() 191 scale() 191 setCompositionMode() 198 shear() 191 systme de coordonnes 188 translate() 191 QPainterPath 186 QPalette 115 ColorGroup 115 foreground() 115 QPen 186 QPixmap 124, 288, 439 fill() 136 QPlastiqueStyle 130 QPluginLoader 438 QPoint 114 QPointer 498 QPointF 138 qPrintable() 289 QPrintDialog 200 choix de limprimante 199 options dimpression 205 setEnabledOptions() 205 QPrinter 200 fromPage() et toPage() 205 newPage() 199 numCopies() 205 setPrintProgram() 199

542

Qt4 et C++ : Programmation dinterfaces GUI

QProcess 86, 288, 303, 424 execute() 307 start() 305 waitForFinished() 424 QProgressBar 42, 343 QProgressDialog 42, 179-180 QPushButton 40 clicked() 7 effets 3D 187 implmentation 108 rpondre aux actions utilisateur 6 setText() 39 show() 11 signal clicked() 170 QRadioButton 40 QReadLocker 413, 423 QReadWriteLock 411, 413, 423 QRect 115-117, 132 contains() 117 normalized() 130, 133 QRegExp 104, 266 PatternSyntax 241 QRegExpValidator 31, 67, 107 qRegisterMetaTypeStreamOperators<T>() 285 qRgb() 111 qRgba() 111 QRubberBand 136 QScrollArea 135, 144, 155-156 viewport() 155 widgets constitutifs 155 QScrollBar 82 QSemaphore 407, 411, 413, 423 QSessionManager 463 QSetting 291 QSettings 71-72, 234 QShortcut 172 setContext() 172 QSizePolicy 149 Expanding 126, 149 Fixed 149 Ignored 149 Maximum 149

Minimum 112, 149 MinimumExpanding 149 Preferred 126, 149 QSlider 7, 8 qSort() 264, 277 QSortFilterProxyModel 236, 240 QSpinBox 7, 8, 106, 118, 158 Acceptable 107 Intermediate 107 Invalid 107 textFromValue() 107 valueFromText() 107 QSpinBoxes 176 QSplashScreen 75 QSplitter 78, 144, 152, 159 setSizes() 154 sizes() 269 QSqlDatabase 310-311, 313 QSqlDriver 313 QSqlQuery 310-312, 314 first() 312 last() 312 previous() 312 seek() 312 QSqlQueryModel 236 QSqlRelationalDelegate 323 QSqlRelationalTableModel 236, 310 formulaires 322 QSqlTableModel 236, 310, 314315, 319 QSqlRelationalTableModel 317 qStableSort() 94-95, 278 QStackedLayout 150 QStackedWidget 39, 150-151 QStackLayout 143 QStatusBar 4 addWidget() 57 QString append() 279 arg() 62, 392

chane de caractres 507 Unicode 387 comparaison 277 concatnation 279 emploi comme conteneur 278 localeAwareCompare() 395 menu Edit 88 mid() 66 number() 107 partage implicite 272 replace() 221 split() 90, 282 sprintf() 279 toInt() 66, 107 toUpper() 107 type de valeur 266 QStringList 94, 168, 201 arguments de ligne de commande 330 concatnation 282 fichiers tlcharger 338 recentFiles 63 takeFirst() 297 QStringListModel 237, 240 QStyle 130 PE_FrameFocusRect 130 QStyleOptionFocusRect 130 QStylePainter 130 qSwap() 250, 278 Qt AlignHCenter 57 BackgroundColorRole 242, 256 CaseInsensitive 17 CaseSensitive 17 classe dlments 82 DiagCrossPattern 187 DisplayRole 231, 242 EditRole 231, 242 FontRole 242, 256 IconRole 231 ItemIsEditable 249

Index

543

LeftDockWidgetArea 158 mcanisme des ressources 50 NoBrush 187 SolidPattern 187 StatusTipRole 242 StrongFocus 126 systme de mta-objets 25 TextAlignmentRole 242, 256 TextColorRole 242, 256 ToolTipRole 242 UserRole 231 WA_DeleteOnClose 166 WA_GroupLeader 378 WA_StaticContents 110 WaitCursor 132 WhatsThisRole 242 Qt Designer 26 crer des widgets personnaliss 108 un formulaire 151 diteur de connexions 37 intgrer des widgets personnaliss 118 Qt Linguist excution 403 prsentation 403 Qt Quaterly 12 Qt/Embedded 469 Qt/Mac (installer) 479 Qt/Windows (installer) 478 Qt/X11(installer) 479 qt_metacall(), dclaration avec Q_OBJECT 25 QT_TR_NOOP() 392 QT_TRANSLATE_NOOP() 393 QTableView 93, 229, 314, 320 QTableWidget 78, 228, 232, 401 ajouter le glisser-dposer 219 attributs QTableWidgetItem 79

driver 78 implmentation 108 oprations TCP 343 QHeaderView 82 QScrollBar 82 selectColumn() 91 selectedRanges() 89 selectRow() 91 setCurrentCell() 66 setItem() 84, 232 setItemPrototype() 98 sous classe Spreadsheet 49 Track Editor 257 widgets constitutifs 82 QTableWidgetItem 79, 82, 90, 222, 232, 235 data() 96, 99 driver 96 text() 96 QTableWidgetSelectionRange 222 QTabWidget 39, 41 QtCore 18, 299 QTcpServer 329, 342 QTcpSocket 86, 288, 329, 341, 342 canReadLine() 352 connected() 345, 346 connectionClosedByServer() 349 disconnected() 349 error() 349 listen() 352 readClient() 351 readLine() 352 readyRead() 347 seek() 346 updateTableWidget() 347 write() 346 QTDIR 122 QTemporaryFile 288, 307 QTextBrowser 42, 379 moteur daide 376 QTextCodec 388

codecForName() 389 setCodecForCStrings() 389 setCodecForTr() 389, 403 QTextDocument 202 QTextEdit 42, 78, 149, 156, 307 glisser-dposer 214 QTextStream 86, 288, 294, 299, 388 AlignAccountingStyle 296 AlignCenter 296 AlignLeft 296 AlignRight 296 FixedNotation 296 ForcePoint 296 ForceSign 296 internationalisation 388 oprations TCP 342 XML 370 readAll() 295 readLine() 295 ScientificNotation 296 setRealNumberNotation() 296 SmartNotation 296 UppercaseBase 296 UppercaseDigits 296 QtGui 18 QThread 407 currentThread() 417 exec() 424 run() 408 terminate() 409 wait() 411 QThreadStorage 423 QTime (toString) 395 QTimeEdit 259, 323, 326 Track Editor 260 QTimer 175, 192, 424 sendDatagram() 354 QtNetwork 18 QToolBar 4 QToolBox 41 QToolButton 40, 127, 158

544

Qt4 et C++ : Programmation dinterfaces GUI

QtOpenGL 18, 207 Qtopia Core 469 formats de police 473 prise en charge de VNC 472 Qtopia PDA 470 Qtopia Phone 470 Qtopia Platform 470 QTranslator 398, 400, 404 load() 394 QTreeView 229, 239, 256 QTreeWidget 39, 147, 149, 158, 228, 233-234, 367 fichier dindex 361 oprations XML 363, 365 QTreeWidgetItem 369, 460 oprations XML 363 QtSql 18 QtSslSocket 341 QtSvg 18 qtTranslator 398, 401 QtXml 18 Quest-ce que cest ? 375 QUdpSocket 86, 288, 329, 353354 writeDatagram() 355 queryInterface() 452, 456 querySubObject() 452 question() (QMessageBox) 59 QUiLaoder 39 quint32 291 quit() (QApplication) 61 quitOnLastWindowClosed 61 qUncompress() 294 QUrl 334 QUrlInfo 336 QVariant 63, 71, 82, 99, 223, 266, 278, 283, 288, 291, 312 double 283 int 283 QBrush 283 QColor 283, 284 QCursor 283 QDateTime 283 QFont 283, 284

QIcon 284 QImage 284 QKeySequence 283 QPalette 283 QPen 283 QPixmap 283, 284 QPoint 283 QRect 283 QRegion 283 QSize 283 QString 283 QVBoxLayout 9, 19, 143, 146, 147 QVector 124, 138 qvfb 471 QWaitCondition 407, 411, 416, 423 wait() 417 QWhatThis, createAction() 376 QWidget 7, 78 changeEvent() 401 close() 19, 61 closeEvent() 47, 61 contextMenuEvent() 55, 67 driver 108 event() 171 find() 444 fontMetrics() 174 geometry() 72 mousePressEvent() 116 move() 72 palette() 115 QGLWidget 207 repaint() 113 resize() 72 scroll() 175 setCursor() 132 setGeometry() 72 setLayout() 9 setMouseTracking() 117 setStyle() 130 setTabOrder() 22 setToolTip() 374 setWindowIcon() 49

setWindowTitle() 8 sizeHint() 20, 38, 57 style() 130 update() 112 updateGeometry() 112 windowModified 62 winID() 444 QWindowsStyle 130 QWindowsXPStyle 130 QWorkspace 78, 144, 159, 167 QWriteLocker 413, 423 QWS_DEPTH 472 QWS_KEYBOARD 471 QWS_MOUSE_PROTO 471 QWS_SIZE 472 qwsEvent() 448 qwsEventFilter() 448 QWSServer 473 qwsServer (variable globale) 473 QXmlContentHandler 362, 365 endElement() 360 startElement() 360 QXmlDeclHandler 360 QXmlDefaultHandler 361-362 errorString() 363 QXmlDTDHandler 360 QXmlEntityResolver 360 QXmlErrorHandler 360, 362, 365 QXmlInputSource 365 QXmlLexicalHandler 360 QXmlSimpleReader 360, 365

R
range.rightColumn() 68 Rational PurifyPlus 490 rawCommand() 333 Ractivit et traitement intensif 178 read() 433 QHttp 342

Index

545

readAll() 295 QHttp 342 QIODevice 293 readBitmap() 434 readDatagram() 357 readFile() 87 Spreadsheet 60 readHeaderIfNecessary() 430, 433 readLine() 295, 297, 352 ReadOnly (QIODevice) 87 readRawBytes() 290, 291 readSettings() 49, 70-71, 155, 234, 235 readyRead() 339, 342, 344 recalculate() 92, 93 recentFileActions 52 recentFiles 62, 63 record() 327 rect() (QImage) 117 Rfrence 500 refreshPixmap() 128, 131, 135 refreshTrackViewHeader() 324, 327 regExpChanged() 256 RegExpModel 250, 256 RegExpParser 250, 256 RegExpWindow 250 Registre systme enregistrement dun serveur ActiveX 460 du serveur 457 stockage des paramtres 71 regsvr32 457 reinterpret_cast() 511 reject() (QDialog) 31, 36 release() 5, 456 releaseDC() 445 remove() 281, 333 avec itrateur 268 removeAll() 62 removePostedEvents() 423 removeRows() 237, 316

rename() 333 repaint() (QWidget) 113 Rpertoire (parcours) 300 replace() 281 QString 221 Reprsentation binaire des types 86 requestFinished() 342 requestPropertyChange() 455 requestStarted() 342 Rseau envoi et rception de datagrammes UDP 353 gestion 329 programmer les application client/ serveur TCP 342 les clients FTP 330 les clients HTTP 339 reserve() 275 reset() (QAbstractItemModel) 246, 250 resize() (QWidget) 72, 127, 131 resizeEvent() 131, 145, 146 resizeGL() 208, 209 resizeImage() 420 ResizeTransaction 422 Ressources (intgration) 302 restore(), matrice de transformation 196 restoreState() (QMainWindow) 159 retranslateUi() 397, 398 retrieveData() 222, 223 right() 280, 283 rightSplitter 154 rmdir() 333 Rle 231, 241 DisplayRole 97 EditRole 97 rollback() 313 rotate() 191, 196 row() (QModelIndex) 242 rowCount() 82, 244

rowSpan 148 rubberBandIsShown 132 rubberBandRect 132 run() 422 excution multithread 408, 409, 412

S
Sauvegarde 85 save() 47, 52 Editor 162 fichier 60 matrice de transformation 196 module de rendu 188 oprations XML 370 saveAs() 47, 52, 60-61, 167 saveFile() 60, 165, 167 saveState() 463 QMainWindow 159 SAX (Simple API for XML) 359 SaxHandler 362 scale() 191 Script configure -help 473 Qtopia 470 scroll() 139 QWidget 175 scrollTo() 239 SDI (Single Document Interface) 75 section 245 Scuriser pour les scripts 456 seek() 294 SELECT 312 Select All (action) 52 selectColumn() 91 selectedId() 230 selectedRange() 89 Selection 224 selectionAsString() 220

546

Qt4 et C++ : Programmation dinterfaces GUI

selectRow() 91 Smaphore emploi 414 freeSpace 414 usedSpace 414 sendDatagram() 354 sender() (QObject) 64 sendRequest() 346, 352 Sparateur 152 sessionFileName() 466 sessionId() 467 sessionKey() 467 setAcceptDrops() 221 setAllowedAreas() 158 setAutoDetectUnicode() 388 setAutoFillBackground() 126 setAutoRecalculate() 93 setBackgroundRole() 126, 136 setBit() 434 setBrush() 186 setByteOrder() 289 setChecked() 163 setClipRect() 138 setCodec() 295, 388 setCodecForCStrings() 389 setCodecForTr() 389 setColumnRange() 38, 68 SortDialog 69 setCompositionMode() 198 setContext() (QShortcut) 172, 367 setCurrentCell() 80 QTableWidget 66 setCurrentFile() 58, 60, 62, 165, 167 setCurrentIndex() 150-151 setCurrentRow() 151 setCursor() 132 setCurveData() 129 setData() champ de base de donnes 315 index de modle 316 QMimeData 219

rimplmentation 247 setDefault() 19 setDirty() 93, 99 setDuration() 193 setEditorData() 258, 260 setEditTriggers() 233 QAbstractItemView 231 setEnabledOptions() (QPrintDialog) 205 setFeatures() (QDockWidget) 157 SetFixedSize (QLayout) 38 setFocus() 165 setFocusPolicy() 126 setFont() 186 setFormat() (QGLWidget) 209 setFormula() 84, 98, 298 setGeometry() (QWidget) 72 setHorizontalHeaderLabels() 232 setHost() 341 setIconImage() 110, 112 setImage() 421 presse-papiers 224 setImagePixel() 116, 117 SetInterfaceSafetyOptions() 456 setItem() 97 QTableWidget 84, 232 setItemPrototype() 81 QTableWidget 98 setLayout() (QWidget) 9 setLayoutDirection() 395 setlocale() 395 setMargin() (QLayout) 147 setMessage() 408, 410 setMimeData() 224 setModal() 66 setModelData() 258 setMouseTracking() (QWidget) 117 setNum() 280 setOutputFormat() 199 setOverrideCursor() 132 setPen() 115, 186 setPenColor() 112

setPixel() (QImage) 117 setPixmap() presse-papiers 224 QDrag 218 setPlotSettings() 127 setPrintProgram() (QPrinter) 199 setRadius() 455 setRelation() 323, 325 setRenderHint() 186 setRootNode() 252 setSelectionMode() 221 setShortcutContext() (QAction) 172 setShowGrid() 80 setSingleShot() 192 setSizePolicy() 110, 112, 126 setSizes() (QSplitter) 154 setSourceModel() 241 setSpacing() (QLayout) 147 setSpeed() 455 setStatusTip() 374 setStretchFactor() 154 setStyle() (QWidget ) 130 setTabOrder() (QWidget) 22 setText() code XML 369 presse-papiers 224 QClipboard 88 QLabel 424 QPushButton 39 widget Ticker 174 settings 235 setToolTip() 374 setupUi() 29, 31, 304 setValue() 9, 268, 276 setVisible() 36 setWidget() 155 setWidgetResizable() 156 setWindowIcon() (QWidget) 49 setWindowModified() 166 setWindowTitle() 62 QWidget 8 setZoomFactor() 113 shear() 191

Index

547

short 489 Show Grid 52, 71, 75 show() 65, 128, 165 ShowControls 451 showEvent() 173-174 showPage() 379 Signal 7 clicked() 170, 237, 304305, 348 closeEditor() 260 connected() 344-346 connexion aux slots 23, 304 contentsChanged() 166 copyAvailable() 162 currentCellChanged() 57 currentRowChanged() 151 description 22 disconnected() 344, 349, 350 done() 331, 340, 342 editingFinished() 259 error() 344, 349 findNext() 65 findPrevious() 65 finished() 422 itemChanged() 81 listInfo() 335 modified() 80, 85 readyRead() 339, 342, 344, 347, 351 requestFinished() 342 requestStarted() 342 stateChanged() 333 timeout() 192 toggled 36 transactionStarted() 423 triggered() 51, 400 valueChanged() 9 versus vnement 170 windowActivated() 161 signals 7, 17, 23 simplified() 282-283 size() (QFontMetrics) 174 sizeConstraint 38

sizeHint (proprit) 35, 124, 129, 150, 167, 465 QWidget 20, 38, 57, 111 sizeof() 502 sizePolicy 112 sizes() (QSplitter) 269 skipRawData() 431 Slot 7 accept() 36 addRow() 233 allTransactionsDone() 420 close() 19 closeAllWindows() 74 commitAndCloseEditor() 259 connectionClosedByServer() 349 connectToHost 345 connectToServer() 345 connexion aux signaux 23, 304 convertTo1Bit() 420 convertTo3Bit() 420 convertTo8Bit() 420 copy() 88 currentCdChanged() 324 cut() 88, 163 del() 90, 237 description 22 documentWasModified() 166 edit() 306 enableFindButton() 19, 21 error() 349 findClicked() 19, 21 findNext() 91 findPrevious() 92 flipHorizontally() 420 flipVertically() 420 ftpDone() 331 ftpListInfo() 335 help() 379 httpDone() 340 insert() 237

newFile() 51, 58, 73, 161 on_browseButton_clicked() 304, 305 on_convertButton_clicked() 304 on_lineEdit_textChanged() 32 on_objectName_signalName () 304 open() 59 openRecentFile() 64 paste() 90 processPendingDatagrams() 356 recalculate() 92 refreshTrackViewHeader() 327 regExpChanged() 256 reject() 36 resizeImage() 420 save() 60, 162 saveAs() 61 selectAll() 52 sendRequest() 346 setAutoRecalculate() 93 setColumnRange() 38 setFocus() 165 setValue() 9 setVisible() 36 show() 165 somethingChanged() 81, 85 Spreadsheet 65 spreadsheetModified() 57 stopSearch() 348 switchLanguage() 400 updateMenus() 163 updateOutputTextEdit() 306 updateStatusBar() 57 updateTableWidget() 347 updateWindowTitle() 378 zoomIn() 127 zoomOut() 127 Slot() 7, 23 mot cl 17

548

Qt4 et C++ : Programmation dinterfaces GUI

SmallGap 204 Socket QTcpSocket 341 QtSslSocket 341 SolidPattern 187 somethingChanged() 81, 85, 9394 sort() 68 MainWindow 69 Spreadsheet 81 SortDialog 68, 69 setColumnRange() 69 Sorties 287 source() (QDragEnterEvent) 218 split() 282 QString 90 Spreadsheet 49, 65 clear() 58 readFile() 60 setFormula() 98 sort() 68, 81 SpreadsheetCompare 68, 81, 94 arbre dhritage 79 spreadsheetModified() 57 sprintf() 279 Square 95 squeeze() 275 start() QDrag 218 QProcess 305 slot 455 startDrag() 217, 220 startElement() 360, 364 startOrStopThreadA() 410 startOrStopThreadB() 411 startPos 217 startsWith() 281 startTimer() 174 stateChanged() 333 static_cast 510 static_cast<T>() 64, 511 status() 290

statusBar() (QMainWindow) 56 StatusTipRole 242 std 125 STL (Standard Template Library) 63 fichiers den-tte 525 Stockage de donnes en tant qulments 82 des caractres ASCII 490 format 288 local de thread 417 XML 359 stop() 408 excution multithread 412 stopped 408, 411 stopSearch() 348 strippedName() 62, 165 strtod() 486 Style de widget 12, 130 Plastique 12 spcifique la plate-forme 12 style() 130 submitAll() 315 sum() 104 supportsSelection() 225 Surcharge doprateur 512 switchLanguage() 400, 401 Symbole de prprocesseur 446 de systme de fentre 446 Synchronisation (des threads) 411 System() 394 Systmes embarqus 469

T
Table de hachage 274 Tableau C++ 502 doctets 278

TableMimeData 224 tagName() 368 takeFirst() (QStringList) 297 Tas 498 tcpSocket 344 TeamLeadersDialog 238 terminate() 409 Tetrahedron 208 text() 27, 83, 137 presse-papiers 224 QLineEdit 66 QTableWidgetItem 96 TextAlignmentRole 99, 242, 245, 256 TextArtDialog 437-438 TextArtInterface 437-438 TextColorRole 242, 256 Texte criture 294 lecture 294 textFromValue() 107 Thread communication 418 consommateur 416 cration 408 producteur 416 setMessage() 408 stop() 408 synchronisation 411 utilisation des classes Qt 423 ThreadDialog 410 Ticker 172 tidyFile() 298 tile() 164 timeout() 175, 192 timer 169, 175 timerEvent() 173, 175, 181, 452 title 34 toAscii() 283 toBack() 268 toCsv() 221 toDouble() 100, 280 toElement() 368 toggled() 36

Index

549

toHtml() 221 toInt() 280 QString 66, 107 toLatin1() 283, 387 toLongLong() 280 toLower() 281, 283 toolTip() 121 ToolTipRole 242 top() 266 toPage() (QPrinter) 205 toString() 99, 284, 395 toUpper() 281, 283 QString 107 tr() dclaration avec Q_OBJECT 25 internationalisation 392 traduction de littraux 18 Trac de dessin 187 TrackDelegate 258, 323, 326 transaction() 313, 422 ajout la file dattente 421 ConvertDepthTransaction 422 excutions simultanes 314 FlipTransaction 422 ResizeTransaction 422 SQL 313 transactionStarted() 422, 423 TransactionThread 420 run() 423 translate() 191, 392-393 triggered() 51 trimmed() 282, 283 TripPlanner 344 TripServer 349 incomingConnection() 349 truncate() 272 type() 284 conversions 509 MIME 215 personnalis de glisser 219 primitifs 489

QEvent 170 reprsentation binaire 86 valeur 514 TypeDef 509

V
Valeur 71 bitsPerPixel 431 compression 431 Valgrind 490 Validateur QDoubleValidator 31 QIntValidator 31 QRegExpValidator 31 value() champ de base de donnes 312, 314 courbe 138 itrateur STL 276 map 274, 275 valeur dune cellule 100 dune map 275 valueChanged() 9 valueFromText() 107 values() 274 Variable globale 516 mutable 97 sur la pile 67 Variable denvironnement LD_LIBRARY_PATH 481 PATH 5, 380 QTDIR 122 QWS_KEYBOARD 471 QWS_MOUSE_PROTO 471 Variant 278 Vecteur dlments 264 de coordonnes 122 de QChar 279 distances 248 initialisation 277 parcourir 265 Version de systme dexploitation 446

U
UDP 329 datagrammes 353 coute du port 355 envoi et rception de datagrammes 353 uic 29, 32, 120 unget-Char() 294 unicode() 386-387 Unit de compilation 484 unlock() 411 unsigned 489 update() forcer un vnement paint 113 planifier un vnement paint 136 QRect 117 QWidget 112 viewport 93 updateGeometry() 113, 174 QWidget 112 updateGL() 211 updateMenus() 163 updateOutputTextEdit() 306 updateRecentFileActions() 62, 74-75 updateRubberBand() 136 updateRubberBandRegion() 132 updateStatusBar() 57 MainWindow 84 updateTableWidget() 348 updateWindowTitle() 378 UserRole 231 using namespace 486 uuidgen 457

550

Qt4 et C++ : Programmation dinterfaces GUI

viewport() 155 dessin 188 OpenGL 209 systme de coordonnes 189 transformation du painter 194 update() 93 widget 82 virtual 494 VNC (Virtual Network Computing) 472 void* 511 Vue QListView 229 QTableView 229 QTreeView 229

W
WA_DeleteOnClose 74, 162, 166 WA_GroupLeader 378 WA_StaticContents 117 wait() 411, 417 waitForDisconnected() 424 waitForFinished() 424 warning() (QMessageBox) 58, 70 WeatherBalloon 354 whatsThis() 121 WhatsThisRole 242 wheel 135 wheelEvent() 135 while 179 Widget ancrable 157

bouton 40 QCheckBox 40 QPushButton 40 QRadioButton 40 QToolButton 40 central 78 classes 40 compagnon 18 conteneur 41 multipage 41 QFrame 41 QTabWidget 41 QToolBox 41 daffichage dlments 41 dentre 43 dfinition 4 disposer 7, 28, 144 HelpBrowser 377 OvenTimer 191, 194 palette de couleurs 115 personnalis 106 crer 105 et Qt Designer 108 intgrer avec le Qt Designer 118 Plotter 122 style 12 viewport 82 windowActivated() 161 windowModified 57, 62 Windows ActiveX 448 identifier la version 446 installer Qt 478 Media Player 449 windowTitle 27 winEvent() 448 winEventFilter() 448

winId() 444 write() 429 QIODevice 293 writeDatagram() 355 writeFile() 85 WriteOnly (QIODevice) 87 writeRawBytes() 290 writeSettings() 70, 71, 154

X
X Render 197 X11 gestion de session 461 installer Qt 479 x11Event() 448 x11EventFilter() 446 Xcode Tools 479 XML 359 criture 370 lecture avec DOM 365 avec SAX 360 xsm 467

Z
Zone daction 188 droulante 155 zoomFactor() 109, 113 zoomIn() 127, 134 zoomOut() 127 zoomStack 127

Chez le mme diteur

ISBN : 2-7440-2086-9 Parution : 07/06 Prix : 52

ISBN : 2-7440-2127-X Parution : 12/06 Prix : 49,90

Tous nos ouvrages sont disponibles sur www.pearsoneducation.fr

CampusPress Rfrence

Qt 4

et

Programmation dinterfaces GUI


Un ouvrage unique sur le dveloppement dinterfaces graphiques avec la bibliohque Qt, crit par des spcialistes de Trolltech. Grce au framework Qt de Trolltech, vous pouvez crer des applications C++ de niveau professionnel qui sexcutent en natif sous Windows, Linux/UNIX, Mac OS 10 et Linux intgr sans quaucune modication dans le code source soit ncessaire. Ce guide complet vous permettra dobtenir des rsultats fantastiques avec la version la plus puissante de QT jamais cre : QT 4.1. En sappuyant sur des exemples ralistes, il prsente des techniques avances sur divers sujets depuis le dveloppement de linterface graphique de base lintgration avance de XML et des bases de donnes. Couvre l'ensemble des lments fondamentaux de Qt, depuis les botes de dialogue et les fentres jusqu' l'implmentation de la fonctionnalit d'une application Prsente des techniques avances que vous ne retrouverez dans aucun autre ouvrage, comme la cration de plugins d'application et pour Qt, ou la cration d'interfaces avec les API natives Contient des annexes dtailles sur la programmation C++/Qt destine aux dveloppeurs Java expriments
A propos des auteurs
Jasmin Blanchette, responsable de la documentation chez Trolltech et dveloppeur expriment travaille pour cette socit depuis 2001. Il intervient comme diteur de Qt Quaterly, le bulletin dinformation technique de Trolltech. Mark Summereld travaille comme consultant et formateur spcialis en C++, Qt et Python. Il a assum la charge de responsable de la documentation chez Trolltech pendant presque trois ans.

C++
TABLE DES MATIRES

Partie I : Qt : notions de base Pour dbuter Crer des botes de dialogue Crer des fentres principales Implmenter la fonctionnalit dapplication Crer des widgets personnaliss Partie II : Qt : niveau intermdiaire 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 Partie III : Qt : niveau avanc Internationalisation Environnement multithread Crer des plug-in Fonctionnalits spciques la plate-forme Programmation embarque Annexes Installer Qt Introduction au langage C++ pour les programmeurs Java et C#

Niveau : Intermdiaire / Avanc Programmation Conguration : Multiplate-forme

Pearson Education France 47 bis, rue des Vinaigriers 75010 Paris Tl. : 01 72 74 90 00 Fax : 01 42 05 22 17 www.pearson.fr

ISBN : 978-2-7440-4092-4
Ofcially Approved by Trolltech

You might also like