Programmering i Qt

Tags:    c++
Skrevet af Bruger #5688 @ 07.07.2004

Introduktion


Denne artikel omhandler programmering i Qt-frameworket. Hvad er Qt kan man så spørge, og til det kan man svare at Qt er et fuldt objektorienteret GUI/udviklings-framework implementeret i C++, men med bindings til mange andre sprog (f.eks. Python og Perl).
Dog vil jeg benytte mig af C++ i denne artikel. Qt er et meget kompliceret framework, det danner eksempelvis basis for det meget avancerede KDE-desktopmiljø på UNIX ( http://www.kde.org ), der er kendt for at have meget fart på i udviklingen, og Qt er en af de primære årsager til dette.

Qt er udviklet af det norske firma Trolltech ( http://www.trolltech.com ), der har udgivet Qt/X11 (Qt til GNU/Linux og andre UNIX-systemer der bruger X11R6) under GPL, den frie licens som næsten al fri software er udgivet under. Hvis man vil udvikle proprietære eller closed-source applikationer i Qt, må man dog betale. Ligeledes må man også betale hvis man vil udvikle Qt på Windows. Der er en gammel Qt-Noncommercial udgave til Windows ( http://www.trolltech.com/download/qt/download_noncomm.html ), men den er ret gammel (april 2001), og integrerer kun med Visual Studio 6.
Derfor er Qt klart bedst hvis man udvikler på UNIX/X11 platformen (GNU/Linux går ind under denne), selvom man dog kan downloade en trial af den nyeste Qt-Windows ( http://www.trolltech.com/download/qt/evaluate.html ). Den virker i 30 dage og integrerer med både Borland C++ Builder 5.5, Visual Studio 6 og Visual Studio .NET. En meget vigtig ting at huske er at software der udvikles med Qt Free Editon skal udgives under en GPL-kompatibel licens, hvis det overhovedet skal udgives.

Denne artikel vil ikke beskæftige sig med at sætte udviklingsmiljøet op, men jeg kan kort nævne hvordan man laver en Qt-projektfil i UNIX (jeg ved ikke om det foregår på samme måde på Windows, men eftersom portabilitet er en af Qt's største fordele vil jeg tro det). Hvis du befinder dig i mappen der indeholder dine .cpp og .h-filer, skriver du bare:

Fold kodeboks ind/udKode 


Den første kommando opretter din projektfil (*.pro), den anden opretter en Makefile ud fra projektfil, den sidste kompilerer baseret på makefilen.

Det forudsættes at du har en vis grad af C++-erfaring, i hvert fald mindst en forståelse for pointere, references, objektorienterede teknikker, osv. Derfor vil jeg ikke i denne artikel gennemgå al C++-koden (!"i++; // Inkrementerer i med én"), men blot fokusere på de Qt-relaterede dele. Erfaring med andre grafiske libraries er ikke forudsat.

Denne artikel vil gennemgå programmeringen af en simpel editor, der kan åbne en fil, gemme den, og præsentere et konsistent brugerinterface. Den vil ikke være synderligt avanceret, og vil ikke med det samme gøre en i stand til at producere kæmpestore Qt-applikation, hvilket er et alt for komplekst emne til en artikel af denne størrelse. Istedet vil den introducere Qt og de teknikker man benytter sig af, så man kan danne sig et billede af hvorvidt det er interessant som en fremtidig udviklingsplatform.

Lidt om Qt's virkemåde



Et grafisk Qt-program er opbygget af widgets - controls i Windows-terminologi. En widget kan være alt, lige fra en knap, over en tekstboks, til en OpenGL-scene. Gennem ActiveQt kan man ligeledes bruge ActiveX-komponenter i sin applikation, dog kun på Windows.

Qt opnår sin store fleksibilitet og styrke gennem MOC - Meta Object Compiler, en compiler der parser dine .cpp-filer før de sendes igennem compileren. Via denne precompiling er det muligt at implementere ganske komplekse makroer der er utroligt læsevenlige og lette at have med at gøre, men samtidigt utroligt kraftfulde. MOC's primære fordel er at den tillader brug af <b>signals og slots</b>.

Signals og slots handler i princippet om at en funktion kan udsende et signal, der så aktiverer et slot. Et signal har ingen effekt i sig selv, men kan kaldes med parametre, der så aktiverer et slot med de pågældende parametre. Et slot er en funktion med en void-returtype, der både kan kaldes via et signal og som enhver anden funktion. Forbindelsen mellem et signal og et slot gøres i makrofunktionen connect, med følgende syntaks:

Fold kodeboks ind/udKode 


F.eks:

Fold kodeboks ind/udKode 


Den ovenstående linie vil så forårsage at medlemsobjektet textWidget kalder funktionen saveFile() hver gang der trykkes på saveButton. connect-funktionen kaldes sædvanligvis i en widgets konstruktor, og omhandler sædvanligvis widget'ens underwidgets (f.eks. knapperne i en menu), selvom både signalet og slot'et kan komme fra et hvilket som helst objekt construktoren kender til. Hvis man er yderligere interesseret i ideen bag signals og slots har Trolltech en fin artikel om det ( http://doc.trolltech.com/3.3/signalsandslots.html ), men det er ikke påkrævet læsning for at gennemføre denne artikel.

Resourcer



http://doc.trolltech.com Indeholder dokumentation for alle Qt's klasser.
http://qtforum.org Det største Qt-forum.
http://sigkill.dk/code/qed.tar.gz Kildekoden til det program artiklen beskriver. Denne fil er <b>påkrævet</b>, idet der medfølger nogle ikoner (i images-mappen) som vi skal bruge. Koden i denne artikel er blevet renset for alle kommentarer, men koden i dette arkiv er rigt kommenteret..

Koden



Lad os så komme i gang med at skrive noget kode. I alt har vi 5 filer - main.cpp, qeditfield.h, qeditfield.cpp, qeditor.h, qeditor.cpp.

Vi starter med main.cpp:

Fold kodeboks ind/udKode 


(Det kan endnu ikke kompileres, forresten, og vil ikke kunne før vi har skrevet det helt færdigt).

Det første vi gør er at inkludere to filer, qapplication.h der er vores indgang til Qt, og qeditor.h, der er vores editors klasse (som vi ikke har lavet endnu). Vi inkluderer også qstring.h, der indeholder Qt's egen string-klasse. Det er ikke noget vi behøver bekymre os om.

I linie 7 erklærer vi en ny QApplication, app, og sender vort programs parametre som parametre til konstruktoren. Dette er vor egentlige applikation.

I linie 10 checker vi om der er mere end ét parameter til programmet, og hvis der er det, gemmes det parameter i en QString og sendes som argument til klassens konstruktor (det gør det også hvis der ikke er nogle argumenter, forresten, men i konstruktoren checker vi om QString'en er tom).

Vi erklærer et nyt objekt af klassen QuickEditor, og sætter vor QApplication, app, til at have den som hovedwidget. Derefter kalder vi en af qe's medlemsfunktioner, show(), der gør widget'en synlig. Til sidst i funktionen kalder vi vor QApplications exec()-funktion, og fra da af kontrolleres applikationen af Qt's interne loop.

Vores applikation er koncentreret omkring én widget (der er nedarvet fra QMainWindow, en klasse der er velegnet som "top"-vindue i et program) - QuickEditor, der er erklæret i qeditor.h og implementeret i qeditor.cpp.

qeditor.h



qeditor.h indeholder blot en klassedeklaration:

Fold kodeboks ind/udKode 


Igen, alle kommentarer er strippet fra koden, det anbefales at hente arkivet der er linket til tidligere. Det mest interessante, og fremmede i denne constructor er formentligt det mærkelige udtryk i toppen - Q_OBJECT.

Q_OBJECT er en makro der får MOC-preprocessoren til at implementere signals og slots for den pågældende klasse. Reelt betyder det at dine kildefiler bliver kørt igennem preprocessoren, og bliver sat op til at tillade connect og den slags, noget der er rædselsfuldt at skrive i hånden, men simpelt hvis det gøres gennem en preprocessor.

Derudover er der nogle rimeligt indlysende funktioner, samt en lidt mere obskur en - closeEvent( QCloseEvent * ). Denne funktion kaldes når man trykker på [X]'et i window manageren. Basalt set når applikationen afsluttes uden at det er en knap i selve programmet der gør det.

En sådan erklæring er imidlertidigt ikke nok, så vi går i gang med at implementere funktionerne i qeditor.cpp.

qeditor.cpp



Først diverse include-direktiver:

Fold kodeboks ind/udKode 


Standard include-direktiver, sørg blot for at de relevante billeder findes under images-mappen. Billederne kan findes i det arkiv der blev linket til tidligere.

Her er den korte constructor:

Fold kodeboks ind/udKode 


Constructoren tager en QString som parameter, og vil, hvis strengen ikke er tom, loade den fil som den indeholder navnet på (load()-funktionen beskrives senere). QuickEditor-konstruktoren kalder, som vi kan se, QMainWindow's konstruktor, hvilket sparer os for en masse manuelt arbejde. Ydermere kalder vi init(), der er en funktion der sætter GUI'en op, forbinder signals til slots, osv. Dette placeres i en seperat funktion, så vi senere kan tilføje alternative konstruktorer, uden at vi skal genskrive en masse GUI-relateret kode. Her er init() i qeditor.cpp:

Fold kodeboks ind/udKode 


Det første vi gør er at erklære nogle QActions. Dette er en abtrakt brugerinterfacehandling, der kan tilføjes både som et menupunkt, i en toolbar, osv. Det vil dog være den samme QAction der bliver refereret til, så de vil konstant være synkrone (hvis det f.eks. er en "Bold"-knap i en teksteditor vil knappen blive rykket ind, selv hvis man aktiverer QAction'en på en anden måde end ved at trykke på den, eksempelvis gennem genvejstaster). Det burde være relativt tydeligt ud fra variabelnavnene, hvilke funktioner de forskellige QActions vil have. De har dog ingen funktion før de connect'es til nogle slots.

Som det kan ses, er variablerne pointers der umiddelbart ikke peger på noget, derfor opretter vi straks nogle QActions til dem. QActions konstruktor modtager parametre i denne rækkefølge:

[Navn når den optræder i en toolbar], ikon, [Navn når den optræder i en menu], genvejstast, parent widget, internt navn

Parent Widget er den widget som QObject'et et en underwidget af - sædvanligvis er det this, men nu og da er det praktisk at erklære en widget som tilhørende en anden end den der skaber den. De fleste klasser i Qt tager parent widget og internt navn som de to sidste parametre ( check http://doc.trolltech.com for detaljer ). Normalt er det ikke værd at bekymre sig om det interne navn.

Efter vi har skabt hvert objekt kalder vi connect() for at forbinde hver QActions activated-signal til en medlemsfunktion i klassen der tilsvarer den funktion som vi ønsker den relevante QAction skal have. Disse funktioner vil vi implementere senere.

Når vores QActions er oprettet kan vi lave en QToolBar med de relevante actions. Bemærk at de tilføjes ved at kalde en medlemsfunktion i den QAction vi ønsker at tilføje, med QToolBar'en som parameter. Til sidst tilføjer vi en seperator - bemærk her at det nu er en medlemsfunktion i QToolBar'en der kaldes.

Derefter skal vi oprette en QPopupMenu. Constructoren modtager ét parameter, parent widget, så der er ikke noget besynderligt over det. For at tilføje vores nye QPopupMenu til applikationen, skal vi kalde menuBar()->insertItem("Navn", QPopupMenu-objekt), hvorefter menuen vil være at finde i applikationen. Derefter kan vi tilføje elementer ganske som i QToolBar. Efter vi har oprettet vor traditionelle "Fil"-menu, opretter vi endnu en klassisk menu - "Hjælp" - på præcist samme måde.

Derefter kalder vi setCaption der sætter programmets titel, hvorefter vi udskriver en besked i statusbaren. Det er meget vigtigt at der skrives til den i konstruktoren, da den ellers ikke vil blive oprettet. Andet argument er tiden (i milisekunder) der går før beskeden forsvinder.

Som noget af det sidste opretter vi et nyt QuickTextEditField - en klasse vi selv opretter der vil blive beskrevet senere, og den sættes til at være programmets centrale widget (programmet består i princippet kun af én widget, da toolbars og statuslinier behandles på en anden måde. Almindelige widgets arrangeres i tabeller der minder en del om dem der benyttes i HTML, men det er ikke relevant for denne artikel).

Den sidste linie i konstruktoren forbinder QuickTextEditField's textChanged()-signal til denne klasses fieldChanged()-slot. Dette indikerer at teksten har ændret sig siden filen sidst blev gemt, så programmet vil spørge om filen skal gemmes først, hvis man forsøger at lukke det.

Nu vil vi implementere de andre medlemsfunktioner i klassen, stadigvæk i qeditor.cpp:

Fold kodeboks ind/udKode 


fileNew() er den funktion der kaldes når der trykkes "Ny" i "Fil"-menuen. Hvis filen er ændret spørger den om den skal gemmes (via canWipeFile(), som vi implementerer senere) samtidigt med at den fjerner teksten fra qfield, sætter changed til FALSE (når filen er ny er der ingen ændringer), resetter currentFile, den string der indeholder filnavnet på den åbne fil, og sætter vinduestitlen til standard (vi ændrer vinduestitlen når vi åbner en fil, dette vil blive implementeret senere).

Fold kodeboks ind/udKode 


Igen starter vi med at kalde en funktion der checker hvorvidt filen har ændret sig siden den sidst blev gemt, og, hvis den har, spørger brugeren om den skal gemmes. Derefter opretter vi en QString som vi giver den værdi QFileDialog::getOpenGileName returnerer.
Vores argumenter til funktionen betyder at der oprettes en QFileDialog der har en "åben"-knap (og ikke en "gem"-knap), der ikke har nogen fil forvalgt (QString::null), der kan vælge alle filer ("Alle filer (*)"), der er en underwidget til this, hvilket i dette tilfælde betyder at fildialogen centreres over applikationen, hvis interne navn er "file dialog" og hvis vinduestitel er "Quick Editor -- Åbn fil".

Der vil formentligt opstå en undtagelse hvis vi forsøger at loade fra et tomt filnavn, så derfor checker vi om filnavnet er tomt (hvilket det er hvis brugeren lukkede fildialogen uden at vælge en fil), før vi loader filen. Selve indlæsninen gøres af funktionen load(), fileOpen() er blot en form for frontend. Hvis vi finder at filnavnet er tomt, skriver vi en besked i statusbaren om at åbningen er blevet annulleret.

Nu er det logisk at implementere load():

Fold kodeboks ind/udKode 


load() tager ét parameter, en reference til en QString der indeholder et filnavn der vil blive indlæst. Vi starter med at oprette et QFile-objekt, et objekt der faciliterer interaktion med en fil, og sætter den til at åbne filen der er givet i parametret QString filename. Bemærk at vi ikke foretager nogen checks for at undersøge om strengen er tom.
Vi kalder QFile::open på vores objekt, og specificerer at vi udelukkende ønsker at læse fra den. Hvis dette slår fejl (f.eks. hvis filen ikke eksisterer, eller vi ikke har rettigheder til den) skriver vi en fejlmeddelelse i statusbaren og returnerer fra funktionen.
For at få data fra filen, opretter vi en QTextStream der opererer på den QFile vi definerede tidligere. QTextStream har en række medlemsfunktioner der gør det nemt at behandle teksten i filen, hvorimod QFile i sig selv er mere low-level og behandler selve I/O-processen. qfield, et objekt af klasse QuickEditTextField, har en medlemsfunktion ved navn setText(), der, som navnet antyder, sætter teksten i feltet til den QString vi leverer som parameter (QuickEditTextField implementeres senere).
Vi bruger QTextStream::read() for at få indholdet af filen. Eftersom editoren behandler klar tekst er der ikke behov for at køre filindholdet gennem noget filter.

Vi afslutter vores I/O-operation med QFile::close(). Vi sætter currentFile til filename, altså den fil vi lige har indlæst, ændrer vinduestitlen til at reflektere denne ændring i filnavnet (som vi kan se kan man erklære en QString med variabler indsat lidt på samme måde som i prinf() i C, se evt. http://doc.trolltech.com/3.3/qstring.html#arg for yderligere detaljer).
Vi skriver en besked i statusbaren og sætter changed til FALSE for at fuldende indlæsningen.

Fold kodeboks ind/udKode 


Meningen med denne funktion er at gemme filen under dens eksisterende navn.

Vi starter med at checke om currentFile er tom - hvilket vil betyde at vi netop har startet programmet eller trykket på "Ny fil", og således ikke har specificeret et filnavn. Hvis dette er tilfældet, returnerer vi samtidigt med at vi kalder funktionen fileSaveAs(), der viser et vindue hvor brugeren kan vælge hvilket filnavn filen skal gemmes under.

Vi opretter et QFile-objekt der opererer på det filnavn currentFile indeholder (notér at, i modsætning til load() ønsker vi her også skriveadgang), hvis vi ikke kan oprette I/O-processen returnerer vi og printer en fejlmeddelelse. Vi opretter en textstream der opererer på filen og overfører indholdet af qfield til den (vi kan benytte operatoren "<<" da vi har specificeret hvordan det skal fungere på en QTextStream i vor QuickEditTextField-klasse, beskrevet senere). Derefter lukker vi filen, printer en besked i statusbaren, ændrer vinduestitlen og sætter changed til FALSE. Basalt set er denne funktion i det store hele load(), men med omvendt fortegn.

Fold kodeboks ind/udKode 


Ideen med denne funktion er at gemme filen under et nyt navn. Altså skal vi ændre currentFile og kalde fileSave(). Vi starter med at oprette en QString og tildele den værdien fra en QFileDialog. Detaljerne vedørerende dette kan findes i forklaringen for fileOpen(). Vi checker om filename-variablen indeholder noget (for at se om brugeren har trykket "Annuller"), og hvis den gør, checker vi om det filnavn der er blevet specificeret findes i forvejen. Hvis dette check også er positivt, viser vi en QMessageBox ( http://doc.trolltech.com/3.3/qmessagebox.html ) der er en child-widget af this, hvis vinduestitel er "Quick Editor -- Overskriv fil", hvis tekst er "Overskriv <filnavn>?", og hvor der er to knapper - "Ja" og "Nej".
Hvis svaret er "Ja", sætter vi currentFile til filename, som brugeren valgte tidligere, og filen gemmes hvorefter der returneres. Hvis ikke, skrives en besked i statusbaren.

Fold kodeboks ind/udKode 


Denne funktion spørger om du vil gemme evt. ugemte filer, spørger en sidste gang om programmet skal afsluttes, og afslutter programmet. qApp->exit() afslutter altid programmet. Det er også et slot, så det kan connectes til et signal.

Fold kodeboks ind/udKode 


Denne funktion returnerer TRUE hvis brugeren vil afslutte programmet, ellers FALSE. QMessageBox::information ligner meget den QMessageBox vi brugte i fileSaveAs(), så jeg henfører til den funktions beskrivelse, eller dokumentationen ( http://doc.trolltech.com/3.3/qmessagebox.html#information ) for yderligere information.

Fold kodeboks ind/udKode 


closeEvent() er den funktion der kaldes når programmet får besked på at afslutte uden om programmets egne knapper, f.eks. ved at trykke på [x]'et i windowmanageren. I dette tilfælde kalder i bare fileQuit().

Fold kodeboks ind/udKode 


helpAbout(), der er den traditionelle "Om"-boks, er interessant fordi den viser at det er muligt (til en hvis grænse) at bruge HTML i koden. Ellers er der ikke meget at se, about-boksen implementeres via endnu en af QMessageBox's medlemsfunktioner.

Fold kodeboks ind/udKode 


Opretter et "Om Qt"-vindue med informationer direkte fra Qt selv, med vinduestitlen "Quick Editor -- Om Qt"

Fold kodeboks ind/udKode 


Ideen med canWipeFile() er, at funktionen checker om filen skal gammes (spørger brugeren) - den kaldes før der indlæses nye filer, før programmet afsluttes, osv. canWipeFile() returnerer TRUE hvis den indlæste fil skal fjernes (det værende gennem indlæsning af en ny fil eller afslutning af programmet), og FALSE hvis den ikke skal.

Den dialogboks der spørger brugeren om han/hun/den/det vil gemme filen, kan enten vise navnet på den indlæste fil (gemt i currentFile-variablen) eller skrive "Ukendt Fil" - eftersom der er to muligheder opretter vi en QString der kan indeholde teksten til dialogboksen, i stedet for at skrive den direkte i funktionskaldet som vi plejer. Hvis currentFile er tom (filen er ikke gemt på disken), skriver vi "Ikke Navngivet."

Vi kalder QMessageBox::information() ( http://doc.trolltech.com/3.3/qmessagebox.html#information-2 ), der har teksten gemt i msg-variablen samt tre knapper. De to tal til sidst i funktionskaldet er hhv. standard-knappen og escape-knappen (de knapper der bliver aktiveret hvis brugeren trykker på hhv. ENTER eller ESCAPE). QMessageBox::information() returnerer en int der tilsvarer hvilken knap der blev trykket på (0, 1 eller 2). Denne værdi behandler vi blot i en switch.

Fold kodeboks ind/udKode 


Dette er et slot der kaldes når signalet textChanged() bliver udsendt af qfield. Det sætter værdien af bool-variablen changed til TRUE, og indikerer dermed at der er ændret i teksten siden den sidst blev gemt.

Nu har vi implementeret QuickEditor-klassen, der udgør selve vort program. Det eneste vi nu mangler er at definere og implementere selve den widget som vi kan skrive tekst i (qedit i QuickEditor). Vi nedarver fra QTextEdit, da vi derved har næsten alt den funktionalitet vi har behov for. Ja, faktisk behøvede vi sikkert ikke nedarve overhovedet, men det gør det lettere at udvide funktionaliteten af klassen senere, eksempelvis med syntakshighlight.

qeditfield.h



Fold kodeboks ind/udKode 


Der er ikke noget interessant i denne deklaration, vi nedarver fra QTextEdit, indsætter Q_OBJECT-makroen og opretter en init()-funktion (ideen med disse er, at hvis vi har flere konstruktorer kan de kalde denne funktion for at afvikle fælles kode).

Der er ikke meget at implementere i denne klasse:

qeditfield.cpp



Fold kodeboks ind/udKode 


I konstruktoren kalder vi init(), der så sætter minimumsdimensionerne på widget'en. Vi har også to overstyrede operatorer, der gør at vi let kan udveksle data med en QTextStream (der, f.eks., kan operere på en fil, så vi let kan foretage disk-I/O).

Konklusion



Som jeg håber der er givet indtryk af i denne artikel, er Qt et ekstremt stort og meget dækkende framework. Det er ikke bar et pænt interface man lige kan smække på en hvilken som helst applikation, Qt kræver sin helt egen programmeringsstil, og udvider ligefrem C++ for at opnå dette.
Beløninngen er let læselig kode, god ydelse, et væld af funktionalitet og et flot brugerinterface.

Når du udvikler dine egne Qt-programmer er det en massiv hjælp at bruge http://doc.trolltech.com - dokumentationen er rigtig, rigtig god.

Held og lykke.



Hvad synes du om denne artikel? Giv din mening til kende ved at stemme via pilene til venstre og/eller lægge en kommentar herunder.

Del også gerne artiklen med dine Facebook venner:  

Kommentarer (6)

User
Bruger #5792 @ 09.07.04 07:58
Super cool artikel! Mere mere :-)
User
Bruger #5688 @ 12.07.04 22:39
Der skal nok komme mere, jeg skal bare finde på noget at skrive om. :)
User
Bruger #3009 @ 29.08.04 22:23
Screenshots af et program vil være dobbelt sejt, så man ved hvad man kan forvente sig, hvis man bare skimter artiklen igennem ;)
User
Bruger #479 @ 17.10.04 22:31
Rigtig dejlig artikel!
Mere er der ikke at sige :)
User
Bruger #714 @ 12.03.06 02:09
Når jeg engang går igang med at lege med linux og KDE igen står det at prøve at programmere i Qt da som nummer 1. på listen!
User
Bruger #8985 @ 08.08.06 12:20
Et kæmpe arbejde du har lavet der!! :D Men som det allerede er sagt, så ville et billede/screenshot af programmet (evt. flere eftersom det udvikler sig) være perfekt... Så du får 4 :)
Du skal være logget ind for at skrive en kommentar.
t