Game Programming Using Qt 5 Beginner's Guide Second Edition Lorenz Haas
User Manual:
Open the PDF directly: View PDF .
Page Count: 1171 [warning: Documents this large are best viewed by clicking the View PDF Link!]
- Title Page
- Copyright and Credits
- Dedication
- Packt Upsell
- Contributors
- Preface
- Introduction to Qt
- Installation
- Qt GUI Programming
- Creating GUI in Qt
- Layouts
- Signals and slots
- Creating a widget for the tic-tac-toe board
- Advanced form editor usage
- Polishing the application
- Pop quiz
- Summary
- Custom 2D Graphics with Graphics View
- Graphics View architecture
- Coordinate systems
- Overview of functionality
- Custom items
- Time for action – Creating a sine graph project
- Time for action – Creating a graphics item class
- Events
- Time for action – Implementing the ability to scale the scene
- Time for action – Taking the zoom level into account
- Time for action – Reacting to an item's selection state
- Time for action – Event handling in a custom item
- Time for action – Implementing the ability to create and delete elements with mouse
- Time for action – Changing the item's size
- Have a go hero – Extending the item's functionality
- Widgets inside Graphics View
- Optimization
- Pop quiz
- Summary
- Animations in Graphics View
- Qt Core Essentials
- Text handling
- Containers
- Data storage
- Pop quiz
- Summary
- Networking
- QNetworkAccessManager
- Setting up a local HTTP server
- Preparing a URL for testing
- Time for action – Downloading a file
- Have a go hero – Extending the basic file downloader
- Single network manager per application
- Time for action – Displaying a proper error message
- Downloading files over FTP
- Downloading files in parallel
- The finished signal
- Time for action – Writing the OOP conform code using QSignalMapper
- The error signal
- The readyRead signal
- Time for action – Showing the download progress
- Using a proxy
- Connecting to Google, Facebook, Twitter, and co.
- Controlling the connectivity state
- Communicating between games
- Time for action – Realizing a simple chat program
- The server – QTcpServer
- Time for action – Setting up the server
- What just happened?
- Time for action – Reacting on a new pending connection
- What just happened?
- Time for action – Forwarding a new message
- Have a go hero – Using QSignalMapper
- Time for action – Detecting a disconnect
- What just happened?
- The client
- Synchronous network operations
- Using UDP
- Pop quiz
- Summary
- QNetworkAccessManager
- Custom Widgets
- Raster and vector graphics
- Raster painting
- Creating a custom widget
- Working with images
- Painting text
- Optimizing widget painting
- Implementing a chess game
- Time for action – Developing the game architecture
- Time for action – Implementing the game board class
- Time for action – Understanding the ChessView class
- Time for action – Rendering the pieces
- Time for action – Making the chess game interactive
- Time for action – Connecting the game algorithm
- Have a go hero – Implementing the UI around the chess board
- Have a go hero – Connecting a UCI-compliant chess engine
- Pop quiz
- Summary
- OpenGL and Vulkan in Qt applications
- Scripting
- Why script?
- Evaluating JavaScript expressions
- Exposing C++ objects and functions to JavaScript code
- Accessing C++ object's properties and methods
- Data type conversions between C++ and JavaScript
- Accessing signals and slots in scripts
- Time for action – Using a button from JavaScript
- Restricting access to C++ classes from JavaScript
- Creating C++ objects from JavaScript
- Exposing C++ functions to JavaScript
- Creating a JavaScript scripting game
- Python scripting
- Pop quiz
- Summary
- Introduction to Qt Quick
- Declarative UI programming
- Time for action – Creating the first project
- Time for action – Editing QML
- Property groups
- Anchors
- Time for action – Positioning items relative to each other
- QML types, components, and documents
- How does it work?
- Time for action – Property binding
- A limitation of automatic property updates
- Overview of QML types provided by Qt
- Qt Quick Designer
- Time for action – Adding a form to the project
- Form editor files
- Form editor interface
- Time for action – Adding an import
- Time for action – Adding items to the form
- Time for action – Editing anchors
- Time for action – Applying layouts to the items
- Time for action – Assigning an expression to the property
- Time for action – Exposing items as properties
- Time for action – Creating an event handler
- Qt Quick and C++
- Bringing life into static user interfaces
- Pop quiz
- Summary
- Declarative UI programming
- Customization in Qt Quick
- Creating a custom QML component
- Event handlers
- Time for action – Making the button clickable
- Time for action – Visualizing button states
- Time for action – Notifying the environment about button states
- Touch input
- Time for action – Dragging an item around
- Time for action – Rotating and scaling a picture by pinching
- Keyboard input
- Text input fields
- Gamepad input
- Sensor input
- Detecting device location
- Creating advanced QML components
- Dynamic and lazy loading of QML objects
- Imperative painting on Canvas using JavaScript
- Using C++ classes as QML components
- Pop quiz
- Summary
- Animations in Qt Quick Games
- Animation framework in Qt Quick
- Generic animations
- Time for action – Scene for an action game
- Time for action – Animating the sun's horizontal movement
- Composing animations
- Time for action – Making the sun rise and set
- Non-linear animations
- Time for action – Improving the path of the sun
- Property value sources
- Time for action – Adjusting the sun's color
- Time for action – Furnishing sun animation
- Behaviors
- Time for action – Animating the car dashboard
- States
- Transitions
- More animation types
- Quick game programming
- Game loops
- Input processing
- Time for action – Character navigation
- Time for action – Another approach to character navigation
- Time for action – Generating coins
- Sprite animation
- Time for action – Implementing simple character animation
- Time for action – Animating characters using sprites
- Time for action – Adding jumping with sprite transitions
- Time for action – Revisiting parallax scrolling
- Collision detection
- Time for action – Collecting coins
- Have a go hero – Extending the game
- Pop quiz
- Summary
- Animation framework in Qt Quick
- Advanced Visual Effects in Qt Quick
- 3D Graphics with Qt
- Pop quiz answers
- Other Books You May Enjoy
GameProgrammingUsingQt5Beginner'sGuide
SecondEdition
CreateamazinggameswithQt5,C++,andQtQuick
PavelStrakhov
WitoldWysota
LorenzHaas
BIRMINGHAM-MUMBAI
GameProgrammingUsingQt5
Beginner'sGuideSecond
Edition
Copyright©2018PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmitted
inanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseof
briefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformation
presented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressor
implied.Neithertheauthors,norPacktPublishingoritsdealersanddistributors,willbeheldliablefor
anydamagescausedorallegedtohavebeencauseddirectlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesand
productsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannot
guaranteetheaccuracyofthisinformation.
AcquisitionEditor:ShwetaPant
ContentDevelopmentEditor:FlavianVaz
TechnicalEditor:AkhilNair
CopyEditor:ShailaKusanale
ProjectCoordinator:DevanshiDoshi
Proofreader:SafisEditing
Indexer:RekhaNair
Graphics:JasonMonteiro
ProductionCoordinator:AparnaBhagat
Firstpublished:January2016
Secondedition:April2018
Productionreference:1240418
PublishedbyPacktPublishingLtd.
LiveryPlace
35LiveryStreet
Birmingham
B32PB,UK.
ISBN978-1-78839-999-9
www.packtpub.com
Idedicatethisbooktoallpeoplewhoarepassionateaboutprogramming;livelongandprosper
–PavelStrakhov
Whysubscribe?
Spendlesstimelearningandmoretimecodingwith
practicaleBooksandVideosfromover4,000industry
professionals
ImproveyourlearningwithSkillPlansbuiltespeciallyfor
you
GetafreeeBookorvideoeverymonth
Maptisfullysearchable
Copyandpaste,print,andbookmarkcontent
PacktPub.com
DidyouknowthatPacktofferseBookversionsofeverybook
published,withPDFandePubfilesavailable?Youcanupgradeto
theeBookversionatwww.PacktPub.comandasaprintbookcustomer,
youareentitledtoadiscountontheeBookcopy.Getintouchwith
usatservice@packtpub.comformoredetails.
Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnical
articles,signupforarangeoffreenewsletters,andreceiveexclusive
discountsandoffersonPacktbooksandeBooks.
Contributors
Abouttheauthors
PavelStrakhovisasoftwarearchitectanddeveloperfromRussia.
HestartedworkingwithQtin2011inMoscowInstituteofPhysics
andTechnology,whereitwasusedtobuildscientificimage
processingsoftware.During2012-2015,hewashighlyactiveinthe
QtsectionofStackOverflow,helpingpeoplelearnQtandsolve
issues.In2016,hestartedworkingonQtbindingsforRust
language.
Iwouldliketothankallthereviewerswhoworkedwithmeonthisbookfortheirinvaluablefeedback.
IamalsoverygratefultoallpeoplefromPacktPublishingwhoworkedwithme.Writingthisbook
wouldn'thavebeenpossiblewithouttheirsupportandmotivation.
WitoldWysotaisasoftwarearchitectanddeveloperlivingin
Poland.HestartedhisadventurewithQtin2004and,sincethen,it
hasbecomehismainareaofexpertise.
HeisanactivetrainerandconsultantinQt,C++,andrelated
technologiesinbothcommercialandacademicenvironments.
Inreallife,heisapassionateadeptofSevenStarPrayingMantis,a
traditionalstyleofChinesemartialarts.
LorenzHaas,apassionateprogrammer,startedhisQtcareerwith
Qt3.Heimmersedhimselfinthisframework,becameoneofthe
firstcertifiedQtdevelopersandspecialists,andturnedhislovefor
Qtintohisprofession.
Lorenzisnowworkingasaleadsoftwarearchitect.Hemainly
developsmachinecontrolsandtheiruserinterfacesaswellas
generalsolutionsfortheindustrysector.
Yearsago,hestartedcontributingtoQtCreatorandQt.Headdeda
coupleofrefactoringoptionsthatyouprobablyrelyonregularlyif
youuseQtCreator.HeisalsotheauthoroftheBeautifierplugin.
Whatthisbookcovers
Chapter1,IntroductiontoQt,familiarizesyouwiththestandard
behaviorthatisrequiredwhencreatingcross-platformapplications
andshowsyouabitofhistoryofQtandhowitevolvedovertime
withanemphasisonthemostrecentarchitecturalchangesinQt.
Chapter2,Installation,guidesyouthroughtheprocessofinstallinga
Qtbinaryreleasefordesktopplatforms,settingupthebundledIDE,
andlooksatvariousconfigurationoptionsrelatedtocross-platform
programming.
Chapter3,QtGUIProgramming,showsyouhowtocreateclassic
userinterfaceswiththeQtWidgetsmodule.Italsofamiliarizesyou
withtheprocessofcompilingapplicationsusingQt.
Chapter4,Custom2DGraphicswithGraphicsView,familiarizesyou
with2Dobject-orientedgraphicsinQt.Youwilllearnhowtouse
built-initemstocomposethefinalresultsaswellascreateyour
ownitemssupplementingwhatisalreadyavailable.
Chapter5,AnimationsinGraphicsView,describestheQtAnimation
framework,thepropertysystem,andshowsyouhowtoimplement
animationsinGraphicsView.Itwillguideyouthroughtheprocess
ofcreatingagamefeaturing2Dgraphicsandanimations.
Chapter6,QtCoreEssentials,coverstheconceptsrelatedtodata
processinganddisplayinQt—filehandlingindifferentformats,
Unicodetexthandlinganddisplayinguser-visiblestringsin
differentlanguages,andregularexpressionmatching.
Chapter7,Networking,demonstratestheIPnetworkingtechnologies
thatareavailableinQt.ItwillteachyouhowtoconnecttoTCP
servers,implementaTCPserver,andimplementfast
communicationviaUDP.
Chapter8,CustomWidgets,describesthewholemechanismrelatedto
2DsoftwarerenderinginQt,andteachesyouhowtocreateyour
ownwidgetclasseswithuniquefunctionalities.
Chapter9,OpenGLandVulkaninQtapplications,discussesQt
capabilitiesrelatedtoaccelerated3Dgraphics.Youwilllearnhow
toperformfast3DdrawingusingOpenGLandVulkanAPIsanduse
theconvenientwrappersQtprovidesforthem.
Chapter10,Scripting,coversthebenefitsofscriptinginapplications.
Itwillteachyouhowtoemployascriptingengineforagameby
usingJavaScriptorPython.
Chapter11,IntroductiontoQtQuick,teachesyouhowtoprogram
resolution-independentfluiduserinterfacesusingaQML
declarativeengineandQtQuickscenegraphenvironment.
Chapter12,CustomizationinQtQuick,focusesonhowtoimplement
newgraphicalitemsinQtQuickandimplementcustomevent
handling.
Chapter13,AnimationsinQtQuickGames,familiarizesyouwiththe
waystoperformanimationsinQtQuickandgivemorehintsfor
implementinggamesinQtQuick.
Chapter14,AdvancedVisualEffectsinQtQuick,goesthroughsome
advancedconceptsthatwillallowyoutoperformtrulyunique
graphicaleffectsinQtQuick.
Chapter15,3DGraphicswithQt,outlinesusingQt'shigh-levelAPI
for3Dgraphicsandshowyouhowtoimplementananimated3D
game.
Chapter16,MiscellaneousandAdvancedConcepts,demonstratesthe
Aboutthereviewers
JulienDéramondisasoftwaredeveloperlivinginParis,France.
HestartedhiscareerdevelopingC++webservicesuntilheentered
theembeddedworldviatheOrangeset-topboxesin2012.
SpecializedinQML,hemainlyprototypesanddevelopsuser
interfaceswithdesigners.Recently,hestartedcontributingtoQt,
especiallyinfindingbugsandproposingpatchesfortheQMLJS
ReformaterofQtCreator.Whenheisnotwritingcode,heenjoys
travelinganddrawing.
SimoneAngeloniisasoftwareengineerwithover13yearsof
experienceinC++andaskillsetincludingcross-platform
development,embeddedsystems,multi-threading,userinterfaces,
networkcommunication,databases,webapplications,game
development,andvisualdesign.
HeiscurrentlyaseniorsoftwareengineerintheR&DdeptofNikon
Corporation,andheisdevelopingsoftware/hardwaresolutionsto
controlrobotsusedinthemotionpictureindustry.
Packtissearchingforauthors
likeyou
Ifyou'reinterestedinbecominganauthorforPackt,pleasevisitauth
ors.packtpub.comandapplytoday.Wehaveworkedwiththousandsof
developersandtechprofessionals,justlikeyou,tohelpthemshare
theirinsightwiththeglobaltechcommunity.Youcanmakea
generalapplication,applyforaspecifichottopicthatweare
recruitinganauthorfor,orsubmityourownidea.
TableofContents
TitlePage
CopyrightandCredits
GameProgrammingUsingQt5Beginner'sGuideSecondEdition
Dedication
PacktUpsell
Whysubscribe?
PacktPub.com
Contributors
Abouttheauthors
Aboutthereviewers
Packtissearchingforauthorslikeyou
Preface
Whothisbookisfor
Whatthisbookcovers
Togetthemostoutofthisbook
Downloadtheexamplecodefiles
Conventionsused
Getintouch
Reviews
1. IntroductiontoQt
Ajourneythroughtime
Thecross-platformprogramming
Supportedplatforms
GUIscalability
Qtversions
StructureofQtframework
QtEssentials
QtAdd-ons
qmake
ModernC++standards
Choosingtherightlicense
Anopensourcelicense
Acommerciallicense
Summary
2. Installation
InstallingtheQtSDK
Timeforaction–InstallingQtusinganonlinei
nstaller
Whatjusthappened?
QtCreator
QtCreator'smodes
Settingupcompilers,Qtversions,andkits
Timeforaction–Loadinganexampleproject
Qtdocumentation
Timeforaction–RunningtheAffineTransformati
onsproject
Whatjusthappened?
Summary
3. QtGUIProgramming
CreatingGUIinQt
Timeforaction–CreatingaQtWidgetsproject
Whatjusthappened?
Designmodeinterface
Timeforaction–Addingwidgetstotheform
Layouts
Timeforaction–Addingalayouttotheform
Signalsandslots
Creatingsignalsandslots
Connectingsignalsandslots
Oldconnectsyntax
Signalandslotaccessspecifiers
Timeforaction–Receivingthebutton-clicksign
alfromtheform
Whatjusthappened?
Automaticslotconnectionanditsdrawbacks
Timeforaction–Changingthetextsonthelabel
sfromthecode
Creatingawidgetforthetic-tac-toeboard
ChoosingbetweendesignerformsandplainC++classes
Timeforaction–Creatingagameboardwidget
Whatjusthappened?
Automaticdeletionofobjects
Timeforaction–Functionalityofatic-tac-toe
board
Timeforaction–Reactingtothegameboard'ssi
gnals
Whatjusthappened?
Advancedformeditorusage
Timeforaction–Designingthegameconfiguratio
ndialog
Acceleratorsandlabelbuddies
Thetaborder
Timeforaction–Publicinterfaceofthedialog
Polishingtheapplication
Sizepolicies
Protectingagainstinvalidinput
Mainmenuandtoolbars
Timeforaction–Creatingamenuandatoolbar
Whatjusthappened?
TheQtresourcesystem
Timeforaction–Addingiconstotheproject
Haveagohero–Extendingthegame
Popquiz
Summary
4. Custom2DGraphicswithGraphicsView
GraphicsViewarchitecture
Timeforaction–CreatingaprojectwithaGraph
icsView
Whatjusthappened?
Coordinatesystems
Theitem'scoordinatesystem
Thescene'scoordinatesystem
Theviewport'scoordinatesystem
Originpointofthetransformation
Whatjusthappened?
Haveagohero–Applyingmultipletrans
formations
Parent–childrelationshipbetweenitems
Timeforaction–Usingchilditems
Haveagohero–Implementingthecustom
rectangleasaclass
Conversionsbetweencoordinatesystems
Overviewoffunctionality
Standarditems
Anti-aliasing
Pensandbrushes
Itemselection
Keyboardfocusingraphicsscene
Painterpaths
Timeforaction–Addingpathitemstot
hescene
Z-orderofitems
Ignoringtransformations
Timeforaction–Addingtexttoacusto
mrectangle
Findingitemsbyposition
Showingspecificareasofthescene
Savingascenetoanimagefile
Whatjusthappened?
Haveagohero–Renderingonlyspecific
partsofascene
Customitems
Timeforaction–Creatingasinegraphproject
Timeforaction–Creatingagraphicsitemclass
Whatjusthappened?
Events
Timeforaction–Implementingtheabilitytosca
lethescene
Whatjusthappened?
Timeforaction–Takingthezoomlevelintoacco
unt
Timeforaction–Reactingtoanitem'sselection
state
Whatjusthappened?
Timeforaction–Eventhandlinginacustomitem
Timeforaction–Implementingtheabilitytocre
ateanddeleteelementswithmouse
Timeforaction–Changingtheitem'ssize
Haveagohero–Extendingtheitem'sfunctionali
ty
WidgetsinsideGraphicsView
Optimization
Abinaryspacepartitiontree
Cachingtheitem'spaintfunction
Optimizingtheview
OpenGLintheGraphicsView
Popquiz
Summary
5. AnimationsinGraphicsView
Thejumpingelephantorhowtoanimatethescene
Thegameplay
Timeforaction-CreatinganitemforBenjamin
Theplayingfield
Timeforaction-MakingBenjaminmove
Whatjusthappened?
Parallaxscrolling
Timeforaction-Movingthebackground
Whatjusthappened?
Haveagohero-Addingnewbackgroundlayers
TheAnimationframework
Properties
Timeforaction-Addingajumpanimation
Propertyanimations
Timeforaction-Usinganimationstomoveitemssmoothl
y
Whatjusthappened?
Haveagohero-LettingtheitemhandleBenjamin'sjump
Timeforaction-Keepingmultipleanimationsinsync
Whatjusthappened?
Chainingmultipleanimations
Addinggamepadsupport
WorkingwithgamepadsinQt
Timeforaction-Handlinggamepadevents
Itemcollisiondetection
Timeforaction-Makingthecoinsexplode
Whatjusthappened?
Finishingthegame
Haveagohero-Extendingthegame
Athirdwayofanimation
Popquiz
Summary
6. QtCoreEssentials
Texthandling
Stringencodings
QByteArrayandQString
Usingotherencodings
Basicstringoperations
Thestringsearchandlookup
Dissectingstrings
Convertingbetweennumbersandstrings
Internationalization
Usingargumentsinstrings
Regularexpressions
Timeforaction–Asimplequizgame
Whatjusthappened?
Extractinginformationoutofastring
Findingallpatternoccurrences
Containers
Maincontainertypes
Conveniencecontainers
Alloweditemtypes
Implicitsharing
Pointerinvalidation
Whatjusthappened?
Unnecessaryallocation
Range-basedforandQtforeachmacro
Whatjusthappened?
Datastorage
Filesanddevices
Traversingdirectories
Readingandwritingfiles
Devices
Timeforaction–Implementing
adevicetoencryptdata
Whatjusthappened?
Haveagohero–AGUIforthe
Caesarcipher
Textstreams
Binarystreams
Timeforaction–Serializationofacus
tomstructure
Whatjusthappened?
XMLstreams
Timeforaction–ImplementinganXMLpa
rserforplayerdata
Whatjusthappened?
Whatjusthappened?
Haveagohero–AnXMLserializerforp
layerdata
QVariant
QSettings
Settingshierarchy
Customizingthesettingslocationandformat
JSONfiles
Timeforaction–TheplayerdataJSONs
erializer
Timeforaction–ImplementingaJSONpa
rser
Whatjusthappened?
Popquiz
Summary
7. Networking
QNetworkAccessManager
SettingupalocalHTTPserver
PreparingaURLfortesting
Timeforaction–Downloadingafile
Haveagohero–Extendingthebasicfiledownloa
der
Singlenetworkmanagerperapplication
Timeforaction–Displayingapropererrormessa
ge
DownloadingfilesoverFTP
Downloadingfilesinparallel
Thefinishedsignal
Timeforaction–WritingtheOOPconformcodeus
ingQSignalMapper
Whatjusthappened?
Theerrorsignal
ThereadyReadsignal
Timeforaction–Showingthedownloadprogress
Whatjusthappened?
Usingaproxy
ConnectingtoGoogle,Facebook,Twitter,andco.
Timeforaction–UsingGoogle'sDistanceMatrix
API
Timeforaction–Constructingthequery
Timeforaction–Parsingtheserver'sreply
Haveagohero–ChoosingXMLasthereply'sform
at
Controllingtheconnectivitystate
QNetworkConfigurationManager
QNetworkConfiguration
QNetworkSession
QNetworkInterface
Communicatingbetweengames
Timeforaction–Realizingasimplechatprogram
Theserver–QTcpServer
Timeforaction–Settinguptheserver
Whatjusthappened?
Timeforaction–Reactingonanewpend
ingconnection
Whatjusthappened?
Timeforaction–Forwardinganewmessa
ge
Haveagohero–UsingQSignalMapper
Timeforaction–Detectingadisconnect
Whatjusthappened?
Theclient
Timeforaction–Settinguptheclient
Whatjusthappened?
Timeforaction–Receivingtextmessage
s
Timeforaction–Sendingtextmessages
Haveagohero–Extendingthechatserv
erandclient
Synchronousnetworkoperations
UsingUDP
Timeforaction–SendingatextviaUDP
Haveagohero–ConnectingplayersoftheBenjam
ingame
Popquiz
Summary
8. CustomWidgets
Rasterandvectorgraphics
Rasterpainting
Painterattributes
Coordinatesystems
Drawingoperations
Creatingacustomwidget
Timeforaction–Custom-paintedwidgets
Whatjusthappened?
Timeforaction –Transformingtheviewport
Whatjusthappened?
Timeforaction–Drawinganoscillogram
Timeforaction–Makingoscillogramsselectable
Haveagohero–Reactingonlytotheleftmouse
button
Touchevents
Workingwithimages
Loading
Modifying
Painting
Paintingtext
Statictext
Optimizingwidgetpainting
Timeforaction–Optimizingoscillogramdrawing
Whatjusthappened?
Haveagohero–Cachingtheoscillograminapix
map
Implementingachessgame
Timeforaction–Developingthegamearchitectur
e
Whatjusthappened?
Timeforaction–Implementingthegameboardcla
ss
Whatjusthappened?
Timeforaction–UnderstandingtheChessViewcla
ss
Whatjusthappened?
Timeforaction–Renderingthepieces
Whatjusthappened?
Timeforaction–Makingthechessgameinteracti
ve
Whatjusthappened?
Timeforaction–Connectingthegamealgorithm
Whatjusthappened?
Haveagohero–ImplementingtheUIaroundthec
hessboard
Haveagohero–ConnectingaUCI-compliantchess
engine
Popquiz
Summary
9. OpenGLandVulkaninQtapplications
IntroductiontoOpenGLwithQt
OpenGLwindowsandcontexts
AccessingOpenGLfunctions
UsingOpenGLinimmediatemode
Timeforaction–DrawingatriangleusingQtand
OpenGL
Multisampling
Timeforaction–Scene-basedrendering
Whatjusthappened?
Timeforaction–Drawingatexturedcube
Haveagohero–Animatingacube
ModernOpenGLwithQt
Shaders
Timeforaction–Shadedobjects
GLbuffers
UsingmultipleOpenGLversions
Offscreenrendering
VulkaninQtapplications
Preparingthedevelopingenvironment
Vulkaninstance,window,andrenderer
Timeforaction–CreatingtheminimalVulkanpro
ject
Whatjusthappened?
UsingVulkantypesandfunctions
Timeforaction–Drawingwithadynamicbackgrou
ndcolor
Logsandvalidation
CombiningOpenGLorVulkanwithQtWidgets
Popquiz
Summary
10. Scripting
Whyscript?
EvaluatingJavaScriptexpressions
Timeforaction–CreatingaJavaScripteditor
Whatjusthappened?
Globalobjectstate
ExposingC++objectsandfunctionstoJavaScriptcode
AccessingC++object'spropertiesandmethods
DatatypeconversionsbetweenC++andJavaScript
Accessingsignalsandslotsinscripts
Timeforaction–UsingabuttonfromJavaScript
RestrictingaccesstoC++classesfromJavaScript
CreatingC++objectsfromJavaScript
ExposingC++functionstoJavaScript
CreatingaJavaScriptscriptinggame
Timeforaction–Implementingthegameengine
Timeforaction–Exposingthegamestatetothe
JSengine
Whatjusthappened?
Timeforaction–Loadingscriptsprovidedbyuse
rs
Timeforaction–Executingthestrategyscripts
Timeforaction–Writingastrategyscript
Haveagohero–Extendingthegame
Pythonscripting
Timeforaction–WritingaQtwrapperforembedd
ingPython
Whatjusthappened?
Timeforaction–ConvertingdatabetweenC++and
Python
Whatjusthappened?
Haveagohero–Implementingtheremain
ingconversions
Timeforaction–Callingfunctionsandreturning
values
Whatjusthappened?
Haveagohero–WrappingQtobjectsint
oPythonobjects
Popquiz
Summary
11. IntroductiontoQtQuick
DeclarativeUIprogramming
Timeforaction–Creatingthefirstproject
Timeforaction–EditingQML
Whatjusthappened?
Propertygroups
Anchors
Timeforaction–Positioningitemsrelativetoe
achother
QMLtypes,components,anddocuments
Howdoesitwork?
Timeforaction–Propertybinding
Alimitationofautomaticpropertyupdates
OverviewofQMLtypesprovidedbyQt
QtQuickDesigner
Timeforaction–Addingaformtotheproject
Formeditorfiles
Formeditorinterface
Timeforaction–Addinganimport
Timeforaction–Addingitemstotheform
Timeforaction–Editinganchors
Timeforaction–Applyinglayoutstotheitems
Timeforaction–Assigninganexpressiontothe
property
Timeforaction–Exposingitemsasproperties
Whatjusthappened?
Timeforaction–Creatinganeventhandler
QtQuickandC++
AccessingC++objectsfromQML
AccessingQMLobjectsfromC++
Bringinglifeintostaticuserinterfaces
Fluiduserinterfaces
Statesandtransitions
Timeforaction–Addingstatestotheform
Timeforaction–Addingsmoothtransitioneffect
Whatjusthappened?
Haveagohero–Addingananimationof
theitem'sposition
Popquiz
Summary
12. CustomizationinQtQuick
CreatingacustomQMLcomponent
Timeforaction–Creatingabuttoncomponent
Whatjusthappened?
Timeforaction–Addingbuttoncontent
Whatjusthappened?
Timeforaction–Sizingthebuttonproperly
Whatjusthappened?
Timeforaction–Makingthebuttonareusableco
mponent
Whatjusthappened?
Importingcomponents
QMLandvirtualresourcepaths
Eventhandlers
Timeforaction–Makingthebuttonclickable
Whatjusthappened?
Timeforaction–Visualizingbuttonstates
Whatjusthappened?
Timeforaction–Notifyingtheenvironmentabout
buttonstates
Whatjusthappened?
Touchinput
Timeforaction–Dragginganitemaround
Whatjusthappened?
Timeforaction–Rotatingandscalingapicture
bypinching
Whatjusthappened?
Haveagohero–Rotatingandscalingwi
thamouse
Keyboardinput
Haveagohero–Practicingkey-eventpr
opagation
Textinputfields
Gamepadinput
Whatjusthappened?
Sensorinput
Detectingdevicelocation
CreatingadvancedQMLcomponents
Timeforaction–Asimpleanalogclockapplicati
on
Whatjusthappened?
Timeforaction–Addingneedlestotheclock
Whatjusthappened?
Timeforaction–Makingtheclockfunctional
Whatjusthappened?
DynamicandlazyloadingofQMLobjects
Creatingobjectsonrequest
Delayingitemcreation
ImperativepaintingonCanvasusingJavaScript
Timeforaction–PreparingCanvasforheartbeat
visualization
Whatjusthappened?
Timeforaction-drawingaheartbeat
Whatjusthappened?
Timeforaction–Hidingproperties
Timeforaction–Makingthediagrammorecolorfu
l
Whatjusthappened?
UsingC++classesasQMLcomponents
Timeforaction–Self-updatingcardashboard
Whatjusthappened?
Timeforaction–Groupingengineproperties
Whatjusthappened?
Timeforaction–RegisteringC++classasQMLty
pe
Timeforaction–MakingCarInfoinstantiablefro
mQML
Whatjusthappened?
Popquiz
Summary
13. AnimationsinQtQuickGames
AnimationframeworkinQtQuick
Genericanimations
Timeforaction–Sceneforanactiongame
Whatjusthappened?
Timeforaction–Animatingthesun'shorizontal
movement
Whatjusthappened?
Composinganimations
Timeforaction–Makingthesunriseandset
Whatjusthappened?
Non-linearanimations
Timeforaction–Improvingthepathofthesun
Whatjusthappened?
Propertyvaluesources
Timeforaction–Adjustingthesun'scolor
Whatjusthappened?
Timeforaction–Furnishingsunanimation
Whatjusthappened?
Haveagohero–Animatingthesun'sray
s
Behaviors
Timeforaction–Animatingthecardashboard
Whatjusthappened?
States
Transitions
Moreanimationtypes
Quickgameprogramming
Gameloops
Inputprocessing
Timeforaction–Characternavigation
Whatjusthappened?
Timeforaction–Anotherapproachtocharactern
avigation
Whatjusthappened?
Haveagohero–Polishingtheanimation
Timeforaction–Generatingcoins
Whatjusthappened?
Spriteanimation
Timeforaction–Implementingsimplecharactera
nimation
Whatjusthappened?
Timeforaction–Animatingcharactersusingspri
tes
Whatjusthappened?
Timeforaction–Addingjumpingwithspritetran
sitions
Whatjusthappened?
Haveagohero–MakingBenjaminwiggle
histailinanticipation
Timeforaction–Revisitingparallaxscrolling
Whatjusthappened?
Haveagohero–Verticalparallaxslidi
ng
Collisiondetection
Timeforaction–Collectingcoins
Whatjusthappened?
Haveagohero–Extendingthegame
Popquiz
Summary
14. AdvancedVisualEffectsinQtQuick
Makingthegamemoreattractive
Auto-scalinguserinterfaces
Graphicaleffects
Haveagohero–Theblurparallaxscrol
ledgameview
Particlesystems
Tuningtheemitter
Renderingparticles
Makingparticlesmove
Timeforaction–Vanishingcoinsspawningpartic
les
Whatjusthappened?
CustomOpenGL-basedQtQuickitems
Thescenegraph
Timeforaction–Creatingaregularpolygonitem
Whatjusthappened?
Haveagohero–Creatingasupportingb
orderforRegularPolygon
UsingQPainterinterfaceinQtQuick
Timeforaction–Creatinganitemfordrawingou
tlinedtext
Whatjusthappened?
Popquiz
Summary
15. 3DGraphicswithQt
Qt3Doverview
Entitiesandcomponents
Qt3Dmodules
Stablemodules
Experimentalmodules
Usingmodules
Rendering3Dobjects
Mesh,material,andtransform
Lighting
Timeforaction–creatinga3Dscene
Whatjusthappened?
Timeforaction–constructingtheTowerofHanoi
scene
Timeforaction–repeating3Dobjects
Whatjusthappened?
Timeforaction– creatingdisks
Handlinguserinput
Devices
Keyboardandmousebuttons
Inputchords
Analog(axis)input
Objectpicker
Frame-basedinputhandling
Timeforaction–receivingmouseinput
Whatjusthappened?
Performinganimations
Timeforaction–animatingdiskmovements
Whatjusthappened?
Timeforaction–implementinggamelogic
Haveagohero–improvingthegame
Integrationwith3Dmodelingsoftware
Timeforaction–usingOBJfilesforthedisks
Loadinga3Dscene
WorkingwithQt3DusingC++
Timeforaction–creatinga3DsceneusingC++
IntegrationwithQtWidgetsandQtQuick
EmbeddingQtQuickUIintoa3Dscene
EmbeddingaQt3DsceneintoaQtQuickform
Popquiz
Summary
Popquizanswers
OtherBooksYouMayEnjoy
Leaveareview-letotherreadersknowwhatyouthink
Preface
Asaleadingcross-platformtoolkitforallsignificantdesktop,
mobile,andembeddedplatforms,Qtisbecomingmorepopularby
theday.Thisbookwillhelpyoulearnthenitty-grittyofQtandwill
equipyouwiththenecessarytoolsetstobuildappsandgames.This
bookisdesignedasabeginner'sguidetotakeprogrammersnewto
Qtfromthebasics,suchasobjects,coreclasses,widgets,andnew
featuresinversion5.9,toalevelwheretheycancreateacustom
applicationwiththebestpracticesofprogrammingwithQt.
Fromabriefintroductionofhowtocreateanapplicationand
prepareaworkingenvironmentforbothdesktopandmobile
platforms,wewilldivedeeperintothebasicsofcreatinggraphical
interfacesandQt'scoreconceptsofdataprocessinganddisplay.As
youprogressthroughthechapters,you'lllearntoenrichyourgames
byimplementingnetworkconnectivityandemployingscripting.
DelveintoQtQuick,OpenGL,andvariousothertoolstoaddgame
logic,designanimation,addgamephysics,handlegamepadinput,
andbuildastonishingUIsforgames.Towardtheendofthisbook,
you'lllearntoexploitmobiledevicefeatures,suchassensorsand
geolocationservices,tobuildengaginguserexperiences.
Whothisbookisfor
Thisbookwillbeinterestingandhelpfultoprogrammers
andapplicationandUIdeveloperswhohavebasicknowledgeof
C++.Additionally,somepartsofQtallowyoutouseJavaScript,so
basicknowledgeofthislanguagewillalsobehelpful.Noprevious
experiencewithQtisrequired.DeveloperswithuptoayearofQt
experiencewillalsobenefitfromthetopicscoveredinthisbook.
Togetthemostoutofthis
book
Youdon'tneedtoownorinstallanyparticularsoftwarebefore
startingtoworkwiththebook.AcommonWindows,Linux,or
MacOSsystemshouldbesufficient.Chapter2,Installation,contains
detailedinstructionsonhowtodownloadandsetupeverything
you'llneed.
Inthisbook,youwillfindseveralheadingsthatappearfrequently:
TheTimeforactionsectioncontainsclearinstructionson
howtocompleteaprocedureortask.
TheWhatjusthappened?sectionexplainstheworkingof
thetasksorinstructionsthatyouhavejustcompleted.
TheHaveagoherosectionscontainpracticalchallenges
thatgiveyouideastoexperimentwithwhatyouhave
learned.
ThePopquizsectionscontainshortsingle-choice
questionsintendedtohelpyoutestyourownunderstanding.
Youwillfindtheanswersattheendofthebook.
Whilegoingthroughthechapters,youwillbepresentedwith
multiplegamesandotherprojectsaswellasdetaileddescriptionsof
howtocreatethem.Weadviseyoutotrytocreatetheseprojects
yourselfusingtheinstructionswe'llgiveyou.Ifatanypointoftime
youhavetroublefollowingtheinstructionsordon'tknowhowtodo
acertainstep,youshouldtakeapickattheexamplecodefilestosee
howitcanbedone.However,themostimportantandexcitingpart
oflearningistodecidewhatyouwanttoimplementandthenfinda
waytodoit,sopayattentiontothe"Haveagohero"sectionsor
thinkofyourownwaytoimproveeachproject.
Downloadtheexamplecode
files
Youcandownloadtheexamplecodefilesforthisbookfromyour
accountatwww.packtpub.com.Ifyoupurchasedthisbookelsewhere,you
canvisitwww.packtpub.com/supportandregistertohavethefilesemailed
directlytoyou.
Youcandownloadthecodefilesbyfollowingthesesteps:
1. Loginorregisteratwww.packtpub.com.
2. SelecttheSUPPORTtab.
3. ClickonCodeDownloads&Errata.
4. EnterthenameofthebookintheSearchboxandfollowthe
onscreeninstructions.
Oncethefileisdownloaded,pleasemakesurethatyouunzipor
extractthefolderusingthelatestversionof:
WinRAR/7-ZipforWindows
Zipeg/iZip/UnRarXforMac
7-Zip/PeaZipforLinux
ThecodebundleforthebookisalsohostedonGitHubathttps://githu
b.com/PacktPublishing/Game-Programming-Using-Qt-5-Beginners-Guide-Second-Edition.
Wealsohaveothercodebundlesfromourrichcatalogofbooksand
videosavailableathttps://github.com/PacktPublishing/.Checkthemout!
Conventionsused
Thereareanumberoftextconventionsusedthroughoutthisbook.
CodeInText:Indicatescodewordsintext,databasetablenames,folder
names,filenames,fileextensions,pathnames,dummyURLs,user
input,andTwitterhandles.Hereisanexample:"ThisAPIis
centeredonQNetworkAccessManager,whichhandlesthecomplete
communicationbetweenyourgameandtheInternet."
Ablockofcodeissetasfollows:
QNetworkRequestrequest;
request.setUrl(QUrl("http://localhost/version.txt"));
request.setHeader(QNetworkRequest::UserAgentHeader,"MyGame");
m_manager->get(request);
Whenwewishtodrawyourattentiontoaparticularpartofacode
block,therelevantlinesoritemsaresetinbold:
voidFileDownload::downloadFinished(QNetworkReply*reply){
constQByteArraycontent=reply->readAll();
_edit->setPlainText(content);
reply->deleteLater();
}
Bold:Indicatesanewterm,animportantword,orwordsthatyou
seeonscreen.Forexample,wordsinmenusordialogboxesappear
inthetextlikethis.Hereisanexample:"OntheSelectDestination
Locationscreen,clickonNexttoacceptthedefaultdestination."
Warningsorimportantnotesappearlikethis.
Tipsandtricksappearlikethis.
Getintouch
Feedbackfromourreadersisalwayswelcome.
Generalfeedback:Emailfeedback@packtpub.comandmentionthebook
titleinthesubjectofyourmessage.Ifyouhavequestionsaboutany
aspectofthisbook,pleaseemailusatquestions@packtpub.com.
Errata:Althoughwehavetakeneverycaretoensuretheaccuracy
ofourcontent,mistakesdohappen.Ifyouhavefoundamistakein
thisbook,wewouldbegratefulifyouwouldreportthistous.Please
visitwww.packtpub.com/submit-errata,selectingyourbook,clickingonthe
ErrataSubmissionFormlink,andenteringthedetails.
Piracy:Ifyoucomeacrossanyillegalcopiesofourworksinany
formontheInternet,wewouldbegratefulifyouwouldprovideus
withthelocationaddressorwebsitename.Pleasecontactusat
copyright@packtpub.comwithalinktothematerial.
Ifyouareinterestedinbecominganauthor:Ifthereisa
topicthatyouhaveexpertiseinandyouareinterestedineither
writingorcontributingtoabook,pleasevisitauthors.packtpub.com.
Reviews
Pleaseleaveareview.Onceyouhavereadandusedthisbook,why
notleaveareviewonthesitethatyoupurchaseditfrom?Potential
readerscanthenseeanduseyourunbiasedopiniontomake
purchasedecisions,weatPacktcanunderstandwhatyouthink
aboutourproducts,andourauthorscanseeyourfeedbackontheir
book.Thankyou!
FormoreinformationaboutPackt,pleasevisitpacktpub.com.
IntroductiontoQt
Inthischapter,youwilllearnwhatQtisandhowitevolved.Wewill
describethestructureoftheQtframeworkandthedifferences
betweenitsversions.Finally,youwilllearnhowtodecidewhichQt
licensingschemeisrightforyourprojects.
Themaintopicscoveredinthischapterare:
Qthistory
Supportedplatforms
StructureoftheQtframework
Qtversions
Qtlicenses
Ajourneythroughtime
ThedevelopmentofQtstartedin1991bytwoNorwegians—Eirik
Chambe-EngandHaavardNord—whowerelookingtocreatea
cross-platformGUIprogrammingtoolkit.Thefirstcommercial
clientofTrolltech(thecompanythatcreatedtheQttoolkit)wasthe
EuropeanSpaceAgency.ThecommercialuseofQthelpedTrolltech
sustainfurtherdevelopment.Atthattime,Qtwasavailablefortwo
platforms—Unix/X11andWindows—however,developingwithQt
forWindowsrequiredbuyingaproprietarylicense,whichwasa
significantdrawbackinportingtheexistingUnix/Qtapplications.
AmajorstepforwardwasthereleaseofQtVersion3.0in2001,
whichsawtheinitialsupportforMacaswellasanoptiontouseQt
forUnixandMacunderaliberalGPLlicense.Still,QtforWindows
wasonlyavailableunderapaidlicense.Nevertheless,atthattime,
Qthadsupportforalltheimportantplayersinthemarket—
Windows,Mac,andUnixdesktops,withTrolltech'smainstream
productandQtforembeddedLinux.
In2005,Qt4.0wasreleased,whichwasarealbreakthroughfora
numberofreasons.First,theQtAPIwascompletelyredesigned,
whichmadeitcleanerandmorecoherent.Unfortunately,atthe
sametime,itmadetheexistingQt-basedcodeincompatiblewith
4.0,andmanyapplicationsneededtoberewrittenfromscratchor
requiredmuchefforttobeadaptedtothenewAPI.Itwasadifficult
decision,butfromthetimeperspective,wecanseeitwasworthit.
DifficultiescausedbychangesintheAPIwerewellcounteredbythe
factthatQtforWindowswasfinallyreleasedunderGPL.Many
optimizationswereintroducedthatmadeQtsignificantlyfaster.
Lastly,Qt,whichwasasinglelibraryuntilnow,wasdividedintoa
numberofmodules.Thisallowedprogrammerstoonlylinktothe
functionalitythattheyusedintheirapplications,reducingthe
memoryfootprintandthedependenciesoftheirsoftware.
In2008,TrolltechwassoldtoNokia,whichatthattimewas
lookingforasoftwareframeworktohelpitexpandandreplaceits
Symbianplatforminthefuture.TheQtcommunitybecamedivided;
somepeoplewerethrilled,otherswereworriedafterseeingQt's
developmentgettransferredtoNokia.Eitherway,newfundswere
pumpedintoQt,speedingupitsprogressandopeningitformobile
platforms—SymbianandthenMaemoandMeeGo.
ForNokia,Qtwasnotconsideredaproductofitsown,butrathera
tool.Therefore,NokiadecidedtointroduceQttomoredevelopers
byaddingaveryliberalLesserGeneralPublicLicense(LGPL)
thatallowedtheusageoftheframeworkforbothopenandclosed
sourcedevelopment.
BringingQttonewplatformsandlesspowerfulhardwarerequired
anewapproachtocreateuserinterfacesandtomakethemmore
lightweight,fluid,andattractive.NokiaengineersworkingonQt
cameupwithanewdeclarativelanguagetodevelopsuchinterfaces
—theQtModelingLanguage(QML)andaQtruntimeforit
calledQtQuick.
Thelatterbecametheprimaryfocusofthefurtherdevelopmentof
Qt,practicallystallingallnon-mobile-relatedwork,channelingall
effortstomakeQtQuickfaster,easier,andmorewidespread.Qt4
wasalreadyinthemarketforsevenyears,anditbecameobvious
thatanothermajorversionofQthadtobereleased.Itwasdecided
tobringmoreengineerstoQtbyallowinganyonetocontributeto
theproject.TheQtProjectfoundedbyNokiain2011providedan
infrastructureforcodereviewandintroducedanopengovernance
model,allowingoutsidedeveloperstoparticipateindecision
making.
NokiadidnotmanagetofinishworkingonQt5.0.Asaresultofan
unexpectedturnoverofNokiatowarddifferenttechnologyin2011,
theQtdivisionwassoldinmid2012totheFinnishcompanyDigia
thatmanagedtocompletetheeffortandreleaseQt5.0,a
completelyrestructuredframework,inDecemberofthesameyear.
WhileQt5.0introducedalotofnewfeatures,itwasmostly
compatiblewithQt4andalloweddeveloperstoseamlesslymigrate
tothenewmajorversion.
In2014,DigiaformedtheQtCompanythatisnowresponsiblefor
Qtdevelopment,commercialization,andlicensing.AllQt-related
webresourcesscatteredacrossQtProjectandDigiawebsiteswere
eventuallyunifiedathttps://www.qt.io/.Qtcontinuestoreceivebug
fixes,newfeatures,andnewplatformsupport.Thisbookisbased
onQt5.9,whichwasreleasedin2017.
Thecross-platform
programming
Qtisanapplication-programmingframeworkthatisusedto
developcross-platformapplications.Whatthismeansisthat
softwarewrittenforoneplatformcanbeportedandexecutedon
anotherplatformwithlittleornoeffort.Thisisobtainedbylimiting
theapplicationsourcecodetoasetofcallstoroutinesandlibraries
availabletoallthesupportedplatforms,andbydelegatingalltasks
thatmaydifferbetweenplatforms(suchasdrawingonthescreen
andaccessingsystemdataorhardware)toQt.Thiseffectively
createsalayeredenvironment(asshowninthefollowingdiagram),
whereQthidesallplatform-dependentaspectsfromtheapplication
code:
Ofcourse,attimes,weneedtousesomefunctionalitythatQt
doesn'tprovide.Insuchsituations,itisimportanttousea
conditionalcompilationforplatform-specificcode.Qtprovidesa
widesetofmacrosspecifyingthecurrentplatform.Wewillreturn
tothistopicinChapter6,QtCoreEssentials.
Supportedplatforms
Theframeworkisavailableforanumberofplatforms,rangingfrom
classicaldesktopenvironmentsthroughembeddedsystemsto
mobiledevices.Qt5.9supportsthefollowingplatforms:
Desktopplatforms:Windows,Linux,andmacOS
Mobileplatforms:UWP,Android,andiOS
Embeddedplatforms:VxWorks,INTEGRITY,QNX,and
EmbeddedLinux
Itislikelythatthelistofsupportedplatformswillchangeinfuture
Qtversions.YoushouldrefertotheSupportedPlatforms
documentationpageforyourQtversionfordetailedinformation
aboutsupportedversionsofoperatingsystemsandcompilers.
GUIscalability
Forthemostpartofthehistoryofdesktopapplication
development,specifyingsizesofGUIelementsinpixelswasthe
commonpractice.Whilemostoperatingsystemshaddotsper
inch(DPI)settingsandAPIsfortakingitintoaccountforalong
time,themajorityofexistingdisplayshadapproximatelythesame
DPI,soapplicationswithouthighDPIsupportwerecommon.
Thesituationchangedwhenhigh-DPIdisplaysbecamemore
commoninthemarket—mostnotablyinmobilephonesandtablets,
butalsoinlaptopsanddesktops.Now,evenifyouonlytarget
desktopplatforms,youshouldthinkaboutsupportingdifferentDPI
settings.Whenyoutargetmobiledevices,thisbecomesmandatory.
IfyouareusingQtWidgetsorQtQuick,youoftendon'tneedto
specifypixelsizesatall.Standardwidgetsandcontrolswilluse
fonts,margins,andoffsetsdefinedbythestyle.Iflayoutsareused,
QtwilldeterminepositionsandsizesofallGUIitemsautomatically.
AvoidspecifyingconstantsizesforGUIelementswhenpossible.
YoumayusesizesrelatedtosizesofotherGUIelements,the
window,orthescreen.QtalsoprovidesanAPIforqueryingscreen
DPI,GUIstylemetrics,andfontmetrics,whichshouldhelpto
determinetheoptimalsizeforthecurrentdevice.
OnmacOSandiOS,QtWidgetsandQtQuickapplicationsare
scaledautomaticallyusingavirtualcoordinatesystem.Pixelvalues
intheapplicationremainthesame,buttheGUIwillscaleaccording
totheDPIofthecurrentdisplay.Forexample,ifthepixelratiois
setto2(acommonvalueforretinadisplays),creatingawidgetwith
100"pixels"widthwillproduceawidgetwith200physicalpixels.
Thatmeansthattheapplicationdoesn'thavetobehighlyawareof
DPIvariations.However,thisscalingdoesnotapplytoOpenGL,
whichalwaysusesphysicalpixels.
Qtversions
EachQtversionnumber(forexample,5.9.2)consistsofmajor,
minor,andpatchcomponents.Qtpaysspecialattentiontoforwards
andbackwardscompatibilitybetweendifferentversions.Small
changeswhicharebothforwardsandbackwardscompatible
(typicallybugfixeswithoutchanginganyAPI)areindicatedby
changingonlythepatchversion.Newminorversionsusuallybring
innewAPIandfeatures,sotheyarenotforwardscompatible.
However,allminorversionsarebackwardsbinaryandsource
compatible.Thismeansthatifyou'retransitioningtoanewer
minorversion(forexample,from5.8to5.9),youshouldalwaysbe
abletorebuildyourprojectwithoutchanges.Youcaneven
transitiontoanewminorversionwithoutrebuilding,byonly
updatingsharedQtlibraries(orlettingthepackagemanagerofthe
OSdothat).Majorreleasesindicatebigchangesandmaybreak
backwardscompatibility.However,thelatestmajorrelease(5.0)
wasmostlysourcecompatiblewiththepreviousversion.
QtdeclaresLongTermSupport(LTS)forcertainversions.LTS
versionsreceivepatch-levelreleaseswithbugfixesandsecurity
fixesforthreeyears.Commercialsupportisavailableforeven
longerperiods.CurrentLTSreleasesatthetimeofwritingare5.6
and5.9.
StructureofQtframework
AsQtexpandedovertime,itsstructureevolved.Atfirst,itwasjust
asinglelibrary,thenasetoflibraries.Whenitbecameharderto
maintainandupdateforthegrowingnumberofplatformsthatit
supported,adecisionwasmadetosplittheframeworkintomuch
smallermodulescontainedintwomodulegroups—QtEssentials
andQtAdd-ons.Amajordecisionrelatingtothesplitwasthateach
modulecouldnowhaveitsownindependentreleaseschedule.
QtEssentials
TheEssentialsgroupcontainsmodulesthataremandatoryto
implementforeverysupportedplatform.Thisimpliesthatifyou
areimplementingyoursystemusingmodulesfromthisgrouponly,
youcanbesurethatitcanbeeasilyportedtoanyotherplatform
thatQtsupports.ThemostimportantrelationsbetweenQt
Essentialsmodulesareshowninthefollowingdiagram:
Someofthemodulesareexplainedasfollows:
TheQtCoremodulecontainsthemostbasicQt
functionalitythatallothermodulesrelyon.Itprovides
supportforeventprocessing,meta-objects,dataI/O,text
processing,andthreading.Italsobringsanumberof
frameworks,suchastheAnimationframework,theState
Machineframework,andthePluginframework.
TheQtGUImoduleprovidesbasiccross-platformsupport
tobuilduserinterfaces.Itcontainsthecommon
functionalityrequiredbymorehigh-levelGUImodules(Qt
WidgetsandQtQuick).QtGUIcontainsclassesthatare
usedtomanipulatewindowsthatcanberenderedusing
eithertherasterengineorOpenGL.Qtsupportsdesktop
OpenGLaswellasOpenGLES1.1and2.0.
QtWidgetsextendstheGUImodulewiththeabilityto
createauserinterfaceusingwidgets,suchasbuttons,edit
boxes,labels,dataviews,dialogboxes,menus,andtoolbars,
whicharearrangedusingaspeciallayoutengine.Qt
WidgetsutilizesQt'seventsystemtohandleinputeventsin
across-platformway.Thismodulealsocontainsthe
implementationofanobject-oriented2Dgraphicscanvas
calledGraphicsView.
QtQuickisanextensionofQtGUI,whichprovidesa
meanstocreatelightweightfluiduserinterfacesusingQML.
Itisdescribedinmoredetaillaterinthischapter,aswellas
inChapter11,IntroductiontoQtQuick.
QtQMLisanimplementationoftheQMLlanguageusedin
QtQuick.ItalsoprovidesAPItointegratecustomC++types
intoQML'sJavaScriptengineandtointegrateQMLcode
withC++.
QtNetworkbringssupportforIPv4andIPv6networking
usingTCPandUDP.ItalsocontainsHTTP,HTTPS,FTP
clients,anditextendssupportforDNSlookups.
QtMultimediaallowsprogrammerstoaccessaudioand
videohardware(includingcamerasandFMradio)torecord
andplaymultimediacontent.Italsofeatures3Dpositional
audiosupport.
QtSQLbringsaframeworkthatisusedtomanipulateSQL
databasesinanabstractway.
Therearealsoothermodulesinthisgroup,butwewillnotfocusontheminthisbook.If
youwanttolearnmoreaboutthem,youcanlookthemupintheQtreferencemanual.
QtAdd-ons
Thisgroupcontainsmodulesthatareoptionalforanyplatform.
Thismeansthatifaparticularfunctionalityisnotavailableonsome
platformorthereisnobodywillingtospendtimeworkingonthis
functionalityforaplatform,itwillnotpreventQtfromsupporting
thisplatform.We'llmentionsomeofthemostimportantmodules
here:
QtConcurrent:Thishandlesmulti-threadedprocessing
Qt3D:Thisprovideshigh-levelOpenGLbuildingblocks
QtGamepad:Thisenablesapplicationstosupport
gamepadhardware
QtD-Bus:Thisallowsyourapplicationtocommunicate
withothersviatheD-Busmechanism
QtXMLPatterns:ThishelpsustoaccessXMLdata
Manyothermodulesarealsoavailable,butwewillnotcoverthem
here.
qmake
SomeQtfeaturesrequireadditionalbuildstepsduringthe
compilationandlinkingoftheproject.Forexample,Meta-Object
Compiler(moc),UserInterfaceCompiler(uic),and
ResourceCompiler(rcc)mayneedtobeexecutedtohandleQt's
C++extensionsandfeatures.Forconvenience,Qtprovides
theqmakeexecutablethatmanagesyourQtprojectandgenerates
filesrequiredforbuildingitonthecurrentplatform(suchas
Makefileforthemakeutility).qmakereadstheproject's
configurationfromaprojectfilewiththe.proextension.QtCreator
(theIDEthatcomeswithQt)automaticallycreatesandupdates
thatfile,butitcanbeeditedmanuallytoalterthebuildprocess.
Alternatively,CMakecanbeusedtoorganizeandbuildtheproject.
QtprovidesCMakepluginsforperformingallthenecessarybuild
actions.QtCreatoralsohasfairlygoodsupportforCMakeprojects.
CMakeismoreadvancedandpowerfulthanqmake,butit's
probablynotneededforprojectswithasimplebuildprocess.
ModernC++standards
YoucanusemodernC++inyourQtprojects.Qt'sbuildtool
(qmake)allowsyoutospecifytheC++standardyouwanttotarget.
QtitselfintroducesanimprovedandextendedAPIbyusingnew
C++featureswhenpossible.Forexample,itusesref-qualified
memberfunctionsandintroducesmethodsacceptinginitializerlists
andrvaluereferences.Italsointroducesnewmacrosthathelpyou
dealwithcompilersthatmayormaynotsupportnewstandards.
IfyouusearecentC++revision,youhavetopayattentiontothe
compilerversionsyouuseacrossthetargetplatformsbecauseolder
compilersmaynotsupportthenewstandard.Inthisbook,wewill
assumeC++11support,asitiswidelyavailablealready.Thus,
we'lluseC++11featuresinourcode,suchasrange-basedforloops,
scopedenumerations,andlambdaexpressions.
Choosingtherightlicense
Qtisavailableundertwodifferentlicensingschemes—youcan
choosebetweenacommerciallicenseandanopensourceone.We
willdiscussbothheretomakeiteasierforyoutochoose.Ifyou
haveanydoubtsregardingwhetheraparticularlicensingscheme
appliestoyourusecase,youbetterconsultaprofessionallawyer.
Anopensourcelicense
Theadvantageofopensourcelicensesisthatwedon'thavetopay
anyonetouseQt;however,thedownsideisthattherearesome
limitationsimposedonhowitcanbeused.
Whenchoosingtheopensourceedition,wehavetochoosebetween
GPL3.0andLGPL3.SinceLGPLismoreliberal,inthischapterwe
willfocusonit.ChoosingLGPLallowsyoutouseQttoimplement
systemsthatareeitheropensourceorclosedsource—youdon't
havetorevealthesourcesofyourapplicationtoanyoneifyoudon't
wantto.
However,thereareanumberofrestrictionsyouneedtobeaware
of:
AnymodificationsthatyoumaketoQtitselfneedtobe
madepublic,forexample,bydistributingsourcecode
patchesalongsideyourapplicationbinary.
LGPLrequiresthatusersofyourapplicationmustbeableto
replaceQtlibrariesthatyouprovidethemwithother
librarieswiththesamefunctionality(forexample,a
differentversionofQt).Thisusuallymeansthatyouhaveto
dynamicallylinkyourapplicationagainstQtsothattheuser
cansimplyreplaceQtlibrarieswithhisown.Youshouldbe
awarethatsuchsubstitutionscandecreasethesecurityof
yoursystem;thus,ifyouneedittobeverysecure,open
sourcemaynotbetheoptionforyou.
LGPLisincompatiblewithanumberoflicenses,especially
proprietaryones,soitispossiblethatyouwon'tbeableto
useQtwithsomecommercialcomponents.
SomeQtmodulesmayhavedifferentlicensingrestrictions.For
example,QtCharts,QtDataVisualization,andQtVirtualKeyboard
modulesarenotavailableunderLGPLandcanonlybeusedunder
GPLorthecommerciallicense.
TheopensourceeditionofQtcanbedownloadeddirectlyfromhttps
://www.qt.io.
Acommerciallicense
Mostoftherestrictionsareliftedifyoudecidetobuyacommercial
licenseforQt.Thisallowsyoutokeeptheentiresourcecodea
secret,includinganychangesyoumaywanttoincorporateintoQt.
YoucanfreelylinkyourapplicationstaticallyagainstQt,which
meansfewerdependencies,asmallerdeploymentbundlesize,anda
fasterstartup.Italsoincreasesthesecurityofyourapplication,as
enduserscannotinjecttheirowncodeintotheapplicationby
replacingadynamicallyloadedlibrarywiththeirown.
Summary
Inthischapter,youlearnedaboutthearchitectureofQt.Wesaw
howitevolvedovertimeandwehadabriefoverviewofwhatit
lookslikenow.Qtisacomplexframeworkandwewillnotmanage
tocoveritall,assomepartsofitsfunctionalityaremoreimportant
forgameprogrammingthanothersthatyoucanlearnonyourown
incaseyoueverneedthem.NowthatyouknowwhatQtis,wecan
proceedwiththenextchapter,whereyouwilllearnhowtoinstall
Qtontoyourdevelopmentmachine.
Installation
Inthischapter,youwilllearnhowtoinstallQtonyour
developmentmachine,includingQtCreator,anIDEtailoredtouse
withQt.YouwillseehowtoconfiguretheIDEforyourneedsand
learnthebasicskillstousethatenvironment.Bytheendofthis
chapter,youwillbeabletoprepareyourworkingenvironmentfor
bothdesktopandembeddedplatformsusingthetoolsincludedin
theQtrelease.
Themaintopicscoveredinthischapterareasfollows:
InstallingQtanditsdevelopertools
MaincontrolsofQtCreator
Qtdocumentation
InstallingtheQtSDK
BeforeyoucanstartusingQtonyourmachine,itneedstobe
downloadedandinstalled.Qtcanbeinstalledusingdedicated
installersthatcomeintwoflavors:theonlineinstaller,which
downloadsalltheneededcomponentsonthefly,andamuchlarger
offlineinstaller,whichalreadycontainsalltherequired
components.Usinganonlineinstalleriseasierforregulardesktop
installs,sowepreferthisapproach.
Timeforaction–InstallingQt
usinganonlineinstaller
AllQtresources,includingtheinstallers,areavailableathttps://qt.io.
ToobtaintheopensourceversionofQt,gotohttps://www.qt.io/download-
open-source/.Thepagesuggeststheonlineinstallerforyourcurrent
operatingsystembydefault,asshowninthefollowingscreenshot.
ClickontheDownloadNowbuttontodownloadtheonlineinstaller,
orclickonViewAllDownloadstoselectadifferentdownload
option:
Whenthedownloadiscompleteruntheinstaller,asshown:
ClickonNexttobegintheinstallationprocess.Ifyouareusinga
proxyserver,clickonSettingsandadjustyourproxyconfiguration.
Then,eitherlogintoyourQtAccountorclickonSkip,ifyoudon't
haveone.
ClickonNextagain,andafterawhileofwaitingasthedownloader
checksremoterepositories,you'llbeaskedfortheinstallationpath.
Ensurethatyouchooseapathwhereyouhavewriteaccessand
enoughfreespace.It'sbesttoputQtintoyourpersonaldirectory,
unlessyourantheinstallerasthesystemadministratoruser.
ClickingonNextagainwillpresentyouwiththechoiceof
componentsthatyouwishtoinstall,asshowninthefollowing
screenshot.Youwillbegivendifferentchoicesdependingonyour
platform:
Beforewecontinue,youneedtochoosewhichQtversionyouwant
toinstall.Werecommendthatyouusethemostrecentstable
version,thatis,thefirstitemundertheQtsection.Ignore
thePreviewsection,asitcontainsprereleasepackagesthatmaybe
unstable.Ifyouwanttobefullyconsistentwiththebook,youcan
chooseQt5.9.0,butit'snotrequired.Theinstalleralsoallowsyou
toinstallmultipleQtversionsatonce.
ExpandthesectioncorrespondingtotheQtversionyouwantto
install,andchoosewhicheverplatformsyouneed.Selectatleast
onedesktopplatformtobeabletobuildandrundesktop
applications.WheninWindows,youhavetomakeadditional
choicesforthedesktopbuilds.Selectthe32-bitor64-bitversion
andchoosethecompileryouwanttobeworkingwith.Ifyouhavea
MicrosoftC++compiler(providedwithVisualStudioorVisualC++
BuildTools),youcanselectthebuildcorrespondingtotheinstalled
MSVCversion.Ifyoudon'thaveaMicrosoftcompileroryousimply
don'twanttouseit,choosetheMinGWbuildandselectthe
correspondingMinGWversionintheToolssectionofthepackage
tree.
IfyouwanttobuildAndroidapplications,choosetheoption
correspondingtothedesiredAndroidplatform.InWindows,you
canselectaUWPbuildtocreateUniversalWindowsPlatform
applications.
TheinstallerwillalwaysinstallQtCreator—theIDE(integrated
developmentenvironment)optimizedforcreatingQtapplications.
YoumayalsoselectQtadd-onsthatyouwanttouse.
AfterchoosingtherequiredcomponentsandclickingonNextagain,
youwillhavetoacceptthelicensingtermsforQtbymarkingan
appropriatechoice,asshowninthefollowingscreenshot:
AfteryouclickonInstall,theinstallerwillbegindownloadingand
installingtherequiredpackages.Oncethisisdone,yourQt
installationwillbeready.Attheendoftheprocess,youwillbe
givenanoptiontolaunchQtCreator:
Whatjusthappened?
TheprocesswewentthroughresultsinthewholeQtinfrastructure
appearingonyourdisk.Youcanexaminethedirectoryyoupointed
totheinstallertoseethatitcreatedanumberofsubdirectoriesin
thisdirectory,oneforeachversionofQtchosenwiththeinstaller,
andanotheronecalledToolsthatcontainsQtCreator.TheQt
directoryalsocontainsaMaintenanceToolexecutable,whichallowsyou
toadd,remove,andupdatetheinstalledcomponents.Thedirectory
structureensuresthatifyoueverdecidetoinstallanotherversionof
Qt,itwillnotconflictwithyourexistinginstallation.Furthermore,
foreachversion,youcanhaveanumberofplatformsubdirectories
thatcontaintheactualQtinstallationsforparticularplatforms.
QtCreator
NowthatQtisinstalled,wewillgetfamiliarwithQtCreatorand
useittoverifytheinstallation.
QtCreator'smodes
AfterQtCreatorstarts,youshouldbepresentedwiththefollowing
screen:
Thepanelontheleftallowsyoutoswitchbetweendifferentmodes
oftheIDE:
Welcomemode:Allowsyoutoquicklyopenlastsessions,
projects,loadexamples,andtutorials.
Editmode:Themainmodeusedtoeditthesourcecodeof
yourapplications.
Designmode:Containsavisualformeditor.Designmodeis
automaticallyactivatedwhenyoucreateoropenaQt
Widgetsformfile(.ui)oraQMLformfile(.ui.qml).
Debugmode:Automaticallyactivatedwhenyoulaunchthe
applicationunderdebugger.Itcontainsadditionalviewsfor
displayingthecallstack,thebreakpointlist,andvaluesof
localvariables.Moreviews(suchasthreadlistsorvaluesof
registers)canbeenabledwhenneeded.
Projectsmode:AllowsyoutoconfigurehowQtCreatorwill
buildandrunyourapplication.Forexample,youcanchoose
whichQtversionitwilluseoraddcommand-linearguments
here.
Helpmode:ProvidesaccesstotheQtdocumentation.We
willfocusonthistopiclaterinthechapter.
Settingupcompilers,Qt
versions,andkits
BeforeQtCreatorcanbuildandrunprojects,itneedstoknow
whichQtbuilds,compilers,debuggers,andothertoolsare
available.Fortunately,Qtinstallerwillusuallydoitautomatically,
andQtCreatorisabletoautomaticallydetecttoolsthatare
availablesystem-wide.However,let'sverifythatourenvironmentis
properlyconfigured.FromtheToolsmenu,chooseOptions.Oncea
dialogboxpopsup,chooseBuild&Runfromthesidelist.Thisis
theplacewherewecanconfigurethewayQtCreatorcanbuildour
projects.Acompletebuildconfigurationiscalledakit.Itconsistsof
aQtinstallationandacompilerthatwillbeexecutedtoperformthe
build.YoucanseetabsforallthethreeentitiesintheBuild&Run
sectionoftheOptionsdialogbox.
Let'sstartwiththeCompilerstab.Ifyourcompilerwasnot
autodetectedproperlyandisnotinthelist,clickontheAddbutton,
chooseyourcompilertypefromthelist,andfillthenameandpath
tothecompiler.Ifthesettingswereenteredcorrectly,Creatorwill
autofillalltheotherdetails.Then,youcanclickonApplytosavethe
changes.
Next,youcanswitchtotheQtVersionstab.Again,ifyourQt
installationwasnotdetectedautomatically,youcanclickonAdd.
ThiswillopenafiledialogboxwhereyouwillneedtofindyourQt
installation'sdirectory,whereallthebinaryexecutablesarestored
(usuallyinthebindirectory),andselectabinarycalledqmake.Qt
Creatorwillwarnyouifyouchooseawrongfile.Otherwise,yourQt
installationandversionshouldbedetectedproperly.Ifyouwant,
youcanadjusttheversionnameintheappropriatebox.
ThelasttabtolookatistheKitstab.Itallowsyoutopairacompiler
withtheQtversiontobeusedforcompilation.Inadditiontothis,
forembeddedandmobileplatforms,youcanspecifyadeviceto
deploytoandasysrootdirectorycontainingallthefilesneededto
buildthesoftwareforthespecifiedembeddedplatform.Checkthat
thenameofeachkitisdescriptiveenoughsothatyouwillbeableto
selectthecorrectkit(orkits)foreachofyourapplications.If
needed,adjustthenamesofthekits.
Timeforaction–Loadingan
exampleproject
ExamplesareagreatwaytoexplorethecapabilitiesofQtandfind
thecoderequiredforsometypicaltasks.EachQtversioncontainsa
largesetofexamplesthatarealwaysuptodate.QtCreatorprovides
aneasywaytoloadandcompileanyexampleproject.
Let'stryloadingonetogetfamiliarwithQtCreator'sprojectediting
interface.Then,wewillbuildtheprojecttocheckwhetherthe
installationandconfigurationweredonecorrectly.
InQtCreator,clickontheWelcomebuttoninthetop-leftcornerof
thewindowtoswitchtotheWelcomemode.ClickontheExamples
button(refertothepreviousscreenshot)toopenthelistof
exampleswithasearchbox.Ensurethatthekitthatyouwanttouse
ischoseninthedrop-downlistnexttothesearchbox.Inthebox,
enterafftofilterthelistofexamplesandclickonAffine
Transformationstoopentheproject.Ifyouareaskedwhetheryou
wanttocopytheprojecttoanewfolder,agree.
Afterselectinganexample,anadditionalwindowappearsthat
containsthedocumentationpageoftheloadedexample.Youcan
closethatwindowwhenyoudon'tneedit.Switchbacktothemain
QtCreatorwindow.
QtCreatorwilldisplaytheConfigureProjectdialogwiththelistof
availablekits:
Verifythatthekitsyouwanttousearemarkedwithcheckboxes,
andclickontheConfigureProjectbutton.QtCreatorwillthen
presentyouwiththefollowingwindow:
ThisistheEditmodeofQtCreator.Let'sgothroughthemost
importantpartsofthisinterface:
Projecttreeislocatedatthetop-leftofthewindow.It
displaysallopenprojectsandthehierarchyoffileswithin
them.Youcandouble-clickonafiletoopenitforediting.
Thecontextmenuofprojects,directories,andfilesinthe
projecttreecontainsalotofusefulfunctions.
Atthebottom-leftofthewindow,there'salistofopen
documents.Thefileselectedinthislistwillappearinthe
codeeditorinthecenterofthewindow.Iftheselectedfileis
aQtDesignerform,QtCreatorwillautomaticallyswitchto
theDesignmode.Eachfileinthelisthasaclosebutton.
TheTypetolocatefieldispresentattheleftofthebottom
panel.Ifyouwanttoquicklynavigatetoanotherfileinthe
project,typethebeginningofitsnameinthefieldandselect
itinthepop-uplist.Specialprefixescanbeusedtoenable
othersearchmodes.Forexample,thecprefixallowsyouto
searchforC++classes.YoucanpressCtrl+Ktoactivate
thisfield.
Thebuttonsatthebottomoftheleftpanelallowyoutobuild
andrunyourcurrentprojectunderdebugger,ornormally.
Thebuttonabovethemdisplaysnamesofthecurrent
projectandthecurrentbuildconfiguration(forexample,
DebugorRelease)andallowsyoutochangethem.
Theoutputpanesappearbelowthecodeeditorwhenyou
selecttheminthebottompanel.TheIssuespanecontains
compilererrorsandotherrelatedmessages.TheSearch
Resultspaneallowsyoutorunatextsearchintheentire
projectandviewitsresults.TheApplicationOutputpane
displaysthetextyourapplicationhasprintedtoitsstandard
output(stderrorstdout).
QtCreatorishighlyconfigurable,soyoucanadjustthelayouttoyourliking.Forexample,
it'spossibletochangethelocationsofpanes,addmorepanes,andchangekeyboard
shortcutsforeveryaction.
Qtdocumentation
Qtprojecthasverythoroughdocumentation.ForeachAPIitem
(class,method,andsoon),thereisasectioninthedocumentation
thatdescribesthatitemandmentionsthingsthatyouneedtoknow.
Therearealsoalotofoverviewpagesdescribingmodulesandtheir
parts.WhenyouarewonderingwhatsomeQtclassormoduleis
madefororhowtouseit,theQtdocumentationisalwaysagood
sourceofinformation.
QtCreatorhasanintegrateddocumentationviewer.Themost
commonlyuseddocumentationfeatureiscontexthelp.Totryitout,
openthemain.cppfile,setthetextcursorinsidetheQApplicationtext,
andpressF1.Thehelpsectionshouldappeartotherightofthecode
editor.ItdisplaysthedocumentationpagefortheQApplicationclass.
ThesameshouldworkforanyotherQtclass,method,macro,and
soon.YoucanclickontheOpeninHelpModebuttonontopofthe
helppagetoswitchtotheHelpmode,whereyouhavemorespace
toviewthepage.
Anotherimportantfeatureisthesearchindocumentationindex.To
dothat,gototheHelpmodebyclickingontheHelpbuttononthe
leftpanel.InHelpmode,inthetop-leftcornerofthewindow,there
isadrop-downlistthatallowsyoutoselectthemodeoftheleft
section:Bookmarks,Contents,Index,orSearch.SelectIndexmode,
inputyourrequestintheLookfor:textfieldandseewhetherthere
areanysearchresultsinthelistbelowthetextfield.Forexample,
trytypingqtcoretosearchfortheQtCoremoduleoverview.Ifthere
areresults,youcanpressEntertoquicklyopenthefirstresultor
double-clickonanyresultinthelisttoopenit.IfmultipleQt
versionsareinstalled,adialogmayappearwhereyouneedtoselect
theQtversionyouareinterestedin.
Laterinthisbook,wewillsometimesrefertoQtdocumentationpagesbytheirnames.You
Laterinthisbook,wewillsometimesrefertoQtdocumentationpagesbytheirnames.You
canusethemethoddescribedpreviouslytoopenthesepagesinQtCreator.
Timeforaction–Runningthe
AffineTransformationsproject
Let'strybuildingandrunningtheprojecttocheckwhetherthe
buildingenvironmentisconfiguredproperly.Tobuildtheproject,
clickonthehammericon(Build)atthebottomoftheleftpanel.At
therightofthebottompanel,agreyprogressbarwillappearto
indicatethebuildprogress.Whenthebuildfinishes,theprogress
barturnsgreenifthebuildwassuccessfulorredotherwise.After
theapplicationwasbuilt,clickonthegreentriangleicontorunthe
project.
QtCreatorcanautomaticallysaveallfilesandbuildtheprojectbeforerunningit,soyou
canjusthittheRun(Ctrl+R)orStartDebugging(F5)buttonaftermakingchangestothe
project.Toverifythatthisfeatureisenabled,clickonToolsandOptionsinthemainmenu,
gototheBuild&Runsection,gototheGeneraltab,andcheckthattheSaveallfilesbefore
build,Alwaysbuildprojectbeforedeployingit,andAlwaysdeployprojectbeforerunning
itoptionsarechecked.
Ifeverythingworks,aftersometime,theapplicationshouldbe
launched,asshowninthenextscreenshot:
Whatjusthappened?
Howexactlywastheprojectbuilt?Toseewhichkitandwhichbuild
configurationwasused,clickontheiconintheactionbardirectly
overthegreentriangleicontoopenthebuildconfigurationpopup,
asshowninthefollowingscreenshot:
Theexactcontentthatyougetvariesdependingonyour
installation,butingeneral,ontheleft,youwillseethelistofkits
configuredfortheprojectandontheright,youwillseethelistof
buildconfigurationsdefinedforthatkit.Youcanclickontheselists
toquicklyswitchtoadifferentkitoradifferentbuildconfiguration.
Ifyourprojectisconfiguredonlyforonekit,thelistofkitswillnot
appearhere.
Whatifyouwanttouseanotherkitorchangehowexactlythe
projectisbuilt?Asmentionedearlier,thisisdoneintheProjects
mode.IfyougotothismodebypressingtheProjectsbuttononthe
leftpanel,QtCreatorwilldisplaythecurrentbuildconfiguration,as
showninthefollowingscreenshot:
Theleftpartofthiswindowcontainsalistofallkits.Kitsthatare
notconfiguredtobeusedwiththisprojectaredisplayedingray
color.Youcanclickonthemtoenablethekitforthecurrent
project.Todisableakit,choosetheDisableKitoptioninitscontext
menu.
Undereachenabledkit,therearetwosectionsoftheconfiguration.
TheBuildsectioncontainssettingsrelatedtobuildingtheproject:
Shadowbuildisabuildmodethatplacesalltemporarybuild
filesinaseparatebuilddirectory.Thisallowsyoutokeep
thesourcedirectorycleanandmakesyoursourcefileseasier
totrack(especiallyifyouuseaversioncontrolsystem).This
modeisenabledbydefault.
Builddirectoryisthelocationoftemporarybuildfiles(only
ifshadowbuildisenabled).Eachbuildconfigurationofthe
projectneedsaseparatebuilddirectory.
TheBuildstepssectiondisplayscommandsthatwillberun
toperformtheactualbuildingoftheproject.Youcanedit
command-lineargumentsoftheexistingstepsandadd
custombuildsteps.Bydefault,thebuildprocessconsistsof
twosteps:qmake(Qt'sprojectmanagementtooldescribedin
thepreviouschapter)readstheproject's.profileand
producesamakefile,andthensomevariationofmaketool
(dependingontheplatform)readsthemakefileand
executesQt'sspecialcompilers,theC++compiler,andthe
linker.Formoreinformationaboutqmake,lookuptheqmake
Manualinthedocumentationindex.
TheBuildenvironmentsectionallowsyoutoviewand
changeenvironmentvariablesthatwillbeavailabletothe
buildtools.
Mostvariationsofthemaketool(includingmingw32-make)acceptthe-jnum_corescommand-
lineargumentthatallowsmaketospawnmultiplecompilerprocessesatthesametime.Itis
highlyrecommendedthatyousetthisargument,asitcandrasticallyreducecompilation
timeforbigprojects.Todothis,clickonDetailsattherightpartoftheMakebuildstepand
input-jnum_corestotheMakeargumentsfield(replacenum_coreswiththeactualnumber
ofprocessorcoresonyoursystem).However,MSVCnmakedoesnotsupportthisfeature.To
fixthisissue,Qtprovidesareplacementtoolcalledjomthatsupportsit.
Therecanbemultiplebuildconfigurationsforeachkit.Bydefault,
threeconfigurationsaregenerated:Debug(requiredforthe
debuggertoworkproperly),Profile(usedforprofiling),andRelease
(thebuildwithmoreoptimizationsandnodebuginformation).
TheRunsectiondetermineshowtheexecutableproducedbyyour
projectwillbestarted.Here,youcanchangeyourprogram's
command-linearguments,workingdirectory,andenvironment
variables.Youcanaddmultiplerunconfigurationsandswitch
betweenthemusingthesamebuttonthatallowsyoutochoosethe
currentkitandbuildconfiguration.
Inmostcasesfordesktopandmobileplatforms,thebinaryreleaseofQtyoudownload
fromthewebpageissufficientforallyourneeds.However,forembeddedsystems,
especiallyforARM-basedsystems,thereisnobinaryreleaseavailable,oritistooheavy
resourcewiseforsuchalightweightsystem.Fortunately,Qtisanopensourceproject,so
youcanalwaysbuilditfromsources.Qtallowsyoutochoosethemodulesyouwanttouse
andhasmanymoreconfigurationoptions.Formoreinformation,lookupBuildingQt
Sourcesinthedocumentationindex.
Summary
Bynow,youshouldbeabletoinstallQtonyourdevelopment
machine.YoucannowuseQtCreatortobrowsetheexisting
examplesandlearnfromthemortoreadtheQtreferencemanual
togainadditionalknowledge.Youshouldhaveabasic
understandingofQtCreator'smaincontrols.Inthenextchapter,
wewillfinallystartusingtheframework,andyouwilllearnhowto
creategraphicaluserinterfacesbyimplementingourveryfirst
simplegame.
QtGUIProgramming
ThischapterwillhelpyoulearnhowtouseQttodevelop
applicationswithagraphicaluserinterfaceusingtheQtCreator
IDE.WewillgetfamiliarwiththecoreQtfunctionality,widgets,
layouts,andthesignalsandslotsmechanismthatwewilllateruse
tocreatecomplexsystemssuchasgames.Wewillalsocoverthe
variousactionsandresourcesystemsofQt.Bytheendofthis
chapter,youwillbeabletowriteyourownprogramsthat
communicatewiththeuserthroughwindowsandwidgets.
Themaintopicscoveredinthischapterareaslisted:
Windowsandwidgets
CreatingaQtWidgetsprojectandimplementingatic-tac-
toegame
Creatingwidgetswithorwithoutthevisualformeditor
Usinglayoutstoautomaticallypositionwidgets
Creatingandusingsignalsandslots
UsingtheQtresourcesystem
CreatingGUIinQt
AsdescribedinChapter1,IntroductiontoQt,Qtconsistsofmultiple
modules.Inthischapter,youwilllearnhowtousetheQtWidgets
module.Itallowsyoutocreateclassicdesktopapplications.The
userinterface(UI)oftheseapplicationsconsistsofwidgets.
AwidgetisafragmentoftheUIwithaspecificlookandbehavior.
Qtprovidesalotofbuilt-inwidgetsthatarewidelyusedin
applications:labels,textboxes,checkboxes,buttons,andsoon.
EachofthesewidgetsisrepresentedasaninstanceofaC++class
derivedfromQWidgetandprovidesmethodsforreadingandwriting
thewidget'scontent.Youmayalsocreateyourownwidgetswith
customcontentandbehavior.
ThebaseclassofQWidgetisQObject—themostimportantQtclassthat
containsmultipleusefulfeatures.Inparticular,itimplements
parent–childrelationshipsbetweenobjects,allowingyouto
organizeacollectionofobjectsinyourprogram.Eachobjectcan
haveaparentobjectandanarbitrarynumberofchildren.Makinga
parent–childrelationshipbetweentwoobjectshasmultiple
consequences.Whenanobjectisdeleted,allitschildrenwillbe
automaticallydeletedaswell.Forwidgets,thereisalsoarulethata
childoccupiesanareawithintheboundariesofitsparent.For
example,atypicalformincludesmultiplelabels,inputfields,and
buttons.Eachoftheform'selementsisawidget,andtheformis
theirparentwidget.
Eachwidgethasaseparatecoordinatesystemthatisusedfor
paintingandeventhandlingwithinthewidget.Bydefault,the
originofthiscoordinatesystemisplacedinitstop-leftcorner.The
child'scoordinatesystemisrelativetoitsparent.
Anywidgetthatisnotincludedintoanotherwidget(thatis,
anytop-levelwidget)becomesawindow,andthedesktopoperating
systemwillprovideitwithawindowframe,whichusuallyusually
allowstheusertodragaround,resize,andclosethewindow
(althoughthepresenceandcontentofthewindowframecanbe
configured).
Timeforaction–CreatingaQt
Widgetsproject
ThefirststeptodevelopanapplicationwithQtCreatoristocreatea
projectusingoneofthetemplatesprovidedbytheIDE.
FromtheFilemenuofQtCreator,chooseNewFileorProject.
Thereareanumberofprojecttypestochoosefrom.Followthe
givenstepsforcreatingaQtDesktopproject:
1. Forawidget-basedapplication,choosetheApplication
groupandtheQtWidgetsApplicationtemplate,asshownin
thefollowingscreenshot:
2. Thenextstepistochooseanameandlocationforyournew
project:
3. Wewillcreateasimpletic-tac-toegame,sowewillname
ourprojecttictactoeandprovideanicelocationforit.
Ifyouhaveacommondirectorywhereyouputallyourprojects,youcanticktheUseas
defaultprojectlocationcheckboxforQtCreatortorememberthelocationandsuggestit
thenexttimeyoustartanewproject.
4. Next,youneedtoselectthekit(ormultiplekits)youwantto
usewiththeproject.SelecttheDesktopQtkitcorresponding
totheQtversionyouwanttouse:
5. Nowyouwillbepresentedwiththeoptionofcreatingthe
firstwidgetforyourproject.Wewanttocreateawidgetthat
willrepresentthemainwindowofourapplication,sowecan
leavetheClassnameandBaseclassfieldsunchanged.We
alsowanttousethevisualformeditortoeditthecontentof
themainwindow,soGenerateformshouldalsobeleft
checked:
6. Then,clickonNextandFinish.
Whatjusthappened?
Creatorcreatedanewsubdirectoryinthedirectorythatyou
previouslychoseforthelocationoftheproject.Thisnewdirectory
(theprojectdirectory)nowcontainsanumberoffiles.Youcan
usetheProjectspaneofQtCreatortolistandopenthesefiles(refer
toChapter2,Installation,foranexplanationofQtCreator'sbasic
controls).Let'sgothroughthesefiles.
Themain.cppfilecontainsanimplementationofthemain()function,
theentrypointoftheapplication,asthefollowingcodeshows:
#include"mainwindow.h"
#include<QApplication>
intmain(intargc,char*argv[])
{
QApplicationa(argc,argv);
MainWindoww;
w.show();
returna.exec();
}
Themain()functioncreatesaninstanceoftheQApplicationclassand
feedsitwithvariablescontainingthecommand-linearguments.
Then,itinstantiatesourMainWindowclass,callsitsshowmethod,and
finally,returnsavaluereturnedbytheexecmethodofthe
applicationobject.
QApplicationisasingletonclassthatmanagesthewholeapplication.
Inparticular,itisresponsibleforprocessingeventsthatcomefrom
withintheapplicationorfromexternalsources.Foreventstobe
processed,aneventloopneedstoberunning.Theloopwaitsfor
incomingeventsanddispatchesthemtoproperroutines.Most
thingsinQtaredonethroughevents:inputhandling,redrawing,
receivingdataoverthenetwork,triggeringtimers,andsoon.Thisis
thereasonwesaythatQtisanevent-orientedframework.Without
anactiveeventloop,theeventhandlingwouldnotfunction
properly.Theexec()callinQApplication(or,tobemorespecific,inits
baseclass—QCoreApplication)isresponsibleforenteringthemainevent
loopoftheapplication.Thefunctiondoesnotreturnuntilyour
applicationrequeststheeventlooptobeterminated.Whenthat
eventuallyhappens,themainfunctionreturnsandyourapplication
ends.
Themainwindow.handthemainwindow.cppfilesimplementtheMainWindow
class.Fornow,thereisalmostnocodeinit.Theclassisderived
fromQMainWindow(which,inturn,isderivedfromQWidget),soitinherits
alotofmethodsandbehaviorfromitsbaseclass.Italsocontainsa
Ui::MainWindow*uifield,whichisinitializedintheconstructorand
deletedinthedestructor.Theconstructoralsocallstheui-
>setupUi(this);function.
Ui::MainWindowisanautomaticallygeneratedclass,sothereisno
declarationofitinthesourcecode.Itwillbecreatedinthebuild
directorywhentheprojectisbuilt.Thepurposeofthisclassistoset
upourwidgetandfillitwithcontentbasedonchangesintheform
editor.TheautomaticallygeneratedclassisnotaQWidget.Infact,it
containsonlytwomethods:setupUi,whichperformstheinitialsetup,
andretranslateUi,whichupdatesvisibletextwhentheUIlanguageis
changed.Allwidgetsandotherobjectsaddedintheformeditorare
availableaspublicfieldsoftheUi::MainWindowclass,sowecanaccess
themfromwithintheMainWindowmethodasui->objectName.
mainwindow.uiisaformfilethatcanbeeditedinthevisualformeditor.
IfyouopenitinQtCreatorbydouble-clickingonitintheProjects
pane,QtCreatorwillswitchtotheDesignmode.Ifyouswitchback
totheEditmode,youwillseethatthisfileisactuallyanXMLfile
containingthehierarchyandpropertiesofallobjectseditedin
Designmode.Duringthebuildingoftheproject,aspecialtool
calledtheUserInterfaceCompilerconvertsthisXMLfiletothe
implementationoftheUi::MainWindowclassusedintheMainWindowclass.
Notethatyoudon'tneedtoedittheXMLfilebyhandoreditanycodeintheUi::MainWindow
class.MakingchangesinthevisualeditorisenoughtoapplythemtoyourMainWindowclass
andmaketheform'sobjectsavailabletoit.
Thefinalfilethatwasgeneratediscalledtictactoe.proandisthe
projectconfigurationfile.Itcontainsalltheinformationthatis
requiredtobuildyourprojectusingthetoolsthatQtprovides.Let's
analyzethisfile(lessimportantdirectivesareomitted):
QT+=coregui
greaterThan(QT_MAJOR_VERSION,4):QT+=widgets
TARGET=tictactoe
TEMPLATE=app
SOURCES+=main.cppmainwindow.cpp
HEADERS+=mainwindow.h
FORMS+=mainwindow.ui
ThefirsttwolinesenableQt'score,gui,andwidgetsmodules.The
TEMPLATEvariableisusedtospecifythatyourprojectfiledescribesan
application(asopposedto,forexample,alibrary).TheTARGET
variablecontainsthenameoftheproducedexecutable(tictactoe).
Thelastthreelineslistallfilesthatshouldbeusedtobuildthe
project.
Infact,qmakeenablesQtCoreandQtGUImodulesbydefault,evenifyoudon'tspecifythem
explicitlyintheprojectfile.Youcanoptoutofusingadefaultmoduleifyouwant.For
example,youcandisableQtGUIbyaddingQT-=guitotheprojectfile.
Beforeweproceed,let'stellthebuildsystemthatwewanttouse
C++11features(suchaslambdaexpressions,scopedenumerations,
andrange-basedforloops)inourprojectbyaddingthefollowing
linetotictactoe.pro:
CONFIG+=c++11
Ifwedothis,theC++compilerwillreceiveaflagindicatingthat
C++11supportshouldbeenabled.Thismaynotbeneededifyour
compilerhasC++11supportenabledbydefault.Ifyouwishtouse
C++14instead,useCONFIG+=c++14.
WhatwehavenowisacompleteQtWidgetsproject.Tobuildand
runit,simplychoosetheRunentryfromtheBuilddrop-down
menuorclickonthegreentriangleiconontheleft-handsideofthe
QtCreatorwindow.Afterawhile,youshouldseeawindowpopup.
Sincewedidn'taddanythingtothewindow,itisblank:
Designmodeinterface
Openthemainwindow.uifileandexamineQtCreator'sDesignmode:
TheDesignmodeconsistsoffivemajorparts(theyaremarkedon
thisscreenshot):
Thecentralarea(1)isthemainworksheet.Itcontainsa
graphicalrepresentationoftheformbeingdesignedwhere
youcanmovewidgetsaround,composethemintolayouts,
andseehowtheyreact.Italsoallowsfurthermanipulation
oftheformusingthepoint-and-clickmethodthatwewill
learnlater.
Thetoolbox(2)islocatedintheleftpartofthewindow.It
containsalistofavailabletypesofwidgetthatarearranged
intogroupscontainingitemswitharelatedorsimilar
functionality.Overthelist,youcanseeaboxthatletsyou
filterwidgetsthataredisplayedinthelisttoshowonlythose
thatmatchtheenteredexpression.Atthebeginningofthe
list,therearealsoitemsthatarenotreallywidgets—one
groupcontainslayouts,andtheotheronecontainsso-called
spacers,whichareawaytopushotheritemsawayfromeach
otherorcreateanemptyspaceinlayouts.Themainpurpose
ofthetoolboxistoadditemstotheformintheworksheet.
Youcandothatbygrabbingawidgetfromthelistwiththe
mouse,draggingittothewidgetinthecentralarea,and
releasingthemousebutton.
Thetwotabs(3)inthelowerpartofthewindow—Action
EditorandSignal/SlotEditor—allowustocreatehelper
entitiessuchasactionsforthemenusandtoolbarsorsignal-
slotconnectionsbetweenwidgets.
Theobjecttree(4)issituatedinthetop-rightcornerand
containsthehierarchytreeoftheform'sitems.Theobject
nameandclassnameofeachitemaddedtotheformis
displayedinthetree.Thetopmostitemcorrespondstothe
formitself.Youcanuseboththecentralareaandtheobject
treetoselecttheexistingitemsandaccesstheircontext
menu(forexample,ifyouwanttodeleteanitem,youcan
selecttheRemove...optioninthecontextmenu).
Thepropertyeditor(5)islocatedinthebottom-right
corner.Itallowsyoutoviewandchangethevaluesofallthe
propertiesoftheitemcurrentlyselectedinthecentralarea
andtheobjecttree.Propertiesaregroupedbytheirclasses
thattheyhavebeendeclaredin,startingfromQObject(the
baseclassimplementingproperties),whichdeclaresonly
one,butanimportant,property—objectName.FollowingQObject,
therearepropertiesdeclaredinQWidget,whichisadirect
descendantofQObject.Theyaremainlyrelatedtothe
geometryandlayoutpoliciesofthewidget.Furtherdown
thelist,youcanfindpropertiesthatcomefromfurther
derivationsofQWidget,downtotheconcreteclassofthe
selectedwidget.TheFilterfieldabovethepropertiescan
helpyoufindtheneededpropertyquickly.
Takingacloserlookatthepropertyeditor,youcanseethatsomeof
themhave arrows,whichrevealnewrowswhenclicked.Theseare
composedpropertieswherethecompletepropertyvalueis
determinedfrommorethanonesubpropertyvalue;forexample,if
thereisapropertycalledgeometrythatdefinesarectangle,itcanbe
expandedtoshowfoursubproperties:x,y,width,andheight.Another
thingthatyoumayquicklynoteisthatsomepropertynamesare
displayedinbold.Thismeansthatthepropertyvaluewasmodified
andisdifferentfromthedefaultvalueforthisproperty.Thislets
youquicklyfindthepropertiesthatyouhavemodified.
Ifyouchangedaproperty'svaluebutdecidedtosticktothedefaultvaluelater,youshould
clickonthecorrespondinginputfieldandthenclickonthesmallbuttonwithanarrowto
itsright: .Thisisnotthesameassettingtheoriginalvaluebyhand.Forexample,ifyou
examinethe
spacingpropertyofsomelayouts,itwouldappearasifithadsomeconstantdefaultvalue
for(example,6).However,theactualdefaultvaluedependsonthestyletheapplication
usesandmaybedifferentonadifferentoperatingsystem,sotheonlywaytosetthe
defaultvalueistousethededicatedbuttonandensurethatthepropertyisnotdisplayedin
boldanymore.
Ifyoupreferapurelyalphabeticalorderwherepropertiesarenot
groupedbytheirclass,youcanswitchtheviewusingapop-up
menuthatbecomesavailableafteryouclickonthewrenchicon
positionedoverthepropertylist;however,onceyougetfamiliar
withthehierarchyofQtclasses,itwillbemucheasiertonavigate
thelistwhenitissortedbyclassaffinity.
Whatwasdescribedhereisthebasictoollayout.Ifyoudon'tlikeit,
youcaninvokethecontextmenufromthemainworksheet,
unchecktheAutomaticallyHideViewTitleBarsentry,andusethe
titlebarsthatappeartore-arrangeallthepanestoyourliking,or
evenclosetheonesyoudon'tcurrentlyneed.
Nowthatyouarefamiliarwiththestructureofthevisualform
editor,youcanfinallyaddsomecontenttoourwidget.Weare
makingatic-tac-toegamewithlocalmultiplayer,soweneedsome
wayofdisplayingwhichofthetwoplayerscurrentlymoves.Let's
putthegameboardinthecenterofthewindowanddisplaythe
namesoftheplayersaboveandbelowtheboard.Whenaplayer
needstomove,wewillmakethecorrespondingname'sfontbold.
Wealsoneedabuttonthatwillstartanewgame.
Timeforaction–Adding
widgetstotheform
LocatetheLabeliteminthetoolbox(it'sintheDisplayWidgets
category)anddragittoourform.Usethepropertyeditortoset
theobjectNamepropertyofthelabeltoplayer1Name.objectNameisaunique
identifierofaformitem.Theobjectnameisusedasthenameofthe
publicfieldintheUi::MainWindowclass,sothelabelwillbeavailableas
ui->player1NameintheMainWindowclass(andwillhaveaQLabel*type).
Then,locatethetextpropertyinthepropertyeditor(itwillbein
theQLabelgroup,asitistheclassthatintroducestheproperty)and
setittoPlayer1.Youwillseethatthetextinthecentralareawillbe
updatedaccordingly.Addanotherlabel,setitsobjectNametoplayer2Name
anditstexttoPlayer2.
YoucanselectawidgetinthecentralareaandpresstheF2keytoeditthetextinplace.
Anotherwayistodouble-clickonthewidgetintheform.Itworksforanywidgetthatcan
displaytext.
DragaPushButton(fromtheButtonsgroup)totheformanduse
theF2keytorenameittoStartnewgame.Ifthenamedoesnotfitin
thebutton,youcanresizeitusingthebluerectanglesonitsedges.
SettheobjectNameofthebuttontostartNewGame.
Thereisnobuilt-inwidgetforourgameboard,sowewillneedto
createacustomwidgetforitlater.Fornow,wewilluseanempty
widget.LocateWidgetintheContainersgroupofthetoolboxand
dragittotheform.SetitsobjectNametogameBoard:
Layouts
Ifyoubuildandruntheprojectnow,youwillseethewindowwith
twolabelsandabutton,buttheywillremainintheexactpositions
youleftthem.Thisiswhatyoualmostneverwant.Usually,itis
desiredthatwidgetsareautomaticallyresizedbasedontheir
contentandthesizeoftheirneighbors.Theyneedtoadjusttothe
changesofthewindow'ssize(or,incontrast,thewindowsizemay
needtoberestrictedbasedonpossiblesizesofthewidgetsinsideof
it).Thisisaveryimportantfeatureforacross-platformapplication,
asyoucannotassumeanyparticularscreenresolutionorsizeof
controls.InQt,allofthisrequiresustouseaspecialmechanism
calledlayouts.
Layoutsallowustoarrangethecontentofawidget,ensuringthat
itsspaceisusedefficiently.Whenwesetalayoutonawidget,we
canstartaddingwidgets,andevenotherlayouts,andthe
mechanismwillresizeandrepositionthemaccordingtotherules
thatwespecify.Whensomethinghappensintheuserinterfacethat
influenceshowwidgetsshouldbedisplayed(forexample,thelabel
textisreplacedwithlongertext,whichmakesthelabelrequire
morespacetoshowitscontent),thelayoutistriggeredagain,which
recalculatesallpositionsandsizesandupdateswidgets,as
necessary.
Qtcomeswithapredefinedsetoflayoutsthatarederivedfromthe
QLayoutclass,butyoucanalsocreateyourown.Theonesthatwe
alreadyhaveatourdisposalareQHBoxLayoutandQVBoxLayout,which
positionitemshorizontallyandvertically;QGridLayout,whicharranges
itemsinagridsothatanitemcanspanacrosscolumnsorrows;
andQFormLayout,whichcreatestwocolumnsofitemswithitem
descriptionsinonecolumnanditemcontentintheother.Thereis
alsoQStackedLayout,whichisrarelyuseddirectlyandwhichmakesone
oftheitemsassignedtoitpossessalltheavailablespace.Youcan
seethemostcommonlayoutsinactioninthefollowingfigure:
Timeforaction–Addinga
layouttotheform
SelecttheMainWindowtop-levelitemintheobjecttreeandclick
on ,theLayOutVerticallyiconintheuppertoolbar.The
button,labels,andtheemptywidgetwillbeautomaticallyresizedto
takealltheavailablespaceoftheforminthecentralarea:
Iftheitemswerearrangedinadifferentorder,youcandragand
dropthemtochangetheorder.
Runtheapplicationandcheckthatthewindow'scontentsare
automaticallypositionedandresizedtousealltheavailablespace
whenthewindowisresized.Unfortunately,thelabelstakemore
verticalspacethantheyreallyrequire,resultinginanemptyspace
intheapplicationwindow.Wewillfixthisissuelaterinthischapter
whenwelearnaboutsizepolicies.
Youcantestthelayoutsofyourformwithoutbuildingandrunningthewholeapplication.
OpentheToolsmenu,gototheFormEditorsubmenu,andchoosethePreviewentry.You
willseeanewwindowopenthatlooksexactlyliketheformwejustdesigned.Youcan
resizethewindowandinteractwiththeobjectsinsidetomonitorthebehaviorofthe
layoutsandwidgets.WhatreallyhappenedhereisthatQtCreatorbuiltarealwindowfor
usbasedonthedescriptionthatweprovidedinalltheareasofthedesignmode.Without
anycompilation,inablinkofaneye,wereceivedafullyworkingwindowwithallthe
layoutsworkingandallthepropertiesadjustedtoourliking.Thisisaveryimportanttool,
soensurethatyouuseitoftentoverifythatyourlayoutsarecontrollingallthewidgetsas
youintendedthemto—itismuchfasterthancompilingandrunningthewholeapplication
justtocheckwhetherthewidgetsstretchorsqueezeproperly.Youcanalsoresizetheform
inthecentralareaoftheformeditorbydraggingitsbottom-rightcorner,andifthe
layoutsaresetupcorrectly,thecontentsshouldberesizedandrepositioned.
Nowthatyoucancreateanddisplayaform,twoimportant
operationsneedtobeimplemented.First,youneedtoreceive
notificationswhentheuserinteractswithyourform(forexample,
pressesabutton)toperformsomeactionsinthecode.Second,you
needtochangethepropertiesoftheform'scontents
programmatically,andfillitwithrealdata(forexample,setplayer
namesfromthecode).
Signalsandslots
Totriggerfunctionalityasaresponsetosomethingthathappensin
anapplication,Qtusesamechanismofsignalsandslots.Thisis
anotherimportantfeatureoftheQObjectclass.It'sbasedon
connectinganotification(whichQtcallsasignal)aboutachange
ofstateinsomeobjectwithafunctionormethod(calledaslot)
thatisexecutedwhensuchanotificationarises.Forexample,ifa
buttonispressed,itemits(sends)aclicked()signal.Ifsomemethod
isconnectedtothissignal,themethodwillbecalledwheneverthe
buttonispressed.
Signalscanhaveargumentsthatserveasapayload.Forexample,
aninputboxwidget(QLineEdit)hasatextEdited(constQString&text)signal
that'semittedwhentheusereditsthetextintheinputbox.Aslot
connectedtothissignalwillreceivethenewtextintheinputboxas
itsargument(providedithasanargument).
Signalsandslotscanbeusedwithallclassesthat
inheritQObject(includingallwidgets).Asignalcanbeconnectedtoa
slot,memberfunction,orfunctor(whichincludesaregularglobal
function).Whenanobjectemitsasignal,anyoftheseentitiesthat
areconnectedtothatsignalwillbecalled.Asignalcanalsobe
connectedtoanothersignal,inwhichcaseemittingthefirstsignal
willmaketheothersignalbeemittedaswell.Youcanconnectany
numberofslotstoasinglesignalandanynumberofsignalstoa
singleslot.
Creatingsignalsandslots
IfyoucreateaQObjectsubclass(oraQWidgetsubclass,asQWidget
inheritsQObject),youcanmarkamethodofthisclassasasignalor
aslot.Iftheparentclasshadanysignalsornon-privateslots,your
classwillalsoinheritthem.
Inorderforsignalsandslotstoworkproperly,theclassdeclaration
mustcontaintheQ_OBJECTmacroinaprivatesectionofitsdefinition
(QtCreatorhasgenerateditforus).Whentheprojectisbuilt,a
specialtoolcalledMeta-ObjectCompiler(moc)willexamine
theclass'sheaderandgeneratesomeextracodenecessaryfor
signalsandslotstoworkproperly.
KeepinmindthatmocandallotherQtbuildtoolsdonotedittheprojectfiles.YourC++
filesarepassedtothecompilerwithoutanychanges.Allspecialeffectsareachievedby
generatingseparateC++filesandaddingthemtothecompilationprocess.
Asignalcanbecreatedbydeclaringaclassmethodin
thesignalssectionoftheclassdeclaration:
signals:
voidvalueChanged(intnewValue);
However,wedon'timplementsuchamethod;thiswillbedone
automaticallybymoc.Wecansend(emit)thesignalbycallingthe
method.Thereisaconventionthatasignalcallshouldbepreceded
bytheemitmacro.Thismacrohasnoeffect(it'sactuallyablank
macro),butithelpsusclarifyourintenttoemitthesignal:
voidMyClass::setValue(intnewValue){
m_value=newValue;
emitvalueChanged(newValue);
}
Youshouldonlyemitsignalsfromwithintheclassmethods,asifit
wereaprotectedfunction.
Slotsareclassmethodsdeclaredintheprivateslots,protectedslots,
orpublicslotssectionoftheclassdeclaration.Contrarytosignals,
slotsneedtobeimplemented.Qtwillcalltheslotwhenasignal
connectedtoitisemitted.Thevisibilityoftheslot(private,
protected,orpublic)shouldbechosenusingthesameprinciplesas
fornormalmethods.
TheC++standardonlydescribesthreetypesofsectionsoftheclassdefinition
(private,protected,andpublic),soyoumaywonderhowthesespecialsectionswork.They
areactuallysimplemacros:thesignals
macroexpandstopublic,andslotsisablankmacro.So,thecompilertreatsthemas
normalmethods.Thesekeywordsare,however,usedbymoctodeterminehowto
generatetheextracode.
Connectingsignalsandslots
Signalsandslotscanbeconnectedanddisconnecteddynamically
usingtheQObject::connect()andQObject::disconnect()functions.Aregular,
signal-slotconnectionisdefinedbythefollowingfourattributes:
Anobjectthatchangesitsstate(sender)
Asignalinthesenderobject
Anobjectthatcontainsthefunctiontobecalled(receiver)
Aslotinthereceiver
Ifyouwanttomaketheconnection,youneedtocall
theQObject::connectfunctionandpassthesefourparameterstoit.For
example,thefollowingcodecanbeusedtocleartheinputbox
wheneverthebuttonisclickedon:
connect(button,&QPushButton::clicked,
lineEdit,&QLineEdit::clear);
SignalsandslotsinthiscodearespecifiedusingastandardC++
featurecalledpointerstomemberfunctions.Suchapointer
containsthenameoftheclassandthenameofthemethod(inour
case,signalorslot)inthatclass.QtCreator'scodeautocompletion
willhelpyouwriteconnectstatements.Inparticular,ifyoupress
Ctrl+Spaceafter
connect(button,&,itwillinsertthenameoftheclass,andifyoudothat
afterconnect(button,&QPushButton::,itwillsuggestoneoftheavailable
signals(inanothercontext,itwouldsuggestalltheexisting
methodsoftheclass).
Notethatyoucan'tsettheargumentsofsignalsorslotswhen
makingaconnection.Argumentsofthesourcesignalarealways
determinedbythefunctionthatemitsthesignal.Argumentsofthe
receivingslot(orsignal)arealwaysthesameastheargumentsof
thesourcesignal,withtwoexceptions:
Ifthereceivingslotorsignalhasfewerargumentsthanthe
sourcesignal,theremainingargumentsareignored.For
example,ifyouwanttousethevalueChanged(int)signalbut
don'tcareaboutthepassedvalue,youcanconnectthis
signaltoaslotwithoutarguments.
Ifthetypesofthecorrespondingargumentsarenotthe
same,butanimplicitconversionbetweenthemexists,that
conversionisperformed.Thismeansthatyoucan,for
example,connectasignalcarryingadoublevaluewithaslot
takinganintparameter.
Ifthesignalandtheslotdonothavecompatiblesignatures,you
willgetacompile-timeerror.
Anexistingconnectionisautomaticallydestroyedafterthesender
orthereceiverobjectsaredeleted.Manualdisconnectionisrarely
needed.Theconnect()functionreturnsaconnectionhandlethatcan
bepassedtodisconnect().Alternatively,youcancalldisconnect()with
thesameargumentstheconnect()wascalledwithtoundothe
connection.
Youdon'talwaysneedtodeclareaslottoperformaconnection.It's
possibletoconnectasignaltoastandalonefunction:
connect(button,&QPushButton::clicked,someFunction);
Thefunctioncanalsobealambdaexpression,inwhichcaseitis
possibletowritethecodedirectlyintheconnectstatement:
connect(pushButton,&QPushButton::clicked,[]()
{
qDebug()<<"clicked!";
});
Itcanbeusefulifyouwanttoinvokeaslotwithafixedargument
valuethatcan'tbecarriedbyasignalbecauseithaslessarguments.
Asolutionistoinvoketheslotfromalambdafunction(ora
standalonefunction):
connect(pushButton,&QPushButton::clicked,[label]()
{
label->setText("buttonwasclicked");
});
Afunctioncanevenbereplacedwithafunctionobject(functor).To
dothis,wecreateaclass,forwhichweoverloadthecalloperator
thatiscompatiblewiththesignalthatwewishtoconnectto,as
showninthefollowingcodesnippet:
classFunctor{
public:
Functor(constQString&name):m_name(name){}
voidoperator()(booltoggled)const{
qDebug()<<m_name<<":buttonstatechangedto"<<toggled;
}
private:
QStringm_name;
};
intmain(intargc,char*argv[])
{
QApplicationa(argc,argv);
QPushButton*button=newQPushButton();
button->setCheckable(true);
QObject::connect(button,&QPushButton::toggled,
Functor("myfunctor"));
button->show();
returna.exec();
}
Thisisoftenanicewaytoexecuteaslotwithanadditional
parameterthatisnotcarriedbythesignal,asthisismuchcleaner
thanusingalambdaexpression.However,keepinmindthat
automaticdisconnectionwillnothappenwhentheobject
referencedinthelambdaexpressionorthefunctorisdeleted.This
canleadtoause-after-freebug.
WhileitisactuallypossibletoconnectasignaltoamethodofaQObject-basedclassthatis
notaslot,doingthisisnotrecommended.Declaringthemethodasaslotshowsyourintent
better.Additionally,methodsthatarenotslotsarenotavailabletoQtatruntime,whichis
requiredinsomecases.
Oldconnectsyntax
BeforeQt5,theoldconnectsyntaxwastheonlyoption.Itlooksas
follows:
connect(spinBox,SIGNAL(valueChanged(int)),
dial,SLOT(setValue(int)));
Thisstatementestablishesaconnectionbetweenthesignalof
thespinBoxobjectcalledvalueChangedthatcarriesanintparameterand
asetValueslotinthedialobjectthatacceptsanintparameter.Itis
forbiddentoputargumentnamesorvaluesina
connectstatement.QtCreatorisusuallyabletosuggestallpossible
inputsinthiscontextifyoupressCtrl+SpaceafterSIGNAL(orSLOT(.
Whilethissyntaxisstillavailable,wediscourageitswideuse,
becauseithasthefollowingdrawbacks:
Ifthesignalortheslotisincorrectlyreferenced(for
example,itsnameorargumenttypesareincorrect)orif
argumenttypesofthesignalsandtheslotarenot
compatible,therewillbenocompile-timeerror,onlya
runtimewarning.Thenewsyntaxapproachperformsallthe
necessarychecksatcompiletime.
Theoldsyntaxdoesn'tsupportcastingargumentvaluesto
anothertype(forexample,connectasignalcarrying
adoublevaluewithaslottakinganintparameter).
Theoldsyntaxdoesn'tsupportconnectingasignaltoa
standalonefunction,alambdaexpression,orafunctor.
Theoldsyntaxalsousesmacrosandmaylookuncleartodevelopers
notfamiliarwithQt.It'shardtosaywhichsyntaxiseasiertoread
(theoldsyntaxdisplaysargumenttypes,whilethenewsyntax
displaystheclassnameinstead).However,thenewsyntaxhasabig
disadvantagewhenusingoverloadedsignalsorslots.Theonlyway
toresolvetheoverloadedfunctiontypeistouseanexplicitcast:
connect(spinBox,
static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged),
...);
Theoldconnectsyntaxincludesargumenttypes,soitdoesn'thave
thisissue.Inthiscase,theoldsyntaxmaylookmoreacceptable,but
compile-timechecksmaystillbeconsideredmorevaluablethan
shortercode.Inthisbook,wepreferthenewsyntax,butusetheold
syntaxwhenworkingwithoverloadedmethodsforthesakeof
clarity.
Signalandslotaccess
specifiers
Asmentionedearlier,youshouldonlyemitsignalsfromtheclass
thatownsitorfromitssubclasses.However,ifsignalswerereally
protectedorprivate,youwouldnotbeabletoconnecttothemusing
thepointer-to-memberfunctionsyntax.Tomakesuchconnections
possible,signalsaremadepublicfunctions.Thismeansthatthe
compilerwon'tstopyoufromcallingthesignalfromoutside.Ifyou
wanttopreventsuchcalls,youcandeclareQPrivateSignalasthelast
argumentofthesignal:
signals:
voidvalueChanged(intvalue,QPrivateSignal);
QPrivateSignalisaprivatestructcreatedineachQObjectsubclassbythe
Q_OBJECTmacro,soyoucanonlycreateQPrivateSignalobjectsinthe
currentclass.
Slotscanbepublic,protected,orprivate,dependingonhowyou
wanttorestrictaccesstothem.Whenusingthepointertoa
memberfunctionsyntaxforconnection,youwillonlybeableto
createpointerstoslotsifyouhaveaccesstothem.It'salsocorrectto
callaslotdirectlyfromanyotherlocationaslongasyouhaveaccess
toit.
Thatbeingsaid,Qtdoesn'treallysupportrestrictingaccessto
signalsandslots.Regardlessofhowasignaloraslotisdeclared,
youcanalwaysaccessitusingtheoldconnectsyntax.Youcanalso
callanysignalorslotusingtheQMetaObject::invokeMethodmethod.While
youcanrestrictdirectC++callstoreducethepossibilityoferrors,
keepinmindthattheusersofyourAPIstillcanaccessanysignalor
Timeforaction–Receivingthe
button-clicksignalfromthe
form
Openthemainwindow.hfileandcreateaprivateslotssectionintheclass
declaration,thendeclarethestartNewGame()privateslot,asshownin
thefollowingcode:
classMainWindow:publicQMainWindow
{
Q_OBJECT
public:
explicitMainWindow(QWidget*parent=nullptr);
~MainWindow();
privateslots:
voidstartNewGame();
}
Toquicklyimplementafreshlydeclaredmethod,wecanaskQt
Creatortocreatetheskeletoncodeforusbypositioningthetext
cursoratthemethoddeclaration,pressingAlt+Enteronthe
keyboard,andchoosingAdddefinitionintictactoewidget.cppfrom
thepopup.
Italsoworkstheotherwayround.Youcanwritethemethodbodyfirstandthenposition
thecursoronthemethodsignature,pressAlt+Enter,andchooseAdd(...)
declarationfromthequick-fixmenu.Therearealsovariousothercontext-dependentfixes
thatareavailableinCreator.
Writethehighlightedcodeintheimplementationofthismethod:
voidMainWindow::startNewGame()
{
qDebug()<<"buttonclicked!";
}
Add#include<QDebug>tothetopsectionofthemainwindow.cppfiletomake
theqDebug()macroavailable.
Finally,addaconnectstatementtotheconstructorafterthesetupUi()
call:
ui->setupUi(this);
connect(ui->startNewGame,&QPushButton::clicked,
this,&MainWindow::startNewGame);
Runtheapplicationandtryclickingonthebutton.Thebuttonclicked!
textshouldappearintheApplicationOutputpaneinthebottom
partofQtCreator'swindow(ifthepaneisn'tactivated,use
theApplicationOutputbuttoninthebottompaneltoopenit):
Whatjusthappened?
WecreatedanewprivateslotintheMainWindowclassandconnected
theclicked()signaloftheStartnewgamebuttontotheslot.When
theuserclicksonthebutton,Qtwillcallourslot,andthecodewe
wroteinsideitgetsexecuted.
EnsurethatyouputanyoperationswiththeformelementsafterthesetupUi()call.This
functioncreatestheelements,so
ui->startNewGamewillsimplybeuninitializedbeforesetupUi()iscalled,andattemptingto
useitwillresultinundefinedbehavior.
qDebug()<<...isaconvenientwaytoprintdebuginformationto
thestderr(standarderroroutput)oftheapplicationprocess.It's
quitesimilartothestd::cerr<<...methodavailableinthestandard
library,butitseparatessuppliedvalueswithspacesandappendsa
newlineattheend.
Puttingdebugoutputseverywherequicklybecomesinconvenient.Luckily,QtCreatorhas
powerfulintegrationwithC++debuggers,soyoucanuseDebugmodetocheckwhether
someparticularlineisexecuting,seethecurrentvaluesofthelocalvariablesatthat
location,andsoon.Forexample,trysettingabreakpointattheline
containingqDebug()byclickingonthespacetotheleftofthelinenumber(aredcircle
indicatingthebreakpointshouldappear).ClickontheStartDebuggingbutton(agreen
trianglewithabugatthebottom-leftcornerofQtCreator),waitfortheapplicationto
launch,andpresstheStartnewgamebutton.Whentheapplicationentersthebreakpoint
location,itwillpause,andQtCreator'swindowwillbebroughttothefront.Theyellow
arrowoverthebreakpointcirclewillindicatethecurrentstepoftheexecution.Youcanuse
thebuttonsbelowthecodeeditortocontinueexecution,stop,orexecutetheprocessin
steps.Learningtousethedebuggerbecomesveryimportantwhendeveloping
largeapplications.Wewilltalkmoreaboutusingthedebuggerlater(OnlineChapter,https
://www.packtpub.com/sites/default/files/downloads/MiscellaneousandAdvancedConcepts.pdf).
Automaticslotconnectionand
itsdrawbacks
Qtalsooffersaneasierwaytomakeaconnectionbetweensignalsof
theform'selementsandtheslotsoftheclass.Youcanright-clickon
thebuttoninthecentralareaoftheformeditorandselecttheGoto
slot...option.Youwillbepromptedtoselectoneofthesignals
availableinthebutton'sclass(QPushButton).Afteryouselect
theclicked()signal,QtCreatorwillautomaticallyadda
newon_startNewGame_clickedslottoourMainWindowclass.
Thetrickypartisthatthereisnoconnect()callthatenforcesthe
connection.Howisthebutton'ssignalconnectedtothisslot,then?
TheanswerisQt'sautomaticslotconnectionfeature.Whenthe
constructorcallstheui->setupUi(this)function,itcreatesthewidgets
andotherobjectsintheformandthencalls
theQMetaObject::connectSlotsByNamemethod.Thismethodlooksatthelist
ofslotsexistinginthewidgetclass(inourcase,MainWindow)and
searchesforonesthathavetheirnameinanon_<objectname>_<signal
name>
pattern,where<objectname>istheobjectNameofanexistingchildwidget
and<signalname>isthenameofoneofthiswidget'ssignals.Inour
case,abuttoncalledstartNewGameisachildwidgetofourwidget,andit
hasaclickedsignal,sothissignalisautomaticallyconnectedto
anon_startNewGame_clickedslot.
Whilethisisareallyconvenientfeature,ithasmanydrawbacks:
Itmakesyourapplicationhardertomaintain.Ifyourename
orremovetheformelement,youhavetoupdateorremove
theslotmanually.Ifyouforgettodothat,theapplication
willonlyproduceawarningatruntimewhentheautomatic
connectionfails.Inalargeapplication,especiallywhennot
allformsareinstantiatedatthestartoftheapplication,
thereisasignificantriskthatyouwillmissthewarningand
theapplicationwillnotworkasintended.
Youhavetouseaspecificnamefortheslot(forexample,
on_startNewGame_clicked()insteadofaclean-lookingstartNewGame()).
Sometimesyouwanttoconnectsignalsfrommultiple
objectstothesameslot.Automaticslotconnectiondoesn't
provideawaytodothis,andcreatingmultipleslotsjustto
callasinglefunctionwillleadtounnecessarycodebloat.
Automaticslotconnectionhasaruntimecost,becauseit
needstoexaminetheavailablechildrenandslotsandfind
thematchingones,butit'susuallyinsignificantsinceitonly
runswhentheformobjectiscreated.
Thebasicapproachshownintheprevioussectionismuchmore
maintainable.Makinganexplicitconnect()callwithpointersto
memberfunctionswillensurethatbothsignalandslotarespecified
properly.Ifyourenameorremovethebutton,itwillimmediately
resultinacompilationerrorthatisimpossibletomiss.Youarealso
freetochooseameaningfulnamefortheslot,soyoucanmakeit
partofyourpublicAPI,ifdesired.
Consideringallthis,weadviseagainstusingtheautomaticslot
connectionfeature,astheconveniencedoesnotoutweighthe
drawbacks.
Timeforaction–Changingthe
textsonthelabelsfromthe
code
Printingtexttotheconsoleisnotasimpressiveaschangingthetext
inourform.Wedon'thaveGUIforlettingusersentertheirnames
yet,sowe'llhardcodesomenamesfornow.Let'schangethe
implementationofourslottothefollowing:
voidMainWindow::startNewGame()
{
ui->player1Name->setText(tr("Alice"));
ui->player2Name->setText(tr("Bob"));
}
Now,whenyouruntheapplicationandclickonthebutton,the
labelsintheformwillchange.Let'sbreakdownthiscodeinto
pieces:
Asmentionedearlier,thefirstlabel'sobjectisaccessiblein
ourclassasui->player1NameandhastheQLabel*type.
We'recallingthesetTextmethodoftheQLabelclass.Thisisthe
setterofthetextpropertyofQLabel(thesamepropertythatwe
editedinthepropertyeditoroftheDesignmode).Asper
Qt'snamingconvention,gettersshouldhavethesamename
asthepropertyitself,andsettersshouldhaveasetprefix,
followedbythepropertyname.Youcansetthetextcursor
onsetTextandpressF1tolearnmoreaboutthepropertyand
itsaccessfunctions.
Thetr()function(whichisshortfor"translate")isusedto
translatethetexttothecurrentUIlanguageofthe
application.Wewilldescribethetranslationinfrastructure
ofQtinChapter6,QtCoreEssentials.Bydefault,thisfunction
returnsthepassedstringunchanged,butit'sagoodhabitto
wrapanyandallstringliteralsthataredisplayedtotheuser
inthisfunction.Anyuser-visibletextthatyouenterinthe
formeditorisalsosubjecttotranslationandispassed
throughasimilarfunctionautomatically.Onlystringsthat
shouldnotbeaffectedbytranslation(forexample,object
namesthatareusedasidentifiers)shouldbecreated
withoutthetr()function.
Creatingawidgetforthetic-
tac-toeboard
Let'smoveontoimplementingtheboard.Itshouldcontainnine
buttonsthatcandisplay"X"or"O"andallowtheplayerstomake
theirmoves.Wecouldaddthebuttondirectlytotheemptywidget
ofourform.However,thebehavioroftheboardisfairlyseparate
fromtherestoftheform,anditwillhavequiteabitoflogicinside.
Followingtheencapsulationprinciple,wepreferimplementingthe
boardasaseparatewidgetclass.Then,we'llreplacetheempty
widgetinourmainwindowwiththeboardwidgetwecreated.
Choosingbetweendesigner
formsandplainC++classes
OnewayofcreatingacustomwidgetisbyaddingaDesignerForm
Classtotheproject.DesignerFormClassisatemplateprovidedby
QtCreator.ItconsistsofaC++classthatinheritsQWidget(directlyor
indirectly)andadesignerform(.uifile),tiedtogetherbysome
automaticallygeneratedcode.OurMainWindowclassalsofollowsthis
template.
However,ifyoutrytousethevisualformeditortocreateourtic-
tac-toeboard,youmayfinditquiteinconvenientforthistask.One
problemisthatyouneedtoaddnineidenticalbuttonstotheform
manually.Anotherissueisaccessingthesebuttonsfromthecode
whenyouneedtomakeasignalconnectionorchangethebutton's
text.Theui->objectNameapproachisnotapplicableherebecauseyou
canonlyaccessaconcretewidgetthisway,soyou'dhavetoresort
toothermeans,suchasthefindChild()methodthatallowsyouto
searchforachildobjectbyitsname.
Inthiscase,weprefertoaddthebuttonsinthecode,wherewecan
makealoop,setupeachbutton,andputthemintoanarrayforeasy
addressing.Theprocessisprettysimilartohowthedesignerforms
operate,butwe'lldoitbyhand.Ofcourse,anythingthattheform
editorcandoisaccessiblethroughtheAPI.
Afteryoubuildtheproject,youcanholdCtrlandclickonui_mainwindow.hatthebeginning
ofmainwindow.cpptoseethecodethatactuallysetsupourmainwindow.Youshouldnotedit
thisfile,becauseyourchangeswillnotbepersistent.
Timeforaction–Creatinga
gameboardwidget
Locatethetictactoefolderintheprojecttree(it'sthetop-levelentry
correspondingtoourwholeproject),openitscontextmenu,and
selectAddNew...SelectC++intheleftlistandC++Classinthe
centrallist.ClickontheChoosebutton,inputTicTacToeWidgetinthe
Classnamefield,andselectQWidgetintheBaseclassdrop-down
list.ClickonNextandFinish.QtCreatorwillcreateheaderand
sourcefilesforournewclassandaddthemtotheproject.
Openthetictactoewidget.hfileinCreatorandupdateitbyaddingthe
highlightedcode:
#ifndefTICTACTOEWIDGET_H
#defineTICTACTOEWIDGET_H
#include<QWidget>
classTicTacToeWidget:publicQWidget
{
Q_OBJECT
public:
TicTacToeWidget(QWidget*parent=nullptr);
~TicTacToeWidget();
private:
QVector<QPushButton*>m_board;
};
#endif//TICTACTOEWIDGET_H
OuradditionscreateaQVectorobject(acontainersimilarto
std::vector)thatcanholdpointerstoinstancesoftheQPushButtonclass,
whichisthemostcommonlyusedbuttonclassinQt.Wehaveto
includetheQtheadercontainingtheQPushButtondeclaration.Qt
Creatorcanhelpusdothisquickly.SetthetextcursoronQPushButton,
pressAlt+Enter,andselectAdd#include<QPushButton>.The
includedirectivewillappearatthebeginningofthefile.Asyoumay
havenoted,eachQtclassisdeclaredintheheaderfilethatiscalled
exactlythesameastheclassitself.
Fromnowon,thisbookwillnotremindyouaboutaddingtheincludedirectivestoyour
sourcecode—youwillhavetotakecareofthisbyyourself.Thisisreallyeasy;just
rememberthattouseaQtclassyouneedtoincludeafilenamedafterthatclass.
Thenextstepistocreateallthebuttonsandusealayouttomanage
theirgeometries.Switchtothetictactoewidget.cppfileandlocatethe
constructor.
YoucanusetheF4keytoswitchbetweenthecorrespondingheaderandthesourcefiles.
YoucanalsousetheF2keytonavigatefromthedefinitionofamethodtoits
implementation,andback.
First,let'screatealayoutthatwillholdourbuttons:
QGridLayout*gridLayout=newQGridLayout(this);
Bypassingthethispointertothelayout'sconstructor,weattached
thelayouttoourwidget.Then,wecanstartaddingbuttonstothe
layout:
for(introw=0;row<3;++row){
for(intcolumn=0;column<3;++column){
QPushButton*button=newQPushButton("");
gridLayout->addWidget(button,row,column);
m_board.append(button);
}
}
Thecodecreatesaloopoverrowsandcolumnsoftheboard.In
eachiteration,itcreatesaninstanceoftheQPushButtonclass.The
contentofeachbuttonissettoasinglespacesothatitgetsthe
correctinitialsize.Then,weaddthebuttontothelayoutinrowand
column.Attheend,westorethepointertothebuttoninthevector
thatwasdeclaredearlier.Thisletsusreferenceanyofthebuttons
lateron.Theyarestoredinthevectorinsuchanorderthatthefirst
threebuttonsofthefirstrowarestoredfirst,thenthebuttonsfrom
thesecondrow,andfinallythosefromthelastrow.
Thisshouldbeenoughfortestingthewidget.Let'saddittoour
mainwindow.Openthemainwindow.uifile.Invokethecontextmenuof
theemptywidgetcalledgameBoardandchoosePromoteto.Thisallows
ustopromoteawidgettoanotherclass,thatis,substituteawidget
intheformwithaninstanceofanotherclass.
Inourcase,wewillwanttoreplacetheemptywidgetwithourgame
board.SelectQWidgetintheBaseclassnamelist,becauseour
TicTacToeWidgetisinheritedfromQWidget.InputTicTacToeWidgetintothe
PromotedclassnamefieldandverifythattheHeaderfilefield
containsthecorrectnameoftheclass'sheaderfile,asillustrated:
Then,clickonthebuttonlabeledAddandthenPromote,toclose
thedialogandconfirmthepromotion.Youwillnotnoteany
changesintheform,becausethereplacementonlytakesplaceat
runtime(however,youwillseetheTicTacToeWidgetclassnamenextto
gameBoardintheobjecttree).
Runtheapplicationandcheckwhetherthegameboardappearsin
themainwindow:
Whatjusthappened?
Notallwidgettypesaredirectlyavailableintheformdesigner.
Sometimes,weneedtousewidgetclassesthatwillonlybecreated
intheprojectthatisbeingbuilt.Thesimplestwaytobeabletoput
acustomwidgetonaformistoaskthedesignertoreplacetheclass
nameofastandardwidgetwithacustomname.Bypromotingan
objecttoadifferentclass,wesavedalotofworktryingtootherwise
fitourgameboardintotheuserinterface.
Youarenowfamiliarwithtwowaysofcreatingcustomwidgets:you
canusetheformeditororaddwidgetsfromthecode.Both
approachesarevaluable.Whencreatinganewwidgetclassinyour
project,choosethemostconvenientwaydependingonyourcurrent
task.
Automaticdeletionofobjects
Youmighthavenotedthatalthoughwecreatedanumberofobjects
intheconstructorusingthenewoperator,wedidn'tdestroythose
objectsanywhere(forexample,inthedestructor).Thisisbecauseof
thewaythememoryismanagedbyQt.Qtdoesn'tdoanygarbage
collecting(asC#orJavadoes),butithasthisnicefeaturerelatedto
QObjectparent–childhierarchies.TheruleisthatwheneveraQObject
instanceisdestroyed,italsodeletesallofitschildren.Thisis
anotherreasontosetparentstotheobjectsthatwecreate—ifwedo
this,wedon'thavetocareaboutexplicitlyfreeinganymemory.
Sincealllayoutsandwidgetsinsideourtop-levelwidget(an
instanceofMainWindowclass)areitsdirectorindirectchildren,they
willallbedeletedwhenthemainwindowisdestroyed.TheMainWindow
objectiscreatedinthemain()functionwithoutthenewkeyword,soit
willbedeletedattheendoftheapplicationaftera.exec()returns.
Whenworkingwithwidgets,it'sprettyeasytoverifythatevery
objecthasaproperparent.Youcanassumethatanythingthatis
displayedinsidethewindowisadirectorindirectchildofthat
window.However,theparent–childrelationshipbecomesless
apparentwhenworkingwithinvisibleobjects,soyoushouldalways
checkthateachobjecthasaproperparentandthereforewillbe
deletedatsomepoint.Forexample,inourTicTacToeWidgetclass,
thegridLayoutobjectreceivesitsparentthroughaconstructor
argument(this).Thebuttonobjectsareinitiallycreatedwithouta
parent,buttheaddWidget()functionassignsaparentwidgettothem.
Timeforaction–Functionality
ofatic-tac-toeboard
Weneedtoimplementafunctionthatwillbecalleduponby
clickingonanyoftheninebuttonsontheboard.Ithastochange
thetextofthebuttonthatwasclickedon—either"X"or"O"—based
onwhichplayermadethemove.Itthenhastocheckwhetherthe
moveresultedinthegamebeingwonbytheplayer(oradrawifno
moremovesarepossible),andifthegameended,itshouldemitan
appropriatesignal,informingtheenvironmentabouttheevent.
Whentheuserclicksonabutton,theclicked()signalisemitted.
Connectingthissignaltoacustomslotletsusimplementthe
mentionedfunctionality,butsincethesignaldoesn'tcarryany
parameters,howdowetellwhichbuttoncausedtheslottobe
triggered?Wecouldconnecteachbuttontoaseparateslot,but
that'sanuglysolution.Fortunately,therearetwowaysofworking
aroundthisproblem.Whenaslotisinvoked,apointertotheobject
thatcausedthesignaltobesentisaccessiblethroughaspecial
methodinQObject,calledsender().Wecanusethatpointertofindout
whichoftheninebuttonsstoredintheboardlististheonethat
causedthesignaltofire:
voidTicTacToeWidget::someSlot(){
QPushButton*button=static_cast<QPushButton*>(sender());
intbuttonIndex=m_board.indexOf(button);
//...
}
Whilesender()isausefulcall,weshouldtrytoavoiditinourown
codeasitbreakssomeprinciplesofobject-orientedprogramming.
Moreover,therearesituationswherecallingthisfunctionisnot
safe.AbetterwayistouseadedicatedclasscalledQSignalMapper,
whichletsusachieveasimilarresultwithoutusingsender()directly.
ModifytheconstructorofTicTacToeWidget,asfollows:
QGridLayout*gridLayout=newQGridLayout(this);
QSignalMapper*mapper=newQSignalMapper(this);
for(introw=0;row<3;++row){
for(intcolumn=0;column<3;++column){
QPushButton*button=newQPushButton("");
gridLayout->addWidget(button,row,column);
m_board.append(button);
mapper->setMapping(button,m_board.count()-1);
connect(button,SIGNAL(clicked()),mapper,SLOT(map()));
}
}
connect(mapper,SIGNAL(mapped(int)),
this,SLOT(handleButtonClick(int)));
Here,wefirstcreatedaninstanceofQSignalMapperandpasseda
pointertotheboardwidgetasitsparentsothatthemapperis
deletedwhenthewidgetisdeleted.
AlmostallsubclassesofQObjectcanreceiveapointertotheparentobjectinthe
constructor.Infact,ourMainWindowandTicTacToeWidgetclassescanalsodothat,thanksto
thecodeQtCreatorgeneratedintheirconstructors.FollowingthisruleincustomQObject-
basedclassesisrecommended.Whiletheparentargumentisoftenoptional,it'sagoodidea
topassitwhenpossible,becauseobjectswillbeautomaticallydeletedwhentheparentis
deleted.However,thereareafewcaseswherethisisredundant,forexample,whenyou
addawidgettoalayout,thelayoutwillautomaticallysettheparentwidgetforit.
Then,whenwecreatebuttons,we"teach"themapperthateachof
thebuttonshasanumberassociatedwithit—thefirstbuttonwill
havethenumber0,thesecondonewillbeboundtothenumber1,
andsoon.Byconnectingtheclicked()signalfromthebuttontothe
mapper'smap()slot,wetellthemappertoprocessthatsignal.When
themapperreceivesthesignalfromanyofthebuttons,itwillfind
themappingofthesenderofthesignalandemitanothersignal
—mapped()—withthemappednumberasitsparameter.Thisallowsus
toconnecttothatsignalwithanewslot(handleButtonClick())thattakes
theindexofthebuttonintheboardlist.
Beforewecreateandimplementtheslot,weneedtocreateauseful
enumtypeandafewhelpermethods.First,addthefollowingcode
tothepublicsectionoftheclassdeclarationinthetictactoewidget.h
file:
enumclassPlayer{
Invalid,Player1,Player2,Draw
};
Q_ENUM(Player)
Thisenumletsusspecifyinformationaboutplayersinthegame.
TheQ_ENUMmacrowillmakeQtrecognizetheenum(forexample,it
willallowyoutopassthevaluesofthistypetoqDebug()andalsomake
serializationeasier).Generally,it'sagoodideatouseQ_ENUMforany
enuminaQObject-basedclass.
WecanusethePlayerenumimmediatelytomarkwhosemoveitis
now.Todoso,addaprivatefieldtotheclass:
Playerm_currentPlayer;
Don'tforgettogivethenewfieldaninitialvalueintheconstructor:
m_currentPlayer=Player::Invalid;
Then,addthetwopublicmethodstomanipulatethevalueofthis
field:
PlayercurrentPlayer()const
{
returnm_currentPlayer;
}
voidsetCurrentPlayer(Playerp)
{
if(m_currentPlayer==p){
return;
}
m_currentPlayer=p;
emitcurrentPlayerChanged(p);
emitcurrentPlayerChanged(p);
}
Thelastmethodemitsasignal,sowehavetoaddthesignal
declarationtotheclassdefinitionalongwithanothersignalthatwe
willuse:
signals:
voidcurrentPlayerChanged(Player);
voidgameOver(Player);
WeonlyemitthecurrentPlayerChangedsignalwhenthecurrentplayerreallychanges.You
alwayshavetopayattentionthatyoudon'temita"changed"signalwhenyousetavalue
toafieldtothesamevaluethatithadbeforethefunctionwascalled.Usersofyourclasses
expectthatifasignaliscalledchanged,itisemittedwhenthevaluereallychanges.
Otherwise,thiscanleadtoaninfiniteloopinsignalemissionsifyouhavetwoobjectsthat
connecttheirvaluesetterstotheotherobject'schangedsignal.
Nowitistimetoimplementtheslotitself.First,declareitinthe
headerfile:
privateslots:
voidhandleButtonClick(intindex);
UseAlt+Entertoquicklygenerateadefinitionforthenew
method,aswedidearlier.
Whenanyofthebuttonsispressed,thehandleButtonClick()slotwillbe
called.Theindexofthebuttonclickedonwillbereceivedasthe
argument.Wecannowimplementtheslotinthe.cppfile:
voidTicTacToeWidget::handleButtonClick(intindex)
{
if(m_currentPlayer==Player::Invalid){
return;//gameisnotstarted
}
if(index<0||index>=m_board.size()){
return;//outofboundscheck
}
QPushButton*button=m_board[index];
if(button->text()!="")return;//invalidmove
button->setText(currentPlayer()==Player::Player1?"X":"O");
Playerwinner=checkWinCondition();
Playerwinner=checkWinCondition();
if(winner==Player::Invalid){
setCurrentPlayer(currentPlayer()==Player::Player1?
Player::Player2:Player::Player1);
return;
}else{
emitgameOver(winner);
}
}
Here,wefirstretrieveapointertothebuttonbasedonitsindex.
Then,wecheckwhetherthebuttoncontainsanemptyspace—ifnot,
thenit'salreadyoccupied,sowereturnfromthemethodsothatthe
playercanpickanotherfieldintheboard.Next,wesetthecurrent
player'smarkonthebutton.Then,wecheckwhethertheplayerhas
wonthegame.Ifthegamedidn'tend,weswitchthecurrentplayer
andreturn;otherwise,weemitagameOver()signal,tellingour
environmentwhowonthegame.ThecheckWinCondition()method
returnsPlayer1,Player2,orDrawifthegamehasended,andInvalid
otherwise.Wewillnotshowtheimplementationofthismethod
here,asitisquitelengthy.Tryimplementingitonyourown,andif
youencounterproblems,youcanseethesolutioninthecode
bundlethataccompaniesthisbook.
Thelastthingweneedtodointhisclassistoaddanotherpublic
methodforstartinganewgame.Itwillcleartheboardandsetthe
currentplayer:
voidTicTacToeWidget::initNewGame(){
for(QPushButton*button:m_board){
button->setText("");
}
setCurrentPlayer(Player::Player1);
}
Nowweonlyneedtocallthismethodin
theMainWindow::startNewGamemethod:
voidMainWindow::startNewGame()
{
{
ui->player1Name->setText(tr("Alice"));
ui->player2Name->setText(tr("Bob"));
ui->gameBoard->initNewGame();
}
Notethatui->gameBoardactuallyhasaTicTacToeWidget*type,andwecan
callitsmethodseventhoughtheformeditordoesn'tknowanything
specificaboutourcustomclass.Thisistheresultofthepromoting
thatwedidearlier.
It'stimetoseehowallthisworkstogether!Runtheapplication,
clickontheStartnewgamebutton,andyoushouldbeabletoplay
sometic-tac-toe.
Timeforaction–Reactingto
thegameboard'ssignals
Whilewritingaturn-basedboardgame,itisagoodideatoalways
clearlymarkwhoseturnitisnowtomakeamove.Wewilldothis
bymarkingthemovingplayer'snameinbold.Thereisalreadya
signalintheboardclassthattellsusthatthecurrentplayerhas
changed,whichwecanreacttoupdatethelabels.
Weneedtoconnecttheboard'scurrentPlayerChangedsignaltoanewslot
intheMainWindowclass.Let'saddappropriatecodeintotheMainWindow
constructor:
ui->setupUi(this);
connect(ui->gameBoard,&TicTacToeWidget::currentPlayerChanged,
this,&MainWindow::updateNameLabels);
Now,fortheslotitself,declarethefollowingmethodsin
theMainWindowclass:
private:
voidsetLabelBold(QLabel*label,boolisBold);
privateslots:
voidupdateNameLabels();
Nowimplementthemusingthefollowingcode:
voidMainWindow::setLabelBold(QLabel*label,boolisBold)
{
QFontf=label->font();
f.setBold(isBold);
label->setFont(f);
}
voidMainWindow::updateNameLabels()
{
setLabelBold(ui->player1Name,
ui->gameBoard->currentPlayer()==
TicTacToeWidget::Player::Player1);
setLabelBold(ui->player2Name,
ui->gameBoard->currentPlayer()==
TicTacToeWidget::Player::Player2);
}
Whatjusthappened?
QWidget(and,byextension,anywidgetclass)hasafontpropertythat
determinesthepropertiesofthefontthiswidgetuses.Thisproperty
hastheQFonttype.Wecan'tjustwritelabel->font()->setBold(isBold);,
becausefont()returnsaconstreference,sowehavetomakeacopy
oftheQFontobject.Thatcopyhasnoconnectiontothelabel,sowe
needtocalllabel->setFont(f)toapplyourchanges.Toavoidrepetition
ofthisprocedure,wecreatedahelperfunction,calledsetLabelBold.
Thelastthingthatneedstobedoneistohandlethesituationwhen
thegameends.ConnectthegameOver()signalfromtheboardtoanew
slotinthemainwindowclass.Implementtheslotasfollows:
voidMainWindow::handleGameOver(TicTacToeWidget::Playerwinner){
QStringmessage;
if(winner==TicTacToeWidget::Player::Draw){
message=tr("Gameendedwithadraw.");
}else{
QStringwinnerName=winner==
TicTacToeWidget::Player::Player1?
ui->player1Name->text():ui->player2Name->text();
message=tr("%1wins").arg(winnerName);
}
QMessageBox::information(this,tr("Info"),message);
}
Thiscodecheckswhowonthegame,assemblesthemessage(we
willlearnmoreaboutQStringinChapter6,QtCoreEssentials),and
showsitusingastaticmethodQMessageBox::information()thatshowsa
modaldialogcontainingthemessageandabuttonthatallowsusto
closethedialog.
Runthegameandcheckthatitnowhighlightsthecurrentplayer
andshowsthemessagewhenthegameends.
Advancedformeditorusage
Nowit'stimetogivetheplayersawaytoinputtheirnames.Wewill
dothatbyaddingagameconfigurationdialogthatwillappearwhen
startinganewgame.
Timeforaction–Designingthe
gameconfigurationdialog
First,selectAddNew...inthecontextmenuofthetictactoeproject
andchoosetocreateanewQtDesignerFormClass,asshowninthe
followingscreenshot:
Inthewindowthatappears,chooseDialogwithButtonsBottom:
AdjusttheclassnametoConfigurationDialog,leavetherestofthe
settingsattheirdefaultvalues,andcompletethewizard.Thefiles
thatappearintheproject(.cpp,.h,and.ui)areverysimilartothe
filesgeneratedfortheMainWindowclasswhenwecreatedourproject.
TheonlydifferenceisthatMainWindowusesQMainWindowasitsbaseclass,
andConfigurationDialogusesQDialog.Also,aMainWindowinstanceiscreated
inthemainfunction,soitshowswhentheapplicationisstarted,
whilewe'llneedtocreateaConfigurationDialoginstancesomewhereelse
inthecode.QDialogimplementsbehaviorthatiscommonfordialogs;
inadditiontothemaincontent,itdisplaysoneormultiplebuttons.
Whenthedialogisselected,theusercaninteractwiththedialog
andthenpressoneofthebuttons.Afterthis,thedialogisusually
destroyed.QDialoghasaconvenientexec()methodthatdoesn'treturn
untiltheusermakesachoice,andthenitreturnsinformationabout
thepressedbutton.Wewillseethatinactionafterwefinish
creatingthedialog.
Draganddroptwolabelsandtwolineeditsontheform,position
themroughlyinagrid,double-clickoneachofthelabels,and
adjusttheircaptionstoreceivearesultsimilartothefollowing:
ChangetheobjectNamepropertyofthelineeditstoplayer1Nameand
player2Name.Then,clickonsomeemptyspaceintheformandchoose
theLayoutinagridentryintheuppertoolbar.Youshouldseethe
widgetssnapintoplace—that'sbecauseyouhavejustapplieda
layouttotheform.OpentheToolsmenu,gototheFormEditor
submenu,andchoosethePreviewentrytopreviewtheform.
Acceleratorsandlabelbuddies
Now,wewillfocusongivingthedialogsomemorepolish.Thefirst
thingwewilldoisaddacceleratorstoourwidgets.Theseare
keyboardshortcutsthat,whenactivated,causeparticularwidgetsto
gainkeyboardfocusorperformapredeterminedaction(for
example,toggleacheckboxorpushabutton).Acceleratorsare
usuallymarkedbyunderliningthem,asfollows:
Wewillsetacceleratorstoourlineeditssothatwhentheuser
activatesanacceleratorforthefirstfield,itwillgainfocus.Through
this,wecanenterthenameofthefirstplayer,and,similarly,when
theacceleratorforthesecondlineeditistriggered,wecanstart
typinginthenameforthesecondplayer.
Startbyselectingthefirstlabelontheleft-handsideofthefirstline
edit.PressF2andchangethetexttoPlayer&AName:.The&character
marksthecharacterdirectlyafteritasanacceleratorforthewidget.
Acceleratorsmaynotworkwithdigitsonsomeplatforms,sowe
decidedtousealetterinstead.Similarly,renamethesecondlabel
toPlayer&BName:.
Forwidgetsthatarecomposedofbothtextandtheactual
functionality(forexample,abutton),thisisenoughtomake
acceleratorswork.However,sinceQLineEditdoesnothaveanytext
associatedwithit,wehavetouseaseparatewidgetforthat.Thisis
whywehavesettheacceleratoronthelabel.Nowweneedto
associatethelabelwiththelineeditsothattheactivationofthe
label'sacceleratorwillforwardittothewidgetofourchoice.Thisis
donebysettingaso-calledbuddyforthelabel.Youcandothisin
codeusingthesetBuddymethodoftheQLabelclassorusingCreator's
formdesigner.Sincewe'realreadyintheDesignmode,we'llusethe
latterapproach.Forthat,weneedtoactivateadedicatedmodein
theformdesigner.
LookattheupperpartofCreator'swindow;directlyabovetheform,
youwillfindatoolbarcontainingacoupleoficons.Clickontheone
labeledEditbuddies .Now,movethemousecursoroverthelabel,
pressthemousebutton,anddragfromthelabeltowardtheline
edit.Whenyoudragthelabeloverthelineedit,you'llseea
graphicalvisualizationofaconnectionbeingsetbetweenthelabel
andthelineedit.Ifyoureleasethebuttonnow,theassociationwill
bemadepermanent.Youshouldnotethatwhensuchanassociation
ismade,theampersandcharacter(&)vanishesfromthelabel,and
thecharacterbehinditgetsanunderscore.Repeatthisfortheother
labelandcorrespondinglineedit.ClickontheEditwidgets
buttonabovetheformtoreturntheformeditortothedefault
mode.Now,youcanpreviewtheformagainandcheckwhether
acceleratorsworkasexpected;pressingAlt+AandAlt+Bshould
setthetextcursortothefirstandsecondtextfield,respectively.
Thetaborder
Whileyou'repreviewingtheform,youcancheckanotheraspectof
theUIdesign.Notewhichlineeditreceivesthefocuswhentheform
isopen.Thereisachancethatthesecondlineeditwillbeactivated
first.Tocheckandmodifytheorderoffocus,closethepreviewand
switchtothetabordereditingmodebyclickingontheiconcalled
EditTabOrder inthetoolbar.
Thismodeassociatesaboxwithanumbertoeachfocusablewidget.
Byclickingontherectangleintheorderyouwishthewidgetsto
gainfocus,youcanreordervalues,thusre-orderingfocus.Now
makeitsothattheorderisasshownhere:
Ourformonlyhastwowidgetsthatcanreceivefocus(exceptforthe
dialog'sbuttons,buttheirtaborderismanagedautomatically).If
youcreateaformwithmultiplecontrols,thereisagoodchancethat
whenyoupresstheTabkeyrepeatedly,thefocuswillstartjumping
backandforthbetweenbuttonsandlineeditsinsteadofalinear
progressfromtoptobottom(whichisanintuitiveorderforthis
particulardialog).Youcanusethismodetocorrectthetaborder.
Enterthepreviewagainandcheckwhetherthefocuschanges
accordingtowhatyou'veset.
Whendecidingaboutthetaborder,itisgoodtoconsiderwhichfieldsinthedialogare
mandatoryandwhichareoptional.Itisagoodideatoallowtheusertotabthroughallthe
mandatoryfieldsfirst,thentothedialogconfirmationbutton(forexample,onethatsays
OKorAccept),andthencyclethroughalltheoptionalfields.Thankstothis,theuserwillbe
abletoquicklyfillallthemandatoryfieldsandacceptthedialogwithouttheneedtocycle
throughalltheoptionalfieldsthattheuserwantstoleaveastheirdefaultvalues.
Timeforaction–Public
interfaceofthedialog
Thenextthingtodoistoallowtostoreandreadplayernamesfrom
outsidethedialog—sincetheuicomponentisprivate,thereisno
accesstoitfromoutsidetheclasscode.Thisisacommonsituation
andonethatQtisalsocompliantwith.Eachdatafieldinalmost
everyQtclassisprivateandmaycontainaccessors(agetterand
optionallyasetter),whicharepublicmethodsthatallowustoread
andstorevaluesfordatafields.Ourdialoghastwosuchfields—the
namesforthetwoplayers.
NamesofsettermethodsinQtareusuallystartedwithset,followed
bythenameofthepropertywiththefirstletterconvertedto
uppercase.Inoursituation,thetwosetterswillbecalled
setPlayer1NameandsetPlayer2Name,andtheywillbothacceptQStringand
returnvoid.Declarethemintheclassheader,asshowninthe
followingcodesnippet:
voidsetPlayer1Name(constQString&p1name);
voidsetPlayer2Name(constQString&p2name);
Implementtheirbodiesinthe.cppfile:
voidConfigurationDialog::setPlayer1Name(constQString&p1name)
{
ui->player1Name->setText(p1name);
}
voidConfigurationDialog::setPlayer2Name(constQString&p2name)
{
ui->player2Name->setText(p2name);
}
GettermethodsinQtareusuallycalledthesameastheproperty
thattheyarerelatedto—player1Nameandplayer2Name.Putthefollowing
codeintheheaderfile:
QStringplayer1Name()const;
QStringplayer2Name()const;
Putthefollowingcodeintheimplementationfile:
QStringConfigurationDialog::player1Name()const
{
returnui->player1Name->text();
}
QStringConfigurationDialog::player2Name()const
{
returnui->player2Name->text();
}
Ourdialogisnowready.Let'suseitin
theMainWindow::startNewGamefunctiontorequestplayernamesbefore
startingthegame:
ConfigurationDialogdialog(this);
if(dialog.exec()==QDialog::Rejected){
return;//donothingifdialogrejected
}
ui->player1Name->setText(dialog.player1Name());
ui->player2Name->setText(dialog.player2Name());
ui->gameBoard->initNewGame();
Inthisslot,wecreatethesettingsdialogandshowittotheuser,
forcingthemtoenterplayernames.Theexec()functiondoesn't
returnuntilthedialogisacceptedorcancelled.Ifthedialogwas
canceled,weabandonthecreationofanewgame.Otherwise,we
askthedialogforplayernamesandsetthemonappropriatelabels.
Finally,weinitializetheboardsothatuserscanplaythegame.The
dialogobjectwascreatedwithoutthenewkeyword,soitwillbe
deletedimmediatelyafterthis.
Nowyoucanruntheapplicationandseehowtheconfiguration
dialogworks.
Polishingtheapplication
Wehaveimplementedalltheimportantfunctionalitiesofourgame,
andnowwewillstartimprovingitbyexploringotherQtfeatures.
Sizepolicies
Ifyouchangetheheightofthemainwindowofourgame,youwill
notethatdifferentwidgetsareresizedinadifferentway.In
particular,buttonsretaintheiroriginalheight,andlabelsgain
emptyfieldstothetopandbottomofthetext:
ThisisbecauseeachwidgethasapropertycalledsizePolicy,which
decideshowawidgetistoberesizedbyalayout.Youcanset
separatesizepoliciesforhorizontalandverticaldirections.Abutton
hasaverticalsizepolicyofFixedbydefault,whichmeansthatthe
heightofthewidgetwillnotchangefromthedefaultheight
regardlessofhowmuchspacethereisavailable.Alabelhas
aPreferredsizepolicybydefault.Thefollowingaretheavailablesize
policies:
Ignored:Inthis,thedefaultsizeofthewidgetisignoredand
thewidgetcanfreelygrowandshrink
Fixed:Inthis,thedefaultsizeistheonlyallowedsizeofthe
widget
Preferred:Inthis,thedefaultsizeisthedesiredsize,butboth
smallerandbiggersizesareacceptable
Minimum:Inthis,thedefaultsizeisthesmallestacceptablesize
forthewidget,butthewidgetcanbemadelargerwithout
hurtingitsfunctionality
Maximum:Inthis,thedefaultsizeisthelargestsizeofthe
widget,andthewidgetcanbeshrunk(eventonothing)
withouthurtingitsfunctionality
Expanding:Inthis,thedefaultsizeisthedesiredsize;asmaller
size(evenzero)isacceptable,butthewidgetisableto
increaseitsusefulnesswhenmoreandmorespaceis
assignedtoit
MinimumExpanding:ThisisacombinationofMinimumandExpanding—
thewidgetisgreedyintermsofspace,anditcannotbemade
smallerthanitsdefaultsize
Howdowedeterminethedefaultsize?Theanswerisbythesize
returnedbythesizeHintvirtualmethod.Forlayouts,thesizeis
calculatedbasedonthesizesandsizepoliciesoftheirchildwidgets
andnestedlayouts.Forbasicwidgets,thevaluereturnedbysizeHint
dependsonthecontentofthewidget.Inthecaseofabutton,ifit
holdsalineoftextandanicon,sizeHintwillreturnthesizethatis
requiredtofullyencompassthetext,icon,somespacebetween
them,thebuttonframe,andthepaddingbetweentheframeand
contentitself.
Inourform,wepreferthatwhenthemainwindowisresized,the
labelswillkeeptheirheight,andthegameboardbuttonswillgrow.
Todothis,openmainwindow.uiintheformeditor,selectthefirstlabel,
andthenholdCtrlandclickonthesecondlabel.Nowbothlabels
areselected,sowecanedittheirpropertiesatthesametime.Locate
sizePolicyinthepropertyeditor(ifyou'rehavingtroublelocatinga
property,usetheFilterfieldabovethepropertyeditor)andexpand
itbyclickingonthetriangletoitsleft.SetVerticalPolicytoFixed.
Youwillseethechangesintheform'slayoutimmediately.
Thebuttonsonthegameboardarecreatedinthecode,sonavigate
totheconstructorofTicTacToeWidgetclassandsetthesizepolicyusing
thefollowingcode:
QPushButton*button=newQPushButton("");
button->setSizePolicy(QSizePolicy::Preferred,
QSizePolicy::Preferred);
Thiswillchangeboththehorizontalandverticalpolicyofbuttonsto
Preferred.Runthegameandobservethechanges:
Protectingagainstinvalidinput
Theconfigurationdialogdidnothaveanyvalidationuntilnow.
Let'smakeitsuchthatthebuttontoacceptthedialogisonly
enabledwhenneitherofthetwolineeditsisempty(thatis,when
boththefieldscontainplayernames).Todothis,weneedto
connectthetextChangedsignalofeachlineedittoaslotthatwill
performthetask.
First,gototheconfigurationdialog.hfileandcreateaprivateslotvoid
updateOKButtonState();intheConfigurationDialogclass(youwillneedtoadd
theprivateslotssectionmanually).Usethefollowingcodeto
implementthisslot:
voidConfigurationDialog::updateOKButtonState()
{
QPushButton*okButton=ui->buttonBox-
>button(QDialogButtonBox::Ok);
okButton->setEnabled(!ui->player1Name->text().isEmpty()&&
!ui->player2Name->text().isEmpty());
}
Thiscodeasksthebuttonboxthatcurrentlycontains
theOKandCancelbuttonstogiveapointertothebuttonthat
acceptsthedialog(wehavetodothatbecausethebuttonsarenot
containedintheformdirectly,sotherearenofieldsfortheminui).
Then,wesetthebutton'senabledpropertybasedonwhetherboth
playernamescontainvalidvaluesornot.
Next,edittheconstructorofthedialogtoconnecttwosignalstoour
newslot.Thebuttonstatealsoneedstobeupdatedwhenwefirst
createthedialog,soaddaninvocationofupdateOKButtonState()tothe
constructor:
ui->setupUi(this);
connect(ui->player1Name,&QLineEdit::textChanged,
this,&ConfigurationDialog::updateOKButtonState);
connect(ui->player2Name,&QLineEdit::textChanged,
this,&ConfigurationDialog::updateOKButtonState);
updateOKButtonState();
Mainmenuandtoolbars
Asyoumayremember,anywidgetthathasnoparentwillbe
displayedasawindow.However,whenwecreatedourmain
window,weselectedQMainWindowasthebaseclass.Ifwehadselected
QWidgetinstead,wewouldstillbeabletodoeverythingwedidupto
thispoint.However,theQMainWindowclassprovidessomeunique
functionalitythatwewillnowuse.
Amainwindowrepresentsthecontrolcenterofanapplication.It
cancontainmenus,toolbars,dockingwidgets,astatusbar,andthe
centralwidgetthatcontainsthemaincontentofthewindow,as
showninthefollowingdiagram:
Ifyouopenthemainwindow.uifileandtakealookattheobjecttree,you
willseethemandatorycentralWidgetthatactuallycontainsourform.
TherearealsooptionalmenuBar,mainToolBar,andstatusBarthatwere
addedautomaticallywhenQtCreatorgeneratedtheform.
Thecentralwidgetpartdoesn'tneedanyextraexplanation;itisa
regularwidgetlikeanyother.Wewillalsonotfocusondock
widgetsorthestatusbarhere.Theyareusefulcomponents,butyou
canlearnaboutthemyourself.Instead,wewillspendsometime
masteringmenusandtoolbars.Youhavesurelyseenandused
toolbarsandmenusinmanyapplications,andyouknowhow
importanttheyareforagooduserexperience.
Themainmenuhasabitofunusualbehavior.It'susually
positionedinthetoppartofthewindow,butinmacOSandsome
Linuxenvironments,themainmenuisseparatedfromthewindow
anddisplayedinthetopareaofthescreen.Toolbars,ontheother
hand,canbemovedfreelybytheuseranddockedhorizontallyor
verticallytothesidesofthemainwindow.
ThemainclasssharedbyboththeseconceptsisQAction,which
representsafunctionalitythatcanbeinvokedbyauser.Asingle
actioncanbeusedinmultipleplaces—itcanbeanentryinamenu
(theQMenuinstances)orinatoolbar(QToolBar),abutton,orakeyboard
shortcut(QShortcut).Manipulatingtheaction(forexample,changing
itstext)causesallitsincarnationstoupdate.Forexample,ifyou
haveaSaveentryinthemenu(withakeyboardshortcutboundto
it),aSaveiconinthetoolbar,andmaybealsoaSavebutton
somewhereelseinyouruserinterfaceandyouwanttodisallow
savingthedocument(forexample,amapinyourdungeonsand
dragonsgameleveleditor)becauseitscontentshaven'tchanged
sincethedocumentwaslastloaded.Inthiscase,ifthemenuentry,
toolbaricon,andbuttonarealllinkedtothesameQActioninstance,
then,onceyousettheenabledpropertyoftheactiontofalse,allthe
threeentitieswillbecomedisabledaswell.Thisisaneasywayto
keepdifferentpartsofyourapplicationinsync—ifyoudisablean
actionobject,youcanbesurethatallentriesthattriggerthe
functionalityrepresentedbytheactionarealsodisabled.Actions
canbeinstantiatedincodeorcreatedgraphicallyusingAction
EditorinQtCreator.Anactioncanhavedifferentpiecesofdata
associatedwithit—atext,tooltip,statusbartip,icons,andothers
thatarelessoftenused.Alltheseareusedbyincarnationsofyour
actions.
Timeforaction–Creatinga
menuandatoolbar
Let'sreplaceourboringStartnewgamebuttonwithamenuentry
andatoolbaricon.First,selectthebuttonandpresstheDeletekey
todeleteit.Then,locateActionEditorinthebottom-centerpartof
theformeditorandclickontheNewbuttononitstoolbar.Enter
thefollowingvaluesinthedialog(youcanfilltheShortcutfieldby
pressingthekeycombinationyouwanttouse):
Locatethetoolbarinthecentralarea(betweentheTypeHeretext
andthefirstlabel)anddragthelinecontainingtheNewGame
actionfromtheactioneditortothetoolbar,whichresultsina
buttonappearinginthetoolbar.
Tocreateamenuforthewindow,double-clickontheTypeHere
textonthetopoftheformandreplacethetextwith&File(although
ourapplicationdoesn'tworkwithfiles,wewillfollowthis
tradition).Then,dragtheNewGameactionfromtheactioneditor
overthenewlycreatedmenu,butdonotdropitthereyet.The
menushouldopennow,andyoucandragtheactionsothatared
barappearsinthesubmenuinthepositionwhereyouwantthe
menuentrytoappear;nowyoucanreleasethemousebuttonto
createtheentry.
Nowweshouldrestorethefunctionalitythatwasbrokenwhenwe
deletedthebutton.NavigatetotheconstructoroftheMainWindowclass
andadjusttheconnect()call:
connect(ui->startNewGame,&QAction::triggered,
this,&MainWindow::startNewGame);
Actions,likewidgets,areaccessiblethroughtheuiobject.Theui-
>startNewGameobjectisnowaQActioninsteadofaQPushButton,andweuse
itstriggered()signaltodetectwhethertheactionwasselectedin
someway.
Now,ifyouruntheapplication,youcanselectthemenuentry,
pressabuttononthetoolbar,orpresstheCtrl+Nkeys.Eitherof
theseoperationswillcausetheactiontoemitthetriggered()signal,
andthegameconfigurationdialogshouldappear.
Likewidgets,QActionobjectshavesomeusefulmethodsthatareaccessibleinourform
class.Forexample,executingui->startNewGame->setEnabled(false)willdisableallwaysto
triggertheNewGameaction.
Let'saddanotheractionforquittingtheapplication(althoughthe
usercanalreadydoitjustbyclosingthemainwindow).Usethe
actioneditortoaddanewactionwithtextQuit,objectnamequit,and
shortcutCtrl+Q.Addittothemenuandthetoolbar,likethefirst
action.
Wecanaddanewslotthatstopstheapplication,butsuchaslot
alreadyexistsinQApplication,solet'sjustreuseit.Locatethe
constructorofourforminmainwindow.cppandappendthefollowing
code:
connect(ui->quit,&QAction::triggered,
qApp,&QApplication::quit);
Whatjusthappened?
TheqAppmacroisashortcutforafunctionthatreturnsapointerto
theapplicationsingletonobject,sowhentheactionistriggered,Qt
willcallthequit()slotontheQApplicationobjectcreatedinmain(),
which,inturn,willcausetheapplicationtoend.
TheQtresourcesystem
Buttonsinthetoolbarusuallydisplayiconsinsteadoftext.To
implementthis,weneedtoaddiconfilestoourprojectandassign
themtotheactionswecreated.
Onewayofcreatingiconsisbyloadingimagesfromthefilesystem.
Theproblemwiththisisthatyouhavetoinstallabunchoffiles
alongwithyourapplication,andyouneedtoalwaysknowwhere
theyarelocatedtobeabletoprovidepathstoaccessthem.
Fortunately,Qtprovidesaconvenientandportablewaytoembed
arbitraryfiles(suchasimagesforicons)directlyintheexecutable
file.Thisisdonebypreparingresourcefilesthatarelatercompiled
inthebinary.QtCreatorprovidesagraphicaltoolforthisaswell.
Timeforaction–Addingicons
totheproject
WewilladdiconstoourStartnewgameandQuitactions.First,use
yourfilemanagertocreateanewsubdirectorycallediconsinthe
projectdirectory.Placetwoiconfilesintheiconsdirectory.Youcan
useiconsfromthefilesprovidedwiththebook.
ClickonAddNew...inthecontextmenuofthetictactoeprojectand
selectQtResourceFile(locatedinQtcategory).Nameitresources,
andfinishthewizard.QtCreatorwilladdanewresources.qrcfileto
theproject(itwillbedisplayedundertheResourcescategoryinthe
projecttree).
Locatethenewresources.qrcfileintheprojecttreeofQtCreatorand
chooseAddExistingFiles...initscontextmenu.Selectbothicons,
andconfirmtheiradditiontotheresources.
Openthemainwindow.uiform,anddouble-clickononeoftheactionsin
theactioneditor.Clickonthe"..."buttonnexttotheIconfield,
selecticonsintheleftpartofthewindow,andselectthe
appropriateiconintherightpartofthewindow.Onceyouconfirm
changesinthedialogs,thecorrespondingbuttononthetoolbarwill
switchtodisplayingtheiconinsteadofthetext.Themenuentry
willalsogaintheselectedicon.Repeatthisoperationforthesecond
action.Ourgameshouldnowlooklikethis:
Haveagohero–Extendingthe
game
Therearealotofsubtleimprovementsyoucanmakeintheproject.
Forexample,youcanchangethetitleofthemainwindow(by
editingitswindowTitleproperty),addacceleratorstotheactions,
disabletheboardbuttonsthatdonothingonclick,removethe
statusbar,oruseitfordisplayingthegamestatus.
Asanadditionalexercise,youcantrytomodifythecodewewrote
inthischaptertoallowplayingthegameonboardsbiggerthan3×
3.Lettheuserdecidethesizeoftheboard(youcanmodifythe
gameoptionsdialogforthatanduseQSliderandQSpinBoxtoallowthe
usertochoosethesizeoftheboard),andyoucantheninstruct
TicTacToeWidgettobuildtheboardbasedonthesizeitgets.Remember
toadjustthegame-winninglogic!Ifatanypointyourunintoadead
endanddonotknowwhichclassesandfunctionstouse,consultthe
referencemanual.
Popquiz
Q1.Whichclassescanhavesignals?
1. AllclassesderivedfromQWidget.
2. AllclassesderivedfromQObject.
3. Allclasses.
Q2.Forwhichofthefollowingdoyouhavetoprovideyourown
implementation?
1. Asignal.
2. Aslot.
3. Both.
Q3.Amethodthatreturnsthepreferredsizeofawidgetiscalled
whichofthese?
1. preferredSize.
2. sizeHint.
3. defaultSize.
Q4.WhatisthepurposeoftheQActionobject?
1. Itrepresentsafunctionalitythatausercaninvokeinthe
program.
2. Itholdsakeysequencetomovethefocusonawidget.
3. Itisabaseclassforallformsgeneratedusingtheform
editor.
Summary
Inthischapter,youlearnedhowtocreatesimplegraphicaluser
interfaceswithQt.Wewentthroughtwoapproaches:designingthe
userinterfacewithagraphicaltoolthatgeneratesmostofthecode
forus,andcreatinguserinterfaceclassesbywritingallthecode
directly.Noneofthemisbetterthantheother.Theformdesigner
allowsyoutoavoidboilerplatecodeandhelpsyouhandlelarge
formswithalotofcontrols.Ontheotherhand,thecodewriting
approachgivesyoumorecontrolovertheprocessandallowsyouto
createautomaticallypopulatedanddynamicinterfaces.
WealsolearnedhowtousesignalsandslotsinQt.Youshouldnow
beabletocreatesimpleuserinterfacesandfillthemwithlogicby
connectingsignalstoslots—predefinedonesaswellascustomones
thatyounowknowhowtodefineandfillwithcode.
Qtcontainsmanywidgettypes,butwedidn'tintroducethemtoyou
onebyone.Thereisareallyniceexplanationofmanywidgettypes
intheQtmanualcalledQtWidgetGallery,whichshowsmostof
theminaction.Ifyouhaveanydoubtsaboutusinganyofthose
widgets,youcanchecktheexamplecodeandalsolookupthe
appropriateclassintheQtreferencemanualtolearnmoreabout
them.
Asyoualreadysaw,Qtallowsyoutocreatecustomwidgetclasses,
butinthischapterourcustomclassesmostlyreusedthedefault
widgets.It'salsopossibletomodifyhowthewidgetrespondsto
eventsandimplementcustompainting.Wewillgettothis
advancedtopicinChapter8,CustomWidgets.However,ifyouwant
toimplementagamewithcustom2Dgraphics,thereisasimpler
alternative—theGraphicsViewFrameworkthatwe'lluseinthenext
chapter.
Custom2DGraphicswith
GraphicsView
Widgetsaregreatfordesigninggraphicaluserinterfaces,butthey
arenotconvenientifyouwanttousemultipleobjectswithcustom
paintingandbehaviortogether,suchasina2Dgame.Youwillalso
runintoproblemsifyouwishtoanimatemultiplewidgetsatthe
sametime,byconstantlymovingthemaroundintheapplication.
Forthesesituations,orgenerallyforfrequentlytransforming2D
graphics,QtoffersyouGraphicsView.Inthischapter,youwill
learnthebasicsoftheGraphicsViewarchitectureanditsitems.You
willalsolearnhowtocombinewidgetswithGraphicsViewitems.
Themaintopicscoveredinthischapterareasfollows:
GraphicsViewarchitecture
Coordinatesystems
Standardgraphicsitems
Pensandbrushes
UsefulfeaturesofGraphicsView
Creatingcustomitems
Eventhandling
Embeddingwidgetsintheview
Optimizations
GraphicsViewarchitecture
TheGraphicsViewFrameworkispartoftheQtWidgetsmodule
andprovidesahigherlevelofabstractionusefulforcustom2D
graphics.Itusessoftwarerenderingbydefault,butitisvery
optimizedandextremelyconvenienttouse.Threecomponents
formthecoreofGraphicsView,asshown:
AninstanceofQGraphicsView,whichisreferredtoasView
AninstanceofQGraphicsScene,whichisreferredtoasScene
InstancesofQGraphicsItem,whicharereferredtoasItems
Theusualworkflowistofirstcreateacoupleofitems,addthemtoa
scene,andthenshowthatsceneonaview:
Afterthat,youcanmanipulateitemsfromthecodeandaddnew
items,whiletheuseralsohastheabilitytointeractwithvisible
items.
ThinkoftheitemsasPost-itnotes.Youtakeanoteandwritea
messageonit,paintanimageonit,bothwriteandpaintonit,or,
quitepossibly,justleaveitblank.Qtprovidesalotofitemclasses,
allofwhichinheritQGraphicsItem.Youcanalsocreateyourownitem
classes.Eachclassmustprovideanimplementationofthepaint()
function,whichperformspaintingofthecurrentitem,and
theboundingRect()function,whichmustreturntheboundaryofthe
areathepaint()functionpaintson.
Whatisthescene,then?Well,thinkofitasalargersheetofpaper
ontowhichyouattachyoursmallerPost-its,thatis,thenotes.On
thescene,youcanfreelymovetheitemsaroundwhileapplying
funnytransformationstothem.Itisthescene'sresponsibilityto
correctlydisplaytheitems'positionandanytransformations
appliedtothem.Thescenefurtherinformstheitemsaboutany
eventsthataffectthem.
Last,butnotleast,let'sturnourattentiontotheview.Thinkofthe
viewasaninspectionwindoworapersonwhoholdsthepaperwith
thenotesintheirhands.Youcanseethepaperasawhole,oryou
canonlylookatspecificparts.Also,asapersoncanrotateand
shearthepaperwiththeirhands,sotheviewcanrotateandshear
thescene'scontentanddoalotmoretransformationswithit.
QGraphicsViewisawidget,soyoucanusetheviewlikeanyotherwidget
andplaceitintolayoutsforcreatingneatgraphicaluserinterfaces.
Youmighthavelookedattheprecedingdiagramandworriedaboutalltheitemsbeing
outsidetheview.Aren'ttheywastingCPUtime?Don'tyouneedtotakecareofthemby
addingaso-calledviewfrustumcullingmechanism(todetectwhichitemnotto
draw/renderbecauseitisnotvisible)?Well,theshortansweris"no",becauseQtisalready
takingcareofthis.
Timeforaction–Creatinga
projectwithaGraphicsView
Let'sputallthesecomponentstogetherinaminimalisticproject.
FromtheWelcomescreen,clickontheNewProjectbuttonand
selectQtWidgetsApplicationagain.Nametheproject
graphics_view_demo,selectthecorrectkit,unchecktheGenerateform
checkbox,andfinishthewizard.Wedon'tactuallyneedtheMainWindow
classthatwasgeneratedforus,solet'sdeleteitfromtheproject.In
theprojecttree,locatemainwindow.handselectRemovefileinthe
contextmenu.EnabletheDeletefilepermanentlycheckboxand
clickonOK.Thiswillresultindeletingthemainwindow.hfilefromthe
diskandremovingitsnamefromthegraphics_view_demo.profile.Ifthe
filewasopeninQtCreator,itwillsuggestthatyoucloseit.Repeat
theprocessformainwindow.cpp.
Openthemain.cppfile,remove#include"mainwindow.h",andwritethe
followingcode:
intmain(intargc,char*argv[])
{
QApplicationa(argc,argv);
QGraphicsScenescene;
QGraphicsRectItem*rectItem=
newQGraphicsRectItem(QRectF(0,0,100,50));
scene.addItem(rectItem);
QGraphicsEllipseItem*circleItem=
newQGraphicsEllipseItem(QRect(0,50,25,25));
scene.addItem(circleItem);
QGraphicsSimpleTextItem*textItem=
newQGraphicsSimpleTextItem(QObject::tr("Demo"));
scene.addItem(textItem);
QGraphicsViewview(&scene);
view.show();
returna.exec();
}
}
Whenyouruntheproject,youshouldgetthefollowingresult:
Whatjusthappened?
Ournewprojectissosimplethatallitscodeislocatedinthemain()
function.Let'sexaminethecode.First,wecreateaQApplicationobject,
asinanyQtWidgetsproject.Next,wecreateasceneobjectand
threeinstancesofdifferentitemclasses.Theconstructorofeach
itemclassacceptsanargumentthatdefinesthecontentoftheitem:
TheQGraphicsRectItemconstructorreceivesaQRectFobjectthat
containsthecoordinatesoftherectangle
TheQGraphicsEllipseItemconstructor,similarly,receivesa
QRectFobjectthatdefinestheboundingrectangleofthecircle
TheQGraphicsSimpleTextItemconstructorreceivesthetextto
display
QRectFisbasicallyahelpfulstructwithfourfieldsthatallowustospecifyfourcoordinates
oftherectangle'sboundaries(left,top,width,andheight).QtalsooffersQPointFthat
containsxandycoordinatesofapoint,QLineFthatcontainsxandycoordinatesoftwo
endsofaline,andQPolygonFthatcontainsavectorofpoints.TheFletterstandsfor
"floatingpoint"andindicatesthattheseclassesholdrealnumbers.Theyarewidelyusedin
GraphicsView,asitalwaysworkswithfloatingpointcoordinates.Thecorresponding
classeswithoutF(QPoint,QRect,andsoon)storeintegercoordinatesandaremoreuseful
whenworkingwithwidgets.
Aftercreatingeachitem,weusetheQGraphicsScene::addItemfunctionto
addtheitemtothescene.Finally,wecreateaQGraphicsViewobjectand
passthepointertothescenetoitsconstructor.Theshow()method
willmaketheviewvisible,asitdoesforanyQWidget.Theprogram
endswithana.exec()call,necessarytostarttheeventloopandkeep
theapplicationalive.
Thescenetakesownershipoftheitems,sotheywillbe
automaticallydeletedalongwiththescene.Thisalsomeansthatan
itemcanonlybeaddedtoonesinglescene.Iftheitemwas
previouslyaddedtoanotherscene,itgetsremovedfromthere
beforeitisaddedtothenewscene.
Ifyouwanttoremoveanitemfromascenewithoutsettingitdirectlytoanothersceneor
withoutdeletingit,youcancallscene.removeItem(rectItem).Beaware,however,thatnow
itisyourresponsibilitytodeleterectItemtofreetheallocatedmemory!
Examinetheresultingwindowandcompareittothecoordinatesof
therectanglesinthecode(theQRectFconstructorweuseacceptsfour
argumentsinthefollowingorder:left,top,width,height).You
shouldbeabletoseethatallthreeelementsarepositionedina
singlecoordinatesystem,wherethexaxispointstotherightand
theyaxispointsdown.Wedidn'tspecifyanycoordinatesforthe
textitem,soit'sdisplayedattheoriginpoint(thatis,thepoint
withzerocoordinates),nexttothetop-leftcorneroftherectangle.
However,that(0,0)pointdoesnotcorrespondtothetop-left
cornerofthewindow.Infact,ifyouresizethewindow,you'llnote
thattheoriginhasshiftedrelativetothewindow,becausetheview
triestodisplaythescene'scontentascentered.
Coordinatesystems
TouseGraphicsViewcorrectly,youneedtounderstandhowthe
coordinatesystemsinthisframeworkwork.We'llgothroughallthe
levelsofhierarchyandseehowwecanchangethepositioningof
itemsandthewholescene,oneachlevel.Wewillprovideexamples
ofthecodethatyoucanpasteintoourdemoprojectandexamine
theeffect.
Theitem'scoordinatesystem
Eachitemhasitsowncoordinatesystem.InourexampleofPost-it
notes,thecontentofeachnoteisdefinedrelativetothetop-left
cornerofthenote.Nomatterhowyoumoveorrotatetheitem,
thesecoordinatesremainthesame.Thecoordinatesofadrawn
objectcanusuallybepassedtotheconstructoroftheclass,likewe
didinourdemoproject,ortoaspecialsetterfunction(for
example,rectItem->setRect(0,10,20,25)).Thesearecoordinatesinthe
item'scoordinatesystem.
Someclasses,suchasQGraphicsSimpleTextItem,donotprovidetheability
tochangethecoordinatesofthecontent,sothey'realways
positionedattheoriginoftheitem'scoordinatesystem.Thisisnot
aproblematall;aswe'llseenext,therearewaystochangethe
visiblepositionofthecontent.
Ifyoutrytocreateyourowngraphicsitemclass(we'llgettoitlater
inthischapter),you'llneedtoimplementthepaint()andboundingRect()
functions,andtheyalwaysoperateintheitem'scoordinatesystem.
That'sright,whenyou'repaintingthecontent,youcanjustpretend
thatyouritemwillneverbemovedortransformed.Whenthat
actuallyhappens,Qtwilltakecareoftransformingpaintoperations
foryou.Additionally,coordinatesinanyeventstheitemreceives
(forexample,coordinatesofamousebuttonclick)areexpressedin
theitem'scoordinatesystem.
Thescene'scoordinatesystem
AnyitemcanbemovedinthesceneusingthesetPos()function.Try
tocalltextItem->setPos(50,50)andverifythatthetextwasmovedinthe
scene.Technically,thisoperationchangesthetransformation
betweentheitem'scoordinatesystemandthescene'scoordinate
system.AconveniencefunctioncalledmoveBy()allowsyoutoshiftthe
positionbyspecifiedamounts.
AnitemcanalsoberotatedwithsetRotation()andscaledwith
setScale().TrycallingtextItem->setRotation(20)toseethisinaction.Ifyou
needamoreadvancedtransformation,suchasshearing,oryou
wanttoperformmultipletranslationsinaparticularorder,youcan
createaQTransformobject,applytherequiredtransformations,and
usethesetTransform()functionoftheitem.
ThesetRotation()functionacceptsqrealastheargumentvalue,whichisusuallyatypedef
fordouble.Thefunctioninterpretsthenumberasdegreesforaclockwiserotationaround
thezcoordinate.Ifyousetanegativevalue,acounter-clockwiserotationisperformed.
Evenifitdoesnotmakemuchsense,youcanrotateanitemby450degrees,whichwill
resultinarotationof90degrees.
Theviewport'scoordinate
system
Theviewconsistsoftheviewportandtwoscrollbars.Theviewport
isasubwidgetthatactuallycontainsthecontentofthescene.The
viewperformsconversionfromthescenecoordinatestothe
viewportcoordinatesbasedonmultipleparameters.
First,theviewneedstoknowtheboundingrectangleofeverything
wecouldwanttoseeinthescene.It'scalledthescenerectandis
measuredinthescene'scoordinatesystem.Bydefault,thescene
rectistheboundingrectangleofallitemsthatwereaddedatthe
scenesinceitwascreated.Thisisusuallyfine,butifyoumoveor
deleteanitem,thatboundingrectanglewillnotshrink(becauseof
performancereasons),soitmayresultinshowingalotofunwanted
emptyspace.Luckily,insuchcases,youcansetthescenerect
manuallyusingthesetSceneRectfunctionofthesceneorview.
ThedifferencebetweenQGraphicsScene::setSceneRectandQGraphicsView::setSceneRectis
usuallymarginal,sinceyouwilltypicallyhaveoneviewperscene.However,itispossible
tohavemultipleviewsforasinglescene.Inthiscase,QGraphicsScene::setSceneRectsetsthe
scenerectforallviews,andQGraphicsView::setSceneRectallowsyoutooverridethescene
rectforeachview.
Iftheareacorrespondingtothescenerectissmallenoughtofitin
theviewport,theviewwillalignthecontentaccordingtotheview's
alignmentproperty.Aswesawearlier,itpositionsthecontentatthe
centerbydefault.Forexample,callingview.setAlignment(Qt::AlignTop|
Qt::AlignLeft)willresultinthescenestayingintheupper-leftcorner
oftheview.
Ifthescenerectareaistoolargetofitintheviewport,the
horizontalorverticalscrollbarsappearbydefault.Theycanbeused
toscrolltheviewandseeanypointinsidethescenerect(butnot
beyondit).Thepresenceofscrollbarscanalsobeconfiguredusing
thehorizontalScrollBarPolicyandtheverticalScrollBarPolicypropertiesof
theview.
Trytocallscene.setSceneRect(0,20,100,100)andseehowtheviewacts
whenresizingthewindow.Ifthewindowistoosmall,thetoppart
ofthescenewillnolongerbevisible.Ifthewindowislargeenough
andtheviewhasthedefaultalignment,thetoppartofthescenewill
bevisible,butonlythedefinedscenerectwillbecentered,withno
regardtotheitemsoutsideofit.
Theviewprovidestheabilitytotransformtheentirescene.For
example,youcancallview.scale(5,5)tomakeeverythingfivetimes
larger,view.rotate(20)torotatethesceneasawhole,orview.shear(1,0)
toshearit.Aswithitems,youcanapplyamorecomplex
transformationusingsetTransform().
YoumaynotethatGraphicsView(andQtWidgetsingeneral)usesaleft-handed
coordinatesystembydefault,wherexaxispointsrightandyaxispointsdown.However,
OpenGLandscience-relatedapplicationsusuallyusearight-handedorstandard
coordinatesystem,whereyaxispointsup.Ifyouneedtochangethedirectionoftheyaxis,
thesimplestsolutionistotransformtheviewbycallingview.scale(1,-1).
Originpointofthe
transformation
Inournextexample,wewillcreateacrossat(0,0)pointandadda
rectangletothescene:
Youcandoitwiththefollowingcode:
scene.addLine(-100,0,100,0);
scene.addLine(0,-100,0,100);
QGraphicsRectItem*rectItem=scene.addRect(50,50,50,50);
Inthiscode,weusetheaddLine()andaddRect()conveniencefunctions.
ThisisthesameascreatingaQGraphicsLineItemorQGraphicsRectItemand
addingittothescenemanually.
Now,imaginethatyouwanttorotatetherectangleby45degreesto
producethefollowingresult:
AstraightforwardattempttodoitwillusethesetRotation()method:
QGraphicsRectItem*rectItem=scene.addRect(50,50,50,50);
rectItem->setRotation(45);
However,ifyoutrytodothat,youwillgetthefollowingresult:
Whatjusthappened?
Mosttransformationsdependontheoriginpointofthecoordinate
system.Forrotationandscaling,theoriginpointistheonlypoint
thatremainsinplace.Intheprecedingexample,weuseda
rectanglewiththetop-leftcornerat(50,50)andthesizeof(50,
50).Thesecoordinatesareintheitem'scoordinatesystem.Sincewe
originallydidn'tmovetheitem,theitem'scoordinatesystemwas
thesameasthescene'scoordinatesystem,andtheoriginpointis
thesameasthescene'sorigin(it'sthepointmarkedwiththecross).
Theappliedrotationuses(0,0)asthecenterofrotation,thus
providinganunexpectedresult.
Therearemultiplewaystoovercomethisproblem.Thefirstwayis
tochangethetransform'soriginpoint:
QGraphicsRectItem*rectItem=scene.addRect(50,50,50,50);
rectItem->setTransformOriginPoint(75,75);
rectItem->setRotation(45);
Thiscodeproducestherotationwewant,becauseitchangesthe
originpointusedbythesetRotation()andsetScale()functions.Note
thattheitem'scoordinatesystemwasnottranslated,and(75,75)
pointcontinuestobethecenteroftherectangleintheitem's
coordinates.
However,thissolutionhasitslimitations.IfyouusesetTransform()
insteadofsetRotation(),youwillgettheunwantedresultagain:
QGraphicsRectItem*rectItem=scene.addRect(50,50,50,50);
rectItem->setTransformOriginPoint(75,75);
QTransformtransform;
transform.rotate(45);
rectItem->setTransform(transform);
Anothersolutionistosetuptherectangleinsuchawaythatits
centerisintheoriginoftheitem'scoordinatesystem:
QGraphicsRectItem*rectItem=scene.addRect(-25,-25,50,50);
rectItem->setPos(75,75);
Thiscodeusescompletelydifferentrectanglecoordinates,butthe
resultisexactlythesameasinourfirstexample.However,now,
(75,75)pointinthescene'scoordinatescorrespondsto(0,0)point
intheitem'scoordinates,soalltransformationswilluseitasthe
origin:
QGraphicsRectItem*rectItem=scene.addRect(-25,-25,50,50);
rectItem->setPos(75,75);
rectItem->setRotation(45);
Thisexampleshowsthatitisusuallymoreconvenienttosetuptheitemssothattheir
originpointcorrespondstotheiractuallocation.
Haveagohero–Applying
multipletransformations
Tounderstandtheconceptoftransformationsandtheirorigin
point,trytoapplyrotate()andscale()toanitem.Also,changethe
pointoforiginandseehowtheitemwillreact.Asasecondstep,use
QTransforminconjunctionwithsetTransform()toapplymultiple
transformationstoaniteminaspecificorder.
Parent–childrelationship
betweenitems
Imaginethatyouneedtocreateagraphicsitemthatcontains
multiplegeometricprimitives,forexample,acircleinsidea
rectangle.Youcancreatebothitemsandaddthemtothescene
individually,butthissolutionisinconvenient.First,whenyouneed
toremovethatcombinationfromthescene,youwouldneedto
manuallydeletebothitems.However,moreimportantly,whenyou
needtomoveortransformthecombination,youwillneedto
calculatepositionsandcomplextransformationsforeachgraphics
item.
Fortunately,graphicsitemsdonothavetobeaflatlistofitems
addeddirectlyintothescene.Itemscanbeaddedintoanyother
items,formingaparent–childrelationshipverysimilartothe
relationshipofQObjectthatweobservedinthelastchapter:
Addinganitemasachildofanotheritemhasthefollowing
consequences:
Whentheparentitemisaddedtothescene,thechilditem
automaticallybecomespartofthatscene,sothereisnoneed
tocallQGraphicsScene::addItem()forit.
Whentheparentitemisdeleted,itschildrenarealso
deleted.
Whentheparentitemishiddenusingthehide()or
setVisible(false)functions,thechilditemswillalsobehidden.
Mostimportantly,thechild'scoordinatesystemisderived
fromtheparent'scoordinatesysteminsteadofthescene's.
Thismeansthatwhentheparentismovedortransformed,
allchildrenarealsoaffected.Thechild'spositionand
transformationsarerelativetotheparent'scoordinate
system.
YoucanalwayscheckwhetheranitemhasaparentusingtheparentItem()function,and
checkthereturnedQGraphicsItempointeragainstnullptr,whichmeansthattheitemdoes
nothaveaparent.Tofigureoutwhetherthereareanychildren,callthechildItems()
functionontheitem.AQListmethodwiththeQGraphicsItempointerstoallchilditemsis
returned.
Forabetterunderstandingofpos()andtheinvolvedcoordinate
systems,thinkofpost-itnotesagain.Ifyouputanoteonalarger
sheetofpaperandthenhadtodetermineitsexactposition,how
wouldyoudoit?Probablylikethis:"Thenote'supper-leftcorneris
positioned3cmtotherightand5cmtothebottomfromthe
paper'stop-leftedge".IntheGraphicsViewworld,thiswill
correspondtoaparentlessitemwhosepos()functionreturnsa
positioninthescenecoordinates,sincetheitem'soriginisdirectly
pinnedtothescene.Ontheotherhand,sayyouputanoteAontop
ofa(larger)noteB,whichisalreadypinnedonapaper,andyou
havetodetermineA'sposition;howwouldyoudescribeitthistime?
ProbablybysayingthatnoteAisplacedontopofnoteBor"2cmto
therightand1cmtothebottomfromthetop-leftedgeofnoteB".
Youmostlikelywouldn'tusetheunderlyingpaperasareference
sinceitisnotthenextpointofreference.Thisisbecauseifyou
movenoteB,A'spositionregardingthepaperwillchange,whereas
A'srelativepositiontoBstillremainsunchanged.Toswitchbackto
GraphicsView,theequivalentsituationisanitemthathasaparent
item.Inthiscase,thepos()function'sreturnedvalueisexpressedin
thecoordinatesystemofitsparent.So,setPos()andpos()specifythe
positionoftheitem'sorigininrelationtothenext(higher)pointof
reference.Thiscanbethesceneortheitem'sparentitem.
Keepinmind,however,thatchanginganitem'spositiondoesnot
affecttheitem'sinternalcoordinatesystem.
Forwidgets,thechildalwaysoccupiesasubareaofitsdirectparent.Forgraphicsitems,
sucharuledoesnotapplybydefault.Achilditemcanbedisplayedoutsidethebounding
rectangleorvisiblecontentoftheparent.Infact,acommonsituationiswhentheparent
itemdoesnothaveanyvisualcontentbyitselfandonlyservesasacontainerforasetof
primitivesbelongingtooneobject.
Timeforaction–Usingchild
items
Let'strytomakeanitemcontainingmultiplechildren.Wewantto
createarectanglewithafilledcircleineachcornerandbeableto
moveandrotateitasawhole,likethis:
First,youneedtocreateafunctionthatcreatesasinglecomplex
rectangle,byusingthefollowingcode:
QGraphicsRectItem*createComplexItem(
qrealwidth,qrealheight,qrealradius)
{
QRectFrect(-width/2,-height/2,width,height);
QGraphicsRectItem*parent=newQGraphicsRectItem(rect);
QRectFcircleBoundary(-radius,-radius,2*radius,2*radius);
for(inti=0;i<4;i++){
QGraphicsEllipseItem*child=
newQGraphicsEllipseItem(circleBoundary,parent);
child->setBrush(Qt::black);
QPointFpos;
switch(i){
case0:
pos=rect.topLeft();
break;
case1:
pos=rect.bottomLeft();
pos=rect.bottomLeft();
break;
case2:
pos=rect.topRight();
break;
case3:
pos=rect.bottomRight();
break;
}
child->setPos(pos);
}
returnparent;
}
WestartwithcreatingaQRectFvariablethatcontainstherectangle
coordinatesintheitem'scoordinatesystem.Followingthetipwe
providedearlier,wecreatearectanglecenteredattheoriginpoint.
Next,wecreatearectanglegraphicsitemcalledparent,asusual.The
circleBoundaryrectangleissettocontaintheboundaryrectofasingle
circle(again,thecenterisattheoriginpoint).Whenwecreatea
newQGraphicsEllipseItemforeachcorner,wepassparenttothe
constructor,sothenewcircleitemisautomaticallyaddedasachild
oftherectangleitem.
Tosetupachildcircle,wefirstusethesetBrush()functionthat
enablesfillingofthecircle.ThisfunctionexpectsaQBrushobjectthat
allowsyoutospecifyanadvancedfillingstyle,butinoursimple
case,weuseanimplicitconversionfromtheQt::GlobalColorenumto
QBrush.Youwilllearnmoreaboutbrusheslaterinthischapter.
Next,weselectadifferentcorneroftherectangleforeachcircleand
callsetPos()tomovethecircletothatcorner.Finally,wereturnthe
parentitemtothecaller.
Youcanusethisfunctionasfollows:
QGraphicsRectItem*item1=createComplexItem(100,60,8);
scene.addItem(item1);
QGraphicsRectItem*item2=createComplexItem(100,60,8);
scene.addItem(item2);
scene.addItem(item2);
item2->setPos(200,0);
item2->setRotation(20);
NotethatwhenyoucallsetPos(),thecirclesaremovedalongwiththe
parentitem,butthepos()valuesofthecirclesdonotchange.Thisis
theconsequenceofthefactthatpos()meansthepositionrelativeto
theparentitem(orthescene'sorigin,ifthereisnoparentitem).
Whentherectangleisrotated,circlesrotatewithit,asiftheywere
fixedtothecorners.Ifthecirclesweren'tchildrenoftherectangle,
positioningthemproperly,inthiscase,wouldbeamore
challengingtask.
Haveagohero–Implementing
thecustomrectangleasa
class
Inthisexample,weavoidedcreatingaclassforourcustom
rectangletokeepthecodeassimpleaspossible.Followingthe
principlesofobject-orientedprogramming,subclassing
QGraphicsRectItemandcreatingchildrenitemsintheconstructorofthe
newclassisagoodidea.Doingthisdoesn'trequireanythingyou
don'talreadyknow.Forexample,whensubclassingQGraphicsRectItem,
youdon'tneedtoimplementanyvirtualfunctions,becausetheyare
allproperlyimplementedinthebaseclasses.
Conversionsbetween
coordinatesystems
IfanitemissimplymovedusingsetPos(),conversionfromtheitem's
coordinatestothescenecoordinatesisassimpleassceneCoord=
itemCoord+item->pos().However,thisconversionquicklybecomesvery
complexwhenyouusetransformationsandparent–child
relationships,soyoushouldalwaysusededicatedfunctionsto
performsuchconversions.QGraphicsItemprovidesthefollowing
functions:
Fu
nc
tio
n
Description
mapT
oSce
ne(
cons
t
QPoi
ntF
&poi
nt)
Mapsthepointpointthatisintheitem'scoordinatesystem
tothecorrespondingpointinthescene'scoordinatesystem.
scen
ePos
Mapstheitem'soriginpointtothescene'scoordinate
()
system.ThisisthesameasmapToScene(0,0).
scen
eBou
ndin
gRec
t()
Returnstheitem'sboundingrectangleinthescene's
coordinatesystem.
mapF
romS
cene
(
cons
t
QPoi
ntF
&poi
nt)
Mapsthepointpointthatisinthescene'scoordinatesystem
tothecorrespondingpointintheitem'scoordinatesystem.
ThisfunctionisthereversefunctiontomapToScene().
mapT
oPar
ent(
cons
t
QPoi
ntF
&poi
nt)
Mapsthepointpointthatisintheitem'scoordinatesystem
tothecorrespondingpointinthecoordinatesystemofthe
item'sparent.Iftheitemdoesnothaveaparent,this
functionbehaveslikemapToScene();thus,itreturnsthe
correspondingpointinthescene'scoordinatesystem.
mapF
romP
aren
t(
cons
t
QPoi
ntF
&poi
nt)
Mapsthepointpointthatisinthecoordinatesystemofthe
item'sparenttothecorrespondingpointintheitem'sown
coordinatesystem.Thisfunctionisthereversefunctionto
mapToParent().
mapT
oIte
m(
cons
t
QGra
phic
sIte
m
*ite
m,
con
st
QPoi
ntF
&poi
nt)
Mapsthepointpointthatisintheitem'sowncoordinate
systemtothecorrespondingpointinthecoordinatesystem
oftheitemitem.
mapF
romI
tem(
cons
t
QGra
phic
sIte
m
*ite
m,
con
st
QPoi
ntF
&poi
nt)
Mapsthepointpointthatisinthecoordinatesystemofthe
itemitemtothecorrespondingpointintheitem'sown
coordinatesystem.Thisfunctionisthereversefunctionto
mapToItem().
Whatisgreataboutthesefunctionsisthattheyarenotonly
availableforQPointF.ThesamefunctionsarealsoavailableforQRectF,
QPolygonF,andQPainterPath,nottomentionthattherearesome
conveniencefunctions:
Ifyoucallthesefunctionswithtwonumbersofthe
qrealtype,thenumbersareinterpretedasthexandy
coordinatesofaQPointFpointer
Ifyoucallthefunctionswithfournumbers,thenumbersare
interpretedasthexandycoordinatesandthewidthand
heightofaQRectFparameter
TheQGraphicsViewclassalsocontainsasetofmapToScene()functionsthat
mapcoordinatesfromtheviewport'scoordinatesystemtothescene
coordinatesandmapFromScene()functionsthatmapthescene
coordinatestotheviewportcoordinates.
Overviewoffunctionality
YoushouldnowhavesomeunderstandingofGraphicsView's
architectureandtransformationmechanics.Wewillnowdescribe
someeasy-to-usefunctionalitythatyou'llprobablyneedwhen
creatingaGraphicsViewapplication.
Standarditems
Inordertoeffectivelyusetheframework,youneedtoknowwhat
graphicsitemclassesitprovides.It'simportanttoidentifythe
classesyoucanusetoconstructthedesirablepictureandresortto
creatingacustomitemclass,onlyifthereisnosuitableitemoryou
needbetterperformance.Qtcomeswiththefollowingstandard
itemsthatmakeyourlifeasadevelopermucheasier:
S
ta
n
d
a
r
d
it
e
m
Description
QG
ra
ph
ic
sL
in
eI
te
m
Drawsaline.YoucandefinethelinewithsetLine(constQLineF&).
QG
ra
ph
ic
sR
ec
Drawsarectangle.Youcandefinetherectangle'sgeometry
withsetRect(constQRectF&).
tI
te
m
QG
ra
ph
ic
sE
ll
ip
se
It
em
Drawsanellipseoranellipsesegment.Youcandefinethe
rectanglewithinwhichtheellipseisbeingdrawnwith
setRect(constQRectF&).Additionally,youcandefinewhetheronly
asegmentoftheellipseshouldbedrawnbycalling
setStartAngle(int)andsetSpanAngle(int).Theargumentsofboth
functionsareinsixteenthsofadegree.
QG
ra
ph
ic
sP
ol
yg
on
It
em
Drawsapolygon.Youcandefinethepolygonwith
setPolygon(constQPolygonF&).
QG
ra
ph
ic
sP
at
hI
te
m
Drawsapath,thatis,asetofvariousgeometricprimitives.
YoucandefinethepathwithsetPath(constQPainterPath&).
QG
ra
ph
ic
sS
im
pl
eT
ex
tI
te
m
Drawsplaintext.YoucandefinethetextwithsetText(const
QString&)andthefontwithsetFont(constQFont&).Thisitem
doesn'tsupportrichformatting.
QG
ra
ph
ic
sT
ex
tI
te
m
Drawsformattedtext.UnlikeQGraphicsSimpleTextItem,thisitem
candisplayHTMLstoredinaQTextDocumentobject.Youcanset
HTMLwithsetHtml(constQString&)andthedocumentwith
setDocument(QTextDocument*).QGraphicsTextItemcaneveninteractwith
thedisplayedtextsothattexteditingorURLopeningis
possible.
QG
ra
ph
ic
sP
ix
ma
pI
te
m
Drawsapixmap(arasterimage).Youcandefinethepixmap
withsetPixmap(constQPixmap&).It'spossibletoloadpixmapsfrom
localfilesorresources,similartoicons(refertoChapter3,Qt
GUIProgramming,formoreinformationaboutresources).
QG
ra
ph
ic
sP
ro
xy
Wi
dg
et
DrawsanarbitraryQWidgetandallowsyoutointeractwithit.
YoucansetthewidgetwithsetWidget(QWidget*).
Aswealreadysaw,youcanusuallypassthecontentoftheitemto
theconstructorinsteadofcallingasettermethodsuchassetRect().
However,keepinmindthatcompactcodemaybeharderto
maintainthancodethatsetsallthevariablesthroughsetter
methods.
Formostitems,youcanalsodefinewhichpenandwhichbrush
shouldbeused.ThepenissetwithsetPen()andthebrushwith
setBrush()(we'vealreadyuseditforthechildcirclesintheprevious
example).Thesetwofunctions,however,donotexistfor
QGraphicsTextItem.TodefinetheappearanceofaQGraphicsTextItemitem,
youhavetousesetDefaultTextColor()orHTMLtagssupportedbyQt.
QGraphicsPixmapItemhasnosimilarmethods,astheconceptsofpenand
brushcannotbeappliedtopixmaps.
UseQGraphicsSimpleTextItemwhereverpossibleandtrytoavoidQGraphicsTextItem,ifitis
notabsolutelynecessary.ThereasonisthatQGraphicsTextItemisasubclassofQObjectand
usesQTextDocument,whichisbasicallyanHTMLengine(althoughquitelimited).Thisisway
heavierthananaveragegraphicsitemandisdefinitelytoomuchoverheadfordisplaying
simpletext.
Itisgenerallyeasiertousestandarditemsthantoimplementthem
fromscratch.WheneveryouwilluseGraphicsView,askyourself
thesequestions:Whichstandarditemsaresuitedformyspecific
needs?AmIre-inventingthewheeloverandoveragain?However,
fromtimetotime,youneedtocreatecustomgraphicsitems,and
we'llcoverthistopiclaterinthischapter.
Anti-aliasing
Ifyoulookattheresultofthepreviousscreenshot,youcan
probablynotethatthedrawinglookspixelated.Thishappens
becauseeachpixelinalineiscompletelyblack,andallthe
surroundingpixelsarecompletelywhite.Thephysicaldisplay's
resolutionislimited,butatechniquecalledanti-aliasingallows
youtoproducemoresmoothimageswiththesameresolution.
Whendrawingalinewithanti-aliasing,somepixelswillbemoreor
lessblackerthanothers,dependingonhowthelinecrossesthe
pixelgrid.
Youcaneasilyenableanti-aliasinginGraphicsViewusingthe
followingcode:
view.setRenderHint(QPainter::Antialiasing);
Withtheanti-aliasingflagturnedon,thepaintingisdonemuch
moresmoothly:
However,linesintherectangleontheleftnowlookthicker.This
happensbecauseweusedlineswithintegercoordinatesand1pixel
width.Suchalineislocatedexactlyontheborderbetweentworows
ofpixels,andwhenanti-aliased,bothadjacentrowsofpixelswillbe
partiallypainted.Thiscanbefixedbyadding0.5toallcoordinates:
QRectFrect(-width/2,-height/2,width,height);
rect.translate(0.5,0.5);
QGraphicsRectItem*parent=newQGraphicsRectItem(rect);
Nowthelineispositionedrightinthemiddleofapixelrow,soit
onlyoccupiesasinglerow:
Anothersolutionistoimplementacustomitemclassanddisable
anti-aliasingwhenpaintingahorizontalorverticalline.
QGraphicsViewalsosupportstheQPainter::TextAntialiasingflagthatenables
anti-aliasingwhendrawingtext,andtheQPainter::SmoothPixmapTransform
flagthatenablessmoothpixmaptransformation.Notetheanti-
aliasingandsmoothingimpactperformanceofyourapplication,so
usethemonlywhenneeded.
Pensandbrushes
Thepenandbrusharetwoattributesthatdefinehowdifferent
drawingoperationsareperformed.Thepen(representedby
theQPenclass)definestheoutline,andthebrush(representedby
theQBrushclass)fillsthedrawnshapes.Eachofthemisreallyasetof
parameters.Themostsimpleoneisthecolordefined,eitherasa
predefinedglobalcolorenumerationvalue(suchasQt::redor
Qt::transparent),oraninstanceoftheQColorclass.Theeffectivecoloris
madeupoffourattributes:threecolorcomponents(red,green,and
blue)andanoptionalalphachannelvaluethatdeterminesthe
transparencyofthecolor(thelargerthevalue,themoreopaquethe
color).Bydefault,allcomponentsareexpressedas8-bitvalues(0
to255)butcanalsobeexpressedasrealvaluesrepresentinga
percentageofthemaximumsaturationofthecomponent;for
example,0.6correspondsto153(0.6⋅255).Forconvenience,oneof
theQColorconstructorsacceptshexadecimalcolorcodesusedin
HTML(with#0000FFbeinganopaquebluecolor)orevenbarecolor
names(forexample,blue)fromapredefinedlistofcolorsreturned
byastaticfunction—QColor::colorNames().Onceacolorobjectisdefined
usingRGBcomponents,itcanbequeriedusingdifferentcolor
spaces(forexample,CMYKorHSV).Also,asetofstaticmethods
areavailablethatactasconstructorsforcolorsexpressedin
differentcolorspaces.
Forexample,toconstructaclearmagentacoloranyofthefollowing
expressionscanbeused:
QColor("magenta")
QColor("#FF00FF")
QColor(255,0,255)
QColor::fromRgbF(1,0,1)
QColor::fromHsv(300,255,255)
QColor::fromCmyk(0,255,0,0)
Qt::magenta
Apartfromthecolor,QBrushhastwoadditionalwaysofexpressing
thefillofashape.YoucanuseQBrush::setTexture()tosetapixmapthat
willbeusedasastamporQBrush::setGradient()tomakethebrushusea
gradienttodothefilling.Forexample,touseagradientthatgoes
diagonallyandstartsasyellowinthetop-leftcorneroftheshape,
becomesredinthemiddleoftheshape,andendsasmagentaatthe
bottom-rightcorneroftheshape,thefollowingcodecanbeused:
QLinearGradientgradient(0,0,width,height);
gradient.setColorAt(0,Qt::yellow);
gradient.setColorAt(0.5,Qt::red);
gradient.setColorAt(1.0,Qt::magenta);
QBrushbrush=gradient;
Whenusedwithdrawingarectangle,thiscodewillgivethe
followingoutput:
Qtcanhandlelinear(QLinearGradient),radial(QRadialGradient),and
conical(QConicalGradient)gradients.QtprovidesaGradientsexample
(showninthefollowingscreenshot)whereyoucanseedifferent
gradientsinaction:
Asforthepen,itsmainattributeisitswidth(expressedinpixels),
whichdeterminesthethicknessoftheshapeoutline.Apencan,of
course,haveacolorsetbut,inadditiontothat,youcanuseany
brushasapen.Theresultofsuchanoperationisthatyoucandraw
thickoutlinesofshapesusinggradientsortextures.
Therearethreemoreimportantpropertiesforapen.Thefirstisthe
penstyle,setusingQPen::setStyle().Itdetermineswhetherlines
drawnbythepenarecontinuousordividedinsomeway(dashes,
dots,andsoon).Youcanseetheavailablelinestyleshere:
Thesecondattributeisthecapstyle,whichcanbeflat,square,or
round.Thethirdattribute—thejoinstyle—isimportantforpolyline
outlinesanddictateshowdifferentsegmentsofthepolylineare
connected.Youcanmakethejoinssharp(withQt::MiterJoinor
Qt::SvgMiterJoin),round(Qt::RoundJoin),orahybridofthetwo
(Qt::BevelJoin).Youcanseethedifferentpenattributeconfigurations
(includingdifferentjoinandcapstyles)inactionbylaunchingthe
PathStrokingexampleshowninthefollowingscreenshot:
Itemselection
Thescenesupportstheabilityofselectingitems,similartohowyou
selectfilesinafilemanager.Tobeselectable,anitemmusthavethe
QGraphicsItem::ItemIsSelectableflagturnedon.Trytoaddparent-
>setFlag(QGraphicsItem::ItemIsSelectable,true)tothecreateComplexItem()
functionwecreatedearlier.Now,ifyouruntheapplicationand
clickonarectangle,itisselected,whichisindicatedbydashed
lines:
YoucanusetheCtrlbuttontoselectmultipleitemsatonce.
Alternatively,youcancallview.setDragMode(QGraphicsView::RubberBandDrag)to
activatetherubberbandselectionfortheview.
AnotherusefuldragmodeoftheGraphicsViewisScrollHandDrag.Itallowsyoutoscrollthe
viewbydraggingthescenewiththeleftmousebutton,withouttheneedtousescrollbars.
Besidesthat,therearedifferentwaystoselectitems
programmatically.Thereistheitem'sQGraphicsItem::setSelected()
function,whichtakesaboolvaluetotoggletheselectionstateonor
off,oryoucancallQGraphicsScene::setSelectionArea()onthescene,which
takesaQPainterPathparameterasanargument,inwhichcaseallitems
withintheareaareselected.
Withthescene'sQGraphicsScene::selectedItems()function,youcanquery
theactualselecteditems.ThefunctionreturnsaQListholding
QGraphicsItempointerstotheselecteditems.Forexample,calling
QList::count()onthatlistwillgiveyouthenumberofselecteditems.
Tocleartheselection,callQGraphicsScene::clearSelection().Toquerythe
selectionstateofanitem,useQGraphicsItem::isSelected(),whichreturns
trueiftheitemisselectedandfalseotherwise.
AnotherinterestingflagofGraphicsItemisItemIsMovable.Itenablesyoutodragtheitem
withinthescenebyholdingitwiththeleftmousebutton,effectivelychangingthepos()of
theitem.Trytoaddparent->setFlag(QGraphicsItem::ItemIsMovable,true)toour
createComplexItemfunctionanddragaroundtherectangles.
Keyboardfocusingraphics
scene
Thesceneimplementstheconceptoffocusthatworkssimilarto
keyboardfocusinwidgets.Onlyoneitemcanhavefocusatatime.
Whenthescenereceivesakeyboardevent,itisdispatchedtothe
focusitem.
Tobefocusable,anitemmusthavetheQGraphicsItem::ItemIsFocusableflag
enabled:
item1->setFlag(QGraphicsItem::ItemIsFocusable,true);
item2->setFlag(QGraphicsItem::ItemIsFocusable,true);
Then,anitemcanbefocusedbyamouseclick.Youcanalsochange
thefocuseditemfromthecode:
item1->setFocus();
Anotherwaytosetthefocusistousethescene's
QGraphicsScene::setFocusItem()function,whichexpectsapointertothe
itemyouliketofocusasaparameter.Everytimeanitemgains
focus,thepreviouslyfocuseditem(ifany)willautomaticallylose
focus.
Todeterminewhetheranitemhasfocus,youagainhavetwo
possibilities.OneisthatyoucancallQGraphicsItem::hasFocus()onan
item,whichreturnstrueiftheitemhasfocusorfalseotherwise.
Alternatively,youcangettheactualfocuseditembycallingthe
scene'sQGraphicsScene::focusItem()method.Ontheotherhand,ifyou
calltheitem'sQGraphicsItem::focusItem()function,thefocuseditemis
returnediftheitemitselforanydescendantitemhasfocus;
otherwise,nullptrisreturned.Toremovefocus,callclearFocus()onthe
focuseditemorclicksomewhereinthescene'sbackgroundoronan
itemthatcannotgetfocus.
Ifyouwantaclickonthescene'sbackgroundnottocausethefocuseditemtoloseitsfocus,
setthescene'sstickyFocuspropertytotrue.
Painterpaths
Ifyouwanttocreateagraphicsitemthatconsistsofmultiple
geometricprimitives,creatingmultipleQGraphicsItemobjectsseemsto
betedious.Fortunately,QtprovidesaQGraphicsPathItemclassthat
allowsyoutospecifyanumberofprimitivesinaQPainterPathobject.
QPainterPathallowsyouto"record"multiplepaintinginstructions
(includingfilling,outlining,andclipping),andthenefficientlyreuse
themmultipletimes.
Timeforaction–Addingpath
itemstothescene
Let'spaintafewobjectsconsistingofalargenumberoflines:
staticconstintSIZE=100;
staticconstintMARGIN=10;
staticconstintFIGURE_COUNT=5;
staticconstintLINE_COUNT=500;
for(intfigureNum=0;figureNum<FIGURE_COUNT;++figureNum){
QPainterPathpath;
path.moveTo(0,0);
for(inti=0;i<LINE_COUNT;++i){
path.lineTo(qrand()%SIZE,qrand()%SIZE);
}
QGraphicsPathItem*item=scene.addPath(path);
item->setPos(figureNum*(SIZE+MARGIN),0);
}
Foreachitem,wefirstcreateaQPainterPathandsetthecurrent
positionto(0,0).Then,weusetheqrand()functiontogenerate
randomnumbers,applythemodulusoperator(%)toproducea
numberfrom0toSIZE(excludingSIZE),andfeedthemtothelineTo()
functionthatstrokesalinefromthecurrentpositiontothegiven
positionandsetsitasthenewcurrentposition.Next,weuse
theaddPath()conveniencefunctionthatcreatesaQGraphicsPathItemobject
andaddsittothescene.Finally,weusesetPos()tomoveeachitemto
adifferentpositioninthescene.Theresultlookslikethis:
QPainterPathallowsyoutousepracticallyeverypaintoperationQtsupports.Forexample,
QGraphicsPathItemistheonlystandarditemabletodrawBeziercurvesinthescene,as
QPainterPathsupportsthem.RefertothedocumentationofQPainterPathformore
information.
Usingpainterpathsinthisexampleisveryefficient,becausewe
avoidedcreatingthousandsofindividuallineobjectsontheheap.
However,puttingalargepartofasceneinasingleitemmayreduce
theperformance.Whenpartsofthesceneareseparategraphics
items,Qtcanefficientlydeterminewhichitemsarenotvisibleand
skipdrawingthem.
Z-orderofitems
Haveyouwonderedwhathappenswhenmultipleitemsarepainted
inthesameareaofthescene?Let'strytodothis:
QGraphicsEllipseItem*item1=scene.addEllipse(0,0,100,50);
item1->setBrush(Qt::red);
QGraphicsEllipseItem*item2=scene.addEllipse(50,0,100,50);
item2->setBrush(Qt::green);
QGraphicsEllipseItem*item3=scene.addEllipse(0,25,100,50);
item3->setBrush(Qt::blue);
QGraphicsEllipseItem*item4=scene.addEllipse(50,25,100,50);
item4->setBrush(Qt::gray);
Bydefault,itemsarepaintedintheordertheywereadded,sothe
lastitemwillbedisplayedinfrontoftheothers:
However,youcanchangethez-orderbycallingthesetZValue()
function:
item2->setZValue(1);
Theseconditemisnowdisplayedinfrontoftheothers:
Itemswithahigherzvaluearedisplayedontopoftheitemswith
lowerzvalues.Thedefaultzvalueis0.Negativevaluesarealso
possible.Ifitemshavethesamezvalue,theorderofinsertion
decidestheplacement,anditemsaddedlateroverlapthoseadded
earlier.
Abilitytochangethez-orderofitemsisveryimportantwhen
developing2Dgames.Anyscenetypicallyconsistsofanumberof
layersthatmustbepaintedinaspecificorder.Youcansetazvalue
foreachitembasedonthelayerthisitembelongsto.
Theparent–childrelationshipbetweenitemsalsohasanimpactonthez-order.Children
aredisplayedontopoftheirparent.Additionally,ifanitemisdisplayedinfrontofanother
item,thechildrenoftheformerarealsodisplayedinfrontofthechildrenofthelatter.
Ignoringtransformations
Ifyoutrytozoominonourcustomrectanglesscene(forexample,
bycallingview.scale(4,4)),youwillnotethateverythingisscaled
proportionally,asyouwouldexpect.However,therearesituations
whereyoudon'twantsomeelementstobeaffectedbyscaleorother
transformations.Qtprovidesmultiplewaystodealwithit.
Ifyouwantlinestoalwayshavethesamewidth,regardlessofthe
zoom,youneedtomakethepencosmetic:
QPenpen=parent->pen();
pen.setCosmetic(true);
parent->setPen(pen);
Now,therectangleswillalwayshavelineswithone-pixelwidth,
regardlessoftheview'sscale(anti-aliasingcanstillblurthem,
though).It'salsopossibletohavecosmeticpenswithanywidth,but
usingtheminGraphicsViewisnotrecommended.
Anothercommonsituationwhereyoudon'twanttransformationto
applyisdisplayingtext.Rotatingandshearingtextusuallymakesit
unreadable,soyou'dusuallywanttomakeithorizontaland
untransformed.Let'strytoaddsometexttoourprojectandlookat
howwecansolvethisproblem.
Timeforaction–Addingtextto
acustomrectangle
Let'saddanumbertoeachofthecornercircles:
child->setPos(pos);
QGraphicsSimpleTextItem*text=
newQGraphicsSimpleTextItem(QString::number(i),child);
text->setBrush(Qt::green);
text->setPos(-text->boundingRect().width()/2,
-text->boundingRect().height()/2);
TheQString::number(i)functionreturnsthestringrepresentationof
numberi.Thetextitemisachildofthecircleitem,soitspositionis
relativetothecircle'soriginpoint(inourcase,itscenter).Aswe
sawearlier,thetextisdisplayedtothetop-leftoftheitem'sorigin,
soifwewanttocenterthetextwithinthecircle,weneedtoshiftit
upandrightbyhalfoftheitem'ssize.Nowthetextispositioned
androtatedalongwithitsparentcircle:
However,wedon'twantthetexttoberotated,soweneedtoenable
theItemIgnoresTransformationsflagforthetextitem:
text->setFlag(QGraphicsItem::ItemIgnoresTransformations);
text->setFlag(QGraphicsItem::ItemIgnoresTransformations);
Thisflagmakestheitemignoreanytransformationsofitsparent
itemsortheview.However,theoriginofitscoordinatesystemis
stilldefinedbythepositionofpos()intheparent'scoordinate
system.So,thetextitemwillstillfollowthecircle,butitwillno
longerbescaledorrotated:
However,nowwehitanotherproblem:thetextisnolonger
properlycenteredinthecircle.Itwillbecomemoreapparentifyou
scaletheviewagain.Whydidthathappen?With
theItemIgnoresTransformationsflag,ourtext->setPos(...)statementisno
longercorrect.Indeed,pos()usescoordinatesintheparent's
coordinatesystem,butweusedtheresultofboundingRect(),whichuses
theitem'scoordinatesystem.Thesetwocoordinatesystemswere
thesamebefore,butwiththeItemIgnoresTransformationsflagenabled,
theyarenowdifferent.
Toelaborateonthisproblem,let'sseewhathappenswiththe
coordinates(wewillconsideronlyxcoordinate,sinceybehavesthe
same).Let'ssaythatourtextitem'swidthiseightpixels,sothepos()
wesethasx=-4.Whennotransformationsareapplied,thispos()
resultsinshiftingthetexttotheleftbyfourpixels.If
theItemIgnoresTransformationsflagisdisabledandtheviewisscaledby2,
thetextisshiftedbyeightpixelsrelativetothecircle'scenter,but
thesizeofthetextitselfisnow16pixels,soit'sstillcentered.If
theItemIgnoresTransformationsflagisenabled,thetextisstillshiftedto
theleftbyeightpixelsrelativetothecircle'scenter(becausepos()
operatesintheparentitem'scoordinatesystem,andthecircleis
scaled),butthewidthoftheitemisnow8,becauseitignoresthe
scaleandsoit'snolongercentered.Whentheviewisrotated,the
resultisevenmoreincorrect,becausesetPos()willshifttheitemin
thedirectionthatdependsontherotation.Sincethetextitemitself
isnotrotated,wealwayswanttoshiftitupandleft.
Thisproblemwouldgoawayiftheitemwerealreadycentered
arounditsorigin.Unfortunately,QGraphicsSimpleTextItemcan'tdothis.
Now,ifitwereQGraphicsRectItem,doingthiswouldbeeasy,butnothing
stopsusfromaddingarectanglethatignorestransformationsand
thenaddingtextinsidethatrectangle!Let'sdothis:
QGraphicsSimpleTextItem*text=
newQGraphicsSimpleTextItem(QString::number(i));
QRectFtextRect=text->boundingRect();
textRect.translate(-textRect.center());
QGraphicsRectItem*rectItem=newQGraphicsRectItem(textRect,child);
rectItem->setPen(QPen(Qt::green));
rectItem->setFlag(QGraphicsItem::ItemIgnoresTransformations);
text->setParentItem(rectItem);
text->setPos(textRect.topLeft());
text->setBrush(Qt::green);
Inthiscode,wefirstcreateatextitem,butdon'tsetitsparent.
Next,wegettheboundingrectoftheitemthatwilltellushow
muchspacethetextneeds.Then,weshifttherectsothatitscenter
isattheoriginpoint(0,0).Nowwecancreatearectitemforthis
rectangle,setthecircleasitsparent,anddisabletransformations
fortherectitem.Finally,wesettherectitemastheparentofthe
textitemandchangethepositionofthetextitemtoplaceitinside
therectangle.
Therectangleisnowproperlypositionedatthecenterofthecircle,
andthetextitemalwaysfollowstherectangle,aschildrenusually
do:
Sincewedidn'toriginallywanttherectangle,wemaywanttohideit.Wecan'tuse
rectItem->hide()inthiscase,becausethatwouldalsoresultinhidingitschilditem(the
text).ThesolutionistodisablethepaintingoftherectanglebycallingrectItem-
>setPen(Qt::NoPen).
Analternativesolutiontothisproblemistotranslatethetextitem's
coordinatesysteminsteadofusingsetPos().QGraphicsItemdoesn'thave
adedicatedfunctionfortranslation,sowewillneedtouse
setTransform:
QTransformtransform;
transform.translate(-text->boundingRect().width()/2,
-text->boundingRect().height()/2);
text->setTransform(transform);
Contrarytowhatyouwouldexpect,ItemIgnoresTransformationsdoesn't
causetheitemtoignoreitsowntransformations,andthiscodewill
positionthetextcorrectlywithoutneedinganadditionalrectangle
item.
Findingitemsbyposition
Ifyouwanttoknowwhichitemisshownatacertainposition,you
canusetheQGraphicsScene::itemAt()functionthattakesthepositionin
thescene'scoordinatesystem(eitheraQPointFortwoqrealnumbers)
andthedevicetransformationobject(QTransform)thatcanbeobtained
usingtheQGraphicsView::transform()function.Thefunctionreturnsthe
topmostitematthespecifiedpositionoranullpointerifnoitem
wasfound.Thedevicetransformationonlymattersifyourscene
containsitemsthatignoretransformations.Ifyouhavenosuch
items,youcanusethedefault-constructedQTransformvalue:
QGraphicsItem*foundItem=scene.itemAt(scenePos,QTransform());
Ifyourscenecontainsitemsthatignoretransformations,itmaybe
moreconvenienttousetheQGraphicsView::itemAt()functionthat
automaticallytakesthedevicetransformintoaccount.Notethat
thisfunctionexpectsthepositiontobeintheviewport'scoordinate
system.
Ifyouwantallitemsthatarelocatedatsomeposition,sayincases
wheremultipleitemsareontopofeachother,orifyouneedto
searchforitemsinsomearea,usethe
QGraphicsScene::items()function.Itwillreturnalistofitemsdefinedby
thespecifiedarguments.Thisfunctionhasanumberofoverloads
thatallowyoutospecifyasinglepoint,arectangle,apolygon,ora
painterpath.ThedeviceTransformargumentworksinthesamewayas
fortheQGraphicsScene::itemAt()functiondiscussedearlier.Themode
argumentallowsyoutoalterhowtheitemsintheareawillbe
determined.Thefollowingtableshowsthedifferentmodes:
Mod Meaning
e
Qt::Co
ntains
ItemBo
unding
Rect
Theitem'sboundingrectanglemustbecompletelyinside
theselectionarea.
Qt::In
tersec
tsItem
Boundi
ngRect
SimilartoQt::ContainsItemBoundingRectbutalsoreturnsitems
whoseboundingrectanglesintersectwiththeselection
area.
Qt::Co
ntains
ItemSh
ape
Theitem'sshapemustbecompletelyinsidetheselection
area.Theshapemaydescribetheitem'sboundariesmore
preciselythantheboundingrectangle,butthisoperation
ismorecomputationallyintensive.
Qt::In
tersec
tsItem
Shape
SimilartoQt::ContainsItemShapebutalsoreturnsitemswhose
shapesintersectwiththeselectionarea.
Theitems()functionsortsitemsaccordingtotheirstacking
order.Theorderargumentallowsyoutochoosetheorderinwhich
theresultswillbereturned.Qt::DescendingOrder(default)willplacethe
topmostitematthebeginning,andQt::AscendingOrderwillresultina
reversedorder.
TheviewalsoprovidesasimilarQGraphicsView::items()functionthat
operatesinviewportcoordinates.
Showingspecificareasofthe
scene
Assoonasthescene'sboundingrectangleexceedstheviewport's
size,theviewwillshowscrollbars.Besidesusingthemwiththe
mousetonavigatetoaspecificitemorpointonthescene,youcan
alsoaccessthembycode.SincetheviewinheritsQAbstractScrollArea,
youcanuseallitsfunctionsforaccessingthescrollbars;
horizontalScrollBar()andverticalScrollBar()returnapointertoQScrollBar,
andthusyoucanquerytheirrangewithminimum()andmaximum().By
invokingvalue()andsetValue(),yougetandcansetthecurrentvalue,
whichresultsinscrollingthescene.
However,normally,youdonotneedtocontrolfreescrollinginside
theviewfromyoursourcecode.Thenormaltaskwouldbetoscroll
toaspecificitem.Inordertodothat,youdonotneedtodoany
calculationsyourself;theviewoffersaprettysimplewaytodothat
foryou—centerOn().WithcenterOn(),theviewensuresthattheitem,
whichyouhavepassedasanargument,iscenteredontheview
unlessitistooclosetothescene'sborderorevenoutside.Then,the
viewtriestomoveitasfaraspossibleonthecenter.The
centerOn()functiondoesnotonlytakeaQGraphicsItemitemasargument;
youcanalsocenteronaQPointFpointerorasaconvenienceonanx
andycoordinate.
Ifyoudonotcarewhereanitemisshown,youcansimplycall
ensureVisible()withtheitemasanargument.Then,theviewscrolls
thesceneaslittleaspossiblesothattheitem'scenterremainsor
becomesvisible.Asasecondandthirdargument,youcandefinea
horizontalandverticalmargin,whichareboththeminimumspace
betweentheitem'sboundingrectangleandtheview'sborder.Both
valueshave50pixelsastheirdefaultvalue.BesidesaQGraphicsItem
item,youcanalsoensurethevisibilityofaQRectFelement(ofcourse,
thereisalsotheconveniencefunctiontakingfourqrealelements).
Ifyouneedtoensuretheentirevisibilityofanitem,useensureVisible(item-
>boundingRect())(sinceensureVisible(item)onlytakestheitem'scenterintoaccount).
centerOn()andensureVisible()onlyscrollthescenebutdonotchangeits
transformationstate.Ifyouabsolutelywanttoensurethevisibility
ofanitemorarectanglethatexceedsthesizeoftheview,youhave
totransformthesceneaswell.Withthistask,againtheviewwill
helpyou.BycallingfitInView()withQGraphicsItemoraQRectFelementas
anargument,theviewwillscrollandscalethescenesothatitfitsin
theviewportsize.
Asasecondargument,youcancontrolhowthescalingisdone.You
havethefollowingoptions:
Value Description
Qt::Ignore
AspectRati
o
Thescalingisdoneabsolutelyfreelyregardlessofthe
item'sorrectangle'saspectratio.
Qt::KeepAs
pectRatio
Theitem'sorrectangle'saspectratioistakeninto
accountwhiletryingtoexpandasfaraspossiblewhile
respectingtheviewport'ssize.
Qt::KeepAs
pectRatioB
yExpanding
Theitem'sorrectangle'saspectratioistakeninto
account,buttheviewtriestofillthewholeviewport's
sizewiththesmallestoverlap.
ThefitInView()functiondoesnotonlyscalelargeritemsdowntofit
theviewport,italsoenlargesitemstofillthewholeviewport.The
followingdiagramillustratesthedifferentscalingoptionsforan
itemthatisenlarged(thecircleontheleftistheoriginalitem,and
theblackrectangleistheviewport):
Savingascenetoanimagefile
We'veonlydisplayedoursceneintheviewsofar,butitisalso
possibletorenderittoanimage,aprinter,oranyotherobjectQt
canuseforpainting.Let'ssaveourscenetoaPNGfile:
QRectrect=scene.sceneRect().toAlignedRect();
QImageimage(rect.size(),QImage::Format_ARGB32);
image.fill(Qt::transparent);
QPainterpainter(&image);
scene.render(&painter);
image.save("scene.png");
Whatjusthappened?
First,youdeterminedtherectangleofthescenewithsceneRect().
SincethisreturnsaQRectFparameterandQImagecanonlyhandleQRect,
youtransformeditontheflybycallingtoAlignedRect().Thedifference
betweenthetoRect()functionandtoAlignedRect()isthattheformer
roundstothenearestinteger,whichmayresultinasmaller
rectangle,whereasthelatterexpandstothesmallestpossible
rectanglecontainingtheoriginalQRectFparameter.
Then,youcreatedaQImagefilewiththesizeofthealignedscene's
rectangle.Astheimageiscreatedwithuninitializeddata,youneed
tocallfill()withQt::transparenttoreceiveatransparentbackground.
Youcanassignanycoloryoulikeasanargumentbothasavalueof
Qt::GlobalColorenumerationandanordinaryQColorobject;QColor(0,0,
255)willresultinabluebackground.Next,youcreateaQPainterobject
thatpointstotheimage.Thispainterobjectisthenusedinthe
scene'srender()functiontodrawthescene.Afterthat,allyouhaveto
doisusethesave()functiontosavetheimagetoaplaceofyour
choice.Theformatoftheoutputfileisdeterminedbyitsextension.
Qtsupportsavarietyofformats,andQtpluginscanaddsupportfor
newformats.Sincewehaven'tspecifiedapath,theimagewillbe
savedintheapplication'sworkingdirectory(whichisusuallythe
builddirectory,unlessyouchangeditusingtheProjectspaneofQt
Creator).Youcanalsospecifyanabsolutepath,suchas
/path/to/image.png.
Ofcourse,you'llneedtoconstructapaththat'svalidonthecurrentsysteminsteadofhard-
codingitinthesources.Forexample,youcanusetheQFileDialog::getSaveFileName()
functiontoasktheuserforapath.
Haveagohero–Rendering
onlyspecificpartsofascene
Thisexampledrawsthewholescene.Ofcourse,youcanalsorender
onlyspecificpartsofthesceneusingtheotherargumentsofrender().
Wewillnotgointothishere,butyoumaywanttotryitasan
exercise.
Customitems
Aswealreadysaw,GraphicsViewprovidesalotofuseful
functionalitythatcoversmosttypicalusecases.However,thereal
powerofQtisitsextensibility,andGraphicsViewallowsusto
createcustomsubclassesofQGraphicsItemtoimplementitemsthatare
tailoredforyourapplication.Youmaywanttoimplementacustom
itemclasswhenyouneedtodothefollowing:
Paintsomethingthatisnotpossibleordifficulttodowith
standarditemclasses
Implementsomelogicrelatedtotheitem,forexample,add
yourownmethods
Handleeventsinindividualitems
Inournextsmallproject,wewillcreateanitemthatcandrawa
graphofthesinefunctionsin(x)andimplementsomeevent
handling.
Timeforaction–Creatinga
sinegraphproject
UseQtCreatortocreateanewQtWidgetsprojectandnameit
sine_graph.OntheClassInformationpageofthewizard,selectQWidget
asthebaseclassandinputViewastheclassname.Uncheck
theGenerateformcheckboxandfinishthewizard.
WewanttheViewclasstobethegraphicsview,soyouneedto
changethebaseclasstoQGraphicsView(thewizarddoesn'tsuggestsuch
anoption).Forthis,edittheclassdeclarationtolooklikeclassView:
publicQGraphicsView...andtheconstructorimplementationtolooklike
View::View(QWidget*parent):QGraphicsView(parent)....
Next,edittheViewconstructortoenableanti-aliasingandsetanew
graphicssceneforourview:
setRenderHint(QPainter::Antialiasing);
setScene(newQGraphicsScene);
Theviewdoesn'tdeletetheassociatedsceneondestruction
(becauseyoumayhavemultipleviewsforthesamescene),soyou
shoulddeletethescenemanuallyinthedestructor:
deletescene();
Youcantrytoruntheapplicationandcheckthatitdisplaysan
emptyview.
Timeforaction–Creatinga
graphicsitemclass
AskQtCreatortoaddanewC++classtotheproject.InputSineItem
astheclassname,leave<Custom>intheBaseclassdrop-downlist,
andinputQGraphicsIteminthefieldbelowit.Finishthewizardand
openthecreatedsineitem.hfile.
SetthetextcursorinsideQGraphicsItemintheclassdeclarationand
pressAlt+Enter.Atfirst;QtCreatorwillsuggestthatyouAdd
#include<QGraphicsItem>.ConfirmthatandpressAlt+Enteron
QGraphicsItemagain.Now,QtCreatorshouldsuggestthatyou
selectInsertVirtualFunctionsofBaseClasses.Whenyouselectthis
option,aspecialdialogwillappear:
Thefunctionlistcontainsallvirtualfunctionsofthebaseclass.The
purevirtualfunctions(whichmustbeimplementedifyouwantto
createobjectsoftheclass)areenabledbydefault.Checkthat
everythingissetasintheprecedingscreenshot,andthenclick
onOK.Thisconvenientoperationaddsdeclarationand
implementationoftheselectedvirtualfunctionstothesourcefiles
ofourclass.Youcanwritethemmanuallyinstead,ifyouwant.
Let'seditsineitem.cpptoimplementthetwopurevirtualfunctions.
Firstofall,acoupleofconstantsatthetopofthefile:
staticconstfloatDX=1;
staticconstfloatMAX_X=50;
Inourgraph,xwillvaryfrom0toMAX_X,andDXwillbethedifference
betweenthetwoconsequentpointsofthegraph.Asyoumayknow,
sin(x)canhavevaluesfrom-1to1.Thisinformationisenoughto
implementtheboundingRect()function:
QRectFSineItem::boundingRect()const
{
returnQRectF(0,-1,MAX_X,2);
}
Thisfunctionsimplyreturnsthesamerectangleeverytime.Inthis
rectangle,xchangesfrom0toMAX_X,andychangesfrom-1to1.This
returnedrectangleisapromisetothescenethattheitemwillonly
paintinthisarea.Thescenereliesonthecorrectnessofthat
information,soyoushouldstrictlyobeythatpromise.Otherwise,
thescenewillbecomeclutteredupwithrelicsofyourdrawing!
Now,implementthepaint()function,asfollows:
voidSineItem::paint(QPainter*painter,
constQStyleOptionGraphicsItem*option,QWidget*widget)
{
QPenpen;
pen.setCosmetic(true);
painter->setPen(pen);
constintsteps=qRound(MAX_X/DX);
QPointFpreviousPoint(0,sin(0));
for(inti=1;i<steps;++i){
constfloatx=DX*i;
QPointFpoint(x,sin(x));
painter->drawLine(previousPoint,point);
previousPoint=point;
}
Q_UNUSED(option)
Q_UNUSED(widget)
}
Add#include<QtMath>tothetopsectionofthefiletomakemath
functionsavailable.
Whatjusthappened?
Whentheviewneedstodisplaythescene,itcallsthepaint()function
ofeachvisibleitemandprovidesthreearguments:aQPainterpointer
thatshouldbeusedforpainting,aQStyleOptionGraphicsItempointerthat
containspainting-relatedparametersforthisitem,andanoptional
QWidgetpointerthatmaypointtothecurrentlypaintedwidget.Inthe
implementationofthefunction,westartwithsettingacosmeticpen
inthepaintersothatthelinewidthofourgraphisalways1.Next,we
calculatethenumberofpointsinthegraphandsaveittothesteps
variable.Then,wecreateavariabletostorethepreviouspointof
thegraphandinitializeitwiththepositionofthefirstpointofthe
graph(correspondingtox=0).Next,weiteratethroughpoints,
calculatexandyforeachpoint,andthenusethepainterobjectto
drawalinefromthepreviouspointtothecurrentpoint.Afterthis,
weupdatethevalueofthepreviousPointvariable.WeusetheQ_UNUSED()
macrotosuppresscompilerwarningsaboutunusedargumentsand
toindicatethatwe,intentionally,didn'tusethem.
EdittheconstructorofourViewclasstocreateaninstanceofournew
item:
SineItem*item=newSineItem();
scene()->addItem(item);
Theapplicationshoulddisplaythesinegraphnow,butitisvery
small:
Weshouldaddawayforuserstoscaleourviewusingthemouse
wheel.However,beforewegettothis,youneedtolearnalittle
moreabouteventhandling.
Events
AnyGUIapplicationneedstoreacttotheinputevents.Weare
alreadyfamiliarwiththesignalsandslotsmechanisminQObject-
basedclasses.However,QObjectisnotexactlyalightweightclass.
Signalsandslotsarepowerfulandconvenientforconnectingparts
oftheapplication,butinvokingasignalforprocessingeach
keyboardpressormousemovewillbetooinefficient.Toprocess
suchevents,QthasaspecialsystemthatusestheQEventclass.
Thedispatcheroftheeventsistheeventloop.AlmostanyQt
applicationusesthemaineventloopthatisstartedbycalling
QCoreApplication::execattheendofthemain()function.Whilethe
applicationisrunning,thecontrolflowiseitherinyourcode(that
is,intheimplementationofanyfunctionintheproject)orinthe
eventloop.Whentheoperatingsystemoracomponentofthe
applicationaskstheeventlooptoprocessanevent,itdetermines
thereceiverandcallsavirtualfunctionthatcorrespondstothe
eventtype.AQEventobjectcontaininginformationabouttheeventis
passedtothatfunction.Thevirtualfunctionhasachoicetoaccept
orignoretheevent.Iftheeventwasnotaccepted,theeventis
propagatedtotheparentobjectinthehierarchy(forexample,
fromawidgettoitsparentwidget,andfromagraphicsitemtothe
parentitem).YoucansubclassaQtclassandreimplementavirtual
functiontoaddcustomeventsprocessing.
Thefollowingtableshowsthemostusefulevents:
Eventtypes Description
QEvent::KeyPress,
QEvent::KeyRelease
Akeyboardbuttonwaspressedor
released.
QEvent::MouseButtonPress,
QEvent::MouseButtonRelease,
QEvent::MouseButtonDblClic
k
Themousebuttonswerepressedor
released.
QEvent::Wheel Themousewheelwasrolled.
QEvent::Enter Themousecursorenteredtheobject's
boundaries.
QEvent::MouseMove Themousecursorwasmoved.
QEvent::Leave Themousecursorlefttheobject's
boundaries.
QEvent::Resize
Thewidgetwasresized(forexample,
becausetheuserresizedthewindowor
thelayoutchanged).
QEvent::Close Theuserattemptedtoclosethewidget's
window.
QEvent::ContextMenu
Theuserrequestedacontextmenu(the
exactactiondependsontheoperating
system'swaytoopenthecontextmenu).
QEvent::Paint Thewidgetneedstoberepainted.
QEvent::DragEnter,
QEvent::DragLeave,
QEvent::DragMove,
QEvent::Drop
Theuserperformsadraganddrop
action.
QEvent::TouchBegin,
QEvent::TouchUpdate,
QEvent::TouchEnd,
QEvent::TouchCancel
Atouchscreenoratrackpadreportedan
event.
EacheventtypehasacorrespondingclassthatinheritsQEvent(for
example,QMouseEvent).Manyeventtypeshavethededicatedvirtual
function,forexample,QWidget::mousePressEventand
QGraphicsItem::mousePressEvent.Moreexoticeventsmustbeprocessedby
re-implementingtheQWidget::event(orQGraphicsItem::sceneEvent)function
thatreceivesallevents,andusingevent->type()tochecktheevent
type.
Eventsdispatchedinthegraphicsscenehavespecialtypes(for
example,QEvent::GraphicsSceneMousePress)andspecialclasses(for
example,QGraphicsSceneMouseEvent)becausetheyhaveanextendedsetof
informationabouttheevent.Inparticular,mouseeventscontain
informationaboutthecoordinatesintheitem'sandthescene's
coordinatesystems.
Timeforaction–Implementing
theabilitytoscalethescene
Let'sallowtheusertoscalethesceneusingthemousewheelonthe
view.Switchtotheview.hfileandaddadeclarationandan
implementationofthewheelEvent()virtualfunctionusingthesame
methodwejustusedintheSineItemclass.Writethefollowingcodein
theview.cppfile:
voidView::wheelEvent(QWheelEvent*event)
{
QGraphicsView::wheelEvent(event);
if(event->isAccepted()){
return;
}
constqrealfactor=1.1;
if(event->angleDelta().y()>0){
scale(factor,factor);
}else{
scale(1/factor,1/factor);
}
event->accept();
}
Ifyouruntheapplicationnow,youcanscalethesinegraphusing
themousewheel.
Whatjusthappened?
Whenaneventoccurs,Qtcallsthecorrespondingvirtualfunction
inthewidgetinwhichtheeventoccurred.Inourcase,wheneverthe
userusesthemousewheelonourview,thewheelEvent()virtual
functionwillbecalled,andtheeventargumentwillholdinformation
abouttheevent.
Inourimplementation,westartwithcallingthebaseclass's
implementation.Itisveryimportanttodothiswheneveryou
reimplementavirtualfunction,unlessyouwantthedefault
behaviortobecompletelydisabled.Inourcase,
QGraphicsView::wheelEvent()willpasstheeventtothescene,andifwe
forgettocallthisfunction,neitherthescenenoranyofitsitemswill
receiveanywheelevents,whichcanbeverymuchunwantedin
somecases.
Afterthedefaultimplementationiscomplete,weusetheisAccepted()
functiontocheckwhetheraneventwasacceptedbythesceneor
anyitems.Theeventwillberejectedbydefault,butifwelateradd
someitemthatcanprocesswheelevents(forexample,atext
documentwithitsownscrollbar),itwillreceiveandacceptthe
event.Inthatcase,wedon'twanttoperformanyotheractionbased
onthisevent,asit'susuallydesirablethatanyeventisonly
processed(andaccepted)inonelocation.
Insomecases,youmaywantyourcustomimplementationtotakepriorityoverthedefault
one.Inthatcase,movethecalltothedefaultimplementationtotheendofthefunction
body.Whenyouwanttopreventaparticulareventfrombeingdispatchedtothescene,use
anearlyreturntopreventthedefaultimplementationfromexecuting.
Thefactorparameterforthezoomingcanbefreelydefined.Youcan
alsocreateagetterandsettermethodforit.Forus,1.1willdothe
work.Withevent->angleDelta(),yougetthedistanceofthemouse's
wheelrotationasaQPointpointer.Sinceweonlycareaboutvertical
scrolling,justtheyaxisisrelevantforus.Inourexample,wealso
donotcareabouthowfarthewheelwasturnedbecause,normally,
everystepisdeliveredseparatelytowheelEvent().However,ifyou
shouldneedit,it'sineighthsofadegree,andsincemostmouses
workingeneralstepsof15degrees,thevalueshouldbe120or-120,
dependingonwhetheryoumovethewheelforwardorbackward.
Onaforwardwheelmove,ify()isgreaterthanzero,wezoomin
usingthealreadyfamiliarscale()function.Otherwise,ifthewheel
wasmovedbackward,wezoomout.Finally,weaccepttheevent,
indicatingthattheuser'sinputwasunderstood,andthereisno
needtopropagatetheeventtoparentwidgets(althoughtheview
currentlydoesn'thaveaparent).That'sallthereistoit.
Whenyoutrythisexample,youwillnotethat,whilezooming,the
viewzoomsinandoutonthecenteroftheview,whichisthedefault
behaviorfortheview.Youcanchangethisbehaviorwith
setTransformationAnchor().QGraphicsView::AnchorViewCenteris,asdescribed,the
defaultbehavior.WithQGraphicsView::NoAnchor,thezoomcenterisinthe
top-leftcorneroftheview,andthevalueyouprobablywanttouse
isQGraphicsView::AnchorUnderMouse.Withthatoption,thepointunderthe
mousebuildsthecenterofthezoomingandthusstaysatthesame
positioninsidetheview.
Timeforaction–Takingthe
zoomlevelintoaccount
Ourgraphcurrentlycontainspointswithintegerxvaluesbecause
wesetDX=1.Thisisexactlywhatwewantforthedefaultlevelof
zoom,butoncetheviewiszoomedin,itbecomesapparentthatthe
graph'slineisnotsmooth.WeneedtochangeDXbasedonthe
currentzoomlevel.Wecandothisbyaddingthefollowingcodeto
thebeginningofthepaint()function():
constqrealdetail=
QStyleOptionGraphicsItem::levelOfDetailFromTransform(
painter->worldTransform());
constqrealdx=1/detail;
DeletetheDXconstantandreplaceDXwithdxintherestofthecode.
Now,whenyouscaletheview,thegraph'slinekeepsbeingsmooth
becausethenumberofpointsincreasesdynamically.The
levelOfDetailFromTransformhelperfunctionexaminesthevalueofthe
painter'stransformation(whichisacombinationofall
transformationsappliedtotheitem)andreturnsthelevelof
detail.Iftheitemiszoomedin2:1,thelevelofdetailis2,andifthe
itemiszoomedout1:2,thelevelofdetailis0.5.
Timeforaction–Reactingto
anitem'sselectionstate
Standarditems,whenselected,changeappearance(forexample,
theoutlineusuallybecomesdashed).Whenwe'recreatingacustom
item,weneedtoimplementthisfeaturemanually.Let'smakeour
itemselectableintheViewconstructor:
SineItem*item=newSineItem();
item->setFlag(QGraphicsItem::ItemIsSelectable);
Now,let'smakethegraphlinegreenwhentheitemisselected:
if(option->state&QStyle::State_Selected){
pen.setColor(Qt::green);
}
painter->setPen(pen);
Whatjusthappened?
Thestatevariableisabitmaskholdingthepossiblestatesofthe
item.YoucancheckitsvalueagainstthevaluesoftheQStyle::StateFlag
parameterusingbitwiseoperators.Intheprecedingcase,thestate
variableischeckedagainsttheState_Selectedparameter.Ifthisflagis
set,weusegreencolorforthepen.
ThetypeofstateisQFlags<StateFlag>.So,insteadofusingthebitwiseoperatortotest
whetheraflagisset,youcanusetheconvenientfunctiontestFlag().
Usedwiththeprecedingexample,itwouldbeasfollows:
if(option->state.testFlag(QStyle::State_Selected)){
Themostimportantstatesyoucanusewithitemsaredescribedin
thefollowingtable:
S
ta
te
Description
St
at
e_
En
ab
le
d
Indicatesthattheitemisenabled.Iftheitemisdisabled,you
maywanttodrawitasgrayedout.
St
at
e_
Ha
sF
oc
Indicatesthattheitemhastheinputfocus.Toreceivethis
state,theitemneedstohavetheItemIsFocusableflagset.
us
St
at
e_
Mo
us
eO
ve
r
Indicatesthatthecursoriscurrentlyhoveringovertheitem.
Toreceivethisstate,theitemneedstohavethe
acceptHoverEventsvariablesettotrue.
St
at
e_
Se
le
ct
ed
Indicatesthattheitemisselected.Toreceivethisstate,the
itemneedstohavetheItemIsSelectableflagset.Thenormal
behaviorwouldbetodrawadashedlinearoundtheitemas
aselectionmarker.
Besidesthestate,QStyleOptionGraphicsItemoffersmuchmore
informationaboutthecurrentlyusedstyle,suchasthepaletteand
thefontused,accessiblethroughtheQStyleOptionGraphicsItem::palette
andQStyleOptionGraphicsItem::fontMetricsparameters,respectively.Ifyou
aimforstyle-awareitems,takeadeeperlookatthisclassinthe
documentation.
Timeforaction–Event
handlinginacustomitem
Items,likewidgets,canreceiveeventsinvirtualfunctions.Ifyou
clickonascene(tobeprecise,youclickonaviewthatpropagates
theeventtothescene),thescenereceivesthemousepressevent,
anditthenbecomesthescene'sresponsibilitytodeterminewhich
itemwasmeantbytheclick.
Let'soverridetheSineItem::mousePressEventfunctionthatiscalledwhen
theuserpressesamousebuttoninsidetheitem:
voidSineItem::mousePressEvent(QGraphicsSceneMouseEvent*event)
{
if(event->button()&Qt::LeftButton){
floatx=event->pos().x();
QPointFpoint(x,sin(x));
staticconstfloatr=0.3;
QGraphicsEllipseItem*ellipse=
newQGraphicsEllipseItem(-r,-r,2*r,2*r,this);
ellipse->setPen(Qt::NoPen);
ellipse->setBrush(QBrush(Qt::red));
ellipse->setPos(point);
event->accept();
}else{
event->ignore();
}
}
Whenamousepresseventoccurs,thisfunctioniscalledandthe
passedeventobjectcontainsinformationabouttheevent.Inour
case,wecheckwhethertheleftmousebuttonwaspressedanduse
theevent->pos()functionthatreturnscoordinatesoftheclickedpoint
intheitem'scoordinatesystem.Inthisexample,weignoredthey
coordinateandusedthexcoordinatetofindthecorresponding
pointonourgraph.Then,wesimplycreatedachildcircleitemthat
showsthatpoint.Weaccepttheeventifwedidunderstandtheaction
performedandignoreitifwedon'tknowwhatitmeanssothatitcan
bepassedtoanotheritem.Youcanruntheapplicationandclickon
thegraphtoseethesecircles.Notethatwhenyouclickoutsideof
thegraph'sboundingrect,thescenedoesn'tdispatchtheeventto
ouritem,anditsmousePressEvent()functionisnotcalled.
Theeventobjectalsocontainsthebutton()functionthatreturnsthe
buttonthatwaspressed,andthescenePos()functionthatreturnsthe
clickedpointinthescene'scoordinatesystem.Thescene's
responsibilityfordeliveringeventsdoesnotonlyapplytomouse
events,butalsotokeyeventsandallothersortsofevents.
Timeforaction–Implementing
theabilitytocreateanddelete
elementswithmouse
Let'sallowtheuserstocreatenewinstancesofoursineitemwhen
theyclickontheviewwiththeleftmousebuttonanddeletethe
itemsiftheyusetherightmousebutton.Reimplement
theView::mousePressEventvirtualfunction,asfollows:
voidView::mousePressEvent(QMouseEvent*event)
{
QGraphicsView::mousePressEvent(event);
if(event->isAccepted()){
return;
}
switch(event->button()){
caseQt::LeftButton:{
SineItem*item=newSineItem();
item->setPos(mapToScene(event->pos()));
scene()->addItem(item);
event->accept();
break;
}
caseQt::RightButton:{
QGraphicsItem*item=itemAt(event->pos());
if(item){
deleteitem;
}
event->accept();
break;
}
default:
break;
}
}
Here,wefirstcheckwhethertheeventwasacceptedbythesceneor
anyofitsitems.Ifnot,wedeterminewhichbuttonwaspressed.For
theleftbutton,wecreateanewitemandplaceitinthe
correspondingpointofthescene.Fortherightbutton,wesearch
foranitematthatpositionanddeleteit.Inbothcases,weaccept
theevent.Whenyouruntheapplication,youwillnotethatifthe
userclicksonanexistingitem,anewcirclewillbeadded,andifthe
userclicksoutsideofanyitems,anewsineitemwillbeadded.
That'sbecauseweproperlysetandreadtheacceptedpropertyofthe
event.
Youmaynotethatthescenejumpswithintheviewwhenweaddanewitem.Thisiscaused
bychangesofthescenerect.Topreventthis,youcansetaconstantrectusing
setSceneRect()orchangethealignmentusingsetAlignment(Qt::AlignTop|Qt::AlignLeft)
intheview'sconstructor.
Timeforaction–Changingthe
item'ssize
Ourcustomgraphicsitemalwaysdisplaysthegraphforxvalues
between0and50.Itwouldbeneattomakethisaconfigurable
setting.Declareaprivatefloatm_maxXfieldintheSineItemclass,remove
theMAX_Xconstant,andreplaceitsuseswithm_maxXintherestofthe
code.Asalways,youmustsettheinitialvalueofthefieldinthe
constructor,orbadthingscanhappen.Finally,implementagetter
andasetterforit,asshown:
floatSineItem::maxX()
{
returnm_maxX;
}
voidSineItem::setMaxX(floatvalue)
{
if(m_maxX==value){
return;
}
prepareGeometryChange();
m_maxX=value;
}
Theonlynon-trivialparthereistheprepareGeometryChange()call.This
methodisinheritedfromQGraphicsItemandnotifiesthescenethatour
boundingRect()functionwillreturnadifferentvalueonthenext
update.Thescenecachesboundingrectanglesoftheitems,soifyou
don'tcallprepareGeometryChange(),thechangeoftheboundingrectangle
maynottakeeffect.Thisactionalsoschedulesanupdateforour
item.
Whentheboundingrectdoesnotchangebuttheactualcontentoftheitemchanges,you
needtocallupdate()ontheitemtonotifythescenethatitshouldrepainttheitem.
Haveagohero–Extendingthe
item'sfunctionality
TheabilitiesofSineItemarestillprettylimited.Asanexercise,you
cantrytoaddanoptiontochangetheminimumxvalueofthe
graphorsetadifferentpen.Youcanevenallowtheusertospecify
anarbitraryfunctionpointertoreplacethesin()function.However,
keepinmindthattheboundingrectoftheitemdependsonthe
valuerangeofthefunction,soyouneedtoupdatetheitem's
geometryaccurately.
WidgetsinsideGraphicsView
InordertoshowaneatfeatureofGraphicsView,takealookatthe
followingcodesnippet,whichaddsawidgettothescene:
QSpinBox*box=newQSpinBox;
QGraphicsProxyWidget*proxyItem=newQGraphicsProxyWidget;
proxyItem->setWidget(box);
scene()->addItem(proxyItem);
proxyItem->setScale(2);
proxyItem->setRotation(45);
First,wecreateaQSpinBoxandaQGraphicsProxyWidgetelement,whichact
ascontainersforwidgetsandindirectlyinheritQGraphicsItem.Then,
weaddthespinboxtotheproxywidgetbycallingaddWidget().When
QGraphicsProxyWidgetgetsdeleted,itcallsdeleteonallassignedwidgets,
sowedonothavetoworryaboutthatourselves.Thewidgetyou
addshouldbeparentlessandmustnotbeshownelsewhere.After
settingthewidgettotheproxy,youcantreattheproxywidgetlike
anyotheritem.Next,weaddittothesceneandapplya
transformationfordemonstration.Asaresult,wegetthis:
Beawarethat,originally,GraphicsViewwasn'tdesignedfor
holdingwidgets.Sowhenyouaddalotofwidgetstothescene,you
willquicklynoticeperformanceissues,butinmostsituations,it
shouldbefastenough.
Ifyouwanttoarrangesomewidgetsinalayout,youcanuse
QGraphicsAnchorLayout,QGraphicsGridLayout,orQGraphicsLinearLayout.Createall
widgets,createalayoutofyourchoice,addthewidgetstothat
layout,andsetthelayouttoaQGraphicsWidgetelement,whichisthe
baseclassforallwidgetsandis,easilyspoken,theQWidgetequivalent
forGraphicsViewbycallingsetLayout():
QGraphicsProxyWidget*edit=scene()->addWidget(
newQLineEdit(tr("SomeText")));
QGraphicsProxyWidget*button=scene()->addWidget(
newQPushButton(tr("Clickme!")));
QGraphicsLinearLayout*layout=newQGraphicsLinearLayout;
layout->addItem(edit);
layout->addItem(button);
QGraphicsWidget*graphicsWidget=newQGraphicsWidget;
graphicsWidget->setLayout(layout);
scene()->addItem(graphicsWidget);
Thescene'saddWidget()functionisaconveniencefunctionand
behavessimilartoaddRect,asshowninthefollowingcodesnippet:
QGraphicsProxyWidget*proxy=newQGraphicsProxyWidget(0);
proxy->setWidget(newQLineEdit(QObject::tr("SomeText")));
scene()->addItem(proxy);
Theitemwiththelayoutwilllooklikethis:
Optimization
Whenaddingmanyitemstoasceneorusingitemswith
complexpaint()functions,theperformanceofyourapplicationmay
decrease.WhiledefaultoptimizationsofGraphicsViewaresuitable
formostcases,youmayneedtotweakthemtoachievebetter
performance.Let'snowtakealookatsomeoftheoptimizationswe
canperformtospeedupthescene.
Abinaryspacepartitiontree
Thesceneconstantlykeepsarecordofthepositionoftheiteminits
internalbinaryspacepartitiontree.Thus,oneverymoveofanitem,
thescenehastoupdatethetree,anoperationthatcanbecomequite
time-consuming,andalsomemoryconsuming.Thisisespecially
trueofsceneswithalargenumberofanimateditems.Ontheother
hand,thetreeenablesyoutofindanitem(forexample,withitems()
oritemAt())incrediblyquickly,evenifyouhavethousandsofitems.
Sowhenyoudonotneedanypositionalinformationaboutthe
items—thisalsoincludescollisiondetection—youcandisablethe
indexfunctionbycallingsetItemIndexMethod(QGraphicsScene::NoIndex).Be
aware,however,thatacalltoitems()oritemAt()resultsinaloop
throughallitemsinordertodothecollisiondetection,whichcan
causeperformanceproblemsforsceneswithmanyitems.Ifyou
cannotrelinquishthetreeintotal,youcanstilladjustthedepthof
thetreewithsetBspTreeDepth(),takingthedepthasanargument.By
default,thescenewillguessareasonablevalueafterittakesseveral
parameters,suchasthesizeandthenumberofitems,intoaccount.
Cachingtheitem'spaint
function
Ifyouhaveitemswithatime-consumingpaintfunction,youcan
changetheitem'scachemode.Bydefault,norenderingiscached.
WithsetCacheMode(),youcansetthemodetoeitherItemCoordinateCacheor
DeviceCoordinateCache.Theformerrenderstheiteminacacheofagiven
QSizeelement.Thesizeofthatcachecanbecontrolledwiththe
secondargumentofsetCacheMode(),sothequalitydependsonhow
muchspaceyouassign.Thecacheisthenusedforeverysubsequent
paintcall.Thecacheisevenusedforapplyingtransformations.If
thequalitydeterioratestoomuch,justadjusttheresolutionby
callingsetCacheMode()again,butwithalargerQSizeelement.
DeviceCoordinateCache,ontheotherhand,doesnotcachetheitemonan
itembasebutonadevicelevel.Thisis,therefore,optimalforitems
thatdonotgettransformedallthetimebecauseeverynew
transformationwillcauseanewcaching.Movingtheitem,however,
doesnotinvalidatethecache.Ifyouusethiscachemode,youdo
nothavetodefinearesolutionwiththesecondargument.The
cachingisalwaysperformedatmaximumquality.
Optimizingtheview
Sincewearetalkingabouttheitem'spaint()function,let'stouchon
somethingrelated.Bydefault,theviewensuresthatthepainter
stateissavedbeforecallingtheitem'spaintfunctionandthatthe
stategetsrestoredafterward.Thiswillendupsavingandrestoring
thepainterstate,say50times,ifyouhaveascenewith50items.
However,youcandisablethisbehaviorbycalling
setOptimizationFlag(DontSavePainterState,true)ontheview.Ifyoudothis,it
isnowyourresponsibilitytoensurethatanypaint()functionthat
changesthestateofthepainter(includingpen,brush,
transformation,andmanyotherproperties)mustrestorethe
previousstateattheend.Ifyoupreventautomaticsavingand
restoring,keepinmindthatnowthestandarditemswillalterthe
painterstate.Soifyouusebothstandardandcustomitems,either
staywiththedefaultbehaviororsetDontSavePainterState,butthenset
upthepenandbrushwithadefaultvalueineachitem'spaint
function.
AnotherflagthatcanbeusedwithsetOptimizationFlag()is
DontAdjustForAntialiasing.Bydefault,theviewadjuststhepaintingarea
ofeachitembytwopixelsinalldirections.Thisisusefulbecause
whenonepaintsanti-aliased,oneeasilydrawsoutsidethebounding
rectangle.Enablethatoptimizationifyoudonotpaintanti-aliased
orifyouaresurethatyourpaintingwillstayinsidethebounding
rectangle.Ifyouenablethisflagandspotpaintingartifactsonthe
view,youhaven'trespectedtheitem'sboundingrectangle!
Asafurtheroptimization,youcandefinehowtheviewshould
updateitsviewportwhenthescenechanges.Youcansetthe
differentmodeswithsetViewportUpdateMode().Bydefault
(QGraphicsView::MinimalViewportUpdate),theviewtriestodetermineonly
thoseareasthatneedanupdateandrepaintsonlythese.However,
sometimesitismoretime-consumingtofindalltheareasthatneed
aredrawthantojustpainttheentireviewport.Thisappliesifyou
havemanysmallupdates.Then,QGraphicsView::FullViewportUpdateisthe
betterchoicesinceitsimplyrepaintsthewholeviewport.Akindof
combinationofthelasttwomodesis
QGraphicsView::BoundingRectViewportUpdate.Inthismode,Qtdetectsallareas
thatneedaredraw,andthenitredrawsarectangleoftheviewport
thatcoversallareasaffectedbythechange.Iftheoptimalupdate
modechangesovertime,youcantellQttodeterminethebestmode
usingQGraphicsView::SmartViewportUpdate.Theviewthentriestofindthe
bestupdatemode.
OpenGLintheGraphicsView
Asalastoptimization,youcantakeadvantageofOpenGL.Instead
ofusingthedefaultviewportbasedonQWidget,adviseGraphicsView
touseanOpenGLwidget:
QGraphicsViewview;
view.setViewport(newQOpenGLWidget());
Thisusuallyimprovestherenderingperformance.However,
GraphicsViewwasn'tdesignedforGPUsandcan'tusethem
effectively.Therearewaystoimprovethesituation,butthatgoes
beyondthetopicandscopeofthischapter.Youcanfindmore
informationaboutOpenGLandGraphicsViewintheBoxesQt
exampleaswellasinRødal'sarticle"AccelerateyourWidgetswith
OpenGL",whichcanbefoundonlineat
https://doc.qt.io/archives/qq/qq26-openglcanvas.html.
IfyouwanttouseaframeworkdesignedtobeGPUaccelerated,you
shouldturnyourattentiontoQtQuick(wewillstartworkingwithit
inChapter11,IntroductiontoQtQuick).However,QtQuickhasits
ownlimitationscomparedtoGraphicsView.Thistopicis
elaboratedinNichols'sarticleShouldyoustillbeusing
QGraphicsView?,availableathttps://blog.qt.io/blog/2017/01/19/should-you-
be-using-qgraphicsview/.Alternatively,youcanaccessthefullpowerof
OpenGLdirectlyusingitsAPIandhelpfulQtutilities.Wewill
describethisapproachinChapter9,OpenGLandVulkaninQt
applications.
Unfortunately,wecan'tsaythatyouhavetodothisorthattooptimizeGraphicsViewasit
highlydependsonyoursystemandview/scene.Whatwecantellyou,however,ishowto
proceed.OnceyouhavefinishedyourgamebasedonGraphicsView,measurethe
performanceofyourgameusingaprofiler.Makeanoptimizationyouthinkmaypayor
simplyguess,andthenprofileyourgameagain.Iftheresultsarebetter,keepthechange,
otherwiserejectit.Thissoundssimpleandistheonlywayoptimizationcanbedone.There
arenohiddentricksordeeperknowledge.Withtime,however,yourforecastingwillget
arenohiddentricksordeeperknowledge.Withtime,however,yourforecastingwillget
better.
Popquiz
Q1.Whichofthefollowingclassesisawidgetclass?
1. QGraphicsView
2. QGraphicsScene
3. QGraphicsItem
Q2.Whichofthefollowingactionsdoesnotchangethegraphics
item'spositiononthescreen?
1. Scalingtheview.
2. Shearingthisitem'sparentitem.
3. Translatingthisitem.
4. Rotatingthisitem'schilditem.
Q3.Whichfunctionisnotmandatorytoimplementinanewclass
derivedfromQGraphicsItem?
1. boundingRect()
2. shape()
3. paint()
Q4.Whichitemclassshouldbeusedtodisplayarasterimageinthe
GraphicsView?
1. QGraphicsRectItem
2. QGraphicsWidget
3. QGraphicsPixmapItem
Summary
Inthischapter,youlearnedhowtheGraphicsViewarchitecture
works.Wewentthroughthebuildingblocksoftheframework
(items,scene,andview).Next,youlearnedhowtheircoordinate
systemsarerelatedandhowtousethemtogetthepictureyou
want.Lateron,wedescribedthemostusefulandfrequentlyneeded
featuresofGraphicsView.Next,wecoveredcreatingcustomitems
andhandlinginputevents.Inordertobuildabridgetotheworldof
widgets,youalsolearnedhowtoincorporateitemsbasedon
QWidgetintoGraphicsView.Finally,wediscussedwaystooptimize
thescene.
Now,youreallyknowmostofthefunctionsoftheGraphicsView
framework.Withthisknowledge,youcanalreadydoalotofcool
stuff.However,foragame,itisstilltoostatic.Inthenextchapter,
wewillgothroughtheprocessofcreatingacompletegameand
learntousetheAnimationframework.
AnimationsinGraphicsView
Thepreviouschaptergaveyoualotofinformationaboutpowersof
GraphicsViewframework.Withthatknowledge,wecannow
proceedtoimplementingourfirst2Dgame.Downtheroad,wewill
learnmoreaboutQt'spropertysystem,exploremultiplewaysof
performinganimations,andaddgamepadsupporttoour
application.Bytheendofthechapter,youwillknowallthemost
usefulfeaturesofGraphicsView.
Maintopicscoveredinthischapterareaslisted:
Usingtimers
Cameracontrol
Parallaxscrolling
Qt'spropertysystem
TheAnimationframework
UsingQtGamepadmodule
Thejumpingelephantorhow
toanimatethescene
Bynow,youshouldhaveagoodunderstandingoftheitems,the
scene,andtheview.Withyourknowledgeofhowtocreateitems,
standardandcustomones,ofhowtopositionthemonthescene,
andofhowtosetuptheviewtoshowthescene,youcanmake
prettyawesomethings.Youcanevenzoomandmovethescenewith
themouse.That'ssurelygood,butforagame,onecrucialpointis
stillmissing—youhavetoanimatetheitems.
Insteadofgoingthroughallpossibilitiesofhowtoanimateascene,
let'sdevelopasimplejump-and-rungamewherewerecappartsof
theprevioustopicsandlearnhowtoanimateitemsonascreen.So
let'smeetBenjamin,theelephant:
Thegameplay
ThegoalofthegameisforBenjamintocollectthecoinsthatare
placedalloverthegamefield.Besideswalkingrightandleft,
Benjamincan,ofcourse,alsojump.Inthefollowingscreenshot,
youseewhatthisminimalisticgameshouldlooklikeattheend:
Timeforaction-Creatingan
itemforBenjamin
Let'screateanewQtWidgetsprojectandstartmakingourgame.
Sincetheprojectwillbecomemorecomplexthanourprevious
projects,wewillnotbegivingyoupreciseinstructionsforediting
thecode.Ifatanytimeyouareunsureaboutthechangesyoumake,
youcanlookatthereferenceimplementationprovidedwiththe
book.Italsocontainstheimagefilesyoucanusetoimplementthe
game.
Let'snowlookathowwecanmobilizeBenjamin.First,weneeda
customitemclassforhim.WecallthePlayerclassandchoose
QGraphicsPixmapItemasthebaseclass,becauseBenjaminisaPNGimage.
Intheitem'sPlayerclass,wefurthercreateaprivatefieldofinteger
typeandcallitm_direction.Itsvaluesignifiesinwhichdirection
Benjaminwalks—leftorright—orifhestandsstill.Next,we
implementtheconstructor:
Player::Player(QGraphicsItem*parent)
:QGraphicsPixmapItem(parent)
,m_direction(0)
{
QPixmappixmap(":/elephant");
setPixmap(pixmap);
setOffset(-pixmap.width()/2,-pixmap.height()/2);
}
Intheconstructor,wesetm_directionto0,whichmeansthatBenjamin
isn'tmovingatall.Ifm_directionis1,Benjaminmovesright,andifthe
valueis-1,hemovesleft.Inthebodyoftheconstructor,wesetthe
imagefortheitembycallingsetPixmap().TheimageofBenjaminis
storedintheQtResourcesystem;thus,weaccessitthrough
QPixmap(":/elephant"),withelephantasthegivenaliasfortheactual
imageofBenjamin.Finally,weusethesetOffset()functiontochange
howthepixmapispositionedintheitem'scoordinatesystem.By
default,theoriginpointcorrespondstothetop-leftcornerofthe
pixmap,butweprefertohaveitatthecenterofthepixmapsothat
applyingtransformationsiseasier.
Whenyouareunsureofhowtospecifythepathtoyourresource,youcanaskQtCreator
aboutit.Todothat,expandtheResourcesbranchintheprojecttree,locatetheresource,
andselecttheCopyPath...entryinitscontextmenu.
Next,wecreateagetterandsetterfunctionforthem_directionfield:
intPlayer::direction()const{
returnm_direction;
}
voidPlayer::setDirection(intdirection)
{
m_direction=direction;
if(m_direction!=0){
QTransformtransform;
if(m_direction<0){
transform.scale(-1,1);
}
setTransform(transform);
}
}
Thedirection()functionisastandardgetterfunctionform_direction
returningitsvalue.ThesetDirection()setterfunctionadditionally
checksinwhichdirectionBenjaminismoving.Ifheismovingleft,
weneedtofliphisimagesothatBenjaminlookstotheleft,the
directioninwhichheismoving.Ifheismovingtowardtheright,we
restorethenormalstatebyassigninganemptyQTransformobject,
whichisanidentitymatrix.
WecannotuseQGraphicsItem::setScalehere,becauseitonlysupportsthesamescale
factorsforxandyaxes.Fortunately,setTransform()enablesustosetanyaffineor
perspectivetransformation.
So,wenowhaveouritemofthePlayerclassforthegame'scharacter,
whichshowstheimageofBenjamin.Theitemalsostoresthe
currentmovingdirection,andbasedonthatinformation,theimage
isflippedverticallyifneeded.
Theplayingfield
Sincewewillhavetodosomeworkonthescene,we
subclassQGraphicsSceneandnamethenewclassMyScene.There,we
implementonepartofthegamelogic.Thisisconvenient
sinceQGraphicsSceneinheritsQObjectandthuswecanuseQt'ssignaland
slotmechanism.
Thescenecreatestheenvironmentinwhichourelephantwillbe
walkingandjumping.Overall,wehaveaviewfixedinsizeholdinga
scene,whichisexactlyasbigastheview.Wedonottakesize
changesoftheviewintoaccount,sincetheywillcomplicatethe
exampletoomuch.
Allanimationsinsidetheplayingfieldaredonebymovingthe
items,notthescene.Sowehavetodistinguishbetweentheview's,
orratherthescene's,widthandthewidthoftheelephant'svirtual
"world"inwhichhecanmove.Inordertohandlethemovement
properly,weneedtocreateafewprivatefieldsintheMySceneclass.
Thewidthofthisvirtualworldisdefinedbytheintm_fieldWidthfield
andhasno(direct)correlationwiththescene.Withintherangeof
m_fieldWidth,whichis500pixelsintheexample,Benjaminorthe
graphicsitemcanbemovedfromtheminimumxcoordinate,
definedbyqrealm_minX,tothemaximumxcoordinate,definedbyqreal
m_maxX.Wekeeptrackofhisactualxpositionwiththeqreal
m_currentXvariable.Next,theminimumycoordinatetheitemis
allowedtohaveisdefinedbyqrealm_groundLevel.Wehavetoalsotake
intoaccounttheitem'ssize.
Lastly,whatisleftistheview,whichhasafixedsizedefinedbythe
scene'sboundingrectanglesize,whichisnotaswideasm_fieldWidth.
Sothescene(andtheview)followstheelephantwhilehewalks
throughhisvirtualworldofthem_fieldWidthlength.Takealookatthe
followingdiagramtoseethevariablesintheirgraphical
representation:
Timeforaction-Making
Benjaminmove
Thenextthingwewanttodoismakeourelephantmovable.In
ordertoachievethat,weaddaQTimerm_timerprivatemember
toMyScene.QTimerisaclassthatcanemitthetimeout()signalperiodically
withthegiveninterval.IntheMySceneconstructor,wesetupthe
timerwiththefollowingcode:
m_timer.setInterval(30);
connect(&m_timer,&QTimer::timeout,
this,&MyScene::movePlayer);
First,wedefinethatthetimeremitsthetimeoutsignalevery30
milliseconds.Then,weconnectthatsignaltothescene'sslotcalled
movePlayer(),butwedonotstartthetimeryet.Thetimerwillbe
startedwhentheplayerpressesakeytomove.
Next,weneedtohandletheinputeventsproperlyandupdatethe
player'sdirection.WeintroducethePlayer*m_playerfieldthatwill
containapointertotheplayerobjectandtheintm_horizontalInputfield
thatwillaccumulatethemovementcommands,aswe'llseeinthe
nextpieceofcode.Finally,wereimplementthekeyPressEventvirtual
function:
voidMyScene::keyPressEvent(QKeyEvent*event)
{
if(event->isAutoRepeat()){
return;
}
switch(event->key()){
caseQt::Key_Right:
addHorizontalInput(1);
break;
break;
caseQt::Key_Left:
addHorizontalInput(-1);
break;
//...
}
}
voidMyScene::addHorizontalInput(intinput)
{
m_horizontalInput+=input;
m_player->setDirection(qBound(-1,m_horizontalInput,1));
checkTimer();
}
Asasmallsidenote,whenevercodesnippetsinthefollowingcodepassagesareirrelevant
fortheactualdetail,wewillskipthecodebutwillindicatemissingcodewith//...sothat
youknowthatitisnottheentirecode.Wewillcovertheskippedpartslaterwhenitismore
appropriate.
Whatjusthappened?
Inthekeypresseventhandler,wefirstcheckwhetherthekeyevent
wastriggeredbecauseofanauto-repeat.Ifthisisthecase,weexit
thefunction,becauseweonlywanttoreactonthefirstrealkey
pressevent.Also,wedonotcallthebaseclassimplementationof
thateventhandlersincenoitemonthesceneneedstogetakey
pressevent.Ifyoudohaveitemsthatcouldandshouldreceive
events,donotforgettoforwardthemwhilereimplementingevent
handlersatthescene.
Ifyoupressandholdakeydown,Qtwillcontinuouslydeliverthekeypressevent.To
determinewhetheritwasthefirstrealkeypressoranautogeneratedevent,use
QKeyEvent::isAutoRepeat().Itreturnstrueiftheeventwasautomaticallygenerated.
Assoonasweknowthattheeventwasnotdeliveredbyanauto
repeat,wereacttothedifferentkeypresses.Insteadofcalling
thesetDirection()methodofthePlayer*m_playerfielddirectly,weuse
them_horizontalInputclassfieldtoaccumulatetheinputvalue.
Wheneverit'schanged,weensurethecorrectnessofthevalue
beforepassingittosetDirection().Forthat,weuseqBound(),which
returnsavaluethatisboundbythefirstandthelastarguments.
Theargumentinthemiddleistheactualvaluethatwewanttoget
bound,sothepossiblevaluesinourcasearerestrictedto-1,0,and
1.
Youmightwonder,whynotsimplycallm_player->setDirection(1)when
therightkeyispressed?Whyaccumulatetheinputsin
them_horizontalInputvariable?Well,Benjaminismovedbytheleft
andrightarrowkeys.Iftherightkeyispressed,1isadded;ifitgets
released,-1isadded.Thesameappliesfortheleftkey,butonlythe
otherwayaround.Theadditionofthevalueratherthansettingitis
nownecessarybecauseofasituationwhereauserpressesandholds
therightkey,andthevalueofm_directionistherefore1.Now,without
releasingtherightkey,theyalsopressandholdtheleftkey.
Therefore,thevalueofm_directionisgettingdecreasedbyone;the
valueisnow0andBenjaminstops.However,rememberthatboth
keysarestillbeingpressed.Whathappenswhentheleftkeyis
released?Howwouldyouknowinthissituationinwhichdirection
Benjaminshouldmove?Toachievethat,youwouldhavetofindout
anadditionalbitofinformation—whethertherightkeyisstill
presseddownornot,whichseemstoomuchtroubleandoverhead.
Inourimplementation,whentheleftkeyisreleased,1isaddedand
thevalueofm_directionbecomes1,makingBenjaminmoveright.
Voilà!Allwithoutanyconcernaboutwhatthestateoftheother
buttonmightbe.
AftercallingsetDirection(),wecallthecheckTimer()function:
voidMyScene::checkTimer()
{
if(m_player->direction()==0){
m_timer.stop();
}elseif(!m_timer.isActive()){
m_timer.start();
}
}
Thisfunctionfirstcheckswhethertheplayermoves.Ifnot,the
timerisstopped,becausenothinghastobeupdatedwhenour
elephantstandsstill.Otherwise,thetimergetsstarted,butonlyifit
isn'talreadyrunning.WecheckthisbycallingisActive()onthe
timer.
Whentheuserpressestherightkey,forexample,atthebeginning
ofthegame,checkTimer()willstartm_timer.Sinceitstimeoutsignalwas
connectedtomovePlayer(),theslotwillbecalledevery30milliseconds
tillthekeyisreleased.
SincethemovePlayer()functionisabitlonger,let'sgothroughitstep
bystep:
constintdirection=m_player->direction();
if(0==direction){
return;
}
First,wecachetheplayer'scurrentdirectioninalocalvariableto
avoidmultiplecallsofdirection().Then,wecheckwhethertheplayer
ismovingatall.Iftheyaren't,weexitthefunctionbecausethereis
nothingtoanimate:
constintdx=direction*m_velocity;
qrealnewX=qBound(m_minX,m_currentX+dx,m_maxX);
if(newX==m_currentX){
return;
}
m_currentX=newX;
Next,wecalculatetheshifttheplayeritemshouldgetandstoreitin
dx.Thedistancetheplayershouldmoveevery30millisecondsis
definedbytheintm_velocitymembervariable,expressedinpixels.
Youcancreatesetterandgetterfunctionsforthatvariableifyou
like.Forus,thedefaultvalueof4pixelswilldothejob.Multiplied
bythedirection(whichcouldonlybe1or-1atthispoint),wegeta
shiftoftheplayerby4pixelstotherightortotheleft.Basedonthis
shift,wecalculatethenewxpositionoftheplayer.Next,wecheck
whetherthatnewpositionisinsidetherangeofm_minXandm_maxX,two
membervariablesthatarealreadycalculatedandsetupproperlyat
thispoint.Then,ifthenewpositionisnotequaltotheactual
position,whichisstoredinm_currentX,weproceedbyassigningthe
newpositionasthecurrentone.Otherwise,weexitthefunction
sincethereisnothingtomove.
Thenextquestiontotackleiswhethertheviewshouldalwaysmove
whentheelephantismoving,whichmeansthattheelephantwould
alwaysstay,say,inthemiddleoftheview.No,heshouldn'tstayata
specificpointinsidetheview.Rather,theviewshouldbefixedwhen
theelephantismoving.Onlyifhereachesthebordersshouldthe
viewfollow.Let'ssaythatwhenthedistancebetweentheelephant's
centerandthewindow'sborderislessthan150pixels,wewilltryto
shifttheview:
constintshiftBorder=150;
constintrightShiftBorder=width()-shiftBorder;
constintvisiblePlayerPos=m_currentX-m_worldShift;
constintnewWorldShiftRight=visiblePlayerPos-rightShiftBorder;
if(newWorldShiftRight>0){
m_worldShift+=newWorldShiftRight;
}
constintnewWorldShiftLeft=shiftBorder-visiblePlayerPos;
if(newWorldShiftLeft>0){
m_worldShift-=newWorldShiftLeft;
}
constintmaxWorldShift=m_fieldWidth-qRound(width());
m_worldShift=qBound(0,m_worldShift,maxWorldShift);
m_player->setX(m_currentX-m_worldShift);
Theintm_worldShiftclassfieldshowshowmuchwehavealready
shiftedourworldtotheright.First,wecalculatetheactual
coordinateofourelephantintheviewandsaveitto
thevisiblePlayerPosvariable.Then,wecalculateitspositionrelativeto
theallowedareadefinedbytheshiftBorderandrightShiftBorder
variables.IfvisiblePlayerPosisbeyondtherightborderoftheallowed
area,newWorldShiftRightwillbepositive,weneedtoshifttheworldby
newWorldShiftRighttotheright.Similarly,whenweneedtoshiftittothe
left,newWorldShiftLeftwillbepositive,anditwillcontaintheneeded
amountofshift.Finally,weupdatethepositionofm_playerusinga
setX()helpermethodthatissimilartosetPos()butleavesthey
coordinateunchanged.
NotethatthevalueforshiftBorderisrandomlychosen.Youcanalteritasyoulike.Of
course,youcancreateasetterandgetterforthisparametertoo.
Thelastimportantparttodohereistoapplythenewvalueof
m_worldShiftbysettingpositionsoftheotherworlditems.Whilewe're
atit,wewillimplementparallaxscrolling.
Parallaxscrolling
Parallaxscrollingisatricktoaddanillusionofdepthtothe
backgroundofthegame.Thisillusionoccurswhenthebackground
hasdifferentlayersthatmoveatdifferentspeeds.Thenearest
backgroundmustmovefasterthantheonesfartheraway.Inour
case,wehavethesefourbackgroundsorderedfromthemost
distanttothenearest:
Thesky:
Thetrees:
Thegrass:
Theground:
Timeforaction-Movingthe
background
Thescenewillcreateagraphicsitemforeachpartofthe
backgroundandstorepointerstotheminthem_sky,m_grass,andm_trees
privatefields.Nowthequestionishowtomovethematdifferent
speeds.Thesolutionisquitesimple—theslowestone,thesky,isthe
smallestimage.Thefastestbackground,thegroundandthegrass,
arethelargestimages.Nowwhenwetakealookattheendofthe
movePlayer()function'sslot,weseethis:
qrealratio=qreal(m_worldShift)/maxWorldShift;
applyParallax(ratio,m_sky);
applyParallax(ratio,m_grass);
applyParallax(ratio,m_trees);
TheapplyParallax()helpermethodcontainsthefollowingcode:
voidMyScene::applyParallax(qrealratio,QGraphicsItem*item){
item->setX(-ratio*(item->boundingRect().width()-width()));
}
Whatjusthappened?
Whatarewedoinghere?Atthebeginning,thesky'sleftborderis
thesameastheview'sleftborder,bothatpoint(0,0).Attheend,
whenBenjaminhaswalkedtothemaximumright,thesky'sright
bordershouldbethesameastheview'srightborder.So,atthis
position,theshiftoftheskywillbeequaltothesky'swidth(m_sky-
>boundingRect().width())minusthewidthoftheview(width()).Theshift
oftheskydependsonthepositionofthecameraand,consequently,
thevalueofthem_worldShiftvariable;ifitisfartotheleft,theskyisn't
shifted,andifthecameraisfartotheright,theskyismaximally
shifted.Thus,wehavetomultiplythesky'smaximumshiftvalue
withafactorbasedonthecurrentpositionofthecamera.The
relationtothecamera'spositionisthereasonthisishandledinthe
movePlayer()function.Thefactorwehavetocalculatehastobe
between0and1.Sowegettheminimumshift(0*shift,which
equals0)andthemaximumshift(1*shift,whichequalsshift).We
namethisfactorasratio.
Howfartheworldwasshiftedissavedinm_worldShift,sobydividing
m_worldShiftbymaxWorldShift,wegettheneededfactor.Itis0whenthe
playeristothefarleftand1iftheyaretothefarright.Then,we
havetosimplymultiplyratiowiththemaximumshiftofthesky.
Thesamecalculationisusedfortheotherbackgrounditems,soitis
movedtoaseparatefunction.Thecalculationalsoexplainswhya
smallerimageismovingslower.It'sbecausetheoverlapofthe
smallerimageislessthanthatofthelargerone,andsincethe
backgroundsaremovedinthesametimeperiod,thelargeronehas
tomovefaster.
Haveagohero-Addingnew
backgroundlayers
Trytoaddadditionalbackgroundlayerstothegame,followingthe
precedingexample.Asanidea,youcanaddabarnbehindthetrees
orletanairplaneflythroughthesky.
TheAnimationframework
Fornow,wehavecalculatedandappliednewpositionsforour
graphicsitemsmanually.However,Qtprovidesawaytodoit
automatically,calledtheAnimationframework.
Theframeworkisanabstractimplementationofanimations,soit
canbeappliedtoanyQObject,suchaswidgets,orevenplain
variables.Graphics,itemscanbeanimatedtoo,andwewillgetto
thistopicsoon.Animationsarenotrestrictedtotheobject's
coordinates.Youcananimatecolor,opacity,oracompletely
invisibleproperty.
Tocreateananimation,youtypicallyneedtoperformthefollowing
steps:
1. Createananimationobject(suchasQPropertyAnimation)
2. Settheobjectthatshouldbeanimated
3. Setthenameofthepropertytobeanimated
4. Definehowexactlythevalueshouldchange(forexample,
setstartingandendingvalues)
5. Starttheanimation
Asyouprobablyknow,callinganarbitrarymethodbynameisnot
possibleinC++,andyet,theanimationobjectsareabletochange
arbitrarypropertiesatwill.Thisispossiblebecause"property"is
notonlyafancyname,butalsoanotherpowerfulfeatureof
theQObjectclassandQtmeta-objectcompiler.
Properties
InChapter3,QtGUIProgramming,weeditedpredefinedproperties
ofwidgetsintheformeditorandusedtheirgetterandsetter
methodsinthecode.However,untilnow,therewasn'tarealreason
forustodeclareanewproperty.It'llbeusefulwiththeAnimation
framework,solet'spaymoreattentiontoproperties.
OnlyclassesthatinheritQObjectcandeclareproperties.Tocreatea
property,wefirstneedtodeclareitinaprivatesectionoftheclass
(usuallyrightaftertheQ_OBJECTmandatorymacro)usingaspecial
Q_PROPERTYmacro.Thatmacroallowsyoutospecifythefollowing
informationaboutthenewproperty:
Thepropertyname—astringthatidentifiesthepropertyin
theQtmetasystem.
Thepropertytype—anyvalidC++typecanbeusedfora
property,butanimationswillonlyworkwithalimitedsetof
types.
Namesofthegetterandsettermethodforthisproperty.If
declaredinQ_PROPERTY,youmustaddthemtoyourclassand
implementthemproperly.
Nameofthesignalthatisemittedwhentheproperty
changes.IfdeclaredinQ_PROPERTY,youmustaddthesignaland
ensurethatit'sproperlyemitted.
Therearemoreconfigurationoptions,buttheyarelessfrequently
needed.YoucanlearnmoreaboutthemfromtheTheProperty
Systemdocumentationpage.
TheAnimationframeworksupportsthefollowingpropertytypes:int,unsignedint,double,
float,QLine,QLineF,QPoint,QPointF,QSize,QSizeF,QRect,QRectF,andQColor.Othertypesare
notsupported,becauseQtdoesn'tknowhowtointerpolatethem,thatis,howtocalculate
intermediatevaluesbasedonthestartandendvalues.However,it'spossibletoadd
supportforcustomtypesifyoureallyneedtoanimatethem.
Similartosignalsandslots,propertiesarepoweredbymoc,which
readstheheaderfileofyourclassandgeneratesextracodethat
enablesQt(andyou)toaccessthepropertyatruntime.For
example,youcanusetheQObject::property()andQObject::setProperty()
methodstogetandsetpropertiesbyname.
Timeforaction-Addingajump
animation
Gotothemyscene.hfileandaddaprivateqrealm_jumpFactorfield.Next,
declareagetter,asetter,andachangesignalforthisfield:
public:
//...
qrealjumpFactor()const;
voidsetJumpFactor(constqreal&jumpFactor);
signals:
voidjumpFactorChanged(qreal);
Intheheaderfile,wedeclarethejumpFactorpropertybyaddingthe
followingcodejustaftertheQ_OBJECTmacro:
Q_PROPERTY(qrealjumpFactor
READjumpFactor
WRITEsetjumpFactor
NOTIFYjumpFactorChanged)
Here,qrealisthetypeoftheproperty,jumpFactoristheregistered
name,andthefollowingthreelinesregisterthecorresponding
memberfunctionsoftheMySceneclassinthepropertysystem.We'll
needthispropertytomakeBenjaminjump,aswewillseelateron.
ThejumpFactor()getterfunctionsimplyreturnsthem_jumpFactorprivate
member,whichisusedtostoretheactualposition.The
implementationofthesetterlookslikethis:
voidMyScene::setjumpFactor(constqreal&pos){
if(pos==m_jumpFactor){
return;
}
m_jumpFactor=pos;
emitjumpFactorChanged(m_jumpFactor);
}
Itisimportanttocheckwhetherposwillchangethecurrentvalueof
m_jumpFactor.Ifthisisnotthecase,exitthefunction,becausewedon't
wantthechangesignaltobeemittedevenifnothinghaschanged.
Otherwise,wesetm_jumpFactortoposandemitthesignalthatinforms
aboutthechange.
Propertyanimations
WeimplementedthehorizontalmovementusingaQTimer.Now,let's
tryasecondwaytoanimateitems—theAnimationframework.
Timeforaction-Using
animationstomoveitems
smoothly
Let'saddanewprivatemembercalledm_jumpAnimationof
theQPropertyAnimation*type,andinitializeitintheconstructorof
MyScene:
m_jumpAnimation=newQPropertyAnimation(this);
m_jumpAnimation->setTargetObject(this);
m_jumpAnimation->setPropertyName("jumpFactor");
m_jumpAnimation->setStartValue(0);
m_jumpAnimation->setKeyValueAt(0.5,1);
m_jumpAnimation->setEndValue(0);
m_jumpAnimation->setDuration(800);
m_jumpAnimation->setEasingCurve(QEasingCurve::OutInQuad);
Whatjusthappened?
FortheinstanceofQPropertyAnimationcreatedhere,wedefinetheitem
asaparent;thus,theanimationwillgetdeletedwhenthescene
deletestheitem,andwedon'thavetoworryaboutfreeingtheused
memory.Then,wedefinethetargetoftheanimation—ourMyScene
class—andthepropertythatshouldbeanimated,jumpFactor,inthis
case.Then,wedefinethestartandtheendvalueofthatproperty;in
additiontothat,wealsodefineavalueinbetween,bysetting
setKeyValueAt().Thefirstargumentoftheqrealtypedefinestimeinside
theanimation,where0isthebeginningand1theend,andthe
secondargumentdefinesthevaluethattheanimationshouldhave
atthattime.SoyourjumpFactorelementwillgetanimatedfrom0to1
andbackto0in800milliseconds.ThiswasdefinedbysetDuration().
Finally,wedefinehowtheinterpolationbetweenthestartandend
valueshouldbedoneandcallsetEasingCurve(),withQEasingCurve::OutInQuad
asanargument.
Qtdefinesupto41differenteasingcurvesforlinear,quadratic,cubic,quartic,quintic,
sinusoidal,exponential,circular,elastic,backeasing,andbouncefunctions.Thesearetoo
manytodescribehere.Instead,takealookatthedocumentation;simplysearchfor
QEasingCurve::Type.
Inourcase,QEasingCurve::OutInQuadensuresthatthejumpspeedof
Benjaminlookslikeanactualjump:fastinthebeginning,slowat
thetop,andfastattheendagain.Westartthisanimationwiththe
jumpfunction:
voidMyScene::jump()
{
if(QAbstractAnimation::Stopped==m_jumpAnimation->state()){
m_jumpAnimation->start();
}
}
Weonlystarttheanimationbycallingstart()whentheanimation
isn'trunning.Therefore,wechecktheanimation'sstatetosee
whetherithasbeenstopped.OtherstatescouldbePausedorRunning.
Wewantthisjumpactiontobeactivatedwhenevertheplayer
pressestheSpacekeyontheirkeyboard.Therefore,weexpandthe
switchstatementinsidethekeypresseventhandlerusingthiscode:
caseQt::Key_Space:
jump();
break;
Nowthepropertygetsanimated,butBenjaminwillstillnotjump.
Therefore,wehandlethechangesofthejumpFactorvalueattheendof
thesetJumpFactorfunction:
voidMyScene::setJumpFactor(constqreal&jumpFactor)
{
//...
qrealgroundY=(m_groundLevel-m_player->boundingRect().height()
/2);
qrealy=groundY-m_jumpAnimation->currentValue().toReal()*
m_jumpHeight;
m_player->setY(y);
//...
}
WhenourQPropertyAnimationisrunning,itwillcalloursetJumpFactor()
functiontoupdatetheproperty'svalue.Insidethatfunction,we
calculatetheycoordinateoftheplayeritemtorespecttheground
leveldefinedbym_groundLevel.Thisisdonebysubtractinghalfofthe
item'sheightfromthegroundlevel'svaluesincetheitem'sorigin
pointisinitscenter.Then,wesubtractthemaximumjumpheight,
definedbym_jumpHeight,whichismultipliedbytheactualjumpfactor.
Sincethefactorisintherangeof0and1,thenewycoordinate
staysinsidetheallowedjumpheight.Then,wealtertheplayer
item'sypositionbycallingsetY(),leavingthexcoordinateasthe
same.Etvoilà,Benjaminisjumping!
Haveagohero-Lettingthe
itemhandleBenjamin'sjump
SincethesceneisalreadyaQObject,addingapropertytoitwaseasy.
However,imaginethatyouwanttocreateagamefortwoplayers,
eachcontrollingaseparatePlayeritem.Inthiscase,thejumpfactors
oftwoelephantsneedtobeanimatedindependently,soyouwantto
makeananimatedpropertyinthePlayerclass,insteadofputtingit
tothescene.
TheQGraphicsItemitemandallstandarditemsintroducedsofardon't
inheritQObjectandthuscan'thaveslotsoremitsignals;theydon't
benefitfromtheQObjectpropertysystemeither.However,wecan
makethemuseQObject!AllyouhavetodoisaddQObjectasabaseclass
andaddtheQ_OBJECTmacro:
classPlayer:publicQObject,publicQGraphicsPixmapItem{
Q_OBJECT
//...
};
Nowyoucanuseproperties,signals,andslotswithitemstoo.Be
awarethatQObjectmustbethefirstbaseclassofanitem.
Awordofwarning
OnlyuseQObjectwithitemsifyoureallyneeditscapabilities.QObject
addsalotofoverheadtotheitem,whichwillhaveanoticeableimpactonperformance
whenyouhavemanyitems,souseitwiselyandnotonlybecauseyoucan.
Ifyoumakethischange,youcanmovethejumpFactorpropertyfrom
MyScenetoPlayer,alongwithalotofrelatedcode.Youcanmakethe
codeevenmoreconsistentbyhandlingthehorizontalmovement
inPlayeraswell.LetMyScenehandletheinputeventsandforwardthe
movementcommandstoPlayer.
Timeforaction-Keeping
multipleanimationsinsync
Nowwe'llstartimplementingthecoinclass.Wecanusea
simpleQGraphicsEllipseItemobject,butwe'llneedtoanimateits
properties,solet'screateanewCoinclassandderiveitfromQObject
andQGraphicsEllipseItem.Definetwoproperties:opacityoftheqreal
typeandrectoftheQRecttype.Thisisdoneonlybythefollowing
code:
classCoin:publicQObject,publicQGraphicsEllipseItem
{
Q_OBJECT
Q_PROPERTY(qrealopacityREADopacityWRITEsetOpacity)
Q_PROPERTY(QRectFrectREADrectWRITEsetRect)
//...
};
Nofunctionorslotwasadded,becausewesimplyusedbuilt-in
functionsofQGraphicsItemandassociatedthemwiththeproperties.
IfyouwantanitemthatinheritsfromQObjectandQGraphicsItem,youcandirectlyinherit
QGraphicsObject.Moreover,italreadyregistersallgeneralQGraphicsItempropertiesinthe
metasystem,includingpos,scale,rotation,andopacity.Allpropertiescomewith
correspondingnotificationsignals,suchasopacityChanged().However,whenyouinherit
QGraphicsObject,youcannot,atthesametime,inheritQGraphicsEllipseItemoranyother
itemclass.Sointhiscase,wewillneedtoeitherimplementpaintingoftheellipsemanually
oraddachildQGraphicsEllipseItemthatcanperformthepaintingforus.
Next,we'llcreatetheexplode()functionthatwillstartsome
animationswhentheplayercollectsthecoin.CreateaBoolean
privatefieldintheclassanduseittoensurethateachcoincanonly
explodeonce:
voidCoin::explode()
{
if(m_explosion){
return;
}
m_explosion=true;
//...
}
WewanttoanimateourtwopropertiesbytwoQPropertyAnimation
objects.Onefadesthecoinout,whiletheotherscalesthecoinin.To
ensurethatbothanimationsgetstartedatthesametime,weuse
QParallelAnimationGroup,asfollows:
QPropertyAnimation*fadeAnimation=
newQPropertyAnimation(this,"opacity");
//...
QPropertyAnimation*scaleAnimation=newQPropertyAnimation(this,
"rect");
//...
QParallelAnimationGroup*group=newQParallelAnimationGroup(this);
group->addAnimation(scaleAnimation);
group->addAnimation(fadeAnimation);
connect(group,&QParallelAnimationGroup::finished,
this,&Coin::deleteLater);
group->start();
Whatjusthappened?
Youalreadyknowhowtosetupasinglepropertyanimation,sowe
omittedthecodeforit.Aftersettingupbothanimations,weadd
themtothegroupanimationbycallingaddAnimation()onthegroup,
whilepassingapointertotheanimationwewouldliketoadd.
Then,whenwestartthegroup;QParallelAnimationGroupensuresthatall
assignedanimationsstartatthesametime.
Whenbothanimationshavefinished,groupwillemitthefinished()
signal.WeconnectedthatsignaltothedeleteLater()slotofourclass
sothatthecoinobjectgetsdeletedwhenit'snolongervisible.This
handyslotisdeclaredintheQObjectclassandisusefulinmanycases.
Insomecases,youmaywanttostopananimation.Youcandothatbycallingthestop()
method.It'salsopossibletopauseandresumeananimationusingpause()andresume().
UsingthesemethodsonaQParallelAnimationGroupwillaffectalltransformationsaddedto
thatgroup.
Chainingmultipleanimations
Whatifwewantedtoperformananimationattheendofanother
animation?Wecouldconnectthefinished()signalofthefirst
animationtothestart()slotofthesecondone.However,amuch
moreconvenientsolutionistouseQSequentialAnimationGroup.For
example,ifwewantcoinstoscaleandthentofade,thefollowing
codewilldothetrick:
QSequentialAnimationGroup*group=new
QSequentialAnimationGroup(this);
group->addAnimation(scaleAnimation);
group->addAnimation(fadeAnimation);
group->start();
Addinggamepadsupport
Theplayercanusethekeyboardtoplayourgame,butitwouldbe
nicetoalsoallowplayingitusingagamepad.Fortunately,Qt
providestheQtGamepadadd-onthatallowsustodothiseasily.As
opposedtoQtEssentials(forexample,QtWidgets),add-onsmay
besupportedonalimitednumberofplatforms.AsofQt5.9,Qt
GamepadsupportsWindows,Linux,Android,macOS,iOS,and
tvOS(includingthetvOSremote).
WorkingwithgamepadsinQt
ThestartingpointofthegamepadAPIistheQGamepadManagerclass.The
singletonobjectofthisclasscanbeobtainedusing
theQGamepadManager::instance()function.Itallowsyoutorequestthelist
ofidentifiersoftheavailablegamepadsusingtheconnectedGamepads()
function.ThegamepadConnected()signalcanbeusedtodetectnew
gamepadsonthefly.QGamepadManageralsoprovidesAPIforconfiguring
buttonsandaxesonthegamepadandisabletosavethe
configurationtothespecifiedsettingsfile.
Afteryoudetectedthatoneormultiplegamepadsareavailablein
thesystem,youshouldcreateanewQGamepadobjectandpassthe
obtaineddeviceidentifierasaconstructor'sargument.Youcanuse
thefirstavailablegamepadorallowtheusertoselectwhich
gamepadtouse.Inthiscase,youcanutilizethegamepad'sname
propertythatreturnsareadablenameofthedevice.
TheGamepadobjectcontainsadedicatedpropertyforeachaxisand
button.Thisgivesyoutwowaystoreceivetheinformationabout
thestateofthecontrols.First,youcanusethegetteroftheproperty
tocheckthecurrentstateofabuttonoranaxis.Forexample,the
buttonL1()functionwillreturntrueiftheL1buttoniscurrently
pressed,andtheaxisLeftX()willreturnthecurrenthorizontal
positionoftheleftstickasadoublevaluethatisintherangeof-1to1.
Fortriggerbuttons(forexample,buttonL2()),thepropertycontainsa
doublevaluethatrangesfrom0(notpressed)to1(fullypressed).
Thesecondwayistousethesignalscorrespondingtoeach
property.Forexample,youcanconnectto
thegamepad'sbuttonL1Changed(boolvalue)andaxisLeftXChanged(doublevalue)
signalstomonitorthechangesofthecorrespondingproperties.
Finally,theQGamepadKeyNavigationclasscanbeusedtoquicklyadd
gamepadsupporttoakeyboard-orientedapplication.Whenyou
createanobjectofthisclass,yourapplicationwillbeginreceiving
keyeventscausedbygamepads.Bydefault,GamepadKeyNavigationwill
emulateup,down,left,right,back,forward,andreturnkeyswhen
thecorrespondinggamepadbuttonsarepressed.However,youcan
overridethedefaultmappingoraddyourownmappingforother
gamepadbuttons.
Timeforaction-Handling
gamepadevents
Let'sstartwithaddingtheQtGamepadadd-ontoourprojectby
editingthejrgame.profile:
QT+=coreguiwidgetsgamepad
Thiswillmaketheheadersofthelibraryavailabletoourproject
andtellqmaketolinktheprojectagainstthislibrary.Nowaddthe
followingcodetotheconstructoroftheMySceneclass:
QList<int>gamepadIds=QGamepadManager::instance()-
>connectedGamepads();
if(!gamepadIds.isEmpty()){
QGamepad*gamepad=newQGamepad(gamepadIds[0],this);
connect(gamepad,&QGamepad::axisLeftXChanged,
this,&MyScene::axisLeftXChanged);
connect(gamepad,&QGamepad::axisLeftYChanged,
this,&MyScene::axisLeftYChanged);
}
Thecodeisprettystraightforward.First,weuse
QGamepadManager::connectedGamepadstogetthelistofIDsoftheavailable
gamepads.Ifsomegamepadswerefound,wecreateaQGamepadobject
forthefirstfoundgamepad.Wepassthistoitsconstructor,soit
becomesachildofourMySceneobject,andwedon'tneedtoworry
aboutdeletingit.Finally,weconnectthe
gamepad'saxisLeftXChanged()andaxisLeftYChanged()signalstonewslots
intheMySceneclass.Now,let'simplementtheseslots:
voidMyScene::axisLeftXChanged(doublevalue)
{
{
intdirection;
if(value>0){
direction=1;
}elseif(value<0){
direction=-1;
}else{
direction=0;
}
m_player->setDirection(direction);
checkTimer();
}
voidMyScene::axisLeftYChanged(doublevalue)
{
if(value<-0.25){
jump();
}
}
Thevalueargumentofthesignalscontainsanumberfrom-1to1.It
allowsusnot
onlytodetectwhetherathumbstickwaspressed,butalsotoget
morepreciseinformation
aboutitsposition.However,inoursimplegame,wedon'tneedthis
precision.IntheaxisLeftXChanged()slot,wecalculateandsetthe
elephant'sdirectionbasedonthesignofthereceivedvalue.
IntheaxisLeftYChanged()slot,ifwereceivealargeenoughnegative
value,weinterpretitasajumpcommand.Thiswillhelpusavoid
accidentaljumps.That'sall!Ourgamenowsupportsboth
keyboardsandgamepads.
Ifyouneedtoreacttootherbuttonsandthumbsticksofthegamepad,usetheothersignals
oftheQGamepadclass.It'salsopossibletoreadmultiplegamepadsatthesametimeby
creatingmultipleQGamepadobjectswithdifferentIDs.
Itemcollisiondetection
Whethertheplayeritemcollideswithacoinischeckedbythe
scene'scheckColliding()function,whichiscalledaftertheplayeritem
hasmovedhorizontallyorvertically.
Timeforaction-Makingthe
coinsexplode
TheimplementationofcheckColliding()lookslikethis:
voidMyScene::checkColliding()
{
for(QGraphicsItem*item:collidingItems(m_player)){
if(Coin*c=qgraphicsitem_cast<Coin*>(item)){
c->explode();
}
}
}
Whatjusthappened?
First,wecallthescene'sQGraphicsScene::collidingItems()function,which
takestheitemforwhichcollidingitemsshouldbedetectedasafirst
argument.Withthesecond,optionalargument,youcandefinehow
thecollisionshouldbedetected.Thetypeofthatargumentis
Qt::ItemSelectionMode,whichwasexplainedearlier.Bydefault,anitem
willbeconsideredcollidingwithm_playeriftheshapesofthetwo
itemsintersect.
Next,weloopthroughthelistoffounditemsandcheckwhetherthe
currentitemisaCoinobject.Thisisdonebytryingtocastthe
pointertoCoin.Ifitissuccessful,weexplodethecoinbycalling
explode().Callingtheexplode()functionmultipletimesisnoproblem,
sinceitwillnotallowmorethanoneexplosion.Thisisimportant
sincecheckColliding()willbecalledaftereachmovementoftheplayer.
Sothefirsttimetheplayerhitsacoin,thecoinwillexplode,butthis
takestime.Duringthisexplosion,theplayerwillmostlikelybe
movedagainandthuscollideswiththecoinoncemore.Insucha
case,explode()maybecalledmultipletimes.
Theqgraphicsitem_cast<>()isafasteralternativetodynamic_cast<>().However,itwill
properlyworkforcustomtypesonlyiftheyimplementtype()properly.Thisvirtual
functionmustreturnadifferentvalueforeachcustomitemclassintheapplication.
ThecollidingItems()functionwillalwaysreturnthebackgrounditems
aswell,sincetheplayeritemisaboveallofthemmostofthetime.
Toavoidthecontinuouscheckiftheyactuallyarecoins,weusea
trick.InsteadofusingQGraphicsPixmapItemdirectly,wesubclassitand
reimplementitsvirtualshape()function,asfollows:
QPainterPathBackgroundItem::shape()const{
returnQPainterPath();
}
WealreadyusedtheQPainterPathclassinthepreviouschapter.This
functionjustreturnsanemptyQPainterPath.Sincethecollision
detectionisdonewiththeitem'sshape,thebackgrounditemscan't
collidewithanyotheritemsincetheirshapeispermanentlyempty.
Don'ttrythistrickwithboundingRect()though,becauseitmustalways
bevalid.
HadwedonethejumpinglogicinsidePlayer,wecouldhave
implementedtheitemcollisiondetectionfromwithintheitem
itself.QGraphicsItemalsooffersacollidingItems()functionthatchecks
againstcollidingitemswithitself.Soscene->collidingItems(item)is
equivalenttoitem->collidingItems().
Ifyouareonlyinterestedinwhetheranitemcollideswithanotheritem,youcancall
collidesWithItem()ontheitem,passingtheotheritemasanargument.
Finishingthegame
Thelastpartwehavetodiscussisthescene'sinitialization.Setthe
initialvaluesforallfieldsandtheconstructor,create
theinitPlayField()functionthatwillsetupalltheitems,andcallthat
functionintheconstructor.First,weinitializethesky,trees,
ground,andplayeritem:
voidMyScene::initPlayField()
{
setSceneRect(0,0,500,340);
m_sky=newBackgroundItem(QPixmap(":/sky"));
addItem(m_sky);
BackgroundItem*ground=newBackgroundItem(QPixmap(":/ground"));
addItem(ground);
ground->setPos(0,m_groundLevel);
m_trees=newBackgroundItem(QPixmap(":/trees"));
m_trees->setPos(0,m_groundLevel-m_trees-
>boundingRect().height());
addItem(m_trees);
m_grass=newBackgroundItem(QPixmap(":/grass"));
m_grass->setPos(0,m_groundLevel-m_grass-
>boundingRect().height());
addItem(m_grass);
m_player=newPlayer();
m_minX=m_player->boundingRect().width()*0.5;
m_maxX=m_fieldWidth-m_player->boundingRect().width()*0.5;
m_player->setPos(m_minX,m_groundLevel-m_player-
>boundingRect().height()/2);
m_currentX=m_minX;
addItem(m_player);
//...
}
Next,wecreatecoinobjects:
m_coins=newQGraphicsRectItem(0,0,m_fieldWidth,m_jumpHeight);
m_coins->setPen(Qt::NoPen);
m_coins->setPen(Qt::NoPen);
m_coins->setPos(0,m_groundLevel-m_jumpHeight);
constintxRange=(m_maxX-m_minX)*0.94;
for(inti=0;i<25;++i){
Coin*c=newCoin(m_coins);
c->setPos(m_minX+qrand()%xRange,qrand()%m_jumpHeight);
}
addItem(m_coins);
Intotal,weareadding25coins.First,wesetupaninvisibleitem
withthesizeofthevirtualworld,calledm_coins.Thisitemshouldbe
theparenttoallcoins.Then,wecalculatethewidthbetweenm_minX
andm_maxX.ThatisthespacewhereBenjamincanmove.Tomakeita
littlebitsmaller,weonlytake94percentofthatwidth.Then,inthe
forloop,wecreateacoinandrandomlysetitsxandyposition,
ensuringthatBenjamincanreachthembycalculatingthemodulo
oftheavailablewidthandofthemaximaljumpheight.Afterall25
coinsareadded,weplacetheparentitemholdingallthecoinson
thescene.Sincemostcoinsareoutsidetheactualview'srectangle,
wealsoneedtomovethecoinswhileBenjaminismoving.
Therefore,m_coinsmustbehavelikeanyotherbackground.Forthis,
wesimplyaddthefollowingcodetothemovePlayer()function,where
wealsomovethebackgroundbythesamepattern:
applyParallax(ratio,m_coins);
Haveagohero-Extendingthe
game
That'sit.Thisisourlittlegame.Ofcourse,thereismuchroomto
improveandextendit.Forexample,youcanaddsomebarricades
Benjaminhastojumpover.Then,youwouldhavetocheckwhether
theplayeritemcollideswithsuchabarricadeitemwhenmoving
forward,andifso,refusemovement.Youhavelearnedallthe
necessarytechniquesyouneedforthattask,sotrytoimplement
someadditionalfeaturestodeepenyourknowledge.
Athirdwayofanimation
BesidesQTimerandQPropertyAnimation,thereisathirdwaytoanimatethe
scene.Thesceneprovidesaslotcalledadvance().Ifyoucallthatslot,
thescenewillforwardthatcalltoallitemsitholdsbycalling
advance()oneachone.Thescenedoesthattwice.First,allitem
advance()functionsarecalledwith0asanargument.Thismeansthat
theitemsareabouttoadvance.Then,inthesecondround,allitems
arecalledpassing1totheitem'sadvance()function.Inthatphase,
eachitemshouldadvance,whateverthatmeans—maybemoving,
maybeacolorchange,andsoon.Thescene'sslotadvanceis
typicallycalledbyaQTimeLineelement;withthis,youcandefinehow
manytimesduringaspecificperiodoftimethetimelineshouldbe
triggered.
QTimeLine*timeLine=newQTimeLine(5000,this);
timeLine->setFrameRange(0,10);
ThistimelinewillemittheframeChanged()signalevery5secondsfor10
times.Allyouhavetodoisconnectthatsignaltothescene'sadvance()
slot,andthescenewilladvance10timesin50seconds.However,
sinceallitemsreceivetwocallsforeachadvance,thismaynotbe
thebestanimationsolutionforsceneswithalotofitemswhereonly
afewshouldadvance.
Popquiz
Q1.Whichofthefollowingisarequirementforanimatinga
property?
1. Thenameofthepropertymuststartwith"m_".
2. Getterandsetterofthepropertymustbeslots.
3. ThepropertymustbedeclaredusingtheQ_PROPERTYmacro.
Q2.Whichclasssendsasignalwhenagamepadbuttonispressed
orreleased?
1. QGamepad
2. QWidget
3. QGraphicsScene
Q3.Whatisthedifferencebetweentheshape()andboundingRect()
functionsofQGraphicsItem?
1. shape()returnstheboundingrectangleasaQPainterPathinstead
ofaQRectF
2. shape()causestheitemtoberepainted.
3. share()canreturnamoreprecisedescriptionoftheitem's
boundariesthanboundingRect()
Summary
Inthischapter,youdeepenedyourknowledgeaboutitems,about
thescene,andabouttheview.Whiledevelopingthegame,you
becamefamiliarwithdifferentapproachesofhowtoanimateitems,
andyouweretaughthowtodetectcollisions.Asanadvancedtopic,
youwereintroducedtoparallaxscrolling.
AfterhavingcompletedthetwochaptersdescribingGraphicsView,
youshouldnowknowalmosteverythingaboutit.Youareableto
createcompletecustomitems,youcanalterorextendstandard
items,andwiththeinformationaboutthelevelofdetail,youeven
havethepowertoalteranitem'sappearance,dependingonits
zoomlevel.Youcantransformitemsandthescene,andyoucan
animateitemsandthustheentirescene.
Furthermore,asyousawwhiledevelopingthegame,yourskillsare
goodenoughtodevelopajump-and-rungamewithparallax
scrolling,asitisusedinhighlyprofessionalgames.Wealsolearned
howtoaddgamepadsupporttoourgame.Tokeepitfluidand
highlyresponsive,finallywesawsometricksonhowtogetthemost
outofGraphicsView.
WhenweworkedwithwidgetsandtheGraphicsViewframework,
wehadtousesomegeneralpurposeQttypes,suchasQStringor
QVector.Insimplecases,theirAPIisprettyobvious.However,these
andmanyotherclassesprovidedbyQtCoremodulearevery
powerful,andyouwillgreatlybenefitfromdeeperknowledgeof
them.Whenyoudevelopaseriousproject,it'sveryimportantto
understandhowthesebasictypesworkandwhatdangerstheymay
posewhenusedincorrectly.Inthenextchapter,wewillturnour
attentiontothistopic.Youwilllearnhowyoucanworkwithtextin
Qt,whichcontainersyoushoulduseindifferentcases,andhowto
manipulatevariouskindofdataandimplementapersistent
storage.Thisisessentialforanygamethatismorecomplicated
thanoursimpleexamples.
QtCoreEssentials
ThischapterwillhelpyoumasterQtwaysofbasicdataprocessing
andstorage.Firstofall,youwilllearnhowtohandletextualdata
andhowtomatchtextagainstregularexpressions.Next,wewill
provideanoverviewofQtcontainersanddescribecommonpitfalls
relatedtothem.Then,youwillseehowtostoreandfetchdatafrom
filesandhowtousedifferentstorageformatsfortextandbinary
data.Bytheendofthischapter,youwillbeabletoimplementnon-
triviallogicanddataprocessinginyourgamesefficiently.Youwill
alsoknowhowtoloadexternaldatainyourgamesandhowtosave
yourowndatainpermanentstorageforfutureuse.
Maintopicscoveredinthischapter:
Texthandling
Qtcontainers
SerializationtoINI,JSON,XML,andbinarydata
Savingtheapplication'ssettings
Texthandling
Applicationswithagraphicaluserinterface(andgamessurelyfall
intothiscategory)areabletointeractwithusersbydisplayingtext
andbyexpectingtextualinputfromtheuser.Wehavealready
scratchedthesurfaceofthistopicinthepreviouschaptersusingthe
QStringclass.Now,wewillgointofurtherdetail.
Stringencodings
TheC++languagedoesnotspecifyencodingofstrings.Thus,any
char*arrayandanystd::stringobjectcanuseanarbitraryencoding.
WhenusingthesetypesforinteractionwithnativeAPIsandthird-
partylibraries,youhavetorefertotheirdocumentationtofindout
whichencodingtheyuse.TheencodingusedbynativeAPIsofthe
operatingsystemusuallydependsonthecurrentlocale.Third-party
librariesoftenusethesameencodingasnativeAPIs,butsome
librariesmayexpectanotherencoding,forexample,UTF-8.
Astringliteral(thatis,eachbaretextyouwrapinquotationmarks)
willuseanimplementationdefinedencoding.SinceC++11,you
haveanoptiontospecifytheencodingyourtextwillhave:
u8"text"willproduceaUTF-8encodedconstchar[]array
u"text"willproduceaUTF-16encodedconstchar16_t[]array
U"text"willproduceaUTF-32encodedconstchar32_t[]array
Unfortunately,theencodingusedforinterpretingthesourcefilesis
stillimplementationdefined,soit'snotsafetoputnon-ASCII
symbolsinstringliterals.Youshoulduseescapesequences(suchas
\unnnn)towritesuchliterals.
TextinQtisstoredusingtheQStringclassthatusesUnicode
internally.Unicodeallowsustorepresentcharactersinalmostall
languagesspokenintheworldandisthedefactostandardfor
nativeencodingoftextinmostmodernoperatingsystems.There
aremultipleUnicode-basedencodings.Memoryrepresentationof
thecontentofQStringresemblesUTF-16encoding.Basically,it
consistsofanarrayof16-bitvalueswhereeachUnicodecharacteris
representedbyeither1or2values.
WhenconstructingaQStringfromachararrayoranstd::stringobject,
it'simportanttouseaproperconversionmethodthatdependson
theinitialencodingofthetext.Bydefault,QStringassumesUTF-8
encodingoftheinputtext.UTF-8iscompatiblewithASCII,so
passingUTF-8orASCII-onlytexttoQString(constchar*str)iscorrect.
QStringprovidesanumberofstaticmethodstoconvertfromother
encodingssuchasQString::fromLatin1()orQString::fromUtf16().
QString::fromLocal8Bit()methodassumestheencodingcorrespondingto
thesystemlocale.
IfyouhavetocombinebothQStringandstd::stringinoneprogram,
QStringoffersyouthetoStdString()andfromStdString()methodsto
performaconversion.ThesemethodsalsoassumeUTF-8encoding
ofstd::string,soyoucan'tusethemifyourstringsareinanother
encoding.
Defaultrepresentationofstringliterals(forexample,"text")isnot
UTF-16,soeachtimeyouconvertittoaQString,anallocationand
conversionhappens.Thisoverheadcanbeavoidedusing
theQStringLiteralmacro:
QStringstr=QStringLiteral("I'mwritingmygamesusingQt");
QStringLiteraldoestwothings:
Itaddsauprefixtoyourstringliteraltoensurethatitwillbe
encodedinUTF-16atcompiletime
ItcheaplycreatesaQStringandinstructsittousetheliteral
withoutperforminganyallocationorencodingconversion
It'sagoodhabittowrapallyourstringliterals(excepttheonesthat
needtobetranslated)intoQStringLiteralbutitisnotrequired,so
don'tworryifyouforgettodothat.
QByteArrayandQString
QStringalwayscontainsUTF-16encodedstrings,butwhatifyouhave
datainanunknown(yet)encoding?Also,whatifthedataisnot
eventext?Inthesecases,QtusestheQByteArrayclass.Whenyouread
datadirectlyfromafileorreceiveitfromanetworksocket,Qtwill
returnthedataasaQByteArray,indicatingthatthisisanarbitrary
arrayofbyteswithoutanyinformationabouttheencoding:
QFilefile("/path/to/file");
file.open(QFile::ReadOnly);
QByteArrayarray=file.readAll();
TheclosestequivalentofQByteArrayinthestandardlibrarywouldbe
std::vector<char>.Asthenameimplies,thisisjustanarrayofbytes
withsomehelpfulmethods.Intheprecedingexample,ifyouknow
thatthefileyoureadisinUTF-8,youcanconvertthedatatoa
string,asfollows:
QStringtext=QString::fromUtf8(array);
Ifyouhavenoideawhatencodingthefileuses,itmaybebesttouse
thesystemencoding,soQString::fromLocal8Bitwouldbebetter.
Similarly,whenwritingtoafile,youneedtoconvertthestringtoa
bytearraybeforepassingittothewrite()function:
QStringtext="newfilecontent\n";
QFilefile("/path/to/file");
file.open(QFile::WriteOnly);
QByteArrayarray=text.toUtf8();
file.write(array);
Youcanusefile.close()toclosethefile.QFilewillalsoautomaticallyclosethefilewhen
deleted,soifyourQFileobjectgoesoutofscopeimmediatelyafteryou'vefinishedworking
withthefile,thereisnoneedforanexplicitclose()call.
Usingotherencodings
Aswe'vealreadymentioned,QStringhasconvenientmethodsfor
decodingandencodingdatainthemostpopularencodings,suchas
UTF-8,UTF-16,andLatin1.However,Qtknowshowtohandle
manyotherencodingsaswell.Youcanaccessthemusingthe
QTextCodecclass.Forexample,ifyouhaveafileinBig-5encoding,you
canaskQtforacodecobjectbyitsnameandmakeuseofthe
fromUnicode()andtoUnicode()methods:
QByteArraybig5Encoded=big5EncodedFile.readAll();
QTextCodec*big5Codec=QTextCodec::codecForName("Big5");
QStringtext=big5Codec->toUnicode(big5Encoded);
QByteArraybig5EncodedBack=big5Codec->fromUnicode(text);
Youcanlistthecodecssupportedonyourinstallationusingthe
QTextCodec::availableCodecs()staticmethod.Inmostinstallations,Qtcanhandlealmost
1,000differenttextcodecs.
Basicstringoperations
Themostbasictasksthatinvolvetextstringsaretheoneswhere
youaddorremovecharactersfromthestring,concatenatestrings,
andaccessthestring'scontent.Inthisregard,QStringoffersan
interfacethatiscompatiblewithstd::string,butitalsogoesbeyond
that,exposingmanymoreusefulmethods.
Addingdataatthebeginningorattheendofthestringcanbedone
usingtheprepend()andappend()methods.Insertingdatainthemiddle
ofastringcanbedonewiththeinsert()methodthattakesthe
positionofthecharacterwhereweneedtostartinsertingasitsfirst
argumentandtheactualtextasitssecondargument.Allthese
methodshaveacoupleofoverloadsthatacceptdifferentobjects
thatcanholdtextualdata,includingtheclassicconstchar*array.
Removingcharactersfromastringissimilar.Thebasicwaytodo
thisistousetheremove()methodthatacceptsthepositionatwhich
weneedtodeletecharacters,andthenumberofcharacterstodelete
isasshown:
QStringstr=QStringLiteral("abcdefghij");
str.remove(2,4);//str="abghij"
Thereisalsoaremove()overloadthatacceptsanotherstring.When
called,allitsoccurrencesareremovedfromtheoriginalstring.This
overloadhasanoptionalargumentthatstateswhethercomparison
shouldbedoneinthedefaultcase-sensitive(Qt::CaseSensitive)orcase-
insensitive(Qt::CaseInsensitive)way:
QStringstr=QStringLiteral("Abracadabra");
str.remove(QStringLiteral("ab"),Qt::CaseInsensitive);
//str="racadra"
Toconcatenatestrings,youcaneithersimplyaddtwostrings
together,oryoucanappendonestringtotheother:
QStringstr1=QStringLiteral("abc");
QStringstr2=QStringLiteral("def");
QStringstr1_2=str1+str2;
QStringstr2_1=str2;
str2_1.append(str1);
Accessingstringscanbedividedintotwousecases.Thefirstis
whenyouwishtoextractapartofthestring.Forthis,youcanuse
oneofthesethreemethods—left(),right(),andmid()—thatreturnthe
givennumberofcharactersfromthebeginningorendofthestring
orextractasubstringofaspecifiedlength,startingfromagiven
positioninthestring:
QStringoriginal=QStringLiteral("abcdefghij");
QStringl=original.left(3);//"abc"
QStringr=original.right(2);//"ij"
QStringm=original.mid(2,5);//"cdefg"
Thesecondusecaseiswhenyouwishtoaccessasinglecharacterof
thestring.TheuseoftheindexoperatorworkswithQStringina
similarfashionaswithstd::string,returningacopyornon-const
referencetoagivencharacterthatisrepresentedbytheQCharclass,
asshowninthefollowingcode:
QStringstr="foo";
QCharf=str[0];//const
str[0]='g';//non-const
Inadditiontothis,Qtoffersadedicatedmethod—at()—thatreturns
acopyofthecharacter:
QCharf=str.at(0);
Youshouldprefertouseat()insteadoftheindexoperatorforoperationsthatdonot
modifythecharacter,asthisexplicitlyusesaconstantmethod.
Thestringsearchandlookup
Thesecondgroupoffunctionalitiesisrelatedtosearchingforthe
string.YoucanusemethodssuchasstartsWith(),endsWith(),and
contains()tosearchforsubstringsinthebeginningorendorinan
arbitraryplaceinthestring.Thenumberofoccurrencesofa
substringinthestringcanberetrievedusingthecount()method.
Becareful,thereisalsoacount()methodthatdoesn'ttakeanyparametersandreturnsthe
numberofcharactersinthestring.
Ifyouneedtoknowtheexactpositionofthematch,youcanuse
indexOf()orlastIndexOf()toreceivethepositioninthestringwherethe
matchoccurs.Thefirstcallworksbysearchingforward,andthe
otheronesearchesbackwards.Eachofthesecallstakestwo
optionalparameters—thesecondonedetermineswhetherthe
searchiscase-sensitive(similartohowremoveworks).Thefirstoneis
thepositioninthestringwherethesearchbegins.Itletsyoufindall
theoccurrencesofagivensubstring:
intpos=-1;
QStringstr=QStringLiteral("Orangutanslikebananas.");
do{
pos=str.indexOf("an",pos+1);
qDebug()<<"'an'foundstartsatposition"<<pos;
}while(pos!=-1);
Dissectingstrings
Thereisonemoregroupofusefulstringfunctionalitiesthatmakes
QStringdifferentfromstd::string,thatis,cuttingstringsintosmaller
partsandbuildinglargerstringsfromsmallerpieces.
Veryoften,astringcontainssubstringsthataregluedtogetherbya
repeatingseparator(forexample,"1,4,8,15").Whileyoucanextract
eachfieldfromtherecordusingfunctionsthatyoualreadyknow
(forexample,indexOf),aneasierwayexists.QStringcontainsasplit()
methodthattakestheseparatorstringasitsparameterandreturns
alistofstringsthatarerepresentedinQtbytheQStringListclass.
Then,dissectingtherecordintoseparatefieldsisaseasyascalling
thefollowingcode:
QStringrecord="1,4,8,15,16,24,42";
QStringListitems=record.split(",");
for(constQString&item:items){
qDebug()<<item;
}
Theinverseofthismethodisthejoin()methodpresentinthe
QStringListclass,whichreturnsalltheitemsinthelistasasingle
stringmergedwithagivenseparator:
QStringListfields={"1","4","8","15","16","24","42"};
QStringrecord=fields.join(",");
Convertingbetweennumbers
andstrings
QStringalsoprovidessomemethodsforconvenientconversion
betweentextualandnumericalvalues.MethodssuchastoInt(),
toDouble(),ortoLongLong()makeiteasytoextractnumericalvaluesfrom
strings.Allsuchmethodstakeanoptionalbool*okparameter.Ifyou
passapointertoaboolvariableasthisparameter,thevariablewill
besettotrueorfalse,dependingonwhethertheconversionwas
successfulornot.Methodsreturningintegersalsotakethesecond
optionalparameterthatspecifiesthenumericalbase(forexample,
binary,octal,decimal,orhexadecimal)ofthevalue:
boolok;
intv1=QString("42").toInt(&ok,10);
//v1=42,ok=true
longlongv2=QString("0xFFFFFF").toInt(&ok,16);
//v2=16777215,ok=true
doublev3=QString("notreallyanumber").toDouble(&ok);
//v3=0.0,ok=false
Astaticmethodcallednumber()performstheconversionintheother
direction—ittakesanumericalvalueandnumberbaseandreturns
thetextualrepresentationofthevalue:
QStringtxt=QString::number(42);//txt="42"
Thisfunctionhassomeoptionalargumentsthatallowyouto
controlthestringrepresentationofthenumber.Forintegers,you
canspecifythenumericalbase.Fordoubles,youcanchoosethe
scientificformat'e'ortheconventionalformat'f'andspecifythe
numberofdigitsafterthedecimaldelimiter:
QStrings1=QString::number(42,16);//"2a"
QStrings2=QString::number(42.0,'f',6);//"42.000000"
QStrings3=QString::number(42.0,'e',6);//"4.200000e+1"
Someoftheotherclassesthatrepresentvaluesalsoprovideconversionstoandfrom
QString.AnexampleofsuchaclassisQDate,whichrepresentsadateandprovidesthe
fromString()andtoString()methods.
Thesemethodsareniceandeasytousefortechnicalpurposes,for
example,forreadingandwritingnumberstoconfigurationfiles.
However,theyarenotsuitablewhenyouneedtodisplayanumber
totheuserorparseauserinputbecausenumbersarewritten
differentlyindifferentcountries.Thisbringsustothetopicof
internationalization.
Internationalization
Mostrealprojectshaveatargetaudienceinmultiplecountries.The
mostnotabledifferencebetweenthemisthespokenlanguage,but
thereareotheraspectssomedevelopersmaynotthinkof.For
example,dot"."andcomma","arebothfairlycommonasthe
decimalseparatorthroughouttheworld.Dateformatsarealsovery
differentandincompatible,andusingawrongformat(for
example,mm/dd/yyyyinsteadofdd/mm/yyyy)willresultinacompletely
differentdate.
QtprovidestheQLocaleclassfordealingwithlocale-dependent
operations,includingconversionsbetweennumbersinstrings.In
thefollowingcode,textandnumbermayhavedifferentvalues,
dependingonthesystemlocale:
QLocalelocale=QLocale::system();
QStringtext=locale.toString(1.2);
doublenumber=locale.toDouble(QStringLiteral("1,2"));
QLocalealsoprovidesmethodsforformattingdatesandprices,and
allowsustorequestadditionalinformationaboutlocalconventions.
Asfortranslations,we'vealreadymentionedthatanytextvisibleto
usersshouldbewrappedinatr()function.Nowwewillexplainthis
requirement.
Qt'stranslationsystemmakesitpossiblefordevelopingand
translationteamstoworkindependently.Theprojectgoesthrough
thefollowingsteps:
1. Developerscreateanapplicationandwrapalltextthat
shouldbetranslatedinspecialtranslationfunctions(suchas
tr()).Visibletextinformsisautomaticallywrappedin
translationfunctions.
2. AspecialQttool(lupdate)searchesforallstringswrapped
intranslationfunctionsandgeneratesatranslationfile(.ts).
3. TranslatorsopenthisfileinaspecialapplicationcalledQt
Linguist.Inthatapplication,theyareabletoseeallstrings
groupedbycontext,whichisusuallytheclassthistext
belongsto.Theycanaddtranslationsandsavetheminthe
translationfile.
4. Whenthisnewtranslationfileiscopiedbacktotheproject
andappliedusingtheQCoreApplication::installTranslatorfunction,
thetranslationfunctionsstartreturningtranslatedtext
insteadofsimplyreturningtheargument.
5. Astheapplicationevolvesandanewuntranslatedtext
appears,it'sshownuntranslatedbydefault.However,itcan
beautomaticallyaddedtotranslationfiles,andtranslators
canaddnewtranslationsfornewcontent,withoutlosingthe
existingtranslations.
Wewillnotgointothedetailsofthisprocess.Asadeveloper,you
onlyneedtoensurethatallvisiblestringsarewrappedina
translationfunctionandapropercontextisprovided.Thecontextis
necessarybecauseashorttext(forexample,onewordonabutton)
maynotbeenoughtounderstandthemeaningandprovidea
propertranslation,buthowdowespecifythecontext?
ThemaintranslationfunctionisQCoreApplication::translate().Itaccepts
threearguments:thecontext,thetexttotranslate,andanoptional
disambiguationtext.Thedisambiguationargumentisrarely
needed.Itcanbeusedtodistinguishbetweenmultipleinstancesof
thesametextinthesamecontextandwhentheyshouldhave
differenttranslations.
InsteadofQCoreApplication::translate(),youshouldusuallyusethetr()
function,whichisdeclaredineachclassthatinheritsQObject.
MyClass::tr(text,disambiguation)isashortcutfor
QCoreApplication::translate("MyClass",text,disambiguation).Duetothis,all
translatabletextslocatedinoneclasswillsharethesamecontext
string,sotheywillbegroupedinQtLinguisttomakethe
translator'sjobeasier.
IfyouhaveatranslatabletextoutsideofasubclassofQObject,thetr()
functionwillnotbeavailablebydefault.Inthiscase,youhavethe
followingoptions:
UsetheQCoreApplication::translate()functionandwritethe
contextargumentexplicitly
Reusethetr()functionofarelevantclass(for
example,MyClass::tr())
Declarethetr()functioninyour(non-QObject-based)classby
addingtheQ_DECLARE_TR_FUNCTIONS(context)macroatthetopofthe
classdeclaration
Notethatthetranslationfunctionsshouldreceivethestringliterals
directly.Otherwise,lupdatewillnotbeabletounderstandwhich
textisbeingtranslated.Thefollowingcodeisincorrect,becausethe
twostringswillnotbeseenbytranslators:
constchar*text;
if(condition){
text="translatable1";
}else{
text="translatable2";
}
QStringresult=tr(text);//notrecognized!
Thesimplestwaytofixthisissueistoapplythetr()function
directlytoeachstringliteral:
QStringresult;
if(condition){
result=tr("translatable1");
}else{
result=tr("translatable2");
}
AnothersolutionistomarktranslatabletextwiththeQT_TR_NOOP
macro:
if(condition){
text=QT_TR_NOOP("translatable1");
}else{
text=QT_TR_NOOP("translatable2");
}
QStringresult=tr(text);
TheQT_TR_NOOPmacroreturnsitsargumentasis,butlupdatewill
recognizethatthesestringsmustbetranslated.
It'salsopossibletoaddacommentforthetranslatorusingaspecial
formofC++comments://:...or/*:...*/.Considerthisexample:
//:Thebuttonforsendingattachmentfiles
QPushButton*button=newQPushButton(tr("Send"));
Inthissection,weonlydescribedtheabsoluteminimumyouneedtoknowbeforestarting
workonamultilanguagegame.Thisknowledgecansaveyoualotoftime,becauseit's
mucheasiertomarksometextfortranslationasyouwriteitthantogothroughalarge
codebaseanddoitlater.However,youwillneedtolearnmoretoactuallyimplement
internationalizationinyourproject.Wewillcoverthistopicindepthlater(Onlinechapter,
https://www.packtpub.com/sites/default/files/downloads/MiscellaneousandAdvancedConcepts.
pdf).
Usingargumentsinstrings
Acommontaskistohaveastringthatneedstobedynamicinsuch
awaythatitscontentdependsonthevalueofsomeexternal
variable—forinstance,youwouldliketoinformtheuseraboutthe
numberoffilesbeingcopied,showing"copyingfile1of2"or
"copyingfile2of5"dependingonthevalueofcountersthatdenote
thecurrentfileandthetotalnumberoffiles.Itmightbetempting
todothisbyassemblingallthepiecestogetherusingoneofthe
availableapproaches:
QStringstr="Copyingfile"+QString::number(current)
+"of"+QString::number(total);
Thereareanumberofdrawbackstosuchanapproach;thebiggest
oneistheproblemoftranslatingthestringintootherlanguages,
whereindifferentlanguagestheirgrammarmightrequirethetwo
argumentstobepositioneddifferentlythaninEnglish.
Instead,Qtallowsustospecifypositionalparametersinstringsand
thenreplacethemwithrealvalues.Thisapproachiscalledstring
interpolation.Positionsinthestringaremarkedwiththe%sign
(forexample,%1,%2,andsoon)andtheyarereplacedbymakinga
calltoarg()andpassingitthevaluethatisusedtoreplacethenext
lowestmarkerinthestring.Ourfilecopymessageconstruction
codethenbecomesthis:
QStringstr=tr("Copyingfile%1of%2").arg(current).arg(total);
Contrarytothebehavioroftheprintf()built-infunction,youdon't
needtospecifythetypesofvaluesintheplaceholders(like%dor%s).
Instead,thearg()methodhasanumberofoverloadsthataccept
singlecharacters,strings,integers,andrealnumbers.Thearg()
methodhasthesameoptionalargumentsthatQString::number()has,
allowingyoutoconfigurehownumbersareformatted.Additionally,
thearg()methodhasthefieldWidthargumentthatforcesittoalways
outputthestringofaspecifiedlength,whichisconvenientfor
formattingtables:
constintfieldWidth=4;
qDebug()<<QStringLiteral("%1|%2").arg(5,fieldWidth).arg(6,
fieldWidth);
qDebug()<<QStringLiteral("%1|%2").arg(15,fieldWidth).arg(16,
fieldWidth);
//output:
//"5|6"
//"15|16"
Ifyouwanttouseacharacterotherthanspacetofillemptyspaces,
usethefillCharoptionalargumentofarg().
Regularexpressions
Let'sbrieflytalkaboutregularexpressions—usuallyshortened
as"regex"or"regexp".Youwillneedtheseregularexpressions
wheneveryouhavetocheckwhetherastringorpartofitmatchesa
givenpatternorwhenyouwanttofindspecificpartsinsidethetext
andpossiblywanttoextractthem.Boththevaliditycheckandthe
finding/extractionarebasedontheso-calledpatternoftheregular
expression,whichdescribestheformatastringmusthavetobe
valid,tobefound,ortobeextracted.Sincethisbookisfocusedon
Qt,thereisunfortunatelynotimetocoverregularexpressionsin
depth.Thisisnotahugeproblem,however,sinceyoucanfind
plentyofgoodwebsitesthatprovideintroductionstoregular
expressionsontheinternet.
Eventhoughtherearemanyflavorsoftheregularexpression's
syntax,theonethatPerluseshasbecomethedefactostandard.In
Qt,theQRegularExpressionclassprovidesPerl-compatibleregular
expressions.
QRegularExpressionwasfirstintroducedwithQt5.0.Inthepreviousversions,theonly
regularexceptionclasswasQRegExp,andit'sstillavailableforcompatibility.Since
QRegularExpressionisclosertothePerlstandardandsinceitsexecutionspeedismuchfaster
ascomparedtoQRegExp,weadviseyoutouseQRegularExpressionwheneverpossible.
Nevertheless,youcanreadtheQRegExpdocumentation,whichcontainsanicegeneral
introductionofregularexpressions.
Timeforaction–Asimplequiz
game
TointroduceyoutothemainusageofQRegularExpression,let'simagine
thisgame:aphoto,showinganobject,isshowntomultipleplayers,
andeachofthemhastoestimatetheobject'sweight.Theplayer
whoseestimateisclosesttotheactualweightwins.Theestimates
willbesubmittedviaQLineEdit.Sinceyoucanwriteanythinginaline
edit,wehavetoensurethatthecontentisvalid.
Sowhatdoesvalidmean?Inthisexample,wedefinethatavalue
between1gand999kgisvalid.Knowingthisspecification,wecan
constructaregularexpressionthatwillverifytheformat.Thefirst
partofthetextisanumber,whichcanbebetween1and999.Thus,
thecorrespondingpatternlookslike[1-9]\d{0,2},where[1-9]allows—
anddemands—exactlyonedigit,exceptzero.It'soptionally
followedbyuptotwodigits,includingzero.Thisisexpressed
through\d{0,2},where\dmeans"anydigit",0istheminimalallowed
count,and2isthemaximalallowedcount.Thelastpartofthe
inputistheweight'sunit.Withapatternsuchas(mg|g|kg),weallow
theweighttobeinputinmilligrams(mg),grams(g),orkilograms
(kg).With\s*,wefinallyallowanarbitrarynumberofwhitespace
charactersbetweenthenumberandunit.Let'scombineitall
togetherandtestourregularexpressionrightaway:
QRegularExpressionregex("[1-9]\\d{0,2}\\s*(mg|g|kg)");
regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
qDebug()<<regex.match("100kg").hasMatch();//true
qDebug()<<regex.match("Idon'tknow").hasMatch();//false
Whatjusthappened?
Inthefirstline,weconstructedtheaforementionedQRegularExpression
object,whilepassingtheregularexpression'spatternasa
parametertotheconstructor.Notethatwehavetoescapethe\
character,becauseithasspecialmeaninginC++syntax.
Regularexpressionsarecase-sensitivebydefault.However,we
wanttoallowtheinputtobeinuppercaseormixedcase.Toachieve
this,wecan,ofcourse,write(mg|mG|Mg|MG|g|G|kg|kG|Kg|KG)orconvertthe
stringtolowercasebeforematching,butthereisamuchcleaner
andmorereadablesolution.Onthesecondlineofthecode
example,youseetheanswer—apatternoption.Weused
setPatternOptions()tosettheQRegularExpression::CaseInsensitiveOptionoption,
whichdoesnotrespectthecaseofthecharactersused.Ofcourse,
thereareafewmoreoptionsthatyoucanreadaboutinQt's
documentationonQRegularExpression::PatternOption.Insteadofcalling
setPatternOptions(),wecouldalsohavepassedtheoptionasasecond
parametertotheconstructorofQRegularExpression:
QRegularExpressionregex("[1-9]\\d{0,2}\\s*(mg|g|kg)",
QRegularExpression::CaseInsensitiveOption);
Whenweneedtotestaninput,allwehavetodoiscallmatch(),
passingthestringwewouldliketocheckagainstit.Inreturn,we
getanobjectoftheQRegularExpressionMatchtypethatcontainsallthe
informationthatisfurtherneeded—andnotonlytocheckthe
validity.WithQRegularExpressionMatch::hasMatch(),wethencandetermine
whethertheinputmatchesourcriteria,asitreturnstrueifthe
patterncouldbefound.Otherwise,ofcourse,falseisreturned.
Ourpatternisnotquitefinished.ThehasMatch()methodwouldalso
returntrueifwematcheditagainst"foo142gbar".So,wehaveto
definethatthepatternischeckedfromthebeginningtotheendof
thematchedstring.Thisisdonebythe\Aand\zanchors.The
formermarksthestartofastringandthelattertheendofastring.
Don'tforgettoescapetheslasheswhenyouusesuchanchors.The
correctpatternwillthenlooklikethis:
QRegularExpressionregex("\\A[1-9]\\d{0,2}\\s*(mg|g|kg)\\z",
QRegularExpression::CaseInsensitiveOption);
Extractinginformationoutofa
string
Afterwehavecheckedthatthesentguessiswellformed,wehaveto
extracttheactualweightfromthestring.Inordertobeableto
easilycomparethedifferentguesses,wefurtherneedtotransform
allvaluestoacommonreferenceunit.Inthiscase,itshouldbea
milligram,thelowestunit.So,let'sseewhatQRegularExpressionMatchcan
offerusforthistask.
WithcapturedTexts(),wegetastringlistofthepattern'scaptured
groups.Inourexample,thislistwillcontain"23kg"and"kg".The
firstelementisalwaysthestringthatwasfullymatchedbythe
pattern.Thenextelementsareallthesubstringscapturedbythe
usedbrackets.Sincewearemissingtheactualnumber,wehaveto
alterthepattern'sbeginningto([1-9]\d{0,2}).Now,thelist'ssecond
elementisthenumber,andthethirdelementistheunit.Thus,we
canwritethefollowing:
intgetWeight(constQString&input){
QRegularExpressionregex("\\A([1-9]\\d{0,2})\\s*(mg|g|kg)\\z");
regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
QRegularExpressionMatchmatch=regex.match(input);
if(match.hasMatch()){
constQStringnumber=match.captured(1);
intweight=number.toInt();
constQStringunit=match.captured(2).toLower();
if(unit=="g"){
weight*=1000;
}elseif(unit=="kg"){
weight*=1000000;
}
returnweight;
}else{
return-1;
}
}
Inthefunction'sfirsttwolines,wesetupthepatternanditsoption.
Then,wematchitagainstthepassedargument.If
QRegularExpressionMatch::hasMatch()returnstrue,theinputisvalidandwe
extractthenumberandunit.Insteadoffetchingtheentirelistof
capturedtextwithcapturedTexts(),wequeryspecificelementsdirectly
bycallingQRegularExpressionMatch::captured().Thepassedinteger
argumentsignifiestheelement'spositioninsidethelist.So,calling
captured(1)returnsthematcheddigitsasaQString.
Beawarethataddingagroupatalatertimewillshifttheindicesofallthefollowing
groupsby1,andyouwillhavetoadjustyourcode!Ifyouhavelongpatternsorifthereisa
highprobabilitythatfurtherbracketswillbeaddedinfuture,youcanusenamedgroups
tomakeyourcodemoremaintainable.Thereisa
QRegularExpressionMatch::captured()overloadthatallowsyoutospecifythegroupname
insteadofindex.Forexample,ifyouhavewritten(?<number>[1-9][0-9]{0,2}),thenyoucan
getthedigitsbycallingmatch.captured("number").
Tobeabletocalculateusingtheextractednumber,weneedto
convertQStringintoaninteger.ThisisdonebycallingQString::toInt().
Theresultofthisconversionisthenstoredintheweightvariable.
Next,wefetchtheunitandtransformittolowercasecharacterson
thefly.Thisway,wecan,forexample,easilydeterminewhetherthe
user'sguessisexpressedingramsbycheckingtheunitagainstthe
lowercase"g".Wedonotneedtotakecareofthecapital"G"orthe
variants"KG","Kg",andtheunusual"kG"forkilogram.
Togetthestandardizedweightinmilligrams,wemultiplyweightby
1,000or1,000,000,dependingonwhetherthiswasexpresseding
orkg.Lastly,wereturnthisstandardizedweight.Ifthestringwasn't
wellformed,wereturn-1toindicatethatthegivenguesswas
invalid.Itisthenthecaller'sdutytodeterminatewhichplayer's
guesswasthebest.
Payattentiontowhetheryourchosenintegertypecanhandletheweight'svalue.Forour
example,999millionisthebiggestpossibleresult,and,fortunately,it'ssmallerthanthe
maximumpossiblevalueofasigned32-bitinteger(2,147,483,647).Ifyou'reunsure
whetherthetypeyouuseisbigenoughonalltargetsystems,useafixedwidthintegertype
(forexample,int64_t).
Asanexercise,trytoextendtheexamplebyallowingdecimal
numberssothat"23.5g"isavalidguess.Toachievethis,youhave
toalterthepatterninordertoenterdecimalnumbers,andyoualso
havetodealwithdoubleinsteadofintforthestandardizedweight.
Findingallpatternoccurrences
Lastly,let'stakeafinallookathowtofind,forexample,allnumbers
insideastring,eventhoseleadingwithzeros:
QStringinput=QStringLiteral("123foo091a3");
QRegularExpressionregex("\\b\\d+\\b");
QRegularExpressionMatchIteratori=regex.globalMatch(input);
while(i.hasNext()){
QRegularExpressionMatchmatch=i.next();
qDebug()<<match.captured();
}
Theinputstringcontainsanexemplarytextinwhichwewouldlike
tofindallnumbers.The"foo"aswellas"1a"variablesshouldnotbe
foundbythepattern,sincethesearenotvalidnumbers.Therefore,
wesetupthepattern,definingthatwerequireatleastonedigit,\d+,
andthatthisdigit—orthesedigits—shouldbewrappedbyword
boundaries,\b.Notethatyouhavetoescapetheslashes.Withthis
pattern,weinitiatetheQRegularExpressionobjectandcallglobalMatch()on
it.Insidethepassedargument,thepatternwillbesearched.This
time,wedonotgetQRegularExpressionMatchback;instead,wegetan
iteratoroftheQRegularExpressionMatchIteratortype.Since
QRegularExpressionMatchIteratorhasaconvenienthasNext()method,we
checkwhetherthereisafurthermatchandifso,webringupthe
nextmatchbycallingnext().Thetypeofthereturnedmatchisthen
QRegularExpressionMatch,whichyoualreadyknow.
Ifyouneedtoknowaboutthenextmatchinsidethewhileloop,youcanuse
QRegularExpressionMatchIterator::peekNext()toreceiveit.Thebenefitofthisfunctionisthat
itdoesnotmovetheiterator.
Thisway,youcaniterateallpatternoccurrencesinthestring.This
ishelpfulifyou,forexample,wanttohighlightasearchstringin
text.
Ourexamplewillgivetheoutputof"123","09",and"3".
Takingintoaccountthatthiswasjustabriefintroductiontoregular
expressions,wewouldliketoencourageyoutoreadtheDetailed
DescriptionsectioninthedocumentationtoQRegularExpression,
QRegularExpressionMatch,andQRegularExpressionMatchIterator.Regular
expressionsareverypowerfulanduseful,so,inyourdaily
programminglife,youcanbenefitfromtheprofoundknowledgeof
regularexpressions!
Containers
Whenyouneedtostoreacollectionofobjects,youneedacontainer
toholdthem.TheC++standardlibraryprovidesmanypowerful
containers,suchasstd::vector,std::list,orstd::map.However,Qt
doesn'tusethesecontainers(actually,ithardlyusesanystandard
libraryclassesatall)andprovidesitsownalternative
implementationofcontainersinstead.WhenQtcontainerswere
introduced,theyprovidedsignificantlymoreconsistent
performanceondifferentplatformscomparedtostandardlibrary
implementations,sotheywererequiredtocreatereliablecross-
platformapplications.Thisisnotreallythecasenow,asSTL
implementationsandcompilershavesinceevolvedandgainednew
optimizationsandfeatures.However,therearestillreasonstouse
Qtcontainers,especiallyinanapplicationthatheavilyusesotherQt
classes:
QtAPIalwaysusesQtcontainers.WhenyoureceiveaQList,
itwillalmostneverbemoreefficientorconvenientto
convertittoastandardlibrarycontainer.Beforecallinga
methodthatacceptsQList,youshouldpopulatetheinput
datainaQListinsteadofconvertingitfromanSTLcontainer.
Qtcontainersprovideuniquefeatures,likeimplicitsharing
(wewilldiscussitlaterinthischapter)orJava-style
iterators,andsomeconveniencemethodsSTLcontainers
lack.
QtcontainersfollowQt'snamingschemeanditsAPI
conventions,sotheylookmorenaturalinanapplication
thatiscenteredaroundQt.Forexample,QVector::isEmpty()is
moreQt-likethanstd::vector::empty().
Inaddition,QtcontainersprovideSTL-compatibleAPI(for
example,theappend()methodhasthepush_back()alias)thatallowsus
toreplaceQtcontainerswithSTLoneswithoutchangingmuchof
thecode.Rangebasedforloopandsomeofthestandardlibrary
algorithmsarealsocompatiblewithQtcontainers.Thatbeingsaid,
ifyouneedsomefeaturesthatarenotavailableinQtcontainers,
usingSTLcontainersisagoodidea.
Maincontainertypes
WhenyouinteractwithaQtAPImethod,youdon'thavemuch
choiceonthecontainertype,becauseyouneedtousethecontainer
themethoduses.However,generally,youarefreetochoose
containerstostoreyourdata.Let'sgothroughthemainQt
containersandlearnwhentousethem.
WewillonlygiveabriefoverviewofQtcontainersandwon'tgointodetailssuchasthe
algorithmiccomplexityofdifferentoperations.FormostQtcontainers,thereisasimilar
STLcontainerthatwewillname.Thetopicofchoosingtherightcontaineriswidely
discussed,andit'snothardtofindmoreinformationonit,especiallyforSTLcontainers.
YoucanalsofindmoreinformationontheContainerClassesQtdocumentationpage.
QVectorstoresitemsinacontinuousregionofmemory.Theitemsare
denselypacked,meaningthatthistypeisthemostmemoryefficient
andcachefriendly.ItsSTLequivalentisstd::vector.QVectorshouldbe
thecontainerofdefaultchoice,meaningthatyoushouldonlyusea
differentcontainerifyouhaveareasontodoit.QVectorprovidesfast
lookupbyitemnumber,fastonaverageappendingitemstotheend
andremovingitemsfromtheend.Insertingandremovingitems
fromthebeginningormiddleofthevectorisslow,becauseitcauses
allitemstotherighttoshiftinmemory.UsingQVectoris
straightforward:
QVector<int>numbers;
numbers.append(1);
numbers.append(5);
numbers.append(7);
qDebug()<<numbers.count();//3
qDebug()<<numbers[1];//5
TheQLinkedListcontainer,asthenameimplies,implementsalinked
list.ItsSTLequivalentisstd::list.AsopposedtoQVector,itprovides
fastinsertingandremovingitemsatanylocation(thebeginning,
middle,ortheend),butslowlookupbyindex,becauseitneedsto
iterateoveritemsfromthebeginningtofindtheitembyitsindex.
QLinkedListissuitablewhenyouneedtoinsertorremoveitemsatthe
middleofahugelistmultipletimes.However,notethatinpractice,
QVectorstillmaysometimesbemoreperformantinthiscase,because
QLinkedListisnotdenselypackedinmemory,whichaddssome
overhead.
QSet,aQtequivalentofstd::unordered_set,isanunorderedcollectionof
uniqueitems.Itsadvantageistheabilitytoefficientlyadditems,
removeitems,andcheckwhetheraparticularitemispresentina
collection.Theotherlistclassesarenotabletodothelastoperation
quickly,becausetheyneedtoiterateoverallitemsandcompare
eachitemwiththeargument.Likewithanyothercollection,you
caniterateovertheset'sitems,buttheiterationorderisnot
specified,thatis,anyitemmayappearonthefirstiteration,andso
on.AnexampleoftheQSetAPIisshowninthefollowingcode:
QSet<QString>names;
names.insert("Alice");
names.insert("Bob");
qDebug()<<names.contains("Alice");//true
qDebug()<<names.contains("John");//false
for(constQString&name:names){
qDebug()<<"Hello,"<<name;
}
ThelastflatcollectionisQList.Usingitiscurrentlynot
recommended,exceptwheninteractingwithmethodsthatacceptor
produceQListobjects.Itsperformanceandmemoryefficiency
dependsontheitemtype,andtherulesthatdefine"good"item
typesarecomplicated.Fora"bad"type,QListisrepresentedasa
vectorofvoid*,witheachitemstoredasaseparatelyallocated
objectontheheap.It'spossiblethatQListimplementationwill
changeinQt6,butthereisnoofficialinformationaboutthisyet.
Therearesomespecializedlistcontainersthatprovideextra
functionalityforaparticularitemtype:
ThealreadyfamiliarQStringclassisessentiallyavectorofQChar
(16-bitUnicodecharacters)
ThefamiliarQByteArrayisavectorofchar
QStringListisaQList<QString>withadditionalconvenient
operations
QBitArrayprovidesamemory-efficientarrayofbitswithsome
usefulAPIs
Next,therearetwomainkey-valuecollections:QMap<K,T>andQHash<K,
T>.Theyallowyoutoassociateavalue(ormultiplevalues)oftypeT
withakeyoftypeK.Theybothproviderelativelyfastlookupbykey.
WheniteratingoveraQMap(similartostd::map),theitemsaresorted
bykeys,regardlessoftheinsertionorder:
QMap<int,QString>map;
map[3]="three";
map[1]="one";
map[2]="two";
for(autoi=map.begin();i!=map.end();++i){
qDebug()<<i.key()<<i.value();
}
//output:
//1"one"
//2"two"
//3"three"
QHash(similartostd::unordered_map)hasverysimilarAPIstoQMap,butwill
iterateoveritemsinunspecifiedorder,likeQSet.YoucanreplaceQMap
withQHashinthepreviousexampleandseethattheiterationorder
willchangeevenwhenrunningthesameprogramrepeatedly.In
exchange,QHashprovidesfasteron-averageinsertionsandlookupsby
keythanQMap.YoushoulduseQHashinsteadofQMapiftheiteration
orderdoesn'tmattertoyou.
Anattentivereadermaywonderhowthecodethatlooksverydeterministiccanproduce
randomresults.Thisrandomnesswasintentionallyintroducedtoprotectagainst
algorithmiccomplexityattacksonQHashandQSet.Youcanreadthecorrespondingsection
oftheQHashdocumentationpageformoredetailsabouttheattackandwaystoconfigure
therandomization.
Finally,QPair<T1,T2>isasimpleclassthatcanholdtwovaluesof
differenttypes,justlikestd::pair.YoucanusetheqMakePair()function
tomakeapairoutoftwovalues.
Conveniencecontainers
Inadditiontothecontainersdescribedearlier,thereareafew
containersbuiltontopofthemthatprovideAPIsandbehaviorthat
aremoreconvenientinsomespecialcases:
C
o
n
t
a
i
n
e
r
Description
Q
S
t
a
c
k
AQVectorimplementingthelastin,firstout(LIFO)
structure.Itcontainsthepush()functionforaddingitemsto
thestack,thepop()functionforremovingthetopelement,and
thetop()functionforreadingthetopelementwithout
removingit.
Q
Q
u
e
u
e
AQListimplementingthefirstin,firstout(FIFO)
structure.Useenqueue()toappendanitemtothequeue,
dequeue()totaketheheaditemfromthequeue,andhead()to
readtheheaditemwithoutremovingit.
Q
M
u
l
t
i
AQMapwithanAPItailoredforhavingmultiplevaluesforone
key.QMapalreadyallowsustodoit;forexample,youcanadd
multipleitemswithonekeyusingtheQMap::insertMulti()
method.However,QMultiMaprenamesittoinsert()andhidesthe
M
a
p
originalQMap::insert()methodthatdoesn'tallowmultiple
valuesperkey.
Q
M
u
l
t
i
H
a
s
h
SimilartoQMultiMap,it'saQHashwithamoreconvenientAPIfor
storingmultiplevaluesperkey.
Q
C
a
c
h
e
Akey-valuestoragesimilartoQHashthatallowsyouto
implementacache.QCachewilldeleteitselementswhenthey
weren'trecentlyusedtokeepthesizeofcacheunderthe
maximumallowedsize.Sincethereisnowaytoknowhow
muchspaceanarbitraryitemactuallyconsumes,you
canmanuallyspecifyacostforeachitemandthemaximum
totalcostforaparticularQCacheobject.
Q
C
o
n
t
i
g
u
o
u
s
C
a
c
h
e
Aflatcontainerthatallowsyoutocacheasublistofalarge
list.Thisisuseful,forexample,whenimplementingaviewer
foralargetable,wherereadsandwritesarelikelytohappen
nearthecurrentscrolllocation.
It'sagoodideatouseoneoftheseclasseswhenyourtaskmatches
theirusecase.
Alloweditemtypes
Notalltypescanbeputincontainers.Allcontainerscanonlyhold
typesthatprovideadefaultconstructor,acopyconstructor,andan
assignmentoperator.AllprimitivetypesandmostQtdatatypes
(suchasQStringorQPointF)satisfytheserequirements.Simplestructs
alsocanbestoredinacontainerbecausetherequiredconstructors
andoperatorsaregeneratedforthemautomatically,asperC++
standard.
Aparticulartypeusuallycannotbeputinacontainerbecauseit
doesn'thaveaconstructorwithoutargumentsorcopyingthistype
wasdeliberatelydisabled.ThisisactuallythecaseforQObjectandall
itsdescendants.TheusagepatternsofQObjectsuggestthatyou
usuallywanttostorepointerstoaQObjecttorefertoitlater.Ifthat
objectwasmovedtoacontainerormovedwithinacontainer,the
pointerwouldbeinvalidated,sothereisnocopyconstructorfor
thesetypes.However,youcanputpointerstoQObjectincontainers
(forexample,QVector<QObject*>)becauseapointerisaprimitivetype
thatsatisfiesallrequirements.Inthiscase,youhavetomanually
ensurethatyourcontainerwillnotcontainanydanglingpointers
aftertheobjectsaredeleted.
Theprecedingrestrictionsapplytoitemsoflistsandvaluesofkey-
valuecollections,butwhatabouttheirkeys?Itturnsoutthatthe
keytypeshavemorerestrictionsthatdependonthecollectiontype.
QMap<K,T>additionallyrequiresthatthekeytypeKhasthe
comparisonoperatoroperator<thatprovidesatotalorder(thatis,
satisfiesaparticularsetofaxioms).Asanexception,pointertypes
arealsoallowedasakeytype.
QHash<K,T>andQSet<K>requirethattheKtypehasoperator==,andaqHash(K
key)functionoverloadexists.Qtprovidestheseoverloadsforalarge
numberoftypesforwhichit'spossible,andyoucancreatean
overloadforyourcustomtypeifneeded.
Implicitsharing
Oneofthemostsignificantdifferencesbetweenstandardlibrary
containersandQt'sistheimplicitsharingfeature.InSTL,creating
acopyofacontainerimmediatelyresultsinamemoryallocation
andcopyingthedatabuffer:
std::vector<int>x{1,2,3};
std::vector<int>y=x;//fullcopy
Ifyoudon'tintendtoeditthecopy,thisisessentiallyawasteof
resources,andyouwanttoavoidit.Thiscanbeeasilydoneinsome
casesbyprovidingareference(conststd::vector<int>&)insteadof
makingacopy.However,sometimesitbecomeshardtoensurethat
thereferencewillbevalidlongenough,forexample,ifyouwantto
storeitinaclassfield.Analternativewaytosolvethistaskisto
wrapavectorinashared_ptrtoexplicitlyshareitbetweenmultiple
objects.ThisbecomesunnecessarywhenyouworkwithQt
containersandsomeotherQttypes.
InQt,allmaincontainertypesimplementimplicitsharingor
copy-on-writesemantics.CopyingaQVectorwillnotresultinanew
memoryallocationuntileitherofthetwovectorsischanged:
QVector<int>x{1,2,3};
QVector<int>y=x;
//xandyshareonebuffernow
y[0]=5;//newallocationhappenshere
//xandyhavedifferentbuffersnow
Aslongasnoeditsaremadetothecopyortheoriginalobject,the
copyingisverycheap.Thisallowsyoutocheaplyandeasilyshare
constantdatabetweenobjectswithoutclutteringthecodewith
manualmanagementofsharedobjects.Thisfeatureisalso
implementedforQString,QPen,andmanyotherQtvaluetypes.Any
copyoperationstillhassomeruntimeoverheadcausedbyreference
counting,soyouareencouragedtopassreferencesinsteadof
makingcopieswhenit'seasy.However,thisoverheadis
insignificantinmostcases,exceptplaceswithheavycomputations.
Ifyoulikeimplicitsharing,youcanimplementitinyourowndatatypesusing
QSharedDataPointer.Refertoitsdocumentationforthein-depthinstructions.
Inmostcases,youcanjustusethecontainersasiftheydidn't
implementimplicitsharing,butthereareafewcaseswhereyou
havetobeawareofit.
Pointerinvalidation
First,implicitsharingmeansthatholdinganyreferencesor
pointerstothecontainer'scontentisdisallowedwhenthereisa
possibilityofchangingthisobjectoranyobjectthatsharesthesame
buffer.Thefollowingsmallexampleillustratestheproblem:
//don'tdothis!
QVector<int>x{1,2,3};
int*x0=x.begin();
QVector<int>y=x;
x[0]=42;
qDebug()<<*x0;//output:1
Weinitializedthex0variablewiththepointertothefirstelementof
thexvector.However,whenwesetanewvalueforthatelementand
thentriedtoreaditusingthepointer,wegottheoldvalueagain.
Whatjusthappened?
Aswecopiedthexvectortoy,thestateoftwovectorsbecame
sharedandtheoriginalbufferwasavailabletobothofthem.
However,whenwemodifiedxusingoperator[],itbecamedetached,
thatis,anewbufferwasallocatedforit,andyretainedtheoriginal
buffer.Thex0pointercontinuestopointattheoriginalbuffer,
whichisnowonlyavailabletoy.IfyouremovetheQVector<int>y=x;
line,theoutputwillchangetotheexpected42.Thegeneralruleis
thatyoushouldavoidstoringpointersorreferencestotheobject's
contentwhileit'schangedorsharedwithanotherobject.
Unnecessaryallocation
Thenextquestioniswhatactionsontheobjecttriggertheactual
allocationofanewbuffer?Obviously,x[0]=42willtriggeran
allocationbecausethevectorneedsabuffertowritethenewdata
to.However,inti=x[0]willalsotriggeranallocationifxisnot
declaredasaconstvalueorreference.ThathappensbecauseinC++
thiscodetriggersthenon-constoverloadofoperator[]ifit'savailable,
eventhoughit'snotnecessaryinthiscase.Thevectordoesn'tknow
whethertherequesteditemwillorwillnotbechanged,soithasto
assumethatitwillbe,andittriggersanallocationbeforereturning
areferencetotheiteminthenewbuffer.
Thesameissuetakeseffectwhenusingothermethodsthathave
constandnon-constoverloads,forexample,begin()ordata().The
range-basedforloopalsocallsbegin(),soitwillalsodetachifyou
iterateoveranon-constvalue.
Ifyouexplicitlydeclarethecontainervariableasconst(for
example,constQVector<int>yorconstQVector<int>&y),thenon-const
methodswillnotbeavailable,anditwillnotbepossibletotrigger
anallocationusingthisvariable.Analternativesolutionistouse
specialmethodaliasesthatareonlyavailableforconstversions,
suchasat()foroperator=,constBegin()forbegin(),andconstData()fordata().
Thissolutionisnotusablewithrange-basedforloop,though.
Range-basedforandQt
foreachmacro
QtprovidestheforeachmacroforiteratingoverQtcontainers:
QVector<int>x{1,2,3};
foreach(constinti,x){
qDebug()<<i;
}
Thismacrowasavailablelongbeforetherange-basedforloopmade
itintotheC++standard,soit'sstillverycommoninQtcode,and
youshouldbefamiliarwithit.Theforeachloopalwayscreatesa
temporaryconstantcopyoftheiteratedobject.Sinceitusesimplicit
sharing,thisisverycheap.Ifyoueditxwhileiteratingoverit,the
changeswillnotaffectthevaluesofibecausetheiterationusesa
copy,butthisalsomeansthatsuchanoperationissafe.Notethat
whenusingrange-basedforloop,STL-styleiterators,orJava-style
iterators,editingthesamecontaineryou'reiteratingoveris
generallynotsafe.Forexample,changingitemvaluesmaybe
permitted,butdeletinganitemmayresultinundefinedbehavior.
Wediscussedhowrange-basedforloopcancauseadeepcopyofthe
containers.Theforeachmacrobyitselfwillnevercauseadeepcopy.
However,ifyoueditthecontainerwhileiteratingoverit,thiswill
resultinadeepcopy,becausetwoversionsofdatahavetobestored
somewhere.
Whenusingtherange-basedforloop,youshouldbecarefulnotto
passareferencetoatemporaryobject.Forexample,thiscodelooks
legitimate,butitresultsinundefinedbehavior:
//don'tdothis!
for(QCharc:QString("abc").replace('a','z')){
qDebug()<<c;
}
Whatjusthappened?
WecreatedatemporaryQStringobjectandcalleditsreplace()method.
Thismethod'sreturntypeisQString&,soitdoesn'townthestring's
data.Ifweimmediatelyassignedthisvaluetoanowningvariable,it
wouldbecorrectbecausethelifeoftheoriginaltemporaryQString
lastsuntiltheendofthefullexpression(inthiscase,the
assignment):
QStringstring=QString("abc").replace('a','z');
for(QCharc:string){//correct
qDebug()<<c;
}
However,thetemporaryobjectintheoriginalexampledoesn'tlive
untiltheendoftheforloop,sothiswillresultinause-after-free
bug.Theforeachversionofthiscodewouldcontainanimplicit
assignmenttoavariable,soitwouldbecorrect.
Ontheotherhand,themacronatureofforeachisitsdisadvantage.
Forexample,thefollowingcodedoesnotcompilebecausetheitem
typecontainsacomma:
QVector<QPair<int,int>>x;
foreach(constQPair<int,int>&i,x){
//...
}
Theerroris"macroQ_FOREACHpassed3arguments,buttakesjust2".
Tofixthisissue,youhavetocreateatypedeffortheitemtype.
SinceC++11,range-basedforloopisanative,cleanalternativeto
foreach,sowesuggestthatyoupreferthenativeconstructoverthe
macro,butkeepinmindthepitfallswedescribed.
Datastorage
Whenimplementinggames,youwilloftenhavetoworkwith
persistentdata;youwillneedtostorethesavedgamedata,load
maps,andsoon.Forthat,youhavetolearnaboutthemechanisms
thatletyouusethedatastoredondigitalmedia.
Filesanddevices
Themostbasicandlow-levelmechanismthatisusedtoaccessdata
istosaveandloaditfromthefiles.Whileyoucanusetheclassicfile
accessapproachesprovidedbyCandC++,suchasstdiooriostream,
Qtprovidesitsownfileabstractionthathidesplatform-dependent
detailsandprovidesacleanAPIthatworksacrossallplatformsina
uniformmanner.
Thetwobasicclassesthatyouwillworkwithwhenusingfilesare
QDirandQFile.Theformerrepresentsthecontentsofadirectory,lets
youtraversefilesystems,createsandremovedirectories,and
finally,accessesallfilesinaparticulardirectory.
Traversingdirectories
TraversingdirectorieswithQDirisreallyeasy.Thefirstthingtodois
tohaveaninstanceofQDirinthefirstplace.Theeasiestwaytodo
thisistopassthedirectorypathtotheQDirconstructor.
Qthandlesfilepathsinaplatform-independentway.Eventhoughtheregulardirectory
separatoronWindowsisabackwardslashcharacter(\)andonotherplatformsitisthe
forwardslash(/),Qtinternallyalwaysusestheforwardslash,andpathsreturnedbymost
Qtmethodsnevercontainbackwardslashes.Youcanalwaysuseforwardslasheswhen
passingpathstoQtmethods,evenonWindows.IfyouneedtoconverttheQt'spath
representationtothenativeform(forexample,forpassingittothestandardlibraryora
third-partylibrary),youcanuseQDir::toNativeSeparators().QDir::fromNativeSeparators()
toperformtheinverseoperation.
Qtprovidesanumberofstaticmethodstoaccesssomespecial
directories.Thefollowingtableliststhesespecialdirectoriesand
functionsthataccessthem:
Access
function Directory
QDir::current()
Thecurrentworkingdirectory
QDir::home()
Thehomedirectoryofthecurrentuser
QDir::root()
Therootdirectory—usually/forUnixandC:\for
Windows
QDir::temp()
Thesystemtemporarydirectory
TheQStandardPathsclassprovidesinformationaboutotherstandard
locationspresentinthesystem.Forexample,
QStandardPaths::writableLocation(QStandardPaths::MusicLocation)returnspathto
theuser'smusicfolder.
RefertotheQStandardPaths::StandardLocationenumdocumentationforthelistofavailable
locations.
WhenyoualreadyhaveavalidQDirobject,youcanstartmoving
betweendirectories.Todothat,youcanusethecd()andcdUp()
methods.Theformermovestothenamedsubdirectory,whilethe
lattermovestotheparentdirectory.Youshouldalwayscheckthat
thesecommandsweresuccessful.Iftheyreturnfalse,yourQDir
objectwillremaininthesamedirectory!
Tolistfilesandsubdirectoriesinaparticulardirectory,youcanuse
theentryList()method,whichreturnsalistofentriesinthedirectory
thatmatchthecriteriapassedtoentryList().Thefiltersargument
takesalistofflagsthatcorrespondtothedifferentattributesthat
anentryneedstohavetobeincludedintheresult.Themostuseful
flagsarelistedinthefollowingtable:
Filter Meaning
QDir::Dirs,
QDir::Files,
QDir::Drives
Listdirectories,files,orWindowsdrives.You
shouldspecifyatleastoneofthesefilterstoget
anyresults.
QDir::AllEntrie
s
Listdirectories,files,anddrives.Thisisashortcut
forDirs|Files|Drives.
QDir::AllDirs
Listdirectorieseveniftheydon'tmatchthename
filters.
QDir::NoDotAndD
otDot
Don'tlist.(currentdirectory)and..(parent
directory)entries.IfDirsflagispresentand
NoDotAndDotDotisnot,theseentrieswillalwaysbe
listed.
QDir::Readable,
QDir::Writable,
QDir::Executabl
e
Listonlyentriesthatcanberead,writtento,or
executed.
QDir::Hidden,
QDir::System
Listhiddenfilesandsystemfiles.Iftheseflagsare
notspecified,hiddenandsystemflagswillnotbe
listed.
ThesortargumentofentryList()allowsyoutochoosetheorderingof
theresults:
Flag Meaning
QDir::Unsorted
Theorderofentriesisundefined.It'sagoodideato
useitiftheorderdoesn'tmattertoyou,sinceit
maybefaster.
QDir::Name,
QDir::Time,
QDir::Size,
QDir::Type
Sortbyappropriateentryattributes.
QDir::DirsFirs
t,
QDir::DirsLast
Determineswhetherdirectoriesshouldbelisted
beforeorafterfiles.Ifneitherflagisspecified,
directorieswillbemixedwithfilesintheoutput.
QDir::Reversed
Reversestheorder.
Additionally,thereisanoverloadofentryList()thatacceptsalistof
filenamepatternsintheformofQStringListasitsfirstparameter.
Here'sanexamplecallthatreturnsallJPEGfilesinthe
directorysortedbysize:
QStringListnameFilters={QStringLiteral("*.jpg"),
QStringLiteral("*.jpeg")};
QStringListentries=dir.entryList(nameFilters,
QDir::Files|QDir::Readable,QDir::Size);
BesidesentryList(),thereistheentryInfoList()methodthatwrapseach
returnedfilenameinaQFileInfoobjectthathasmanyconvenient
functions.Forexample,QFileInfo::absoluteFilePath()returnsthe
absolutepathtothefile,andQFileInfo::suffix()returnstheextension
ofthefile.
Ifyouneedtotraversedirectoriesrecursively(forexample,forfindingallfilesinall
subdirectories),youcanusetheQDirIteratorclass.
Readingandwritingfiles
Onceyouknowthepathtoafile(forexample,usingQDir::entryList(),
QFileDialog::getOpenFileName(),orsomeexternalsource),youcanpassit
toQFiletoreceiveanobjectthatactsasahandletothefile.Before
thefilecontentscanbeaccessed,thefileneedstobeopenedusing
theopen()method.Thebasicvariantofthismethodtakesamodein
whichweneedtoopenthefile.Thefollowingtableexplainsthe
modesthatareavailable:
M
o
d
e
Description
Re
ad
On
ly
Thisfilecanbereadfrom.
Wr
it
eO
nl
y
Thisfilecanbewrittento.
Re
ad
Wr
it
e
Thisfilecanbereadfromandwrittento.
Ap
pe
nd
Alldatawriteswillbewrittenattheendofthefile.
Tr
un
ca
te
Ifthefileispresent,itscontentisdeletedbeforeweopenit.
Te
xt
Whenreading,alllineendingsaretransformedto\n.When
writing,all\nsymbolsaretransformedtothenativeformat
(forexample,\r\nonWindowsor\nonLinux).
Un
bu
ff
er
ed
Theflagpreventsthefilefrombeingbuffered.
Theopen()methodreturnstrueorfalse,dependingonwhetherthefile
wasopenedornot.Thecurrentstatusofthefilecanbecheckedby
callingisOpen()onthefileobject.Oncethefileisopen,itcanberead
fromorwrittento,dependingontheoptionsthatarepassedwhen
thefileisopened.Readingandwritingisdoneusingtheread()and
write()methods.Thesemethodshaveanumberofoverloads,butwe
suggestthatyoufocusonusingthosevariantsthatacceptorreturn
thealreadyfamiliarQByteArrayobjects,becausetheymanagethe
memoryautomatically.Ifyouareworkingwithplaintext,thena
usefuloverloadforwriteistheonethatacceptsthetextdirectlyas
input.Justrememberthatthetexthastobenullterminated.When
readingfromafile,Qtoffersanumberofothermethodsthatmight
comeinhandyinsomesituations.OneofthesemethodsisreadLine(),
whichtriestoreadfromthefileuntilitencountersanewline
character.IfyouuseitalongwiththeatEnd()methodthattellsyou
whetheryouhavereachedtheendofthefile,youcanrealizethe
line-by-linereadingofatextfile:
QStringListlines;
while(!file.atEnd()){
QByteArrayline=file.readLine();
lines.append(QString::fromUtf8(line));
}
AnotherusefulmethodisreadAll(),whichsimplyreturnsthefile
content,startingfromthecurrentpositionofthefilepointeruntil
theendofthefile.
Youhavetoremember,though,thatwhenusingthesehelper
methods,youshouldbereallycarefulifyoudon'tknowhowmuch
datathefilecontains.Itmighthappenthatwhenreadinglineby
lineortryingtoreadthewholefileintomemoryinonestep,you
exhausttheamountofmemorythatisavailableforyourprocess.If
youonlyintendtoworkwithsmallfilesthatfitintomemory,you
cancheckthesizeofthefilebycallingsize()ontheQFileinstanceand
abortifthefileistoolarge.Ifyouneedtohandlearbitraryfiles,
however,youshouldprocessthefile'sdatainsteps,readingonlya
smallportionofbytesatatime.Thismakesthecodemorecomplex
butallowsustomanagetheavailableresourcesbetter.
Ifyourequireconstantaccesstothefile,youcanusethemap()and
unmap()callsthataddandremovemappingsofthepartsofafiletoa
memoryaddressthatyoucanthenuselikearegulararrayofbytes:
QFilef("myfile");
if(!f.open(QFile::ReadWrite)){
return;
}
uchar*addr=f.map(0,f.size());
if(!addr){
return;
}
f.close();
doSomeComplexOperationOn(addr);
ThemappingwillautomaticallyberemovedwhentheQFileobjectis
destroyed.
Devices
QFileisreallyadescendantclassofQIODevice("input/outputdevice"),
whichisaQtinterfaceusedtoabstractentitiesrelatedtoreading
andwritingofblocksofdata.Therearetwotypesofdevices:
sequentialandrandomaccessdevices.QFilebelongstothelatter
group;ithastheconceptsofstart,end,size,andcurrentposition
thatcanbechangedbytheuserwiththeseek()method.Sequential
devices,suchassocketsandpipes,representstreamsofdata—there
isnowaytorewindthestreamorcheckitssize;youcanonlykeep
readingthedatasequentially—piecebypiece,andyoucancheck
howfarawayyoucurrentlyarefromtheendofdata.Wewillwork
withsuchdevicesinChapter7,Networking.
AllI/Odevicescanbeopenedandclosed.Theyallimplement
theopen(),read(),andwrite()interfaces.Writingtothedevicequeues
thedataforwriting;whenthedataisactuallywritten,the
bytesWritten()signalisemittedthatcarriestheamountofdatathat
waswrittentothedevice.Ifmoredatabecomesavailableinthe
sequentialdevice,itemitsthereadyRead()signal,whichinformsyou
thatifyoucallreadnow,youcanexpecttoreceivesomedatafrom
thedevice.
Timeforaction–Implementing
adevicetoencryptdata
Let'simplementareallysimpledevicethatencryptsordecryptsthe
datathatisstreamedthroughitusingaverysimplealgorithm—the
Caesarcipher.Whenencrypting,itshiftseachcharacterinthe
plaintextbyanumberofcharactersdefinedbythekey.Itdoesthe
reversewhendecrypting.Thus,ifthekeyis2andtheplaintext
characterisa,theciphertextbecomesc.Decryptingzwiththekey4
willyieldthevaluev.
First,createanewemptyprojectbyselectingtheEmptyqmake
ProjecttemplatefromtheOtherProjectcategory.Next,adda
main.cppfileandanewCaesarCipherDeviceclassderivedfromQIODevice.The
basicinterfaceoftheclasswillacceptanintegerkeyandsetan
underlyingdevicethatservesasthesourceordestinationofdata.
Thisisallsimplecodingthatyoushouldalreadyunderstand,soit
shouldn'tneedanyextraexplanation,asshown:
classCaesarCipherDevice:publicQIODevice
{
Q_OBJECT
Q_PROPERTY(intkeyREADkeyWRITEsetKey)
public:
explicitCaesarCipherDevice(QObject*parent=0)
:QIODevice(parent){
m_key=0;
m_baseDevice=0;
}
voidsetBaseDevice(QIODevice*dev){
m_baseDevice=dev;
}
QIODevice*baseDevice()const{
returnm_baseDevice;
}
voidsetKey(intk){
voidsetKey(intk){
m_key=k;
}
inlineintkey()const{
returnm_key;
}
private:
intm_key;
QIODevice*m_baseDevice;
};
Thenextthingistoensurethatthedevicecannotbeusedifthereis
nodevicetooperateon(thatis,whenm_baseDevice==nullptr).Forthis,
wehavetoreimplementtheQIODevice::open()methodandreturnfalse
whenwewanttopreventoperatingonourdevice:
boolCaesarCipherDevice::open(OpenModemode){
if(!m_baseDevice){
returnfalse;
}
if(!m_baseDevice->isOpen()){
returnfalse;
}
if(m_baseDevice->openMode()!=mode){
returnfalse;
}
returnQIODevice::open(mode);
}
Themethodacceptsthemodethattheuserwantstoopenthe
devicewith.Weperformanadditionalchecktoverifythatthebase
devicewasopenedinthesamemodebeforecallingthebaseclass
implementationthatwillmarkthedeviceasopen.
It'sagoodideatocallQIODevice::setErrorStringtolettheuserknowaboutanerror.
Additionally,youcanuseqWarning("message")toprintawarningtotheconsolewhenan
erroroccurs.
Tohaveafullyfunctionaldevice,westillneedtoimplementthetwo
protectedpurevirtualmethods,whichdotheactualreadingand
writing.ThesemethodsarecalledbyQtfromothermethodsofthe
classwhenneeded.Let'sstartwithwriteData(),whichacceptsa
pointertoabuffercontainingthedataandsizeequaltothatofa
buffer:
qint64CaesarCipherDevice::writeData(constchar*data,qint64len){
QByteArraybyteArray;
byteArray.resize(len);
for(inti=0;i<len;++i){
byteArray[i]=data[i]+m_key;
}
intwritten=m_baseDevice->write(byteArray);
emitbytesWritten(written);
returnwritten;
}
First,wecreatealocalbytearrayandresizeittothelengthofthe
input.Then,weiteratebytesoftheinput,addthevalueofthekeyto
eachbyte(whicheffectivelyperformstheencryption)andputitin
thebytearray.Finally,wetrytowritethebytearraytothe
underlyingdevice.Beforeinformingthecallerabouttheamountof
datathatwasreallywritten,weemitasignalthatcarriesthesame
information.
Thelastmethodweneedtoimplementistheonethatperforms
decryptionbyreadingfromthebasedeviceandaddingthekeyto
eachcellofthedata.ThisisdonebyimplementingreadData(),which
acceptsapointertothebufferthatthemethodneedstowritetoand
thesizeofthebuffer.
ThecodeisquitesimilartothatofwriteData(),exceptthatweare
subtractingthekeyvalueinsteadofaddingit:
qint64CaesarCipherDevice::readData(char*data,qint64maxlen){
QByteArraybaseData=m_baseDevice->read(maxlen);
constintsize=baseData.size();
for(inti=0;i<size;++i){
data[i]=baseData[i]-m_key;
}
returnsize;
}
First,wetrytoreadmaxlenbytesfromtheunderlyingdeviceand
storethedatainabytearray.Notethatthebytearraycancontain
fewerbytesthanmaxlen(forexample,ifwereachedtheendofthe
file)butitcan'tcontainmore.Then,weiteratethearrayandset
subsequentbytesofdatabuffertothedecryptedvalue.Finally,we
returntheamountofdatathatwasreallyread.
Asimplemain()functionthatcantesttheclasslooksasfollows:
intmain(intargc,char**argv){
QByteArrayba="plaintext";
QBufferbuf;
buf.open(QIODevice::WriteOnly);
CaesarCipherDeviceencrypt;
encrypt.setKey(3);
encrypt.setBaseDevice(&buf);
encrypt.open(buf.openMode());
encrypt.write(ba);
qDebug()<<buf.data();
CaesarCipherDevicedecrypt;
decrypt.setKey(3);
decrypt.setBaseDevice(&buf);
buf.open(QIODevice::ReadOnly);
decrypt.open(buf.openMode());
qDebug()<<decrypt.readAll();
return0;
}
WeusetheQBufferclassthatimplementstheQIODeviceAPIandactsas
anadapterforQByteArrayorQString.
Whatjusthappened?
Wecreatedanencryptionobjectandsetitskeyto3.Wealsotoldit
touseaQBufferinstancetostoretheprocessedcontent.After
openingitforwriting,wesentsomedatatoitthatgetsencrypted
andwrittentothebasedevice.Then,wecreatedasimilardevice,
passingthesamebufferagainasthebasedevice,butnow,weopen
thedeviceforreading.Thismeansthatthebasedevicecontains
ciphertext.Afterthis,wereadalldatafromthedevice,whichresults
inreadingdatafromthebuffer,decryptingit,andreturningthe
datasothatitcanbewrittentothedebugconsole.
Haveagohero–AGUIforthe
Caesarcipher
Youcancombinewhatyoualreadyknowbyimplementingafull-
blownGUIapplicationthatisabletoencryptordecryptfilesusing
theCaesarcipherQIODeviceclassthatwejustimplemented.
RememberthatQFileisalsoQIODevice,soyoucanpassitspointer
directlytosetBaseDevice().
Thisisjustastartingpointforyou.TheQIODeviceAPIisquiterich
andcontainsnumerousmethodsthatarevirtual,soyoucan
reimplementtheminsubclasses.
Textstreams
Muchofthedataproducedbycomputersnowadaysisbasedon
text.Youcancreatesuchfilesusingamechanismthatyoualready
know—openingQFiletowrite,convertingalldataintostringsusing
QString::arg(),optionallyencodingstringsusingQTextCodec,and
dumpingtheresultingbytestothefilebycallingwrite.However,Qt
providesanicemechanismthatdoesmostofthisautomaticallyfor
youinawaysimilartohowthestandardC++iostreamclasseswork.
TheQTextStreamclassoperatesonanyQIODeviceAPIinastream-
orientedway.Youcansendtokenstothestreamusingthe<<
operator,wheretheygetconvertedintostrings,separatedby
spaces,encodedusingacodecofyourchoice,andwrittentothe
underlyingdevice.Italsoworkstheotherwayround;usingthe>>
operator,youcanstreamdatafromatextfile,transparently
convertingitfromstringstoappropriatevariabletypes.Ifthe
conversionfails,youcandiscoveritbyinspectingtheresultofthe
status()method—ifyougetReadPastEndorReadCorruptData,itmeansthat
thereadhasfailed.
WhileQIODeviceisthemainclassthatQTextStreamoperateson,itcanalsomanipulate
QStringorQByteArray,whichmakesitusefulforustocomposeorparsestrings.
UsingQTextStreamissimple—youjustpassadevicetoitsconstructor,
andyou'regoodtogo.TheQTextStreamobjectwillreadtoorwrite
fromthatdevice.Bydefault,QTextStreamusestheencodingspecified
bythecurrentlocale,butifitencountersaUTF-16orUTF-32BOM
(byteordermark),itwillswitchtotheencodingspecifiedbythe
BOM.Thestreamacceptsstringsandnumericalvalues:
QFilefile("output.txt");
file.open(QFile::WriteOnly|QFile::Text);
QTextStreamstream(&file);
stream<<"Todayis"<<QDate::currentDate().toString()<<endl;
QTimet=QTime::currentTime();
stream<<"Currenttimeis"<<t.hour()<<"hand"
<<t.minute()<<"m."<<endl;
Apartfromdirectingcontentintothestream,thestreamcanaccept
anumberofmanipulators,suchasendl,whichhaveadirector
indirectinfluenceonhowthestreambehaves.Forinstance,youcan
tellthestreamtodisplayanumberasdecimalandanotheras
hexadecimalwithuppercasedigitsusingthefollowingcode
(highlightedinthecodeareallmanipulators):
for(inti=0;i<10;++i){
intnum=qrand()%100000;//randomnumberbetween0and99999
stream<<dec<<num
<<showbase<<hex<<uppercasedigits<<num<<endl;
}
ThisisnottheendofthecapabilitiesofQTextStream.Italsoallowsus
todisplaydatainatabularmannerbydefiningcolumnwidthsand
alignments.Consideragameplayerrecorddefinedbythefollowing
structure:
structPlayer{
QStringname;
qint64experience;
QPointposition;
chardirection;
};
Supposeyouhaveasetofrecordsforplayersstoredina
QVector<Player>playersvariable.Let'sdumpsuchinformationintoafile
inatabularmanner:
QFilefile("players.txt");
file.open(QFile::WriteOnly|QFile::Text);
QTextStreamstream(&file);
stream<<center;
stream<<qSetFieldWidth(16)<<"Player"<<qSetFieldWidth(0)<<"";
stream<<qSetFieldWidth(10)<<"Experience"<<qSetFieldWidth(0)<<"
";
stream<<qSetFieldWidth(13)<<"Position"<<qSetFieldWidth(0)<<"
";
stream<<"Direction"<<endl;
for(constPlayer&player:players){
stream<<left<<qSetFieldWidth(16)<<player.name
<<qSetFieldWidth(0)<<"";
stream<<right<<qSetFieldWidth(10)<<player.experience
<<qSetFieldWidth(0)<<"";
stream<<right<<qSetFieldWidth(6)<<player.position.x()
<<qSetFieldWidth(0)<<"";
stream<<qSetFieldWidth(6)<<player.position.y()
<<qSetFieldWidth(0)<<"";
stream<<center<<qSetFieldWidth(10);
switch(player.direction){
case'n':stream<<"north";break;
case's':stream<<"south";break;
case'e':stream<<"east";break;
case'w':stream<<"west";break;
default:stream<<"unknown";break;
}
stream<<qSetFieldWidth(0)<<endl;
}
Theprogramcreatesafilethatshouldlooklikethis:
PlayerExperiencePositionDirection
Gondael4678310-5north
Olrael123648-5103east
Nazaal9937264148634south
OnelastthingaboutQTextStreamisthatitcanoperateonstandardC
filestructures,whichmakesitpossibleforustouseQTextStreamto,for
example,writetostdoutorreadfromstdin,asshowninthefollowing
code:
QTextStreamstdoutStream(stdout);
stdoutStream<<"Thistextgoestostandardoutput."<<endl;
Binarystreams
Morethanoften,wehavetostoreobjectdatainadevice-
independentwaysothatitcanberestoredlater,possiblyona
differentmachinewithadifferentdatalayoutandsoon.In
computerscience,thisiscalledserialization.Qtprovidesseveral
serializationmechanismsandnowwewilltakeabrieflookatsome
ofthem.
IfyoulookatQTextStreamfromadistance,youwillnotethatwhatit
reallydoesisserializeanddeserializedatatoatextformat.Itsclose
cousinistheQDataStreamclassthathandlesserializationand
deserializationofarbitrarydatatoabinaryformat.Itusesacustom
dataformattostoreandretrievedatafromQIODeviceinaplatform-
independentway.Itstoresenoughdatasothatastreamwrittenon
oneplatformcanbesuccessfullyreadonadifferentplatform.
QDataStreamisusedinasimilarfashionasQTextStream—the<<and>>
operatorsareusedtoredirectdataintooroutofthestream.The
classsupportsmostofthebuilt-inQtdatatypessothatyoucan
operateonclassessuchasQColor,QPoint,orQStringListdirectly:
QFilefile("outfile.dat");
file.open(QFile::WriteOnly|QFile::Truncate);
QDataStreamstream(&file);
doubledbl=3.14159265359;
QColorcolor=Qt::red;
QPointpoint(10,-4);
QStringListstringList{"foo","bar"};
stream<<dbl<<color<<point<<stringList;
Ifyouwanttoserializecustomdatatypes,youcanteachQDataStreamto
dothatbyimplementingproperredirectionoperators.
Timeforaction–Serialization
ofacustomstructure
Let'sperformanothersmallexercisebyimplementingfunctions
thatarerequiredtouseQDataStreamtoserializethesamesimple
structurethatcontainstheplayerinformationthatweusedfortext
streaming:
structPlayer{
QStringname;
qint64experience;
QPointposition;
chardirection;
};
Forthis,twofunctionsneedtobeimplemented,bothreturninga
QDataStreamreferencethatwastakenearlierasanargumenttothecall.
Apartfromthestreamitself,theserializationoperatoracceptsa
constantreferencetotheclassthatisbeingsaved.Themostsimple
implementationjuststreamseachmemberintothestreamand
returnsthestreamafterward:
QDataStream&operator<<(QDataStream&stream,constPlayer&p){
stream<<p.name;
stream<<p.experience;
stream<<p.position;
stream<<p.direction;
returnstream;
}
Complementarytothis,deserializingisdonebyimplementinga
redirectionoperatorthatacceptsamutablereferencetothe
structurethatisfilledbydatathatisreadfromthestream:
QDataStream&operator>>(QDataStream&stream,Player&p){
stream>>p.name;
stream>>p.experience;
stream>>p.position;
stream>>p.direction;
returnstream;
}
Again,attheend,thestreamitselfisreturned.
NowwecanuseQDataStreamtowriteourobjecttoanyI/Odevice(for
example,afile,abuffer,oranetworksocket):
Playerplayer=/*...*/;
QDataStreamstream(device);
stream<<player;
Readingtheobjectbackisjustassimple:
Playerplayer;
QDataStreamstream(device);
stream>>player;
Whatjusthappened?
Weprovidedtwostandalonefunctionsthatdefineredirection
operatorsforthePlayerclasstoandfromaQDataStreaminstance.This
letsyourclassbeserializedanddeserializedusingmechanisms
offeredandusedbyQt.
XMLstreams
XMLhasbecomeoneofthemostpopularstandardsthatisusedto
storehierarchicaldata.Despiteitsverbosityanddifficultytoread
byhumaneye,itisusedinvirtuallyanydomainwheredata
persistencyisrequired,asitisveryeasytoreadbymachines.Qt
providessupportforreadingandwritingXMLdocumentsintwo
modules:
TheQtXmlmoduleprovidesaccessusingtheDocument
ObjectModel(DOM)standardwithclassessuchas
QDomDocument,QDomElement,andothers
TheQtCoremodulecontainsQXmlStreamReaderand
QXmlStreamWriterclassesthatimplementstreamingAPI
OneofthedownsidesofQDomDocumentisthatitrequiresustoloadthe
wholeXMLtreeintothememorybeforeparsingit.Additionally,Qt
Xmlisnotactivelymaintained.Therefore,wewillfocusonthe
streamingapproachprovidedbyQtCore.
Insomesituations,downsidesoftheDOMapproacharecompensatedforbyitseaseofuse
ascomparedtoastreamedapproach,soyoucanconsiderusingitifyoufeelthatyouhave
foundtherighttaskforit.IfyouwanttousetheDOMaccesstoXMLinQt,rememberto
enabletheQtXmlmoduleinyourapplicationsbyaddingaQT+=xmllineintheproject
configurationfile.
Timeforaction–Implementing
anXMLparserforplayerdata
Inthisexercise,wewillcreateaparsertofilldatathatrepresents
playersandtheirinventoryinanRPGgame.First,let'screatethe
typesthatwillholdthedata:
classInventoryItem{
Q_GADGET
public:
enumclassType{
Weapon,
Armor,
Gem,
Book,
Other
};
Q_ENUM(Type)
Typetype;
QStringsubType;
intdurability;
staticTypetypeByName(constQStringRef&r);
};
classPlayer{
public:
QStringname;
QStringpassword;
intexperience;
inthitPoints;
QVector<InventoryItem>inventory;
QStringlocation;
QPointposition;
};
structPlayerInfo{
QVector<Player>players;
QVector<Player>players;
};
Whatjusthappened?
WewanttousetheQ_ENUMmacroonourenum,becauseitwillallow
ustoeasilyconvertenumvaluestostringsandback,whichisvery
usefulforserialization.SinceInventoryItemisnotaQObject,weneedto
addaQ_GADGETmacrotothebeginningoftheclassdeclarationto
maketheQ_ENUMmacrowork.ThinkofQ_GADGETasofalightweight
variationofQ_OBJECTthatenablessomeofitsfeaturesbutnotothers.
ThetypeByName()methodwillreceiveastringandreturnthe
correspondingenumvariant.Wecanimplementthismethodas
follows:
InventoryItem::TypeInventoryItem::typeByName(constQStringRef&r){
QMetaEnummetaEnum=QMetaEnum::fromType<InventoryItem::Type>();
QByteArraylatin1=r.toLatin1();
intresult=metaEnum.keyToValue(latin1.constData());
returnstatic_cast<InventoryItem::Type>(result);
}
Theimplementationmaylookcomplicated,butit'smuchlesserror-
pronethanmanuallywritingabunchofifstatementstochoosethe
correctreturnvaluemanually.First,weusetheQMetaEnum::fromType<T>()
templatemethodtogettheQMetaEnumobjectcorrespondingtoourenum.
ThekeyToValue()methodofthisobjectperformstheconversionthat
weneed,butitneedstobeaccompaniedwithafewconversions.
YoucannotethatweareusingaclasscalledQStringRef.Itrepresents
astringreference—asubstringinanexistingstring—andis
implementedinawaythatavoidsexpensivestringconstruction;
therefore,itisveryfast.Thesimilarstd::string_viewtypewasaddedto
thestandardlibraryinC++17.Weuseitastheargumenttype
becauseQXmlStreamReaderwillprovidestringsinthisformat.
However,thekeyToValue()methodexpectsaconstchar*argument,so
weusethetoLatin1()methodtoconvertourstringtoQByteArray,and
thenuseconstData()togettheconstchar*pointertoitsbuffer.Finally,
weusestatic_casttoconverttheresultfrominttoourenumtype.
SavethefollowingXMLdocumentsomewhere.Wewilluseittotest
whethertheparsercanreadit:
<PlayerInfo>
<Playerhp="40"exp="23456">
<Name>Gandalf</Name>
<Password>mithrandir</Password>
<Inventory>
<InvItemtype="Weapon"durability="3">
<SubType>Longsword</SubType>
</InvItem>
<InvItemtype="Armor"durability="10">
<SubType>Chainmail</SubType>
</InvItem>
</Inventory>
<Locationname="room1">
<Positionx="1"y="0"/>
</Location>
</Player>
</PlayerInfo>
Let'screateaclasscalledPlayerInfoReaderthatwillwrapQXmlStreamReader
andexposeaparserinterfaceforthePlayerInfoinstances:
classPlayerInfoReader{
public:
PlayerInfoReader(QIODevice*device);
PlayerInforead();
private:
QXmlStreamReaderreader;
};
TheclassconstructoracceptsaQIODevicepointerthatthereaderwill
usetoretrievedataasitneedsit.Theconstructoristrivial,asit
simplypassesthedevicetothereaderobject:
PlayerInfoReader(QIODevice*device){
reader.setDevice(device);
}
Beforewegointoparsing,let'spreparesomecodetohelpuswith
theprocess.First,let'saddanenumerationtypetotheclassthat
willlistallthepossibletokens—tagnamesthatwewanttohandlein
theparser:
enumclassToken{
Invalid=-1,
PlayerInfo,//roottag
Player,//inPlayerInfo
Name,Password,Inventory,Location,//inPlayer
Position,//inLocation
InvItem//inInventory
};
Then,justlikewedidintheInventoryItemclass,weusetheQ_GADGETand
Q_ENUMmacrosandimplementthePlayerInfoReader::tokenByName()
conveniencemethod.
Now,let'simplementtheentrypointoftheparsingprocess:
PlayerInfoPlayerInfoReader::read(){
if(!reader.readNextStartElement()){
returnPlayerInfo();
}
if(tokenByName(reader.name())!=Token::PlayerInfo){
returnPlayerInfo();
}
PlayerInfoinfo;
while(reader.readNextStartElement()){
if(tokenByName(reader.name())==Token::Player){
Playerp=readPlayer();
info.players.append(p);
}else{
reader.skipCurrentElement();
}
}
returninfo;
}
First,wecallreadNextStartElement()onthereadertomakeitfindthe
startingtagofthefirstelement,andifitisfound,wecheckwhether
theroottagofthedocumentiswhatweexpectittobe.Ifnot,we
returnadefault-constructedPlayerInfo,indicatingthatnodatais
available.
Next,wecreateaPlayerInfovariable.Weiterateallthestartingsub-
elementsinthecurrenttag(PlayerInfo).Foreachofthem,wecheck
whetheritisaPlayertagandcallreadPlayer()todescendintothelevel
ofparsingdataforasingleplayer.Otherwise,wecall
skipCurrentElement(),whichfast-forwardsthestreamuntilamatching
endingelementisencountered.
Theothermethodsinthisclasswillusuallyfollowthesamepattern.
Eachparsingmethoditeratesallthestartingelements,handling
thoseitknowsandignoringallothers.Suchanapproachletsus
maintainforwardcompatibility,sincealltagsintroducedinthe
newerversionsofthedocumentaresilentlyskippedbyanolder
parser.
ThestructureofreadPlayer()issimilar;however,itismore
complicated,aswealsowanttoreaddatafromtheattributesofthe
Playertagitself.Let'stakealookatthefunctionpiecebypiece.First,
wegetthelistofattributesassociatedwiththeopeningtagandask
forvaluesofthetwoattributesthatweareinterestedin:
Playerp;
constQXmlStreamAttributes&playerAttrs=reader.attributes();
p.hitPoints=playerAttrs.value("hp").toString().toInt();
p.experience=playerAttrs.value("exp").toString().toInt();
Afterthis,weloopallchildtagsandfillthePlayerstructurebasedon
thetagnames.Byconvertingtagnamestotokens,wecanusea
switchstatementtoneatlystructurethecodeinordertoextract
informationfromdifferenttagtypes,asinthefollowingcode:
while(reader.readNextStartElement()){
Tokent=tokenByName(reader.name());
switch(t){
caseToken::Name:
p.name=reader.readElementText();
break;
caseToken::Password:
p.password=reader.readElementText();
break;
caseToken::Inventory:
p.inventory=readInventory();
break;
//...
}
}
Ifweareinterestedinthetextualcontentofthetag,wecanuse
readElementText()toextractit.Thismethodreadsuntilitencounters
theclosingtagandreturnsthetextcontainedwithinit.Forthe
Inventorytag,wecallthededicatedreadInventory()method.
FortheLocationtag,thecodeismorecomplexthanearlieraswe
againdescendintoreadingchildtags,extractingtherequired
informationandskippingallunknowntags:
caseToken::Location:
p.location=reader.attributes().value("name").toString();
while(reader.readNextStartElement()){
if(tokenByName(reader.name())==Token::Position){
constQXmlStreamAttributes&attrs=reader.attributes();
p.position.setX(attrs.value("x").toString().toInt());
p.position.setY(attrs.value("y").toString().toInt());
reader.skipCurrentElement();
}else{
reader.skipCurrentElement();
}
}
break;
Next,weagainskipthetagsthatdidn'tmatchanyknowntokens.At
theendofreadPlayer(),wesimplyreturnthepopulatedPlayervalue.
Thelastmethodissimilarinstructuretothepreviousone—iterate
allthetags,skipeverythingthatwedon'twanttohandle
(everythingthatisnotaninventoryitem),filltheinventoryitem
datastructure,andappendtheitemtothelistofalreadyparsed
items,asfollows:
QVector<InventoryItem>PlayerInfoReader::readInventory(){
QVector<InventoryItem>inventory;
while(reader.readNextStartElement()){
if(tokenByName(reader.name())!=Token::InvItem){
reader.skipCurrentElement();
continue;
}
InventoryItemitem;
constQXmlStreamAttributes&attrs=reader.attributes();
item.durability=
attrs.value("durability").toString().toInt();
item.type=InventoryItem::typeByName(attrs.value("type"));
while(reader.readNextStartElement()){
if(reader.name()=="SubType"){
item.subType=reader.readElementText();
}
else{
reader.skipCurrentElement();
}
}
inventory<<item;
}
returninventory;
}
Inmain()ofyourproject,writesomecodethatwillcheckwhetherthe
parserworkscorrectly.YoucanusetheqDebug()statementstooutput
thesizesoflistsandcontentsofvariables.Takealookatthe
followingcodeforanexample:
QFilefile(filePath);
file.open(QFile::ReadOnly|QFile::Text);
PlayerInfoReaderreader(&file);
PlayerInfoplayerInfo=reader.read();
if(!playerInfo.players.isEmpty()){
qDebug()<<"Count:"<<playerInfo.players.count();
qDebug()<<"Sizeofinventory:"<<
qDebug()<<"Sizeofinventory:"<<
playerInfo.players.first().inventory.size();
qDebug()<<"Inventoryitem:"
<<playerInfo.players.first().inventory[0].type
<<playerInfo.players.first().inventory[0].subType;
qDebug()<<"Room:"<<playerInfo.players.first().location
<<playerInfo.players.first().position;
}
Whatjusthappened?
Thecodeyoujustwroteimplementsafulltop-downparserofthe
XMLdata.First,thedatagoesthroughatokenizer,whichreturns
identifiersthataremucheasiertohandlethanstrings.Then,each
methodcaneasilycheckwhetherthetokenitreceivesisan
acceptableinputforthecurrentparsingstage.Basedonthechild
token,thenextparsingfunctionisdeterminedandtheparser
descendstoalowerleveluntilthereisnowheretodescendto.Then,
theflowgoesbackuponelevelandprocessesthenextchild.If,at
anypoint,anunknowntagisfound,itgetsignored.Thisapproach
supportsasituationwhenanewversionofsoftwareintroducesnew
tagstothefileformatspecification,butanoldversionofsoftware
canstillreadthefilebyskippingallthetagsthatitdoesn't
understand.
Haveagohero–AnXML
serializerforplayerdata
NowthatyouknowhowtoparseXMLdata,youcancreatethe
complementarypart—amodulethatwillserializePlayerInfo
structuresintoXMLdocumentsusingQXmlStreamWriter.Usemethods
suchaswriteStartDocument(),writeStartElement(),writeCharacters(),and
writeEndElement()forthis.Verifythatthedocumentssavedwithyour
codecanbeparsedwithwhatweimplementedtogether.
QVariant
QVariantisaclassthatcanholdvaluesofmultipletypes:
QVariantintValue=1;
intx=intValue.toInt();
QVariantstringValue="ok";
QStringy=stringValue.toString();
WhenyouassignavaluetoaQVariantobject,thatvalueisstored
insidealongwiththetypeinformation.Youcanuseitstype()method
tofindoutwhichtypeofvalueitholds.Thedefaultconstructor
ofQVariantcreatesaninvalidvaluethatyoucandetectusing
theisValid()method.
QVariantsupportsagreatamountoftypes,includingQtvaluetypes
suchasQDateTime,QColor,andQPoint.Youcanalsoregisteryourown
typestostoretheminQVariant.Oneofthemostpowerfulfeatures
ofQVariantistheabilitytostoreacollectionorahierarchyofvalues.
YoucanusetheQVariantListtype(whichisatypedefforQList<QVariant>)to
createalistofQVariantobjects,andyoucanactuallyputthewholelist
intoasingleQVariantobject!You'llbeabletoretrievethelistand
examineindividualvalues:
QVariantlistValue=QVariantList{1,"ok"};
for(QVariantitem:listValue.toList()){
qDebug()<<item.toInt()<<item.toString();
}
Similarly,youcanuseQVariantMaporQVariantHashtocreateakey-value
collectionwithQStringkeysandQVariantvalues.Needlesstosay,you
canstoresuchacollectioninasingleQVariantaswell.Thisallowsyou
toconstructahierarchywithunlimiteddepthandarbitrary
structure.
Asyoucansee,QVariantisaprettypowerfulclass,buthowcanweuse
itforserializing?Forastart,QVariantissupportedbyQDataStream,so
youcanusethebinaryserializationdescribedearliertoserialize
andrestoreanyQVariantvalueyoucanconstruct.Forexample,
insteadofputtingeachfieldofyourstructureintoQDataStream,youcan
putthemintoaQVariantMapandthenputitintothestream:
Playerplayer;
QVariantMapmap;
map["name"]=player.name;
map["experience"]=player.experience;
//...
stream<<map;
Loadingthedataisalsostraightforward:
QVariantMapmap;
stream>>map;
Playerplayer;
player.name=map["name"].toString();
player.experience=map["experience"].toLongLong();
Thisapproachallowsyoutostorearbitrarydatainanarbitrary
location.However,youcanalsouseQVariantalongwithQSettingsto
convenientlystorethedatainanappropriatelocation.
QSettings
Whilenotstrictlyaserializationissue,theaspectofstoring
applicationsettingsiscloselyrelatedtothedescribedsubject.AQt
solutionforthisistheQSettingsclass.Bydefault,itusesdifferent
backendsondifferentplatforms,suchassystemregistryon
WindowsorINIfilesonLinux.ThebasicuseofQSettingsisveryeasy
—youjustneedtocreatetheobjectandusesetValue()andvalue()to
storeandloaddatafromit:
QSettingssettings;
settings.setValue("level",4);
settings.setValue("playerName","Player1");
//...
intlevel=settings.value("level").toInt();
TheonlythingyouneedtorememberisthatitoperatesonQVariant,
sothereturnvalueneedstobeconvertedtothepropertypeif
needed,liketoInt()intheprecedingcode.Acalltovalue()cantakean
additionalargumentthatcontainsthevaluetobereturnedifthe
requestedkeyisnotpresentinthemap.Thisallowsyoutohandle
defaultvalues,forexample,inasituationwhentheapplicationis
firststartedandthesettingsarenotsavedyet:
intlevel=settings.value("level",1).toInt();
Ifyoudon'tspecifythedefaultvalue,aninvalidQVariantwillbe
returnedwhennothingisstored,andyoucancheckforthatusing
theisValid()method.
Inorderforthedefaultsettingslocationtobecorrect,youneedto
settheorganizationnameandtheapplicationname.They
determinewhereexactlyQSettingsstoredatabydefaultandensure
thatthestoreddatawillnotconflictwithanotherapplication.This
istypicallydoneatthebeginningofyourmain()function:
intmain(intargc,char*argv[]){
QApplicationapp(argc,argv);
QCoreApplication::setOrganizationName("Packt");
QCoreApplication::setApplicationName("GameProgrammingusingQt");
//...
}
Settingshierarchy
Thesimplestscenarioassumesthatsettingsare"flat",inthatall
keysaredefinedonthesamelevel.However,thisdoesnothaveto
bethecase—correlatedsettingscanbeputintonamedgroups.To
operateonagroup,youcanusethebeginGroup()andendGroup()calls:
settings.beginGroup("server");
QStringserverIP=settings.value("host").toString();
intport=settings.value("port").toInt();
settings.endGroup();
Whenusingthissyntax,youhavetoremembertoendthegroup
afteryouaredonewithit.Analternativewaytodothesamethingis
topassthegroupnamedirectlytoinvocationofvalue(),using/to
separateitfromthevaluename:
QStringserverIP=settings.value("server/host").toString();
intport=settings.value("server/port").toInt();
YoucancreatemultiplenestedgroupsbycallingbeginGroup()multiple
times(or,equivalently,writingmultipleslashesinthevaluename).
Thereisanotherwaytointroduceanon-flatstructuretoQSettings.It
canhandlecompositeQVariantvalues—QVariantMapandQVariantList.You
cansimplyconvertyourdatatoaQVariant,muchlikeweconvertedit
toaQJsonValueearlier:
QVariantinventoryItemToVariant(constInventoryItem&item){
QVariantMapmap;
map["type"]=InventoryItem::typeToName(item.type);
map["subtype"]=item.subType;
map["durability"]=item.durability;
returnmap;
returnmap;
}
ThisQVariantvaluecanbepassedtoQSettings::setValue().Ofcourse,you
willneedtoimplementtheinverseoperationaswell.Morethan
that,nothingstopsyoufromconvertingyourdatatoJSONand
savingittoQSettingsasaQByteArray.However,theseapproachesmay
beslowerthanproperserialization,andtheresultingsettingsfile
willbehardtoeditmanually.
VariousQtclasseshavemethodsthataremeanttobeused
withQSettingstoeasilysaveasetofproperties.For
example,QWidget::saveGeometry()andQWidget::restoreGeometry()helpers
allowyoutosavethewindow'spositionandsizetoQSettings:
settings.setValue("myWidget/geometry",myWidget->saveGeometry());
//...
myWidget->restoreGeometry(
settings.value("myWidget/geometry").toByteArray());
Similarly,multiplewidgetclasses
havesaveState()andrestoreState()methodstosaveinformationabout
thewidget'sstate:
QMainWindowcansavepositionsoftoolbarsanddockwidgets
QSplittercansavepositionsofitshandles
QHeaderViewcansavesizesofthetable'srowsorcolumns
QFileDialogcansavethedialog'slayout,history,andcurrent
directory
Thesemethodsareagreatwaytopreserveallchangestheuserhas
madeinyourapplication'sinterface.
Customizingthesettings
locationandformat
TheconstructoroftheQSettingsclasshasanumberofoverloadsthat
allowyoutochangethelocationwherethedatawillbestoredbya
particularQSettingsobject,insteadofusingthedefaultlocation.First,
youcanoverridetheorganizationnameandtheapplicationname:
QSettingssettings("Packt","GameProgrammingusingQt");
Next,youcanusethesystem-widestoragelocationbypassing
QSettings::SystemScopeasthescopeargument:
QSettingssettings(QSettings::SystemScope,
"Packt","GameProgrammingusingQt");
Inthiscase,QSettingswilltrytoreadthesettingsforallusersand
thenfallbacktotheuser-specificlocation.Notethatasystem-wide
locationmaynotbewritable,sousingsetValue()onitwon'thavethe
desiredeffect.
Youcanalsooptoutofthepreferredformatdetectionusingthe
QSettings::setDefaultFormat()function.Forexample,usethefollowing
codeonWindowstodisableusingtheregistry:
QSettings::setDefaultFormat(QSettings::IniFormat);
Finally,thereisonemoreoptionavailablefortotalcontrolofwhere
thesettingsdataresides—telltheconstructordirectlywherethe
datashouldbelocated:
QSettingssettings(
QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)+
"/myapp.ini",
QSettings::IniFormat
);
IfyoupassQSettings::NativeFormattothisconstructor,themeaningof
thepathwilldependontheplatform.Forexample,itwillbe
interpretedasaregistrypathonWindows.
SinceyoucanuseQSettingstoreadandwritetoanarbitraryINIfile,it'saconvenientand
easywaytoimplementserializationofanobjecttotheINIformat,whichissuitablein
simplecases.
QSettingsalsoallowsyoutoregisteryourownformatssothatyoucan
controlthewayyoursettingsarestored,forexample,bystoring
themusingXMLorbyaddingon-the-flyencryption.Thisisdone
usingQSettings::registerFormat(),whereyouneedtopassthefile
extensionandtwopointerstofunctionsthatperformreadingand
writingofthesettings,respectively,asfollows:
boolreadCCFile(QIODevice&device,QSettings::SettingsMap&map){
CeasarCipherDeviceccDevice;
ccDevice.setBaseDevice(&device);
//...
returntrue;
}
boolwriteCCFile(QIODevice&device,constQSettings::SettingsMap&map)
{
//...
}
constQSettings::FormatCCFormat=QSettings::registerFormat(
"ccph",readCCFile,writeCCFile);
JSONfiles
JSONstandsfor"JavaScriptObjectNotation",whichisapopular
lightweighttextualformatthatisusedtostoreobject-orienteddata
inahuman-readableform.ItcomesfromJavaScriptwhereitisthe
nativeformatusedtostoreobjectinformation;however,itis
commonlyusedacrossmanyprogramminglanguagesanda
popularformatforwebdataexchange.QtCoresupportsJSON
format,aswe'llseeinthefollowingcode.AsimpleJSON-formatted
definitionlooksasfollows:
{
"name":"Joe",
"age":14,
"inventory":[
{"type":"gold","amount":"144000"},
{"type":"short_sword","material":"iron"}
]
}
JSONobjectscancontainvaluesofthefollowingtypes:
T
y
p
e
Description
b
o
o
l
Abooleanvalue(trueorfalse).
d
o
u
u
b
le
Anumbervalue(forexample,42.1).
st
ri
n
g
Aquotedstring(forexample,"Qt").
a
rr
a
y
Acollectionofvaluesofanytypeenclosedinsquarebrackets
(forexample,[42.1,"Qt"]).
o
b
je
ct
Asetofkey-valuepairsenclosedinbraces.Keysarestrings,
andvaluescanbeofanytype(forexample,{"key1":42.1,
"key2":[42.1,"Qt"]}).
n
u
ll
Aspecialvalue(null)indicatinglackofdata.
AproperJSONdocumentmusthaveeitheranarrayoranobject
atthetoplevel.Intheprecedingexample,wehadanobject
containingthreeproperties:name,age,andinventory.Thefirsttwo
propertiesaresimplevalues,andthelastpropertyisanarraythat
containstwoobjectswithtwopropertieseach.
QtcancreateandreadJSONdescriptionsusingtheQJsonDocument
class.AdocumentcanbecreatedfromtheUTF-8-encodedtext
usingtheQJsonDocument::fromJson()staticmethod,andcanlaterbe
storedinatextualformagainusingtoJson().OnceaJSONdocument
iscreated,youcancheckwhetheritrepresentsanobjectoranarray
usingoneoftheisArray()andisObject()calls.Then,thedocumentcan
betransformedintoQJsonArrayorQJsonObjectusingthearray()orobject()
methods.
SincethestructureofJSONcloselyresemblesthatofQVariant(whichcanalsoholdkey-
valuepairsusingQVariantMapandarraysusingQVariantList),conversionmethods
QJsonDocument::fromVariant()andQJsonDocument::toVariant()alsoexist.
QJsonObjectisaniterabletypethatcanbequeriedforalistofkeys
(usingkeys())oraskedforavalueofaspecifickey(withavalue()
methodoroperator[]).ValuesarerepresentedusingtheQJsonValue
class,whichcanstoreanyofthevaluetypeslistedearlier.New
propertiescanbeaddedtotheobjectusingtheinsert()methodthat
takesakeyasastring,andavaluecanbeaddedasQJsonValue.The
existingpropertiescanberemovedusingremove().
QJsonArrayisalsoaniterabletypethatcontainsaclassiclistAPI;it
containsmethodssuchasappend(),insert(),removeAt(),at(),andsize()to
manipulateentriesinthearray,againworkingonQJsonValueasthe
itemtype.
Timeforaction–Theplayer
dataJSONserializer
OurnextexerciseistocreateaserializerofthesamePlayerInfo
structureasweusedfortheXMLexercise,butthistimethe
destinationdataformatwillbeJSON.
StartbycreatingaPlayerInfoJsonclassandgiveitaninterfacesimilar
totheoneshowninthefollowingcode:
classPlayerInfoJson{
public:
PlayerInfoJson(){}
QByteArrayplayerInfoToJson(constPlayerInfo&pinfo);
};
AllthatisreallyrequiredistoimplementtheplayerInfoToJsonmethod.
Generally,weneedtoconvertourPlayerInfodatatoaQJsonArrayand
thenuseQJsonDocumenttoencodeitasJSON:
QByteArrayPlayerInfoJson::playerInfoToJson(constPlayerInfo&pinfo)
{
QJsonDocumentdoc(toJson(pinfo));
returndoc.toJson();
}
Now,let'sstartimplementingthetoJson()method:
QJsonArrayPlayerInfoJson::toJson(constPlayerInfo&pinfo){
QJsonArrayarray;
for(constPlayer&p:pinfo.players){
array<<toJson(p);
}
returnarray;
returnarray;
}
Sincethestructureisreallyalistofplayers,wecaniterateoverit,
converteachplayertoaQJsonValue,andappendtheresulttoQJsonArray.
Havingthisfunctionready,wecandescendalevelandimplement
anoverloadfortoJson()thattakesaPlayerobject:
QJsonValuePlayerInfoJson::toJson(constPlayer&player){
QJsonObjectobject;
object["name"]=player.name;
object["password"]=player.password;
object["experience"]=player.experience;
object["hitpoints"]=player.hitPoints;
object["location"]=player.location;
object["position"]=QJsonObject({{"x",player.position.x()},
{"y",player.position.y()}
});
object["inventory"]=toJson(player.inventory);
returnobject;
}
Thistime,weareusingQJsonObjectasourbasetype,sincewewantto
associatevalueswithkeys.Foreachkey,weusetheindexoperator
toaddentriestotheobject.ThepositionkeyholdsaQPointvalue,
whichisnotavalidJSONvalue,soweconvertthepointtoa
QJsonObjectwithtwokeys(xandy)usingtheC++11initializerlist.The
situationisdifferentwiththeinventory—again,wehavetowritean
overloadfortoJsonthatwillperformtheconversion:
QJsonValuePlayerInfoJson::toJson(constQVector<InventoryItem>&items)
{
QJsonArrayarray;
for(constInventoryItem&item:items){
array<<toJson(item);
}
returnarray;
}
ThecodeisalmostidenticaltotheonehandlingPlayerInfoobjects,so
let'sfocusonthelastoverloadoftoVariant—theonethatacceptsItem
instances:
QJsonValuePlayerInfoJson::toJson(constInventoryItem&item){
QJsonObjectobject;
object["type"]=InventoryItem::typeToName(item.type);
object["subtype"]=item.subType;
object["durability"]=item.durability;
returnobject;
}
Thereisnotmuchtocommenthere—weaddallkeystotheobject,
convertingtheitemtypetoastring.Forthis,wehavetoaddthe
staticInventoryItem::typeToName()methodthatisthereverseoftypeByName(),
thatis,ittakesaenumvariantandoutputsitsnameasastring:
constchar*InventoryItem::typeToName(InventoryItem::Typevalue)
{
QMetaEnummetaEnum=QMetaEnum::fromType<InventoryItem::Type>();
returnmetaEnum.valueToKey(static_cast<int>(value));
}
ThisisprettymuchawrapperovertheQMetaEnum::valueToKey()method
thatdoesallthemagicthatwouldn'tbepossiblewithoutQt.
Theserializeriscomplete!Nowyoucanuse
PlayerInfoJson::playerInfoToJson()toconvertPlayerInfointoaQByteArray
containingtheJSON.It'ssuitableforwritingittoafileorsendingit
overthenetwork.However,tomakeitmoreuseful,weneedto
implementthereverseoperation(deserialization).
Timeforaction–Implementing
aJSONparser
Let'sextendthePlayerInfoJSONclassandequipitwitha
playerInfoFromJson()method:
PlayerInfoPlayerInfoJson::playerInfoFromJson(constQByteArray&ba){
QJsonDocumentdoc=QJsonDocument::fromJson(ba);
if(!doc.isArray()){
returnPlayerInfo();
}
QJsonArrayarray=doc.array();
PlayerInfopinfo;
for(constQJsonValue&value:array){
pinfo.players<<playerFromJson(value.toObject());
}
returnpinfo;
}
First,wereadthedocumentandcheckwhetheritisvalidandholds
theexpectedarray.Uponfailure,anemptystructureisreturned;
otherwise,weiterateoverthereceivedarrayandconverteachofits
elementstoanobject.Similartotheserializationexample,we
createahelperfunctionforeachcomplexitemofourdatastructure.
Thus,wewriteanewplayerFromJson()methodthatconvertsQJsonObject
toaPlayer,thatis,performsareverseoperationascomparedto
toJson(Player):
PlayerPlayerInfoJson::playerFromJson(constQJsonObject&object){
Playerplayer;
player.name=object["name"].toString();
player.password=object["password"].toString();
player.experience=object["experience"].toDouble();
player.hitPoints=object["hitpoints"].toDouble();
player.location=object["location"].toString();
QJsonObjectpositionObject=object["position"].toObject();
QJsonObjectpositionObject=object["position"].toObject();
player.position=QPoint(positionObject["x"].toInt(),
positionObject["y"].toInt());
player.inventory=
inventoryFromJson(object["inventory"].toArray());
returnplayer;
}
Inthisfunction,weusedoperator[]toextractdatafromQJsonObject,
andthenweuseddifferentfunctionstoconvertthedatatothe
desiredtype.NotethatinordertoconverttoQPoint,wefirst
convertedittoQJsonObjectandthenextractedthevaluesbeforeusing
themtobuildQPoint.Ineachcase,iftheconversionfails,wegeta
defaultvalueforthattype(forexample,anemptystringorazero
number).Toreadtheinventory,weemployanothercustom
method:
QVector<InventoryItem>PlayerInfoJson::inventoryFromJson(
constQJsonArray&array)
{
QVector<InventoryItem>inventory;
for(constQJsonValue&value:array){
inventory<<inventoryItemFromJson(value.toObject());
}
returninventory;
}
WhatremainsistoimplementinventoryItemFromJson():
InventoryItemPlayerInfoJson::inventoryItemFromJson(
constQJsonObject&object)
{
InventoryItemitem;
item.type=InventoryItem::typeByName(object["type"].toString());
item.subType=object["subtype"].toString();
item.durability=object["durability"].toDouble();
returnitem;
}
Unfortunately,ourtypeByName()functionrequiresQStringRef,notQString.
Wecanfixthisbyaddingacoupleofoverloadsandforwarding
themtoasingleimplementation:
InventoryItem::TypeInventoryItem::typeByName(constQStringRef&r){
returntypeByName(r.toLatin1());
}
InventoryItem::TypeInventoryItem::typeByName(constQString&r){
returntypeByName(r.toLatin1());
}
InventoryItem::TypeInventoryItem::typeByName(constQByteArray
&latin1){
QMetaEnummetaEnum=QMetaEnum::fromType<InventoryItem::Type>();
intresult=metaEnum.keyToValue(latin1.constData());
returnstatic_cast<InventoryItem::Type>(result);
}
Whatjusthappened?
Theclassthatwasimplementedcanbeusedforbidirectional
conversionbetweenIteminstancesandaQByteArrayobject,which
containstheobjectdataintheJSONformat.Wedidn'tdoanyerror
checkinghere;instead,wereliedonQt'srulethatanerrorresultsin
asensibledefaultvalue.
Whatifyouwanttoperformerrorchecking?Themoststraightforwardsolutioninthis
caseistouseexceptions,astheywillautomaticallypropagatefromthemultiplenested
callstothecaller'slocation.Ensurethatyoucatchanyexceptionsyouthrow,orthe
applicationwillterminate.AmoreQt-likesolutionistocreateabool*okargumentinall
methods(includinginternalones)andsetthebooleanvaluetofalseincaseofanyerror.
Popquiz
Q1.Whatistheclosestequivalentofstd::stringinQt?
1. QString
2. QByteArray
3. QStringLiteral
Q2.Whichstringsmatchthe\A\d\zregularexpression?
1. Stringsconsistingofdigits
2. Stringsconsistingofasingledigit
3. Thisisnotavalidregularexpression
Q3.Whichofthefollowingcontainertypescanyouusetostorea
listofwidgets?
1. QVector<QWidget>
2. QList<QWidget>
3. QVector<QWidget*>
Q4.Whichclasscanyouusetoconvertatextstringcontaining
JSONtoaQtJSONrepresentation?
1. QJsonValue
2. QJsonObject
3. QJsonDocument
Summary
Inthischapter,youlearnedanumberofcoreQttechnologies,
rangingfromtextmanipulationandcontainerstoaccessingdevices
thatcanbeusedtotransferorstoredata,usinganumberof
populartechnologiessuchasXMLorJSON.Youshouldbeaware
thatwehavebarelyscratchedthesurfaceofwhatQtoffersand
therearemanyotherinterestingclassesyoushouldfamiliarize
yourselfwith,butthisminimumamountofinformationshouldgive
youaheadstartandshowyouthedirectiontofollowwithyour
futureresearch.
Inthenextchapter,wewillgobeyondtheboundariesofyour
computerandexplorewaystousethepowerfulworldofthe
moderninternet.Youwilllearnhowtointeractwiththeexisting
networkservices,checkthecurrentnetworkavailability,and
implementyourownserversandclients.Thisknowledgewillcome
inhandyifyouwanttoimplementmultiplayernetworkedgames.
Networking
Inthischapter,youwillbetaughthowtocommunicatewith
internetserversandwithsocketsingeneral.First,wewilltakea
lookatQNetworkAccessManager,whichmakessendingnetworkrequests
andreceivingrepliesreallyeasy.Buildingonthisbasicknowledge,
wewillthenuseGoogle'sDistanceAPItogetinformationaboutthe
distancebetweentwolocationsandhowlongitwouldtaketoget
fromonetotheother.Thistechnique,andtherespective
knowledge,canalsobeusedtoincludeFacebookorTwitterinyour
applicationviatheirrespectiveAPIs.Then,wewilltakealookat
Qt'sBearerAPI,whichprovidesinformationaboutadevice's
connectivitystate.Inthelastsection,youwilllearnhowtouse
socketstocreateyourownserverandclientsusingTCPorUDPas
thenetworkprotocol.
Themaintopicscoveredinthischapterarethefollowing:
DownloadingfilesusingQNetworkAccessManager
UsingGoogle'sDistanceMatrixAPI
ImplementingaTCPchatserverandclient
UsingUDPsockets
QNetworkAccessManager
Allnetwork-relatedfunctionalityinQtisimplementedintheQt
Networkmodule.Theeasiestwaytoaccessfilesontheinternetisto
usetheQNetworkAccessManagerclass,whichhandlesthecomplete
communicationbetweenyourgameandtheinternet.
SettingupalocalHTTPserver
Inournextexample,wewillbedownloadingafileoverHTTP.If
youdon'thavealocalHTTPserver,youcanjustuseanypublicly
availableHTTPorHTTPSresourcetotestyourcode.However,
whenyoudevelopandtestanetwork-enabledapplication,itis
recommendedthatyouuseaprivate,localnetworkiffeasible.This
way,itispossibletodebugbothendsoftheconnection,anderrors
willnotexposesensitivedata.
Ifyouarenotfamiliarwithsettingupawebserverlocallyonyour
machine,thereare,luckily,anumberofall-in-oneinstallersthat
arefreelyavailable.ThesewillautomaticallyconfigureApache2,
MySQL(orMariaDB),PHP,andmanyotherserversonyour
system.OnWindows,forexample,youcanuseXAMPP
(https://www.apachefriends.org),ortheUniformServer
(http://www.uniformserver.com);onApplecomputers,thereisMAMP
(https://www.mamp.info);andonLinux,youcanopenyourpreferred
packagemanager,searchforapackagecalledApache2orasimilar
one,andinstallit.Alternatively,takealookatyourdistribution's
documentation.
BeforeyouinstallApacheonyourmachine,thinkaboutusinga
virtualmachine,suchasVirtualBox(http://www.virtualbox.org)forthis
task.Thisway,youkeepyourmachineclean,andyoucaneasilytry
differentsettingsforyourtestserver.Withmultiplevirtual
machines,youcaneventesttheinteractionbetweendifferent
instancesofyourgame.IfyouareonUnix,Docker
(http://www.docker.com)mightbeworthtakingalookat.
PreparingaURLfortesting
Ifyou'vesetupalocalHTTPserver,createafilecalledversion.txtin
therootdirectoryoftheinstalledserver.Thisfileshouldcontaina
smallpieceoftextsuchas"Iamafileonlocalhost"orsomething
similar.Asyoumighthaveguessed,areal-lifescenariocouldbeto
checkwhetherthereisanupdatedversionofyourgameor
applicationontheserver.Totestwhethertheserverandthefileare
correctlysetup,startawebbrowserandopen
http://localhost/version.txt.Youshouldthenseethefile'scontent:
.
Ifthisfails,itmaybethecasethatyourserverdoesnotallowyouto
displaytextfiles.Insteadofgettinglostintheserver's
configuration,justrenamethefiletoversion.html.Thisshoulddothe
trick!
Ifyoudon'thaveanHTTPserver,youcanusetheURLofyour
favoritewebsite,butbepreparedtoreceiveHTMLcodeinsteadof
plaintext,asthemajorityofwebsitesuseHTML.Youcanalsouse
thehttps://www.google.com/robots.txtURL,asitrespondswithplaintext.
Timeforaction–Downloading
afile
CreateaQtWidgetsprojectandaddawidgetclass
namedFileDownload.Addabuttonthatwillstartthedownloadanda
plaintexteditthatwilldisplaytheresult.Asalways,youcanlookat
thecodefilesprovidedwiththebookifyouneedanyhelp.
Next,enabletheQtNetworkmodulebyaddingQT+=networktothe
projectfile.Then,createaninstanceofQNetworkAccessManagerinthe
constructorandputitinaprivatefield:
m_network_manager=newQNetworkAccessManager(this);
SinceQNetworkAccessManagerinheritsQObject,ittakesapointertoQObject,
whichisusedasaparent.Thus,youdonothavedeletethemanager
lateron.
Secondly,weconnectthemanager'sfinished()signaltoaslotofour
choice;forexample,inourclass,wehaveaslotcalled
downloadFinished():
connect(m_network_manager,&QNetworkAccessManager::finished,
this,&FileDownload::downloadFinished);
WehavetodothisbecausetheAPIofQNetworkAccessManageris
asynchronous.Thismeansthatnoneofthenetworkrequests,or
thereadorwriteoperations,willblockthecurrentthread.Instead,
whenthedataisavailableoranothernetworkeventoccurs,Qtwill
sendacorrespondingsignalsothatyoucanhandlethedata.
Thirdly,weactuallyrequesttheversion.txtfilefromlocalhostwhen
thebuttonisclicked:
QUrlurl("http://localhost/version.txt");
m_network_manager->get(QNetworkRequest(url));
Withget(),wesendarequesttogetthecontentsofthefilespecified
bytheURL.ThefunctionexpectsaQNetworkRequestobject,which
definesalltheinformationneededtosendarequestoverthe
network.Themaininformationforsucharequestis,naturallythe
URLofthefile.ThisisthereasonQNetworkRequesttakesQUrlasan
argumentinitsconstructor.YoucanalsosettheURLwithsetUrl()to
arequest.Ifyouwishtodefinearequestheader(forexample,a
customuseragent),youcanusesetHeader():
QNetworkRequestrequest;
request.setUrl(QUrl("http://localhost/version.txt"));
request.setHeader(QNetworkRequest::UserAgentHeader,"MyGame");
m_network_manager->get(request);
ThesetHeader()functiontakestwoarguments:thefirstisavalueof
theQNetworkRequest::KnownHeadersenumeration,whichholdsthemost
common(self-explanatory)headers,suchasLastModifiedHeaderor
ContentTypeHeader,andthesecondistheactualvalue.Youcanalsowrite
theheaderusingsetRawHeader():
request.setRawHeader("User-Agent","MyGame");
WhenyouusesetRawHeader(),youhavetowritetheheaderfieldnames
yourself.Besidesthis,itbehaveslikesetHeader().Alistofallthe
availableheadersfortheHTTPprotocolVersion1.1canbefoundin
section14ofRFC2616(https://www.w3.org/Protocols/rfc2616/rfc2616-
sec14.html#sec14).
Gettingbacktoourexample,withtheget()function,werequested
theversion.txtfilefromthelocalhost.Allwehavetodofromnowon
iswaitfortheservertoreply.Assoonastheserver'sreplyis
finished,thedownloadFinished()slotwillbecalledthatwasdefinedby
theprecedingconnectionstatement.ApointertoaQNetworkReply
objectwillbepassedastheargumenttotheslot,andwecanread
thereply'sdataandshowitinm_edit,aninstanceofQPlainTextEdit,with
thefollowing:
voidFileDownload::downloadFinished(QNetworkReply*reply){
constQByteArraycontent=reply->readAll();
m_edit->setPlainText(QString::fromUtf8(content));
reply->deleteLater();
}
SinceQNetworkReplyinheritsQIODevice,therearealsootherpossibilities
toreadthecontentofthereply.Forexample,youcanuseQDataStream
orQTextStreamtoreadandinterpretbinaryortextualdata,
respectively.Here,asthefourthcommand,QIODevice::readAll()isused
togetthefullcontentoftherequestedfileinaQByteArrayobject.This
isverysimilartoreadingfromfiles,whichwasshowninthe
previouschapter.Theresponsibilityforthetransferredpointerto
thecorrespondingQNetworkReplylieswithus,soweneedtodeleteitat
theendoftheslot.However,becarefulanddonotcalldeleteonthe
replydirectly.AlwaysusedeleteLater(),asthedocumentation
suggests!
Inthepreviouschapter,wewarnedyouthatyoushouldn'tusereadAll()toreadlargefiles,
astheycan'tfitinasingleQByteArray.ThesameholdsforQNetworkReply.Iftheserver
decidestosendyoualargeresponse(forexample,ifyoutrytodownloadalargefile),the
firstportionoftheresponsewillbesavedtoabufferinsidetheQNetworkReplyobject,and
thenthedownloadwillthrottledownuntilyoureadsomedatafromthebuffer.However,
youcan'tdothatifyouonlyusethefinished()signal.Instead,youneedtouse
theQNetworkReply::readyRead()signalandreadeachportionofthedatainordertofreethe
bufferandallowmoredatatobereceived.Wewillshowhowtodothislaterinthis
chapter.
ThefullsourcecodecanbefoundintheFileDownloadexample
bundledwiththisbook.Ifyoustartthesmalldemoapplicationand
clickontheLoadFilebutton,youshouldseethecontentofthe
loadedfile:
Haveagohero–Extendingthe
basicfiledownloader
Ofcourse,havingtoalterthesourcecodeinordertodownload
anotherfileisfarfromanidealapproach,sotrytoextendthedialog
byaddingalineeditinwhichyoucanspecifytheURLyouwantto
download.Also,youcanofferafiledialogtochoosethelocation
wherethedownloadedfilewillbesaved.Thesimplestwayofdoing
thatistousetheQFileDialog::getSaveFileName()staticfunction.
Singlenetworkmanagerper
application
OnesingleinstanceofQNetworkAccessManagerisenoughforanentire
application.Forexample,youcancreateaninstance
ofQNetworkAccessManagerinyourmainwindowclassandpassapointer
toittoalltheotherplaceswhereit'sneeded.Foreaseofuse,you
canalsocreateasingletonandaccessthemanagerthroughthat.
Asingletonpatternensuresthataclassisinstantiatedonlyonce.Thepatternisusefulfor
accessingapplication-wideconfigurationsor—asinourcase—aninstance
ofQNetworkAccessManager.
Asimpletemplate-basedapproachtocreateasingletonwilllook
likethis(asaheaderfile):
template<classT>
classSingleton
{
public:
staticT&instance()
{
staticTstatic_instance;
returnstatic_instance;
}
private:
Singleton();
~Singleton();
Singleton(constSingleton&);
Singleton&operator=(constSingleton&);
};
Inthesourcecode,youwillincludethatheaderfileandacquirea
singletonofaclasscalledMyClasswiththis:
MyClass&singleton=Singleton<MyClass>::instance();
Thissingletonimplementationisnotthread-safe,meaningthatattemptingtoaccessthe
instancefrommultiplethreadssimultaneouslywillresultinundefinedbehavior.An
exampleofthread-safeimplementationofthesingletonpatterncanbefound
athttps://wiki.qt.io/Qt_thread-safe_singleton.
IfyouareusingQtQuick—itwillbeexplainedinChapter11,
IntroductiontoQtQuick—withQQmlApplicationEngine,youcandirectly
usetheengine'sinstanceofQNetworkAccessManager:
QQmlApplicationEngineengine;
QNetworkAccessManager*network_manager=
engine.networkAccessManager();
Timeforaction–Displayinga
propererrormessage
Ifyoudonotseethecontentofthefile,somethingwentwrong.Just
asinreallife,thiscanoftenhappen.So,weneedtoensurethat
thereisagooderrorhandlingmechanisminsuchcasestoinform
theuseraboutwhatisgoingon.Fortunately,QNetworkReplyoffers
severalpossibilitiestodothis.
IntheslotcalleddownloadFinished(),wefirstwanttocheckwhetheran
erroroccurred:
if(reply->error()!=QNetworkReply::NoError){
//erroroccurred
}
TheQNetworkReply::error()functionreturnstheerrorthatoccurred
whilehandlingtherequest.Theerrorisencodedasavalueofthe
QNetworkReply::NetworkErrortype.Themostcommonerrorsareprobably
these:
Error
code Meaning
QNetworkReply:
:ConnectionRef
usedError
Theprogramwasnotabletoconnecttotheserver
atall(forexample,ifnoserverwasrunning)
QNetworkReply:
:ContentNotFou
TheserverrespondedwithHTTPerrorcode404,
indicatingthatapagewiththerequestedURL
ndError
couldnotbefound
QNetworkReply:
:ContentAccess
Denied
TheserverrespondedwithHTTPerrorcode403,
indicatingthatyoudonothavethepermissionto
accesstherequestedfile
Therearemorethan30possibleerrortypes,andyoucanlookthem
upinthedocumentationoftheQNetworkReply::NetworkErrorenumeration.
However,normally,youdonotneedtoknowexactlywhatwent
wrong.Youonlyneedtoknowwhethereverythingworkedout
—QNetworkReply::NoErrorwouldbethereturnvalueinthiscase—or
whethersomethingwentwrong.Toprovidetheuserwitha
meaningfulerrordescription,youcanuseQIODevice::errorString().The
textisalreadysetupwiththecorrespondingerrormessage,andwe
onlyhavetodisplayit:
if(reply->error()){
constQStringerror=reply->errorString();
m_edit->setPlainText(error);
return;
}
Inourexample,assumingthatwemadeanerrorintheURLand
wroteversions.txtbymistake,theapplicationwilllooklikethis:
IftherequestwasanHTTPrequestandthestatuscodeisof
interest,itcanberetrievedbyQNetworkReply::attribute():
intstatusCode=
reply-
>attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
SinceitreturnsQVariant,youneedtouseQVariant::toInt()togetthe
codeasaninteger.BesidestheHTTPstatuscode,youcanquerya
lotofotherinformationthroughattribute().Takealookatthe
descriptionoftheQNetworkRequest::Attributeenumerationinthe
documentation.There,youwillalsofind
QNetworkRequest::HttpReasonPhraseAttribute,whichholdsahuman-readable
reasonphrasefortheHTTPstatuscode,forexample,"NotFound"
ifanHTTPerror404hasoccurred.Thevalueofthisattributeis
usedtosettheerrortextforQIODevice::errorString().So,youcaneither
usethedefaulterrordescriptionprovidedbyerrorString()orcompose
yourownbyinterpretingthereply'sattributes.
Ifadownloadfailedandyouwanttoresumeit,orifyouonlywanttodownloadaspecific
partofafile,youcanusetheRangeheader.However,theservermustsupportthis.
Inthefollowingexample,onlythebytesfrom300to500willbe
downloaded:
QNetworkRequestrequest(url);
request.setRawHeader("Range","bytes=300-500");
QNetworkReply*reply=m_network_manager->get(request);
Ifyouwanttosimulatesendingaformonawebsite,youwillusuallyneedtosendaPOST
requestinsteadofGET.ThisisdonebyusingtheQNetworkAccessManager::post()function
insteadoftheget()functionweused.Youwillalsoneedtospecifythepayload,for
example,usingtheQHttpMultiPartclass.
DownloadingfilesoverFTP
DownloadingafileoverFTPisassimpleasdownloadingfilesover
HTTP.IfitisananonymousFTPserverforwhichyoudonotneed
anauthentication,justusetheURLaswedidbefore.Assumingthat
thereisagainafilecalledversion.txtontheFTPserveronthe
localhost,typethis:
m_network_manager->get(QNetworkRequest(
QUrl("ftp://localhost/version.txt")));
That'sall;everythingelsestaysthesame.IftheFTPserverrequires
anauthentication,you'llgetanerror;considerthisexample:
Likewise,settingtheusernameandpasswordtoaccessanFTP
serveriseasy—eitherwriteitintheURL,orusethesetUserName()and
setPassword()functionsofQUrl.Iftheserverdoesnotuseastandard
port,youcansettheportexplicitlywithQUrl::setPort().
TouploadafiletoanFTPserver,useQNetworkAccessManager::put(),whichtakes
QNetworkRequestasitsfirstargument,callingaURLthatdefinesthenameofthenewfileon
theserver,andtheactualdataasitssecondargument,whichshouldbeuploaded.For
smalluploads,youcanpassthecontentasQByteArray.Forlargercontent,it'sbettertouse
apointertoQIODevice.Ensurethatthedeviceisopenandstaysavailableuntiltheuploadis
complete.
Downloadingfilesinparallel
AveryimportantnoteconcerningQNetworkAccessManageristhefactthat
itworksasynchronously.Thismeansthatyoucanpostanetwork
requestwithoutblockingthemaineventloop,andthisiswhat
keepstheGUIresponsive.Ifyoupostmorethanonerequest,they
areputinthemanager'squeue.Dependingontheprotocolused,
theymaybeprocessedinparallel.IfyouaresendingHTTP
requests,normallyuptosixrequestswillbehandledatatime.If
morerequestsarequeued,theywillbeautomaticallyprocessed
later.Thiswillnotblocktheapplication,asQNetworkAccessManageruses
threadsinternally.
ThereisreallynoneedtoencapsulateQNetworkAccessManagerinathread;however,
unfortunately,thisunnecessaryapproachisfrequentlyrecommendedallovertheinternet.
Really,don'tmoveQNetworkAccessManagertoathreadunlessyouknowexactlywhatyouare
doing.
Ifyousendmultiplerequests,theslotconnectedtothemanager's
finished()signaliscalledinanarbitraryorder,dependingonhow
quicklyarequestgetsareplyfromtheserver.Thisiswhyyouneed
toknowtowhichrequestareplybelongs.Thisisonereasonwhy
everyQNetworkReplycarriesitsrelatedQNetworkRequest.Itcanbeaccessed
throughQNetworkReply::request().
Evenifthedeterminationoftherepliesandtheirpurposemaywork
forasmallapplicationinasingleslot,itwillquicklygetlargeand
confusingifyousendalotofrequestswithdifferentpurposes.It
wouldbebettertoconnectrequeststomultipleslotsthatare
specializedforagiventask.Fortunately,thiscanbeachievedvery
easily.
AnymethodthataddsarequesttoQNetworkAccessManager(suchasget())
returnsapointertoQNetworkReply.Usingthispointer,youcanthen
connectthereply'ssignalstoyourspecificslots.Forexample,ifyou
haveseveralURLs,andyouwanttosavealllinkedimagesfrom
thesesitestoyourharddrive,yourequestallwebpagesvia
QNetworkAccessManager::get()andconnecttheirrepliestoaslotspecialized
forparsingthereceivedHTML.Iflinkstotheimagesarefound,this
slotwillrequestthemagainwithget().Thistime,however,the
repliestotheserequestswillbeconnectedtoasecondslot,whichis
designedforsavingtheimagestothedisk.Thus,youcanseparate
thetwotasks:parsingHTMLandsavingdatatoalocaldrive.
ThemostimportantsignalsofQNetworkReplyarediscussednext.
Thefinishedsignal
Thefinished()signalisanequivalentofthe
QNetworkAccessManager::finished()signalthatweusedearlier.Itis
triggeredassoonasareplyisreturned—successfullyornot.After
thissignalisemitted,neitherthereply'sdatanoritsmetadatawill
bealteredanymore.Withthissignal,youarenowabletoconnecta
replytoaspecificslot.Thisway,youcanrealizethescenarioon
savingimagesthatwasoutlinedintheprevioussection.
However,oneproblemremains:ifyoupostsimultaneousrequests,
youdonot
knowwhichonehasfinishedandthuscalledtheconnectedslot.
UnlikeQNetworkAccessManager::finished(),QNetworkReply::finished()doesnot
passapointertoQNetworkReply;thiswouldactuallybeapointerto
itselfinthiscase.We'vealreadyhadasimilarprobleminChapter3,
QtGUIProgramming,solet'srememberhowwecandealwithit.
Aquicksolutiontosolvethisproblemistousesender().Itreturnsa
pointertotheQObjectinstancethathascalledtheslot.Sinceweknow
thatitwasQNetworkReply,wecanwritethefollowing:
QNetworkReply*reply=qobject_cast<QNetworkReply*>(sender());
if(!reply){
return;
}
Inthiscode,weneededtocasttheQObjectpointerreturnedbysender()
toapointeroftheQNetworkReplytype.
Wheneveryou'recastingclassesthatinheritQObject,useqobject_cast.Unlikedynamic_cast,
itdoesnotuseRTTIandworksacrossthedynamiclibraryboundaries.
Althoughwecanbeprettyconfidentthatthecastwillwork,donot
forgettocheckwhetherthepointerisvalid.Ifitisanullpointer,
exittheslot.
Timeforaction–Writingthe
OOPconformcodeusing
QSignalMapper
Amoreelegantwaythatdoesnotrelyonsender()wouldbetouse
QSignalMappertoreceivethereplyobjectintheargumentoftheslot.
First,youneedtoaddtheQSignalMapper*m_imageFinishedMapperprivatefield
toyourclass.WhenyoucallQNetworkAccessManager::get()torequesteach
image,setupthemapperasfollows:
for(constQString&url:urls){
QNetworkRequestrequest(url);
QNetworkReply*reply=m_network_manager->get(request);
connect(reply,SIGNAL(finished()),
m_imageFinishedMapper,SLOT(map()));
m_imageFinishedMapper->setMapping(reply,reply);
}
Inaprominentplace,mostlikelytheconstructoroftheclass,
connectthemapper'smap()signaltoacustomslot.Takethisexample
intoconsideration:
connect(m_imageFinishedMapper,SIGNAL(mapped(QObject*)),
this,SLOT(imageFinished(QObject*)));
Nowyourslotreceivesthereplyobjectastheargument:
voidObject::imageFinished(QObject*replyObject)
{
QNetworkReply*reply=qobject_cast<QNetworkReply*>(replyObject);
//...
}
Whatjusthappened?
First,wepostedtherequestandfetchedthepointertothe
QNetworkReplyobject.
Then,weconnectedthereply'sfinishedsignaltothemapper'sslot
map().Next,wecalledthesetMapping()methodofthemapperto
indicatethatthesenderitselfshouldbe
sentastheslot'sargument.Theeffectisverysimilartothedirect
useoftheQNetworkAccessManager::finished(QNetworkReply*reply)signal,but
thisway,wecanusemultipleslotsdedicatedtodifferentpurposes
(withaseparatemappercorrespondingtoeachslot),allservedbya
singleQNetworkAccessManagerinstance.
QSignalMapperalsoallowsyoutomapwithintorQStringasanidentifierinsteadofQObject
*,asusedintheprecedingcode.So,youcanrewritetheexampleandusetheURLto
identifythecorrespondingrequest.
Theerrorsignal
Insteadofdealingwitherrorsintheslotconnectedtothefinished()
signal,youcanusethereply'serror()signal,whichpassestheerror
oftheQNetworkReply::NetworkErrortypetotheslot.Aftertheerror()signal
hasbeenemitted,thefinished()signalwill,mostlikely,alsobe
emittedshortly.
ThereadyReadsignal
Untilnow,wehaveusedtheslotconnectedtothefinished()signalto
getthereply'scontent.Thisworksperfectlyifyouaredealwith
smallfiles.However,thisapproachisunsuitablewhendealingwith
largefiles,astheywillunnecessarilybindtoomanyresources.For
largerfiles,itisbettertoreadandsavethetransferreddataassoon
asitisavailable.WeareinformedbyQIODevice::readyRead()whenever
newdataisavailabletoberead.So,forlargefiles,youshoulduse
thefollowingcode:
QNetworkReply*reply=m_network_manager->get(request);
connect(reply,&QIODevice::readyRead,
this,&SomeClass::readContent);
m_file.open(QIODevice::WriteOnly);
Thiswillhelpyouconnectthereply'sreadyRead()signaltoaslot,set
upQFile,andopenit.Intheconnectedslot,typeinthefollowing
snippet:
QNetworkReply*reply=/*...*/;
constQByteArraybyteArray=reply->readAll();
m_file.write(byteArray);
m_file.flush();
Now,youcanfetchthecontent,whichhasbeentransferredsofar,
andsaveittothe(alreadyopen)file.Thisway,theresourcesneeded
areminimized.Don'tforgettoclosethefileafterthefinished()signal
isemitted.
Inthiscontext,itwouldbehelpfulifyouknewupfrontthesizeof
thefileyouwanttodownload.Withthisinformation,wecancheck
upfrontwhetherthereisenoughspaceleftonthedisk.Wecanuse
QNetworkAccessManager::head()forthispurpose.Itbehavesliketheget()
function,butitdoesnotrequestthecontentofthefile.Onlythe
headersaretransferred,andifwearelucky,theserversendsthe
Content-Lengthheader,whichholdsthefilesizeinbytes.Togetthat
information,wetypethis:
intlength=reply-
>header(QNetworkRequest::ContentLengthHeader).toInt();
Timeforaction–Showingthe
downloadprogress
Especiallywhenabigfileisdownloaded,theuserusuallywantsto
knowhowmuchdatahasalreadybeendownloadedand
approximatelyhowlongitwilltakeforthedownloadtofinish.
Inordertoachievethis,wecanusethereply'sdownloadProgress()
signal.Asthefirstargument,itpassestheinformationonhow
manybyteshavealreadybeenreceivedandasthesecondargument,
howmanybytesthereareintotal.Thisgivesusthepossibilityto
indicatetheprogressofthedownloadwithQProgressBar.Asthepassed
argumentsareoftheqint64type,wecan'tusethemdirectlywith
QProgressBar,asitonlyacceptsint.So,intheconnectedslot,wecando
thefollowing:
voidSomeClass::downloadProgress(qint64bytesReceived,qint64
bytesTotal){
qrealprogress=(bytesTotal<1)?1.0
:static_cast<qreal>(bytesReceived)/bytesTotal;
progressBar->setValue(qRound(progress*progressBar->maximum()));
}
Whatjusthappened?
First,wecalculatethepercentageofthedownloadprogress.The
calculatedprogressvaluewillrangefrom0(0%)to1(100%).Then,
wesetthenewvaluefortheprogressbarwhereprogressBaristhe
pointertothisbar.However,whatvaluewillprogressBar->maximum()have
andwheredowesettherangefortheprogressbar?Whatisniceis
thatyoudonothavetosetitforeverynewdownload.Itisonly
doneonce,forexample,intheconstructoroftheclasscontaining
thebar.Asrangevalues,wewouldrecommendthis:
progressBar->setRange(0,2048);
Thereasonisthatifyoutake,forexample,arangeof0to100and
theprogressbaris500pixelswide,thebarwouldjump5pixels
forwardforeveryvaluechange.Thiswilllookugly.Togetasmooth
progressionwherethebarexpandsby1pixelatatime,arangeof0
to99.999.999wouldsurelywork,butitwouldbehighlyinefficient.
Thisisbecausethecurrentvalueofthebarwouldchangealot
withoutanygraphicaldepiction.So,thebestvaluefortherange
wouldbe0totheactualbar'swidthinpixels.Unfortunately,the
widthofthebarcanchangedependingontheactualwidgetwidth,
andfrequentlyqueryingtheactualsizeofthebareverytimethe
valuechangesisalsonotagoodsolution.Why2048,then?It'sjust
aniceroundnumberthatisbiggerthananyscreenresolutionwe're
likelytoget.Thisensuresthattheprogressbarrunssmoothly,even
ifitisfullyexpanded.Ifyouaretargetingsmallerdevices,choosea
smaller,moreappropriatenumber.
Tobeabletocalculatethetimeremainingforthedownloadto
finish,youhavetostartatimer.Inthiscase,useQElapsedTimer.After
postingtherequestwithQNetworkAccessManager::get(),startthetimerby
callingQElapsedTimer::start().Assumingthatthetimeriscalledm_timer,
thecalculationwillbeasfollows:
qrealremaining=m_timer.elapsed()*
(1.0-progress)/progress;
intremainingSeconds=qRound(remaining/1000);
QElapsedTimer::elapsed()returnsthemillisecondsthatarecountedfrom
themomentthetimerisstarted.Assumingthatthedownload
progressislinear,theratiooftheremainingtimetotheelapsed
timeequals(1.0-progress)/progress.Forexample,ifprogressis0.25
(25%),theexpectedremainingtimewillbethreetimesbiggerthan
theelapsedtime:(1.0-0.25)/0.25)=3.Ifyoudividetheresultby
1,000androundittothenearestinteger,you'llgettheremaining
timeinseconds.
QElapsedTimerisnottobeconfusedwithQTimer.QTimerisusedtocallaslotafteracertain
amountoftimehaspassed.QElapsedTimerismerelyaconvenienceclassthatisableto
rememberthestarttimeandcalculatetheelapsedtimebysubtractingthestarttimefrom
thecurrenttime.
Usingaproxy
Ifyouwanttouseaproxy,youfirsthavetosetupQNetworkProxy.You
candefinethetypeofproxywithsetType().Asarguments,youwill
mostlikelywanttopassQNetworkProxy::Socks5Proxyor
QNetworkProxy::HttpProxy.Then,setupthehostnamewithsetHostName(),the
usernamewithsetUserName(),andthepasswordwithsetPassword().The
lasttwopropertiesare,ofcourse,onlyneedediftheproxyrequires
authentication.Oncetheproxyissetup,youcansetittotheaccess
managerviaQNetworkAccessManager::setProxy().Now,allnewrequestswill
usethisproxy.
ConnectingtoGoogle,
Facebook,Twitter,andco.
SincewediscussedQNetworkAccessManager,younowhavetheknowledge
youneedtointegrateFacebook,Twitter,orsimilarsitesintoyour
application.TheyallusetheHTTPSprotocolandsimplerequestsin
ordertoretrievedatafromthem.ForFacebook,youhavetousethe
so-calledGraphAPI.Itdescribeswhichinterfacesareavailableand
whatoptionstheyoffer.Ifyouwanttosearchforuserswhoare
calledHelena,youhavetorequesthttps://graph.facebook.com/search?
q=helena&type=user.Ofcourse,youcandothiswithQNetworkManager.You
willfindmoreinformationaboutthepossiblerequeststoFacebook
athttps://developers.facebook.com/docs/graph-api.
Ifyouwishtodisplaytweetsinyourgame,youhavetouseTwitter's
RESTorSearchAPI.AssumingthatyouknowtheIDofatweetyou
wouldliketodisplay,youcangetitthrough
https://api.twitter.com/1.1/statuses/show.json?id=12345,where12345isthe
actualIDforthetweet.Ifyouwouldliketofindtweetsmentioning
#Helena,youwouldwritehttps://api.twitter.com/1.1/search/tweets.json?
q=%23Helena.Youcanfindmoreinformationabouttheparametersand
theotherpossibilitiesofTwitter'sAPIat
https://developer.twitter.com/en/docs.
SincebothFacebookandTwitterneedanauthenticationtouse
theirAPIs,wewilltakealookatGoogleinstead.Let'suseGoogle's
DistanceMatrixAPIinordertogetinformationabouthowlongit
wouldtakeforustogetfromonecitytoanother.Thetechnical
documentationfortheAPIwewillusecanbefoundat
https://developers.google.com/maps/documentation/distancematrix.
Timeforaction–Using
Google'sDistanceMatrixAPI
TheGUIforthisexampleiskeptsimple—thesourcecodeis
attachedwiththebook.Itconsistsoftwolineedits(ui->fromandui-
>to)thatallowyoutoentertheoriginanddestinationofthejourney.
Italsoprovidesyouwithacombobox(ui->vehicle)thatallowsyouto
chooseamodeoftransportation—whetheryouwanttodriveacar,
rideabicycle,orwalk—apushbutton(ui->search)tostarttherequest,
andatextedit,or(ui->result)toshowtheresults.
Itlookslikethis:
MainWindow—asubclassofQMainWindow—istheapplication'smainclass
thatholdstwoprivatemembers:m_network_manager,whichisapointer
toQNetworkAccessManager,andm_reply,whichisapointertoQNetworkReply.
Timeforaction–Constructing
thequery
Wheneverthebuttonispressed,thesendRequest()slotiscalled:
voidMainWindow::sendRequest()
{
if(m_reply!=nullptr&&m_reply->isRunning()){
m_reply->abort();
}
ui->result->clear();
//...
}
Inthisslot,wefirstcheckwhetherthereisanoldrequest,which
wasstoredinm_reply,andwhetheritisstillrunning.Ifthatistrue,we
aborttheoldrequest,asweareabouttoscheduleanewone.Then,
wealsowipeouttheresultofthelastrequestbycalling
QPlainTextEdit::clear()onthetextedit.
Next,wewillconstructtheURLfortherequest.Wecandothisby
composingthestringbyhandwhereweaddthequeryparameters
tothebaseURLsimilartothefollowing:
//don'tdothis!
QStringurl=baseUrl+"?origin="+ui->from->text()+"&...";
Besidestheproblemthatthisquicklybecomeshardtoreadwhen
weincludemultipleparameters,itisalsorathererror-prone.The
valuesofthelineeditshavetobeencodedtofitthecriteriafora
validURL.Foreveryuservalue,we,therefore,havetocall
QUrl::toPercentEncoding()explicitly.Amuchbetterapproach,whichis
easiertoreadandlesserror-prone,istouseQUrlQuery.Itcircumvents
theproblemthatmayresultwhenyouforgettoencodethedata.So,
wedothis:
QUrlQueryquery;
query.addQueryItem(QStringLiteral("sensor"),
QStringLiteral("false"));
query.addQueryItem(QStringLiteral("language"),QStringLiteral("en"));
query.addQueryItem(QStringLiteral("units"),
QStringLiteral("metric"));
query.addQueryItem(QStringLiteral("mode"),ui->vehicle-
>currentText());
query.addQueryItem(QStringLiteral("origins"),ui->from->text());
query.addQueryItem(QStringLiteral("destinations"),ui->to->text());
Theusageisprettyclear:wecreateaninstanceandthenaddthe
queryparameterswithaddQueryItem().Thefirstargumentistakenas
thekeyandthesecondasthevalueresultinginastringsuchas
"key=value".Thevaluewillbeautomaticallyencodedwhenweuse
QUrlQueryinconjunctionwithQUrl.OtherbenefitsofusingQUrlQueryare
thatwecancheckwhetherwehavealreadysetakeywith
hasQueryItem(),takingthekeyasanargument,orremovedapreviously
setkeybycallingremoveQueryItem().
Let'sreviewwhichparameterswehaveset.Thesensorkeyissetto
falseaswearenotusingaGPSdevicetolocateourposition.The
languagekeyissettoEnglish,andforunits,wefavormetricover
imperial.Then,thesearch-relatedparametersareset.Theorigins
keyholdstheplaceswewanttostartfrom.Asitsvalue,thetextof
theui->fromlineeditischosen.Ifyouwanttoquerymultiplestarting
positions,youjusthavetocombinethemusing|.Equivalenttothe
origins,wesetupthevaluefordestinations.Last,wepassthevalue
ofthecomboboxtomode,whichdefineswhetherwewanttogoby
acar,bicycle,orwhetherwewanttowalk.Next,weexecutethe
request:
QUrlurl(QStringLiteral(
"https://maps.googleapis.com/maps/api/distancematrix/json"));
url.setQuery(query);
url.setQuery(query);
m_reply=m_network_manager->get(QNetworkRequest(url));
WecreateQUrlthatcontainstheaddresstowhichthequeryshould
beposted.Byincludingjsonattheend,wedefinethattheserver
shouldtransferitsreplyusingtheJSONformat.Googlealso
providestheoptionforustogettheresultasXML.Toachievethis,
simplyreplacejsonwithxml.However,sincetheAPIsofFacebook
andTwitterreturnJSON,wewillusethisformat.
Then,wesetthepreviouslyconstructedquerytotheURLbycalling
QUrl::setQuery().Thisautomaticallyencodesthevalues,sowedonot
havetoworryaboutthat.Last,weposttherequestbycallingthe
get()functionandstorethereturnedQNetworkReplyinm_reply.
Timeforaction–Parsingthe
server'sreply
Intheconstructor,wehaveconnectedthemanager'sfinished()signal
tothefinished()slotoftheMainWindowclass.Itwillthusbecalledafter
therequesthasbeenposted:
voidMainWindow::finished(QNetworkReply*reply)
{
if(m_reply!=reply){
reply->deleteLater();
return;
}
//...
}
First,wecheckwhetherthereplythatwaspassedistheonethatwe
haverequestedthroughm_network_manager.Ifthisisnotthecase,we
deletethereplyandexitthefunction.Thiscanhappenifareplywas
abortedbythesendRequest()slot.Sincewearenowsurethatitisour
request,wesetm_replytonullptr,becausewehavehandleditanddo
notneedthisinformationanymore:
m_reply=nullptr;
if(reply->error()!=QNetworkReply::NoError){
ui->result->setPlainText(reply->errorString());
reply->deleteLater();
return;
}
Next,wecheckwhetheranerroroccurred,andifitdid,weputthe
reply'serrorstringinthetextedit,deletethereply,andexitthe
function.Afterthis,wecanfinallystartdecodingtheserver's
response:
constQByteArraycontent=reply->readAll();
constQJsonDocumentdoc=QJsonDocument::fromJson(content);
if(!doc.isObject()){
ui->result->setPlainText(tr("ErrorwhilereadingtheJSON
file."));
reply->deleteLater();
return;
}
WithreadAll(),wegetthecontentoftheserver'sreply.Sincethe
transferreddataisnotlarge,wedonotneedtousepartialreading
withreadyRead().ThecontentisthenconvertedtoQJsonDocumentusing
theQJsonDocument::fromJson()staticfunction,whichtakesQByteArrayasan
argumentandparsesitsdata.Ifthedocumentisnotanobject,the
server'sreplywasn'tvalid,astheAPIcallshouldrespondwitha
singleobject.Inthiscase,weshowanerrormessageonthetext
edit,deletethereply,andexitthefunction.Let'slookatthenext
partofthecode:
constQJsonObjectobj=doc.object();
constQJsonArrayorigins=obj.value("origin_addresses").toArray();
constQJsonArraydestinations=
obj.value("destination_addresses").toArray();
Sincewehavenowensuredthatthereisanobject,westoreitinobj.
Furthermore,duetotheAPI,wealsoknowthattheobjectholdsthe
origin_addressesanddestination_addresseskeys.Bothvaluesarearraysthat
holdtherequestedoriginsanddestinations.Fromthispointon,we
willskipanytestsifthevaluesexistandarevalidsincewetrustthe
API.Theobjectalsoholdsakeycalledstatus,whosevaluecanbe
usedtocheckwhetherthequerymayhavefailedandifyes,why.
Thelasttwolinesofthesourcecodestoretheoriginsand
destinationsintwovariables.Withobj.value("origin_addresses"),weget
QJsonValuethatholdsthevalueofthepairspecifiedbythe
origin_addresseskey,andQJsonValue::toArray()convertsthisvalueto
QJsonArray.ThereturnedJSONfileforasearchrequestingthe
distancefromWarsaworErlangentoBirminghamwilllooklike
this:
{
"destination_addresses":["Birmingham,WestMidlands,UK"],
"origin_addresses":["Warsaw,Poland","Erlangen,Germany"],
"rows":[...],
"status":"OK"
}
Therowskeyholdstheactualresultsasanarray.Thefirstobjectin
thisarraybelongstothefirstorigin,thesecondobjecttothesecond
origin,andsoon.Eachobjectholdsakeynamedelements,whose
valueisalsoanarrayofobjectsthatbelongtothecorresponding
destinations:
"rows":[
{
"elements":[{...},{...}]
},
{
"elements":[{...},{...}]
}
],
EachJSONobject({...}intheprecedingexample)foranorigin-
destinationpairconsistsoftwopairswiththedistanceandduration
keys.Bothvaluesofthesekeysarearraysthatholdthetextandvalue
keys,wheretextisahuman-readablephraseforvalue.Theobjectfor
theWarsaw-Birminghamsearchlooksasshowninthefollowing
snippet:
{
"distance":{
"text":"1,835km",
"value":1834751
},
"duration":{
"text":"16hours37mins",
"value":59848
"value":59848
},
"status":"OK"
}
Asyoucansee,thevalueofvaluefordistanceisthedistance
expressedinmeters—sincewehaveusedunits=metricintherequest—
andthevalueoftextisvaluetransformedintokilometerswiththe
"km"postfix.Thesameappliestoduration.Here,valueisexpressed
inseconds,andtextisvalueconvertedintohoursandminutes.
NowthatweknowhowthereturnedJSONisstructured,wedisplay
thevalueofeachorigin-destinationpairinthetextedit.Therefore,
weloopthrougheachpossiblepairingusingthetwoQJsonArray.We
needtheindicesaswellasvalues,soweusetheclassicforloop
insteadoftherange-basedone:
QStringoutput;
for(inti=0;i<origins.count();++i){
constQStringorigin=origins.at(i).toString();
constQJsonArrayrow=
obj.value("rows").toArray().at(i).toObject()
.value("elements").toArray();
for(intj=0;j<destinations.count();++j){
First,wecreateanoutputstringvariabletocachetheconstructed
text.Beforestartingthesecondloop,wecalculatetwovariablesthat
willbethesameforalldestinations.Theoriginvariableholdsthe
textrepresentationofthecurrentorigin,andtherowvariable
containsthecorrespondingrowofthetable.Wheneverwetrytoget
anitemoutofaQJsonArrayoraQJsonObject,thereturnedvaluewillhave
theQJsonValuetype,soeachtimewedothat,weneedtoconvertitto
anarray,anobject,orastring,dependingonwhatweexpecttoget
accordingtotheAPI.Whenwecalculatetherowvariable,startingat
thereply'srootobject,wefetchthevalueoftherowskeyandconvert
ittoanarray(obj.value("rows").toArray()).Then,wefetchthevalueof
thecurrentrow(.at(i)),convertittoanobject,andfetchitselements
key(.toObject().value("elements")).Sincethisvalueisalsoanarray—the
columnsoftherow—weconvertittoanarray.
Thescopeinsidethetwoloopswillbereachedforeach
combination.Thinkofthetransferredresultasatablewherethe
originsarerowsandthedestinationsarecolumns:
output+=tr("From:%1\n").arg(origin);
output+=tr("To:%1\n").arg(destinations.at(j).toString());
First,weaddthe"From:"stringandthecurrentorigintooutput.The
sameisdoneforthedestination,whichresultsinthefollowingas
thevalueforoutput:
From:Warsaw,Poland
To:Birmingham,WestMidlands,UK
Next,wewillreadthedurationanddistancefromthe
correspondingQJsonObjectfromwherewecalldata:
constQJsonObjectdata=row.at(j).toObject();
constQStringstatus=data.value("status").toString();
Inthiscode,wefetchthecurrentcolumnfromtherow(at(j))and
convertittoanobject.Thisistheobjectthatcontainsthedistance
anddurationforanorigin-destinationpairinthe(i;
j)cell.Besidesdistanceandduration,theobjectalsoholdsakeycalled
status.Itsvalueindicateswhetherthesearchwassuccessful(OK),
whethertheoriginordestinationcouldnotbefound(NOT_FOUND),or
whetherthesearchcouldnotfindaroutebetweentheoriginand
destination(ZERO_RESULTS).Westorethevalueofstatusinalocal
variablethatisalsonamedstatus.
Next,wecheckthestatusandappendthedistanceandtheduration
totheoutput:
if(status=="OK"){
output+=tr("Distance:%1\n").arg(
data.value("distance").toObject().value("text").toString());
output+=tr("Duration:%1\n").arg(
output+=tr("Duration:%1\n").arg(
data.value("duration").toObject().value("text").toString());
}else{/*...*/}
Fordistance,wewanttoshowthephrasedresult.Therefore,we
firstgettheJSONvalueofthedistancekey(data.value("distance")),
convertittoanobject,andrequestthevalueforthetextkey
(toObject().value("text")).Lastly,weconvertQJsonValuetoQStringusing
toString().Thesameappliesforduration.Finally,weneedtohandle
theerrorstheAPImightreturn:
}elseif(status=="NOT_FOUND"){
output+=tr("Originand/ordestinationofthis"
"pairingcouldnotbegeocoded.\n");
}elseif(status=="ZERO_RESULTS"){
output+=tr("Noroutecouldbefound.\n");
}else{
output+=tr("Unknownerror.\n");
}
output+=QStringLiteral("=").repeated(35)+QStringLiteral("\n");
Attheendoftheoutputforeachcell,weaddalineconsistingof35
equalssigns(QStringLiteral("=").repeated(35))toseparatetheresultfrom
theothercells.Finally,afterallloopsfinish,weputthetextintothe
texteditanddeletethereplyobject:
ui->result->setPlainText(output);
reply->deleteLater();
Theactualresultthenlooksasfollows:
Controllingtheconnectivity
state
Beforetryingtoaccessanetworkresource,it'susefultocheck
whetheryouhaveanactiveconnectiontotheinternet.Qtallows
youtocheckwhetherthecomputer,mobiledevice,ortabletis
online.Youcanevenstartanewconnectioniftheoperatingsystem
supportsit.
TherelevantAPImainlyconsistsoffourclasses.
QNetworkConfigurationManageristhebaseandstartingpoint.Itholdsall
networkconfigurationsavailableonthesystem.Furthermore,it
providesinformationaboutthenetworkcapabilities,forexample,
whetheryoucanstartandstopinterfaces.Thenetwork
configurationsfoundbyitarestoredasQNetworkConfigurationclasses.
QNetworkConfigurationholdsallinformationaboutanaccesspointbut
notaboutanetworkinterface,asaninterfacecanprovidemultiple
accesspoints.Thisclassalsoprovidesonlytheinformationabout
networkconfigurations.Youcan'tconfigureanaccesspointora
networkinterfacethroughQNetworkConfiguration.Thenetwork
configurationisuptotheoperatingsystemand
thereforeQNetworkConfigurationisaread-onlyclass.With
QNetworkConfiguration,however,youcandeterminewhetherthetypeof
connectionisanEthernet,WLAN,or4Gconnection.Thismay
influencewhatkindofdataand,moreimportantly,whatsizeof
datayouwilldownload.
WithQNetworkSession,youcanthenstartorstopsystemnetwork
interfaces,whicharedefinedbytheconfigurations.Thisway,you
gaincontroloveranaccesspoint.QNetworkSessionalsoprovidessession
managementthatisusefulwhenasystem'saccesspointisusedby
morethanoneapplication.Thesessionensuresthattheunderlying
interfaceonlygetsterminatedafterthelastsessionhasbeenclosed.
Lastly,QNetworkInterfaceprovidesclassicinformation,suchasthe
hardwareaddressorinterfacename.
QNetworkConfigurationManage
r
QNetworkConfigurationManagermanagesallnetworkconfigurationsthatare
availableonasystem.Youcanaccesstheseconfigurationsby
callingallConfigurations().Ofcourse,youhavetocreateaninstanceof
themanagerfirst:
QNetworkConfigurationManagermanager;
QList<QNetworkConfiguration>cfgs=manager.allConfigurations();
Theconfigurationsarereturnedasalist.Thedefaultbehaviorof
allConfigurations()istoreturnallpossibleconfigurations.However,
youcanalsoretrieveafilteredlist.Ifyoupass
QNetworkConfiguration::Activeasanargument,thelistonlycontains
configurationsthathaveatleastoneactivesession.Ifyoucreatea
newsessionbasedonsuchaconfiguration,itwillbeactiveand
connected.BypassingQNetworkConfiguration::Discoveredasanargument,
youwillgetalistwithconfigurationsthatcanbeusedto
immediatelystartasession.Note,however,thatatthispoint,you
cannotbesurewhethertheunderlyinginterfacecanbestarted.The
lastimportantargumentisQNetworkConfiguration::Defined.Withthis
argument,allConfigurations()returnsalistofconfigurationsthatare
knowntothesystembutarenotusablerightnow.Thismaybea
previouslyusedWLANhotspot,whichiscurrentlyoutofrange.
Youwillbenotifiedwhenevertheconfigurationschange.Ifanew
configurationbecomesavailable,themanageremitsthe
configurationAdded()signal.Thismayhappen,forexample,ifmobile
datatransmissionbecomesavailableoriftheuserturnshis/her
device'sWLANadapteron.Ifaconfigurationisremoved,for
example,iftheWLANadapteristurnedoff,configurationRemoved()is
emitted.Lastly,whenaconfigurationischanged,youwillbe
notifiedbytheconfigurationChanged()signal.Allthreesignalspassa
constantreferencetotheconfigurationaboutwhatwasadded,
removed,orchanged.Theconfigurationpassedbythe
configurationRemoved()signalis,ofcourse,invalid.Itstillcontainsthe
nameandidentifieroftheremovedconfiguration.
Tofindoutwhetheranynetworkinterfaceofthesystemisactive,
callisOnline().Ifyouwanttobenotifiedaboutamodechange,track
theonlineStateChanged()signal.
SinceaWLANscantakesacertainamountoftime,allConfigurations()maynotreturnall
theavailableconfigurations.Toensurethatconfigurationsarecompletelypopulated,call
updateConfigurations()first.Duetothelongtimeitmaytaketogatheralltheinformation
aboutthesystem'snetworkconfigurations,thiscallisasynchronous.Waitforthe
updateCompleted()signalandonlythen,callallConfigurations().
QNetworkConfigurationManageralsoinformsyouabouttheBearerAPI's
capabilities.Thecapabilities()functionreturnsaflagofthe
QNetworkConfigurationManager::Capabilitiestypeanddescribestheavailable
possibilitiesthatareplatform-specific.Thevaluesyoumaybemost
interestedinareasfollows:
Val
ue Meaning
CanSta
rtAndS
topInt
erface
s
Thismeansthatyoucanstartandstopaccesspoints.
Applic
ationL
evelRo
aming
Thisindicatesthatthesystemwillinformyouifamore
suitableaccesspointisavailable,andthatyoucanactively
changetheaccesspointifyouthinkthereisabetterone
available.
DataSt
atisti
cs
Withthiscapability,QNetworkSessioncontainsinformation
aboutthetransmittedandreceiveddata.
QNetworkConfiguration
QNetworkConfigurationholds,asmentionedearlier,informationaboutan
accesspoint.Withname(),yougettheuser-visiblenamefora
configuration,andwithidentifier(),yougetaunique,system-specific
identifier.Ifyoudevelopgamesformobiledevices,itmaybeof
advantagetoyoutoknowwhichtypeofconnectionisbeingused.
Thismightinfluencethedatathatyourequest;forexample,the
qualityandthus,thesizeofavideo.WithbearerType(),thetypeof
bearerusedbyaconfigurationisreturned.Thereturned
enumerationvaluesareratherself-explanatory:BearerEthernet,
BearerWLAN,Bearer2G,BearerCDMA2000,BearerWCDMA,BearerHSPA,BearerBluetooth,
BearerWiMAX,andsoon.Youcanlookupthefull-valuelistinthe
documentationforQNetworkConfiguration::BearerType.
Withpurpose(),yougetthepurposeoftheconfiguration,forexample,
whetheritissuitabletoaccessaprivatenetwork
(QNetworkConfiguration::PrivatePurpose)ortoaccessapublicnetwork
(QNetworkConfiguration::PublicPurpose).Thestateoftheconfiguration,ifit
isdefined,discoveredoractive,aspreviouslydescribed,canbe
accessedthroughstate().
QNetworkSession
Tostartanetworkinterfaceortotellthesystemtokeepaninterface
connectedforaslongasyouneedit,youhavetostartasession:
QNetworkConfigurationManagermanager;
QNetworkConfigurationcfg=manager.defaultConfiguration();
QNetworkSession*session=newQNetworkSession(cfg,this);
session->open();
Asessionisbasedonaconfiguration.Whenthereismorethanone
sessionandyouarenotsurewhichonetouse,use
QNetworkConfigurationManager::defaultConfiguration().Itreturnsthesystem's
defaultconfiguration.Basedonthis,youcancreateaninstanceof
QNetworkSession.Thefirstargument,theconfiguration,isrequired.The
secondisoptionalbutisrecommendedsinceitsetsaparent,and
wedonothavetotakecareofthedeletion.Youmaywanttocheck
whethertheconfigurationisvalid(QNetworkConfiguration::isValid())first.
Callingopen()willstartthesessionandconnecttheinterfaceif
neededandsupported.Sinceopen()cantakesometime,thecallis
asynchronous.So,eitherlistentotheopened()signal,whichis
emittedassoonasthesessionisopen,ortotheerror()signalifan
errorhappened.Theerrorinformationisrepresentedusingthe
QNetworkSession::SessionErrortype.Alternatively,insteadofcheckingthe
opened()signal,youcanalsowatchthestateChanged()signal.The
possiblestatesforasessioncanbeInvalid,NotAvailable,Connecting,
Connected,Closing,Disconnected,andRoaming.
Ifyouwanttoopenthesessioninasynchronousway,call
waitForOpened()rightaftercallingopen().Itwillblocktheeventloopuntil
thesessionisopen.Thisfunctionwillreturntrueifsuccessfuland
falseotherwise.Tolimitthewaitingtime,youcandefineatime-out.
Justpassthemillisecondsthatyouarewillingtowaitasan
argumenttowaitForOpened().Tocheckwhetherasessionisopen,use
isOpen().
Toclosethesession,callclose().Ifnosessionisleftontheinterface,
itwillbeshotdown.Toforceaninterfacetodisconnect,callstop().
Thiscallwillinvalidateallthesessionsthatarebasedonthat
interface.
YoumayreceivethepreferredConfigurationChanged()signal,which
indicatesthatthepreferredconfiguration,thatis,forexample,the
preferredaccesspoint,haschanged.Thismaybethecaseifa
WLANnetworkisnowinrangeandyoudonothavetouse2G
anymore.Thenewconfigurationispassedasthefirstargument,
andthesecondoneindicateswhetherchangingthenewaccess
pointwillalsoaltertheIPaddress.Besidescheckingforthesignal,
youcanalsoinquirewhetherroamingisavailablefora
configurationbycallingQNetworkConfiguration::isRoamingAvailable().If
roamingisavailable,youhavetodecidetoeitherrejecttheofferby
callingignore()ortoacceptitbycallingmigrate().Ifyouaccept
roaming,itwillemitnewConfigurationActivated()whenthesessionis
roamed.Afteryouhavecheckedthenewconnection,youcaneither
acceptthenewaccesspointorrejectit.Thelattermeansthatyou
willreturntothepreviousaccesspoint.Ifyouacceptthenewaccess
point,thepreviousonewillbeterminated.
QNetworkInterface
Togettheinterfacethatisusedbyasession,call
QNetworkSession::interface().ItwillreturntheQNetworkInterfaceobject,
whichdescribestheinterface.WithhardwareAddress(),yougetthelow-
levelhardwareaddressoftheinterfacethatisnormallytheMAC
address.Thenameoftheinterfacecanbeobtainedbyname(),which
isastringsuchas"eth0"or"wlan0".AlistofIPaddressesaswellas
theirnetmasksandbroadcastaddressesregisteredwiththe
interfaceisreturnedbyaddressEntries().Furthermore,information
aboutwhethertheinterfaceisaloopbackorwhetheritsupports
multicastingcanbequeriedwithflags().Thereturnedbitmaskisa
combinationofthesevalues:IsUp,IsRunning,CanBroadcast,IsLoopBack,
IsPointToPoint,andCanMulticast.
Communicatingbetween
games
AfterhavingdiscussedQt'shigh-levelnetworkclassessuchas
QNetworkAccessManagerandQNetworkConfigurationManager,wewillnowtakea
lookatlower-levelnetworkclassesandseehowQtsupportsyou
whenitcomestoimplementingTCPorUDPserversandclients.
Thisbecomesrelevantwhenyouplantoextendyourgameby
includingamultiplayermode.Forsuchatask,QtoffersQTcpSocket,
QUdpSocket,andQTcpServer.
Timeforaction–Realizinga
simplechatprogram
TogetfamiliarwithQTcpServerandQTcpSocket,let'sdevelopasimple
chatprogram.Thisexamplewillteachyouthebasicknowledgeof
networkhandlinginQtsothatyoucanusethisskilllatertoconnect
twoormorecopiesofagame.Attheendofthisexercise,wewantto
seesomethinglikethis:
Onboththeleft-handsideandtheright-handsideofthepreceding
screenshot,youcanseeaclient,whereastheserverisinthemiddle.
We'llstartbytakingacloserlookattheserver.
Theserver–QTcpServer
Asaprotocolforcommunication,wewilluseTransmission
ControlProtocol(TCP).Youmayknowthisnetworkprotocol
fromthetwomostpopularinternetprotocols:HTTPandFTP.Both
useTCPfortheircommunicationandsodothegloballyused
protocolsforemailtraffic:SMTP,POP3,andIMAP.Themain
advantageofTCPisitsreliabilityandconnection-based
architecture.DatatransferredbyTCPisguaranteedtobecomplete,
ordered,andwithoutanyduplicates.Theprotocolisfurthermore
streamoriented,whichallowsustouseQDataStreamorQTextStream.A
downsidetoTCPisitsspeed.Thisisbecausethemissingdatahas
toberetransmitteduntilthereceiverfullyreceivesit.Bydefault,
thiscausesaretransmissionofallthedatathatwastransmitted
afterthemissingpart.So,youshouldonlychooseTCPasaprotocol
ifspeedisnotyourtoppriority,butratherthecompletenessand
correctnessofthetransmitteddata.Thisappliesifyousendunique
andnonrepetitivedata.
Timeforaction–Settingupthe
server
Alookattheserver'sGUIshowsusthatitprincipallyconsistsof
QPlainTextEdit(ui->log)thatisusedtodisplaysystemmessagesanda
button(ui->disconnectClients),whichallowsustodisconnectallthe
currentlyconnectedclients.Onthetop,nexttothebutton,the
server'saddressandportaredisplayed(ui->addressandui->port).After
settinguptheuserinterfaceintheconstructoroftheserver'sclass
TcpServer,weinitiatetheinternallyusedQTcpServer,whichisstoredin
them_serverprivatemembervariable:
if(!m_server->listen(QHostAddress::LocalHost,52693)){
ui->log->setPlainText(tr("Failurewhilestartingserver:%1")
.arg(m_server->errorString()));
return;
}
connect(m_server,&QTcpServer::newConnection,
this,&TcpServer::newConnection);
Whatjusthappened?
WithQTcpServer::listen(),wedefinedthattheservershouldlistento
thelocalhostandtheport52693fornewincomingconnections.The
valueusedhere,QHostAddress::LocalHostoftheQHostAddress::SpecialAddress
enumeration,willresolveto127.0.0.1.Instead,ifyoupass
QHostAddress::Any,theserverwilllistentoallIPv4interfacesaswellas
toIPv6interfaces.Ifyouonlywanttolistentoaspecificaddress,
justpassthisaddressasQHostAddress:
m_server->listen(QHostAddress("127.0.0.1"),0);
Thiswillbehaveliketheoneintheprecedingcode,onlyinthatthe
serverwillnowlistentoaportthatwillbechosenautomatically.On
success,listen()willreturntrue.So,ifsomethinggoeswronginthe
example,itwillshowanerrormessageonthetexteditandexitthe
function.Tocomposetheerrormessage,weareusing
QTcpServer::errorString(),whichholdsahuman-readableerrorphrase.
Tohandletheerrorinyourgame'scode,theerrorstringisnot
suitable.Inanycasewhereyouneedtoknowtheexacterror,use
QTcpServer::serverError(),whichreturnstheenumerationvalueof
QAbstractSocket::SocketError.Basedonthis,youknowexactlywhatwent
wrong,forexample,QAbstractSocket::HostNotFoundError.Iflisten()was
successful,weconnecttheserver'snewConnection()signaltotheclass's
newConnection()slot.Thesignalwillbeemittedeverytimeanew
connectionisavailable.Lastly,weshowtheserver'saddressand
portnumberthatcanbeaccessedthroughserverAddress()and
serverPort():
ui->address->setText(m_server->serverAddress().toString());
ui->port->setText(QString::number(m_server->serverPort()));
Thisinformationisrequiredbytheclientssothattheyareableto
connecttotheserver.
Timeforaction–Reactingona
newpendingconnection
Assoonasaclienttriestoconnecttotheserver,thenewConnection()
slotwillbecalled:
voidTcpServer::newConnection()
{
while(m_server->hasPendingConnections()){
QTcpSocket*socket=m_server->nextPendingConnection();
m_clients<<socket;
ui->disconnectClients->setEnabled(true);
connect(socket,&QTcpSocket::disconnected,
this,&TcpServer::removeConnection);
connect(socket,&QTcpSocket::readyRead,
this,&TcpServer::readyRead);
ui->log->appendPlainText(tr("*Newconnection:%1,port%2\n")
.arg(socket-
>peerAddress().toString())
.arg(socket->peerPort()));
}
}
Whatjusthappened?
Sincemorethanoneconnectionmaybepending,weuse
hasPendingConnections()todeterminewhetherthereisatleastonemore
pendingconnection.Eachoneisthenhandledintheiterationofthe
whileloop.TogetapendingconnectionoftheQTcpSockettype,wecall
nextPendingConnection()andaddthisconnectiontoaprivatevector
calledm_clients,whichholdsallactiveconnections.Inthenextline,
asthereisnowatleastoneconnection,weenablethebuttonthat
allowsallconnectionstobeclosed.Theslotconnectedtothe
button'sclick()signalwillcallQTcpSocket::close()oneachsingle
connection.Whenaconnectionisclosed,itssocketemitsa
disconnected()signal.WeconnectthissignaltoourremoveConnection()
slot.Withthelastconnection,wereacttothesocket'sreadyRead()
signal,whichindicatesthatnewdataisavailable.Insucha
situation,ourreadyRead()slotiscalled.Lastly,weprintasystem
messagethatanewconnectionhasbeenestablished.Theaddress
andportoftheconnectingclientandpeercanberetrievedbythe
socket'speerAddress()andpeerPort()functions.
Ifanewconnectioncan'tbeaccepted,theacceptError()signalisemittedinsteadof
newConnection().ItpassesthereasonforthefailureoftheQAbstractSocket::SocketErrortype
asanargument.Ifyouwanttotemporarilydeclinenewconnections,callpauseAccepting()
onQTcpServer.Toresumeacceptingnewconnections,callresumeAccepting().
Timeforaction–Forwardinga
newmessage
Whenaconnectedclientsendsanewchatmessage,theunderlying
socket—sinceitinheritsQIODevice—emitsreadyRead(),andthus,our
readyRead()slotwillbecalled.
Beforewetakealookatthisslot,thereissomethingimportantthat
youneedtokeepinmind.EventhoughTCPisorderedandwithout
anyduplicates,thisdoesnotmeanthatallthedataisdeliveredin
onebigchunk.So,beforeprocessingthereceiveddata,weneedto
ensurethatwegettheentiremessage.Unfortunately,thereis
neitheraneasywaytodetectwhetheralldatawastransmittednora
globallyusablemethodforsuchatask.Therefore,itisuptoyouto
solvethisproblem,asitdependsontheusecase.Twocommon
solutions,however,aretoeithersendmagictokenstoindicatethe
startandendofamessage,forexample,singlecharactersorXML
tags,oryoucansendthesizeofthemessageupfront.
ThesecondsolutionisshownintheQtdocumentationwherethe
lengthisputinaquint16infrontofthemessage.We,ontheother
hand,willlookatanapproachthatusesasimplemagictokento
handlethemessagescorrectly.Asadelimiter,weusethe"Endof
TransmissionBlock"character—ASCIIcode23—toindicatetheend
ofamessage.WealsochooseUTF-8astheencodingoftransmitted
messagestoensurethatclientswithdifferentlocalescan
communicatewitheachother.
Sincetheprocessingofreceiveddataisquitecomplex,wewillgo
throughthecodestepbystepthistime:
voidTcpServer::readyRead()
{
{
QTcpSocket*socket=qobject_cast<QTcpSocket*>(sender());
if(!socket){
return;
}
//...
}
Todeterminewhichsocketcalledtheslot,weusesender().Ifthecast
toQTcpSocketisunsuccessful,weexittheslot.
Notethatsender()isusedforsimplicity.Ifyouwritereal-lifecode,itisbettertouse
QSignalMapper.
Next,wereadthetransferred—potentiallyfragmentary—message
withreadAll():
QByteArray&buffer=m_receivedData[socket];
buffer.append(socket->readAll());
Here,QHash<QTcpSocket*,QByteArray>m_receivedDataisaprivateclass
memberwherewestorethepreviouslyreceiveddataforeach
connection.Whenthefirstchunkofdataisreceivedfromaclient,
m_receivedData[socket]willautomaticallyinsertanemptyQByteArrayinto
thehashandreturnareferencetoit.Onsubsequentcalls,itwill
returnareferencetothesamearray.Weuseappend()toappendthe
newlyreceiveddatatotheendofthearray.Finally,weneedto
identifythemessagesthatwerecompletelyreceivedbynow,ifthere
areanysuchmessages:
while(true){
intendIndex=buffer.indexOf(23);
if(endIndex<0){
break;
}
QStringmessage=QString::fromUtf8(buffer.left(endIndex));
buffer.remove(0,endIndex+1);
newMessage(socket,message);
}
Oneachiterationoftheloop,wetrytofindthefirstseparator
character.Ifwedidn'tfindone(endIndex<0),weexittheloop,and
leavetheremainingpartialmessageinm_receivedData.Ifwefounda
separator,wetakethefirstmessage'sdatausingtheleft(endIndex)
functionthatreturnstheleftmostendIndexbytesfromthearray.To
removethefirstmessagefrombuffer,weusetheremove()functionthat
willremovethespecifiednumberofbytes,shiftingtheremaining
bytestotheleft.WewanttoremoveendIndex+1bytes(themessage
itselfandtheseparatorafterit).Followingourtransmission
protocol,weinterpretthedataasUTF-8andcallournewMessage()
functionthatwillhandlethereceivedmessage.
InthenewMessage()function,weappendthenewmessagetotheserver
logandsendittoallclients:
voidTcpServer::newMessage(QTcpSocket*sender,constQString&message)
{
ui->log->appendPlainText(tr("Sendingmessage:
%1\n").arg(message));
QByteArraymessageArray=message.toUtf8();
messageArray.append(23);
for(QTcpSocket*socket:m_clients){
if(socket->state()==QAbstractSocket::ConnectedState){
socket->write(messageArray);
}
}
Q_UNUSED(sender)
}
Inthisfunction,weencodethemessageaccordingtoour
transmissionprotocol.First,weusetoUtf8()toconvertQStringto
QByteArrayinUTF-8encoding.Next,weappendtheseparator
character.Finally,weiterateoverthelistofclients,checkwhether
theyarestillconnected,andsendtheencodedmessagetothem.
SincethesocketinheritsQIODevice,youcanusemostofthefunctions
thatyouknowfromQFile.Thecurrentbehaviorofourserverisvery
simple,sowehavenouseforthesenderargument,soweadd
theQ_UNUSEDmacrotosuppresstheunusedargumentwarning.
Haveagohero–Using
QSignalMapper
Asdiscussedearlier,usingsender()isaconvenient,butnotanobject-
oriented,approach.Thus,trytouseQSignalMapperinsteadtodetermine
whichsocketcalledtheslot.Toachievethis,youhavetoconnectthe
socket'sreadyRead()signaltoamapperandtheslotdirectly.Allthe
signal-mapper-relatedcodewillgointothenewConnection()slot.
ThesameappliestotheconnectiontotheremoveConnection()slot.Let's
takealookatitnext.
Timeforaction–Detectinga
disconnect
Whenaclientterminatestheconnection,wehavetodeletethe
socketfromthelocalm_clientslist.Thesocket'sdisconnected()signalis
alreadyconnectedtotheremoveConnection()slot,sowejustneedto
implementitasfollows:
voidTcpServer::removeConnection()
{
QTcpSocket*socket=qobject_cast<QTcpSocket*>(sender());
if(!socket){
return;
}
ui->log->appendPlainText(tr("*Connectionremoved:%1,port%2\n")
.arg(socket->peerAddress().toString())
.arg(socket->peerPort()));
m_clients.removeOne(socket);
m_receivedData.remove(socket);
socket->deleteLater();
ui->disconnectClients->setEnabled(!m_clients.isEmpty());
}
Whatjusthappened?
Aftergettingthesocketthatemittedthecallthroughsender(),we
posttheinformationthatasocketisbeingremoved.Then,we
removethesocketfromm_clients,removetheassociatedbufferfrom
m_receivedDataandcalldeleteLater()onit.Donotusedelete.Lastly,ifno
clientisleft,thedisconnectbuttonisdisabled.
Theserverisready.Nowlet'stakealookattheclient.
Theclient
TheGUIoftheclient(TcpClient)isprettysimple.Ithasthreeinput
fieldstodefinetheserver'saddress(ui->address),theserver'sport(ui-
>port),andausername(ui->user).Ofcourse,thereisalsoabuttonto
connectto(ui->connect)anddisconnectfrom(ui->disconnect)theserver.
Finally,theGUIhasatexteditthatholdsthereceivedmessages(ui-
>chat)andalineedit(ui->text)tosendmessages.
Timeforaction–Settingupthe
client
Afterprovidingtheserver'saddressandportandchoosinga
username,theusercanconnecttotheserver:
voidTcpClient::on_connect_clicked()
{
//...
if(m_socket->state()!=QAbstractSocket::ConnectedState){
ui->chat->appendPlainText(tr("==Connecting..."));
m_socket->connectToHost(ui->address->text(),ui->port-
>value());
//...
}
}
Whatjusthappened?
Them_socketprivatemembervariableholdsaninstanceofQTcpSocket.If
thissocketisalreadyconnected,nothinghappens.Otherwise,the
socketisconnectedtothegivenaddressandportbycalling
connectToHost().Besidestheobligatoryserveraddressandport
number,youcanpassathirdargumenttodefinethemodeinwhich
thesocketwillbeopened.Forpossiblevalues,youcanuseOpenMode
justlikewedidforQIODevice.
Sincethiscallisasynchronous,weprintanotificationtothechatso
thattheuserisinformedthattheapplicationiscurrentlytryingto
connecttotheserver.Whentheconnectionisestablished,the
socketsendstheconnected()signalthatprints"Connectedtoserver"
onthechattoindicatethatwehaveconnectedtoaslot.Besidesthe
messagesinthechat,wealsoupdatedtheGUIby,forexample,
disablingtheconnectbutton,butthisisallbasicstuff.Youwon't
haveanytroubleunderstandingthisifyouhavehadalookatthe
sources.So,thesedetailsareleftouthere.
Ofcourse,somethingcouldgowrongwhentryingtoconnecttoa
server,butluckily,weareinformedaboutafailureaswellthrough
theerror()signal,passingadescriptionoferrorintheformof
QAbstractSocket::SocketError.Themostfrequenterrorswillprobablybe
QAbstractSocket::ConnectionRefusedErrorifthepeerrefusedtheconnection
orQAbstractSocket::HostNotFoundErrorifthehostaddresscouldnotbe
found.Iftheconnection,however,wassuccessfullyestablished,it
shouldbeclosedlateron.Youcaneithercallabort()toimmediately
closethesocket,whereasdisconnectFromHost()willwaituntilallpending
datahasbeenwritten.
Timeforaction–Receiving
textmessages
Intheconstructor,wehaveconnectedthesocket'sreadyRead()signal
toalocalslot.So,whenevertheserversendsamessagethrough
QTcpSocket::write(),wereadthedataanddecodeit:
m_receivedData.append(m_socket->readAll());
while(true){
intendIndex=m_receivedData.indexOf(23);
if(endIndex<0){
break;
}
QStringmessage=
QString::fromUtf8(m_receivedData.left(endIndex));
m_receivedData.remove(0,endIndex+1);
newMessage(message);
}
ThiscodeisverysimilartothereadyRead()slotoftheserver.It'seven
simplerbecauseweonlyhaveonesocketandonedatabuffer,so
m_receivedDataisasingleQByteArray.ThenewMessage()implementationin
theclientisalsomuchsimplerthanintheserver:
voidTcpClient::newMessage(constQString&message)
{
ui->chat->appendPlainText(message);
}
Here,wejustneedtodisplaythereceivedmessagetotheuser.
Timeforaction–Sendingtext
messages
Whatisleftnowistodescribehowtosendachatmessage.On
hittingreturnbuttoninsidethelineedit,alocalslotwillbecalled
thatcheckswhetherthereisactualtexttosendandwhetherm_socket
isstillconnected.Ifeverythingisready,weconstructamessage
thatcontainstheself-givenusername,acolon,andthetextofthe
lineedit:
QStringmessage=QStringLiteral("%1:%2")
.arg(m_user).arg(ui->text->text());
Then,weencodeandsendthemessage,justlikewedidonthe
serverside:
QByteArraymessageArray=message.toUtf8();
messageArray.append(23);
m_socket->write(messageArray);
That'sall.It'slikewritingandreadingfromafile.Forthecomplete
example,takealookatthesourcesbundledwiththisbookandrun
theserverandseveralclients.
Youcanseethattheserverandclientshareasignificantamountofcode.Inarealproject,
youdefinitelywanttoavoidsuchduplication.Youcanmoveallrepeatingcodetoa
commonlibraryusedbybothserverorclient.Alternatively,youcanimplementserverand
clientinasingleprojectandenabletheneededfunctionalityusingcommand-line
argumentsorconditionalcompilation.
Haveagohero–Extendingthe
chatserverandclient
Thisexamplehasshownushowtosendasimpletext.Ifyounowgo
onanddefineaschemaforhowthecommunicationshouldwork,
youcanuseitasabaseformorecomplexcommunication.For
instance,ifyouwanttoenabletheclienttoreceivealistofallother
clients(andtheirusernames),youneedtodefinethattheserverwill
returnsuchalistifitgetsaspecialmessagefromaclient.Youcan
usespecialtextcommandssuchas/allClients,oryoucanimplement
amorecomplexmessagestructureusingQDataStreamorJSON
serialization.Therefore,youhavetoparseallmessagesreceivedby
theserverbeforeforwardingthemtoalltheconnectedclients.Go
aheadandtrytoimplementsucharequirementyourself.
Bynow,itispossiblethatmultipleusershavechosenthesame
username.Withthenewfunctionalityofgettingauserlist,youcan
preventthisfromhappening.Therefore,youhavetosendthe
usernametotheserverthatkeepstrackofthem.Inthecurrent
implementation,nothingstopstheclientfromsendingmessages
underadifferentusernameeachtime.Youcanmaketheserver
handleusernamesinsteadoftrustingtheclient'seachmessage.
Synchronousnetwork
operations
Theexampleweexplainedusesanonblocking,asynchronous
approach.Forexample,afterasynchronouscallssuchas
connectToHost(),wedonotblockthethreaduntilwegetaresult,but
instead,weconnecttothesocket'ssignalstoproceed.Onthe
InternetaswellasQt'sdocumentation,ontheotherhand,youwill
finddozensofexamplesexplainingtheblockingandthe
synchronousapproaches.Youwilleasilyspotthembytheiruseof
waitFor...()functions.Thesefunctionsblockthecurrentthreaduntil
afunctionsuchasconnectToHost()hasaresult—thetimeconnected()or
error()willbeemitted.Thecorrespondingblockingfunctionto
connectToHost()iswaitForConnected().Theotherblockingfunctionsthat
canbeusedarewaitForReadyRead(),whichwaitsuntilnewdatais
availableonasocketforreading;waitForBytesWritten(),whichwaits
untilthedatahasbeenwrittentothesocket;andwaitForDisconnected(),
whichwaitsuntiltheconnectionhasbeenclosed.
Lookout!EvenifQtoffersthesewaitFor...()functions,donotuse
them!Thesynchronousapproachisnotthesmartestone,sinceit
willfreezeyourgame'sGUI.AfrozenGUIistheworstthingthat
canhappeninyourgame,anditwillannoyeveryuser.So,when
workinginsidetheGUIthread,youarebettertoreacttothe
QIODevice::readyRead(),QIODevice::bytesWritten(),QAbstractSocket::connected(),
andQAbstractSocket::disconnected()signals.
QAbstractSocketisthebaseclassofQTcpSocketaswellasofQUdpSocket.
Followingtheasynchronousapproachshown,theapplicationwill
onlybecomeunresponsivewhileyourownslotsarebeingexecuted.
Ifyourslotscontainmoreheavycomputations,youwillneedto
movethemtoanextrathread.Then,theGUIthreadwillonlyget
signals,passingthenewmessages,andtosend,itwillsimplypass
therequireddatatotheworkerthread.Thisway,youwillgeta
superfluentvelvetGUI.
UsingUDP
IncontrasttoTCP,UDPisunreliableandconnectionless.Neither
theorderofpacketsnortheirdeliveryisguaranteed.These
limitations,however,allowUDPtobeveryfast.So,ifyouhave
frequentdata,whichdoesnotnecessarilyneedtobereceivedbythe
peer,useUDP.Thisdatacould,forexample,bereal-timepositions
ofaplayerthatgetupdatedfrequentlyorlivevideo/audio
streaming.SinceQUdpSocketismostlythesameasQTcpSocket—both
inheritQAbstractSocket—thereisnotmuchtoexplain.Themain
differencebetweenthemisthatTCPisstream-oriented,whereas
UDPisdatagram-oriented.Thismeansthatthedataissentinsmall
packages,containingamongtheactualcontent,thesender'saswell
asthereceiver'sIPaddressandportnumber.
UnlikeQTcpSocketandQTcpServer,UDPdoesnotneedaseparateserver
classbecauseitisconnectionless.AsingleQUdpSocketcanbeusedasa
server.Inthiscase,youhavetouseQAbstractSocket::bind()insteadof
QTcpServer::listen().Likelisten(),bind()takestheaddressesandports
thatareallowedtosenddatagramsasarguments.NotethatTCP
portsandUDPportsarecompletelyunrelatedtoeachother.
Wheneveranewpackagearrives,theQIODevice::readyRead()signalis
emitted.Toreadthedata,usethereceiveDatagram()orreadDatagram()
function.ThereceiveDatagram()functionacceptsanoptionalmaxSize
argumentthatallowsyoutolimitthesizeofthereceiveddata.This
functionreturnsaQNetworkDatagramobjectthatcontainsthedatagram
andhasanumberofmethodstogetthedata.Themostusefulof
themaredata(),whichreturnsthepayloadasaQByteArrayaswellas
senderAddress()andsenderPort()thatallowyoutoidentifythesender.
ThereadDatagram()functionisamorelow-levelfunctionthattakesfour
parameters.Thefirstoneofthechar*typeisusedtowritethedata
in,thesecondspecifiestheamountofbytestobewritten,andthe
lasttwoparametersoftheQHostAddress*andquint16*typesareusedto
storethesender'sIPaddressandportnumber.Thisfunctionisless
convenient,butyoucanuseitmoreefficientlythanreceiveDatagram(),
becauseit'spossibletousethesamedatabufferforalldatagrams
insteadofallocatinganewoneforeachdatagram.
QUdpSocketalsoprovidestheoverloadedwriteDatagram()functionfor
sendingthedata.Oneoftheoverloadssimplyacceptsa
QNetworkDatagramobject.Youcanalsosupplythedataintheformof
QByteArrayorachar*bufferwithasize,butinthesecases,youalso
needtospecifytherecipient'saddressandportnumberasseparate
arguments.
Timeforaction–Sendinga
textviaUDP
Asanexample,let'sassumethatwehavetwosocketsoftheQUdpSocket
type.WewillcallthefirstonesocketAandtheothersocketB.Bothare
boundtothelocalhost,socketAtothe52000portandsocketBtothe52001
port.So,ifwewanttosendthestringHello!fromsocketAtosocketB,we
havetowriteintheapplicationthatisholdingsocketA:
socketA->writeDatagram(QByteArray("Hello!"),
QHostAddress("127.0.0.1"),52001);
TheclassthatholdssocketBmusthavethesocket'sreadyRead()signal
connectedtoaslot.Thisslotwillthenbecalledbecauseofour
writeDatagram()call,assumingthatthedatagramisnotlost!Intheslot,
wereadthedatagramandthesender'saddressandportnumber
with:
while(socketB->hasPendingDatagrams()){
QNetworkDatagramdatagram=socketB->receiveDatagram();
qDebug()<<"receiveddata:"<<datagram.data();
qDebug()<<"from:"<<datagram.senderAddress()
<<datagram.senderPort();
}
Aslongastherearependingdatagrams—thisischeckedby
hasPendingDatagrams()—wereadthemusingthehigh-levelQNetworkDatagram
API.Afterthedatagramwasreceived,weusethegetterfunctionsto
readthedataandidentifythesender.
Haveagohero–Connecting
playersoftheBenjamingame
Withthisintroductoryknowledge,youcangoaheadandtrysome
stuffbyyourself.Forexample,youcantakethegameBenjaminthe
elephantandsendBenjamin'scurrentpositionfromoneclientto
another.Thisway,youcaneitherclonethescreenfromoneclientto
theother,orbothclientscanplaythegameand,additionally,can
seewheretheelephantoftheotherplayercurrentlyis.Forsucha
task,youwoulduseUDP,asitisimportantthatthepositionis
updatedveryfastwhileitisn'tadisasterwhenonepositiongets
lost.
Keepinmindthatweonlyscratchedthesurfaceofnetworkingduetoitscomplexity.
Coveringitfullywouldhaveexceededthisbeginner'sguide.Forarealgame,whichusesa
network,youshouldlearnmoreaboutQt'spossibilitiesforestablishingasecureconnection
viaSSLorsomeothermechanism.
Popquiz
Q1.Whichclasscanyouusetoreadthedatareceivedoverthe
network?
1. QNetworkReply
2. QNetworkRequest
3. QNetworkAccessManager
Q2.WhatshouldyouusuallydowiththeQNetworkReply*replyobjectin
thefinished()signalhandler?
1. Deleteitusingdeletereply
2. Deleteitusingreply->deleteLater()
3. Don'tdeleteit
Q3.Howtoensurethatyourapplicationwon'tfreezebecauseof
processinganHTTPrequest?
1. UsewaitForConnected()orwaitForReadyRead()functions
2. UsereadyRead()orfinished()signals
3. MoveQNetworkAccessManagertoaseparatethread
Q4.WhichclasscanyouusetocreateaUDPserver?
1. QTcpServer
2. QUdpServer
3. QUdpSocket
Summary
Inthefirstpartofthischapter,youfamiliarizedyourselfwith
QNetworkAccessManager.Thisclassisattheheartofyourcodewhenever
youwanttodownloadoruploadfilestotheinternet.Afterhaving
gonethroughthedifferentsignalsthatyoucanusetofetcherrors,
togetnotifiedaboutnewdataortoshowtheprogress,youshould
nowknoweverythingyouneedonthattopic.
TheexampleabouttheDistanceMatrixAPIdependedonyour
knowledgeofQNetworkAccessManager,anditshowsyouareal-life
applicationcaseforit.DealingwithJSONastheserver'sreply
formatwasasummaryofChapter4,QtCoreEssentials,butitwas
highlyneededsinceFacebookandTwitteronlyuseJSONtoformat
theirnetworkreplies.
Inthelastsection,youlearnedhowtosetupyourownTCPserver
andclients.Thisenablesyoutoconnectdifferentinstancesofa
gametoprovidethemultiplayerfunctionality.Alternatively,you
weretaughthowtouseUDP.
YouarenowfamiliarwithQtwidgets,theGraphicsView
framework,thecoreQtclasses,andthenetworkingAPI.This
knowledgewillalreadyallowyoutoimplementgameswithrichand
advancedfunctionality.TheonlylargeandsignificantpartofQtwe
willexploreisQtQuick.However,beforewegettothat,let's
consolidateourknowledgeofwhatwealreadyknowandinvestigate
someadvancedtopics.
Nowwearereturningtotheworldofwidgets.InChapter3,QtGUI
Programming,weonlyusedthewidgetclassesprovidedbyQt.In
thenextchapter,youwilllearntocreateyourownwidgetsand
integratethemintoyourforms.
CustomWidgets
Wehavesofarbeenusingonlyready-madewidgetsfortheuser
interface,whichresultedinthecrudeapproachofusingbuttonsfor
atic-tac-toegame.Inthischapter,youwilllearnaboutmuchof
whatQthastoofferwithregardtocustomwidgets.Thiswillletyou
implementyourownpaintingandeventhandling,incorporating
contentthatisentirelycustomized.
Themaintopicscoveredinthischapterareasfollows:
WorkingwithQPainter
Creatingcustomwidgets
Imagehandling
Implementingachessgame
Rasterandvectorgraphics
Whenitcomestographics,Qtsplitsthisdomainintotwoseparate
parts.Oneofthemisrastergraphics(usedbywidgetsandthe
GraphicsView,forexample).Thispartfocusesonusinghigh-level
operations(suchasdrawinglinesorfillingrectangles)to
manipulatecolorsofagridofpointsthatcanbevisualizedon
differentdevices,suchasimages,printers,orthedisplayofyour
computerdevice.Theotherisvectorgraphics,whichinvolves
manipulatingvertices,triangles,andtextures.Thisistailoredfor
maximumspeedofprocessinganddisplay,usinghardware
accelerationprovidedbymoderngraphicscards.
Qtabstractsgraphicsusingtheconceptofasurface(representedby
theQSurfaceclass)thatitdrawson.Thetypeofthesurface
determineswhichdrawingoperationscanbeperformedonthe
surface:surfacesthatsupportsoftwarerenderingandraster
graphicshavetheRasterSurfacetype,andsurfacesthatsupportthe
OpenGLinterfacehavetheOpenGLSurfacetype.Inthischapter,youwill
deepenyourknowledgeofQt'srasterpaintingsystem.Wewillcome
backtothetopicofOpenGLinthenextchapter.
QSurfaceobjectscanhaveothertypes,buttheyareneededlessoften.RasterGLSurfaceis
intendedforinternalQtuse.OpenVGSurfacesupportsOpenVG(ahardwareaccelerated2D
vectorgraphicsAPI)andisusefulonembeddeddevicesthatsupportOpenVGbutlack
OpenGLsupport.Qt5.10introducesVulkanSurface,whichsupportsVulkangraphicsAPI.
Rasterpainting
WhenwetalkaboutGUIframeworks,rasterpaintingisusually
associatedwithdrawingonwidgets.However,sinceQtis
somethingmorethanaGUItoolkit,thescopeofrasterpaintingthat
itoffersismuchbroader.
Ingeneral,Qt'sdrawingarchitectureconsistsofthreeparts.The
mostimportantpartisthedevicethedrawingtakesplaceon,
representedbytheQPaintDeviceclass.Qtprovidesanumberofpaint
devicesubclasses,suchasQWidgetorQImageandQPrinterorQPdfWriter.You
canseethattheapproachfordrawingonawidgetandprintingona
printerisquitethesame.Thedifferenceisinthesecondcomponent
ofthearchitecture—thepaintengine(QPaintEngine).Theengineis
responsibleforperformingtheactualpaintoperationsona
particularpaintdevice.Differentpaintenginesareusedtodrawon
imagesandtoprintonprinters.Thisiscompletelyhiddenfrom
you,asadeveloper,soyoureallydon'tneedtoworryaboutit.
Foryou,themostimportantpieceisthethirdcomponent—QPainter—
whichisanadapterforthewholepaintingframework.Itcontainsa
setofhigh-leveloperationsthatcanbeinvokedonthepaintdevice.
Behindthescenes,thewholeworkisdelegatedtoanappropriate
paintengine.Whiletalkingaboutpainting,wewillbefocusing
solelyonthepainterobject,asanypaintingcodecanbeinvokedon
anyofthetargetdevicesonlybyusingapainterinitializedona
differentpaintdevice.ThiseffectivelymakespaintinginQtdevice
agnostic,asinthefollowingexample:
voiddoSomePainting(QPainter*painter){
painter->drawLine(QPoint(0,0),QPoint(100,40));
}
Thesamecodecanbeexecutedonapainterworkingonany
possibleQPaintDeviceclass,beitawidget,animage,oranOpenGL
context(throughtheuseofQOpenGLPaintDevice).We'vealreadyseen
QPainterinactioninChapter4,Custom2DGraphicswithGraphics
View,whenwecreatedacustomgraphicsitem.Now,let'slearn
moreaboutthisimportantclass.
TheQPainterclasshasarichAPI.Themostimportantmethodsinthis
classcanbedividedintothreegroups:
Settersandgettersforattributesofthepainter
Methods,withnamesstartingwithdrawandfill,that
performdrawingoperationsonthedevice
Methodsthatallowmanipulatingthecoordinatesystemof
thepainter
Painterattributes
Let'sstartwiththeattributes.Thethreemostimportantonesare
thepen,brush,andfont.Thepenholdspropertiesoftheoutline
drawnbythepainter,andthebrushdetermineshowitwillfill
shapes.We'vealreadydescribedpensandbrushesinChapter
4,Custom2DGraphicswithGraphicsView,soyoushouldalready
understandhowtoworkwiththem.
ThefontattributeisaninstanceoftheQFontclass.Itcontainsalarge
numberofmethodsforcontrollingfontparameterssuchasfont
family,style(italicoroblique),fontweight,andfontsize(eitherin
pointsordevice-dependentpixels).Alltheparametersareself-
explanatory,sowewillnotdiscussthemhereindetail.Itis
importanttonotethatQFontcanuseanyfontinstalledonthesystem.
Incasemorecontroloverfontsisrequiredorafontthatisnot
installedinthesystemneedstobeused,youcantakeadvantageof
theQFontDatabaseclass.Itprovidesinformationabouttheavailable
fonts(suchaswhetheraparticularfontisscalableorbitmapor
whatwritingsystemsitsupports)andallowsaddingnewfontsinto
theregistrybyloadingtheirdefinitionsdirectlyfromfiles.
Animportantclass,whenitcomestofonts,istheQFontMetricsclass.It
allowscalculatinghowmuchspaceisneededtopaintparticulartext
usingafontorcalculatestexteliding.Themostcommonusecaseis
tocheckhowmuchspacetoallocateforaparticularuser-visible
string;considerthisexample:
QFontMetricsfm=painter.fontMetrics();
QRectrect=fm.boundingRect("GameProgrammingusingQt");
ThisisespeciallyusefulwhentryingtodeterminesizeHintfora
widget.
Coordinatesystems
Thenextimportantaspectofthepainterisitscoordinatesystem.
Thepainterinfacthastwocoordinatesystems.Oneisitsown
logicalcoordinatesystemthatoperatesonrealnumbers,andthe
otheristhephysicalcoordinatesystemofthedevicethepainter
operateson.Eachoperationonthelogicalcoordinatesystemis
mappedtophysicalcoordinatesinthedeviceandappliedthere.
Let'sstartwithexplainingthelogicalcoordinatesystemfirst,and
thenwe'llseehowthisrelatestophysicalcoordinates.
Thepainterrepresentsaninfinitecartesiancanvas,withthe
horizontalaxispointingrightandtheverticalaxispointingdownby
default.Thesystemcanbemodifiedbyapplyingaffine
transformationstoit—translating,rotating,scaling,andshearing.
Thisway,youcandrawananalogclockfacethatmarkseachhour
withalinebyexecutingaloopthatrotatesthecoordinatesystemby
30degreesforeachhouranddrawsalinethatisverticalinthe
newly-obtainedcoordinatesystem.Anotherexampleiswhenyou
wishtodrawasimpleplotwithanxaxisgoingrightandayaxis
goingup.Toobtainthepropercoordinatesystem,youwouldscale
thecoordinatesystemby−1intheverticaldirection,effectively
reversingthedirectionoftheverticalaxis.
Whatwedescribedheremodifiestheworldtransformationmatrix
forthepainterrepresentedbyaninstanceoftheQTransformclass.You
canalwaysquerythecurrentstateofthematrixbycallingtransform()
onthepainter,andyoucansetanewmatrixbycallingsetTransform().
QTransformhasmethodssuchasscale(),rotate(),andtranslate()that
modifythematrix,butQPainterhasequivalentmethodsfor
manipulatingtheworldmatrixdirectly.Inmostcases,usingthese
wouldbepreferable.
Eachpaintingoperationisexpressedinlogicalcoordinates,goes
throughtheworldtransformationmatrix,andreachesthesecond
stageofcoordinatemanipulation,whichistheviewmatrix.The
painterhastheconceptofviewport()andwindow()rectangles.The
viewportrectanglerepresentsthephysicalcoordinatesofanarbitrary
rectangle,whilethewindowrectangleexpressesthesamerectanglebut
inlogicalcoordinates.Mappingonetoanothergivesa
transformationthatneedstobeappliedtoeachdrawnprimitiveto
calculatetheareaofthephysicaldevicethatistobepainted.
Bydefault,thetworectanglesareidenticaltotherectangleofthe
underlyingdevice(thus,nowindow–viewportmappingisdone).Such
transformationisusefulifyouwishtoperformpaintingoperations
usingmeasurementunitsotherthanthepixelsofthetargetdevice.
Forexample,ifyouwanttoexpresscoordinatesusingpercentages
ofthewidthandheightofthetargetdevice,youwouldsetboththe
windowwidthandheightto100.Then,todrawalinestartingat20%
ofthewidthand10%oftheheightandendingat70%ofthewidth
and30%oftheheight,youwouldtellthepaintertodrawtheline
between(20,10)and(70,30).Ifyouwantedthosepercentagesto
applynottothewholeareaofanimagebuttoitslefthalf,you
wouldsettheviewportrectangleonlytothelefthalfoftheimage.
Settingthewindowandviewportrectanglesonlydefinescoordinatemapping;itdoesnot
preventdrawingoperationsfrompaintingoutsidetheviewportrectangle.Ifyouwantsuch
behavior,youhavetoenableclippinginthepainteranddefinetheclippingregionor
path.
Drawingoperations
Onceyouhavethepainterproperlyset,youcanstartissuing
paintingoperations.QPainterhasarichsetofoperationsfordrawing
differentkindsofprimitives.Alloftheseoperationshavethedraw
prefixintheirnames,followedbythenameoftheprimitivethatis
tobedrawn.Thus,operationssuchasdrawLine,drawRoundedRect,and
drawTextareavailablewithanumberofoverloadsthatusuallyallow
ustoexpresscoordinatesusingdifferentdatatypes.Thesemaybe
purevalues(eitherintegerorreal),Qt'sclasses,suchasQPointand
QRect,ortheirfloatingpointequivalents—QPointFandQRectF.Each
operationisperformedusingcurrentpaintersettings(font,pen,
andbrush).
RefertothedocumentationoftheQPainterclassforthelistofalldrawingoperations.
Beforeyoustartdrawing,youhavetotellthepainterwhichdevice
youwishtodrawon.Thisisdoneusingthebegin()andend()
methods.TheformeracceptsapointertoaQPaintDeviceinstanceand
initializesthedrawinginfrastructure,andthelattermarksthe
drawingascomplete.Usually,wedon'thavetousethesemethods
directly,astheconstructorofQPaintercallsbegin()forus,andthe
destructorinvokesend().
Thus,thetypicalworkflowistoinstantiateapainterobject,passit
tothedevice,thendothedrawingbycallingthesetanddraw
methods,andfinallyletthepainterbedestroyedbygoingoutof
scope,asfollows:
{
QPainterpainter(this);//paintonthecurrentobject
QPenpen(Qt::red);
pen.setWidth(2);
painter.setPen(pen);
painter.setPen(pen);
painter.setBrush(Qt::yellow);
painter.drawRect(0,0,100,50);
}
Wewillcovermoremethodsfromthedrawfamilyinthefollowing
sectionsofthischapter.
Creatingacustomwidget
Itistimetoactuallygetsomethingontothescreenbypaintingona
widget.Awidgetisrepaintedasaresultofreceivingapaintevent,
whichishandledbyreimplementingthepaintEvent()virtual
method.Thismethodacceptsapointertotheeventobjectof
theQPaintEventtypethatcontainsvariousbitsofinformationabout
therepaintrequest.Rememberthatyoucanonlypaintonthe
widgetfromwithinthatwidget'spaintEvent()call.
Timeforaction–Custom-
paintedwidgets
Let'simmediatelyputournewskillsintopractice!Startbycreating
anewQtWidgetsApplicationinQtCreator,choosingQWidgetasthe
baseclass,andensuringthattheGenerateFormboxisunchecked.
ThenameofourwidgetclasswillbeWidget.
Switchtotheheaderfileforthenewlycreatedclass,addaprotected
sectiontotheclass,andtypevoidpaintEventinthatsection.Then,
pressCtrl+SpaceonyourkeyboardandCreatorwillsuggestthe
parametersforthemethod.Youshouldendupwiththefollowing
code:
protected:
voidpaintEvent(QPaintEvent*);
Creatorwillleavethecursorpositionedrightbeforethesemicolon.
PressingAlt+Enterwillopentherefactoringmenu,lettingyouadd
thedefinitionintheimplementationfile.Thestandardcodefora
painteventisonethatinstantiatesapainteronthewidget,as
shown:
voidWidget::paintEvent(QPaintEvent*)
{
QPainterpainter(this);
}
Ifyourunthiscode,thewidgetwillremainblank.Nowwecanstart
addingtheactualpaintingcodethere:
voidWidget::paintEvent(QPaintEvent*)
voidWidget::paintEvent(QPaintEvent*)
{
QPainterpainter(this);
QPenpen(Qt::black);
pen.setWidth(4);
painter.setPen(pen);
QRectr=rect().adjusted(10,10,-10,-10);
painter.drawRoundedRect(r,20,10);
}
Buildandrunthecode,andyou'llobtainthefollowingoutput:
Whatjusthappened?
First,wesetafourpixelswideblackpenforthepainter.Then,we
calledrect()toretrievethegeometryrectangleofthewidget.By
callingadjusted(),wereceiveanewrectanglewithitscoordinates(in
theleft,top,right,andbottomorder)modifiedbythegiven
arguments,effectivelygivingusarectanglewitha10pixelmargin
oneachside.
Qtusuallyofferstwomethodsthatallowustoworkwithmodifieddata.Callingadjusted()
returnsanewobjectwithitsattributesmodified,whileifwehadcalledadjust(),the
modificationwouldhavebeendoneinplace.Payspecialattentiontowhichmethodyouuse
toavoidunexpectedresults.It'sbesttoalwayscheckthereturnvalueforamethod—
whetheritreturnsacopyorvoid.
Finally,wecalldrawRoundedRect(),whichpaintsarectanglewithits
cornersroundedbythenumberofpixels(inthex,yorder)givenas
thesecondandthirdargument.Ifyoulookclosely,youwillnote
thattherectanglehasnastyjaggedroundedparts.Thisiscausedby
theeffectofaliasing,wherealogicallineisapproximatedusingthe
limitedresolutionofthescreen;duetothis,apixeliseitherfully
drawnornotdrawnatall.AswelearnedinChapter4,Custom2D
GraphicswithGraphicsView,Qtoffersamechanismcalledanti-
aliasingtocounterthiseffectusingintermediatepixelcolorswhere
appropriate.Youcanenablethismechanismbysettingaproper
renderhintonthepainterbeforeyoudrawtheroundedrectangle,
asshown:
voidWidget::paintEvent(QPaintEvent*)
{
QPainterpainter(this);
painter.setRenderHint(QPainter::Antialiasing,true);
//...
}
Nowyou'llgetthefollowingoutput:
Ofcourse,thishasanegativeimpactonperformance,souseanti-
aliasingonlywherethealiasingeffectisnoticeable.
Timeforaction–Transforming
theviewport
Let'sextendourcodesothatallfutureoperationsfocusonlyon
drawingwithintheborderboundariesaftertheborderisdrawn.
Usethewindowandviewporttransformation,asfollows:
voidWidget::paintEvent(QPaintEvent*){
QPainterpainter(this);
painter.setRenderHint(QPainter::Antialiasing,true);
QPenpen(Qt::black);
pen.setWidth(4);
painter.setPen(pen);
QRectr=rect().adjusted(10,10,-10,-10);
painter.drawRoundedRect(r,20,10);
painter.save();
r.adjust(2,2,-2,-2);
painter.setViewport(r);
r.moveTo(0,-r.height()/2);
painter.setWindow(r);
drawChart(&painter,r);
painter.restore();
}
Also,createaprotectedmethodcalleddrawChart():
voidWidget::drawChart(QPainter*painter,constQRect&rect){
painter->setPen(Qt::red);
painter->drawLine(0,0,rect.width(),0);
}
Let'stakealookatouroutput:
Whatjusthappened?
Thefirstthingwedidinthenewlyaddedcodeiscallpainter.save().
Thiscallstoresallparametersofthepainterinaninternalstack.
Wecanthenmodifythepainterstate(bychangingitsattributes,
applyingtransformations,andsoon)andthen,ifatanypointwe
wanttogobacktothesavedstate,itisenoughtocallpainter.restore()
toundoallthemodificationsinonego.
Thesave()andrestore()methodscanbecalledasmanytimesasneeded.Statesarestored
inastack,soyoucansavemultipletimesinarowandthenrestoretoundoeachchange.
Justremembertoalwayspairacalltosave()withasimilarcalltorestore(),orthe
internalpainterstatewillgetcorrupted.Eachcalltorestore()willrevertthepaintertothe
lastsavedstate.
Afterthestateissaved,wemodifytherectangleagainbyadjusting
forthewidthoftheborder.Then,wesetthenewrectangleasthe
viewport,informingthepainteraboutthephysicalrangeof
coordinatestooperateon.Then,wemovetherectanglebyhalfits
heightandsetthatasthepainterwindow.Thiseffectivelyputsthe
originofthepainterathalftheheightofthewidget.Then,the
drawChart()methodiscalled,wherebyaredlineisdrawnonthexaxis
ofthenewcoordinatesystem.
Timeforaction–Drawingan
oscillogram
Let'sfurtherextendourwidgettobecomeasimpleoscillogram
renderer.Forthat,wehavetomakethewidgetrememberasetof
valuesanddrawthemasaseriesoflines.
Let'sstartbyaddingaQVector<quint16>membervariablethatholdsa
listofunsigned16-bitintegervalues.Wewillalsoaddslotsfor
addingvaluestothelistandforclearingthelist,asshown:
classWidget:publicQWidget
{
//...
publicslots:
voidaddPoint(unsignedyVal){
m_points<<qMax(0u,yVal);
update();
}
voidclear(){
m_points.clear();
update();
}
protected:
//...
QVector<quint16>m_points;
};
Notethateachmodificationofthelistinvokesamethodcalled
update().Thisschedulesapainteventsothatourwidgetcanbe
redrawnwiththenewvalues.
Drawingcodeisalsoeasy;wejustiterateoverthelistanddraw
symmetricbluelinesbasedonthevaluesfromthelist.Sincethe
linesarevertical,theydon'tsufferfromaliasingandsowecan
disablethisrenderhint,asshown:
voidWidget::drawChart(QPainter*painter,constQRect&rect){
painter->setPen(Qt::red);
painter->drawLine(0,0,rect.width(),0);
painter->save();
painter->setRenderHint(QPainter::Antialiasing,false);
painter->setPen(Qt::blue);
for(inti=0;i<m_points.size();++i){
painter->drawLine(i,-m_points.at(i),i,m_points.at(i));
}
painter->restore();
}
Toseetheresult,let'sfillthewidgetwithdatainthemain()function:
for(inti=0;i<450;++i){
w.addPoint(qrand()%120);
}
Thislooptakesarandomnumberbetween0and119andaddsitasa
pointtothewidget.Asampleresultfromrunningsuchcodecanbe
seeninthefollowingscreenshot:
Ifyouscaledownthewindow,youwillnotethattheoscillogramextendspastthe
boundariesoftheroundedrectangle.Rememberaboutclipping?Youcanuseitnowto
constrainthedrawingbyaddingasimplepainter.setClipRect(r)calljustbeforeyoucall
drawChart().
Sofar,thecustomwidgetwasnotinteractiveatall.Althoughthe
widgetcontentcouldbemanipulatedfromwithinthesourcecode
(saybyaddingnewpointstotheplot),thewidgetwasdeaftoany
useractions(apartfromresizingthewidget,whichcauseda
repaint).InQt,anyinteractionbetweentheuserandthewidgetis
donebydeliveringeventstothewidget.Suchafamilyofeventsis
generallycalledinputeventsandcontainseventssuchaskeyboard
eventsanddifferentformsofpointing-deviceevents—mouse,
tablet,andtouchevents.
Inatypicalmouseeventflow,awidgetfirstreceivesamousepress
event,thenanumberofmousemoveevents(whentheusermoves
themousearoundwhilethemousebuttoniskeptpressed),and
finally,amousereleaseevent.Thewidgetcanalsoreceivean
additionalmousedouble-clickeventinadditiontotheseevents.Itis
importanttorememberthatbydefault,mousemoveeventsareonly
deliveredifamousebuttonispressedwhenthemouseismoved.To
receivemousemoveeventswhennobuttonispressed,awidget
needstoactivateafeaturecalledmousetracking.
Timeforaction–Making
oscillogramsselectable
It'stimetomakeouroscillogramwidgetinteractive.Wewillteachit
toaddacoupleoflinesofcodetoitthatlettheuserselectpartof
theplot.Let'sstartwithstoragefortheselection.We'llneedtwo
integervariablesthatcanbeaccessedviaread-onlyproperties;
therefore,addthefollowingtwopropertiestotheclass:
Q_PROPERTY(intselectionStartREADselectionStart
NOTIFYselectionChanged)
Q_PROPERTY(intselectionEndREADselectionEnd
NOTIFYselectionChanged)
Next,youneedtocreatecorrespondingprivatefields(youcan
initializethembothto−1),getters,andsignals.
Theusercanchangetheselectionbydraggingthemousecursor
overtheplot.Whentheuserpressesthemousebuttonoversome
placeintheplot,we'llmarkthatplaceasthestartoftheselection.
Draggingthemousewilldeterminetheendoftheselection.The
schemefornamingeventsissimilartothepaintevent;therefore,
weneedtodeclareandimplementthefollowingtwoprotected
methods:
voidWidget::mousePressEvent(QMouseEvent*mouseEvent){
m_selectionStart=m_selectionEnd=mouseEvent->pos().x()-12;
emitselectionChanged();
update();
}
voidWidget::mouseMoveEvent(QMouseEvent*mouseEvent){
m_selectionEnd=mouseEvent->pos().x()-12;
emitselectionChanged();
update();
update();
}
Thestructureofbotheventhandlersissimilar.Weupdatethe
neededvalues,takingintoconsiderationtheleftpadding(12pixels)
oftheplot,similartowhatwedowhiledrawing.Then,asignalis
emittedandupdate()iscalledtoschedulearepaintofthewidget.
Whatremainsistointroducechangestothedrawingcode.We
suggestthatyouaddadrawSelection()methodsimilartodrawChart(),but
thatitiscalledfromthepainteventhandlerimmediatelybefore
drawChart(),asshown:
voidWidget::drawSelection(QPainter*painter,constQRect&rect){
if(m_selectionStart<0){
return;
}
painter->save();
painter->setPen(Qt::NoPen);
painter->setBrush(palette().highlight());
QRectselectionRect=rect;
selectionRect.setLeft(m_selectionStart);
selectionRect.setRight(m_selectionEnd);
painter->drawRect(selectionRect);
painter->restore();
}
First,wecheckwhetherthereisanyselectiontobedrawnatall.
Then,wesavethepainterstateandadjustthepenandbrushofthe
painter.ThepenissettoQt::NoPen,whichmeansthepaintershould
notdrawanyoutline.Todeterminethebrush,weusepalette();this
returnsanobjectoftheQPalettetypeholdingbasiccolorsfora
widget.Oneofthecolorsheldintheobjectisthecolorofthe
highlightoftenusedformarkingselections.Ifyouuseanentryfrom
thepaletteinsteadofmanuallyspecifyingacolor,yougainan
advantagebecausewhentheuseroftheclassmodifiesthepalette,
thismodificationistakenintoaccountbyourwidgetcode.
Youcanuseothercolorsfromthepaletteinthewidgetforotherthingswedrawinthe
widget.YoucanevendefineyourownQPaletteobjectintheconstructorofthewidgetto
providedefaultcolorsforit.
Finally,weadjusttherectangletobedrawnandissuethedrawing
call.
Whenyourunthisprogram,youwillnotethattheselectioncolor
doesn'tcontrastverywellwiththeplotitself.Toovercomethis,a
commonapproachistodrawthe"selected"contentwithadifferent
(ofteninverted)color.Thiscaneasilybeappliedinthissituationby
modifyingthedrawChart()codeslightly:
for(inti=0;i<m_points.size();++i){
if(m_selectionStart<=i&&m_selectionEnd>=i){
painter->setPen(Qt::white);
}else{
painter->setPen(Qt::blue);
}
painter->drawLine(i,-m_points.at(i),i,m_points.at(i));
}
Nowyouseethefollowingoutput:
Haveagohero–Reactingonly
totheleftmousebutton
Asanexercise,youcanmodifytheeventhandlingcodesothatit
onlychangestheselectionifthemouseeventwastriggeredbythe
leftmousebutton.Toseewhichbuttontriggeredthemousepress
event,youcanusetheQMouseEvent::button()method,whichreturns
Qt::LeftButtonfortheleftbutton,Qt::RightButtonfortheright,andsoon.
Touchevents
Handlingtoucheventsisdifferent.Foranysuchevent,youreceive
acalltothetouchEvent()virtualmethod.Theparameterofsuchacall
isanobjectthatcanretrievealistofpointscurrentlytouchedbythe
userwithadditionalinformationregardingthehistoryofuser
interaction(whetherthetouchwasjustinitiatedorthepointwas
pressedearlierandmoved)andwhatforceisappliedtothepointby
theuser.Notethatthisisalow-levelframeworkthatallowsyouto
preciselyfollowthehistoryoftouchinteraction.Ifyouaremore
interestedinhigher-levelgesturerecognition(pan,pinch,and
swipe),thereisaseparatefamilyofeventsavailableforit.
Handlinggesturesisatwo-stepprocedure.First,youneedto
activategesturerecognitiononyourwidgetbycallinggrabGesture()
andpassinginthetypeofgestureyouwanttohandle.Agoodplace
forsuchcodeisthewidgetconstructor.
Then,yourwidgetwillstartreceivinggestureevents.Thereareno
dedicatedhandlersforgestureeventsbut,fortunately,alleventsfor
anobjectflowthroughitsevent()method,whichwecan
reimplement.Here'ssomeexamplecodethathandlespangestures:
boolWidget::event(QEvent*e){
if(e->type()==QEvent::Gesture){
QGestureEvent*gestureEvent=static_cast<QGestureEvent*>(e);
QGesture*pan=gestureEvent->gesture(Qt::PanGesture);
if(pan){
handlePanGesture(static_cast<QPanGesture*>(pan));
}
}
returnQWidget::event(e);
}
First,acheckfortheeventtypeismade;ifitmatchestheexpected
value,theeventobjectiscasttoQGestureEvent.Then,theeventisasked
whetherQt::PanGesturewasrecognized.Finally,ahandlePanGesture
methodiscalled.Youcanimplementsuchamethodtohandleyour
pangestures.
Workingwithimages
Qthastwoclassesforhandlingimages.ThefirstoneisQImage,more
tailoredtowarddirectpixelmanipulation.Youcancheckthesizeof
theimageorcheckandmodifythecolorofeachpixel.Youcan
converttheimageintoadifferentinternalrepresentation(sayfrom
8-bitcolormaptofull32-bitcolorwithapremultipliedalpha
channel).Thistype,however,isnotthatfitforrendering.Forthat,
wehaveadifferentclasscalledQPixmap.Thedifferencebetweenthe
twoclassesisthatQImageisalwayskeptintheapplicationmemory,
whileQPixmapcanonlybeahandletoaresourcethatmayresidein
thegraphicscardmemoryoronaremoteXserver.Itsmain
advantageoverQImageisthatitcanberenderedveryquicklyatthe
costoftheinabilitytoaccesspixeldata.Youcanfreelyconvert
betweenthetwotypes,butbearinmindthatonsomeplatforms,
thismightbeanexpensiveoperation.Alwaysconsiderwhichclass
servesyourparticularsituationbetter.Ifyouintendtocropthe
image,tintitwithsomecolor,orpaintoverit,QImageisabetter
choice,butifyoujustwanttorenderabunchoficons,it'sbestto
keepthemasQPixmapinstances.
Loading
Loadingimagesisveryeasy.BothQPixmapandQImagehaveconstructors
thatsimplyacceptapathtoafilecontainingtheimage.Qtaccesses
imagedatathroughpluginsthatimplementreadingandwriting
operationsfordifferentimageformats.Withoutgoingintothe
detailsofplugins,itisenoughtosaythatthedefaultQtinstallation
supportsreadingthefollowingimagetypes:
Type Description
BMP
WindowsBitmap
GIF
GraphicsInterchangeFormat
JPG/JPEG
JointPhotographyExpertsGroup
PNG
PortableNetworkGraphics
PPM/PBM/PGM
Portableanymap
XBM
XBitmap
XPM
XPixmap
Asyoucansee,themostpopularimageformatsareavailable.The
listcanbefurtherextendedbyinstallingadditionalplugins.
YoucanaskQtforalistofsupportedimagetypesbycallingastaticmethod,
QImageReader::supportedImageFormats(),whichreturnsalistofformatsthatcanbereadby
Qt.Foralistofwritableformats,callQImageWriter::supportedImageFormats().
Animagecanalsobeloadeddirectlyfromanexistingmemory
buffer.Thiscanbedoneintwoways.Thefirstoneistousethe
loadFromData()method(itexistsinbothQPixmapandQImage),which
behavesthesameaswhenloadinganimagefromafile—youpassit
adatabufferandthesizeofthebufferandbasedonthat,theloader
determinestheimagetypebyinspectingtheheaderdataandloads
thepictureintoQImageorQPixmap.Thesecondsituationiswhenyou
don'thaveimagesstoredina"filetype"suchasJPEGorPNG;
rather,youhaverawpixeldataitself.Insuchasituation,QImage
offersaconstructorthattakesapointertoablockofdatatogether
withthesizeoftheimageandformatofthedata.Theformatisnot
afileformatsuchastheoneslistedearlierbutamemorylayoutfor
datarepresentingasinglepixel.
ThemostpopularformatisQImage::Format_ARGB32,whichmeansthat
eachpixelisrepresentedby32-bits(4bytes)ofdatadividedequally
betweenalpha,red,green,andbluechannels—8-bitsperchannel.
AnotherpopularformatisQImage::Format_ARGB32_Premultiplied,where
valuesforthered,green,andbluechannelsarestoredafterbeing
multipliedbythevalueofthealphachannel,whichoftenresultsin
fasterrendering.Youcanchangetheinternaldatarepresentation
usingacalltoconvertToFormat().Forexample,thefollowingcode
convertsatrue-colorimageto256colors,wherecolorforeachpixel
isrepresentedbyanindexinacolortable:
QImagetrueColor("image.png");
QImageindexed=trueColor.convertToFormat(QImage::Format_Indexed8);
Thecolortableitselfisavectorofcolordefinitionsthatcanbe
fetchedusingcolorTable()andreplacedusingsetColorTable().For
example,youcanconvertanindexedimagetograyscaleby
adjustingitscolortable,asfollows:
QImageindexed=...;
QVector<QRgb>colorTable=indexed.colorTable();
for(QRgb&item:colorTable){
intgray=qGray(item);
item=qRgb(gray,gray,gray);
}
indexed.setColorTable(colorTable);
However,thereisamuchcleanersolutiontothistask.Youcan
convertanyimagetotheFormat_Grayscale8format:
QImagegrayImage=
coloredImage.convertToFormat(QImage::Format_Grayscale8);
Thisformatuses8bitsperpixelanddoesn'thaveacolortable,soit
canonlystoregrayscaleimages.
Modifying
Therearetwowaystomodifyimagepixeldata.Thefirstoneworks
onlyforQImageandinvolvesdirectmanipulationofpixelsusingthe
setPixel()call,whichtakesthepixelcoordinatesandcolortobeset
forthatpixel.ThesecondoneworksforbothQImageandQPixmapand
makesuseofthefactthatboththeseclassesaresubclassesof
QPaintDevice.Therefore,youcanopenQPainteronsuchobjectsanduse
itsdrawingAPI.Here'sanexampleofobtainingapixmapwitha
bluerectangleandredcirclepaintedoverit:
QPixmappx(256,256);
px.fill(Qt::transparent);
QPainterpainter(&px);
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::blue);
QRectr=px.rect().adjusted(10,10,-10,-10);
painter.drawRect(r);
painter.setBrush(Qt::red);
painter.drawEllipse(r);
First,wecreatea256x256pixmapandfillitwithtransparent
color.Then,weopenapainteronitandinvokeaseriesofcallsthat
drawsabluerectangleandredcircle.
QImagealsooffersanumberofmethodsfortransformingtheimage,
includingscaled(),mirrored(),transformed(),andcopy().TheirAPIis
intuitive,sowewon'tdiscussithere.
Painting
PaintingimagesinitsbasicformisassimpleascallingdrawImage()or
drawPixmap()fromtheQPainterAPI.Therearedifferentvariantsofthe
twomethods,but,basically,allofthemallowonetospecifywhich
portionofagivenimageorpixmapistobedrawnandwhere.Itis
worthnotingthatpaintingpixmapsispreferredtopaintingimages,
asanimagehastofirstbeconvertedintoapixmapbeforeitcanbe
drawn.
Ifyouhavealotofpixmapstodraw,aclasscalledQPixmapCachemay
comeinhandy.Itprovidesanapplication-widecacheforpixmaps.
Usingit,youcanspeeduppixmaploadingwhileintroducingacap
onmemoryusage.
Finally,ifyoujustwanttoshowapixmapasaseparatewidget,you
canuseQLabel.Thiswidgetisusuallyusedfordisplayingtext,but
youcanconfigureittoshowapixmapinsteadwiththesetPixmap()
function.Bydefault,thepixmapisdisplayedwithoutscaling.When
thelabelislargerthanthepixmap,it'spositionisdeterminedbythe
label'salignmentthatyoucanchangewiththesetAlignment()
function.YoucanalsocallsetScaledContents(true)tostretchthepixmap
tothewholesizeofthelabel.
Paintingtext
DrawingtextusingQPainterdeservesaseparateexplanation,not
becauseitiscomplicated,butbecauseQtoffersmuchflexibilityin
thisregard.Ingeneral,paintingtexttakesplacebycalling
QPainter::drawText()orQPainter::drawStaticText().Let'sfocusontheformer
first,whichallowsthedrawingofgenerictext.
Themostbasiccalltopaintsometextisavariantofthismethod,
whichtakesxandycoordinatesandthetexttodraw:
painter.drawText(10,20,"Drawingsometextat(10,20)");
Theprecedingcalldrawsthegiventextatposition10horizontally
andplacesthebaselineofthetextatposition20vertically.Thetext
isdrawnusingthepainter'scurrentfontandpen.Thecoordinates
canalternativelybepassedasQPointinstances,insteadofbeinggiven
xandyvaluesseparately.Theproblemwiththismethodisthatit
allowslittlecontroloverhowthetextisdrawn.Amuchmore
flexiblevariantisonethatletsusgiveasetofflagsandexpresses
thepositionofthetextasarectangleinsteadofapoint.Theflags
canspecifythealignmentofthetextwithinthegivenrectangleor
instructtherenderingengineaboutwrappingandclippingthetext.
Youcanseetheresultofgivingadifferentcombinationofflagsto
thecallinthefollowingdiagram:
Inordertoobtaineachoftheprecedingresults,runcodesimilarto
thefollowing:
painter.drawText(rect,Qt::AlignLeft|Qt::TextShowMnemonic,"&ABC");
YoucanseethatunlessyousettheQt::TextDontClipflag,thetextis
clippedtothegivenrectangle;settingQt::TextWordWrapenablesline
wrapping,andQt::TextSingleLinemakestheengineignoreanynewline
charactersencountered.
Statictext
Qthastoperformanumberofcalculationswhenlayingoutthe
text,andthishastobedoneeachtimethetextisrendered.Thiswill
beawasteoftimeifthetextanditsattributeshavenotchanged
sincethelasttime.Toavoidtheneedtorecalculatethelayout,the
conceptofstatictextwasintroduced.
Touseit,instantiateQStaticTextandinitializeitwiththetextyou
wanttorenderalongwithanyoptionsyoumightwantittohave
(keptastheQTextOptioninstance).Then,storetheobjectsomewhere,
andwheneveryouwantthetexttoberendered,justcall
QPainter::drawStaticText(),passingthestatictextobjecttoit.Ifthe
layoutofthetexthasnotchangedsincetheprevioustimethetext
wasdrawn,itwillnotberecalculated,resultinginimproved
performance.Here'sanexampleofacustomwidgetthatsimply
drawstextusingthestatictextapproach:
classTextWidget:publicQWidget{
public:
TextWidget(QWidget*parent=nullptr):QWidget(parent){}
voidsetText(constQString&txt){
m_staticText.setText(txt);
update();
}
protected:
voidpaintEvent(QPaintEvent*){
QPainterpainter(this);
painter.drawStaticText(0,0,m_staticText);
}
private:
QStaticTextm_staticText;
};
Optimizingwidgetpainting
Asanexercise,wewillmodifyouroscillogramwidgetsothatitonly
rerendersthepartofitsdatathatisrequired.
Timeforaction–Optimizing
oscillogramdrawing
Thefirststepistomodifythepainteventhandlingcodetofetch
informationabouttheregionthatneedsupdatingandpassittothe
methoddrawingthechart.Thechangedpartsofthecodehavebeen
highlightedhere:
voidWidget::paintEvent(QPaintEvent*event)
{
QRectexposedRect=event->rect();
...
drawSelection(&painter,r,exposedRect);
drawChart(&painter,r,exposedRect);
painter.restore();
}
ThenextstepistomodifydrawSelection()toonlydrawthepartofthe
selectionthatintersectswiththeexposedrectangle.Luckily,QRect
offersamethodtocalculatetheintersectionforus:
voidWidget::drawSelection(QPainter*painter,constQRect&rect,
constQRect&exposedRect)
{
//...
QRectselectionRect=rect;
selectionRect.setLeft(m_selectionStart);
selectionRect.setRight(m_selectionEnd);
painter->drawRect(selectionRect.intersected(exposedRect));
painter->restore();
}
Finally,drawChartneedstobeadjustedtoomitthevaluesoutsidethe
exposedrectangle:
voidWidget::drawChart(QPainter*painter,constQRect&rect,
constQRect&exposedRect)
{
painter->setPen(Qt::red);
painter->drawLine(exposedRect.left(),0,exposedRect.width(),0);
painter->save();
painter->setRenderHint(QPainter::Antialiasing,false);
constintlastPoint=qMin(m_points.size(),
exposedRect.right()+1);
for(inti=exposedRect.left();i<lastPoint;++i){
if(m_selectionStart<=i&&m_selectionEnd>=i){
painter->setPen(Qt::white);
}else
painter->setPen(Qt::blue);
painter->drawLine(i,-m_points.at(i),i,m_points.at(i));
}
painter->restore();
Q_UNUSED(rect)
}
Whatjusthappened?
Byimplementingthesechanges,wehaveeffectivelyreducedthe
paintedareatotherectanglereceivedwiththeevent.Inthis
particularsituation,wewillnotsavemuchtimeasdrawingtheplot
isnotthattime-consuming;inmanysituations,however,youwill
beabletosavealotoftimeusingthisapproach.Forexample,ifwe
weretoplotaverydetailedaerialmapofagameworld,itwouldbe
veryexpensivetoreplotthewholemapifonlyasmallpartofitwere
modified.Wecaneasilyreducethenumberofcalculationsand
drawingcallsbytakingadvantageoftheinformationaboutthe
exposedarea.
Makinguseoftheexposedrectangleisalreadyagoodsteptoward
efficiency,butwecangoastepfurther.Thecurrentapproach
requiresthatweredraweachandeverylineoftheplotwithinthe
exposedrectangle,whichstilltakessometime.Instead,wecan
paintthoselinesonlyonceintoapixmap,andthenwheneverthe
widgetneedsrepainting,tellQttorenderpartofthepixmaptothe
widget.
Haveagohero–Cachingthe
oscillograminapixmap
Now,itshouldbeveryeasyforyoutoimplementthisapproachfor
ourexamplewidget.Themaindifferenceisthateachchangetothe
plotcontentsshouldnotresultinacalltoupdate()butinacallthat
willrerenderthepixmapandthencallupdate().ThepaintEventmethod
thenbecomessimplythis:
voidWidget::paintEvent(QPaintEvent*event)
{
QRectexposedRect=event->rect();
QPainterpainter(this);
painter.drawPixmap(exposedRect,m_pixmap,exposedRect);
}
You'llalsoneedtorerenderthepixmapwhenthewidgetisresized.
ThiscanbedonefromwithintheresizeEvent()virtualfunction.
Whileitisusefultomastertheavailableapproachestooptimization,it'salwaysimportant
tocheckwhethertheyactuallymakeyourapplicationfaster.Thereareoftencaseswhere
thestraightforwardapproachismoreoptimalthanacleveroptimization.Inthepreceding
example,resizingthewidget(andsubsequentlyresizingthepixmap)cantriggera
potentiallyexpensivememoryallocation.Usethisoptimizationonlyifdirectpaintingon
thewidgetisevenmoreexpensive.
Implementingachessgame
Atthispoint,youarereadytoemployyournewlygainedskillsin
renderinggraphicswithQttocreateagamethatuseswidgetswith
customgraphics.Theherooftodaywillbechessandotherchess-
likegames.
Timeforaction–Developing
thegamearchitecture
CreateanewQtWidgetsApplicationproject.Aftertheproject
infrastructureisready,chooseNewFileorProjectfromtheFile
menuandchoosetocreateaC++Class.CallthenewclassChessBoard
andsetQObjectasitsbaseclass.Repeattheprocesstocreatea
ChessAlgorithmclassderivedfromQObjectandanotheronecalled
ChessView,butchooseQWidgetasthebaseclassthistime.Youshould
endupwithafilenamedmain.cppandfourclasses:
MainWindowwillbeourmainwindowclassthatcontains
aChessView
ChessViewwillbethewidgetthatdisplaysourchessboard
ChessAlgorithmwillcontainthegamelogic
ChessBoardwillholdthestateofthechessboardandprovideit
toChessViewandChessAlgorithm
Now,navigatetotheheaderfileforChessAlgorithmandaddthe
followingmethodstotheclass:
public:
ChessBoard*board()const;
publicslots:
virtualvoidnewGame();
signals:
voidboardChanged(ChessBoard*);
protected:
virtualvoidsetupBoard();
voidsetBoard(ChessBoard*board);
voidsetBoard(ChessBoard*board);
Also,addaprivatem_boardfieldoftheChessBoard*type.Rememberto
eitherincludechessboard.horforward-declaretheChessBoardclass.
Implementboard()asasimplegettermethodform_board.ThesetBoard()
methodwillbeaprotectedsetterform_board:
voidChessAlgorithm::setBoard(ChessBoard*board)
{
if(board==m_board){
return;
}
deletem_board;
m_board=board;
emitboardChanged(m_board);
}
Next,let'sprovideabaseimplementationforsetupBoard()tocreatea
defaultchessboardwitheightranksandeightcolumns:
voidChessAlgorithm::setupBoard()
{
setBoard(newChessBoard(8,8,this));
}
Thenaturalplacetopreparetheboardisinafunctionexecuted
whenanewgameisstarted:
voidChessAlgorithm::newGame()
{
setupBoard();
}
Thelastadditiontothisclassfornowistoextendtheprovided
constructortoinitializem_boardtoanullpointer.
Inthelastmethodshown,weinstantiatedaChessBoardobject,solet's
focusonthatclassnow.First,extendtheconstructortoaccepttwo
additionalintegerparametersbesidestheregularparentargument.
Storetheirvaluesinprivatem_ranksandm_columnsfields(rememberto
declarethefieldsthemselvesintheclassheaderfile).
Intheheaderfile,justundertheQ_OBJECTmacro,addthefollowing
twolinesaspropertydefinitions:
Q_PROPERTY(intranksREADranksNOTIFYranksChanged)
Q_PROPERTY(intcolumnsREADcolumnsNOTIFYcolumnsChanged)
Declaresignalsandimplementgettermethodstocooperatewith
thosedefinitions.Also,addtwoprotectedmethods:
protected:
voidsetRanks(intnewRanks);
voidsetColumns(intnewColumns);
Thesewillbesettersfortherankandcolumnproperties,butwe
don'twanttoexposethemtotheoutsideworld,sowewillgivethem
protectedaccessscope.
PutthefollowingcodeintothesetRanks()methodbody:
voidChessBoard::setRanks(intnewRanks)
{
if(ranks()==newRanks){
return;
}
m_ranks=newRanks;
emitranksChanged(m_ranks);
}
Next,inasimilarway,youcanimplementsetColumns().
Thelastclasswewilldealwithnowisourcustomwidget,ChessView.
Fornow,wewillprovideonlyarudimentaryimplementationfor
onemethod,butwewillexpanditlaterasourimplementation
grows.AddapublicsetBoard(ChessBoard*)methodwiththefollowing
body:
voidChessView::setBoard(ChessBoard*board)
{
if(m_board==board){
return;
}
if(m_board){
//disconnectallsignal-slotconnectionsbetweenm_boardand
this
m_board->disconnect(this);
}
m_board=board;
//connectsignals(tobedonelater)
updateGeometry();
}
Now,let'sdeclarethem_boardmember.Aswearenottheownersof
theboardobject(thealgorithmclassisresponsibleformanaging
it),wewillusetheQPointerclass,whichtracksthelifetimeofQObject
andsetsitselftonulloncetheobjectisdestroyed:
private:
QPointer<ChessBoard>m_board;
QPointerinitializesitsvaluetonull,sowedon'thavetodoitourselves
intheconstructor.Forcompleteness,let'sprovideagettermethod
fortheboard:
ChessBoard*ChessView::board()const{
returnm_board;
}
Whatjusthappened?
Inthelastexercise,wedefinedthebasearchitectureforour
solution.Wecanseethattherearethreeclassesinvolved:ChessView
actingastheuserinterface,ChessAlgorithmfordrivingtheactualgame,
andChessBoardasadatastructuresharedbetweentheviewandthe
engine.Thealgorithmwillberesponsibleforsettinguptheboard
(throughsetupBoard()),makingmoves,checkingwinconditions,and
soon.Theviewwillberenderingthecurrentstateoftheboardand
willsignaluserinteractiontotheunderlyinglogic.
Mostofthecodeisself-explanatory.Youcanseeinthe
ChessView::setBoard()methodthatwearedisconnectingallsignalsfrom
anoldboardobject,attachingthenewone(wewillcomebackto
connectingthesignalslaterwhenwehavealreadydefinedthem),
andfinallytellingthewidgettoupdateitssizeandredrawitself
withthenewboard.
Timeforaction–Implementing
thegameboardclass
Nowwewillfocusonourdatastructure.Addanewprivatemember
toChessBoard,avectorofcharactersthatwillcontaininformation
aboutpiecesontheboard:
QVector<char>m_boardData;
Considerthefollowingtablethatshowsthepiecetypeandthe
lettersusedforit:
Piecetype White Black
King
K
k
Queen
Q
q
Rook
R
r
Bishop
B
b
Knight
N
n
Pawn
P
P
Youcanseethatwhitepiecesuseuppercaselettersandblackpieces
uselowercasevariantsofthesameletters.Inadditiontothat,we
willuseaspacecharacter(0x20ASCIIvalue)todenotethatafield
isempty.Wewilladdaprotectedmethodforsettingupanempty
boardbasedonthenumberofranksandcolumnsontheboardand
aboardReset()signaltoinformthatthepositionontheboardhas
changed:
voidChessBoard::initBoard()
{
m_boardData.fill('',ranks()*columns());
emitboardReset();
}
Wecanupdateourmethodsforsettingrankandcolumncountsto
makeuseofthatmethod:
voidChessBoard::setRanks(intnewRanks)
{
if(ranks()==newRanks){
return;
}
m_ranks=newRanks;
initBoard();
emitranksChanged(m_ranks);
}
voidChessBoard::setColumns(intnewColumns)
{
if(columns()==newColumns){
return;
}
m_columns=newColumns;
initBoard();
emitcolumnsChanged(m_columns);
}
TheinitBoard()methodshouldalsobecalledfromwithinthe
constructor,soplacethecallthereaswell.
Next,weneedamethodtoreadwhichpieceispositionedina
particularfieldoftheboard:
charChessBoard::data(intcolumn,intrank)const
{
returnm_boardData.at((rank-1)*columns()+(column-1));
}
Ranksandcolumnshaveindexesstartingfrom1,butthedata
structureisindexedstartingfrom0;therefore,wehavetosubtract1
fromboththerankandcolumnindex.Itisalsorequiredtohavea
methodtomodifythedatafortheboard.Implementthefollowing
publicmethod:
voidChessBoard::setData(intcolumn,intrank,charvalue)
{
if(setDataInternal(column,rank,value)){
emitdataChanged(column,rank);
}
}
Themethodmakesuseofanotheronethatdoestheactualjob.
However,thismethodshouldbedeclaredwithprotectedaccessscope.
Again,weadjustforindexdifferences:
boolChessBoard::setDataInternal(intcolumn,intrank,charvalue)
boolChessBoard::setDataInternal(intcolumn,intrank,charvalue)
{
intindex=(rank-1)*columns()+(column-1);
if(m_boardData.at(index)==value){
returnfalse;
}
m_boardData[index]=value;
returntrue;
}
SincesetData()makesuseofasignal,wehavetodeclareitaswell:
signals:
voidranksChanged(int);
voidcolumnsChanged(int);
voiddataChanged(intc,intr);
voidboardReset();
Thesignalwillbeemittedeverytimethereisasuccessfulchangeto
thesituationontheboard.Wedelegatetheactualworktothe
protectedmethodtobeabletomodifytheboardwithoutemitting
thesignal.
HavingdefinedsetData(),wecanaddanothermethodforour
convenience:
voidChessBoard::movePiece(intfromColumn,intfromRank,
inttoColumn,inttoRank)
{
setData(toColumn,toRank,data(fromColumn,fromRank));
setData(fromColumn,fromRank,'');
}
Canyouguesswhatitdoes?That'sright!Itmovesapiecefromone
fieldtoanotherone,leavinganemptyspacebehind.
Thereisstillonemoremethodworthimplementing.Aregularchess
gamecontains32pieces,andtherearevariantsofthegamewhere
startingpositionsforthepiecesmightbedifferent.Settingthe
positionofeachpiecethroughaseparatecalltosetData()wouldbe
verycumbersome.Fortunately,thereisaneatchessnotationcalled
theForsyth-EdwardsNotation(FEN),withwhichthecomplete
stateofthegamecanbestoredasasinglelineoftext.Ifyouwant
thecompletedefinitionofthenotation,youcanlookitupyourself.
Inshort,wecansaythatthetextualstringlistspieceplacement
rankbyrank,startingfromthelastrankwhereeachpositionis
describedbyasinglecharacterinterpretedasinourinternaldata
structure(Kforwhiteking,qforblackqueen,andsoon).Eachrank
descriptionisseparatedbya/character.Ifthereareemptyfieldson
theboard,theyarenotstoredasspaces,butasadigitspecifyingthe
numberofconsecutiveemptyfields.Therefore,thestartingposition
forastandardgamecanbewrittenasfollows:
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
Thiscanbeinterpretedvisually,asfollows:
Let'swriteamethodcalledsetFen()tosetuptheboardbasedonan
FENstring:
voidChessBoard::setFen(constQString&fen)
{
intindex=0;
intskip=0;
constintcolumnCount=columns();
QCharch;
for(intrank=ranks();rank>0;--rank){
for(intcolumn=1;column<=columnCount;++column){
if(skip>0){
ch='';
skip--;
}else{
ch=fen.at(index++);
if(ch.isDigit()){
skip=ch.toLatin1()-'0';
ch='';
skip--;
}
}
setDataInternal(column,rank,ch.toLatin1());
}
QCharnext=fen.at(index++);
if(next!='/'&&next!=''){
initBoard();
return;//failonerror
}
}
emitboardReset();
}
Themethoditeratesoverallfieldsontheboardanddetermines
whetheritiscurrentlyinthemiddleofinsertingemptyfieldsonthe
boardorshouldratherreadthenextcharacterfromthestring.Ifa
digitisencountered,itisconvertedintoanintegerbysubtracting
theASCIIvalueofthe0character(thatis,'7'-'0'=7).Aftersetting
eachrank,werequirethataslashoraspacebereadfromthestring.
Otherwise,weresettheboardtoanemptyoneandbailoutofthe
method.
Whatjusthappened?
WetaughttheChessBoardclasstostoresimpleinformationabout
chesspiecesusingaone-dimensionalarrayofcharacters.Wealso
equippeditwithmethodsthatallowqueryingandmodifyinggame
data.Weimplementedafastwayofsettingthecurrentstateofthe
gamebyadoptingtheFENstandard.Thegamedataitselfisnottied
toclassicchess.Althoughwecomplywithastandardnotationfor
describingpieces,itispossibletouseotherlettersandcharacters
outsidethewell-definedsetforchesspieces.Thiscreatesaversatile
solutionforstoringinformationaboutchess-likegames,suchas
checkers,andpossiblyanyothercustomgamesplayedonatwo-
dimensionalboardofanysizewithranksandcolumns.Thedata
structurewecameupwithisnotastupidone—itcommunicates
withitsenvironmentbyemittingsignalswhenthestateofthegame
ismodified.
Timeforaction–
UnderstandingtheChessView
class
Thisisachapteraboutdoinggraphics,soitishightimewefocuson
displayingourchessgame.Ourwidgetcurrentlydisplaysnothing,
andourfirsttaskwillbetoshowachessboardwithrankand
columnsymbolsandfieldscoloredappropriately.
Bydefault,thewidgetdoesnothaveanypropersizedefined,and
wewillhavetofixthatbyimplementingsizeHint().However,tobe
abletocalculatethesize,wehavetodecidehowbigasinglefieldon
theboardwillbe.Therefore,inChessView,youshoulddeclarea
propertycontainingthesizeofthefield,asshown:
Q_PROPERTY(QSizefieldSize
READfieldSizeWRITEsetFieldSize
NOTIFYfieldSizeChanged)
Tospeedupcoding,youcanpositionthecursorovertheproperty
declaration,hittheAlt+Entercombination,andchoosethe
GeneratemissingQ_PROPERTYmembersfix-upfromthepop-up
menu.Creatorwillprovideminorimplementationsforthegetter
andsetterforyou.Youcanmovethegeneratedcodetothe
implementationfilebypositioningthecursorovereachmethod,
hittingAlt+Enter,andchoosingtheMovedefinitionto
chessview.cppfilefixup.Whilethegeneratedgettermethodisfine,
thesetterneedssomeadjusting.Modifyitbyaddingthefollowing
highlightedcode:
voidChessView::setFieldSize(QSizearg)
{
{
if(m_fieldSize==arg){
return;
}
m_fieldSize=arg;
emitfieldSizeChanged(arg);
updateGeometry();
}
Thistellsourwidgettorecalculateitssizewheneverthesizeofthe
fieldismodified.NowwecanimplementsizeHint():
QSizeChessView::sizeHint()const
{
if(!m_board){
returnQSize(100,100);
}
QSizeboardSize=QSize(fieldSize().width()
*m_board->columns()+1,
m_fieldSize.height()*m_board->ranks()+1);
//'M'isthewidestletter
intrankSize=fontMetrics().width('M')+4;
intcolumnSize=fontMetrics().height()+4;
returnboardSize+QSize(rankSize,columnSize);
}
First,wecheckwhetherwehaveavalidboarddefinitionandifnot,
returnasanesizeof100×100pixels.Otherwise,themethod
calculatesthesizeofallthefieldsbymultiplyingthesizeofeachof
thefieldsbythenumberofcolumnsorranks.Weaddonepixelto
eachdimensiontoaccommodatetherightandbottomborder.A
chessboardnotonlyconsistsoffieldsthemselvesbutalsodisplays
ranksymbolsontheleftedgeoftheboardandcolumnnumberson
thebottomedgeoftheboard.
Sinceweuseletterstoenumerateranks,wecheckthewidthofthe
widestletterusingtheQFontMetricsclass.Weusethesameclassto
checkhowmuchspaceisrequiredtorenderalineoftextusingthe
currentfontsothatwehaveenoughspacetoputcolumnnumbers.
Inbothcases,weadd4totheresulttomakea2pixelmargin
betweenthetextandtheedgeoftheboardandanother2pixel
marginbetweenthetextandtheedgeofthewidget.
Actually,thewidestletterinthemostcommonfontsisW,butitwon'tappearinourgame.
Itisveryusefultodefineahelpermethodforreturningarectangle
thatcontainsaparticularfield,asshown:
QRectChessView::fieldRect(intcolumn,intrank)const
{
if(!m_board){
returnQRect();
}
constQSizefs=fieldSize();
QPointtopLeft((column-1)*fs.width(),
(m_board->ranks()-rank)*fs.height());
QRectfRect=QRect(topLeft,fs);
//offsetrectbyranksymbols
intoffset=fontMetrics().width('M');
returnfRect.translated(offset+4,0);
}
Sinceranknumbersdecreasefromthetoptowardthebottomofthe
board,wesubtractthedesiredrankfromthemaximumrankthere
iswhilecalculatingfRect.Then,wecalculatethehorizontaloffsetfor
ranksymbols,justlikewedidinsizeHint(),andtranslatethe
rectanglebythatoffsetbeforereturningtheresult.
Finally,wecanmoveontoimplementingtheeventhandlerforthe
paintevent.DeclarethepaintEvent()method(thefixupmenu
availableundertheAlt+Enterkeyboardshortcutwillletyou
generateastubimplementationofthemethod)andfillitwiththe
followingcode:
voidChessView::paintEvent(QPaintEvent*)
{
if(!m_board){
return;
}
QPainterpainter(this);
for(intr=m_board->ranks();r>0;--r){
painter.save();
drawRank(&painter,r);
painter.restore();
}
for(intc=1;c<=m_board->columns();++c){
painter.save();
drawColumn(&painter,c);
painter.restore();
}
for(intr=1;r<=m_board->ranks();++r){
for(intc=1;c<=m_board->columns();++c){
painter.save();
drawField(&painter,c,r);
painter.restore();
}
}
}
Thehandlerisquitesimple.First,weinstantiatetheQPainterobject
thatoperatesonthewidget.Then,wehavethreeloops:thefirstone
iteratesoverranks,thesecondovercolumns,andthethirdoverall
fields.Thebodyofeachloopisverysimilar;thereisacalltoa
customdrawmethodthatacceptsapointertothepainterandindex
oftherank,column,orbothofthem,respectively.Eachofthecalls
issurroundedbyexecutingsave()andrestore()onourQPainter
instance.Whatarethecallsforhere?Thethreedrawmethods
—drawRank(),drawColumn(),anddrawField()—willbevirtualmethods
responsibleforrenderingtheranksymbol,thecolumnnumber,and
thefieldbackground.ItwillbepossibletosubclassChessViewand
providecustomimplementationsforthoserendererssothatitis
possibletoprovideadifferentlookofthechessboard.Sinceeachof
thesemethodstakesthepainterinstanceasitsparameter,overrides
ofthesemethodscanalterattributevaluesofthepainterbehindour
back.Callingsave()beforehandingoverthepaintertosuchoverride
storesitsstateonaninternalstack,andcallingrestore()after
returningfromtheoverrideresetsthepaintertowhatwasstored
withsave().Notethatthepaintercanstillbeleftinaninvalidstateif
theoverridecallssave()andrestore()adifferentnumberoftimes.
Callingsave()andrestore()veryoftenintroducesaperformancehit,soyoushouldavoid
savingandrestoringpainterstatestoooftenintime-criticalsituations.Asourpaintingis
verysimple,wedon'thavetoworryaboutthatwhenpaintingourchessboard.
Havingintroducedourthreemethods,wecanstartimplementing
them.Let'sstartwithdrawRankanddrawColumn.Remembertodeclare
themasvirtualandputtheminprotectedaccessscope(that's
usuallywhereQtclassesputsuchmethods),asfollows:
voidChessView::drawRank(QPainter*painter,intrank)
{
QRectr=fieldRect(1,rank);
QRectrankRect=QRect(0,r.top(),r.left(),r.height())
.adjusted(2,0,-2,0);
QStringrankText=QString::number(rank);
painter->drawText(rankRect,
Qt::AlignVCenter|Qt::AlignRight,rankText);
}
voidChessView::drawColumn(QPainter*painter,intcolumn)
{
QRectr=fieldRect(column,1);
QRectcolumnRect=
QRect(r.left(),r.bottom(),r.width(),height()-r.bottom())
.adjusted(0,2,0,-2);
painter->drawText(columnRect,
Qt::AlignHCenter|Qt::AlignTop,QChar('a'+column-1));
}
Bothmethodsareverysimilar.WeusefieldRect()toqueryforthe
left-mostcolumnandbottom-mostrank,and,basedonthat,we
calculatewhereranksymbolsandcolumnnumbersshouldbe
placed.ThecalltoQRect::adjusted()istoaccommodatethe2pixel
marginaroundthetexttobedrawn.Finally,weusedrawText()to
renderappropriatetext.Fortherank,weaskthepaintertoalign
thetexttotherightedgeoftherectangleandtocenterthetext
vertically.Inasimilarway,whendrawingthecolumn,wealignto
thetopedgeandcenterthetexthorizontally.
Nowwecanimplementthethirddrawmethod.Itshouldalsobe
declaredprotectedandvirtual.Placethefollowingcodeinthe
methodbody:
voidChessView::drawField(QPainter*painter,intcolumn,intrank)
{
QRectrect=fieldRect(column,rank);
QColorfillColor=(column+rank)%2?
palette().color(QPalette::Light):
palette().color(QPalette::Mid);
painter->setPen(palette().color(QPalette::Dark));
painter->setBrush(fillColor);
painter->drawRect(rect);
}
Inthismethod,weusetheQPaletteobjectcoupledwitheachwidget
toqueryforLight(usuallywhite)andMid(darkish)color,depending
onwhetherthefieldwearedrawingonthechessboardis
consideredwhiteorblack.Wedothatinsteadofhardcodingthe
colorstomakeitpossibletomodifycolorsofthetileswithout
subclassingsimplybyadjustingthepaletteobject.Then,weusethe
paletteagaintoaskfortheDarkcolorandusethatasapenforour
painter.Whenwedrawarectanglewithsuchsettings,thepenwill
stroketheborderoftherectangletogiveitamoreelegantlook.
Notehowwemodifyattributesofthepainterinthismethodanddo
notsetthembackafterward.Wecangetawaywithitbecauseofthe
save()andrestore()callssurroundingthedrawField()execution.
Wearenowreadytoseetheresultsofourwork.Let'sswitchtothe
MainWindowclassandequipitwiththefollowingtwoprivatevariables:
ChessView*m_view;
ChessAlgorithm*m_algorithm;
Then,modifytheconstructorbyaddingthefollowinghighlighted
codetosetuptheviewandthegameengine:
MainWindow::MainWindow(QWidget*parent):
QMainWindow(parent),
ui(newUi::MainWindow)
{
ui->setupUi(this);
m_view=newChessView;
m_view=newChessView;
m_algorithm=newChessAlgorithm(this);
m_algorithm->newGame();
m_view->setBoard(m_algorithm->board());
setCentralWidget(m_view);
m_view->setSizePolicy(QSizePolicy::Fixed,QSizePolicy::Fixed);
m_view->setFieldSize(QSize(50,50));
layout()->setSizeConstraint(QLayout::SetFixedSize);
}
Afterward,youshouldbeabletobuildtheproject.Whenyourunit,
youshouldseearesultsimilartotheoneinthefollowing
screenshot:
Whatjusthappened?
Inthisexercise,wedidtwothings.First,weprovidedanumberof
methodsforcalculatingthegeometryofimportantpartsofthe
chessboardandthesizeofthewidget.Second,wedefinedthree
virtualmethodsforrenderingvisualprimitivesofachessboard.By
makingthemethodsvirtual,weprovidedaninfrastructuretoletthe
lookbecustomizedbysubclassingandoverridingbase
implementations.Furthermore,byreadingcolorfromQPalette,we
allowedcustomizingthecolorsoftheprimitivesevenwithout
subclassing.
Thelastlineofthemainwindowconstructortellsthelayoutofthe
windowtoforceafixedsizeofthewindowequaltowhatthesize
hintofthewidgetinsideitreports.
Timeforaction–Renderingthe
pieces
Nowthatwecanseetheboard,itistimetoputthepiecesonit.We
willuseimagesforthatpurpose.Inmycase,wefoundanumberof
SVGfileswithchesspiecesanddecidedtousethem.SVGisavector
graphicsformatwhereallcurvesaredefinednotasafixedsetof
pointsbutasmathematiccurves.Theirmainbenefitisthatthey
scaleverywellwithoutcausinganaliasingeffect.
Let'sequipourviewwitharegistryofimagestobeusedfor
"stamping"aparticularpiecetype.Sinceeachpiecetypeis
identifiedwithchar,wecanuseittogeneratekeysforamapof
images.Let'sputthefollowingAPIintoChessView:
public:
voidsetPiece(chartype,constQIcon&icon);
QIconpiece(chartype)const;
private:
QMap<char,QIcon>m_pieces;
Fortheimagetype,wedonotuseQImageorQPixmapbutQIcon.Thisis
becauseQIconcanstoremanypixmapsofdifferentsizesandusethe
mostappropriateonewhenwerequestaniconofagivensizetobe
painted.Itdoesn'tmatterifweusevectorimages,butitdoesmatter
ifyouchoosetousePNGorothertypesofimage.Insuchcases,you
canuseaddFile()toaddmanyimagestoasingleicon.
Goingbacktoourregistry,theimplementationisverysimple.We
juststoretheiconinamapandaskthewidgettorepaintitself:
voidChessView::setPiece(chartype,constQIcon&icon)
{
{
m_pieces.insert(type,icon);
update();
}
QIconChessView::piece(chartype)const
{
returnm_pieces.value(type,QIcon());
}
Nowwecanfilltheregistrywithactualimagesrightafterwecreate
theviewinsidetheMainWindowconstructor.Notethatwestoredallthe
imagesinaresourcefile,asshown:
m_view->setPiece('P',QIcon(":/pieces/Chess_plt45.svg"));//pawn
m_view->setPiece('K',QIcon(":/pieces/Chess_klt45.svg"));//king
m_view->setPiece('Q',QIcon(":/pieces/Chess_qlt45.svg"));//queen
m_view->setPiece('R',QIcon(":/pieces/Chess_rlt45.svg"));//rook
m_view->setPiece('N',QIcon(":/pieces/Chess_nlt45.svg"));//knight
m_view->setPiece('B',QIcon(":/pieces/Chess_blt45.svg"));//bishop
m_view->setPiece('p',QIcon(":/pieces/Chess_pdt45.svg"));//pawn
m_view->setPiece('k',QIcon(":/pieces/Chess_kdt45.svg"));//king
m_view->setPiece('q',QIcon(":/pieces/Chess_qdt45.svg"));//queen
m_view->setPiece('r',QIcon(":/pieces/Chess_rdt45.svg"));//rook
m_view->setPiece('n',QIcon(":/pieces/Chess_ndt45.svg"));//knight
m_view->setPiece('b',QIcon(":/pieces/Chess_bdt45.svg"));//bishop
ThenextthingtodoistoextendthepaintEvent()methodoftheview
toactuallyrenderourpieces.Forthat,wewillintroduceanother
protectedvirtualmethodcalleddrawPiece().We'llcallitwhen
iteratingoveralltheranksandcolumnsoftheboard,asshown:
voidChessView::paintEvent(QPaintEvent*)
{
//...
for(intr=m_board->ranks();r>0;--r){
for(intc=1;c<=m_board->columns();++c){
drawPiece(&painter,c,r);
}
}
}
Itisnotacoincidencethatwestartdrawingfromthehighest(top)
ranktothelowest(bottom)one.Bydoingthat,weallowapseudo-
3Deffect;ifapiecedrawnextendspasttheareaofthefield,itwill
intersectthefieldfromthenextrank(whichispossiblyoccupiedby
anotherpiece).Bydrawinghigherrankpiecesfirst,wecausethem
tobepartiallycoveredbypiecesfromthelowerrank,which
imitatestheeffectofdepth.Bythinkingahead,weallow
reimplementationsofdrawPiece()tohavemorefreedominwhatthey
cando.
Thefinalstepistoprovideabaseimplementationforthismethod,
asfollows:
voidChessView::drawPiece(QPainter*painter,intcolumn,intrank)
{
QRectrect=fieldRect(column,rank);
charvalue=m_board->data(column,rank);
if(value!=''){
QIconicon=piece(value);
if(!icon.isNull()){
icon.paint(painter,rect,Qt::AlignCenter);
}
}
}
Themethodisverysimple;itqueriesfortherectangleofagiven
columnandrankandthenaskstheChessBoardinstanceaboutthe
pieceoccupyingthegivenfield.Ifthereisapiecethere,weaskthe
registryforthepropericon;ifwegetavalidone,wecallitspaint()
routinetodrawthepiececenteredinthefield'srect.Theimage
drawnwillbescaledtothesizeoftherectangle.Itisimportantthat
youonlyuseimageswithatransparentbackground(suchasPNG
orSVGfilesandnotJPEGfiles)sothatthecolorofthefieldcanbe
seenthroughthepiece.
Whatjusthappened?
Totesttheimplementation,youcanmodifythealgorithmtofillthe
boardwiththedefaultpiecesetupbyintroducingthefollowing
changetotheChessAlgorithmclass:
voidChessAlgorithm::newGame()
{
setupBoard();
board()->setFen(
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNRwKQkq-01"
);
}
Runningtheprogramshouldshowthefollowingresult:
Themodificationwedidinthisstepwasverysimple.First,we
providedawaytotelltheboardwhateachpiecetypelookslike.
Thisincludesnotonlystandardchesspiecesbutanythingthatfits
intocharandcanbesetinsidetheChessBoardclass'sinternaldata
array.Second,wemadeanabstractionfordrawingthepieceswith
thesimplestpossiblebaseimplementation:takinganiconfromthe
registryandrenderingittothefield.BymakinguseofQIcon,wecan
addseveralpixmapsofdifferentsizestobeusedwithdifferentsizes
ofasinglefield.Alternatively,theiconcancontainasinglevector
imagethatscalesverywellallbyitself.
Timeforaction–Makingthe
chessgameinteractive
Wehavemanagedtodisplaythechessboard,buttoactuallyplaya
game,wehavetotelltheprogramwhatmoveswewanttoplay.We
candothatbyaddingtheQLineEditwidgetwherewewillinputthe
moveinalgebraicform(forexample,Nf3tomoveaknighttof3),but
amorenaturalwayistoclickonapiecewiththemousecursor(or
tapitwithafinger)andthenclickagainonthedestinationfield.To
obtainsuchfunctionality,thefirstthingtodoistoteachChessViewto
detectmouseclicks.Therefore,addthefollowingmethod:
QPointChessView::fieldAt(constQPoint&pt)const
{
if(!m_board){
returnQPoint();
}
constQSizefs=fieldSize();
intoffset=fontMetrics().width('M')+4;
//'M'isthewidestletter
if(pt.x()<offset){
returnQPoint();
}
intc=(pt.x()-offset)/fs.width();
intr=pt.y()/fs.height();
if(c<0||c>=m_board->columns()||
r<0||r>=m_board->ranks()){
returnQPoint();
}
returnQPoint(c+1,m_board->ranks()-r);
//maxrank-r
}
ThecodelooksverysimilartotheimplementationoffieldRect().This
isbecausefieldAt()implementsitsreverseoperation—ittransforms
apointinthewidgetcoordinatespacetothecolumnandrankindex
ofafieldthepointiscontainedin.Theindexiscalculatedby
dividingpointcoordinatesbythesizeofthefield.Yousurely
rememberthat,inthecaseofcolumns,thefieldsareoffsetbythe
sizeofthewidestletterandamarginof4,andwehavetoconsider
thatinourcalculationshereaswell.Wedotwochecks:firstwe
checkthehorizontalpointcoordinateagainsttheoffsettodetect
whethertheuserclickedonthepartofthewidgetwherecolumn
symbolsaredisplayed,andthenwecheckwhethertherankand
columncalculatedfittherangerepresentedintheboard.Finally,
wereturntheresultasaQPointvalue,sincethisistheeasiestwayin
Qttorepresentatwo-dimensionalvalue.
Nowweneedtofindawaytomakethewidgetnotifyits
environmentthataparticularfieldwasclickedon.Wecandothis
throughthesignal-slotmechanism.Switchtotheheaderfileof
ChessView(ifyoucurrentlyhavechessview.cppopenedinQtCreator,you
cansimplypresstheF4keytobetransferredtothecorresponding
headerfile)anddeclareaclicked(constQPoint&)signal:
signals:
voidclicked(constQPoint&);
Todetectmouseinput,wehavetooverrideoneofthemouseevent
handlersawidgethas:eithermousePressEventormouseReleaseEvent.It
seemsobviousthatweshouldchoosetheformerevent;thiswould
work,butitisnotthebestdecision.Justthinkaboutthesemantics
ofamouseclick:itisacomplexeventcomposedofpushingand
releasingthemousebutton.Theactual"click"takesplaceafterthe
mouseisreleased.Therefore,let'susemouseReleaseEventasourevent
handler:
voidChessView::mouseReleaseEvent(QMouseEvent*event)
{
QPointpt=fieldAt(event->pos());
if(pt.isNull()){
return;
}
}
emitclicked(pt);
}
Thecodeissimple;weusethemethodwejustimplementedand
passitthepositionreadfromtheQMouseEventobject.Ifthereturned
pointisinvalid,wequietlyreturnfromthemethod.Otherwise,
clicked()isemittedwiththeobtainedcolumnandrankvalues.
Wecanmakeuseofthesignalnow.Gototheconstructorof
MainWindowandaddthefollowinglinetoconnectthewidget'sclicked
signaltoacustomslot:
connect(m_view,&ChessView::clicked,
this,&MainWindow::viewClicked);
Declaretheslotandimplementit,asfollows:
voidMainWindow::viewClicked(constQPoint&field)
{
if(m_clickPoint.isNull()){
m_clickPoint=field;
}else{
if(field!=m_clickPoint){
m_view->board()->movePiece(
m_clickPoint.x(),m_clickPoint.y(),
field.x(),field.y()
);
}
m_clickPoint=QPoint();
}
}
Thefunctionusesaclassmembervariable—m_clickPoint—tostorethe
clickedfield.Thevariablevalueismadeinvalidafteramoveis
made.Thus,wecandetectwhethertheclickwearecurrently
handlinghas"select"or"move"semantics.Inthefirstcase,we
storetheselectioninm_clickPoint;intheothercase,weasktheboard
tomakeamoveusingthehelpermethodweimplementedsome
timeago.Remembertodeclarem_clickPointasaprivatemember
variableofMainWindow.
Allshouldbeworkingnow.However,ifyoubuildtheapplication,
runit,andstartclickingaroundonthechessboard,youwillsee
thatnothinghappens.Thisisbecauseweforgottotelltheviewto
refreshitselfwhenthegamepositionontheboardischanged.We
havetoconnectthesignalsthattheboardemitstotheupdate()slotof
theview.OpenthesetBoard()methodofthewidgetclassandfixit,as
follows:
voidChessView::setBoard(ChessBoard*board)
{
//...
m_board=board;
//connectsignals
if(board){
connect(board,SIGNAL(dataChanged(int,int)),
this,SLOT(update()));
connect(board,SIGNAL(boardReset()),
this,SLOT(update()));
}
updateGeometry();
}
Ifyouruntheprogramnow,movesyoumakewillbereflectedin
thewidget,asshown:
Atthispoint,wemightconsiderthevisualpartofthegameas
finished,butthereisstilloneproblemyoumighthavespottedwhile
testingourlatestadditions.Whenyouclickontheboard,thereis
novisualhintthatanypiecewasactuallyselected.Let'sfixthatnow
byintroducingtheabilitytohighlightanyfieldontheboard.
Todothat,wewilldevelopagenericsystemfordifferenthighlights.
BeginbyaddingaHighlightclassasaninternalclasstoChessView:
classChessView:publicQWidget
//...
public:
classHighlight{
public:
Highlight(){}
virtual~Highlight(){}
virtualinttype()const{return0;}
};
//...
};
Itisaminimalisticinterfaceforhighlightsandonlyexposesa
methodreturningthetypeofthehighlightusingavirtualmethod.
Inourexercise,wewillfocusonjustabasictypethatmarksasingle
fieldwithagivencolor.Suchasituationwillberepresentedbythe
FieldHighlightclass:
classFieldHighlight:publicHighlight{
public:
enum{Type=1};
FieldHighlight(intcolumn,intrank,QColorcolor)
:m_field(column,rank),m_color(color){}
inlineintcolumn()const{returnm_field.x();}
inlineintrank()const{returnm_field.y();}
inlineQColorcolor()const{returnm_color;}
inttype()const{returnType;}
private:
QPointm_field;
QColorm_color;
};
Youcanseethatweprovidedaconstructorthattakesthecolumn
andrankindicesandacolorforthehighlightanditstoresthemin
privatemembervariables.Also,type()isredefinedtoreturn
FieldHighlight::Type,whichwecanusetoeasilyidentifythetypeof
highlight.ThenextstepistoextendChessViewwithabilitiestoadd
andremovehighlights.Asthecontainerdeclaresaprivate
QList<Highlight*>m_highlightsmembervariable,addmethod
declarations:
public:
voidaddHighlight(Highlight*hl);
voidremoveHighlight(Highlight*hl);
inlineHighlight*highlight(intindex)const{
returnm_highlights.at(index);
}
inlineinthighlightCount()const{
returnm_highlights.size();
}
}
Next,provideimplementationsfornon-inlinemethods:
voidChessView::addHighlight(ChessView::Highlight*hl){
m_highlights.append(hl);
update();
}
voidChessView::removeHighlight(ChessView::Highlight*hl){
m_highlights.removeOne(hl);
update();
}
Drawingthehighlightsisreallyeasy;wewilluseyetanothervirtual
drawmethod.PlacethefollowingcallinthepaintEvent()
implementationrightbeforetheloopthatisresponsiblefor
renderingpieces:
drawHighlights(&painter);
Theimplementationsimplyiteratesoverallthehighlightsand
rendersthoseitunderstands:
voidChessView::drawHighlights(QPainter*painter)
{
for(intidx=0;idx<highlightCount();++idx){
Highlight*hl=highlight(idx);
if(hl->type()==FieldHighlight::Type){
FieldHighlight*fhl=static_cast<FieldHighlight*>(hl);
QRectrect=fieldRect(fhl->column(),fhl->rank());
painter->fillRect(rect,fhl->color());
}
}
}
Bycheckingthetypeofthehighlight,weknowwhichclasstocast
thegeneric
pointerto.Then,wecanquerytheobjectfortheneededdata.
Finally,weuseQPainter::fillRect()tofillthefieldwiththegivencolor.
AsdrawHighlights()iscalledbeforethepiecepaintingloopandafter
thefieldpaintingloop,thehighlightwillcoverthebackgroundbut
notthepiece.
That'sthebasichighlightingsystem.Let'smakeourviewClicked()slot
useit:
voidMainWindow::viewClicked(constQPoint&field)
{
if(m_clickPoint.isNull()){
if(m_view->board()->data(field.x(),field.y())!=''){
m_clickPoint=field;
m_selectedField=newChessView::FieldHighlight(
field.x(),field.y(),QColor(255,0,0,50)
);
m_view->addHighlight(m_selectedField);
}
}else{
if(field!=m_clickPoint){
m_view->board()->movePiece(
m_clickPoint.x(),m_clickPoint.y(),field.x(),
field.y()
);
};
m_clickPoint=QPoint();
m_view->removeHighlight(m_selectedField);
deletem_selectedField;
m_selectedField=nullptr;
}
}
Notehowwecheckthatafieldcanonlybeselectedifitisnotempty
(thatis,thereisanexistingpieceoccupyingthatfield).
YoushouldalsoaddaChessView::FieldHighlight*m_selectedFieldprivate
membervariableandinitializeitwithanullpointerinthe
constructor.Youcannowbuildthegame,executeit,andstart
movingpiecesaround:
Whatjusthappened?
Byaddingafewlinesofcode,wemanagedtomaketheboard
clickable.Weconnectedacustomslotthatreadswhichfieldwas
clickedonandcanhighlightitwithasemitransparentredcolor.
Clickingonanotherfieldwillmovethehighlightedpiecethere.The
highlightingsystemwedevelopedisverygeneric.Weuseitto
highlightasinglefieldwithasolidcolor,butyoucanmarkasmany
fieldsasyouwantwithanumberofdifferentcolors,forexample,to
showvalidmovesafterselectingapiece.Thesystemcaneasilybe
extendedwithnewtypesofhighlights;forexample,youcandraw
arrowsontheboardusingQPainterPathtohaveacomplexhinting
system(say,showingtheplayerthesuggestedmove).
Timeforaction–Connecting
thegamealgorithm
Itwouldtakeustoolongtoimplementafullchessgamealgorithm
here,soinstead,wewillsettleforamuchsimplergamecalledFox
andHounds.Oneoftheplayershasfourpawns(hounds),which
canonlymoveoverblackfieldsandthepawncanonlymoveina
forwardfashion(towardhigherranks).Theotherplayerhasjusta
singlepawn(fox),whichstartsfromtheoppositesideoftheboard:
Itcanalsomoveonlyoverblackfields;howeveritcanmoveboth
forward(towardhigherranks)aswellasbackward(towardlower
ranks).Playersmovetheirpawnsinturn.Thegoalofthefoxisto
reachtheoppositeendoftheboard;thegoalofthehoundsisto
trapthefoxsothatitcan'tmakeamove:
It'stimetogettowork!First,wewillextendtheChessAlgorithmclass
withtherequiredinterface:
classChessAlgorithm:publicQObject
{
Q_OBJECT
Q_PROPERTY(ResultresultREADresult)
Q_PROPERTY(PlayercurrentPlayer
READcurrentPlayer
NOTIFYcurrentPlayerChanged)
public:
enumResult{NoResult,Player1Wins,Draw,Player2Wins};
Q_ENUM(Result)
enumPlayer{NoPlayer,Player1,Player2};
Q_ENUM(Player)
explicitChessAlgorithm(QObject*parent=0);
ChessBoard*board()const;
inlineResultresult()const{
returnm_result;
}
inlinePlayercurrentPlayer()const{
returnm_currentPlayer;
}
signals:
voidboardChanged(ChessBoard*);
voidgameOver(Result);
voidcurrentPlayerChanged(Player);
publicslots:
virtualvoidnewGame();
virtualboolmove(intcolFrom,intrankFrom,intcolTo,int
rankTo);
boolmove(constQPoint&from,constQPoint&to);
protected:
virtualvoidsetupBoard();
voidsetBoard(ChessBoard*board);
voidsetResult(Result);
voidsetCurrentPlayer(Player);
private:
ChessBoard*m_board;
Resultm_result;
Playerm_currentPlayer;
};
Therearetwosetsofmembershere.First,wehaveanumberof
enums,variables,signals,andmethodsthatarerelatedtothestate
ofthegame:whichplayershouldmaketheirmovenowandwhatis
theresultofthegamecurrently.TheQ_ENUMmacroisusedtoregister
enumerationsinQt'smetatypesystemsothattheycanbeusedas
valuesforpropertiesorargumentsinsignals.Propertydeclarations
andgettersforthemdon'tneedanyextraexplanation.Wehavealso
declaredprotectedmethodsforsettingthevariablesfromwithin
subclasses.Here'stheirsuggestedimplementation:
voidChessAlgorithm::setResult(Resultvalue)
{
if(result()==value){
return;
}
}
if(result()==NoResult){
m_result=value;
emitgameOver(m_result);
}else{
m_result=value;
}
}
voidChessAlgorithm::setCurrentPlayer(Playervalue)
{
if(currentPlayer()==value){
return;
}
m_currentPlayer=value;
emitcurrentPlayerChanged(m_currentPlayer);
}
Rememberaboutinitializingm_currentPlayerandm_resulttoNoPlayerand
NoResultintheconstructoroftheChessAlgorithmclass.
Thesecondgroupoffunctionsismethodsthatmodifythestateof
thegame:thetwovariantsofmove().Thevirtualvariantismeantto
bereimplementedbytherealalgorithmtocheckwhetheragiven
moveisvalidinthecurrentgamestateandifthatisthecase,to
performtheactualmodificationofthegameboard.Inthebase
class,wecansimplyrejectallpossiblemoves:
boolChessAlgorithm::move(intcolFrom,intrankFrom,
intcolTo,intrankTo)
{
Q_UNUSED(colFrom)
Q_UNUSED(rankFrom)
Q_UNUSED(colTo)
Q_UNUSED(rankTo)
returnfalse;
}
TheoverloadissimplyaconveniencemethodthatacceptstwoQPoint
objectsinsteadoffourintegers:
boolChessAlgorithm::move(constQPoint&from,constQPoint&to)
boolChessAlgorithm::move(constQPoint&from,constQPoint&to)
{
returnmove(from.x(),from.y(),to.x(),to.y());
}
Theinterfaceforthealgorithmisreadynow,andwecanimplement
itfortheFoxandHoundsgame.SubclassChessAlgorithmtocreatea
FoxAndHoundsclass:
classFoxAndHounds:publicChessAlgorithm
{
public:
FoxAndHounds(QObject*parent=0);
voidnewGame();
boolmove(intcolFrom,intrankFrom,intcolTo,intrankTo);
};
TheimplementationofnewGame()isprettysimple:wesetupthe
board,placepiecesonit,andsignalthatitistimeforthefirstplayer
tomaketheirmove:
voidFoxAndHounds::newGame()
{
setupBoard();
board()->setFen("3p4/8/8/8/8/8/8/P1P1P1P1w");
//'w'-whitetomove
m_fox=QPoint(5,8);
setResult(NoResult);
setCurrentPlayer(Player1);
}
Thealgorithmforthegameisquitesimple.Implementmove()as
follows:
boolFoxAndHounds::move(intcolFrom,intrankFrom,
intcolTo,intrankTo)
{
if(currentPlayer()==NoPlayer){
returnfalse;
}
//isthereapieceoftherightcolor?
charsource=board()->data(colFrom,rankFrom);
if(currentPlayer()==Player1&&source!='P')returnfalse;
if(currentPlayer()==Player2&&source!='p')returnfalse;
//bothcanonlymoveonecolumnrightorleft
if(colTo!=colFrom+1&&colTo!=colFrom-1)returnfalse;
//dowemovewithintheboard?
if(colTo<1||colTo>board()->columns())returnfalse;
if(rankTo<1||rankTo>board()->ranks())returnfalse;
//isthedestinationfieldblack?
if((colTo+rankTo)%2)returnfalse;
//isthedestinationfieldempty?
chardestination=board()->data(colTo,rankTo);
if(destination!='')returnfalse;
//iswhiteadvancing?
if(currentPlayer()==Player1&&rankTo<=rankFrom)returnfalse;
board()->movePiece(colFrom,rankFrom,colTo,rankTo);
//makethemove
if(currentPlayer()==Player2){
m_fox=QPoint(colTo,rankTo);//cachefoxposition
}
//checkwincondition
if(currentPlayer()==Player2&&rankTo==1){
setResult(Player2Wins);//foxhasescaped
}elseif(currentPlayer()==Player1&&!foxCanMove()){
setResult(Player1Wins);//foxcan'tmove
}else{
//theotherplayermakesthemovenow
setCurrentPlayer(currentPlayer()==Player1?Player2:
Player1);
}
returntrue;
}
DeclareaprotectedfoxCanMove()methodandimplementitusingthe
followingcode:
boolFoxAndHounds::foxCanMove()const
{
if(emptyByOffset(-1,-1)||emptyByOffset(-1,1)||
if(emptyByOffset(-1,-1)||emptyByOffset(-1,1)||
emptyByOffset(1,-1)||emptyByOffset(1,1)){
returntrue;
}
returnfalse;
}
Then,dothesamewithemptyByOffset():
boolFoxAndHounds::emptyByOffset(intx,inty)const
{
constintdestCol=m_fox.x()+x;
constintdestRank=m_fox.y()+y;
if(destCol<1||destRank<1||
destCol>board()->columns()||
destRank>board()->ranks()){
returnfalse;
}
return(board()->data(destCol,destRank)=='');
}
Lastly,declareaprivateQPointm_foxmembervariable.
Thesimplestwaytotestthegameistomaketwochangestothe
code.First,intheconstructorofthemainwindowclass,replace
m_algorithm=newChessAlgorithm(this)withm_algorithm=newFoxAndHounds(this).
Second,modifytheviewClicked()slot,asfollows:
voidMainWindow::viewClicked(constQPoint&field)
{
if(m_clickPoint.isNull()){
//...
}else{
if(field!=m_clickPoint){
m_algorithm->move(m_clickPoint,field);
}
//...
}
}
Youcanalsoconnectsignalsfromthealgorithmclasstocustom
slotsofthevieworwindowtonotifyabouttheendofthegameand
provideavisualhintastowhichplayershouldmaketheirmove
now.
Whatjusthappened?
WecreatedaverysimplisticAPIforimplementingchess-likegames
byintroducingthenewGame()andmove()virtualmethodstothe
algorithmclass.Theformermethodsimplysetsupeverything.The
latterusessimplecheckstodeterminewhetheraparticularmoveis
validandwhetherthegamehasended.Weusethem_foxmember
variabletotrackthecurrentpositionofthefoxtobeabletoquickly
determinewhetherithasanyvalidmoves.Whenthegameends,the
gameOver()signalisemittedandtheresultofthegamecanbeobtained
fromthealgorithm.Youcanusetheexactsameframeworkfor
implementingallchessrules.
Haveagohero–Implementing
theUIaroundthechessboard
Duringtheexercise,wefocusedondevelopingthegameboardview
andnecessaryclassestomakethegameactuallyrun.However,we
completelyneglectedtheregularuserinterfacethegamemight
possess,suchastoolbarsandmenus.Youcantrydesigningasetof
menusandtoolbarsforthegame.Makeitpossibletostartanew
game,saveagameinprogress(saybyimplementingaFEN
serializer),loadasavedgame(saybyleveragingtheexistingFEN
stringparser),orchoosedifferentgametypesthatwillspawn
differentChessAlgorithmsubclasses.Youcanalsoprovideasettings
dialogforadjustingthelookofthegameboard.Ifyoufeellikeit,
youcanaddchessclocksorimplementasimpletutorialsystemthat
willguidetheplayerthroughthebasicsofchessusingtextand
visualhintsviathehighlightsystemweimplemented.
Haveagohero–Connectinga
UCI-compliantchessengine
Ifyoureallywanttotestyourskills,youcanimplementa
ChessAlgorithmsubclassthatwillconnecttoaUniversalChess
Interface(UCI)chessenginesuchasStockFish
(http://stockfishchess.org)andprovideachallengingartificial
intelligenceopponentforahumanplayer.UCIisthedefacto
standardforcommunicationbetweenachessengineandachess
frontend.Itsspecificationisfreelyavailable,soyoucanstudyiton
yourown.TotalktoaUCI-compliantengine,youcanuseQProcess,
whichwillspawntheengineasanexternalprocessandattachitself
toitsstandardinputandstandardoutput.Then,youcansend
commandstotheenginebywritingtoitsstandardinputandread
messagesfromtheenginebyreadingitsstandardoutput.Toget
youstarted,here'sashortsnippetofcodethatstartstheengineand
attachestoitscommunicationchannels:
classUciEngine:publicQObject{
Q_OBJECT
public:
UciEngine(QObject*parent=0):QObject(parent){
m_uciEngine=newQProcess(this);
m_uciEngine->setReadChannel(QProcess::StandardOutput);
connect(m_uciEngine,SIGNAL(readyRead()),
SLOT(readFromEngine()));
}
publicslots:
voidstartEngine(constQString&enginePath){
m_uciEngine->start(enginePath);
}
voidsendCommand(constQString&command){
m_uciEngine->write(command.toLatin1());
}
privateslots:
voidreadFromEngine(){
voidreadFromEngine(){
while(m_uciEngine->canReadLine()){
QStringline=QString::fromLatin1(m_uciEngine-
>readLine());
emitmessageReceived(line);
}
}
signals:
voidmessageReceived(QString);
private:
QProcess*m_uciEngine;
};
Popquiz
Q1.WhichclassshouldyouusetoloadaJPEGimagefromafile
andchangeafewpixelsinit?
1. QImage
2. QPixmap
3. QIcon
Q2.Whichfunctioncanbeusedtoschedulearepaintofthewidget?
1. paintEvent()
2. update()
3. show()
Q3.Whichfunctioncanbeusedtochangethecoloroftheoutline
drawnbyQPainter?
1. setColor()
2. setBrush()
3. setPen()
Summary
Inthischapter,welearnedaboutusingrastergraphicswithQt
Widgets.Whatwaspresentedinthischapterwillletyouimplement
customwidgetswithpaintingandeventhandling.Wealso
describedhowtohandleimagefilesanddosomebasicpaintingon
images.ThischapterconcludesouroverviewofCPUrenderingin
Qt.
Inthenextchapter,wewillswitchfromrasterpaintingto
acceleratedvectorgraphicsandexploreQtcapabilitiesrelatedto
OpenGLandVulkan.
OpenGLandVulkaninQt
applications
Hardwareaccelerationiscrucialforimplementingmoderngames
withadvancedgraphicseffects.QtWidgetsmoduleusestraditional
approachoptimizedforCPU-basedrendering.Eventhoughyoucan
makeanywidgetuseOpenGL,theperformancewillusuallynotbe
maximized.However,QtallowsyoutouseOpenGLorVulkan
directlytocreatehigh-performancegraphicslimitedonlybythe
graphicscard'sprocessingpower.Inthischapter,youwilllearn
aboutemployingyourOpenGLandVulkanskillstodisplayfast3D
graphics.Ifyouarenotfamiliarwiththesetechnologies,this
chaptershouldgiveyouakickstartforfurtherresearchinthistopic.
WewillalsodescribemultipleQthelperclassesthatsimplifyusage
ofOpenGLtextures,shaders,andbuffers.Bytheendofthechapter,
youwillbeabletocreate2Dand3Dgraphicsforyourgamesusing
OpenGLandVulkanclassesofferedbyQtandintegratethemwith
therestoftheuserinterface.
Themaintopicscoveredinthischapterareaslisted:
OpenGLinQtapplications
Immediatemode
Textures
Shaders
OpenGLbuffers
VulkaninQtapplications
IntroductiontoOpenGLwithQt
WearenotexpertsonOpenGL,sointhispartofthechapter,we
willnotteachyoutodoanyfancystuffwithOpenGLandQtbutwill
showyouhowtoenabletheuseofyourOpenGLskillsinQt
applications.TherearealotoftutorialsandcoursesonOpenGLout
there,soifyou'renotthatskilledwithOpenGL,youcanstillbenefit
fromwhatisdescribedherebyemployingtheknowledgegained
heretomoreeasilylearnfancystuff.Youcanuseexternalmaterials
andahigh-levelAPIofferedbyQt,whichwillspeedupmanyofthe
tasksdescribedinthetutorials.
OpenGLwindowsandcontexts
TherearemanywaysyoucanperformOpenGLrenderinginQt.
Themoststraightforwardwaythatwewillmainlyuseisto
subclassQOpenGLWindow.ItallowsOpenGLtorenderyourcontent
directlytoawholewindowandissuitableifyoudraweverythingin
yourapplicationwithOpenGL.Youcanmakeitafullscreenwindow
ifyouwant.However,laterwewillalsodiscussotherapproaches
thatwillallowyoutointegrateOpenGLcontentintoawidget-based
application.
TheOpenGLcontextrepresentstheoverallstateoftheOpenGL
pipeline,whichguidestheprocessofdataprocessingandrendering
toaparticulardevice.InQt,itisrepresentedby
theQOpenGLContextclass.Arelatedconceptthatneedsexplanationis
theideaofanOpenGLcontextbeing"current"inathread.Theway
OpenGLcallsworkisthattheydonotuseanyhandletoanyobject
containinginformationonwhereandhowtoexecutetheseriesof
low-levelOpenGLcalls.Instead,itisassumedthattheyare
executedinthecontextofthecurrentmachinestate.Thestatemay
dictatewhethertorenderascenetoascreenortoaframebuffer
object,whichmechanismsareenabled,orthepropertiesofthe
surfaceOpenGLisrenderingon.Makingacontext"current"means
thatallfurtherOpenGLoperationsissuedbyaparticularthreadwill
beappliedtothiscontext.Toaddtothat,acontextcanbe"current"
onlyinonethreadatthesametime;therefore,itisimportantto
makethecontextcurrentbeforemakinganyOpenGLcallsandthen
markingitasavailableafteryouaredoneaccessingOpenGL
resources.
QOpenGLWindowhasaverysimpleAPIthathidesmostoftheunnecessary
detailsfromthedeveloper.Apartfromconstructorsanda
destructor,itprovidesasmallnumberofveryusefulmethods.First,
thereareauxiliarymethodsformanagingtheOpenGLcontext:
context(),whichreturnsthecontext,andmakeCurrent()aswellas
doneCurrent()foracquiringandreleasingthecontext.Theclassalso
providesanumberofvirtualmethodswecanre-implementto
displayOpenGLgraphics.
Wewillbeusingthefollowingthreevirtualmethods:
initializeGL()isinvokedbytheframeworkonce,beforeany
paintingisactuallydonesothatyoucanprepareany
resourcesorinitializethecontextinanywayyourequire.
paintGL()istheequivalentofpaintEvent()forthewidgetclasses.
Itgetsexecutedwheneverthewindowneedstoberepainted.
ThisisthefunctionwhereyoushouldputyourOpenGL
renderingcode.
resizeGL()isinvokedeverytimethewindowisresized.It
acceptsthewidthandheightofthewindowasparameters.
Youcanmakeuseofthatmethodbyre-implementingitso
thatyoucanprepareyourselfforthefactthatthenextcall
topaintGL()renderstoaviewportofadifferentsize.
Beforecallinganyofthesevirtualfunctions,QOpenGLWindowensuresthat
theOpenGLcontextiscurrent,sothereisnoneedto
manuallycallmakeCurrent()inthem.
AccessingOpenGLfunctions
InteractionwithOpenGLisusuallydonethroughcallingfunctions
providedbytheOpenGLlibrary.Forexample,inaregularC++
OpenGLapplication,youcanseecallstoOpenGLfunctionssuch
asglClearColor().Thesefunctionsareresolvedwhenyourbinaryis
linkedagainsttheOpenGLlibrary.However,whenyouwritea
cross-platformapplication,resolvingalltherequiredOpenGL
functionsisnottrivial.Luckily,QtprovidesawaytocallOpenGL
functionswithouthavingtoworryabouttheplatform-specific
details.
InaQtapplication,youshouldaccessOpenGLfunctionsthrougha
familyofQOpenGLFunctionsclasses.TheQOpenGLFunctionsclassitselfonly
providesaccesstofunctionsthatarepartofOpenGLES2.0API.
Thissubsetisexpectedtoworkatmostdesktopandembedded
platformssupportedbyQt(whereOpenGLisavailableatall).
However,thisisareallylimitedsetoffunctions,andsometimesyou
maywanttouseamorerecentOpenGLversionatthecostof
supportinglessplatforms.ForeachknownOpenGLversionand
profile,Qtprovidesaseparateclassthatcontainsthesetof
availablefunctions.Forexample,theQOpenGLFunctions_3_3_Coreclasswill
containallfunctionsprovidedbytheOpenGL3.3coreprofile.
TheapproachrecommendedbyQtistoselecttheOpenGL
functionsclasscorrespondingtotheversionyouwanttouseand
addthisclassanthesecondbaseclassofyourwindoworwidget.
ThiswillmakeallOpenGLfunctionsfromthatversionavailable
withinyourclass.Thisapproachallowsyoutousecodethatwas
usingtheOpenGLlibrarydirectlywithoutchangingit.Whenyou
putsuchcodeinyourclass,thecompilerwill,forexample,use
theQOpenGLFunctions::glClearColor()functioninsteadofthe
globalglClearColor()functionprovidedbytheOpenGLlibrary.
However,whenusingthisapproach,youmustbecarefultoonlyuse
functionsprovidedbyyourbaseclass.Youcanaccidentallyusea
globalfunctioninsteadofafunctionprovidedbyQtclassesiftheQt
classyouchoosedoesnotcontainit.Forexample,ifyou
useQOpenGLFunctionsasthebaseclass,youcan'tusetheglBegin()
function,asitisnotprovidedbythisQtclass.Sucherroneouscode
mayworkononeoperatingsystemandthensuddenlynotcompile
onanotherbecauseyoudon'tlinkagainsttheOpenGLlibrary.As
longasyouonlyuseOpenGLfunctionsprovidedbyQtclasses,you
don'thavetothinkaboutlinkingwiththeOpenGLlibraryor
resolvingfunctionsinacross-platformway.
IfyouwanttoensurethatyouonlyuseQtOpenGLfunction
wrappers,youcanusetheQtclassasaprivatefieldinsteadofa
baseclass.Inthatcase,youhavetoaccesseveryOpenGLfunction
throughtheprivatefield,forexample,m_openGLFunctions->glClearColor().
Thiswillmakeyourcodemoreverbose,butatleastyouwillbesure
thatyoudon'taccidentallyuseaglobalfunction.
BeforeusingQtOpenGLfunctions,youhavetocall
theinitializeOpenGLFunctions()methodofthefunctionsclassinthe
currentOpenGLcontext.ThisisusuallydoneintheinitializeGL()
functionofthewindow.TheQOpenGLFunctionsclassisexpectedto
alwaysinitializesuccessfully,soitsinitializeOpenGLFunctions()method
doesn'treturnanything.Inalltheotherfunctions'classes,this
functionreturnsbool.Ifitreturnsfalse,itmeansthatQtwasnotable
toresolvealltherequiredfunctionssuccessfully,andyour
applicationshouldexitwithanerrormessage.
Inourexamples,wewillusetheQOpenGLFunctions_1_1classthatcontains
allOpenGLfunctionswe'lluse.Whenyou'recreatingyourown
project,thinkabouttheOpenGLprofileyouwanttotargetand
selecttheappropriatefunctionsclass.
UsingOpenGLinimmediate
mode
Wewillstartwiththemostbasicapproachthat'scalledimmediate
mode.Inthismode,noadditionalsetupofOpenGLbuffersor
shadersisrequired.Youcanjustsupplyabunchofgeometric
primitivesandgettheresultrightaway.Immediatemodeisnow
deprecatedbecauseitworksmuchslowerandislessflexiblethan
moreadvancedtechniques.However,it'ssomucheasierthanthem
thatbasicallyeveryOpenGLtutorialstartswithdescribingthe
immediatemodecalls.Inthissection,we'llshowhowtoperform
somesimpleOpenGLdrawingwithverylittlecode.Amoremodern
approachwillbecoveredinthenextsectionofthischapter.
Timeforaction–Drawinga
triangleusingQtandOpenGL
Forthefirstexercise,wewillcreateasubclassofQOpenGLWindowthat
rendersatriangleusingsimpleOpenGLcalls.Createanewproject,
startingwithEmptyqmakeProjectfromtheOtherProjectgroupas
thetemplate.Intheprojectfile,putthefollowingcontent:
QT=coregui
TARGET=triangle
TEMPLATE=app
NotethatourprojectdoesnotincludeQtWidgetsmodule.UsingtheQOpenGLWindow
approachallowsustoremovethisunnecessarydependencyandmakeourapplication
morelightweight.
NotethatQtCoreandQtGUImodulesareenabledbydefault,so
youdon'thavetoaddthemtotheQTvariable,butwepreferto
explicitlyshowthatweareusingtheminourproject.
Havingthebasicprojectsetupready,let'sdefineaSimpleGLWindowclass
asasubclassofQOpenGLWindowandQOpenGLFunctions_1_1.Sincewedon't
wanttoallowexternalaccesstoOpenGLfunctions,weuse
protectedinheritanceforthesecondbase.Next,weoverridethe
virtualinitializeGL()methodofQOpenGLWindow.Inthismethod,we
initializeourQOpenGLFunctions_1_1baseclassandusetheglClearColor()
functionthatitprovides:
classSimpleGLWindow:publicQOpenGLWindow,
protectedQOpenGLFunctions_1_1
{
public:
SimpleGLWindow(QWindow*parent=0):
QOpenGLWindow(NoPartialUpdate,parent){
}
protected:
protected:
voidinitializeGL(){
if(!initializeOpenGLFunctions()){
qFatal("initializeOpenGLFunctionsfailed");
}
glClearColor(1,1,1,0);
}
};
IninitializeGL(),wefirstcallinitializeOpenGLFunctions(),whichisa
methodoftheQOpenGLFunctions_1_1class,oneofthebaseclassesofour
windowclass.Themethodtakescareofsettingupallthefunctions
accordingtotheparametersofthecurrentOpenGLcontext(thus,it
isimportanttofirstmakethecontextcurrent,whichluckilyisdone
forusbehindthescenesbeforeinitializeGL()isinvoked).Ifthis
functionfails,weusetheqFatal()macrotoprintanerrormessageto
stderrandaborttheapplication.Then,weuse
theQOpenGLFunctions_1_1::glClearColor()functiontosettheclearcolorof
thescenetowhite.
Thenextstepistore-implementpaintGL()andputtheactualdrawing
codethere:
voidSimpleGLWindow::paintGL(){
glClear(GL_COLOR_BUFFER_BIT);
glViewport(0,0,width(),height());
glBegin(GL_TRIANGLES);
{
glColor3f(1,0,0);
glVertex3f(0.0f,1.0f,0.0f);
glColor3f(0,1,0);
glVertex3f(1.0f,-1.0f,0.0f);
glColor3f(0,0,1);
glVertex3f(-1.0f,-1.0f,0.0f);
}
glEnd();
}
ThisfunctionfirstclearsthecolorbufferandsetstheOpenGL
viewportofthecontexttobethesizeofthewindow.Then,wetell
OpenGLtostartdrawingusingtriangleswiththeglBegin()calland
passingGL_TRIANGLESasthedrawingmode.Then,wepassthree
verticesalongwiththeircolorstoformatriangle.Finally,we
informthepipeline,byinvokingglEnd(),thatwearedonedrawing
usingthecurrentmode.
Whatisleftisatrivialmain()functionthatsetsupthewindowand
startstheeventloop.AddanewC++SourceFile,callitmain.cpp,and
implementmain(),asfollows:
intmain(intargc,char**argv){
QGuiApplicationapp(argc,argv);
SimpleGLWindowwindow;
window.resize(600,400);
window.show();
returnapp.exec();
}
Thisfunctionisverysimilartowhatweusuallyhaveinthemain()
function,butweuseQGuiApplicationinsteadofQApplication,becausewe
onlyusetheQtGUImodule.Afterrunningtheproject,youshould
seethefollowing:
Multisampling
Youcanseethatthetrianglehasjaggededges.That'sbecauseofthe
aliasingeffect.Youcancounteritbyenablingmultisamplingforthe
window,whichwillmakeOpenGLrenderthecontentsasifthe
screenhadhigherresolutionandthenaveragetheresult,whichacts
asanti-aliasing.Todothat,addthefollowingcodetothe
constructorofthewindow:
QSurfaceFormatfmt=format();
fmt.setSamples(16);//multisamplingsetto16
setFormat(fmt);
Notethatmultisamplingisresource-demanding,sosettingahighnumberofsamplesmay
causeyourapplicationtofailifyourhardwareordrivercan'thandleit.Iftheapplication
doesn'tworkafterenablingmultisampling,trytolowerthenumberofsamplesorjust
disableit.
Timeforaction–Scene-based
rendering
Let'stakeourrenderingcodetoahigherlevel.PuttingOpenGL
codedirectlyintothewindowclassrequiressubclassingthewindow
classandmakesthewindowclassmoreandmorecomplex.Let's
followgoodprogrammingpracticeandseparaterenderingcode
fromwindowcode.
CreateanewclassandcallitAbstractGLScene.Itwillbethebaseclass
for
definitionsofOpenGLscenes.Wealsoderivetheclass(with
protectedscope)fromQOpenGLFunctions_1_1tomakeaccessingdifferent
OpenGLfunctionseasier.Makethesceneclassacceptapointerto
QOpenGLWindow,eitherintheconstructororthroughadedicatedsetter
method.Ensurethatthepointerisstoredintheclassforeasier
access,aswewillrelyonthatpointerforaccessingphysical
propertiesofthewindow.Addmethodsforqueryingthewindow's
OpenGLcontext.Youshouldendupwithcodesimilartothe
following:
classAbstractGLScene:protectedQOpenGLFunctions_1_1{
public:
AbstractGLScene(QOpenGLWindow*window=nullptr){
m_window=window;
}
QOpenGLWindow*window()const{returnm_window;}
QOpenGLContext*context(){
returnm_window?m_window->context():nullptr;
}
constQOpenGLContext*context()const{
returnm_window?m_window->context():nullptr;
}
private:
QOpenGLWindow*m_window=nullptr;
QOpenGLWindow*m_window=nullptr;
};
Nowtheessentialpartbegins.Addtwopurevirtualmethodscalled
paint()andinitialize().Also,remembertoaddavirtualdestructor.
Insteadofmakinginitialize()apurevirtualfunction,youcanimplementitsbodyinsuch
awaythatitwillcallinitializeOpenGLFunctions()tofulfilltherequirementsofthe
QOpenGFunctionsclass.Then,subclassesofAbstractGLScenecanensurethatthefunctionsare
initializedproperlybycallingthebaseclassimplementationofinitialize().
Next,createasubclassofQOpenGLWindowandcallitSceneGLWindow.Add
anAbstractGLScene*m_sceneprivatefieldandimplementagetteranda
setterforit.Createaconstructorusingthefollowingcode:
SceneGLWindow::SceneGLWindow(QWindow*parent):
QOpenGLWindow(NoPartialUpdate,parent)
{
}
Thisconstructorforwardstheparentargumenttothebase
constructorandassignsNoPartialUpdateasthewindow'sUpdateBehavior.
Thisoptionmeansthatthewindowwillbefullypaintedoneach
paintGL()callandthusnoframebufferisneeded.Thisisthedefault
valueofthefirstargument,butsinceweprovidethesecond
argument,weareobligatedtoprovidethefirstargumentexplicitly.
Then,re-implementtheinitializeGL()andpaintGL()methodsandmake
themcallappropriateequivalentsinthescene:
voidSceneGLWindow::initializeGL(){
if(m_scene){
m_scene->initialize();
}
}
voidSceneGLWindow::paintGL(){
if(m_scene){
m_scene->paint();
}
}
Finally,instantiateSceneGLWindowinthemain()function.
Whatjusthappened?
Wehavejustsetupaclasschainthatseparatesthewindowcode
fromtheactualOpenGLscene.Thewindowforwardsallcalls
relatedtoscenecontentstothesceneobjectsothatwhenthe
windowisrequestedtorepaintitself,itdelegatesthetasktothe
sceneobject.Notethatpriortodoingthat,thewindowwillmake
theOpenGLcontextcurrent;therefore,allOpenGLcallsthatthe
scenemakeswillberelatedtothatcontext.Youcanstorethecode
createdinthisexerciseforlaterreuseinfurtherexercisesandyour
ownprojects.
Timeforaction–Drawinga
texturedcube
CreateanewclassnamedCubeGLSceneandderiveitfromAbstractGLScene.
Implementtheconstructortoforwarditsargumenttothebaseclass
constructor.AddamethodtostoreaQImageobjectinthescenethat
willcontaintexturedataforthecube.AddaQOpenGLTexturepointer
memberaswell,whichwillcontainthetexture,initializeittonullptr
intheconstructor,anddeleteitinthedestructor.Let'scallthe
m_textureImageimageobjectandthem_texturetexture.Nowadda
protectedinitializeTexture()methodandfillitwiththefollowingcode:
voidCubeGLScene::initializeTexture(){
m_texture=newQOpenGLTexture(m_textureImage.mirrored());
m_texture-
>setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_texture->setMagnificationFilter(QOpenGLTexture::Linear);
}
Thefunctionfirstmirrorstheimagevertically.Thisisbecausethey
axisinOpenGLpointsupbydefault,soatexturewouldbe
displayed"upsidedown".Then,wecreateaQOpenGLTextureobject,
passingitourimage.Afterthat,wesetminificationand
magnificationfilterssothatthetexturelooksbetterwhenitis
scaled.
Wearenowreadytoimplementtheinitialize()methodthatwilltake
careofsettingupthetextureandthesceneitself:
voidCubeGLScene::initialize(){
AbstractGLScene::initialize();
m_initialized=true;
if(!m_textureImage.isNull()){
initializeTexture();
initializeTexture();
}
glClearColor(1,1,1,0);
glShadeModel(GL_SMOOTH);
}
Wemakeuseofaflagcalledm_initialized.Thisflagisneededto
preventthetexturefrombeingsetuptooearly(whennoOpenGL
contextisavailableyet).Then,wecheckwhetherthetextureimage
isset(usingtheQImage::isNull()method);ifso,weinitializethe
texture.Then,wesetsomeadditionalpropertiesoftheOpenGL
context.
Inthesetterform_textureImage,addcodethatcheckswhetherm_initializedissettotrue
and,ifso,callsinitializeTexture().Thisistomakecertainthatthetextureisproperlyset
regardlessoftheorderinwhichthesetterandinitialize()arecalled.Alsorememberto
setm_initializedtofalseintheconstructor.
Thenextstepistopreparethecubedata.Wewilldefineaspecial
datastructureforthecubethatgroupsvertexcoordinatesand
texturedatainasingleobject.Tostorecoordinates,wewilluse
classestailoredtothatpurpose—QVector3DandQVector2D:
structTexturedPoint{
QVector3Dcoord;
QVector2Duv;
TexturedPoint(constQVector3D&pcoord=QVector3D(),
constQVector2D&puv=QVector2D()):
coord(pcoord),uv(puv){
}
};
QVector2D,QVector3D,andQVector4Darehelperclassesthatrepresentasinglepointinspace
andprovidesomeconvenientmethods.Forinstance,QVector2Dstorestwofloatvariables(x
andy),muchliketheQPointFclassdoes.Theseclassesarenottobeconfusedwith
QVector<T>,acontainertemplateclassthatstoresacollectionofelements.
QVector<TexturedPoint>willholdinformationforthewholecube.The
vectorisinitializedwithdatausingthefollowingcode:
voidCubeGLScene::initializeCubeData(){
m_data={
//FRONTFACE
{{-0.5,-0.5,0.5},{0,0}},{{0.5,-0.5,0.5},{1,0}},
{{-0.5,-0.5,0.5},{0,0}},{{0.5,-0.5,0.5},{1,0}},
{{0.5,0.5,0.5},{1,1}},{{-0.5,0.5,0.5},{0,1}},
//TOPFACE
{{-0.5,0.5,0.5},{0,0}},{{0.5,0.5,0.5},{1,0}},
{{0.5,0.5,-0.5},{1,1}},{{-0.5,0.5,-0.5},{0,1}},
//...
};
}
ThecodeusesC++11initializerlistsyntaxtosetthevector'sdata.
Thecubeconsistsofsixfacesandiscenteredontheoriginofthe
coordinatesystem.Thefollowingdiagrampresentsthesamedatain
graphicalform:
initializeCubeData()shouldbecalledfromthesceneconstructoror
fromtheinitialize()method.Whatremainsisthepaintingcode:
voidCubeGLScene::paint(){
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
glViewport(0,0,window()->width(),window()->height());
glLoadIdentity();
glRotatef(45,1.0,0.0,0.0);
glRotatef(45,0.0,1.0,0.0);
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
paintCube();
}
First,wesetuptheviewportandthenwerotatetheview.Before
callingpaintCube(),whichwillrenderthecubeitself,weenabledepth
testingandfacecullingsothatonlyvisiblefacesaredrawn.The
paintCube()routinelooksasfollows:
voidCubeGLScene::paintCube(){
if(m_texture){
m_texture->bind();
}
glEnable(GL_TEXTURE_2D);
glBegin(GL_QUADS);
for(constTexturedPoint&point:m_data){
glTexCoord2d(point.uv.x(),point.uv.y());
glVertex3f(point.coord.x(),point.coord.y(),point.coord.z());
}
glEnd();
glDisable(GL_TEXTURE_2D);
}
First,thetextureisboundandtexturingisenabled.Then,weenter
thequaddrawingmodeandstreamindatafromourdatastructure.
Finally,wedisabletexturingagain.
Forcompleteness,here'samain()functionthatexecutesthescene:
intmain(intargc,char**argv){
QGuiApplicationapp(argc,argv);
SceneGLWindowwindow;
QSurfaceFormatfmt;
fmt.setSamples(16);
window.setFormat(fmt);
CubeGLScenescene(&window);
window.setScene(&scene);
scene.setTexture(QImage(":/texture.jpg"));
window.resize(600,600);
window.show();
window.show();
returnapp.exec();
}
NotetheuseofQSurfaceFormattoenablemultisampleantialiasingfor
thescene.Wehavealsoputthetextureimageintoaresourcefileto
avoidproblemswiththerelativepathtothefile.
Haveagohero–Animatinga
cube
Trymodifyingthecodetomakethecubeanimated.Todothat,have
thesceneinheritQObject,addananglepropertyofthefloattypetoit
(rememberabouttheQ_OBJECTmacro).Then,modifyoneofthe
glRotatef()linestousetheanglevalueinsteadofaconstantvalue.Put
thefollowingcodeinmain(),rightbeforecallingapp.exec():
QPropertyAnimationanimation(&scene,"angle");
animation.setStartValue(0);
animation.setEndValue(359);
animation.setDuration(5000);
animation.setLoopCount(-1);
animation.start();
Remembertoputacalltowindow()->update()inthesetterfortheangle
propertysothatthesceneisredrawn.
ModernOpenGLwithQt
TheOpenGLcodeshownintheprevioussectionusesaveryold
techniqueofstreamingverticesonebyoneintoafixedOpenGL
pipeline.Nowadays,modernhardwareismuchmorefeature-rich
andnotonlydoesitallowfasterprocessingofvertexdatabutalso
offerstheabilitytoadjustdifferentprocessingstages,withtheuse
ofreprogrammableunitscalledshaders.Inthissection,wewill
takealookatwhatQthastoofferinthedomainofa"modern"
approachtousingOpenGL.
Shaders
Qtcanmakeuseofshadersthroughasetofclassesbasedaround
QOpenGLShaderProgram.Thisclassallowscompiling,linking,andexecuting
ofshaderprogramswritteninGLSL.Youcancheckwhetheryour
OpenGLimplementationsupportsshadersbyinspectingtheresult
ofastaticQOpenGLShaderProgram::hasOpenGLShaderPrograms()callthatacceptsa
pointertoanOpenGLcontext.Allmodernhardwareandalldecent
graphicsdriversshouldhavesomesupportforshaders.
Qtsupportsallkindsofshaders,withthemostcommonbeing
vertexandfragmentshaders.Thesearebothpartoftheclassic
OpenGLpipeline.Youcanseeanillustrationofthepipelineinthe
followingdiagram:
Asingleshaderisrepresentedbyaninstanceof
theQOpenGLShaderclass.Youneedtospecifythetypeoftheshaderin
theconstructorofthisclass.Then,youcancompiletheshader's
sourcecodebycallingQOpenGLShader::compileSourceCode(),whichhasa
numberofoverloadsforhandlingdifferentinputformats,
orQOpenGLShader::compileSourceFile().TheQOpenGLShaderobjectstorestheID
ofthecompiledshaderforfutureuse.
Whenyouhaveasetofshadersdefined,youcanassemblea
completeprogramusingQOpenGLShaderProgram::addShader().Afterall
shadersareadded,youcanlink()theprogramandbind()ittothe
currentOpenGLcontext.Theprogramclasshasanumberof
methodsforsettingvaluesofdifferentinputparameters—uniforms
andattributesbothinsingularandarrayversions.Qtprovides
mappingsbetweenitsowntypes(suchasQSizeorQColor)toGLSL
counterparts(forexample,vec2andvec4)tomaketheprogrammer's
lifeeveneasier.
Atypicalcodeflowforusingshadersforrenderingisasfollows
(firstavertexshaderiscreatedandcompiled):
QOpenGLShadervertexShader(QOpenGLShader::Vertex);
vertexShader.compileSourceCode(
"uniformvec4color;\n"
"uniformhighpmat4matrix;\n"
"voidmain(void){gl_Position=gl_Vertex*matrix;}"
);
Theprocessisrepeatedforafragmentshader:
QOpenGLShaderfragmentShader(QOpenGLShader::Fragment);
fragmentShader.compileSourceCode(
"uniformvec4color;\n"
"voidmain(void){gl_FragColor=color;}"
);
Then,shadersarelinkedintoasingleprograminagivenOpenGL
context:
QOpenGLShaderProgramprogram(context);
program.addShader(&vertexShader);
program.addShader(&fragmentShader);
program.link();
Whenshadersarelinkedtogether,OpenGLsearchesforcommon
variables(suchasuniformsorbuffers)inthemandmapsthem
together.Thisallowsyou,forexample,topassavaluefromthe
vertexshadertothefragmentshader.Behindthescenes,thelink()
functionusestheglLinkProgram()OpenGLcall.
Whenevertheprogramisused,itshouldbeboundtothecurrent
OpenGLcontextandfilledwiththerequireddata:
program.bind();
QMatrix4x4matrix=/*...*/;
QColorcolor=Qt::red;
program.setUniformValue("matrix",matrix);
program.setUniformValue("color",color);
Afterthat,callsactivatingtherenderpipelinewillusethebound
program:
glBegin(GL_TRIANGLE_STRIP);
//...
glEnd();
Timeforaction–Shaded
objects
Let'sconvertourlastprogramsothatitusesshaders.Tomakethe
cubebetter,wewillimplementasmoothlightingmodelusingthe
Phongalgorithm.Atthesametime,wewilllearntousesomehelper
classesthatQtoffersforusewithOpenGL.
Thebasicgoalsforthisminiprojectareasfollows:
Usevertexandfragmentshadersforrenderingacomplex
object
Handlemodel,view,andprojectionmatrices
Useattributearraysforfasterdrawing
StartbycreatinganewsubclassofAbstractGLScene.Let'sgiveitthe
followinginterface:
classShaderGLScene:publicQObject,publicAbstractGLScene{
Q_OBJECT
public:
ShaderGLScene(SceneGLWindow*window);
voidinitialize();
voidpaint();
protected:
voidinitializeObjectData();
private:
structScenePoint{
QVector3Dcoords;
QVector3Dnormal;
ScenePoint(constQVector3D&c=QVector3D(),
constQVector3D&n=QVector3D()):
constQVector3D&n=QVector3D()):
coords(c),normal(n)
{
}
};
QOpenGLShaderProgramm_shader;
QMatrix4x4m_modelMatrix;
QMatrix4x4m_viewMatrix;
QMatrix4x4m_projectionMatrix;
QVector<ScenePoint>m_data;
};
We'renotusingtexturesinthisproject,soTexturedPointwas
simplifiedtoScenePointwithUVtexturecoordinatesremoved.Update
themain()functiontousetheShaderGLSceneclass.
WecanstartimplementingtheinterfacewiththeinitializeObjectData()
functionthatwillbecalledintheconstructor.Thisfunctionmust
fillthem_datamemberwithinformationaboutverticesandtheir
normals.Theimplementationwilldependonthesourceofyour
data.
Inthesamplecodethatcomeswiththisbook,youcanfindcodethatloadsdatafromafile
inthePLYformatgeneratedwiththeBlender3Dprogram.Toexportamodelfrom
Blender,ensurethatitconsistsofjusttriangles(forthat,selectthemodel,gointotheEdit
modebypressingTab,opentheFacesmenuwithCtrl+F,andchooseTriangulateFaces).
Then,clickonFileandExport;chooseStanford(.ply).Youwillendupwithatextfile
containingvertexandnormaldataaswellasfacedefinitionsforthevertices.Weaddthe
PLYfiletotheproject'sresourcessothatitisalwaysavailabletoourprogram.Then,we
usethePlyReaderC++classthatimplementstheparsing.
Youcanalwaysreusethecubeobjectfromthepreviousproject.
Justbeawarethatitsnormalsarenotcalculatedproperlyfor
smoothshading;thus,youwillhavetocorrectthem.
Beforewecansetuptheshaderprogram,wehavetobeawareof
whattheactualshaderslooklike.Shadercodewillbeloadedfrom
externalfiles,sothefirststepistoaddanewfiletotheproject.In
Creator,right-clickontheprojectintheprojecttreeandchoose
AddNew...;fromtheleftpane,chooseGLSL,andfromthelistof
availabletemplates,chooseVertexShader(DesktopOpenGL).Call
thenewfilephong.vertandinputthefollowingcode:
uniformhighpmat4modelViewMatrix;
uniformhighpmat3normalMatrix;
uniformhighpmat4projectionMatrix;
uniformhighpmat4mvpMatrix;
attributehighpvec4Vertex;
attributemediumpvec3Normal;
varyingmediumpvec3N;
varyinghighpvec3v;
voidmain(void){
N=normalize(normalMatrix*Normal);
v=vec3(modelViewMatrix*Vertex);
gl_Position=mvpMatrix*Vertex;
}
Thecodeisverysimple.Wedeclarefourmatricesrepresenting
differentstagesofcoordinatemappingforthescene.Wealsodefine
twoinputattributes—VertexandNormal—whichcontainthevertex
data.Theshaderwilloutputtwopiecesofdata—anormalized
vertexnormalandatransformedvertexcoordinateasseenbythe
camera.Ofcourse,apartfromthat,wesetgl_Positiontobethefinal
vertexcoordinate.Ineachcase,wewanttobecompliantwiththe
OpenGL/ESspecification,soweprefixeachvariabledeclaration
withaprecisionspecifier.
Next,addanotherfile,callitphong.frag,andmakeitafragment
shader(DesktopOpenGL).Thecontentofthefileisatypical
ambient,diffuse,andspecularcalculation:
structMaterial{
lowpvec3ka;
lowpvec3kd;
lowpvec3ks;
lowpfloatshininess;
};
structLight{
lowpvec4position;
lowpvec3intensity;
};
uniformMaterialmat;
uniformLightlight;
varyingmediumpvec3N;
varyinghighpvec3v;
voidmain(void){
vec3n=normalize(N);
vec3L=normalize(light.position.xyz-v);
vec3E=normalize(-v);
vec3R=normalize(reflect(-L,n));
floatLdotN=dot(L,n);
floatdiffuse=max(LdotN,0.0);
vec3spec=vec3(0,0,0);
if(LdotN>0.0){
floatRdotE=max(dot(R,E),0.0);
spec=light.intensity*pow(RdotE,mat.shininess);
}
vec3color=light.intensity*(mat.ka+mat.kd*diffuse+mat.ks
*spec);
gl_FragColor=vec4(color,1.0);
}
Apartfromusingthetwovaryingvariablestoobtainthe
interpolatednormal(N)andfragment(v)position,theshader
declarestwostructuresforkeepinglightandmaterialinformation.
Withoutgoingintothedetailsofhowtheshaderitselfworks,it
calculatesthreecomponents—ambientlight,diffusedlight,and
specularreflection—addsthemtogether,andsetsthatasthe
fragmentcolor.Sinceallthepervertexinputdataisinterpolatedfor
eachfragment,thefinalcoloriscalculatedindividuallyforeach
pixel.
Onceweknowwhattheshadersexpect,wecansetuptheshader
programobject.Let'sgothroughtheinitialize()method.First,we
callthebaseclassimplementationandsetthebackgroundcolorof
thescenetoblack,asshowninthefollowingcode:
voidinitialize(){
voidinitialize(){
AbstractGLScene::initialize();
glClearColor(0,0,0,0);
//...
}
Addbothshaderfilestotheproject'sresources.Then,usethe
followingcodetoreadshadersfromthesefilesandlinktheshader
program:
m_shader.addShaderFromSourceFile(QOpenGLShader::Vertex,
":/phong.vert");
m_shader.addShaderFromSourceFile(QOpenGLShader::Fragment,
":/phong.frag");
m_shader.link();
Thelink()functionreturnsaBooleanvalue,but,weskiptheerror
checkhereforsimplicity.Thenextstepistopreparealltheinput
datafortheshader,asshown:
m_shader.bind();
m_shader.setAttributeArray("Vertex",GL_FLOAT,
&m_data[0].coords,3,sizeof(ScenePoint));
m_shader.enableAttributeArray("Vertex");
m_shader.setAttributeArray("Normal",GL_FLOAT,
&m_data[0].normal,3,sizeof(ScenePoint));
m_shader.enableAttributeArray("Normal");
m_shader.setUniformValue("mat.ka",QVector3D(0.1,0,0.0));
m_shader.setUniformValue("mat.kd",QVector3D(0.7,0.0,0.0));
m_shader.setUniformValue("mat.ks",QVector3D(1.0,1.0,1.0));
m_shader.setUniformValue("mat.shininess",128.0f);
m_shader.setUniformValue("light.position",QVector3D(2,1,1));
m_shader.setUniformValue("light.intensity",QVector3D(1,1,1));
First,theshaderprogramisboundtothecurrentcontextsothatwe
canoperateonit.Then,weenablethesetupoftwoattributearrays
—oneforvertexcoordinatesandtheotherfortheirnormals.Inour
program,thedataisstoredinaQVector<ScenePoint>,whereeach
ScenePointhascoordsandnormalfields,sotherearenoseparateC++
arraysforcoordinatesandnormals.Fortunately,OpenGLissmart
enoughtouseourmemorylayoutasis.Wejustneedtomapour
vectortotwoattributearrays.
WeinformtheprogramthatanattributecalledVertexisanarray.
Eachitemofthatarrayconsistsofthreevaluesof
theGL_FLOATtype.Thefirstarrayitemislocatedat&m_data[0].coords,and
dataforthenextvertexislocatedatsizeof(ScenePoint)byteslaterthan
thedataforthecurrentpoint.Thenwehaveasimilardeclaration
fortheNormalattribute,withtheonlyexceptionthatthefirstpieceof
dataisstoredat&m_data[0].normal.Byinformingtheprogramabout
layoutofthedata,weallowittoquicklyreadallthevertex
informationwhenneeded.
Afterattributearraysareset,wepassvaluesforuniformvariables
totheshaderprogram,whichconcludestheshaderprogramsetup.
Youwillnotethatwedidn'tsetvaluesforuniformsrepresentingthe
variousmatrices;wewilldothatseparatelyforeachrepaint.The
paint()methodtakescareofthat:
voidShaderGLScene::paint(){
m_projectionMatrix.setToIdentity();
floataspectRatio=qreal(window()->width())/window()->height();
m_projectionMatrix.perspective(90,aspectRatio,0.5,40);
m_viewMatrix.setToIdentity();
QVector3Deye(0,0,2);
QVector3Dcenter(0,0,0);
QVector3Dup(0,1,0);
m_viewMatrix.lookAt(eye,center,up);
//...
}
Inthismethod,wemakeheavyuseoftheQMatrix4x4classthat
representsa4×4matrixinaso-calledrow-majororder,whichis
suitedtousewithOpenGL.Atthebeginning,weresetthe
projectionmatrixandusetheperspective()methodtogiveita
perspectivetransformationbasedonthecurrentwindowsize.
Afterward,theviewmatrixisalsoresetandthelookAt()methodis
usedtopreparethetransformationforthecamera;centervalue
indicatesthecenteroftheviewthattheeyeislookingat.Theup
vectordictatestheverticalorientationofthecamera(withrespect
totheeyeposition).
Thenextcoupleoflinesaresimilartowhatwehadintheprevious
project:
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
glViewport(0,0,window()->width(),window()->height());
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
Afterthat,wedotheactualpaintingoftheobject:
m_modelMatrix.setToIdentity();
m_modelMatrix.rotate(45,0,1,0);
QMatrix4x4modelViewMatrix=m_viewMatrix*m_modelMatrix;
paintObject(modelViewMatrix);
Westartbysettingthemodelmatrix,whichdictateswherethe
renderedobjectispositionedrelativetothecenteroftheworld(in
thiscase,wesaythatitisrotated45degreesaroundtheyaxis).
Thenweassemblethemodel-viewmatrix(denotingthepositionof
theobjectrelativetothecamera)andpassittothepaintObject()
method:
voidShaderGLScene::paintObject(constQMatrix4x4&mvMatrix){
m_shader.bind();
m_shader.setUniformValue("projectionMatrix",m_projectionMatrix);
m_shader.setUniformValue("modelViewMatrix",mvMatrix);
m_shader.setUniformValue("mvpMatrix",
m_projectionMatrix*mvMatrix);
m_shader.setUniformValue("normalMatrix",mvMatrix.normalMatrix());
glDrawArrays(GL_TRIANGLES,0,m_data.size());
}
Thismethodisveryeasy,sincemostoftheworkwasdonewhen
settinguptheshaderprogram.First,theshaderprogramis
activated,andthenalltherequiredmatricesaresetasuniformsfor
theshader.Includedisthenormalmatrixcalculatedfromthe
model-viewmatrix.Finally,acalltoglDrawArrays()isissued,tellingit
torenderwiththeGL_TRIANGLESmodeusingactivearrays,startingfrom
thebeginningofthearray(offset0)andreadinginthem_data.size()
entitiesfromthearray.
Afteryouruntheproject,youshouldgetaresultsimilartothe
followingone,whichhappenstocontaintheBlendermonkey,
Suzanne:
GLbuffers
Usingattributearrayscanspeedupprogramming,butfor
renderingalldatastillneedstobecopiedtothegraphicscardon
eachuse.ThiscanbeavoidedwithOpenGLbufferobjects.Qt
providesaneatinterfaceforsuchobjectswithitsQOpenGLBufferclass.
Thecurrentlysupportedbuffertypesarevertexbuffers(wherethe
buffercontainsvertexinformation),indexbuffers(wherethe
contentofthebufferisasetofindexestootherbuffersthatcanbe
usedwithglDrawElements()),andalsoless-commonly-usedpixelpack
buffersandpixelunpackbuffers.Thebufferisessentiallyablockof
memorythatcanbeuploadedtothegraphicscardandstoredthere
forfasteraccess.Therearedifferentusagepatternsavailablethat
dictatehowandwhenthebufferistransferredbetweenthehost
memoryandtheGPUmemory.Themostcommonpatternisaone-
timeuploadofvertexinformationtotheGPUthatcanlaterbe
referredtoduringrenderingasmanytimesasneeded.Changingan
existingapplicationthatusesanattributearraytousevertexbuffers
isveryeasy.First,abufferneedstobeinstantiated:
ShaderGLScene::ShaderGLScene(SceneGLWindow*window):
AbstractGLScene(window),
m_vertexBuffer(QOpenGLBuffer::VertexBuffer)
{/*...*/}
Then,itsusagepatternneedstobeset.Incaseofaone-time
upload,themostappropriatetypeisStaticDraw:
m_vertexBuffer.setUsagePattern(QOpenGLBuffer::StaticDraw);
Then,thebufferitselfhastobecreatedandboundtothecurrent
context(forexample,intheinitializeGL()function):
m_vertexBuffer.create();
m_vertexBuffer.bind();
Thenextstepistoactuallyallocatesomememoryforthebufferand
initializeit:
m_vertexBuffer.allocate(m_data.constData(),
m_data.count()*sizeof(ScenePoint));
Tochangedatainthebuffer,therearetwooptions.First,youcan
attachthebuffertotheapplication'smemoryspace,usingacallto
map()andthenfillthedata,usingareturnedpointer:
ScenePoint*buffer=static_cast<ScenePoint*>(
vbo.map(QOpenGLBuffer::WriteOnly));
assert(buffer!=nullptr);
for(inti=0;i<vbo.size();++i){
buffer[i]=...;
}
vbo.unmap();
Analternativeapproachistowritetothebufferdirectly,using
write():
vbo.write(0,m_data.constData(),m_data.size()*sizeof(ScenePoint));
Finally,thebuffercanbeusedintheshaderprograminaway
similartoanattributearray:
vbo.bind();
m_shader.setAttributeBuffer("Vertex",GL_FLOAT,
0,3,sizeof(ScenePoint));
m_shader.enableAttributeArray("Vertex");
m_shader.setAttributeBuffer("Normal",GL_FLOAT,
sizeof(QVector3D),3,sizeof(ScenePoint));
m_shader.enableAttributeArray("Normal");
TheresultisthatallthedataisuploadedtotheGPUonceandthen
usedasneededbythecurrentshaderprogramorotherOpenGL
call-supportingbufferobjects.
UsingmultipleOpenGL
versions
Earlierinthischapter,wediscussedafamilyofQOpenGLFunctionsclasses
thatprovideaccesstoOpenGLfunctionsincludedinaspecific
OpenGLprofile.Ifyourwholeapplicationcanuseoneprofile,you
canjustselecttheappropriateQtclassanduseit.However,
sometimesyoudon'twanttheapplicationtoshutdowncompletely
iftherequestedprofileisnotsupportedonthecurrentsystem.
Instead,youcanrelaxyourrequirementsanduseanolderOpenGL
versionandprovidesimplifiedbutstillworkingrenderingfor
systemsthatdon'tsupportthenewprofile.InQt,youcan
implementsuchanapproachusingQOpenGLContext::versionFunctions():
classMyWindow:publicQOpenGLWindow{
protected:
QOpenGLFunctions_4_5_Core*glFunctions45;
QOpenGLFunctions_3_3_Core*glFunctions33;
voidinitializeGL()
{
glFunctions33=context()-
>versionFunctions<QOpenGLFunctions_3_3_Core>();
glFunctions45=context()-
>versionFunctions<QOpenGLFunctions_4_5_Core>();
}
voidpaintGL(){
if(glFunctions45){
//OpenGL4.5rendering
//glFunctions45->...
}elseif(glFunctions33){
//OpenGL3.3rendering
//glFunctions33->...
}else{
qFatal("unsupportedOpenGLversion");
}
}
};
};
IntheinitializeGL()function,wetrytorequestwrapperobjectsfor
multipleOpenGLversions.Iftherequestedversionisnotcurrently
available,versionFunctions()willreturnnullptr.InthepaintGL()function,
weusethebestavailableversiontoperformtheactualrendering.
Next,youcanusetheQSurfaceFormatclasstospecifytheOpenGL
versionandprofileyouwanttouse:
MyWindowwindow;
QSurfaceFormatformat=window.format();
format.setVersion(4,0);
format.setProfile(QSurfaceFormat::CoreProfile);
window.setFormat(format);
window.show();
Byrequestingthecoreprofile,youcanensurethatolddeprecated
functionalitywillnotbeavailableinourapplication.
Offscreenrendering
Sometimes,itisusefultorenderanOpenGLscenenottothescreen
buttosomeimagethatcanbelaterprocessedexternallyorusedas
atextureinsomeotherpartofrendering.Forthat,theconceptof
FramebufferObjects(FBO)wascreated.AnFBOisarendering
surfacethatbehavesliketheregulardeviceframebuffer,withthe
onlyexceptionthattheresultingpixelsdonotlandonthescreen.
AnFBOtargetcanbeboundasatextureinanexistingsceneor
dumpedasanimagetoregularcomputermemory.InQt,suchan
entityisrepresentedbyaQOpenGLFramebufferObjectclass.
OnceyouhaveacurrentOpenGLcontext,youcancreatean
instanceofQOpenGLFramebufferObject,usingoneoftheavailable
constructors.Amandatoryparametertopassisthesizeofthe
canvas(eitherasaQSizeobjectorasapairofintegersdescribingthe
widthandheightoftheframe).Differentconstructorsacceptother
parameters,suchasthetypeoftexturetheFBOistogenerateora
setofparametersencapsulatedinQOpenGLFramebufferObjectFormat.
Whentheobjectiscreated,youcanissueabind()callonit,which
switchestheOpenGLpipelinetorendertotheFBOinsteadofthe
defaulttarget.Acomplementarymethodisrelease(),whichrestores
thedefaultrenderingtarget.Afterward,theFBOcanbequeriedto
returntheIDoftheOpenGLtexture(usingthetexture()method)or
toconvertthetexturetoQImage(byinvokingtoImage()).
VulkaninQtapplications
OpenGLhasundergonesignificantchangesasgraphicscards
hardwarehasevolved.ManyoldpartsofOpenGLAPIarenow
deprecated,andevenup-to-dateAPIisnotidealforutilizingthe
capabilitiesofmodernhardware.Vulkanwasdesignedasan
attempttocreateanAPImoresuitableforthispurpose.
VulkanisanewAPIthatcanbeusedinsteadofOpenGLtoperform
hardware-acceleratedrenderingandcomputation.WhileVulkanis
moreverboseandcomplexthanOpenGL,itcloselyrepresentsthe
actualinteractionbetweenCPUandGPU.ThisallowsVulkanusers
toachievebettercontroloverutilizingGPUresources,whichcan
leadtobetterperformance.ThefirststableversionofVulkanAPI
wasreleasedin2016.
WhileVulkanisacross-platformsolution,aVulkanapplicationstill
needstocontainabitofplatform-specificcode,mainlyrelatedto
windowcreationandeventhandling.SinceVersion5.10,Qt
providesawaytouseVulkanalongwithQt'sexistingwindowand
eventinfrastructure.Youstillretainfullaccesstotheoriginal
VulkanAPIforrendering,but,atthesametime,youcanusethe
alreadyfamiliarQtAPIforeverythingelse.
AswithOpenGL,wewillnotgiveanin-depthguideofVulkanhere.
Wewillonlyprovidesimpleexamplesandcovertheinteraction
betweenQtandVulkan.Ifyouneedmoreinformationabout
Vulkan,youcanrefertoitsofficialpage
athttps://www.khronos.org/vulkan/.
Preparingthedeveloping
environment
BeforeyoucanstartdevelopinggameswithVulkanandQt,you
needtomakeafewpreparations.First,youneedtoinstallthe
VulkanSDK.Todothat,headtohttps://www.lunarg.com/vulkan-sdk/,
downloadafileforyouroperatingsystem,andexecuteorunpackit.
Examinetheindex.htmlfileinthedocsubdirectoryintheinstallation
foldertoseewhetheryouneedtoperformanyadditionalactions.
Next,youneedaQtbuildwithVulkansupport;itmustbeQt5.10
orlater.Ifyouhaveinstalledthemostrecentversionavailable
throughtheinstaller,itmayalreadybesuitable.
TocheckwhetheryourQtversionhasVulkansupport,createanew
QtConsoleApplication,ensurethatyouselectthekitcorresponding
tothemostrecentlyinstalledQtversion.TheVulkanSDKalso
requiresyoutosetsomeenvironmentvariables,such
asVULKAN_SDK,PATH,LD_LIBRARY_PATH,andVK_LAYER_PATH(exactnamesand
valuescandependontheoperatingsystem,sorefertotheSDK
documentation).Youcaneditenvironmentvariablesforyour
projectbyswitchingtoQtCreator'sProjectspaneandexpanding
theBuildEnvironmentsection.
Putthefollowingcodeinmain.cpp:
#include<QGuiApplication>
#include<vulkan/vulkan.h>
#include<QVulkanInstance>
intmain(intargc,char*argv[]){
QGuiApplicationapp(argc,argv);
QVulkanInstancevulkan;
returnapp.exec();
}
}
Additionally,adjusttheprojectfilesothatweactuallyhaveaQt
GUIapplicationinsteadofaconsoleapplication:
QT+=gui
CONFIG+=c++11
DEFINES+=QT_DEPRECATED_WARNINGS
SOURCES+=main.cpp
Iftheprojectbuildssuccessfully,yoursetupiscomplete.
Ifthecompilercan'tfindthevulkan/vulkan.hheader,thentheVulkan
SDKwasnotinstalledproperlyoritsheadersarenotlocatedinthe
defaultincludepath.ChecktheVulkanSDKdocumentationtosee
whetheryouhavemissedsomething.Youcanalsoswitchtothe
ProjectspaneofQtCreatorandeditthebuildenvironmentofthe
projecttomaketheinstalledheadersvisible.Dependingonthe
compiler,youmayneedtosettheINCLUDEPATHorCPATHenvironment
variable.
IfyouhaveacompileerrorcorrespondingtotheQVulkanInstance
header,youareusingaQtversionpriorto5.10.Ensurethatyou
installarecentversionandselectthecorrectkitontheProjects
paneofQtCreator.
However,iftheQVulkanInstanceincludesdirectiveworks,but
theQVulkanInstanceclassisstillnotdefined,itmeansthatyourQtbuild
lacksVulkansupport.Inthiscase,firsttrytoinstallthemostrecent
versionusingtheofficialinstaller,ifyouhaven'tdonesoalready:
1. CloseQtCreator
2. LaunchtheMaintenanceToolexecutablefromtheQt
installationdirectory
3. SelectAddorremovecomponents
4. SelectthemostrecentDesktopQtversion
5. Confirmthechanges
Aftertheinstallationisdone,re-openQtCreator,switchtothe
Projectspane,andselectthenewkitfortheproject.
Unfortunately,atthetimeofwriting,theQtbuildsavailablethroughtheofficialinstaller
donothaveVulkansupport.It'spossible(andlikely)thatitwillbeenabledinthefuture
versions.
IftheQVulkanInstanceclassisstillnotrecognized,youhavetobuildQt
fromsources.Thisprocessvariesdependingontheoperating
systemandtheQtversion,sowewillnotcoverthedetailsinthe
book.Gotothehttp://doc.qt.io/qt-5/build-sources.htmlpageandfollow
theinstructionscorrespondingtoyouroperatingsystem.Ifthe
VulkanSDKisproperlyinstalled,theoutputoftheconfigure
commandshouldcontainVulkan...yes,indicatingthatVulkan
supportisenabled.AfteryoubuildQt,openQtCreator'soptions
dialogandsetupaQtversionandakit,asdescribedinChapter2,
Installation.
Finally,selectthenewkitfortheprojectontheProjectspane:
Ifyou'vedoneeverythingcorrectly,theprojectshouldnowbuild
andexecutesuccessfully.
Vulkaninstance,window,and
renderer
BeforewestartcreatingourfirstminimalVulkanapplication,let's
getfamiliarwiththeQtclasseswe'llneedforthetask.
UnlikeOpenGL,Vulkandoesn'thaveaglobalstate.Interactionwith
VulkanstartswiththeinstanceobjectrepresentedbytheVkInstance
type.AnapplicationusuallycreatesasingleVkInstanceobjectthat
containstheapplication-widestate.AllotherVulkanobjectscan
onlybecreatedfromtheinstanceobject.InQt,thecorresponding
classisQVulkanInstance.Thisclassprovidesaconvenientwayto
configureVulkanandtheninitializeitwiththegivenconfiguration.
YoucanalsouseitssupportedExtensions()andsupportedLayers()functions
toquerysupportedfeaturesbeforeusingthem.Afterthe
configurationisdone,youshouldcallthecreate()functionthat
actuallytriggersloadingVulkanlibraryandcreatingaVkInstance
object.Ifthisfunctionreturnstrue,theVulkaninstanceobjectis
readytobeused.
ThenextstepistocreateawindowcapableofVulkanrendering.
ThisisdonebysubclassingtheQVulkanWindowclass.Similarto
QOpenGLWindow,QVulkanWindowextendsQWindowandprovides
functionalityrequiredforutilizingVulkancapabilitiesaswell
assomeconveniencefunctions.Youcanalsousevirtualfunctions
inheritedfromQWindowtohandleanyeventsdispatchedbyQt'sevent
system.However,subclassesofQVulkanWindowshouldnotperformany
actualrendering.ThistaskisdelegatedtotheQVulkanWindowRenderer
class.TheQVulkanWindow::createRenderer()virtualfunctionwillbecalled
onceafterthewindowisfirstshown,andyoushouldreimplement
thisfunctiontoreturnyourrendererobject.
Now,abouttherendereritself:QVulkanWindowRendererisasimpleclass
containingnothingmorethanasetofvirtualfunctions.Youcan
createyourownrendererbysubclassingQVulkanWindowRendererandre-
implementingtheonlypurevirtualfunctioncalledstartNextFrame().
Thisfunctionwillbecalledwhenthedrawingofthenextframeis
requested.Youcanperformallrequireddrawingoperationsinthis
functionandenditwithacalltoQVulkanWindow::frameReady()toindicate
thatthedrawingiscomplete.Youcanalsore-implementother
virtualfunctionsoftherenderer.Themostusefulofthem
areinitResources()andreleaseResources(),whichallowyoutocreate
requiredresources,storetheminprivatemembersofyourrenderer
class,andthendestroythemwhennecessary.
ThesethreeclassesdefinethebasicstructureofyourVulkan
application.Let'sseetheminaction.
Timeforaction–Creatingthe
minimalVulkanproject
We'vealreadycreatedaprojectwhiletestingthedeveloping
environment.Nowlet'saddtwonewclassestotheproject.One
classnamedMyWindowshouldbederivedfromQVulkanWindow,andthe
otherclassnamedMyRenderershouldbederived
fromQVulkanWindowRenderer.Implementthewindow'screateRenderer()
virtualfunction:
QVulkanWindowRenderer*MyWindow::createRenderer(){
returnnewMyRenderer(this);
}
AddtheQVulkanWindow*m_windowprivatefieldtotherendererclass.
Implementtheconstructortoinitializethisfieldandoverride
thestartNextFrame()virtualfunction,asshown:
MyRenderer::MyRenderer(QVulkanWindow*window)
{
m_window=window;
}
voidMyRenderer::startNextFrame(){
m_window->frameReady();
}
Finally,editthemain()function:
intmain(intargc,char*argv[]){
QGuiApplicationapp(argc,argv);
QVulkanInstancevulkan;
if(!vulkan.create()){
qFatal("FailedtocreateVulkaninstance:%d",
vulkan.errorCode());
vulkan.errorCode());
}
MyWindowwindow;
window.resize(1024,768);
window.setVulkanInstance(&vulkan);
window.show();
returnapp.exec();
}
Whenyoucompileandruntheproject,ablankwindowwithablack
backgroundshouldappear.
Whatjusthappened?
We'vecreatedawindowthatwillberenderedusingVulkan.The
main()functioninitializesVulkan,createsawindow,passes
theinstanceobjecttothewindow,andshowsitonthescreen.As
usual,thefinalcalltoexec()startsQt'seventloop.Whenthewindow
isshown,QtwillcallthecreateRenderer()functiononthewindowand
anewrendererobjectwillbecreatedinyourimplementationofthis
function.Therendererisattachedtothewindowandwill
automaticallybedeletedalongwithit,sothereisnoneedtodelete
itmanually.Eachtimethewindowneedstobepainted,Qtwillcall
therenderer'sstartNextFrame()function.Wedon'tperformany
paintingyet,sothewindowremainsblank.
It'simportantthatthedrawingofeveryframeendswithacall
toframeReady().Untilthisfunctioniscalled,processingoftheframe
cannotbecompleted.However,it'snotrequiredtocallthisfunction
directlyfromthestartNextFrame()function.Youcandelaythiscallif
youneed,forexample,towaitforcalculationstocompleteina
separatethread.
SimilartohowpaintEvent()works,startNextFrame()willnotbecalledcontinuouslyby
default.Itwillonlybecalledonceaftershowingthewindow.Itwillalsobecalledeachtime
apartofthewindowisexposed(forexample,asaresultofmovingawindoworrestoring
aminimizedwindow).Ifyouneedtorenderadynamicscenecontinuously,callm_window-
>requestUpdate()aftercallingm_window->frameReady().
UsingVulkantypesand
functions
WecanletQthandleloadingtheVulkanlibraryandresolving
functionsforus.ItworkssimilartotheQOpenGLFunctionssetofclasses.
QtprovidestwofunctionsclassesforVulkan:
TheQVulkanFunctionsclassprovidesaccesstotheVulkan
functionsthatarenotdevice-specific
TheQVulkanDeviceFunctionsclassprovidesfunctionsthatworkon
aspecificVkDevice
Youcanobtaintheseobjectsbycallingthefunctions()
anddeviceFunctions(VkDevicedevice)methodsoftheQVulkanInstanceclass,
respectively.Youwillusuallyusethedevicefunctionsalotinthe
renderer,soacommonpatternistoaddtheQVulkanDeviceFunctions
*m_devFuncsprivatefieldtoyourrendererclassandinitializeitinthe
initResources()virtualfunction:
voidMyRenderer::initResources()
{
VkDevicedevice=m_window->device();
m_devFuncs=m_window->vulkanInstance()->deviceFunctions(device);
//...
}
Nowyoucanusem_devFuncstoaccesstheVulkanAPIfunctions.We
won'tusethemdirectly,sowedon'tneedtofigureouthowtolink
againsttheVulkanlibraryoneachplatform.Qtdoesthisjobforus.
Asforstructures,unions,andtypedefs,wecanusethemdirectly
withoutworryingabouttheplatformdetails.It'senoughtohavethe
VulkanSDKheaderspresentinthesystem.
Timeforaction–Drawingwith
adynamicbackgroundcolor
Let'sseehowwecanusetheVulkanAPIinourQtprojecttochange
thebackgroundcolorofthewindow.We'llcyclethroughallpossible
huesofthecolorwhileretainingconstantsaturationandlightness.
ThismaysoundcomplicatedwhenyouthinkaboutacolorinRGB
space,butit'sactuallyveryeasyifyouworkwiththeHSL(Hue,
Saturation,Lightness)colormodel.Luckily,QColorsupportsmultiple
colormodels,includingHSL.
First,addandinitializethem_devFuncsprivatefield,asjustshown.
Next,addthefloatm_hueprivatefieldthatwillholdthecurrenthueof
thebackgroundcolor.Setitsinitialvaluetozero.Wecannowstart
writingourstartNextFrame()functionthatwilldoallthemagic.Let'sgo
throughitpiecebypiece.First,weincrementourm_huevariableand
ensurethatwedon'tgooutofbounds;then,weuse
theQColor::fromHslF()functiontoconstructaQColorvaluebasedon
givenhue,saturation,andlightness(eachofthemrangesfrom0to
1):
voidMyRenderer::startNextFrame()
{
m_hue+=0.005f;
if(m_hue>1.0f){
m_hue=0.0f;
}
QColorcolor=QColor::fromHslF(m_hue,1,0.5);
//...
}
Next,weusethiscolorvariabletoconstructaVkClearValuearraythat
we'lluseforsettingthebackgroundcolor:
VkClearValueclearValues[2];
memset(clearValues,0,sizeof(clearValues));
clearValues[0].color={
static_cast<float>(color.redF()),
static_cast<float>(color.greenF()),
static_cast<float>(color.blueF()),
1.0f
};
clearValues[1].depthStencil={1.0f,0};
TostartanewrenderpassinVulkan,weneedtoinitialize
aVkRenderPassBeginInfostructure.Itrequiresalotofdata,but,
luckily,QVulkanWindowprovidesmostofthedataforus.Wejustneedto
putitintothestructureandusetheclearValuesarraywesetup
earlier:
VkRenderPassBeginInfoinfo;
memset(&info,0,sizeof(info));
info.sType=VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
info.renderPass=m_window->defaultRenderPass();
info.framebuffer=m_window->currentFramebuffer();
constQSizeimageSize=m_window->swapChainImageSize();
info.renderArea.extent.width=imageSize.width();
info.renderArea.extent.height=imageSize.height();
info.clearValueCount=2;
info.pClearValues=clearValues;
Finally,it'stimetoperformtherendering:
VkCommandBuffercommandBuffer=m_window->currentCommandBuffer();
m_devFuncs->vkCmdBeginRenderPass(commandBuffer,&info,
VK_SUBPASS_CONTENTS_INLINE);
m_devFuncs->vkCmdEndRenderPass(commandBuffer);
m_window->frameReady();
m_window->requestUpdate();
ThevkCmdBeginRenderPass()VulkanAPIfunctionwillbegintherender
pass,whichwillresultinclearingthewindowwiththecolorwe've
set.Sincewedon'thaveanythingelsetodraw,wecompletethe
renderpassimmediatelyusingthevkCmdEndRenderPass()function.Then,
weindicatethatwe'vealreadydoneeverythingwewantforthis
framebycallingtheframeReady()function.ThisallowsQttoadvance
therenderingloop.Asthefinalstep,werequestanupdateofthe
windowtoensurethatthenewframewillberequestedsoonandthe
coloranimationwillgoon.
Ifyouruntheprojectnow,youshouldseeawindowthatconstantly
changesitsbackgroundcolor:
Wewouldlovetoshowamoreadvancedexample.However,even
drawingasimpletriangleinVulkanusuallyrequiresafewhundred
linesofcode,becauseVulkanrequiresyoutoexplicitlysetupalot
ofthings.WhileQtprovidesalotofhelperclassesforOpenGL
rendering,itdoesnotcontainanysimilarclassesthatwouldhelp
withVulkanrenderingorcomputation(asofQt5.10),sothereis
nothingspecifictoQtinthesetasks.
IfyouwanttodeepenyourknowledgeofVulkan,youcanstudythedocumentationand
tutorialspresentonitsofficialwebsiteandtheVulkanSDKwebsite.Qtalsoincludes
severalgoodexamplesbasedonVulkan,suchasHelloVulkanTriangle,HelloVulkan
Texture,andHelloVulkanCubes.
Logsandvalidation
QtautomaticallyreceivesmessagesfromtheVulkanlibraryand
putsthemintoQt'sownloggingsystem.Thecriticalerrorswillbe
passedtoqWarning(),sotheywillappearintheapplicationoutputby
default.However,Qtalsologsadditionalinformationthatcanbe
usefulwhendebugging.Thisinformationishiddenbydefault,but
youcanmakeitvisiblebyaddingthefollowinglinetothemain()
functionjustaftertheconstructionofQGuiApplication:
QLoggingCategory::setFilterRules(QStringLiteral("qt.vulkan=true"));
TheVulkanAPIdoesnotperformanysanitychecksbydefault.If
youpassaninvalidparametertoaVulkanAPIfunction,the
applicationmaysilentlycrash,orworkinconsistently.However,
youcanenablevalidationlayersforyourVulkaninstance.They
donotchangethefunctionalityoftheAPIcalls,buttheyenable
additionalcheckswhenpossible.It'sagoodideatoenable
validationlayersinadebugbuild.Youcandothatbycalling
setLayers()ontheinstanceobjectbeforecallingcreate():
vulkan.setLayers({"VK_LAYER_LUNARG_standard_validation"});
Keepinmindthatanattempttorequestacurrentlyunsupportedlayerorextensionwillbe
ignoredbyQt.
Let'stestthevalidationlayersbyinsertinganinvalidparameterto
ourcode:
info.renderArea.extent.width=-5;//invalid
Whenyouruntheapplication,Qtshouldprintawarningtothe
applicationoutput:
vkDebug:CORE:4:CannotexecutearenderpasswithrenderAreanot
withintheboundoftheframebuffer.RenderArea:x0,y0,width-5,
height768.Framebuffer:width1024,height768.
Ifthewarningdoesnotappear,itmeansthatthevalidationlayersarenotavailableor
theyfailedtoload.Checktheapplicationoutputforthepresenceofvalidationlayers(they
willbeprintedafterthe"SupportedVulkaninstancelayers"line)andanylibraryloading
errors.Ensurethatyou'vesetuptheVulkanSDKandtheproject'senvironmentvariables
accordingtothedocumentation.
However,keepinmindthatvalidationlayershaveaperformance
impactonyourapplication.Youshouldprobablydisablethemin
yourfinalbuilds.YoucanalsodisableredirectingVulkan'sdebug
outputtotheQtloggingsystem,usingthefollowingcode:
QVulkanInstancevulkan;
vulkan.setFlags(QVulkanInstance::NoDebugOutputRedirect);
CombiningOpenGLorVulkan
withQtWidgets
Sometimesyouwanttocombinethepowersofacceleratedgraphics
andQtWidgets.WhileOpenGLandVulkanaregreatforrendering
high-performance2Dand3Dscenes,theQtWidgetsmoduleisfar
easiertouseforcreatinguserinterfaces.Qtoffersafewwaysto
combinethemintoasinglepowerfulinterface.Thiscanbeusefulif
yourapplicationdependsheavilyonwidgets(forexample,the3D
viewisonlyoneoftheviewsinyourapplicationandiscontrolled
usingabunchofotherwidgetssurroundingthemainview).
ThefirstwayistheQWidget::createWindowContainer()function.Ittakesan
arbitraryQWindowandcreatesaQWidgetthatkeepsthewindowwithinits
bounds.Thatwidgetcanbeputintoanotherwidgetandcanbe
managedbyalayout.Whilethewindowappearstobeembedded
intoanotherwindow,itstillremainsanativewindowfromthe
operatingsystem'sperspective,andanyacceleratedrenderingwill
beperformeddirectlyonthewindowwithoutaheavyperformance
impact.Thisapproachhasafewlimitations,though.Forexample,
theembeddedwindowwillalwaysstackontopofotherwidgets.
However,it'ssuitableinmostcases.
Let'sreturntoourOpenGLcubeprojectandputitintoalayout
withanadditionallabel:
QWidgetwidget;
QVBoxLayout*layout=newQVBoxLayout(&widget);
layout->addWidget(newQLabel("Scene"),0);
QWidget*container=QWidget::createWindowContainer(&window,&widget);
layout->addWidget(container,1);
widget.resize(600,600);
widget.show();
InsteadofshowingtheOpenGLwindow,wecreatedawidgetand
putthewindowintothelayoutofthatwidget:
YoucanapplythisapproachtoanyQWindow,includingVulkan-based
windowsandQtQuickwindows,whichwe'llworkwithin
subsequentchapters.
Thereisanotherwaytosolvethesametask,butitonlyworkswith
OpenGL.YoucansimplyreplaceQOpenGLWindowwithQOpenGLWidgettoturn
awindowintoafullyfeaturedwidget.TheAPI
ofQOpenGLWidget(includingvirtualfunctions)iscompatible
withQOpenGLWindow,soitcanactasadrop-inreplacement.Thereareno
limitationsforthestackingorder,focus,oropacityofQOpenGLWidget.
YoucanevenmixtheOpenGLrenderingwithQPainteroperations.
However,thissolutionhasaperformancecost.QOpenGLWindowrenders
directlytothegivenwindow,whileQOpenGLWidgetfirstrenderstoan
offscreenbufferthatisthenrenderedtothewidget,soitwillbe
slower.
Popquiz
Q1.Whichofthefollowingprogramminglanguagesisacceptedby
theQOpenGLShader::compileSourceCode()function?
1. C
2. C++
3. GLSL
Q2.WhichvirtualfunctionoftheQOpenGLWindowclassshouldyou
implementtoperformOpenGLpainting?
1. paintGL()
2. paintEvent()
3. makeCurrent()
Q3.Whenshouldyoudeletetheobjectof
yourQVulkanWindowRenderersubclass?
1. InthedestructoroftheQVulkanWindowsubclass
2. AfterdeletingtheQVulkanInstanceobject
3. Never
Summary
Inthischapter,welearnedaboutusingOpenGLandVulkan
graphicswithQt.Withthisknowledge,youcancreatehardware
accelerated2Dand3Dgraphics.WealsoexploredQtclassesthat
simplifyusageofthesetechnologiesinQtapplications.Ifyouwant
tosharpenyourOpenGLandVulkanskills,youcanstudy
numerousbooksandarticlesfocusedonthesetopics.Qtprovides
verytransparentaccesstohardwareacceleratedgraphics,so
adaptinganypureOpenGLorVulkanapproachesforQtshouldbe
easy.Ifyouprefertohaveahigher-levelAPIforaccelerated
graphics,youshouldturnyourattentiontoQtQuickandQt3D.We
willcoveritinthelastpartofthisbook.
Inthenextchapter,youwilllearntoimplementscriptinginyour
game.Thiswillmakeitmoreextensibleandeasiertomodify.
Scriptingcanalsobeusedtoenablemoddinginyourgame,
allowingplayerstocustomizethegameplayhowtheywant.
Scripting
Inthischapter,youwilllearnhowtobringscriptingfacilitiesto
yourprograms.YouwillgainknowledgeofhowtouseJavaScriptto
implementthelogicanddetailsofyourgame,withouthavingto
rebuildthemaingameengine.Theseskillswillalsobeusefulinthe
lastpartofthebookwhenweworkwithQtQuick.Althoughthe
environmentwewillfocusonblendsbestwithQtapplications,
thereareotheroptionsifyoudon'tlikeJavaScript.Wewillalso
showhowyoucanusePythontomakeyourgamesscriptable.
Themaintopicscoveredinthischapterareaslisted:
ExecutingJavaScriptcode
InteractionbetweenC++andJavaScript
Implementingascriptinggame
IntegratingthePythoninterpreter
Whyscript?
Youmightaskyourself,"whyshouldIuseanyscriptinglanguageif
IcanimplementeverythingIneedinC++"?Thereareanumberof
benefitstoprovidingascriptingenvironmenttoyourgames.Most
moderngamesreallyconsistoftwoparts.Oneisthemaingame
enginethatimplementsthecoreofthegame(datastructures,
processingalgorithms,andtherenderinglayer)andexposesanAPI
totheothercomponent,whichprovidesdetails,behaviorpatterns,
andactionflowsforthegame.Thisothercomponentissometimes
writteninascriptinglanguage.Themainbenefitofthisisthatstory
designerscanworkindependentlyfromtheenginedevelopers,and
theydon'thavetorebuildthewholegamejusttomodifysomeofits
parametersorcheckwhetherthenewquestfitswellintothe
existingstory.Thismakesthedevelopmentmuchquickercompared
tothemonolithicapproach.
Anotherbenefitisthatthisdevelopmentopensthegameto
modding—skilledenduserscanextendormodifythegameto
providesomeaddedvaluetothegame.It'salsoawaytoimplement
extensionsofthegameontopoftheexistingscriptingAPIwithout
havingtoredeploythecompletegamebinarytoeveryplayer.
Finally,youcanreusethesamegamedriverforothergamesand
justreplacethescriptstoobtainatotallydifferentproduct.
Inthischapter,wewillusetheQtQMLmoduletoimplement
scripting.ThismoduleimplementsQMLlanguageusedinQtQuick.
SinceQMLisJavaScript-based,QtQMLincludesaJavaScript
engineandprovidesAPIforrunningJavaScriptcode.Italsoallows
youtoexposeC++objectstoJavaScriptandviceversa.
WewillnotdiscussthedetailsoftheJavaScriptlanguageitself,as
therearemanygoodbooksandwebsitesavailablewhereyoucan
learnJavaScript.Besides,theJavaScriptsyntaxisverysimilarto
thatofC,andyoushouldn'thaveanyproblemsunderstandingthe
scriptsthatweuseinthischapterevenifyouhaven'tseenany
JavaScriptcodebefore.
EvaluatingJavaScript
expressions
TouseQtQMLinyourprograms,youhavetoenablethescript
moduleforyourprojectsbyaddingtheQT+=qmllinetotheproject
file.
C++compilersdonotunderstandJavaScript.Therefore,toexecute
anyscript,youneedtohavearunninginterpreterthatwillparsethe
scriptandevaluateit.InQt,thisisdonewiththeQJSEngineclass.This
isaJavaScriptruntimethathandlestheexecutionofscriptcode
andmanagesalltheresourcesrelatedtoscripts.Itprovidesthe
evaluate()method,whichcanbeusedtoexecuteJavaScript
expressions.Let'slookata"HelloWorld"programusingQJSEngine:
#include<QCoreApplication>
#include<QJSEngine>
intmain(intargc,char**argv){
QCoreApplicationapp(argc,argv);
QJSEngineengine;
engine.installExtensions(QJSEngine::ConsoleExtension);
engine.evaluate("console.log('HelloWorld!');");
return0;
}
Thisprogramisverysimple.First,itcreatesanapplicationobject
thatisrequiredforthescriptenvironmenttofunctionproperlyand
instantiatesaQJSEngineobject.Next,weaskQJSEnginetoinstallthe
consoleextension—theglobalconsoleobjectthatcanbeusedtoprint
messagestotheconsole.It'snotpartoftheECMAScriptstandard,
soit'snotavailablebydefault,butwecaneasilyenableitusing
theinstallExtensions()function.Finally,wecalltheevaluate()function
toexecutethescriptsourcegiventoitasaparameter.After
buildingandrunningtheprogram,youwillseeawell-knownHello
World!printedtotheconsolewiththejs:prefix.
Bydefault,QJSEngineprovidesbuilt-inobjectsdefinedbyECMA-262standard,including
Math,Date,andString.Forexample,ascriptcanuseMath.abs(x)togettheabsolutevalueof
anumber.
Ifyoudon'tgetanyoutput,itprobablymeansthatthescriptdidn't
getexecutedproperly,possiblybecauseofanerrorinthescript's
sourcecode.Toverifythat,wecancheckthevaluereturnedfrom
evaluate():
QJSValueresult=engine.evaluate("console.log('HelloWorld!')");
if(result.isError()){
qDebug()<<"JSerror:"<<result.toString();
}
Thiscodecheckswhetherthereisanexceptionorasyntaxerror
andifyes,itdisplaysthecorrespondingerrormessage.For
example,ifyouomittheclosingsinglequoteinthescriptsource
textandruntheprogram,thefollowingmessagewillbedisplayed:
JSerror:"SyntaxError:Expectedtoken`)'"
Youcanseethatevaluate()returnsaQJSValue.Thisisaspecialtypethat
isusedtoexchangedatabetweentheJavaScriptengineandthe
C++world.LikeQVariant,itcanholdanumberofprimitivetypes
(boolean,integer,string,andsoon).However,itisinfactmuchmore
powerful,becauseitcanholdareferencetoaJavaScriptobjector
functionthatlivesintheJavaScriptengine.CopyingaQJSValuewill
produceanotherobjectthatreferencesthesameJavaScriptobject.
YoucanusethememberfunctionsofQJSValuetointeractwiththe
objectsfromC++.Forexample,youcanuseproperty()andsetProperty()
tomanipulatetheobject'spropertiesandcall()tocallthefunction
andgetthereturnedvalueasanotherQJSValue.
Inthepreviousexample,QJSEngine::evaluate()returnedanErrorobject.
WhentheJavaScriptcoderunssuccessfully,youcanusethe
returnedvaluelaterinyourC++code.Forexample,thescriptcan
calculatetheamountofdamagedonetoacreaturewhenitishit
withaparticularweapon.Modifyingourcodetousetheresultof
thescriptisverysimple.Allthatisrequiredistostorethevalue
returnedbyevaluate()andthenitcanbeusedelsewhereinthecode:
QJSValueresult=engine.evaluate("(7+8)/2");
if(result.isError()){
//...
}else{
qDebug()<<result.toNumber();
}
Timeforaction–Creatinga
JavaScripteditor
Let'sdoasimpleexerciseandcreateagraphicaleditortowriteand
executescripts.StartbycreatinganewQtWidgetsprojectand
implementamainwindowcomposedoftwoplaintexteditwidgets
(ui->codeEditorandui->logWindow)thatareseparatedusingavertical
splitter.Oneoftheeditboxeswillbeusedasaneditortoinputcode
andtheotherwillbeusedasaconsoletodisplayscriptresults.
Then,addamenuandtoolbartothewindowandcreateactionsto
open(ui->actionOpenDocument)andsave(ui->actionSaveDocumentandui-
>actionSaveDocumentAs)thedocument,createanewdocument(ui-
>actionNewDocument),executethescript(ui->actionExecuteScript),andtoquit
theapplication(ui->actionQuit).Remembertoaddthemtothemenu
andtoolbar.
Asaresult,youshouldreceiveawindowsimilartotheoneshownin
thefollowingscreenshot:
ConnectthequitactiontotheQApplication::quit()slot.Then,createan
openDocument()slotandconnectittothetriggeredsignalofthe
appropriateaction.Intheslot,useQFileDialog::getOpenFileName()toask
theuserforadocumentpath,asfollows:
voidMainWindow::openDocument()
{
QStringfilePath=QFileDialog::getOpenFileName(
this,tr("OpenDocument"),
QDir::homePath(),tr("JavaScriptDocuments(*.js)"));
if(filePath.isEmpty()){
return;
}
open(filePath);
}
Inasimilarfashion,implementtheNew,Save,andSaveAsaction
handlers.Lastly,createtheopen(constQString&filePath)slotthatshould
readthedocumentandputitscontentsintothecodeeditor:
voidMainWindow::open(constQString&filePath)
voidMainWindow::open(constQString&filePath)
{
QFilefile(filePath);
if(!file.open(QFile::ReadOnly|QFile::Text)){
QMessageBox::critical(this,tr("Error"),tr("Can'topen
file."));
return;
}
setWindowFilePath(filePath);
ui->codeEditor->setPlainText(QString::fromUtf8(file.readAll()));
ui->logWindow->clear();
}
ThewindowFilePathpropertyofQWidgetcanbeusedtoassociateafilewithawindow.When
thispropertyisset,Qtwillautomaticallyadjustthewindowtitleandevenaddaproxy
icononmacOS,allowingconvenientaccesstothefile.Youcanthenusethispropertyin
actionsrelatedtousingthefile—whensavingadocument,youcancheckwhetherthis
propertyisemptyandasktheusertoprovideafilename.Then,youcanresetthisproperty
whencreatinganewdocumentorwhentheuserprovidesanewpathforthedocument.
Atthispoint,youshouldbeabletoruntheprogramanduseitto
createscriptsandsaveandreloadthemintheeditor.
Now,toexecutethescripts,addaQJSEnginem_enginemembervariable
tothewindowclass.Createanewslot,callitrun,andconnectitto
theexecuteaction.Putthefollowingcodeinthebodyoftheslot:
voidMainWindow::run()
{
ui->logWindow->clear();
QTextCursorlogCursor=ui->logWindow->textCursor();
QStringscriptSourceCode=ui->codeEditor->toPlainText();
QJSValueresult=m_engine.evaluate(scriptSourceCode,
windowFilePath());
if(result.isError()){
QTextCharFormaterrFormat;
errFormat.setForeground(Qt::red);
logCursor.insertText(tr("Exceptionatline%1:\n")
.arg(result.property("lineNumber").toInt()),errFormat);
logCursor.insertText(result.toString(),errFormat);
logCursor.insertBlock();
logCursor.insertText(result.property("stack").toString(),
errFormat);
}else{
QTextCharFormatresultFormat;
resultFormat.setForeground(Qt::blue);
logCursor.insertText(result.toString(),resultFormat);
logCursor.insertText(result.toString(),resultFormat);
}
}
Buildandruntheprogram.Todoso,enterthefollowingscriptin
theeditor:
functionfactorial(n){
if(n<0){
return;
}
if(n==0){
return1;
}
returnn*factorial(n-1);
}
factorial(7)
Savethescriptinafilecalledfactorial.jsandthenrunit.Youshould
getanoutputasshown:
Next,replacethescriptwiththefollowingone:
functionfactorial(n){
returnN;
}
factorial(7)
Runningthescriptshouldyieldthefollowingresult:
Whatjusthappened?
Therun()methodclearsthelogwindowandevaluatesthescript
usingthemethodthatwelearnedearlierinthischapter.Ifthe
evaluationissuccessful,itprintstheresultinthelogwindow,which
iswhatweseeinthefirstscreenshotshownintheprevioussection.
Inthesecondattempt,wemadeanerrorinthescriptusinga
nonexistentvariable.Evaluatingsuchcoderesultsinanexception.
Inadditiontoreportingtheactualerror,wealsousethelineNumber
propertyofthereturnedErrorobjecttoreportthelinethatcaused
theproblem.Next,wedisplaythestackpropertyoftheerrorobject,
whichreturnsthebacktrace(astackoffunctioncalls)ofthe
problem,whichwealsoprintonthelog.
Globalobjectstate
Let'stryanotherscript.Thefollowingcodedefinesthefunlocal
variable,whichisassignedananonymousfunctionthatreturnsa
number:
varfun=function(){
return42;
}
Youcanthencallfun()likearegularfunction,asfollows:
Now,let'slookatwhathappensifwedeletethedefinitionoffun
fromthescript,butstillkeeptheinvocation:
Westillgetthesameresulteventhoughwedidn'tdefinewhatfun
means!Thisisbecauseanyvariablesatthetopscopebecome
propertiesoftheglobalobject.Thestateoftheglobalobjectis
preservedduringtheexistenceofQJSEngine,sothefunvariablewill
remainavailableuntilit'soverwrittenortheengineisdestroyed.
Topreventusersfromaccidentallychangingtheglobalobjectwith
localvariables,wecanwraptheprovidedcodeinananonymous
function:
QStringwrappedCode=
QStringLiteral("(function(){%1\n})()").arg(scriptSourceCode);
QJSValueresult=m_engine.evaluate(wrappedCode,windowFilePath());
Inthiscase,theJavaScriptcodemustusethereturnstatementto
actuallyreturnavaluetotheeditor:
varfun=function(){
return42;
}
}
returnfun();
Removingthefunvariableinitializationwillnowresultinanerror:
ReferenceError:funisnotdefined
However,removingthevarkeywordwillmakethevariableglobal
andpreserved.Amalicioususercanalsobreaktheexistingglobal
object'sproperties.Forexample,evaluatingMath.floor=null;will
makethebuilt-inMath.floorfunctionunavailableinallsubsequent
calls.
Thereisn'treallyagoodwaytoguardorresettheglobalobject.If
youareconcernedaboutmaliciousscripts,destroyingandcreating
anewQJSEngineobjectisthebestoption.Ifyouneedtorunmultiple
scriptsthatarenotallowedtointerferewitheachother,youhaveto
createaseparateQJSEngineforeachofthem.However,inmost
applications,suchsandboxingseemstobeanoverkill.
ExposingC++objectsand
functionstoJavaScriptcode
Sofar,wewereonlyevaluatingsomestandalonescriptsthatcan
makeuseofbuilt-inJavaScriptfeatures.Now,itistimetolearnto
usedatafromyourprogramsinthescripts.Thisisdoneby
exposingdifferentkindsofentitiestoandfromscripts.
AccessingC++object's
propertiesandmethods
ThesimplestwaytoexposeaC++objecttoJavaScriptcodeisto
takeadvantageofQt'smeta-objectsystem.QJSEngineisabletoinspect
QObjectinstancesanddetecttheirpropertiesandmethods.Touse
theminscripts,theobjecthastobevisibletothescript.Theeasiest
waytomakethishappenistoaddittotheengine'sglobalobject.As
youremember,alldatabetweenthescriptengineandC++is
exchangedusingtheQJSValueclass,sofirstwehavetoobtainaJS
valuehandlefortheC++object:
QJSEngineengine;
QPushButton*button=newQPushButton("Button");
//...
QJSValuescriptButton=engine.newQObject(button);
engine.globalObject().setProperty("pushButton",scriptButton);
QJSEngine::newQObject()createsaJavaScriptobjectwrappinganexisting
QObjectinstance.Wethensetthewrapperasapropertyoftheglobal
objectcalledpushButton.Thismakesthebuttonavailableintheglobal
contextoftheengineasaJavaScriptobject.Alltheproperties
definedwithQ_PROPERTYareavailableaspropertiesoftheobject,and
everyslotisaccessibleasamethodofthatobject.InJavaScript,you
willbeabletousethepushButtonobjectlikethis:
pushButton.text='MyScriptedButton';
pushButton.checkable=true;
pushButton.setChecked(true);
pushButton.show();
Qtslotsconventionallyreturnvoid.Theytechnicallycanhaveany
returntype,butQtwon'tusethereturnvalue,soinmostcases,
thereisnosenseinreturninganyvalue.Onthecontrary,whenyou
exposeaC++methodtotheJavaScriptengine,youoftenwantto
returnavalueandreceiveitinJavaScript.Inthesecases,you
shouldnotcreateslots,asthatwillbreaktheconvention.You
shouldmakethemethodinvokableinstead.Todothis,placethe
methoddeclarationinaregularpublicscopeand
addQ_INVOKABLEbeforeit:
public:
Q_INVOKABLEintmyMethod();
Thismacroinstructsmoctomakethismethodinvokableinthe
meta-objectsystemsothatQtwillbeabletocallitatruntime.All
invokablemethodsareautomaticallyexposedtoscripts.
Datatypeconversionsbetween
C++andJavaScript
Qtwillautomaticallyconvertargumentsandreturntypesof
methodstoitsJavaScriptcounterparts.Thesupportedconversions
includethefollowing:
Basictypes(bool,int,double,andsuch)areexposedwithout
changes
Qtdatatypes(QString,QUrl,QColor,QFont,QDate,QPoint,QSize,QRect,
QMatrix4x4,QQuaternion,QVector2D,andsuch)areconvertedto
objectswiththeavailableproperties
QDateTimeandQTimevaluesareautomaticallyconvertedto
JavaScriptDateobjects
EnumsdeclaredwithQ_ENUMmacrocanbeusedinJavaScript
FlagsdeclaredwithQ_FLAGmacrocanbeusedasflagsin
JavaScript
QObject*pointerswillbeautomaticallyconvertedto
JavaScriptwrapperobjects
QVariantobjectscontaininganysupportedtypesare
recognized
QVariantListisanequivalentofaJavaScriptarraywith
arbitraryitems
QVariantMapisanequivalentofaJavaScriptobjectwith
arbitraryproperties
SomeC++listtypes(QList<int>,QList<qreal>,QList<bool>,
QList<QString>,QStringList,QList<QUrl>,QVector<int>,QVector<qreal>,and
QVector<bool>)areexposedtoJavaScriptwithoutperforming
additionaldataconversions
Ifyouwantmorefine-grainedcontroloverdatatypeconversions,
youcansimplyuseQJSValueasanargumenttypeorareturntype.For
example,thiswillallowyoutoreturnareferencetoanexisting
JavaScriptobjectinsteadofcreatinganewoneeachtime.This
approachisalsousefulforcreatingoraccessingmultidimensional
arraysorotherobjectswithcomplexstructure.Whileyoucanuse
nestedQVariantListorQVariantMapobjects,creatingQJSValueobjects
directlymaybemoreefficient.
Qtwillnotbeabletorecognizeandautomaticallyconvertacustom
type.Attemptingtoaccesssuchmethodorpropertyfrom
JavaScriptwillresultinanerror.YoucanusetheQ_GADGETmacroto
makeaC++datatypeavailabletoJavaScriptanduseQ_PROPERTYto
declarepropertiesthatshouldbeexposed.
Formoreinformationonthistopic,refertotheDataTypeConversionBetweenQMLand
C++documentationpage.
Accessingsignalsandslotsin
scripts
QJSEnginealsooffersthecapabilitytousesignalsandslots.Theslot
canbeeitheraC++methodoraJavaScriptfunction.The
connectioncanbemadeeitherinC++orinthescript.
First,let'sseehowtoestablishaconnectionwithinascript.Whena
QObjectinstanceisexposedtoascript,theobject'ssignalsbecomethe
propertiesofthewrappingobject.Thesepropertieshaveaconnect
methodthatacceptsafunctionobjectthatistobecalledwhenthe
signalisemitted.ThereceivercanbearegularslotoraJavaScript
function.Themostcommoncaseiswhenyouconnectthesignalto
ananonymousfunction:
pushButton.toggled.connect(function(){
console.log('buttontoggled!');
});
Ifyouneedtoundotheconnection,youwillneedtostorethe
functioninavariable:
functionbuttonToggled(){
//...
}
pushButton.toggled.connect(buttonToggled);
//...
pushButton.toggled.disconnect(buttonToggled);
Youcandefinethethisobjectforthefunctionbyprovidinganextra
argumenttoconnect():
varobj={'name':'FooBar'};
varobj={'name':'FooBar'};
pushButton.clicked.connect(obj,function(){
console.log(this.name);
});
Youcanalsoconnectthesignaltoasignalorslotofanother
exposedobject.Toconnecttheclicked()signalofanobjectcalled
pushButtontoaclear()slotofanotherobjectcalledlineEdit,youcanuse
thefollowingstatement:
pushButton.clicked.connect(lineEdit.clear);
Emittingsignalsfromwithinthescriptisalsoeasy—justcallthe
signalasafunctionandpasstoitanynecessaryparameters:
pushButton.clicked();
spinBox.valueChanged(7);
Tocreateasignal-slotconnectionontheC++sidewherethe
receiverisaJavaScriptfunction,youcanutilizeC++lambda
functionsandtheQJSValue::call()function:
QJSValuefunc=engine.evaluate(
"function(checked){console.log('func',checked);}");
QObject::connect(&button,&QPushButton::clicked,[func](boolchecked)
{
QJSValue(func).call({checked});
});
Timeforaction–Usinga
buttonfromJavaScript
Let'sputallthistogetherandbuildacompleteexampleofa
scriptablebutton:
intmain(intargc,char*argv[]){
QApplicationapp(argc,argv);
QJSEngineengine;
engine.installExtensions(QJSEngine::ConsoleExtension);
QPushButtonbutton;
engine.globalObject().setProperty("pushButton",
engine.newQObject(&button));
QStringscript=
"pushButton.text='MyScriptedButton';\n"
"pushButton.checkable=true;\n"
"pushButton.setChecked(true);\n"
"pushButton.toggled.connect(function(checked){\n"
"console.log('buttontoggled!',checked);\n"
"});\n"
"pushButton.show();";
engine.evaluate(script);
QJSValuefunc=engine.evaluate(
"function(checked){console.log('buttontoggled2!',
checked);}");
QObject::connect(&button,&QPushButton::clicked,[func](bool
checked){
QJSValue(func).call({checked});
});
returnapp.exec();
}
Inthiscode,weexposethefunctiontoJavaScriptandexecutecode
thatsetssomepropertiesofthebuttonandaccessesitstoggled
signal.Next,wecreateaJavaScriptfunction,storeareferencetoit
inthefuncvariable,andconnectthetoggledsignalofthebuttonto
thisfunctionfromC++side.
RestrictingaccesstoC++
classesfromJavaScript
Therearecaseswhenyouwanttoprovidearichinterfaceforaclass
tomanipulateitfromwithinC++easily,buttohavestrictcontrol
overwhatcanbedoneusingscripting,youwanttopreventscripters
fromusingsomeofthepropertiesormethodsoftheclass.
Thesafestapproachistocreateawrapperclassthatonlyexposes
theallowedmethodsandsignals.Thiswillallowyoutodesignyour
originalclassesfreely.Forexample,ifyouwanttohidesome
methods,it'squiteeasy—justdon'tmakethemslotsanddon't
declarethemwithQ_INVOKABLE.However,youmaywantthemtobe
slotsintheinternalimplementation.Bycreatingawrapperclass,
youcaneasilyhideslotsoftheinternalclassfromtheJavaScript
code.We'llshowhowtoapplythisapproachlaterinthischapter.
Anotherissuemayariseifthedatatypesusedbyyourinternal
objectcannotbedirectlyexposedtoJavaScript.Forexample,ifone
ofyourmethodsreturnsaQVector<QVector<int>>,youwillnotbeableto
callsuchamethoddirectlyfromJavaScript.Thewrapperclassisa
goodplacetoputtherequireddataconversionoperations.
YoushouldalsobeawarethatJavaScriptcodecanemitanysignals
ofexposedC++objects.Insomecases,thiscanbreakthelogicof
yourapplication.Ifyou'reusingawrapper,youcanjustconnectthe
signaloftheinternalclasstothesignaloftheexposedwrapper.The
scriptwillbeabletoconnecttothewrapper'ssignal,butitwon'tbe
abletoemittheoriginalsignal.However,thescriptwillbeableto
emitthewrapper'ssignal,andthiscanaffectalltheother
JavaScriptcodeintheengine.
IfalloralmostallAPIsoftheclassaresafetoexposetoJavaScript,
it'smucheasiertomaketheobjectsthemselvesavailable,insteadof
creatingwrappers.Ifyouwanttorestrictaccesstocertainmethods,
keepinmindthatJavaScriptcodecanonlyaccesspublicand
protectedmethodsdeclaredwithQ_INVOKABLEandslots.Remember
thatyoucanstillconnectsignalstonon-slotmethodsifyouusethe
connect()variantthattakesafunctionpointerasanargument.
JavaScriptcodealsocannotaccessanyprivatemethods.
Forproperties,youcanmarktheminaccessiblefromscriptsusing
theSCRIPTABLEkeywordintheQ_PROPERTYdeclaration.Bydefault,all
propertiesarescriptable,butyoucanforbidtheirexposureto
scriptsbysettingSCRIPTABLEtofalse,asshowninthefollowing
example:
Q_PROPERTY(QStringinternalNameREADinternalNameSCRIPTABLEfalse)
CreatingC++objectsfrom
JavaScript
We'veonlyexposedtheexistingC++objectstoJavaScriptsofar,
butwhatifyouwanttocreateanewC++objectfromJavaScript?
Youcandothisusingwhatyoualreadyknow.AC++methodofan
alreadyexposedobjectcancreateanewobjectforyou:
public:
Q_INVOKABLEQObject*createMyObject(intargument){
returnnewMyObject(argument);
}
WeuseQObject*insteadofMyObject*inthefunctionsignature.Thisallowsustoimportthe
objectintotheJSengineautomatically.Theenginewilltakeownershipoftheobjectand
deleteitwhentherearenomorereferencestoitinJavaScript.
UsingthismethodfromJavaScriptisalsoprettystraightforward:
varnewObject=originalObject.createMyObject(42);
newObject.slot1();
ThisapproachisfineifyouhaveagoodplaceforthecreateMyObject
function.However,sometimesyouwanttocreatenewobjects
independentlyoftheexistingones,oryoudon'thaveanyobjects
createdyet.Forthesesituations,thereisaneatwaytoexposethe
constructoroftheclasstotheJavaScriptengine.First,youneedto
makeyourconstructorinvokableintheclassdeclaration:
public:
Q_INVOKABLEexplicitMyObject(intargument,QObject*parent=
nullptr);
Then,youshouldusethenewQMetaObject()functiontoimportthemeta-
objectoftheclasstotheengine.Youcanimmediatelyassignthe
importedmeta-objecttoapropertyoftheglobalobject:
engine.globalObject().setProperty("MyObject",
engine.newQMetaObject(&MyObject::staticMetaObject));
Youcannowinvoketheconstructorbycallingtheexposedobject
withthenewkeyword:
varnewObject=newMyObject(42);
newObject.slot1();
ExposingC++functionsto
JavaScript
Sometimesyoujustwanttoprovideasinglefunctioninsteadofan
object.Unfortunately,QJSEngineonlysupportsfunctionsthatbelong
toQObject-derivedclasses.However,wecanhidethisimplementation
detailfromtheJavaScriptside.First,createasubclassofQObjectand
addaninvokablememberfunctionthatproxiestheoriginal
standalonefunction:
Q_INVOKABLEdoublefactorial(intx){
returnsuperFastFactorial(x);
}
Next,exposethewrapperobjectusingthenewQObject()function,as
usual.However,insteadofassigningthisobjecttoapropertyofthe
globalobject,extractthefactorialpropertyfromtheobject:
QJSValuemyObjectJS=engine.newQObject(newMyObject());
engine.globalObject().setProperty("factorial",
myObjectJS.property("factorial"));
Now,theJavaScriptcodecanaccessthemethodasifitwerea
globalfunction,likefactorial(4).
CreatingaJavaScriptscripting
game
Let'sperfectourskillsbyimplementingagamethatallowsplayers
touseJavaScript.Therulesaresimple.Eachplayerhasanumberof
entitiesthatmoveontheboard.Allentitiesmoveinturns;during
eachturn,theentitycanstandstillormovetoanadjacenttile
(cardinallyordiagonally).Ifanentitymovestothetileoccupiedby
anotherentity,thatentityiskilledandremovedfromtheboard.
Atthebeginningofthegame,allentitiesareplacedrandomlyon
theboard.Anexampleofastartingpositionisdisplayedonthe
followingimage:
EachplayermustprovideaJavaScriptfunctionthatreceivesan
entityobjectandreturnsitsnewposition.Thisfunctionwillbe
calledwhenoneoftheplayer'sentitiesshouldmove.Additionally,
theplayermayprovideaninitializationfunctionthatwillbecalled
atthebeginningofthegame.Thestateoftheboardandentitieson
itwillbeexposedthroughapropertyoftheglobalJavaScriptobject.
Inourgame,theplayerswillcompetetocreatethebestsurvival
strategy.Oncethegameisstarted,theplayershavenocontrolover
theentities,andtheprovidedJavaScriptfunctionsmustaccountfor
anypossiblegamesituation.Whenonlyentitiesofoneplayer
remainontheboard,thatplayerwins.Therulesallowanynumber
ofplayerstoparticipate,althoughwewillonlyhavetwoplayersin
ourexample.
Timeforaction–Implementing
thegameengine
WewillusetheGraphicsViewframeworktoimplementtheboard
visualization.Wewillnotprovidetoomanydetailsaboutthe
implementation,sincewefocusonscriptinginthischapter.The
basicskillsyoulearnedinChapter4,Custom2DGraphicswith
GraphicsView,shouldbeenoughforyoutoimplementthisgame.
Thefullcodeofthisexampleisprovidedwiththebook.However,
wewillhighlightthearchitectureoftheprojectandbrieflydescribe
howitworks.
Thegameengineimplementationconsistsoftwoclasses:
TheSceneclass(derivedfromQGraphicsScene)managesthe
graphicsscene,createsitems,andimplementsthegeneral
gamelogic
TheEntityclass(derivedfromQGraphicsEllipseItem)representsa
singlegameentityontheboard
EachEntityobjectisacirclewith0.4radiusand(0,0)center.Itis
initializedintheconstructor,usingthefollowingcode:
setRect(-0.4,-0.4,0.8,0.8);
setPen(Qt::NoPen);
Wewillusetheposproperty(inheritedfromQGraphicsItem)tomovethe
circleontheboard.Thetilesoftheboardwillhaveaunitsize,sowe
canjusttreatposasintegerQPointinsteadofQPointFwithdouble
coordinates.Wewillzoomintothegraphicsviewtoachievethe
desiredvisiblesizeoftheentities.
TheEntityclasshastwospecialpropertieswithgettersandsetters.
Theteampropertyisthenumberoftheplayerthisentitybelongsto.
Thispropertyalsodefinesthecolorofthecircle:
voidEntity::setTeam(intteam){
m_team=team;
QColorcolor;
switch(team){
case0:
color=Qt::green;
break;
case1:
color=Qt::red;
break;
}
setBrush(color);
}
Thealiveflagindicateswhethertheentityisstillinplay.For
simplicity,wewillnotimmediatelydeletethekilledentityobjects.
Wewilljusthidetheminstead:
voidEntity::setAlive(boolalive)
{
m_alive=alive;
setVisible(alive);
//...
}
Let'sturnourattentiontotheSceneclass.First,itdefinessomegame
configurationoptions:
ThefieldSizepropertydeterminesthetwo-dimensionalsize
oftheboard
TheteamSizepropertydetermineshowmanyentitieseach
playerhasatthebeginningofthegame
ThestepDurationpropertydeterminesthenumberof
millisecondspassedbetweenexecutingthenextroundof
turns
ThesetterofthefieldSizepropertyadjuststhescenerectsothatthe
graphicsviewiscorrectlyresizedatthebeginningofthegame:
voidScene::setFieldSize(constQSize&fieldSize)
{
m_fieldSize=fieldSize;
setSceneRect(-1,-1,
m_fieldSize.width()+2,
m_fieldSize.height()+2);
}
Theexecutionofeachroundofthegamewillbedoneinthestep()
function.Intheconstructor,weinitializeaQTimerobjectresponsible
forcallingthisfunction:
m_stepTimer=newQTimer(this);
connect(m_stepTimer,&QTimer::timeout,
this,&Scene::step);
m_stepTimer->setInterval(1000);
InthesetStepDuration()function,wesimplychangetheintervalofthis
timer.
TheQVector<Entity*>m_entitiesprivatefieldoftheSceneclasswillcontain
alltheentitiesinplay.Thegameisstartedbycallingthestart()
function.Let'stakealookatit:
voidScene::start(){
constintTEAM_COUNT=2;
for(inti=0;i<m_teamSize;i++){
for(intteam=0;team<TEAM_COUNT;team++){
Entity*entity=newEntity(this);
entity->setTeam(team);
QPointpos;
do{
pos.setX(qrand()%m_fieldSize.width());
pos.setY(qrand()%m_fieldSize.height());
}while(itemAt(pos,QTransform()));
entity->setPos(pos);
addItem(entity);
m_entities<<entity;
}
}
//...
m_stepTimer->start();
}
Wecreatetherequestednumberofentitiesforeachteamandplace
thematrandomlocationsontheboard.Ifwehappentochoosean
alreadyoccupiedplace,wegoonthenextiterationofthedo-while
loopandchooseanotherlocation.Next,weaddthenewitemtothe
sceneandtothem_entitiesvector.Finally,westartourtimersothat
thestep()functionwillbecalledperiodically.
Inthemain()function,weinitializetherandomnumbergenerator
becausewewanttogetnewrandomnumberseachtime:
qsrand(QDateTime::currentMSecsSinceEpoch());
Then,wecreateandinitializetheSceneobject,andwecreatea
QGraphicsViewtodisplayourscene.
Thegameengineisalmostready.Weonlyneedtoimplementthe
scripting.
Timeforaction–Exposingthe
gamestatetotheJSengine
Beforewemoveontoexecutingtheplayers'scripts,weneedto
createaQJSEngineandinsertsomeinformationintoitsglobalobject.
Thescriptswillusethisinformationtodecidetheoptimalmove.
First,weaddtheQJSEnginem_jsEngineprivatefieldtotheSceneclass.
Next,wecreateanewSceneProxyclassandderiveitfromQObject.This
classwillexposethepermittedAPIofScenetothescripts.Wepassa
pointertotheSceneobjecttotheconstructoroftheSceneProxyobject
andstoreitinaprivatevariable:
SceneProxy::SceneProxy(Scene*scene):
QObject(scene),m_scene(scene)
{
}
Addtwoinvokablemethodstotheclassdeclaration:
Q_INVOKABLEQSizesize()const;
Q_INVOKABLEQJSValueentities()const;
Theimplementationofthesize()functionisprettystraightforward:
QSizeSceneProxy::size()const{
returnm_scene->fieldSize();
}
However,theentities()functionisabittrickier.WecannotaddEntity
objectstotheJSenginebecausetheyarenotbasedonQObject.Even
ifwecould,weprefertocreateaproxyclassforentitiesaswell.
Let'sdothisrightnow.CreatetheEntityProxyclass,deriveitfrom
QObject,andpassapointertotheunderlyingEntityobjecttothe
constructor,likewedidinSceneProxy.Declaretwoinvokablefunctions
andasignalinthenewclass:
classEntityProxy:publicQObject
{
Q_OBJECT
public:
explicitEntityProxy(Entity*entity,QObject*parent=nullptr);
Q_INVOKABLEintteam()const;
Q_INVOKABLEQPointpos()const;
//...
signals:
voidkilled();
private:
Entity*m_entity;
};
Implementationofthemethodsjustforwardthecallstothe
underlyingEntityobject:
intEntityProxy::team()const
{
returnm_entity->team();
}
QPointEntityProxy::pos()const
{
returnm_entity->pos().toPoint();
}
TheEntityclasswillberesponsibleforcreatingitsownproxyobject.
AddthefollowingprivatefieldstotheEntityclass:
EntityProxy*m_proxy;
QJSValuem_proxyValue;
Them_proxyfieldwillholdtheproxyobject.Them_proxyValuefieldwill
containthereferencetothesameobjectaddedtotheJSengine.
Initializethesefieldsintheconstructor:
m_proxy=newEntityProxy(this,scene);
m_proxyValue=scene->jsEngine()->newQObject(m_proxy);
WemodifytheEntity::setAlive()functiontoemitthekilled()signal
whentheentityiskilled:
voidEntity::setAlive(boolalive)
{
m_alive=alive;
setVisible(alive);
if(!alive){
emitm_proxy->killed();
}
}
It'sgenerallyconsideredbadpracticetoemitsignalsfromoutsidetheclassthatownsthe
signal.IfthesourceofthesignalisanotherQObject-basedclass,youshouldcreatea
separatesignalinthatclassandconnectittothedestinationsignal.Inourcase,wecannot
dothat,sinceEntityisnotaQObject,sowechoosetoemitthesignaldirectlytoavoid
furthercomplication.
Createtheproxy()andproxyValue()gettersforthesefields.Wecannow
returntotheSceneProxyimplementationandusetheentityproxy:
QJSValueSceneProxy::entities()const
{
QJSValuelist=m_scene->jsEngine()->newArray();
intarrayIndex=0;
for(Entity*entity:m_scene->entities()){
if(entity->isAlive()){
list.setProperty(arrayIndex,entity->proxyValue());
arrayIndex++;
}
}
returnlist;
}
Whatjusthappened?
First,weasktheJSenginetocreateanewJavaScriptarrayobject.
Then,weiterateoverallentitiesandskipentitiesthatwerealready
killed.WeuseQJSValue::setPropertytoaddtheproxyobjectofeach
entitytothearray.Weneedtospecifytheindexofthenewarray
item,sowecreatethearrayIndexcounterandincrementitaftereach
insertion.Finally,wereturnthearray.
ThisfunctioncompletestheSceneProxyclassimplementation.Wejust
needtocreateaproxyobjectandaddittotheJSengineinthe
constructoroftheSceneclass:
SceneProxy*sceneProxy=newSceneProxy(this);
m_sceneProxyValue=m_jsEngine.newQObject(sceneProxy);
Timeforaction–Loading
scriptsprovidedbyusers
Eachplayerwillprovidetheirownstrategyscript,sotheSceneclass
shouldhaveafieldforstoringallprovidedscripts:
QHash<int,QJSValue>m_teamScripts;
Let'sprovidethesetScript()functionthatacceptstheplayer'sscript
andloadsitintotheJSengine:
voidScene::setScript(intteam,constQString&script){
QJSValuevalue=m_jsEngine.evaluate(script);
if(value.isError()){
qDebug()<<"jserror:"<<value.toString();
return;
}
if(!value.isObject()){
qDebug()<<"scriptmustreturnanobject";
return;
}
m_teamScripts[team]=value;
}
Inthisfunction,wetrytoevaluatetheprovidedcode.Ifthecode
returnedaJavaScriptobject,weputitinthem_teamScriptshashtable.
Weexpectthattheprovidedobjecthasthesteppropertycontaining
thefunctionthatdecidestheentity'smove.Theobjectmayalso
containtheinitpropertythatwillbeexecutedatthebeginningof
thegame.
Inthemain()function,weloadthescriptsfromtheproject's
resources:
scene.setScript(0,loadFile(":/scripts/1.js"));
scene.setScript(1,loadFile(":/scripts/2.js"));
TheloadFile()helperfunctionsimplyloadsthecontentofthefiletoa
QString:
QStringloadFile(constQString&path){
QFilefile(path);
if(!file.open(QFile::ReadOnly)){
qDebug()<<"failedtoopen"<<path;
returnQString();
}
returnQString::fromUtf8(file.readAll());
}
Ifyouwanttoallowuserstoprovidetheirscriptswithoutneeding
torecompiletheproject,youcanacceptthescriptfilesfromthe
command-lineargumentsinstead:
QStringListarguments=app.arguments();
if(arguments.count()<3){
qDebug()<<"usage:"<<argv[0]<<"path/to/script1.js
path/to/script2.js";
return1;
}
scene.setScript(0,loadFile(arguments[1]));
scene.setScript(1,loadFile(arguments[2]));
Tosetthecommand-lineargumentsforyourproject,switchtotheProjectspane,select
RunintheleftcolumnandlocatetheCommandlineargumentsinputbox.Theprovided
projectcontainstwosamplescriptsinthescriptssubdirectory.
Timeforaction–Executingthe
strategyscripts
First,weneedtocheckwhethertheplayerprovidedaninitfunction
andexecuteit.We'lldoitintheScene::start()function:
for(intteam=0;team<TEAM_COUNT;team++){
QJSValuescript=m_teamScripts.value(team);
if(script.isUndefined()){
continue;
}
if(!script.hasProperty("init")){
continue;
}
m_jsEngine.globalObject().setProperty("field",m_sceneProxyValue);
QJSValuescriptOutput=script.property("init").call();
if(scriptOutput.isError()){
qDebug()<<"scripterror:"<<scriptOutput.toString();
continue;
}
}
Inthiscode,weuseisUndefined()tocheckwhetherthecodewas
providedandparsedsuccessfully.Next,weusehasProperty()tocheck
whetherthereturnedobjectcontainstheoptionalinitfunction.If
wefoundit,weexecuteitusingQJSValue::call().Weprovidesome
informationabouttheboardbyassigningourSceneProxyinstanceto
thefieldpropertyoftheglobalobject.
Themostexcitingpartisthestep()functionthatimplementsthe
actualgameexecution.Let'stakealookatit:
voidScene::step(){
for(Entity*entity:m_entities){
if(!entity->isAlive()){
if(!entity->isAlive()){
continue;
}
QJSValuescript=m_teamScripts.value(entity->team());
if(script.isUndefined()){
continue;
}
m_jsEngine.globalObject().setProperty("field",
m_sceneProxyValue);
QJSValuescriptOutput=
script.property("step").call({entity->proxyValue()});
//...
}
}
First,weiterateoverallentitiesandskipthekilledones.Next,we
useEntity::team()todeterminewhichplayerthisentitybelongsto.We
extractthecorrespondingstrategyscriptfromthem_teamScriptsfield
andextractthesteppropertyfromit.Then,wetrytocallitasa
functionandpassthecurrententity'sproxyobjectasanargument.
Let'sseewhatwedowiththescriptoutput:
if(scriptOutput.isError()){
qDebug()<<"scripterror:"<<scriptOutput.toString();
continue;
}
QJSValuescriptOutputX=scriptOutput.property("x");
QJSValuescriptOutputY=scriptOutput.property("y");
if(!scriptOutputX.isNumber()||!scriptOutputY.isNumber()){
qDebug()<<"invalidscriptoutput:"<<scriptOutput.toVariant();
continue;
}
QPointpos(scriptOutputX.toInt(),scriptOutputY.toInt());
if(!moveEntity(entity,pos)){
qDebug()<<"invalidmove";
}
Wetrytointerpretthefunction'sreturnvalueasanobjectwithx
andyproperties.Ifbothpropertiescontainnumbers,weconstructa
QPointfromthemandcallourmoveEntity()functionthattriestoexecute
themovechosenbythestrategy.
Wewillnotblindlytrustthevaluereturnedbytheuser'sscript.
Instead,wecarefullycheckwhetherthemoveisvalid:
boolScene::moveEntity(Entity*entity,QPointpos){
if(pos.x()<0||pos.y()<0||
pos.x()>=m_fieldSize.width()||
pos.y()>=m_fieldSize.height())
{
returnfalse;//outoffieldbounds
}
QPointposChange=entity->pos().toPoint()-pos;
if(posChange.isNull()){
returntrue;//nochange
}
if(qAbs(posChange.x())>1||qAbs(posChange.y())>1){
returnfalse;//invalidmove
}
QGraphicsItem*item=itemAt(pos,QTransform());
Entity*otherEntity=qgraphicsitem_cast<Entity*>(item);
if(otherEntity){
otherEntity->setAlive(false);
}
entity->setPos(pos);
returntrue;
}
Wecheckthatthenewpositionisinboundsandisnottoofarfrom
theentity'scurrentposition.Ifeverythingiscorrect,weexecutethe
move.Ifanotherentitywasonthedestinationtile,wemarkitas
killed.Thefunctionreturnstrueifthemovewassuccessful.
That'sit!Ourgameisreadytorun.Let'screatesomestrategy
scriptstoplaywith.
Timeforaction–Writinga
strategyscript
Ourfirstscriptwillsimplyselectarandommove:
{
"step":function(current){
functiongetRandomInt(min,max){
returnMath.floor(Math.random()*(max-min))+min;
}
return{
x:current.pos().x+getRandomInt(-1,2),
y:current.pos().y+getRandomInt(-1,2),
}
}
}
Ofcourse,amoreintelligentstrategycanbeatthisscript.Youcan
findamoreadvancedscriptinthecodebundle.First,whenitsees
anenemyentitynearby,italwaysgoesforthekill.Ifthereisno
suchenemy,ittriestomoveawayfromtheclosestally,attempting
tofillthewholeboard.Thisscriptwilleasilywipeouttherandomly
movingenemy:
Ofcourse,thereisalwaysroomforimprovement.Trytothinkofa
betterstrategyandwriteascriptthatcanwinthegame.
Haveagohero–Extendingthe
game
Thereareacoupleofwaysforyoutoimprovethegame
implementation.Forexample,youcandetectwhenaplayerhas
wonanddisplayapop-upmessage.Youcanalsoallowanarbitrary
numberofplayers.YoujustneedtoreplacetheTEAM_COUNTconstant
withanewpropertyintheSceneclassanddefinemoreteamcolors.
YoucanevencreateaGUIforuserstoprovidetheirscriptsinstead
ofpassingthemascommand-linearguments.
Thescriptingenvironmentcanalsobeimproved.Youcanprovide
morehelperfunctions(forexample,afunctiontocalculatethe
distancebetweentwotiles)tomakecreatingscriptseasier.Onthe
otherhand,youcanmodifytherulesandreducetheamountof
availableinformationsothat,forexample,eachentitycanonlysee
otherentitiesatacertaindistance.
Asdiscussedearlier,eachscripthaswaystobreaktheglobalobject
oremitthesignalsoftheexposedC++objects,affectingtheother
players.Topreventthat,youcancreateaseparateQJSEngineanda
separatesetofproxyobjectsforeachplayer,effectivelysandboxing
them.
Pythonscripting
QtQMLisanenvironmentthatisdesignedtobepartoftheQt
world.SincenoteveryoneknowsorlikesJavaScript,wewillpresent
anotherlanguagethatcaneasilybeusedtoprovidescripting
environmentsforgamesthatarecreatedwithQt.Justbeawarethat
thiswillnotbeanin-depthdescriptionoftheenvironment—wewill
justshowyouthebasicsthatcanprovidefoundationsforyourown
research.
ApopularlanguageusedforscriptingisPython.Therearetwo
variantsofQtbindingsthatareavailableforPython:PySide2and
PyQt.PySide2istheofficialbindingthatisavailableunderLGPL.
PyQtisathird-partylibrarythatisavailableunderGPLv3anda
commerciallicense.
PyQtisnotavailableunderLGPL,soforcommercialclosed-sourceproducts,youneedto
obtainacommerciallicensefromRiverbankcomputing!
ThesebindingsallowyoutousetheQtAPIfromwithinPython—
youcanwriteacompleteQtapplicationusingjustPython.
However,tocallPythoncodefromwithinC++,youwillneeda
regularPythoninterpreter.Luckily,itisveryeasytoembedsuchan
interpreterinaC++application.
First,youwillneedPythoninstalled,alongwithitsdevelopment
package.Forexample,forDebian-basedsystems,itiseasiestto
simplyinstallthelibpythonX.Y-devpackage,whereX.Ystandsforthe
versionofPythonavailableintherepository:
sudoapt-getinstalllibpython3.5-dev
WewillusePython3.5inourexample,butlaterminorversions
shouldalsobecompatiblewithourcode.
Then,youneedtotellqmaketolinkyourprogramagainstthe
library.ForLinux,youcanusepkgconfigtodothisautomatically:
CONFIG+=link_pkgconfigno_keywords
#adjusttheversionnumbertosuityourneeds
PKGCONFIG+=python-3.5m
Theno_keywordsconfigurationoptiontellsthebuildsystemtodisableQt-specifickeywords
(signals,slots,andemit).WehavetodothisbecausePythonheadersusetheslots
identifierthatwouldconflictwiththesameQtkeyword.YoucanstillaccesstheQt
keywordsifyouwritethemasQ_SIGNALS,Q_SLOTS,andQ_EMIT.
ForWindows,youneedtomanuallypassinformationtothe
compiler:
CONFIG+=no_keywords
INCLUDEPATH+=C:\Python35\include
LIBS+=-LC:\Python35\include-lpython35
TocallPythoncodefromwithinaQtapp,thesimplestwayistouse
thefollowingcode:
#include<Python.h>
#include<QtCore>
intmain(intargc,char**argv){
QCoreApplicationapp(argc,argv);
Py_Initialize();
constchar*script="print(\"HellofromPython\")";
PyRun_SimpleString(script);
Py_Finalize();
returnapp.exec();
}
ThiscodeinitializesaPythoninterpreter,theninvokesascriptby
passingitdirectlyasastring,andfinally,itshutsdownthe
interpreterbeforeinvokingQt'seventloop.Suchcodemakessense
onlyforsimplescripting.Inreallife,you'dwanttopasssomedata
tothescriptorfetchtheresult.Forthat,wehavetowritesome
morecode.AsthelibraryexposestheCAPIonly,let'swriteanice
Qtwrapperforit.
Timeforaction–WritingaQt
wrapperforembeddingPython
Asthefirsttask,wewillimplementthelastprogramusingan
object-orientedAPI.Createanewconsoleprojectandaddthe
followingclasstoit:
classQtPython:publicQObject{
Q_OBJECT
public:
QtPython(QObject*parent=0);
~QtPython();
voidrun(constQString&program);
private:
QVector<wchar_t>programNameBuffer;
};
Theimplementationfileshouldlooklikethis:
#include<Python.h>
//...
QtPython::QtPython(QObject*parent):QObject(parent){
QStringListargs=qApp->arguments();
if(args.count()>0){
programNameBuffer.resize(args[0].count());
args[0].toWCharArray(programNameBuffer.data());
Py_SetProgramName(programNameBuffer.data());
}
Py_InitializeEx(0);
}
QtPython::~QtPython(){
Py_Finalize();
}
voidQtPython::run(constQString&program){
voidQtPython::run(constQString&program){
PyRun_SimpleString(qPrintable(program));
}
Then,addamain()function,asshowninthefollowingsnippet:
intmain(intargc,char*argv[])
{
QCoreApplicationapp(argc,argv);
QtPythonpython;
python.run("print('HellofromPython')");
return0;
}
Finally,openthe.profileandtellQttolinkwiththePythonlibrary,
aswasshownearlier.
Whatjusthappened?
WecreatedaclasscalledQtPythonthatwrapsthePythonCAPIforus.
NeveruseaQprefixtocallyourcustomclasses,asthisprefixisreservedforofficialQt
classes.Thisistoensurethatyourcodewillneverhaveanameclashwithfuturecode
addedtoQt.TheQtprefix,ontheotherhand,ismeanttobeusedwithclassesthatare
extensionstoQt.Youprobablystillshouldn'tuseit,buttheprobabilityofanameclashis
muchsmallerandyieldsalesserimpactthanclasheswithanofficialclass.Itisbestto
comeupwithanapplication-specificprefixoruseanamespace.
TheclassconstructorcreatesaPythoninterpreter,andtheclass
destructordestroysit.WeusePy_InitializeEx(0),whichhasthesame
functionalityasPy_Initialize(),butitdoesnotapplyCsignal
handlers,asthisisnotsomethingwewouldwantwhenembedding
Python.Priortothis,weusePy_SetProgramName()toinformthe
interpreterofourcontext.Wealsodefinedarun()method,taking
QStringandreturningvoid.ItusesqPrintable(),whichisaconvenience
functionthatextractsaCstringpointerfromaQStringobject,which
isthenfedintoPyRun_SimpleString().
NeverstoretheoutputofqPrintable(),asitreturnsaninternalpointertoatemporarybyte
array(thisisequivalenttocallingtoLocal8Bit().constData()onastring).Itissafetouse
directly,butthebytearrayisdestroyedimmediatelyafterward;thus,ifyoustorethe
pointerinavariable,thedatamaynotbevalidlaterwhenyoutryusingthatpointer.
Themostdifficultworkwhenusingembeddedinterpretersisto
convertvaluesbetweenC++andthetypesthattheinterpreter
expects.WithQtScript,theQScriptValuetypewasusedforthis.We
canimplementsomethingsimilarforourPythonscripting
environment.
Timeforaction–Converting
databetweenC++andPython
CreateanewclassandcallitQtPythonValue:
classQtPythonValue{
public:
QtPythonValue();
QtPythonValue(constQtPythonValue&other);
QtPythonValue&operator=(constQtPythonValue&other);
QtPythonValue(intval);
QtPythonValue(constQString&str);
~QtPythonValue();
inttoInt()const;
QStringtoString()const;
boolisNone()const;
private:
QtPythonValue(PyObject*ptr);
voidincRef();
voidincRef(PyObject*val);
voiddecRef();
PyObject*m_value;
friendclassQtPython;
};
Next,implementtheconstructors,theassignmentoperator,andthe
destructor,asfollows:
QtPythonValue::QtPythonValue(){
incRef(Py_None);
}
QtPythonValue::QtPythonValue(constQtPythonValue&other){
incRef(other.m_value);
}
QtPythonValue::QtPythonValue(PyObject*ptr){
m_value=ptr;
}
QtPythonValue::QtPythonValue(constQString&str){
m_value=PyUnicode_FromString(qPrintable(str));
}
QtPythonValue::QtPythonValue(intval){
m_value=PyLong_FromLong(val);
}
QtPythonValue&QtPythonValue::operator=(constQtPythonValue&other){
if(m_value==other.m_value){
return*this;
}
decRef();
incRef(other.m_value);
return*this;
}
QtPythonValue::~QtPythonValue()
{
decRef();
}
Then,implementtheincRef()anddecRef()functions:
voidQtPythonValue::incRef(PyObject*val){
m_value=val;
incRef();
}
voidQtPythonValue::incRef(){
if(m_value){
Py_INCREF(m_value);
}
}
voidQtPythonValue::decRef(){
if(m_value){
Py_DECREF(m_value);
}
}
Next,implementconversionsfromQtPythonValuetoC++types:
intQtPythonValue::toInt()const{
returnPyLong_Check(m_value)?PyLong_AsLong(m_value):0;
returnPyLong_Check(m_value)?PyLong_AsLong(m_value):0;
}
QStringQtPythonValue::toString()const{
returnPyUnicode_Check(m_value)?
QString::fromUtf8(PyUnicode_AsUTF8(m_value)):QString();
}
boolQtPythonValue::isNone()const{
returnm_value==Py_None;
}
Finally,let'smodifythemain()functiontotestournewcode:
intmain(intargc,char*argv[]){
QCoreApplicationapp(argc,argv);
QtPythonpython;
QtPythonValueinteger=7,string=QStringLiteral("foobar"),
none;
qDebug()<<integer.toInt()<<string.toString()<<none.isNone();
return0;
}
Whenyouruntheprogram,youwillseethattheconversion
betweenC++andPythonworkscorrectlyinbothdirections.
Whatjusthappened?
TheQtPythonValueclasswrapsaPyObjectpointer(throughthem_value
member),providinganiceinterfacetoconvertbetweenwhatthe
interpreterexpectsandourQttypes.Let'sseehowthisisdone.
First,takealookatthethreeprivatemethods:twoversionsof
incRef()andonedecRef().PyObjectcontainsaninternalreference
counterthatcountsthenumberofhandlesonthecontainedvalue.
Whenthatcounterdropsto0,theobjectcanbedestroyed.Our
threemethodsuseadequatePythonCAPIcallstoincreaseor
decreasethecounterinordertopreventmemoryleaksandkeep
Python'sgarbagecollectorhappy.
Thesecondimportantaspectisthattheclassdefinesaprivate
constructorthattakesaPyObjectpointer,effectivelycreatinga
wrapperoverthegivenvalue.Theconstructorisprivate;however,
theQtPythonclassisdeclaredasafriendofQtPythonValue,whichmeans
thatonlyQtPythonandQtPythonValuecaninstantiatevaluesbypassing
PyObjectpointerstoit.Now,let'stakealookatpublicconstructors.
ThedefaultconstructorcreatesanobjectpointingtoaNonevalue,
whichrepresentstheabsenceofavalue.Thecopyconstructorand
assignmentoperatorareprettystandard,takingcareof
bookkeepingofthereferencecounter.Then,wehavetwo
constructors—onetakingintandtheothertakingaQStringvalue.
TheyuseappropriatePythonCAPIcallstoobtainaPyObject
representationofthevalue.Notethatthesecallsalreadyincrease
thereferencecountforus,sowedon'thavetodoitourselves.
Thecodeendswithadestructorthatdecreasesthereference
counterandthreemethodsthatprovidesafeconversionsfrom
QtPythonValuetoappropriateQt/C++types.
Haveagohero–Implementing
theremainingconversions
Now,youshouldbeabletoimplementotherconstructorsand
conversionsforQtPythonValuethatoperatesonthefloat,bool,orevenon
QDateandQTimetypes.Tryimplementingthemyourself.Ifneeded,
takealookatthePythondocumentationtofindappropriatecalls
thatyoushoulduse.
ThedocumentationforPython3.5isavailableonlineathttps://docs.python.org/3.5/.If
you'veinstalledadifferentPythonversion,youcanfindthedocumentationforyour
versiononthesamewebsite.
We'llgiveyouaheadstartbyprovidingaskeletonimplementation
ofhowtoconvertQVarianttoQtPythonValue.Thisisespeciallyimportant,
becausePythonmakesuseoftwotypeswhoseequivalentsarenot
availableinC++,namely,tuplesanddictionaries.Wewillneed
themlater,sohavingaproperimplementationiscrucial.Here'sthe
code:
QtPythonValue::QtPythonValue(constQVariant&variant)
{
switch(variant.type()){
caseQVariant::Invalid:
incRef(Py_None);
return;
caseQVariant::String:
m_value=
PyUnicode_FromString(qPrintable(variant.toString()));
return;
caseQVariant::Int:
m_value=PyLong_FromLong(variant.toInt());
return;
caseQVariant::LongLong:
m_value=PyLong_FromLongLong(variant.toLongLong());
return;
caseQVariant::List:{
QVariantListlist=variant.toList();
constintlistSize=list.size();
PyObject*tuple=PyTuple_New(listSize);
for(inti=0;i<listSize;++i){
PyTuple_SetItem(tuple,i,
QtPythonValue(list.at(i)).m_value);
}
m_value=tuple;
return;
}
caseQVariant::Map:{
QVariantMapmap=variant.toMap();
PyObject*dict=PyDict_New();
for(autoiter=map.begin();iter!=map.end();++iter){
PyDict_SetItemString(dict,qPrintable(iter.key()),
QtPythonValue(iter.value()).m_value);
}
m_value=dict;
return;
}
default:
incRef(Py_None);
return;
}
}
Thehighlightedcodeshowshowtocreateatuple(whichisalistof
arbitraryelements)fromQVariantListandhowtocreateadictionary
(whichisanassociativearray)fromQVariantMap.Youshouldalsoadda
QtPythonValueconstructorthattakesQStringListandproducesatuple.
Wehavewrittenquitealotofcodenow,butthereisnowayof
bindinganydatafromourprogramswithPythonscriptingsofar.
Let'schangethat.
Timeforaction–Calling
functionsandreturningvalues
ThenexttaskistoprovidewaystoinvokePythonfunctionsand
returnvaluesfromscripts.Let'sstartbyprovidingaricherrun()API.
ImplementthefollowingmethodintheQtPythonclass:
QtPythonValueQtPython::run(constQString&program,
constQtPythonValue&globals,constQtPythonValue&locals)
{
PyObject*retVal=PyRun_String(qPrintable(program),
Py_file_input,globals.m_value,locals.m_value);
returnQtPythonValue(retVal);
}
We'llalsoneedafunctionalitytoimportPythonmodules.Addthe
followingmethodstotheclass:
QtPythonValueQtPython::import(constQString&name)const
{
returnQtPythonValue(PyImport_ImportModule(qPrintable(name)));
}
QtPythonValueQtPython::addModule(constQString&name)const
{
PyObject*retVal=PyImport_AddModule(qPrintable(name));
Py_INCREF(retVal);
returnQtPythonValue(retVal);
}
QtPythonValueQtPython::dictionary(constQtPythonValue&module)const
{
PyObject*retVal=PyModule_GetDict(module.m_value);
Py_INCREF(retVal);
returnQtPythonValue(retVal);
}
ThelastpieceofthecodeistoextendQtPythonValuewiththiscode:
boolQtPythonValue::isCallable()const{
returnPyCallable_Check(m_value);
}
QtPythonValueQtPythonValue::attribute(constQString&name)const{
returnQtPythonValue(PyObject_GetAttrString(m_value,
qPrintable(name)));
}
boolQtPythonValue::setAttribute(constQString&name,const
QtPythonValue&value){
intretVal=PyObject_SetAttrString(m_value,qPrintable(name),
value.m_value);
returnretVal!=-1;
}
QtPythonValueQtPythonValue::call(constQVariantList&arguments)const
{
returnQtPythonValue(
PyObject_CallObject(m_value,
QtPythonValue(arguments).m_value));
}
QtPythonValueQtPythonValue::call(constQStringList&arguments)const
{
returnQtPythonValue(
PyObject_CallObject(m_value,
QtPythonValue(arguments).m_value));
}
Finally,youcanmodifymain()totestthenewfunctionality:
intmain(intargc,char*argv[])
{
QCoreApplicationapp(argc,argv);
QtPythonpython;
QtPythonValuemainModule=python.addModule("__main__");
QtPythonValuedict=python.dictionary(mainModule);
python.run("foo=(1,2,3)",dict,dict);
python.run("print(foo)",dict,dict);
QtPythonValuemodule=python.import("os");
QtPythonValuechdir=module.attribute("chdir");
chdir.call(QStringList()<<"/home");
QtPythonValuefunc=module.attribute("getcwd");
qDebug()<<func.call(QVariantList()).toString();
return0;
}
Youcanreplace/homewithadirectoryofyourchoice.Then,youcan
runtheprogram.
Whatjusthappened?
Wedidtwotestsinthelastprogram.First,weusedthenewrun()
method,passingtoitthecodethatistobeexecutedandtwo
dictionariesthatdefinethecurrentexecutioncontext—thefirst
dictionarycontainsglobalsymbolsandthesecondcontainslocal
symbols.ThedictionariescomefromPython's__main__module
(which,amongotherthings,definestheprintfunction).Therun()
methodmaymodifythecontentsofthetwodictionaries—thefirst
calldefinesthetuplecalledfoo,andthesecondcallprintsittothe
standardoutput.
Thesecondtestcallsafunctionfromanimportedmodule;inthis
case,wecalltwofunctionsfromtheosmodule—thefirstfunction,
chdir,changesthecurrentworkingdirectory,andtheother,called
getcwd,returnsthecurrentworkingdirectory.Theconventionisthat
weshouldpassatupletocall(),wherewepasstheneeded
parameters.Thefirstfunctiontakesastringasaparameter;
therefore,wepassaQStringListobject,assumingthatthereisa
QtPythonValueconstructorthatconvertsQStringListtoatuple(youneed
toimplementitifyouhaven'tdoneitalready).Sincethesecond
functiondoesnottakeanyparameters,wepassanemptytupleto
thecall.Inthesameway,youcanprovideyourownmodulesand
callfunctionsfromthem,querytheresults,inspectdictionaries,
andsoon.ThisisaprettygoodstartforanembeddedPython
interpreter.Rememberthatapropercomponentshouldhavesome
errorcheckingcodetoavoidcrashingthewholeapplication.
Youcanextendthefunctionalityoftheinterpreterinmanyways.
YoucanevenusePyQt5touseQtbindingsinscripts,combining
Qt/C++codewithQt/Pythoncode.
Haveagohero–WrappingQt
objectsintoPythonobjects
Atthispoint,youshouldbeexperiencedenoughtotryand
implementawrapperfortheQObjectinstancestoexposesignalsand
slotstoPythonscripting.Ifyoudecidetopursuethegoal,
https://docs.python.org/3/willbeyourbestfriend,especiallythesection
aboutextendingPythonwithC++.RememberthatQMetaObject
providesinformationaboutthepropertiesandmethodsofQt
objectsandQMetaObject::invokeMethod()allowsyoutoexecuteamethod
byitsname.Thisisnotaneasytask,sodon'tbehardonyourselfif
youarenotabletocompleteit.Youcanalwaysreturntoitonceyou
gainmoreexperienceinusingQtandPython.
Beforeyouheadontothenextchapter,trytestingyourknowledge
aboutscriptinginQt.
Popquiz
Q1.WhichisthemethodthatyoucanusetoexecuteJavaScript
code?
1. QJSValue::call()
2. QJSEngine::evaluate()
3. QJSEngine::fromScriptValue()
Q2.Whatisthenameoftheclassthatservesasabridgeto
exchangedatabetweenJSengineandC++?
1. QObject
2. QJSValue
3. QVariant
Q3.IfyouwanttoexposeaC++objecttothescript,whichclass
mustthisobjectbederivedfrom?
1. QObject
2. QJSValue
3. QGraphicsItem
Q4.Whichofthefollowingkindsoffunctionsisnotavailableto
JavaScriptcode?
1. Signals
2. Q_INVOKABLEmethods
3. Slots
4. Globalfunctions
Q5.WhenisaPyObjectinstancedestroyed?
1. WhenitsvalueissettoPy_None
2. Whenitsinternalreferencecounterdropsto0
3. WhenthecorrespondingQtPythonValueisdestroyed
Summary
Inthischapter,youlearnedthatprovidingascriptingenvironment
toyourgamesopensupnewpossibilities.Implementinga
functionalityusingscriptinglanguagesisusuallyfasterthandoing
thefullwrite-compile-testcyclewithC++,andyoucanevenusethe
skillsandcreativityofyouruserswhohavenounderstandingofthe
internalsofyourgameenginetomakeyourgamesbetterandmore
feature-rich.YouwereshownhowtouseQJSEngine,whichblendsthe
C++andJavaScriptworldstogetherbyexposingQtobjectsto
JavaScriptandmakingcross-languagesignal-slotconnections.You
alsolearnedthebasicsofscriptingwithPython.Thereareother
scriptinglanguagesavailable(forexample,Lua),andmanyofthem
canbeusedalongwithQt.Usingtheexperiencegainedinthis
chapter,youshouldevenbeabletobringotherscripting
environmentstoyourprograms,asmostembeddableinterpreters
offersimilarapproachestothatofPython.
Inthenextchapter,youwillbeintroducedtoQtQuick—alibrary
forcreatingfluidanddynamicuserinterfaces.Itmaynotsoundlike
it'srelatedtothischapter,butQtQuickisbasedonQtQML.Infact,
anyQtQuickapplicationcontainsaQJSEngineobjectthatexecutes
JavaScriptcodeoftheapplication.Beingfamiliarwiththissystem
willhelpyouunderstandhowsuchapplicationswork.Youwillalso
beabletoapplytheskillsyou'velearnedherewhenyouneedto
accessC++objectsfromQtQuickandviceversa.Welcometothe
worldofQtQuick.
IntroductiontoQtQuick
Inthischapter,youwillbeintroducedtoatechnologycalledQt
Quickthatallowsustoimplementresolution-independentuser
interfaceswithlotsofeye-candy,animations,andeffectsthatcanbe
combinedwithregularQtcodethatimplementsthelogicofthe
application.YouwilllearnthebasicsoftheQMLdeclarative
languagethatformsthefoundationofQtQuick.Youwillcreatea
simpleQtQuickapplicationandseetheadvantagesofferedbythe
declarativeapproach.
Themaintopicscoveredinthischapterarethese:
QMLbasics
OverviewofQtmodules
UsingQtQuickDesigner
UtilizingQtQuickmodules
Propertybindingsandsignalhandling
QtQuickandC++
Statesandtransitions
DeclarativeUIprogramming
AlthoughitistechnicallypossibletouseQtQuickbywritingC++
code,themoduleisaccompaniedbyadedicatedprogramming
languagecalledQML(QtModelingLanguage).QMLisaneasy
toreadandunderstanddeclarativelanguagethatdescribesthe
worldasahierarchyofcomponentsthatinteractandrelatetoone
another.ItusesaJSON-likesyntaxandallowsustouseimperative
JavaScriptexpressionsaswellasdynamicpropertybindings.So,
whatisadeclarativelanguage,anyway?
Declarativeprogrammingisoneoftheprogrammingparadigms
thatdictatesthattheprogramdescribesthelogicofthe
computationwithoutspecifyinghowthisresultshouldbeobtained.
Incontrasttoimperativeprogramming,wherethelogicis
expressedasalistofexplicitstepsforminganalgorithmthat
directlymodifiestheintermediateprogramstate,adeclarative
approachfocusesonwhattheultimateresultoftheoperation
shouldbe.
Timeforaction–Creatingthe
firstproject
Let'screateaprojecttobetterunderstandwhatQMLis.InQt
Creator,selectFileandthenNewFileorProjectinthemainmenu.
ChooseApplicationintheleftcolumnandselecttheQtQuick
Application-Emptytemplate.Nametheprojectascalculatorandgo
throughtherestofthewizard.
QtCreatorcreatedasampleapplicationthatdisplaysanempty
window.Let'sexaminetheprojectfiles.Thefirstfileistheusual
main.cpp:
#include<QGuiApplication>
#include<QQmlApplicationEngine>
intmain(intargc,char*argv[])
{
QGuiApplicationapp(argc,argv);
QQmlApplicationEngineengine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if(engine.rootObjects().isEmpty())
return-1;
returnapp.exec();
}
Thiscodesimplycreatestheapplicationobject,instantiatesthe
QMLengine,andasksittoloadthemain.qmlfilefromtheresources.
Ifanerroroccurs,rootObjects()willreturnanemptylist,andthe
applicationwillterminate.IftheQMLfilewasloadedsuccessfully,
theapplicationentersthemaineventloop.
The*.qrcfileisaresourcefile.Theconceptofresourcefilesshould
befamiliartoyoufromChapter3,QtGUIProgramming.Basically,it
containsthelistofarbitraryprojectfilesthatarerequiredfor
projectexecution.Duringcompilation,thecontentsofthesefiles
areembeddedintotheexecutable.Youcanthenretrievethecontent
atruntimebyspecifyingavirtualfilename,suchasqrc:/main.qmlin
theprecedingcode.YoucanexpandtheResourcessectionofthe
Projecttreefurthertoseeallfilesaddedtotheresourcefile.
Inthesampleproject,qml.qrcreferencesaQMLfilenamedmain.qml.If
youdon'tseeitintheprojecttree,expandResources,qml.qrc,andthen/
sections.Themain.qmlfileisthetop-levelQMLfilethatisloadedinto
theengine.Let'stakealookatit:
importQtQuick2.9
importQtQuick.Window2.2
Window{
visible:true
width:640
height:480
title:qsTr("HelloWorld")
}
Thisfiledeclareswhatobjectsshouldbecreatedatthestartofthe
application.AsitusessomeQMLtypesprovidedbyQt,itcontains
twoimportdirectivesatthetopofthefile.Eachimportdirective
containsthenameandtheversionoftheimportedmodule.Inthis
example,importQtQuick.Window2.2enablesustousetheWindowQMLtype
providedbythismodule.
Therestofthefileisthedeclarationoftheobjectstheengineshould
create.TheWindow{...}constructiontellsQMLtocreateanew
objectoftheWindowtype.Thecodewithinthissectionassignsvalues
topropertiesofthisobject.Weexplicitlyassignaconstantto
thevisible,width,andheightpropertiesofthewindowobject.TheqsTr()
functionisthetranslationfunction,justliketr()inC++code.It
returnsthepassedstringwithoutchangebydefault.Thetitle
propertywillcontaintheresultofevaluatingthepassedexpression.
Timeforaction–EditingQML
Let'saddsomecontenttoourwindow.Editthemain.qmlfilewiththe
followingcode:
importQtQuick2.9
importQtQuick.Window2.2
importQtQuick.Controls2.2
Window{
visible:true
width:640
height:480
title:qsTr("HelloWorld")
TextField{
text:"Editme"
anchors{
top:parent.top
left:parent.left
}
}
Label{
text:"Helloworld"
anchors{
bottom:parent.bottom
left:parent.left
}
}
}
Whenyouruntheproject,youwillseeatextfieldandalabelinthe
window:
Whatjusthappened?
First,weaddedanimportstatementtomaketheQtQuick.Controls
moduleavailableinthecurrentscope.Ifyou'renotsurewhich
versiontouse,invokeQtCreator'scodecompletionandusethe
mostrecentversion.Duetothenewimport,wecannowuse
theTextFieldandLabelQMLtypesinourQMLfile.
Next,wedeclaredtwochildrenofthetop-levelWindowobject.QML
objectsformaparent-childrelationship,muchlikeQObjectinC++.
However,youdon'tneedtoexplicitlyassignparentstoitems.
Instead,youdeclaretheobjectwithinthedeclarationofitsparent,
andQMLwillautomaticallyensurethatrelationship.Inour
example,theTextField{...}parttellsQMLtocreateanewQML
objectoftheTextFieldtype.
SincethisdeclarationlieswithintheWindow{...}declaration,the
TextFieldobjectwillhavetheWindowobjectasitsparent.Thesame
appliestotheLabelobject.Youcancreatemultiplelevelsofnesting
inasinglefile,ifneeded.Youcanusetheparentpropertytoaccess
theparentitemofthecurrentitem.
Afterdeclaringanewobject,weassignvaluestoitsproperties
withinitsdeclaration.Thetextpropertyisself-explanatory—it
containsthetextdisplayedintheUI.NotethattheTextFieldobject
allowstheusertoeditthetext.WhenthetextiseditedintheUI,
thetextpropertyoftheobjectwillreflectthenewvalue.
Finally,weassignvaluetotheanchorspropertygrouptoposition
theitemsaswelike.Weputthetextfieldinthetop-leftcornerof
thewindowandputthelabelinthebottom-leftcorner.Thisstep
requiresamorethoroughexplanation.
Propertygroups
Beforewediscussanchors,let'stalkaboutpropertygroupsin
general.ThisisanewconceptintroducedinQML.Propertygroups
areusedwhentherearemultiplepropertieswithasimilarpurpose.
Forexample,theLabeltypehasanumberofpropertiesrelatedtothe
font.Theycanbeimplementedasseparateproperties;considerthe
followingexample:
Label{
//thiscodedoesnotwork
fontFamily:"Helvetica"
fontSize:12
fontItalic:true
}
However,suchrepetitivecodeishardtoread.Luckily,font
propertiesareimplementedasapropertygroup,soyoucanset
themusingthegroupnotationsyntax:
Label{
font{
family:"Helvetica"
pointSize:12
italic:true
}
}
Thiscodeismuchcleaner!Notethatthereisnocoloncharacter
afterfont,soyoucantellthatthisisapropertygroupassignment.
Inaddition,ifyouonlyneedtosetonesubpropertyofthegroup,
youcanusethedotnotationsyntax:
Label{
font.pointSize:12
}
Thedotnotationisalsousedtorefertosubpropertiesinthe
documentation.Notethatyoushouldprefergroupnotationifyou
needtosetmorethanonesubproperty.
That'sallyouneedtoknowaboutpropertygroups.Besidesfont,you
canfindmanyotherpropertygroupsinsomeQMLtypes,for
example,border,easing,andanchors.
Anchors
Anchorsallowyoutomanageitemgeometrybyattachingcertain
pointsofsomeobjectstopointsofanotherobject.Thesepointsare
calledanchorlines.Thefollowingdiagramshowstheanchorlines
thatareavailableforeachQtQuickitem:
Youcanestablishbindingsbetweenanchorlinestomanagerelative
positioningofitems.Foreachanchorline,thereisapropertythat
returnsthecurrentcoordinateofthatanchorline.For
example,theleftpropertyreturnsthexcoordinateoftheleftborder
oftheitem,andthetoppropertyreturnstheycoordinateofitstop
border.Next,eachobjectcontainstheanchorspropertygroupthat
allowsyoutosetcoordinatesoftheanchorlineforthatitem.For
example,theanchors.leftpropertycanbeusedtorequesttheposition
oftheleftborderoftheobject.Youcanusethesetwokindsof
propertiestogethertospecifyrelativepositionsofobjects:
anchors.top:otherObject.bottom
Thiscodedeclaresthatthetopanchorlineoftheobjectmustbe
boundtothebottomanchorlineoftheotherobject.It'salso
possibletospecifyamarginforsuchbindingthroughproperties,
suchasanchors.topMargin.
Theanchors.fillpropertyistheshortcutforbindingthetop,bottom,left,
andrightanchorstothespecifiedobject'srespectiveanchorlines.As
aresult,theitemwillhavethesamegeometryastheotherobject.
Thefollowingcodesnippetisoftenusedtoexpandtheitemtothe
wholeareaofitsparent:
anchors.fill:parent
Timeforaction–Positioning
itemsrelativetoeachother
Inourpreviousexample,weusedthefollowingcodetopositionthe
label:
anchors{
bottom:parent.bottom
left:parent.left
}
Youshouldbeabletounderstandthiscodebynow.Theparent
propertyreturnsthereferencetotheparentQMLobject.Inour
case,it'sthewindow.Theparent.bottomexpressionreturnsthey
coordinateoftheparent'sbottomanchorline.Byassigningthis
expressiontotheanchors.bottomproperty,weensurethatthebottom
anchorlineofthelabelstaysinthesamepositionasthebottom
anchorlineofthewindow.Thexcoordinateisrestrictedinasimilar
way.
Now,let'sseewhetherwecanpositionthelabeljustbelowthetext
field.Inordertodothat,weneedtobindtheanchors.toppropertyof
thelabeltothebottomanchorlineofthetextfield.However,we
havenowaytoaccessthetextfieldfromwithinthelabelyet.We
canfixthisbydefiningtheidpropertyofthetextfield:
TextField{
id:textField
text:"Editme"
anchors{
top:parent.top
left:parent.left
}
}
Label{
text:"Helloworld"
anchors{
top:textField.bottom
topMargin:20
left:parent.left
}
}
SettinganIDissimilartoassigningtheobjecttoavariable.Wecan
nowusethetextFieldvariabletorefertoourTextFieldobject.Thelabel
isnowpositioned20pixelsbelowthetextfield.
QMLtypes,components,and
documents
QMLintroducessomenewconceptsthatyoushouldbefamiliar
with.AQMLtypeisaconceptsimilartoC++class.Anyvalueor
objectinQMLshouldhavesometypeandshouldbeexposedto
JavaScriptcodeinacertainway.TherearetwomajorkindsofQML
types:
Basictypesaretypesthatholdaconcretevalueanddonot
refertoanyotherobjects,forexample,stringorpoint
Objecttypesaretypesthatcanbeusedtocreateobjects
withcertainfunctionalityandconsistentinterface
BasicQMLtypesaresimilartoC++primitivetypesanddata
structures,suchasQPoint.Objecttypesareclosertowidgetclasses,
suchasQLineEdit,buttheyarenotnecessarilytiedtoGUI.
TherearenumerousQMLtypesprovidedbyQt.We'vealreadyused
theWindow,TextField,andLabeltypesinourpreviousexamples.Youcan
alsocreateyourowncustomQMLtypeswithuniquefunctionality
andbehavior.ThesimplestwaytocreateaQMLtypeistoadda
new.qmlfilewithacapitalizednametotheproject.Thebasefile
namedefinesthenameofthecreatedQMLtype.Forexample,
theMyTextField.qmlfilewilldeclareanewMyTextFieldQMLtype.
AnycompleteandvalidQMLcodeiscalledadocument.Anyvalid
QMLfilecontainsadocument.It'salsopossibletoloaddocuments
fromanysource(forexample,overthenetwork).Acomponentis
adocumentloadedintotheQMLengine.
Howdoesitwork?
QtQuickinfrastructurehidesmostoftheimplementationdetails
fromthedeveloperandallowsyoutokeepyourapplicationcode
clean.Nevertheless,it'salwaysimportanttounderstandwhat's
goingon.
TheQMLengineisaC++classthatunderstandsQMLcodeand
executestherequiredactionstomakeitwork.Inparticular,the
QMLengineisresponsibleforcreatingobjectsaccordingtothe
requestedhierarchy,assigningvaluestoproperties,andexecuting
eventhandlersinresponsetoevents.
WhileQMLlanguageitselfisquitefarfromJavaScript,itallowsyou
touseanyJavaScriptexpressionsandcodeblocksforcalculating
valuesandhandlingevents.ThismeansthattheQMLenginemust
becapableofexecutingJavaScript.Underthehood,the
implementationusesaveryfastJavaScriptengine,soyoushouldn't
usuallyworryabouttheperformanceofyourJavaScriptcode.
TheJavaScriptcodeshouldbeabletointeractwithQMLobjects,so
everyQMLobjectisexposedasaJavaScriptobjectwith
correspondingpropertiesandmethods.Thisintegrationusesthe
samemechanismthatwelearnedinChapter10,Scripting.InC++
code,youhavesomecontrolovertheobjectsembeddedintothe
QMLengineandcanevencreatenewobjects.Wewillgetbackto
thistopiclaterinthechapter.
WhileQMLisageneralpurposelanguage,QtQuickisaQML-based
modulethatfocusesonuserinterfaces.Itprovidesatwo-
dimensionalhardwareacceleratedcanvasthatcontainsahierarchy
ofinterconnecteditems.UnlikeQtWidgets,QtQuickwasdesigned
tosupportvisualeffectsandanimationsefficiently,soyoucanuse
itspowerswithoutsignificantperformancedegradation.
QtQuickviewsarenotbasedonawebbrowserengine.Browserstendtobequiteheavy,
especiallyformobiledevices.However,youcanuseawebengineexplicitlywhenyouneed
itbyaddingtheWebVieworWebEngineobjecttoyourQMLfiles.
Timeforaction–Property
binding
QMLismuchmorepowerfulthansimpleJSON.Insteadof
specifyinganexplicitvalueforaproperty,youcanuseanarbitrary
JavaScriptexpressionthatwillbeautomaticallyevaluatedand
assignedtotheproperty.Forexample,thefollowingcodewill
display"ab"inthelabel:
Label{
text:"a"+"b"
//...
}
Youcanalsorefertopropertiesoftheotherobjectsinthefile.Aswe
sawearlier,youcanusethetextEditvariabletosetrelativeposition
ofthelabel.Thisisoneexampleofapropertybinding.Ifthevalue
ofthetextField.bottomexpressionchangesforsomereason,the
anchors.toppropertyofthelabelwillbeautomaticallyupdatedwith
thenewvalue.QMLallowsyoutousethesamemechanismfor
everyproperty.Tomaketheeffectmoreobvious,let'sassignan
expressiontothelabel'stextproperty:
Label{
text:"Yourinput:"+textField.text
//...
}
Nowthelabel'stextwillbechangedaccordingtothis
expression.Whenyouchangethetextintheinputfield,thetextof
thelabelwillbeautomaticallyupdated!:
Thepropertybindingdiffersfromaregularvalueassignmentand
bindsthevalueofthepropertytothevalueofthesupplied
JavaScriptexpression.Whenevertheexpression'svaluechanges,
thepropertywillreflectthatchangeinitsownvalue.Notethatthe
orderofstatementsinaQMLdocumentdoesnotmatterasyou
declarerelationsbetweenproperties.
Thisexampleshowsoneoftheadvantagesofthedeclarative
approach.Wedidn'thavetoconnectsignalsorexplicitlydetermine
whenthetextshouldbechanged.Wejustdeclaredthatthetext
shouldbeinfluencedbytheinputfield,andtheQMLenginewill
enforcethatrelationautomatically.
Iftheexpressioniscomplex,youcanreplaceitwithamultiline
blockoftextthatworksasafunction:
text:{
varx=textField.text;
return"("+x+")";
}
YoucanalsodeclareanduseanamedJavaScriptfunctionwithin
anyQMLobjectdeclaration:
Label{
functioncalculateText(){
varx=textField.text;
return"("+x+")";
}
text:calculateText()
//...
//...
}
Alimitationofautomatic
propertyupdates
QMLdoesitsbesttodeterminewhenthefunctionvaluemay
change,butitisnotomnipotent.Forourlastfunction,itcaneasily
determinethatthefunctionresultdependsonthevalueof
thetextField.textproperty,soitwillre-evaluatethebindingifthat
valuechanges.However,insomecases,itcan'tknowthatafunction
mayreturnadifferentvaluethenexttimeitiscalled,andinsuch
situations,thestatementwillnotbere-evaluated.Considerthe
followingpropertybinding:
Label{
functioncolorByTime(){
vard=newDate();
varseconds=d.getSeconds();
if(seconds<15)return"red";
if(seconds<30)return"green";
if(seconds<45)return"blue";
return"purple";
}
color:colorByTime()
//...
}
Thecolorwillbesetatthestartoftheapplication,butitwillnot
workproperly.QMLwillonlycallthecolorByTime()functiononce
whentheobjectisinitialized,anditwillnevercallitagain.Thisis
becauseithasnowayofknowinghowoftenthisfunctionmustbe
called.WewillseehowtoovercomethisinChapter12,Customization
inQtQuick.
OverviewofQMLtypes
providedbyQt
BeforewecontinuetoworkonourQMLapplication,let'sseewhat
thebuilt-inlibrariesarecapableof.Thiswillallowustopickthe
rightmodulesforthetask.QtprovidesalotofusefulQMLtypes.In
thissection,wewillprovideanoverviewofthemostusefulmodules
availableinQt5.9.
Thefollowingmodulesareimportantforbuildinguserinterfaces:
TheQtQuickbasemoduleprovidesfunctionalityrelatedto
drawing,eventhandling,positioningofelements,
transformations,andmanyotherusefultypes
QtQuick.Controlsprovidesbasiccontrolsforuserinterfaces,
suchasbuttonsandinputfields
QtQuick.Dialogscontainsfiledialogs,colordialogs,andmessage
boxes
QtQuick.Extrasprovidesadditionalcontrols,suchasdials,
tumblers,andgauges
QtQuick.Windowenableswindowmanagement
QtQuick.Layoutsprovidelayoutsforautomaticpositioningof
objectsonscreen
UIComponentsprovidestabwidget,progressbar,andswitch
types
QtWebViewallowsyoutoaddwebcontenttotheapplication
QtWebEngineprovidesmoresophisticatedwebbrowser
functionality
Ifyouwanttoimplementrichgraphics,thefollowingmodulesmay
beofhelp:
QtCanvas3Dprovidesacanvasfor3Drendering
Qt3Dmodulesprovideaccesstoreal-timesimulationsystems
supporting2Dand3Drendering
QtChartsallowsyoutocreatesophisticatedcharts
QtDataVisualizationcanbeusedtobuild3Dvisualizationsof
datasets
QtQuick.Particlesallowsyoutoaddparticleeffects
QtGraphicalEffectscanapplygraphicaleffects(suchasbluror
shadow)tootherQtQuickobjects
Qtprovidesalotoffunctionalitycommonlyrequiredonmobile
devices:
QtBluetoothsupportsbasiccommunicationwithotherdevices
overBluetooth
QtLocationallowsyoutodisplaymapsandfindroutes
QtPositioningprovidesinformationaboutthecurrentlocation
QtNfcallowsyoutoutilizeNFChardware
QtPurchasingimplementsin-apppurchases
QtSensorsprovidesaccesstoon-boardsensors,suchas
accelerometerorgyroscope
QtQuick.VirtualKeyboardprovidesanimplementationofan
onscreenkeyboard
Finally,therearetwomodulesprovidingmultimediacapabilities:
QtMultimediaprovidesaccesstoaudioandvideoplayback,
audiorecording,camera,andradio
QtAudioEngineimplements3Dpositionalaudioplayback
TherearemanymoreQMLmodulesthatwedidn'tmentionhere.Youcanfindthefulllist
ontheAllQMLModulesdocumentationpage.Notethatsomeofthemodulesarenot
providedunderLGPLlicense.
QtQuickDesigner
WecanuseQMLtoeasilycreateahierarchyofobjects.Ifweneeda
fewinputboxesorbuttons,wecanjustaddsomeblockstothecode,
justlikeweaddedtheTextFieldandLabelcomponentsintheprevious
example,andourchangeswillappearinthewindow.However,
whendealingwithcomplexforms,it'ssometimeshardtoposition
theobjectsproperly.Insteadoftryingdifferentanchorsand
relaunchingtheapplication,youcanusethevisualformeditorto
seethechangesasyoumakethem.
Timeforaction–Addinga
formtotheproject
Locatetheqml.qrcfileinQtCreator'sprojecttreeandinvoketheAdd
New...optioninitscontextmenu.FromQtsection,select
theQtQuickUIFiletemplate.InputCalculatorintheComponent
namefield.TheComponentformnamefieldwillbeautomatically
settoCalculatorForm.Finishthewizard.
Twonewfileswillappearinourproject.TheCalculatorForm.ui.qmlfileis
theformfilethatcanbeeditedintheformeditor.TheCalculator.qml
fileisaregularQMLfilethatcanbeeditedmanuallytoimplement
thebehavioroftheform.EachofthesefilesintroducesanewQML
type.TheCalculatorFormQMLtypeisimmediatelyusedin
thegeneratedCalculator.qmlfile:
importQtQuick2.4
CalculatorForm{
}
Next,weneedtoeditthemain.qmlfiletoaddaCalculatorobjecttothe
window:
importQtQuick2.9
importQtQuick.Window2.2
importQtQuick.Controls2.2
Window{
visible:true
width:640
height:480
title:qsTr("Calculator")
Calculator{
anchors.fill:parent
}
}
QMLcomponentsaresimilartoC++classesinsomeway.AQML
componentencapsulatesanobjecttreesothatyoucanuseit
withoutknowingabouttheexactcontentofthecomponent.When
theapplicationisstarted,themain.qmlfilewillbeloadedintothe
engine,sotheWindowandCalculatorobjectswillbecreated.The
Calculatorobject,inturn,willcontainaCalculatorFormobject.
TheCalculatorFormobjectwillcontaintheitemsthatweaddlaterinthe
formeditor.
Formeditorfiles
WhenweworkedwithQtWidgetsformeditor,youmayhavenoted
thatawidgetformisanXMLfilethatisconvertedtoaC++class
duringcompilation.ThisdoesnotapplytoQtQuickDesigner.In
fact,thefilesproducedbythisformeditorarecompletelyvalid
QMLfilesthataredirectlyincludedintheproject.However,the
formeditorfileshaveaspecialextension(.ui.qml),andthereare
someartificialrestrictionsthatprotectyoufromdoingbadthings.
Theui.qmlfilesshouldonlycontaincontentthatisvisibleintheform
editor.Youdonotneedtoeditthesefilesbyhand.It'snotpossible
tocallfunctionsorexecuteJavaScriptcodefromthesefiles.
Instead,youshouldimplementanylogicinaseparateQMLfilethat
usestheformasacomponent.
Ifyou'recuriousaboutthecontentofaui.qmlfile,youcanclickontheTextEditortabthat
ispositionedontherightborderoftheformeditor'scentralarea.
Formeditorinterface
Whenyouopena.ui.qmlfile,QtCreatorgoestotheDesignmode
andopenstheQtQuickDesignerinterface:
We'vehighlightedthefollowingimportantpartsoftheinterface:
Themainarea(1)containsvisualizationofthedocument's
content.YoucanclickontheTextEditortabattheright
borderofthemainareatoviewandedittheQMLcodeof
theformwithoutexitingtheformeditor.Thebottompartof
themainareadisplayslistofstatesofthecomponent.
TheLibrarypane(2)showstheavailableQMLobjecttypes
andallowsyoutocreatenewobjectsbydraggingthemtothe
navigatorortothemainarea.TheImportstabcontainsa
listofavailableQMLmodulesandallowsyoutoexporta
moduleandaccessmoreQMLtypes.
TheNavigatorpane(3)displaysthehierarchyoftheexisting
objectsandtheirnames.Thebuttonstotherightofthe
namesallowyoutoexportanobjectaspublicpropertyand
toggleitsvisibilityintheformeditor.
TheConnectionspane(4)providesabilitytoconnect
signals,changepropertybindings,andmanagepublic
propertiesoftheform.
ThePropertiespane(5)allowsyoutoviewandedit
propertiesoftheselectedobject.
Wewillnowusetheformeditortocreateasimplecalculator
application.Ourformwillcontaintwoinputboxesforoperands,
tworadiobuttonsforselectingtheoperation,alabeltodisplaythe
result,andabuttontoreseteverythingtotheoriginalstate.
Timeforaction–Addingan
import
Thedefaultobjectpalettecontainsaveryminimalsetoftypes
providedbytheQtQuickmodule.Toaccessarichersetofcontrols,we
needtoaddanimportdirectivetoourdocument.Todothis,locate
theLibrarypaneinthetop-leftcornerofthewindowandgotoits
Importstab.Next,clickonAddImportandselectQtQuick.Controls
2.2inthedrop-downlist.Theselectedimportwillappearinthetab.
Youcanclickonthe×buttontotheleftoftheimporttoremoveit.
Notethatyoucannotremovethedefaultimport.
AddingtheimportusingtheformeditorwillresultinaddingtheimportQtQuick.Controls
2.2directivetothe.ui.qmlfile.YoucanswitchthemainareatotheTextEditormodetosee
thischange.
NowyoucanswitchbacktotheQMLTypestaboftheLibrarypane.
Thepalettewillcontaincontrolsprovidedbytheimportedmodule.
Timeforaction–Addingitems
totheform
LocatetheTextFieldtypeintheQtQuick-Controls2sectionofthe
librarypaneanddragittothemainarea.Anewtextfieldwillbe
created.WewillalsoneedtheRadioButton,Label,andButton
typesfromthesamesection.Dragthemtotheformandarrange
themasshown:
Next,youneedtoselecteachelementandedititsproperties.Click
onthefirsttextfieldinthemainareaorinthenavigator.Theblue
framearoundtheobjectinthemainareawillindicatethatitis
selected.Nowyoucanusethepropertyeditortoviewandedit
propertiesoftheselectedelement.First,wewanttosettheid
propertythatwillbeusedtorefertotheobjectinthecode.Settheid
propertyofthetexteditstoargument1andargument2.LocatetheText
propertyundertheTextFieldtabinthepropertyeditor.Setitto0
forbothtextfields.Thechangedtextwillbeimmediatelydisplayed
inthemainarea.
SetidoftheradiobuttonstooperationAddandoperationMultiply.Settheir
textto+and×.SetthecheckedpropertyoftheoperationAddbuttontotrue
bytogglingthecorrespondingcheckboxinthepropertyeditor.
Thefirstlabelwillbeusedtostaticallydisplaythe=sign.Setitsidto
equalSignandtextto=.Thesecondlabelwillactuallydisplaythe
result.Setitsidtoresult.Wewilltakecareofthetextpropertylater.
Thebuttonwillresetthecalculatortotheoriginalstate.Setitsidto
resetandtexttoReset.
Youcanruntheapplicationnow.Youwillseethatthecontrolsare
showninthewindow,buttheyarenotrepositionedinrespecttothe
windowsize.Theyalwaysstayinthesamepositions.Ifyoucheck
outthetextcontentofCalculatorForm.ui.qml,youwillseethattheform
editorsetsthexandypropertiesofeachelement.Tomakeamore
responsiveform,weneedtoutilizetheanchorspropertyinstead.
Timeforaction–Editing
anchors
Let'sseehowwecaneditanchorsintheformeditorandseethe
resultonthefly.Selecttheargument1textfieldandswitchtothe
LayouttabinthemiddlepartofthePropertiespane.Thetab
containsAnchorstext,followedbyasetofbuttonsforallanchor
linesofthisitem.Youcanmouseoverthebuttonstoseetheir
tooltips.Clickonthefirstbutton,
Anchoritemtothetop.Anewsetofcontrolswillappearbelowthe
button,allowingyoutoconfigurethisanchor.
First,youcanselectthetargetobject,thatis,theobjectcontaining
theanchorlinethatwillbeusedasthereference.Next,youcan
selectthemarginbetweenthereferenceanchorlineandtheanchor
lineofthecurrentobject.Totherightofthemargin,thereare
buttonsthatallowyoutochoosewhichanchorlineofthetargetto
useasthereference.Forexample,ifyouchoosethebottomline,our
textfieldwillretainitspositionrelativetothebottomborderofthe
form.
Anchorthetoplineofthetextfieldtothetoplineoftheparentand
setMarginto20.Next,anchorthehorizontalcenterlinetoparent
withMargin0.Thepropertyeditorshouldlooklikethis:
YoucanalsoverifytheQMLrepresentationofthesesettings:
TextField{
id:a
text:qsTr("0")
anchors.horizontalCenter:parent.horizontalCenter
anchors.top:parent.top
anchors.topMargin:20
}
Ifyoudragthetextfieldaroundusingthemouseinsteadofsettingtheanchors,theform
editorwillsetthexandypropertiestopositiontheelementaccordingtoyouractions.If
youeditanchorsoftheitemafterward,thexandypropertiesmayremainset,buttheir
effectwillbeoverriddenbytheanchoreffects.
Let'srepeatthisprocessfortheoperationAddradiobutton.First,we
needtoadjustitshorizontalpositionrelativetothehorizontal
centeroftheform.Selecttheradiobutton,clickonthe Anchor
itemtotherightbutton,leaveparentasthetarget,andclickonthe
Anchortothehorizontalcenterofthetargetbuttontotheright
ofthemargininput.Setmarginto10.Thiswillallowustoposition
thesecondradiobutton10pointstotherightofthehorizontal
center,andthespacebetweentheradiobuttonswillbe20.
Now,whataboutthetopanchor?Wecanattachittotheparentand
justsetthemarginthatwilllooknice.However,ultimately,whatwe
wantisaspecificverticalmarginbetweenthefirsttextfieldandthe
firstradiobutton.Wecandothiseasily.
EnablethetopanchorfortheoperationAddradiobutton,selectargument1
intheTargetdrop-downlist,clickonthe Anchortothebottomof
thetargetbuttontotherightofthemarginfield,andinput20inthe
marginfield.Nowtheradiobuttonisanchoredtothetextfield
aboveit.Evenifwechangetheheightofthetextfield,thevertical
marginbetweentheelementswillstayintact.Youcanrunthe
applicationandverifythattheargument1andoperationAddelementsnow
respondtowindowsizechanges.
Now,allweneedistorepeatthisprocessfortherestoftheobjects.
However,thisisquiteatedioustask.Itwillgetevenmore
inconvenientinalargerform.Makingchangestosuchformswill
alsobecumbersome.Forexample,tochangetheorderoffields,you
willneedtocarefullyedittheanchorsofinvolvedobjects.While
anchorsaregoodinsimplecases,it'sbettertouseamore
automatedapproachforlargeforms.Luckily,QtQuickprovides
layoutsforthispurpose.
Timeforaction–Applying
layoutstotheitems
Beforeweapplylayoutstoobjects,removetheanchorswehad
created.Todothis,selecteachelementandclickonthebuttons
underAnchorstexttouncheckthem.Theanchorpropertiesbelow
thebuttonswilldisappear.Thelayoutwillnowbeabletoposition
theobjects.
First,importtheQtQuick.Layouts1.3moduleintotheform,likewedid
earlierforQtQuick.Controls.LocatetheQtQuick-Layoutssectionin
thepaletteandexaminetheavailablelayouts:
ColumnLayoutwillarrangeitschildrenvertically
RowLayoutwillarrangeitschildrenhorizontally
GridLayoutwillarrangeitschildrenverticallyand
horizontallyinagrid
StackLayoutwilldisplayonlyoneofitschildrenandhide
therestofthem
Layoutsaresensitivetothehierarchyoftheobjects.Let'suse
Navigatorinsteadofthemainareatomanageouritems.Thiswill
allowustoseetheparent-childrelationshipsbetweenitemsmore
clearly.First,dragaRowLayoutanddropitovertherootitemin
theNavigator.AnewrowLayoutobjectwillbeaddedasachildofthe
rootobject.Next,dragtheoperationAddandoperationMultiplyobjectsin
theNavigatoranddropthemtotherowLayout.Theradiobuttonsare
nowchildrenoftherowlayout,andtheyareautomatically
positionednexttoeachother.
Now,dragaColumnLayouttotherootobject.Selectallother
childrenoftherootobject,includingrowLayout,intheNavigator,and
dragthemtothecolumnLayoutobject.Iftheitemsendupinwrong
order,usetheMoveupandMovedownbuttonsatthetoppartof
theNavigatortoarrangetheitemsproperly.Youshouldgetthe
followinghierarchy:
ThecolumnLayoutobjectwillautomaticallypositionitschildren,but
howtopositiontheobjectitself?Weshoulduseanchorstodothat.
SelectcolumnLayout,switchtotheLayouttabinthepropertyeditorand
clickonthe Fillparentitembutton.Thiswillautomatically
create4anchorbindingsandexpandcolumnLayouttofilltheform.
Theitemsarenowpositionedautomatically,buttheyareboundto
theleftborderofthewindow.Let'salignthemtothemiddle.Select
thefirsttextfieldandswitchtotheLayouttab.Astheobjectisnow
inalayout,theanchorsettingsarereplacedwithsettingsthelayout
understands.TheAlignmentpropertydefineshowtheitemis
positionedwithintheavailablespace.SelectAlignHCenterinthefirst
drop-downlist.Repeattheprocessforeachdirectchildof
columnLayout.
Youcannowruntheapplicationandseehowitreactstochanging
windowsize:
Theformisready.Let'simplementthecalculationsnow.
Timeforaction–Assigningan
expressiontotheproperty
Asyoualreadysaw,assigningconstanttexttoalabeliseasy.
However,youcanalsoassignadynamicexpressiontoanyproperty
intheformeditor.Todothat,selecttheresultlabelandmouseover
thecircleintheleftpartofTextpropertyinputfield.Whenthe
circleturnsintoanarrow,clickonitandselectSetBindinginthe
menu.Inputargument1.text+argument2.textinthebindingeditorand
confirmthechange.
Ifyouruntheapplicationnow,youwillseethattheresultlabelwill
alwaysdisplaytheconcatenationofthestringstheuserinputsin
thefields.That'sbecausetheargument1.textandargument2.textproperties
havethestringtype,sothe+operationperformsconcatenation.
Thisfeatureisveryusefulifyouneedtoapplysimplebindings.
However,itisnotsufficientinourcase,asweneedtoconvert
stringstonumbersandselectwhicharithmeticoperationtheuser
requested.Usingfunctionsintheformeditorisnotallowed,sowe
cannotimplementthiscomplexlogicrighthere.Weneedtodoitin
theCalculator.qmlfile.Thisrestrictionwillhelpusseparatetheview
fromthelogicbehindit.
Timeforaction–Exposing
itemsasproperties
Childrenofacomponentarenotavailablefromoutsideofitby
default.ThismeansthatCalculator.qmlcannotaccessinputfieldsor
radiobuttonsofourform.Toimplementthelogicofthecalculator,
weneedtoaccesstheseobjects,solet'sexposethemaspublic
properties.Selecttheargument1textfieldintheNavigatorandclickon
the Toggleswhetherthisitemisexportedasanaliaspropertyof
therootitembuttontotherightoftheobjectID.Afteryouclickon
thebutton,itsiconwillchangetoindicatethattheitemisexported.
Nowwecanusetheargument1publicpropertyinCalculator.qmltoaccess
theinputfieldobject.
Enablepublicpropertiesfortheargument1,argument2,operationAdd,
operationMultiply,andresultobjects.Therestoftheobjectswillremain
hiddenasimplementationdetailsoftheform.
NowgototheCalculator.qmlfileandusetheexposedpropertiesto
implementthecalculatorlogic:
CalculatorForm{
result.text:{
varvalue1=parseFloat(argument1.text);
varvalue2=parseFloat(argument2.text);
if(operationMultiply.checked){
returnvalue1*value2;
}else{
returnvalue1+value2;
}
}
}
Whatjusthappened?
Sinceweexportedobjectsasproperties,wecanaccessthembyID
fromoutsideoftheform.Inthiscode,webindthetextpropertyof
theresultobjecttothereturnvalueofthecodeblockthatisenclosed
inbraces.Weuseargument1.textandargument2.texttoaccessthecurrent
textoftheinputfields.WealsouseoperationMultiply.checkedtosee
whethertheusercheckedtheoperationMultiplyradiobutton.Therest
isjuststraightforwardJavaScriptcode.
Runtheapplicationandseehowtheresultlabelautomatically
displaystheresultwhentheuserinteractswiththeform.
Timeforaction–Creatingan
eventhandler
Let'simplementthelastbitoffunctionality.Whentheuserclickson
theResetbutton,weshouldchangetheform'svalues.Gobackto
theformeditorandright-clickontheresetbuttonintheNavigator
orinthemainarea.SelectAddNewSignalHandler.QtCreatorwill
navigatetothecorrespondingimplementationfile(Calculator.qml)
anddisplaytheImplementSignalHandlerdialog.Selecttheclicked
signalinthedrop-downlistandclickontheOKbuttontoconfirm
theoperation.Thisoperationwilldotwothings:
Theresetbuttonwillbeautomaticallyexportedasapublic
property,justlikewediditmanuallyfortheothercontrols
QtCreatorwillcreateaboilerplateforthenewsignal
handlerintheCalculator.qmlfile
Let'saddourimplementationtotheautomaticallygeneratedblock:
reset.onClicked:{
argument1.text="0";
argument2.text="0";
operationAdd.checked=true;
}
Whenthebuttonisclickedon,thiscodewillbeexecuted.Thetext
fieldswillbesetto0,andtheoperationAddradiobuttonwillbe
checked.TheoperationMultiplyradiobuttonwillbeunchecked
automatically.
Ourcalculatorfullyworksnow!Weuseddeclarativeapproachto
implementanicelylookingandresponsiveapplication.
QtQuickandC++
WhileQMLhasalotofbuilt-infunctionalityavailable,itwillalmost
neverbeenough.Whenyou'redevelopingarealapplication,it
alwaysneedssomeuniquefunctionalitythatisnotavailablein
QMLmodulesprovidedbyQt.TheC++Qtclassesaremuchmore
powerful,andthird-partyC++librariesarealsoalwaysanoption.
However,theC++worldisseparatedfromourQMLapplicationby
therestrictionsofQMLengine.Let'sbreakthatboundaryright
away.
AccessingC++objectsfrom
QML
Let'ssaythatwewanttoperformaheavycalculationinC++and
accessitfromourQMLcalculator.Wewillchoosefactorialforthis
project.
TheQMLengineisreallyfast,soyoucanmostlikelycalculatefactorialsdirectlyin
JavaScriptwithoutperformanceproblems.Wejustuseithereasasimpleexample.
OurgoalistoinjectourC++classintotheQMLengineasa
JavaScriptobjectthatwillbeavailableinourQMLfiles.Wewilldo
thatexactlylikewediditinChapter10,Scripting.Themainfunction
createsaQQmlApplicationEngineobjectthatinheritsQJSEngine,sowehave
accesstotheAPIthatisalreadyfamiliartousfromthatchapter.
Here,we'lljustshowhowwecanapplythisknowledgetoour
applicationwithoutgoingintodetail.
GototheEditmode,right-clickontheprojectintheprojecttree
andselectAddNew.SelecttheC++Classtemplate,input
AdvancedCalculatorastheclassnameandselectQObjectintheBase
Classdrop-downlist.
Declaretheinvokablefactorialfunctioninthegenerated
advancedcalculator.hfile:
Q_INVOKABLEdoublefactorial(intargument);
Wecanimplementthisfunctionusingthefollowingcode:
doubleAdvancedCalculator::factorial(intargument){
if(argument<0){
returnstd::numeric_limits<double>::quiet_NaN();
}
if(argument>180){
returnstd::numeric_limits<double>::infinity();
}
doubler=1.0;
for(inti=2;i<=argument;++i){
r*=i;
}
returnr;
}
Weguardtheimplementationagainsttoolargeinputsbecausedouble
wouldn'tbeabletofittheresultingvaluesanyway.Wealsoreturn
NaNoninvalidinputs.
Next,weneedtocreateaninstanceofthisclassandimportitinto
theQMLengine.Wedothisinthemain():
engine.globalObject().setProperty("advancedCalculator",
engine.newQObject(newAdvancedCalculator));
returnapp.exec();
OurobjectisnowavailableastheadvancedCalculatorglobalvariable.
NowweneedtousethisvariableintheQMLfile.Opentheform
editorandaddthethirdradiobuttontotherowLayoutitem.Setidof
theradiobuttontooperationFactorialandtextto!.Exportthisradio
buttonasapublicpropertysothatwecanaccessitfromthe
outside.Next,let'sadjusttheresult.textpropertybindinginthe
Calculator.qmlfile:
result.text:{
varvalue1=parseFloat(argument1.text);
varvalue2=parseFloat(argument2.text);
if(operationMultiply.checked){
returnvalue1*value2;
}elseif(operationFactorial.checked){
returnadvancedCalculator.factorial(value1);
}else{
returnvalue1+value2;
}
}
IftheoperationFactorialradiobuttonischecked,thiscodewillcall
thefactorial()methodoftheadvancedCalculatorvariableandreturnitas
theresult.Theuserwillseeitastextoftheresultlabel.When
factorialoperationisselected,thesecondtextfieldisunused.We'll
dosomethingaboutthatlaterinthischapter.
FormoreinformationaboutexposingC++APItoJavaScript,refertoChapter10,Scripting.
MostofthetechniquesdescribedthereapplytotheQMLengineaswell.
WeexposedaC++objectasaJavaScriptobjectthatisaccessible
fromtheQMLengine.However,itisnotaQMLobject,soyoucan't
includeitintheQMLobjectshierarchyorapplypropertybindings
topropertiesoftheobjectthatwascreatedthisway.It'spossibleto
createaC++classthatwillworkasafullyfunctionalQMLtype,
leadingtoamorepowerfulintegrationofC++andQML.Wewill
showthatapproachinChapter12,CustomizationinQtQuick.
ThereisanotherwaytoexposeourAdvancedCalculatorclassto
JavaScript.Insteadofaddingittotheglobalobject,wecanregister
itasasingletonobjectintheQMLmodulesystemusing
theqmlRegisterSingletonType()function:
qmlRegisterSingletonType("CalculatorApp",1,0,"AdvancedCalculator",
[](QQmlEngine*engine,QJSEngine*scriptEngine)->QJSValue{
Q_UNUSED(scriptEngine);
returnengine->newQObject(newAdvancedCalculator);
});
QQmlApplicationEngineengine;
WepasstheQMLmodulename,majorandminorversions,andthe
singletonnametothisfunction.Youcanchoosethesevalues
arbitrarily.Thelastargumentisacallbackfunctionthatwillbe
calledwhenthissingletonobjectisaccessedintheJSengineforthe
firsttime.
TheQMLcodealsoneedstobeslightlyadjusted.First,importour
newQMLmoduleintoscope:
importCalculatorApp1.0
Nowyoucanjustaccessthesingletonbyname:
returnAdvancedCalculator.factorial(value1);
Whenthislineisexecutedforthefirsttime,QtwillcallourC++
callbackandcreatethesingletonobject.Forsubsequentcalls,the
sameobjectwillbeused.
AccessingQMLobjectsfrom
C++
ItisalsopossibletocreateQMLobjectsfromC++andaccessthe
existingobjectslivingintheQMLengine(forexample,those
declaredinsomeQMLfile).However,ingeneral,doingthisthingis
badpractice.Ifweassumethemostcommoncase,whichisthatthe
QMLpartofourapplicationdealswithauserinterfaceinQtQuick
forthelogicwritteninC++,thenaccessingQtQuickobjectsfrom
C++breakstheseparationbetweenlogicandthepresentationlayer,
whichisoneofthemajorprinciplesinGUIprogramming.Theuser
interfaceispronetodynamicchanges,relayoutinguptoacomplete
revamp.HeavymodificationsofQMLdocuments,suchasaddingor
removingitemsfromthedesign,willthenhavetobefollowedby
adjustingtheapplicationlogictocopewiththosechanges.In
addition,ifweallowasingleapplicationtohavemultipleuser
interfaces(skins),itmighthappenthatbecausetheyareso
different,itisimpossibletodecideuponasinglesetofcommon
entitieswithhard-codednamesthatcanbefetchedfromC++and
manipulated.Evenifyoumanagedtodothat,suchanapplication
couldcrasheasilyiftheruleswerenotstrictlyfollowedintheQML
part.
Thatsaid,wehavetoadmitthattherearecaseswhenitdoesmake
sensetoaccessQMLobjectsfromC++,andthatiswhywedecided
tofamiliarizeyouwiththewaytodoit.Oneofthesituationswhere
suchanapproachisdesirediswhenQMLservesusasawayto
quicklydefineahierarchyofobjectswithpropertiesofdifferent
objectslinkedthroughmoreorfewercomplexexpressions,allowing
themtoanswertochangestakingplaceinthehierarchy.
TheQQmlApplicationEngineclassprovidesaccesstoitstop-levelQML
objectsthroughtherootObjects()function.AllnestedQMLobjects
formaparent-childhierarchyvisiblefromC++,soyoucanuse
QObject::findChildorQObject::findChildrentoaccessthenestedobjects.The
mostconvenientwaytofindaspecificobjectistosetitsobjectName
property.Forexample,ifwewanttoaccesstheresetbuttonfrom
C++,weneedtosetitsobjectname.
TheformeditordoesnotprovideawaytosetobjectNameforitsitems,
soweneedtousethetexteditortomakethischange:
Button{
id:reset
objectName:"buttonReset"
//...
}
Wecannowaccessthisbuttonfromthemainfunction:
if(engine.rootObjects().count()==1){
QObject*window=engine.rootObjects()[0];
QObject*resetButton=window->findChild<QObject*>("buttonReset");
if(resetButton){
resetButton->setProperty("highlighted",true);
}
}
Inthiscode,wefirstaccessthetop-levelWindowQMLobject.Then,
weusethefindChildmethodtofindtheobjectcorrespondingtoour
resetbutton.ThefindChild()methodrequiresustopassaclass
pointerasthetemplateargument.Withoutknowingwhatclass
actuallyimplementsagiventype,itissafesttosimplypassQObject*
as,onceagain,weknowallQMLobjectsinheritit.Itismore
importantwhatgetspassedasthefunctionargumentvalue—itis
thenameoftheobjectwewantreturned.Notethatitisnottheidof
theobjectbutthevalueoftheobjectNameproperty.Whentheresult
getsassignedtothevariables,weverifywhethertheitemhasbeen
successfullyfoundandifthatisthecase,thegenericQObjectAPIis
usedtosetitshighlightedpropertytotrue.Thispropertywillchange
theappearanceofthebutton.
TheQObject::findChildandQObject::findChildrenfunctionsperformrecursivesearchwith
unlimiteddepth.Whilethey'reeasytouse,thesefunctionsmaybeslowiftheobjecthas
manychildren.Toimproveperformance,youcanturnoffrecursivesearchbypassing
theQt::FindDirectChildrenOnlyflagtothesefunctions.Ifthetargetobjectisnotadirect
child,considercallingQObject::findChildrepeatedlytofindeachintermediateparent.
IfyouneedtocreateanewQMLobject,youcanusetheQQmlComponent
classforthat.ItacceptsaQMLdocumentandallowsyoutocreatea
QMLobjectfromit.Thedocumentisusuallyloadedfromafile,but
youcanevenprovideitdirectlyinC++code:
QQmlComponentcomponent(&engine);
component.setData(
"importQtQuick2.6\n"
"importQtQuick.Controls2.2\n"
"importQtQuick.Window2.2\n"
"Window{Button{text:\"C++button\"}}",QUrl());
QObject*object=component.create();
object->setProperty("visible",true);
Thecomponent.create()functioninstantiatesournewcomponentand
returnsapointertoitasQObject.Infact,anyQMLobjectderives
fromQObject.YoucanuseQtmeta-systemtomanipulatetheobject
withoutneedingtocastittoaconcretetype.Theobject'sproperties
canbeaccessedusingtheproperty()andsetProperty()functions.Inthis
example,wesetthevisiblepropertyoftheWindowQMLobjecttotrue.
Whenourcodeisexecuted,anewwindowwithabuttonwillappear
onscreen.
Youcanalsocalltheobject'smethodsusing
theQMetaObject::invokeMethod()function:
QMetaObject::invokeMethod(object,"showMaximized");
IfyouwanttoembedanewobjectintotheexistingQMLform,you
needtosetvisualparentofthenewobject.Let'ssaythatwewantto
addabuttontothecalculator'sform.First,youneedtoassign
objectNametoitinmain.qml:
Calculator{
anchors.fill:parent
objectName:"calculator"
}
YoucannowaddabuttontothisformfromC++:
QQmlComponentcomponent(&engine);
component.setData(
"importQtQuick2.6\n"
"importQtQuick.Controls2.2\n"
"Button{text:\"C++button2\"}",QUrl());
QObject*object=component.create();
QObject*calculator=window->findChild<QObject*>("calculator");
object->setProperty("parent",QVariant::fromValue(calculator));
Inthiscode,wecreateacomponentandassignthemainformasits
parentproperty.Thiswillmaketheobjectappearinthetop-left
corneroftheform.LikewithanyotherQMLobject,youcanuse
theanchorspropertygrouptochangepositionoftheobject.
Whencreatingcomplexobjects,ittakestimeforthemtoinstantiate
andattimes,itisdesiredtonotblockthecontrolflowfortoolong
bywaitingfortheoperationtocomplete.Insuchcases,you
cancreateanobjectintheQMLengineasynchronouslyusing
theQQmlIncubatorobject.Thisobjectcanbeusedtoschedule
instantiationandcontinuetheflowoftheprogram.Wecanquery
thestateoftheincubatorandwhentheobjectisconstructed,we
willbeabletoaccessit.Thefollowingcodedemonstrateshowtouse
theincubatortoinstantiateanobjectandkeeptheapplication
respondingwhilewaitingfortheoperationtocomplete:
QQmlComponentcomponent(&engine,
QUrl::fromLocalFile("ComplexObject.qml"));
QQmlIncubatorincubator;
component.create(incubator);
while(!incubator.isError()&&!incubator.isReady()){
while(!incubator.isError()&&!incubator.isReady()){
QCoreApplication::processEvents();
}
QObject*object=incubator.isReady()?incubator.object():0;
Bringinglifeintostaticuser
interfaces
Ouruserinterfacehasbeenquitestaticuntilnow.Inthissection,
wewilladdasimpleanimationtoourcalculator.Whentheuser
selectsthefactorialoperation,thesecond(unused)textfieldwill
fadeout.Itwillfadeinwhenanotheroperationisselected.Let'ssee
howQMLallowsustoimplementthat.
Fluiduserinterfaces
Sofar,wehavebeenlookingatgraphicaluserinterfacesasasetof
panelsembeddedoneintoanother.Thisiswellreflectedinthe
worldofdesktoputilityprogramscomposedofwindowsand
subwindowscontainingmostlystaticcontentscatteredthroughout
alargedesktopareawheretheusercanuseamousepointerto
movearoundwindowsoradjusttheirsize.
However,thisdesigndoesn'tcorrespondwellwithmodernuser
interfacesthatoftentrytominimizetheareatheyoccupy(because
ofeitherasmalldisplaysizelikewithembeddedandmobiledevices
ortoavoidobscuringthemaindisplaypanellikeingames),atthe
sametimeprovidingrichcontentwithalotofmovingor
dynamicallyresizingitems.Suchuserinterfacesareoftencalled
"fluid",tosignifythattheyarenotformedasanumberofseparate
differentscreensbutcontaindynamiccontentandlayoutwhereone
screenfluentlytransformsintoanother.TheQtQuickmoduleprovides
aruntimetocreaterichapplicationswithfluiduserinterfaces.
Statesandtransitions
QtQuickintroducesaconceptofstates.AnyQtQuickobjectcan
haveapredefinedsetofstates.Eachstatecorrespondstoacertain
situationintheapplicationlogic.Forexample,wecansaythatour
calculatorapplicationhastwostates:
Whenaddormultiplyoperationsareselected,theuserhas
toinputtwooperands
Whenfactorialoperationisselected,theuserhastoinput
onlyoneoperand
Statesareidentifiedbystringnames.Implicitly,anyobjecthasthe
basestatewithanemptyname.Todeclareanewstate,youneedto
specifythestatenameandasetofpropertyvaluesthataredifferent
inthatstate,comparedtothebasestate.
EachQtQuickobjectalsohasthestateproperty.Whenyouassigna
statenametothisproperty,theobjectgoestothespecifiedstate.
Thishappensimmediatelybydefault,butit'spossibletodefine
transitionsfortheobjectandperformsomevisualeffectswhen
changingstates.
Let'sseehowwecanutilizestatesandtransitionsinourproject.
Timeforaction–Addingstates
totheform
OpentheCalculatorForm.ui.qmlfileintheformeditor.Thebottompart
ofthemainareacontainsthestateseditor.Thebasestateitemis
alwayspresentontheleft.ClickontheAddanewstatebuttonon
therightofthestateseditor.Anewstatewillappearintheeditor.It
containsatextfieldthatyoucanusetosetthestate'sname.Setthe
nametosingle_argument.
Onlyoneofthestatescanbeselectedatatime.Whenacustom
stateisselected,anychangesintheformeditorwillonlyaffectthe
selectedstate.Whenthebasestateisselected,youcaneditthebase
stateandallthechangeswillaffectallotherstatesunlessthe
changedpropertyisoverriddeninsomestate.
Selectthesingle_argumentstatebyclickingonitinthestateeditor.It
willalsobeautomaticallyselecteduponcreation.Next,select
theargument2textfieldandsetitsopacitypropertyto0.Thefieldwill
becomecompletelytransparent,exceptfortheblueoutline
providedbytheformeditor.However,thischangeonlyaffectsthe
single_argumentstate.Whenyouswitchtothebasestate,thetextfield
willbecomevisible.Whenyouswitchbacktothesecondstate,the
textfieldwillbecomeinvisibleagain.
Youcanswitchtothetexteditortoseehowthisstateisrepresented
inthecode:
states:[
State{
name:"single_argument"
PropertyChanges{
target:b
opacity:0
}
}
]
Asyoucansee,thestatedoesnotcontainafullcopyoftheform.
Instead,itonlyrecordsthedifferencebetweenthisstateandthe
basestate.
Nowweneedtoensurethattheform'sstateisproperlyupdated.
Youjustneedtobindthestatepropertyoftheformtoafunction
thatreturnsthecurrentstate.SwitchtotheCalculator.qmlfileandadd
thefollowingcode:
CalculatorForm{
state:{
if(operationFactorial.checked){
return"single_argument";
}else{
return"";
}
}
//...
}
Aswithanyotherpropertybinding,theQMLenginewill
automaticallyupdatethevalueofthestatepropertywhenneeded.
Whentheuserselectsthefactorialoperation,thecodeblockwill
return"single_argument",andthesecondtextfieldwillbehidden.In
othercases,thefunctionwillreturnanemptystringthat
correspondstothebasestate.Whenyouruntheapplication,you
shouldbeabletoseethisbehavior.
Timeforaction–Adding
smoothtransitioneffect
QtQuickallowsustoeasilyimplementsmoothtransitionbetween
states.Itwillautomaticallydetectwhensomepropertyneedstobe
changed,andifthereisamatchinganimationattachedtothe
object,thatanimationwilltakeovertheprocessofapplyingthe
change.Youdon'tevenneedtospecifythestartingandending
valuesoftheanimatedproperty;it'salldoneautomatically.
Toaddasmoothtransitiontoourform,addthefollowingcodeto
theCalculator.qmlfile:
CalculatorForm{
//...
transitions:Transition{
PropertyAnimation{
property:"opacity"
duration:300
}
}
}
Runtheapplicationandyouwillseethatthetextfield'sopacity
changesgraduallywhentheformtransitionstoanotherstate.
Whatjusthappened?
ThetransitionspropertyholdsthelistofTransitionobjectsforthis
object.It'spossibletospecifyadifferentTransitionobjectforeach
pairofstatesifyouwanttoperformdifferentanimationsin
differentcases.However,youcanalsouseasingleTransitionobject
thatwillaffectalltransitions.Forconvenience,QMLallowsusto
assignasingleobjecttoapropertythatexpectsalist.
ATransitionobjectmustcontainoneormultipleanimationsthatwill
beappliedduringthistransition.Inthisexample,weadded
PropertyAnimationthatallowsustoanimateanypropertyofanychild
objectofthemainform.ThePropertyAnimationQMLtypehas
propertiesthatallowyoutoconfigurewhatexactlyitwilldo.We
instructedittoanimatetheopacitypropertyandtake300msto
performtheanimation.Theopacitychangewillbelinearbydefault,
butyoucanusetheeasingpropertytoselectanothereasingfunction.
Asalways,theQtdocumentationisagreatsourceofdetailedinformationaboutavailable
typesandproperties.RefertoTransitionQMLTypeandAnimationQMLType
documentationpagesformoreinformation.Wewillalsotalkmoreaboutstatesand
transitionsinChapter13,AnimationsinQtQuickGames.
Haveagohero–Addingan
animationoftheitem's
position
Youcanmakethecalculator'stransitionevenmoreappealingifyou
makethetextfieldflyawayoffscreenwhilefadingout.Justusethe
formeditortochangethetextfield'spositioninthesingle_argument
state,andthenattachanotherPropertyAnimationtotheTransitionobject.
Youcanplaywithdifferenteasingtypestoseewhichlooksbetter
forthispurpose.
Popquiz
Q1.WhichpropertyallowsyoutopositionaQMLobjectrelativeto
anotherobject?
1. border
2. anchors
3. id
Q2.Whichfilenameextensionindicatesthatthefilecannotbe
loadedintoaQMLengine?
1. .qml
2. .ui
3. .ui.qml
4. AlloftheabovearevalidQMLfiles
Q3.WhatisaQtQuicktransition?
1. Achangeofparent-childrelationshipsamongtheexisting
QtQuickobjects
2. Asetofpropertiesthatchangewhenaneventoccurs
3. Asetofanimationsthatplaywhentheobject'sstatechanges
Summary
Inthischapter,youwereintroducedtowithadeclarativelanguage
calledQML.ThelanguageisusedtodriveQtQuick—aframework
forhighlydynamicandinteractivecontent.Youlearnedthebasics
ofQtQuick—howtocreatedocumentswithanumberofelement
typesandhowtocreateyourowninQML,orinC++.Youalso
learnedhowtobindexpressionstopropertiestoautomatically
reevaluatethem.YousawhowtoexposetheC++coreofyour
applicationtoQML-baseduserinterfaces.Youlearnedtousethe
visualformeditorandhowtocreateanimatedtransitionsinthe
interface.
YoualsolearnedwhichQMLmodulesareavailable.Youwere
shownhowtousetheQtQuick.ControlsandQtQuick.Layoutsmodulesto
buildtheapplication'suserinterfaceoutofstandardcomponents.
Inthenextchapter,wewillseehowyoucanmakeyourownfully
customizedQMLcomponentswithauniquelookandfeel.Wewill
showhowtoimplementcustomgraphicsandeventhandlingin
QMLapplications.
CustomizationinQtQuick
Inthepreviouschapter,youlearnedhowtousecontrolsand
layoutsprovidedbyQtQuicktobuildtheuserinterfaceofyour
application.QtcontainsnumerousQMLtypesthatcanserveas
buildingblocksforyourgame,providingrichfunctionalityanda
niceappearance.However,sometimesyouneedtocreateacustom
componentthatsatisfiestheneedsofyourgame.Inthischapter,we
willshowacoupleofconvenientwaystoextendyourQMLproject
withcustomcomponents.Bytheendofthischapter,youwillknow
howtoperformcustompaintingonacanvas,handlevariousinput
events,andimplementlazyloadingforyourcomponents.Wewill
alsoseehowtointegrateaC++objectintoQML'sobjecttree.
Themaintopicscoveredinthischapterareaslisted:
Creatingacustomcomponent
Handlingmouse,touch,keyboard,andgamepadevents
Dynamicandlazyloading
PaintingonCanvasusingJavaScript
CreatingacustomQML
component
Wealreadytouchedthetopicofcustomcomponentswhenwe
workedwiththeformeditorinthepreviouschapter.OurQMLfiles
implementedreusablecomponentswithacleaninterfacethatcan
beusedintherestoftheapplication.Wewillnowtakeamorelow-
levelapproachandcreateanewQMLcomponentdirectlyfrom
QMLcodeusingthebasicQtQuickbuildingblocks.Ourcomponent
willbeabuttonwitharoundedshapeandanicebackground.The
buttonwillholddefinabletextandanicon.Ourcomponentshould
lookgoodfordifferenttextsandicons.
Timeforaction–Creatinga
buttoncomponent
StartbycreatinganewprojectinQtCreator.ChooseQtQuick
Application-Emptyastheprojecttemplate.Namethe
projectcustom_buttonandleavetherestoftheoptionsunchanged.
Atthispoint,youshouldendupwithaQMLdocumentcontaining
anemptywindow.Let'sstartbycreatingthebuttonframe.
Editthemain.qmlfiletoaddanewRectangleitemtothewindow:
importQtQuick2.9
importQtQuick.Window2.2
Window{
visible:true
width:640
height:480
title:qsTr("HelloWorld")
Rectangle{
id:button
anchors.centerIn:parent
border{width:1;color:"black"}
radius:5
width:100;height:30
gradient:Gradient{
GradientStop{position:0;color:"#eeeeee"}
GradientStop{position:1;color:"#777777"}
}
}
}
Afterrunningtheproject,youshouldseearesultsimilartothe
following:
Whatjusthappened?
Youcanseethattherectangleiscenteredinthewindowusing
acenterInanchorbindingthatwedidn'tmentionbefore.Thisisone
ofthetwospecialanchorsthatareprovidedforconvenience,to
avoidhavingtowritetoomuchcode.UsingcenterInisequivalentto
settingbothhorizontalCenterandverticalCenter.Theotherconvenience
bindingisfill,whichmakesoneitemoccupythewholeareaof
anotheritem(similartosettingtheleft,right,top,andbottom
anchorstotheirrespectiveanchorlinesinthedestinationitem).
Insteadofsettingasolidcolorforthebutton,wedeclaredthe
backgroundtobealineargradient.WeboundaGradientelementto
thegradientpropertyanddefinedtwoGradientStopelementsasits
children,wherewespecifiedtwocolorstoblend
between.GradientdoesnotinheritfromItemandthusisnotavisual
QtQuickelement.Instead,itisjustaQMLobjectthatservesasa
dataholderforthegradientdefinition.
TheItemtypehasapropertycalledchildrenthatcontainsalistofthe
visualchildren(Iteminstances)ofanitemandanotherproperty
calledresources,whichcontainsalistofnon-visualobjects(such
asGradientorGradientStop)foranitem.Normally,youdon'tneedtouse
thesepropertieswhenaddingvisualornon-visualobjectstoan
item,astheitemwillautomaticallyassignchildobjectsto
appropriateproperties.Notethatinourcode,theGradientobjectis
notachildobjectoftheRectangle;itisjustassignedto
itsgradientproperty.
Timeforaction–Adding
buttoncontent
Thenextstepistoaddtextandanicontothebutton.First,copythe
iconfiletotheprojectdirectory.InQtCreator,
locatetheqml.qrcresourcefileintheprojecttree.Inthecontext
menuoftheresourcefile,selectAddExistingFilesandselectyour
iconfile.Thefilewillbeaddedtotheresourcesandwillappearin
theprojecttree.Ourexamplefileiscallededit-undo.png,andthe
correspondingresourceURLisqrc:/edit-undo.png.
YoucangettheresourcepathorURLofafilebylocatingthatfileintheprojecttreeand
usingtheCopyPathorCopyURLoptioninitscontextmenu.
Next,wewilladdtheiconandthetexttoourbuttonusinganother
itemtypecalledRow,asshown:
Rectangle{
id:button
anchors.centerIn:parent
border{width:1;color:"black"}
radius:5
gradient:Gradient{
GradientStop{position:0;color:"#eeeeee"}
GradientStop{position:1;color:"#777777"}
}
width:buttonContent.width+8
height:buttonContent.height+8
Row{
id:buttonContent
anchors.centerIn:parent
spacing:4
Image{
id:buttonIcon
source:"qrc:/edit-undo.png"
}
}
Text{
id:buttonText
text:"ButtonText"
}
}
}
You'llgetthefollowingoutput:
Whatjusthappened?
RowisapositionerQMLtypeprovidedbytheQtQuickmodule.Its
purposeissimilartotheRowLayouttypefromtheQtQuick.Layoutsmodule.
TheRowitemspreadsitschildreninahorizontalrow.Itmakesit
possibletopositionaseriesofitemswithoutusing
anchors.Rowhasthespacingpropertythatdictateshowmuchspaceto
leavebetweenitems.
TheQtQuickmodulealsocontainstheColumntypethatarrangeschildrenina
column,theGridtypethatcreatesagridofitems,andtheFlowtypethatpositionsits
childrensidebyside,wrappingasnecessary.
Timeforaction–Sizingthe
buttonproperly
Ourcurrentpaneldefinitionstilldoesn'tbehavewellwhenitcomes
tosizingthebutton.Ifthebuttoncontentisverysmall(for
example,theicondoesn'texistorthetextisveryshort),thebutton
willnotlookgood.Typically,pushbuttonsenforceaminimumsize
—ifthecontentissmallerthanaspecifiedsize,thebuttonwillbe
expandedtotheminimumsizeallowed.Anotherproblemisthatthe
usermightwanttooverridethewidthorheightoftheitem.Insuch
cases,thecontentofthebuttonshouldnotoverflowpasttheborder
ofthebutton.Let'sfixthesetwoissuesbyreplacing
thewidthandheightpropertybindingswiththefollowingcode:
clip:true
implicitWidth:Math.max(buttonContent.implicitWidth+8,80)
implicitHeight:buttonContent.implicitHeight+8
Whatjusthappened?
TheimplicitWidthandimplicitHeightpropertiescancontainthedesired
sizetheitemwantstohave.It'sadirectequivalentofsizeHint()from
QtWidgets.Byusingthesetwopropertiesinstead
ofwidthandheight(whichareboundtoimplicitWidthand
implicitHeightbydefault),weallowtheuserofourcomponentto
overridethoseimplicitvalues.Whenthishappensandtheuserdoes
notsetthewidthorheightbigenoughtocontaintheiconandtext
ofthebutton,wepreventthecontentfromcrossingtheboundaries
oftheparentitembysettingtheclippropertytotrue.
Clippingcanreduceperformanceofyourgame,souseitonlywhennecessary.
Timeforaction–Makingthe
buttonareusablecomponent
Sofar,wehavebeenworkingonasinglebutton.Addinganother
buttonbycopyingthecode,changingtheidentifiersofall
components,andsettingdifferentbindingstopropertiesisavery
tedioustask.Instead,wecanmakeourbuttonitemareal
component,thatis,anewQMLtypethatcanbeinstantiatedon
demandasmanytimesasrequired.
First,positionthetextcursorinthebeginningofourRectangleitem
andpressAlt+Enteronthekeyboardtoopentherefactoring
menu,likeinthefollowingscreenshot:
Fromthemenu,chooseMoveComponentintoSeparateFile.Inthe
popup,typeinanameforthenewtype(forexample,Button)and
checkanchors.centerIninthePropertyassignmentsformain.qmllist:
AcceptthedialogbyclickingontheOKbutton.
Whatjusthappened?
YoucanseethatwehaveanewfilecalledButton.qmlintheproject,
whichcontainseverythingthebuttonitemusedtohave,withthe
exceptionoftheidandanchors.centerInproperties.Themainfilewas
simplifiedtothefollowing:
Window{
visible:true
width:640
height:480
title:qsTr("HelloWorld")
Button{
id:button
anchors.centerIn:parent
}
}
Buttonhasbecomeacomponent—adefinitionofanewtypeof
elementthatcanbeusedthesamewayasstandardQMLelement
types.RememberthatQMLcomponentnames,aswellasnamesof
filesrepresentingthem,needtobeginwithacapitalletter!Ifyou
nameafilebutton.qmlinsteadofButton.qml,thenyouwillnotbeableto
useButtonasacomponentname,andtryingtouse"button"will
resultinanerrormessage.Thisworksbothways—everyQMLfile
startingwithacapitallettercanbetreatedasacomponent
definition.
Sincewecheckedanchors.centerIninthedialog,thispropertywasnot
movedtoButton.qml.Thereasonforthatchoiceisthatourbuttoncan
beputanywhere,soitcan'tpossiblyknowhowitshouldbe
positioned.Instead,positioningofthebuttonshouldbedoneatthe
locationwhereweusethecomponent.Nowwecaneditmain.qml
toputthebuttonintoalayoutoruseotherpositioningproperties
withouthavingtochangethecomponent'scode.
Importingcomponents
AcomponentdefinitioncanbeuseddirectlybyotherQMLfiles
residinginthesamedirectoryasthecomponentdefinition.Inour
example,themain.qmlandButton.qmlfilesarelocatedinthesame
directory,soyoucanusetheButtonQMLtypeinsidemain.qmlwithout
havingtoimportanything.
Ifyouneedtoaccessacomponentdefinitionfromafileresiding
elsewhere,youwillhavetofirstimportthemodulecontainingthe
componentinthefilewhereyouwanttouseit.Thedefinitionofa
moduleisverysimple—itisjustarelativepathtothedirectory
containingQMLfiles.Thismeansthatifyouhaveafile
namedBaz.qmlinadirectorycalledBase/Foo/Barandyouwanttouse
theBazcomponentfromwithintheBase/Foo/Ham.qmlfile,youwillhave
toputthefollowingimportstatementinHam.qml:
import"Bar"
Ifyouwanttousethesamecomponentfromwithin
theBase/Spam.qmlfile,youwillhavetoreplacetheimportstatement
withthis:
import"Foo/Bar"
Importingamodulemakesallitscomponentsavailableforuse.You
canthendeclareobjectsoftypesimportedfromacertainmodule.
QMLandvirtualresourcepaths
OurprojectusesaQtresourcefiletomakeourQMLfilesembedded
intothebinaryandensurethattheyarealwaysavailabletothe
application,evenifthesourcedirectoryisnotpresentatthe
computer.Duringstartup,werefertothemainQMLfileusing
theqrc:/main.qmlURL.Thismeansthattheruntimeonlyseesthefile
hierarchyintheresourcefile,andtheactualsourcedirectoryofthe
projectisnottakenintoaccount.
TheotherQMLfilehastheqrc:/Button.qmlURL,soQtconsidersthem
tobeinthesamevirtualdirectoryandeverythingstillworks.
However,ifyoucreateaQMLfilebutforgettoaddittotheproject's
resources,Qtwillbeunabletoloadthatfile.Evenifthefileis
presentinthesamerealdirectoryasmain.qml,Qtwillonlylookforit
inthevirtualqrc:/directory.
It'spossibletoaddafiletotheresourceswithaprefix,inwhichcaseitcanhaveanURL
likeqrc:/some/prefix/Button.qml,andtheruntimewillconsiderittobeinanothervirtual
directory.Thatbeingsaid,unlessyouexplicitlycreateanewprefix,youshouldbefine.If
yourQMLfilesarearrangedinsubdirectories,theirhierarchywillbepreservedwhenyou
addthemtotheresourcefile.
Eventhandlers
QtQuickismeanttobeusedforcreatinguserinterfacesthatare
highlyinteractive.Itoffersanumberofelementsfortakinginput
eventsfromtheuser.Inthissection,wewillgothroughthemand
seehowyoucanusethemeffectively.
Timeforaction–Makingthe
buttonclickable
Sofar,ourcomponentonlylookslikeabutton.Thenexttaskisto
makeitrespondtomouseinput.
TheMouseAreaQMLtypedefinesatransparentrectanglethatexposes
anumberofpropertiesandsignalsrelatedtomouseinput.
Commonlyusedsignalsincludeclicked,pressed,andreleased.Let'sdoa
coupleofexercisestoseehowtheelementcanbeused.
OpentheButton.qmlfileandaddaMouseAreachilditemtothebutton
anduseanchorstomakeitfillthewholeareaofthebutton.Callthe
elementbuttonMouseArea.Putthefollowingcodeinthebodyoftheitem:
Rectangle{
id:button
//...
Row{...}
MouseArea{
id:buttonMouseArea
anchors.fill:parent
onClicked:button.clicked()
}
}
Inadditiontothis,setthefollowingdeclarationinthebuttonobject
justafteritsIDisdeclared:
Rectangle{
id:button
signalclicked()
//...
}
Totestthemodification,gotothemain.qmlfileandaddasignal
handlertothebutton:
Button{
id:button
anchors.centerIn:parent
onClicked:console.log("Clicked!")
}
Then,runtheprogramandclickonthebutton.You'llseeyour
messageprintedtotheQtCreator'sconsole.
Whatjusthappened?
Withthesignalclicked()statement,wedeclaredthatthebuttonobject
canemitasignalcalledclicked.WiththeMouseAreaitem,wedefineda
rectangulararea(coveringthewholebutton)thatreactstomouse
events.Then,wedefinedonClicked,whichisasignalhandler.For
everysignalanobjecthas,ascriptcanbeboundtoahandlernamed
likethesignalandprefixedwith"on";hence,fortheclickedsignal,
thehandleriscalledonClicked,and,forvalueChanged,itis
calledonValueChanged.
Inthisparticularcase,wehavetwohandlersdefined—oneforthe
buttonwherewewriteasimplestatementtotheconsole,andthe
otherfortheMouseAreaelementwherewecallthebutton'ssignal
function,effectivelyemittingthatsignal.
MouseAreahasevenmorefeatures,sonowlet'stryputtingthemtothe
rightusetomakeourbuttonmorefeature-rich.
Timeforaction–Visualizing
buttonstates
Currently,thereisnovisualreactiontoclickingonthebutton.In
therealworld,thebuttonhassomedepthandwhenyoupushitand
lookatitfromabove,itscontentsseemstoshiftalittletowardthe
rightanddownward.Let'smimicthisbehaviorbymakinguseofthe
pressedpropertyMouseAreahas,whichdenoteswhetherthemouse
buttoniscurrentlybeingpressed(notethatthepressedpropertyis
differentfromthepressedsignalthatwasmentionedearlier).The
contentofthebuttonisrepresentedbytheRowelement,soaddthe
followingstatementsinsideitsdefinition:
Row{
id:buttonContent
//...
anchors.verticalCenterOffset:buttonMouseArea.pressed?1:0
anchors.horizontalCenterOffset:buttonMouseArea.pressed?1:0
//...
}
Wecanalsomakethetextchangecolorwhenthemousecursor
hoversoverthebutton.Forthis,wehavetodotwothings.First,
let'senablereceivinghovereventsontheMouseAreabysetting
itshoverEnabledproperty:
hoverEnabled:true
Whenthispropertyisset,MouseAreawillbesetting
itscontainsMousepropertytotruewheneveritdetectsthemousecursor
overitsownarea.Wecanusethisvaluetosetthetextcolor:
Text{
id:buttonText
text:"ButtonText"
color:buttonMouseArea.containsMouse?"white":"black"
}
Whatjusthappened?
Inthelastexercise,welearnedtousesomepropertiesandsignals
fromMouseAreatomakethebuttoncomponentmoreinteractive.
However,theelementismuchricherinfeatures.Inparticular,if
hovereventsareenabled,youcangetthecurrentmousepositionin
theitem'slocalcoordinatesystemthrough
themouseXandmouseYpropertiesthatreturnvalues.Thecursor
positioncanalsobereportedbyhandlingthepositionChangedsignal.
Speakingofsignals,mostMouseAreasignalscarryaMouseEventobjectas
theirargument.Thisargumentiscalledmouseandcontainsuseful
informationaboutthecurrentstateofthemouse,includingits
positionandbuttonscurrentlypressed.Bydefault,MouseAreaonly
reactstotheleftmousebutton,butyoucan
usetheacceptedButtonspropertytoselectwhichbuttonsitshould
handle.Thesefeaturesareshowninthefollowingexample:
MouseArea{
id:buttonMouseArea
anchors.fill:parent
hoverEnabled:true
acceptedButtons:Qt.LeftButton|Qt.MiddleButton|Qt.RightButton
onClicked:{
switch(mouse.button){
caseQt.LeftButton:
console.log("Leftbuttonclicked");break;
caseQt.MiddleButton:
console.log("Middlebuttonclicked");break;
caseQt.RightButton:
console.log("Rightbuttonclicked");break;
}
}
onPositionChanged:{
console.log("Position:["+mouse.x+";"+mouse.y+"]");
}
}
Timeforaction–Notifyingthe
environmentaboutbutton
states
Wehaveaddedsomecodetomakethebuttonlookmorenaturalby
changingitsvisualaspects.Now,let'sextendthebutton
programminginterfacesothatdeveloperscanusemorefeaturesof
thebutton.
Thefirstthingwecandoismakebuttoncolorsdefinableby
introducingsomenewpropertiesforthebutton.Let'sputthe
highlightedcodeatthebeginningofthebuttoncomponent
definition:
Rectangle{
id:button
propertycolortopColor:"#eeeeee"
propertycolorbottomColor:"#777777"
propertycolortextColor:"black"
propertycolortextPressedColor:"white"
signalclicked()
Then,we'llusethenewdefinitionsforthebackgroundgradient:
gradient:Gradient{
GradientStop{position:0;color:button.topColor}
GradientStop{position:1;color:button.bottomColor}
}
Nowforthetextcolor:
Text{
id:buttonText
text:"ButtonText"
color:buttonMouseArea.pressed?
button.textPressedColor:button.textColor
}
Asyoucannote,weusedthepressedpropertyofMouseAreatodetect
whetheramousebuttoniscurrentlybeingpressedonthearea.We
canequipourbuttonwithasimilarproperty.Addthefollowing
codetothetoplevelRectangleoftheButtoncomponent:
propertyaliaspressed:buttonMouseArea.pressed
Whatjusthappened?
Thefirstsetofchangesintroducedfournewpropertiesdefining
fourcolorsthatwelaterusedinstatementsdefininggradientand
textcolorsforthebutton.InQML,youcandefinenewproperties
forobjectswiththepropertykeyword.Thekeywordshouldbe
followedbythepropertytypeandpropertyname.QML
understandsmanypropertytypes,themostcommon
beingint,real,string,font,andcolor.Propertydefinitionscancontain
anoptionaldefaultvaluefortheproperty,precededwithacolon.
Thesituationisdifferentwiththepressedpropertydefinition.
Youcanseethatforthepropertytype,thedefinitioncontainsthe
wordalias.Itisnotapropertytypebutanindicatorthatthe
propertyisreallyanaliastoanotherproperty—eachtime
thepressedpropertyofthebuttonisaccessed,thevalueof
thebuttonMouseArea.pressedpropertyisreturned,andeverytimethe
propertyischanged,itisthemousearea'spropertythatreallygets
changed.Witharegularpropertydeclaration,youcanprovideany
validexpressionasthedefaultvaluebecausetheexpressionis
boundtotheproperty.Withapropertyalias,itisdifferent—the
valueismandatoryandhastobepointingtoanexistingpropertyof
thesameoranotherobject.
Considerthefollowingtwodefinitions:
propertyintfoo:someobject.prop
propertyaliasbar:someobject.prop
Atfirstglance,theyaresimilarastheypointtothesameproperty
andthereforethevaluesreturnedforthepropertiesarethesame.
However,thepropertiesarereallyverydifferent,whichbecomes
apparentifyoutrytomodifytheirvalues:
foo=7
bar=7
Thefirstpropertyactuallyhasanexpressionboundtoit,so
assigning7tofoosimplyreleasesthebindingandassignsthe
value7tothefooproperty,leavingsomeobject.propwithitsoriginal
value.Thesecondstatement,however,isanalias;therefore,
assigninganewvalueappliesthemodificationto
thesomeobject.proppropertythealiasisreallypointingto.
Speakingofproperties,thereisaneasywaytoreactwhena
propertyvalueismodified.Foreachexistingproperty,thereisa
handleravailablethatisexecutedwheneverthepropertyvalueis
modified.Thehandlernameisonfollowedbythepropertyname,
thenfollowedbythewordChanged,allincamelcase—thus,for
afooproperty,itbecomesonFooChangedandfortopColor,it
becomesonTopColorChanged.Tologthecurrentpressstateofthebutton
totheconsole,allweneedtodoisimplementthepropertychange
handlerforthisproperty:
Button{
//...
onPressedChanged:{
console.log("Thebuttoniscurrently"+
(pressed?"":"not")+"pressed")
}
}
Inthisexample,wecreatedafullyfunctionalcustomQML
component.Ourbuttonreactstomouseinputandexposessome
usefulpropertiesandsignalstotheuser.Thismakesitareusable
andcustomizableobject.Inarealproject,alwaysthinkofthe
repeatingpartsofyourUIandconsidermovingthemintoasingle
componenttoreducecodeduplication.
Touchinput
MouseAreaisthesimplestofinputeventelements.Nowadays,more
andmoredeviceshavetouchcapabilitiesandQtQuickcanhandle
themaswell.Currently,wehavethreewaysofhandlingtouch
input.
Firstofall,simpletoucheventsarealsoreportedasmouse
events.Tappingandslidingafingeronthescreencanbehandled
usingMouseArea,justlikemouseinput.
Timeforaction–Draggingan
itemaround
CreateanewQtQuickApplication-Emptyproject.
Editthemain.qmlfiletoaddacircletothewindow:
Rectangle{
id:circle
width:60;height:width
radius:width/2
color:"red"
}
Next,addaMouseAreatothecircleanduseitsdragpropertytoenable
movingthecirclebytouch(ormouse):
Rectangle{
//...
MouseArea{
anchors.fill:parent
drag.target:circle
}
}
Then,youcanstarttheapplicationandbeginmovingthecircle
around.
Whatjusthappened?
Acirclewascreatedbydefiningarectanglewithitsheightequalto
width,makingitasquareandroundingtheborderstohalftheside
length.ThedragpropertycanbeusedtotellMouseAreatomanagea
givenitem'spositionusinginputeventsflowinginto
thisMouseAreaelement.Wedenotetheitemtobedraggedusing
thetargetsubproperty.Youcanuseothersubpropertiestocontrol
theaxistheitemisallowedtomovealongorconstrainthemoveto
agivenarea.Animportantthingtorememberisthattheitembeing
draggedcannotbeanchoredfortheaxisonwhichthedragis
requested;otherwise,theitemwillrespecttheanchorandnotthe
drag.Wedidn'tanchorourcircleitematallsincewewantittobe
draggablealongbothaxes.
ThesecondapproachtohandlingtouchinputinQtQuick
applicationsistousePinchArea,whichisanitemsimilartoMouseArea,
butratherthandragginganitemaround,itallowsyoutorotateor
scaleitusingtwofingers(withasocalled"pinch"gesture),as
shown:
BeawarethatPinchAreareactsonlytotouchinput,sototestthe
example,youwillneedarealmultitouchcapabledevice.
Timeforaction–Rotatingand
scalingapicturebypinching
StartanewQtQuickApplication-Emptyproject.Addanimagefile
totheresources,justlikewepreviouslydidinthebuttonproject.In
themain.qmlfile,addanimagetothewindowandmakeitcenteredin
itsparent:
Image{
id:image
anchors.centerIn:parent
source:"qrc:/wilanow.jpg"
}
Now,wewilladdaPinchAreaelement.Thiskindofitemcanbeused
intwoways—eitherbymanuallyimplementingsignal
handlersonPinchStarted,onPinchUpdated,andonPinchFinishedtohavetotal
controloverthefunctionalityofthegesture,orusingasimplified
interfacesimilartothedragpropertyofMouseArea.Sincethesimplified
interfacedoesexactlywhatwewant,thereisnoneedtohandle
pincheventsmanually.Let'saddthefollowingdeclarationtothe
file:
PinchArea{
anchors.fill:parent
pinch{
target:image
minimumScale:0.2
maximumScale:2.0
minimumRotation:-90
maximumRotation:90
}
}
You'llgetanoutputsimilartothefollowingscreenshot:
Whatjusthappened?
Oursimpleapplicationloadsanimageandcentersitintheview.
Then,thereisaPinchAreaitemfillingtheviewareathatistoldto
operateontheimageobject.Wedefinetherangeofthescalingand
rotatingoftheitem.TherestislefttothePinchAreaitemitself.Ifyou
startinteractingwiththeapplication,youwillseetheitemrotate
andscale.Whatreallyhappensbehindthescenesis
thatPinchAreamodifiesthevaluesofthetwopropertieseachQtQuick
itemhas—rotationandscale.
PinchAreacanalsocontrolthedraggingoftheitemwithpinch.dragAxis,just
likeMouseAreadoeswithdrag,butforsimplicity,wedidn'tusethispartoftheAPI.Feelfree
toexperimentwithitinyourowncode.
Haveagohero–Rotatingand
scalingwithamouse
Ofcourse,youdon'thavetousePinchAreatorotateorscaleanitem.
Propertiescontrollingthoseaspectsareregularpropertiesthatyou
canreadandwriteatanytime.TryreplacingPinchAreawithMouseAreato
obtainaresultsimilartowhatwejustdidbymodifyingthescale
androtationpropertiesasaresultofreceivingmouseevents—when
theuserdragsthemousewhilepressingtheleftbutton,theimageis
scaledandwhentheuserdoesthesamewhilepressingtheright
button,theimageisrotated.
Ifyoumanagetodothetask,try
replacingMouseAreawithPinchAreaagain,butthen,insteadofusing
thepinchproperty,handleeventsmanuallytoobtainthesameeffect
(theeventobjectiscalledpinchandhasanumberofpropertiesyou
canplaywith).
Athirdapproachtohandlingtouchinputisusing
theMultiPointTouchAreaitem.Itprovidesalow-levelinterfaceto
gesturesbyreportingeachtouchpointseparately.Itcanbeusedto
createcustomhigh-levelgesturehandlerssimilartoPinchArea.
Keyboardinput
Sofar,we'vebeendealingwithpointerinput,butuserinputisnot
justthat—wecanalsohandlekeyboardinput.Thisisquitesimple
andbasicallyboilsdowntotwoeasysteps.
First,youhavetoenablereceivingkeyboardeventsbystatingthata
particularitemhaskeyboardfocus:
focus:true
Then,youcanstarthandlingeventsbywritinghandlersinasimilar
fashionasformouseevents.However,Itemdoesn'tprovideitsown
handlerformanipulatingkeysthatisacounterpart
forkeyPressEventandkeyReleaseEventofQWidget.Instead,adequate
handlersareprovidedbytheKeysattachedproperty.
Attachedpropertiesareprovidedbyelementsthatarenotusedas
standaloneelementsbutprovidepropertiestootherobjectsby
gettingattachedtothemaswell.Thisisawayofaddingsupportfor
newpropertieswithoutmodifyingtheAPIoftheoriginalelement(it
doesn'taddnewpropertiesthroughanis-arelation,butrather
throughahas-aone).Eachobjectthatreferencesanattached
propertygetsitsowncopyoftheattachingobjectthatthenhandles
theextraproperties.Wewillcomebacktoattachedpropertieslater
inthechapter.Fornow,youjustneedtorememberthatincertain
situations,anelementcanobtainadditionalpropertiesthatarenot
partofitsAPI.
Let'sgobacktoimplementingeventhandlersforkeyboardinput.
Aswesaidearlier,eachItemhasaKeysattachedpropertythatallows
ustoinstallourownkeyboardhandlers.Thebasictwo
signalsKeysaddstoItemarepressedandreleased;therefore,wecan
implementtheonPressedandonReleasedhandlersthathave
aKeyEventargumentprovidingsimilarinformationasQKeyEventinthe
widgetworld.Asanexample,wecanseeanitemthatdetectswhen
thespacebarwaspressed:
Rectangle{
focus:true
color:"black"
width:100
height:100
Keys.onPressed:{
if(event.key===Qt.Key_Space){
color="red";
}
}
Keys.onReleased:{
if(event.key===Qt.Key_Space){
color="blue";
}
}
}
Itmightbecomeproblematicifyouwanttohandlemanydifferent
keysinthesameitem,astheonPressedhandlerwouldlikelycontaina
giantswitchsectionwithbranchesforeverypossiblekey.
Fortunately,Keysoffersmoreproperties.Mostofthecommonlyused
keys(butnotletters)havetheirownhandlersthatarecalledwhen
theparticularkeyispressed.Thus,wecaneasilyimplementanitem
thattakesadifferentcolordependingonwhichkeywaspressed
last:
Rectangle{
//...
focus:true
Keys.onSpacePressed:color="purple"
Keys.onReturnPressed:color="navy"
Keys.onVolumeUpPressed:color="blue"
Keys.onRightPressed:color="green"
Keys.onEscapePressed:color="yellow"
Keys.onTabPressed:color="orange"
Keys.onDigit0Pressed:color="red"
Keys.onDigit0Pressed:color="red"
}
Notethatthereleasedsignalwillstillbeemittedforeveryreleased
keyevenifthekeyhasitsownpressedsignal.
Now,consideranotherexample:
Item{
id:item
propertyintnumber:0
width:200;height:width
focus:true
Keys.onSpacePressed:{
number++;
}
Text{
text:item.number
anchors.centerIn:parent
}
}
Wewouldexpectthatwhenwepressandholdthespacebar,wewill
seethetextchangefrom0to1andstayonthatvalueuntilwe
releasethekey.Ifyouruntheexample,youwillseethatinstead,the
numberkeepsincrementingaslongasyouholddownthekey.This
isbecausebydefault,thekeysautorepeat—whenyouholdthekey,
theoperatingsystemkeepssendingasequenceofpress-release
eventsforthekey(youcanverifythatbyadding
theconsole.log()statementsto
theKeys.onPressedandKeys.onReleasedhandlers).Tocounterthiseffect,
youcandifferentiatebetweenautorepeatandregularevents.InQt
Quick,youcandothiseasily,aseachkeyeventcarriesthe
appropriateinformation.Simplyreplacethehandlerfromthelast
examplewiththefollowingone:
Keys.onSpacePressed:{
if(!event.isAutoRepeat){
number++;
}
}
Theeventvariableweusehereisthenameoftheparameterof
thespacePressedsignal.Aswecannotdeclareourownnamesforthe
parameterslikewecandoinC++,foreachsignalhandler,youwill
havetolookupthenameoftheargumentinthedocumentation.
YoucansearchforKeysinthedocumentationindextoopentheKeys
QMLTypepage.Thesignallistwillcontaintypeandnameofthe
signal'sparameter,forexample,spacePressed(KeyEventevent).
Wheneveryouprocessanevent,youshouldmarkitasacceptedto
preventitfrombeingpropagatedtootherelementsandhandledby
them:
Keys.onPressed:{
if(event.key===Qt.Key_Space){
color="blue";
event.accepted=true;
}
}
However,ifyouuseahandlerdedicatedtoanindividualbutton
(likeonSpacePressed),youdon'tneedtoaccepttheevent,asQtwilldo
thatforyouautomatically.
InstandardC++applications,weusuallyusetheTabkeyto
navigatethroughfocusableitems.Withgames(andfluiduser
interfacesingeneral),itismorecommontousearrowkeysforitem
navigation.Ofcourse,wecanhandlethissituationusing
theKeysattachedpropertyand
addingKeys.onRightPressed,Keys.onTabPressed,andothersignalhandlersto
eachofouritemswherewewanttomodifythefocuspropertyofthe
desireditem,butitwouldquicklyclutterourcode.QtQuickcomes
toourhelponceagainbyprovidingaKeyNavigationattachedproperty,
whichismeanttohandlethisspecificsituationandallowsusto
greatlysimplifytheneededcode.Now,wecanjustspecifywhich
itemshouldgetintofocuswhenaspecifickeyistriggered:
Row{
spacing:5
Rectangle{
id:first
width:50;height:width
color:focus?"blue":"lightgray"
focus:true
KeyNavigation.right:second
}
Rectangle{
id:second
width:50;height:width
color:focus?"blue":"lightgray"
KeyNavigation.right:third
}
Rectangle{
id:third
width:50;height:width
color:focus?"blue":"lightgray"
}
}
Notethatwemadethefirstitemgetintofocusinthebeginningby
explicitlysettingthefocusproperty.BysettingtheKeyNavigation.right
property,weinstructQttofocusonthespecifieditemwhenthis
itemreceivesarightkeypressevent.Thereversetransitionisadded
automatically—whentheleftkeyispressedontheseconditem,the
firstitemwillreceivefocus.Besidesright,KeyNavigationcontains
theleft,down,up,tab,andbacktab(Shift+Tab)properties.
BoththeKeysandKeyNavigationattachedpropertieshaveawayto
define
theorderinwhicheachofthemechanismsreceivetheevents.This
ishandledbythepriorityproperty,whichcanbesetto
eitherBeforeItemorAfterItem.Bydefault,Keyswillgettheeventfirst
(BeforeItem),thentheinternaleventhandlingcantakeplaceand
finally,KeyNavigationwillhaveachanceofhandlingtheevent
(AfterItem).Notethatifthekeyishandledbyoneofthemechanisms,
theeventisacceptedandtheremainingmechanismswillnot
receivethatevent.
Haveagohero–Practicing
key-eventpropagation
Asanexercise,youcanexpandourlastexamplebybuildingalarger
arrayofitems(youcanusetheGridelementtopositionthem)and
defininganavigationsystemthatmakesuseof
theKeyNavigationattachedproperty.Havesomeoftheitemshandle
eventsthemselvesusingtheKeysattachedproperty.Seewhat
happenswhenthesamekeyishandledbybothmechanisms.Try
influencingthebehaviorusingthepriorityproperty.
Whenyousetthefocuspropertyofanitemtotrue,anypreviously
useditemlosesfocus.Thisbecomesaproblemwhenyoutryto
writeareusablecomponentthatneedstosetfocustoitschildren.If
youaddmultipleinstancesofsuchacomponenttoasinglewindow,
theirfocusrequestswillconflictwitheachother.Onlythelastitem
willhavefocusbecauseitwascreatedlast.Toovercomethis
problem,QtQuickintroducesaconceptoffocusscopes.By
wrappingyourcomponentintoaFocusScopeitem,yougainabilityto
setfocustoaniteminsidethecomponentwithoutinfluencingthe
globalfocusdirectly.Whenaninstanceofyourcomponentreceives
focus,theinternalfocuseditemwillalsoreceivefocusandwillbe
abletohandlekeyboardevents.Agoodexplanationofthisfeatureis
givenontheKeyboardFocusinQtQuickdocumentationpage.
Textinputfields
Apartfromtheattachedpropertieswedescribed,QtQuickprovides
built-inelementsforhandlingkeyboardinput.Thetwomostbasic
typesareTextInputandTextEdit,whichareQMLequivalents
ofQLineEditandQTextEdit.Theformerareusedforsingle-linetext
input,whilethelatterservesasitsmultilinecounterpart.Theyboth
offercursorhandling,undo-redofunctionality,andtextselections.
YoucanvalidatetexttypedintoTextInputbyassigningavalidatorto
thevalidatorproperty.Forexample,toobtainanitemwheretheuser
caninputadot-separatedIPaddress,wecanusethefollowing
declaration:
TextInput{
id:ipAddress
width:100
validator:RegExpValidator{
//fournumbersseparatedbydots
regExp:/\d+\.\d+\.\d+\.\d+/
}
focus:true
}
Theregularexpressiononlyverifiestheformatoftheaddress.The
usercanstillinsertbogusnumbers.Youshouldeitherdoaproper
checkbeforeusingtheaddressorprovideamorecomplexregular
expressionthatwillconstraintherangeofnumberstheusercan
enter.
OnethingtorememberisthatneitherTextInputnorTextEdithasany
visualappearance(apartfromthetextandcursortheycontain),so
ifyouwanttogivetheusersomevisualhintastowheretheitemis
positioned,theeasiestsolutionistowrapitinastyledrectangle:
Rectangle{
id:textInputFrame
width:200
height:40
border{color:"black";width:2}
radius:10
antialiasing:true
color:"darkGray"
}
TextInput{
id:textInput
anchors.fill:textInputFrame
anchors.margins:5
font.pixelSize:height-2
verticalAlignment:TextInput.AlignVCenter
clip:true
}
Notethatthehighlightedcode—theclippropertyoftextInput—is
enabledsuchthatbydefault,ifthetextenteredintheboxdoesn'tfit
intheitem,itwilloverflowitandremainvisibleoutsidetheactual
item.Byenablingclipping,weexplicitlysaythatanythingthat
doesn'tfittheitemshouldnotbedrawn.
TheQtQuick.Controlsmoduleprovidesmoreadvancedtextinput
controls,suchasTextFieldandTextArea.We'vealreadyusedthemin
ourprojectinChapter11,IntroductiontoQtQuick.
Gamepadinput
Handlinggamepadeventsisaverycommontaskwhendevelopinga
game.Fortunately,QtprovidesQtGamepadmoduleforthis
purpose.WealreadylearnedhowtouseitinC++.Nowlet'ssee
howtodothisinQMLapplications.
ToenableQtGamepadmodule,addQT+=gamepadtotheprojectfile.
Next,importtheQMLmodulebyaddingthefollowinglineatthe
beginningofyourQMLfile:
importQtGamepad1.0
ThisimportallowsyoutodeclareobjectsoftheGamepadtype.Addthe
followingobjectinsideyourtop-levelQMLobject:
Gamepad{
id:gamepad
deviceId:GamepadManager.connectedGamepads.length>0?
GamepadManager.connectedGamepads[0]:-1
}
TheGamepadManagerobjectallowsustolistidentifiersofgamepads
availableinthesystem.Weusethefirstavailablegamepadifany
arepresentinthesystem.Ifyouwantthegametopickupa
connectedgamepadonthefly,usethefollowingcodesnippet:
Connections{
target:GamepadManager
onGamepadConnected:gamepad.deviceId=deviceId
}
Whatjusthappened?
Theprecedingcodesimplyaddsasignalhandlerfor
thegamepadConnectedsignaloftheGamepadManagerobject.Theusualwayto
addasignalhandleristodeclareitdirectlyinthesectionofthe
sender.However,wecan'tdothatinthiscase,sinceGamepadManageris
anexistingglobalobjectthatisnotpartofourQMLobjecttree.
Thus,weusetheConnectionsQMLtypethatallowsustospecifyan
arbitrarysender(usingthetargetproperty)andattachasignal
handlertoit.YoucanthinkofConnectionsasadeclarativeversionof
QObject::connectcalls.
Theinitializationisdone,sowecannowusethegamepadobjectto
requestinformationaboutthegamepadinput.Therearetwoways
todothat.
First,wecanusepropertybindingstosetpropertiesofotherobjects
dependingonthebuttonspressedonthegamepad:
Text{
text:gamepad.buttonStart?"Start!":""
}
Wheneverthestartbuttonispressedorreleasedonthegamepad,
thevalueofthegamepad.buttonStartpropertywillbesettotrueorfalse,
andtheQMLenginewillautomaticallyupdatethedisplayedtext.
Thesecondwayistoaddasignalhandlertodetectwhenaproperty
changes:
Gamepad{
//...
onButtonStartChanged:{
if(value){
if(value){
console.log("startpressed");
}else{
console.log("startreleased");
}
}
}
TheGamepadQMLtypehasaseparatepropertyandsignalforeach
gamepadbutton,justliketheQGamepadC++class.
YoucanalsousetheGamepadKeyNavigationQMLtypetointroduce
gamepadsupporttoagamethatsupportskeyboardinput:
GamepadKeyNavigation{
gamepad:gamepad
active:true
buttonStartKey:Qt.Key_S
}
WhenthisobjectisdeclaredinyourQMLfile,gamepadevents
providedbythegamepadobjectwillbeautomaticallyconvertedtokey
events.Bydefault,GamepadKeyNavigationisabletoemulateup,down,
left,right,back,forward,andreturnkeyswhenthecorresponding
gamepadbuttonsarepressed.However,youcanoverridethe
defaultmappingoraddyourownmappingforothergamepad
buttons.Intheprecedingexample,wetellGamepadKeyNavigationthatthe
startkeyonthegamepadshouldactasiftheSkeywaspressedon
thekeyboard.Youcannowhandletheseeventsjustasanyregular
keyboardevent.
Sensorinput
Qtisreachingouttomoreandmoreplatformsthatareused
nowadays.Thisincludesanumberofpopularmobileplatforms.
Mobiledevicesareusuallyequippedwithadditionalhardware,less
oftenseenondesktopsystems.Let'sseehowtohandlesensorinput
inQtsothatyoucanuseitinyourgames.
Mostofthefeaturesdiscussedinthissectionarenotusuallyavailableondesktops.Ifyou
wanttoplaywiththem,youneedtosetuprunningQtapplicationsonamobiledevice.
Thisrequiresafewconfigurationstepsthatdependonyourtargetplatform.Pleasereferto
Qtdocumentationforexactinstructions,astheywilloffercompleteandup-to-date
informationthatwouldn'tbepossibletoprovideinabook.Goodstartingpoints
areGettingStartedwithQtforAndroidandQtforiOSdocumentationpages.
AccesstosensorspresentonmobiledevicesisprovidedbytheQt
Sensorsmoduleandmustbeimportedbeforeitcanbeused:
importQtSensors5.0
TherearealotofQMLtypesyoucanusetointeractwithsensors.
Havealookatthisimpressivelist:
QMLtype Description
Accelerome
ter
Reportsthedevice'slinearaccelerationalongthex,y,
andzaxes.
Altimeter
Reportsthealtitudeinmetersrelativetomeansea
level.
AmbientLig
AmbientLig
htSensor
Reportstheintensityoftheambientlight.
AmbientTem
peratureSe
nsor
ReportsthetemperatureindegreeCelsiusofthe
currentdevice'sambient.
Compass
Reportstheazimuthofthedevice'stopasdegrees
frommagneticnorth.
DistanceSe
nsor
Reportsdistanceincmfromanobjecttothedevice.
Gyroscope
Reportsthedevice'smovementarounditsaxesin
degreespersecond.
HolsterSen
sor
Reportsifthedeviceisholsteredinaspecificpocket.
HumiditySe
nsor
Reportsonhumidity.
IRProximit
ySensor
Reportsthereflectanceoftheoutgoinginfraredlight.
Therangeisfrom0(zeroreflection)to1(total
reflection).
LidSensor
Reportsonwhetheradeviceisclosed.
Reportsonwhetheradeviceisclosed.
LightSenso
r
Reportstheintensityoflightinlux.
Magnetomet
er
Reportsthedevice'smagneticfluxdensityalongits
axes.
Orientatio
nSensor
Reportstheorientationofthedevice.
PressureSe
nsor
ReportstheatmosphericpressureinPascals.
ProximityS
ensor
Reportsifanobjectisclosetothedevice.Which
distanceisconsidered"close"dependsonthedevice.
RotationSe
nsor
Reportsthethreeanglesthatdefinetheorientationof
thedeviceinathree-dimensionalspace.
TapSensor
Reportsifadevicewastapped.
TiltSensor
Reportstheangleoftiltindegreesalongthedevice's
axes.
Unfortunately,notallsensorsaresupportedonallplatforms.CheckouttheCompatibility
Mapdocumentationpagetoseewhichsensorsaresupportedonyourtargetplatforms
beforetryingtousethem.
AllthesetypesinherittheSensortypeandprovidesimilarAPI.First,
createasensorobjectandactivateitbysettingitsactivepropertyto
true.Whenthehardwarereportsnewvalues,theyareassignedto
thesensor'sreadingproperty.AswithanypropertyinQML,youcan
choosebetweenusingthepropertydirectly,usingitinaproperty
binding,orusingtheonReadingChangedhandlertoreacttoeachnew
valueoftheproperty.
Thetypeofthereadingobjectcorrespondstothetypeofthesensor.
Forexample,ifyouuseatiltsensor,you'llreceiveaTiltReadingobject
thatprovidessuitablepropertiestoaccesstheangleoftiltaround
thex(xRotation)andy(yRotation)axes.Foreachsensortype,Qt
providesacorrespondingreadingtypethatcontainsthesensor
data.
Allreadingsalsohavethetimestamppropertythatcontainsthe
numberofmicrosecondssincesomefixedpoint.Thatpointcanbe
differentfordifferentsensorobjects,soyoucanonlyuseitto
calculatetimeintervalsbetweentworeadingsofthesamesensor.
ThefollowingQMLcodecontainsacompleteexampleofusingatilt
sensor:
importQtQuick2.9
importQtQuick.Window2.2
importQtSensors5.0
Window{
visible:true
width:640
height:480
title:qsTr("HelloWorld")
Text{
anchors.centerIn:parent
text:{
if(!tiltSensor.reading){
return"Nodata";
}
varx=tiltSensor.reading.xRotation;
varx=tiltSensor.reading.xRotation;
vary=tiltSensor.reading.yRotation;
return"X:"+Math.round(x)+
"Y:"+Math.round(y)
}
}
TiltSensor{
id:tiltSensor
active:true
onReadingChanged:{
//processnewreading
}
}
}
Whenthisapplicationreceivesanewreading,thetextonscreen
willbeautomaticallyupdated.YoucanalsousetheonReadingChanged
handlertoprocessnewdatainanotherway.
Detectingdevicelocation
Somemoderngamesrequireinformationabouttheplayer's
geographiclocationandotherrelateddata,suchasmovement
speed.TheQtPositioningmoduleallowsyoutoaccessthis
information.Let'sseeabasicQMLexampleofdeterminingthe
location:
importQtQuick2.9
importQtQuick.Window2.2
importQtPositioning5.0
Window{
visible:true
width:640
height:480
title:qsTr("HelloWorld")
Text{
anchors.centerIn:parent
text:{
varpos=positionSource.position;
varcoordinate=pos.coordinate;
return"latitude:"+coordinate.latitude+
"\nlongitude:"+coordinate.longitude;
}
}
PositionSource{
id:positionSource
active:true
onPositionChanged:{
console.log("poschanged",
position.coordinate.latitude,
position.coordinate.longitude);
}
}
}
First,weimporttheQtPositioningmoduleintoscope.Next,wecreate
aPositionSourceobjectandsetitsactivepropertytotrue.Theposition
propertyofthePositionSourceobjectwillbeautomaticallyupdatedas
newinformationisavailable.Inadditiontolatitudeandlongitude,
thispropertyalsocontainsinformationaboutaltitude,direction
andspeedoftravel,andaccuracyoflocation.Sincesomeofvalues
maynotbeavailable,eachvalueisaccompaniedwithaBoolean
propertythatindicatesifthedataispresent.Forexample,
ifdirectionValidistrue,thendirectionvaluewasset.
Therearemultiplewaystodeterminetheplayer'slocation.
ThePositionSourcetypehasafewpropertiesthatallowyoutospecify
thesourceofthedata.First,thepreferredPositioningMethodsproperty
allowsyoutochoosebetweensatellitedata,non-satellitedata,or
usingbothofthem.ThesupportedPositioningMethodspropertyholds
informationaboutcurrentlyavailablemethods.Youcanalsouse
thenmeaSourcepropertytoprovideanNMEAposition-specification
datafilewhichoverridesanyotherdatasourcesandcanbeusedto
simulatethedevice'slocationandmovementwhichisveryuseful
duringdevelopmentandtestingofthegame.
CreatingadvancedQML
components
Bynow,youshouldbefamiliarwiththeverybasicsofQMLandQt
Quick.Now,wecanstartcombiningwhatyouknowandfillthe
gapswithmoreinformationtobuildmoreadvancedQML
components.Ourtargetwillbetodisplayananalogclock.
Timeforaction–Asimple
analogclockapplication
CreateanewQtQuickApplication-Emptyproject.Tocreatea
clock,wewillimplementacomponentrepresentingtheclock
needle,andwewilluseinstancesofthatcomponentintheactual
clockelement.Inadditiontothis,wewillmaketheclockareusable
component;therefore,wewillcreateitinaseparatefileand
instantiateitfromwithinmain.qml:
Window{
visible:true
width:640
height:480
title:qsTr("HelloWorld")
Clock{
id:clock
anchors{
fill:parent
margins:20
}
}
}
Weusetheanchorspropertygrouptoexpandtheitemtofitthewhole
windowexceptforthe20-pixelmarginforallfoursides.
Beforethiscodeworks,however,weneedtoaddanewQMLfilefor
theClockcomponent.Locatetheqml.qrcresourcefileintheproject
treeandselectAddNew...initscontextmenu.
FromtheQtcategory,selectQMLFile(QtQuick2),inputClockas
thenameandconfirmtheoperation.AnewfilecalledClock.qmlwill
becreatedandaddedtotheresourceslist.
Let'sstartbydeclaringacircularclockplate:
importQtQuick2.9
Item{
id:clock
propertycolorcolor:"lightgray"
Rectangle{
id:plate
anchors.centerIn:parent
width:Math.min(clock.width,clock.height)
height:width
radius:width/2
color:clock.color
border.color:Qt.darker(color)
border.width:2
}
}
Ifyouruntheprogramnow,you'llseeaplaingraycirclehardly
resemblingaclockplate:
Thenextstepistoaddmarksdividingtheplateinto12sections.We
candothisbyputtingthefollowingdeclarationinside
theplateobject:
Repeater{
model:12
Item{
id:hourContainer
propertyinthour:index
height:plate.height/2
transformOrigin:Item.Bottom
rotation:index*30
x:plate.width/2
y:0
Rectangle{
width:2
height:(hour%3==0)?plate.height*0.1
:plate.height*0.05
color:plate.border.color
antialiasing:true
anchors.horizontalCenter:parent.horizontalCenter
anchors.top:parent.top
anchors.topMargin:4
}
}
}
Runningtheprogramshouldnowgivethefollowingresult,looking
muchmorelikeaclockplate:
Whatjusthappened?
Thecodewejustcreatedintroducesacoupleofnewfeatures.Let's
gothroughthemonebyone.
Firstofall,weusedanewelementcalledRepeater.Itdoesexactly
whatitsnamesays—itrepeatsitemsdeclaredwithinitusingagiven
model.Foreachentryinthemodel,itcreatesaninstanceofa
componentassignedtoapropertycalleddelegate(thepropertyname
meansthatitcontainsanentitytowhichthecallerdelegatessome
responsibility,suchasdescribingacomponenttobeusedasa
stencilbythecaller).ItemdeclaredinRepeaterdescribesthedelegate
eventhoughwecannotseeitexplicitlyassignedtoaproperty.This
isbecausedelegateisadefaultpropertyoftheRepeatertype,which
meansanythingunassignedtoanypropertyexplicitlygetsimplicitly
assignedtothedefaultpropertyofthetype.
TheItemtypealsohasadefaultpropertycalleddata.Itholdsalistofelementsthatgets
automaticallysplitintotwo"sublists"—thelistoftheitem'schildren(whichcreatesthe
hierarchyofIteminstancesinQtQuick)andanotherlistcalledresources,whichcontains
all"child"elementsthatdonotinheritfromItem.Youhavedirectaccesstoallthreelists,
whichmeanscallingchildren[2]willreturnthethirdItemelementdeclaredintheitem,
anddata[5]willreturnthesixthelementdeclaredintheItem,regardlessofwhetherthe
givenelementisavisualitem(thatinheritsItem)ornot.
Themodelcanbeanumberofthings,butinourcase,itissimplya
numberdenotinghowmanytimesthedelegateshouldberepeated.
Thecomponenttoberepeatedisatransparentitemcontaininga
rectangle.Theitemhasapropertydeclaredcalledhourthathas
theindexvariableboundtoit.Thelatterisapropertyassigned
byRepeatertoeachinstanceofthedelegatecomponent.Thevalueit
containsistheindexoftheinstanceintheRepeaterobject—sincewe
haveamodelcontainingtwelveelements,indexwillholdvalues
withinarangeof0to11.Theitemcanmakeuseoftheindexproperty
tocustomizeinstancescreatedbyRepeater.Inthisparticularcase,we
useindextoprovidevaluesforarotationpropertyandbymultiplying
theindexby30,wegetvaluesstartingfrom0forthefirstinstance
andendingat330forthelastone.
Therotationpropertybringsustothesecondmostimportantsubject
—itemtransformations.Eachitemcanbetransformedinanumber
ofways,includingrotatingtheitemandscalingitintwo-
dimensionalspace,aswealreadymentionedearlier.Another
propertycalledtransformOrigindenotestheoriginpointaroundwhich
scaleandrotationareapplied.Bydefault,itpointstoItem.Center,
whichmakestheitemscaleandrotatearounditscenter,butwecan
changeittoeightothervalues,suchasItem.TopLeftforthetop-left
cornerorItem.Rightforthemiddleoftherightedgeoftheitem.Inthe
codewecrafted,werotateeachitemclockwisearounditsbottom
edge.Eachitemispositionedhorizontallyinthemiddleoftheplate
usingtheplate.width/2expressionandverticallyatthetopofthe
platewiththedefaultwidthof0andtheheightofhalftheplate's
height;thus,eachitemisathinverticallinespanningfromwithin
thetoptothecenteroftheplate.Then,eachitemisrotatedaround
thecenteroftheplate(eachitem'sbottomedge)by30degrees
morethanapreviousitemeffectivelylayingitemsevenlyonthe
plate.
Finally,eachitemhasagrayRectangleattachedtothetopedge(offset
by4)andhorizontallycenteredinthetransparentparent.
Transformationsappliedtoaniteminfluencetheitem'schildren
similartowhatwehaveseeninGraphicsView;thus,theeffective
rotationoftherectanglefollowsthatofitsparent.Theheightofthe
rectangledependsonthevalueofhour,whichmapstotheindexof
theiteminRepeater.Here,youcannotuseindexdirectlyasitisonly
visiblewithinthetopmostitemofthedelegate.That'swhywe
createarealpropertycalledhourthatcanbereferencedfromwithin
thewholedelegateitemhierarchy.
Ifyouwantmorecontroloveritemtransformations,thenwearehappytoinformyouthat
apartfromrotationandscaleproperties,eachitemcanbeassignedanarrayofelements
suchasRotation,Scale,andTranslatetoapropertycalledtransform,whichareappliedin
order,oneatatime.Thesetypeshavepropertiesforfine-grainedcontroloverthe
transformation.Forinstance,usingRotation,youcanimplementrotationoveranyofthe
threeaxesandaroundacustomoriginpoint(insteadofbeinglimitedtoninepredefined
originpointsaswhenusingtherotationpropertyofItem).
Timeforaction–Adding
needlestotheclock
Thenextstepistoaddthehour,minute,andsecondneedlestothe
clock.Let'sstartbycreatinganewcomponentcalledNeedleinafile
calledNeedle.qml(rememberthatcomponentnamesandfiles
representingthemneedtostartwithacapitalletter):
importQtQuick2.9
Rectangle{
id:root
propertyintvalue:0
propertyintgranularity:60
propertyaliaslength:root.height
width:2
height:parent.height/2
radius:width/2
antialiasing:true
anchors.bottom:parent.verticalCenter
anchors.horizontalCenter:parent.horizontalCenter
transformOrigin:Item.Bottom
rotation:360/granularity*(value%granularity)
}
Needleisbasicallyarectangleanchoredtothecenterofitsparentby
itsbottomedge,whichisalsotheitem'spivot.Italso
hasthevalueandgranularitypropertiesdrivingtherotationofthe
item,wherevalueisthecurrentvaluetheneedleshows
andgranularityisthenumberofdifferentvaluesitcandisplay.Also,
anti-aliasingfortheneedleisenabledaswewantthetipofthe
needlenicelyrounded.Havingsuchadefinition,wecanusethe
componenttodeclarethethreeneedlesinsidetheclockplate
object:
Needle{
length:plate.height*0.3
color:"blue"
value:clock.hours
granularity:12
}
Needle{
length:plate.height*0.4
color:"darkgreen"
value:clock.minutes
granularity:60
}
Needle{
width:1
length:plate.height*0.45
color:"red"
value:clock.seconds
granularity:60
}
Thethreeneedlesmakeuseofthehours,minutes,andsecondsproperties
ofclock,sotheseneedtobedeclaredaswell:
propertyinthours:0
propertyintminutes:0
propertyintseconds:0
ByassigningdifferentvaluestothepropertiesofClockinmain.qml,you
canmaketheclockshowadifferenttime:
importQtQuick2.9
Clock{
//...
hours:7
minutes:42
seconds:17
}
You'llgetanoutputasshown:
Whatjusthappened?
MostNeedlefunctionalityisdeclaredinthecomponentitself,
includinggeometryandtransformations.Then,wheneverwewant
tousethecomponent,wedeclareaninstanceofNeedleand
optionallycustomizethelengthandcolorpropertiesaswellasset
itsvalueandgranularitytoobtaintheexactfunctionalityweneed.
Timeforaction–Makingthe
clockfunctional
Thefinalstepincreatingaclockistomakeitactuallyshowthe
currenttime.InJavaScript,wecanquerythecurrenttimeusing
theDateobject:
varcurrentDate=newDate();
varhours=currentDate.getHours();
varminutes=currentDate.getMinutes();
varseconds=currentDate.getSeconds();
Therefore,thefirstthingthatcomestomindistousethepreceding
codetoshowthecurrenttimeontheclock:
Item{
id:clock
propertyinthours:currentDate.getHours()
propertyintminutes:currentDate.getMinutes()
propertyintseconds:currentDate.getSeconds()
propertydatecurrentDate:newDate()
//...
}
Thiswillindeedshowthecurrenttimeonceyoustartthe
application,buttheclockwillnotbeupdatingitselfasthetime
passes.ThisisbecausenewDate()returnsanobjectrepresentingone
particularmomentintime(thedateandtimeatthemomentwhen
theobjectwasinstantiated).WhileQMLusuallyiscapableof
automaticallyupdatingapropertywhenthevalueofthebound
expressionchanges,it'sunabletodosointhiscase.EvenifQML
wassmartenoughtoseethatthenewDate()propertyalwaysreturnsa
differentdate,itdoesn'tknowhowoftenwewanttoupdatethe
value,andupdatingitasfrequentlyaspossibleisgenerallyabad
idea.Thus,weneedawaytomanuallyscheduleperiodicexecution
ofanaction.
ToobtainthiseffectinQML,wecanuseaTimerelementthatisan
equivalentofQTimerinC++andletsusperiodicallyexecutesome
code.Let'smodifythecodetouseatimer:
Item{
id:clock
//...
propertyaliasrunning:timer.running
Timer{
id:timer
repeat:true
interval:500
running:true
onTriggered:clock.currentDate=newDate()
}
//...
}
Whatjusthappened?
Bysettingtheintervalproperty,weaskthetimertoemit
thetriggeredsignalevery500ms,causingourcurrentDatepropertyto
beupdatedwithanewDateobjectrepresentingthecurrenttime.
Theclockisalsogiventherunningproperty(pointingtoitsequivalent
inthetimer)thatcancontrolwhetherupdatesshouldbeenabled.
Thetimer'srepeat
propertyissettotrue;otherwise,itwilltriggerjustonce.
Tobrieflysumupwhatyouhavelearnedsofar,wecansaythatyou
knowhowtocreatehierarchiesofobjectsbydeclaringtheir
instances,andyoualsoknowhowtoprogramnewtypesinseparate
files,makingdefinitionsavailableascomponentstobeinstantiated
inotherQMLfiles.YoucanevenusetheRepeaterelementtodeclarea
seriesofobjectsbasedonacommonstencil.
Dynamicandlazyloadingof
QMLobjects
AllourpreviousQMLprojectscontainanexplicitdeclarationofthe
objecttree.Weusuallycreateawindowandplacemultiplespecific
elementsintoitinspecificorder.TheQMLenginecreatesthese
objectsonstartupandkeepsthemaliveuntiltheapplication
terminates.Thisisaveryconvenientapproachthatallowsyouto
savealotoftime,asyoucouldseeinourpreviousexamples.
However,sometimesyouneedtheobjecttreetobemoreflexible—
forexample,ifyoudon'tknowupfrontwhichelementsshouldbe
created.QMLoffersafewwaystocreateobjectsdynamicallyandto
delaycreatinganobjectuntilyoureallyneedit.Utilizingthese
featurescanmakeyourQMLapplicationmoreperformantand
flexible.
Creatingobjectsonrequest
TheproblemwithpredeclaringobjectsdirectlyinaQMLfileisthat
youneedtoknowupfronthowmanyobjectsyouwillneed.More
oftenthannot,youwillwanttodynamicallyaddandremove
objectstoyourscene,forexample,inanalieninvasiongame,
where,astheplayerprogresses,newaliensaucerswillbeentering
thegamescreenandothersaucerswillbegettingshotdownand
destroyed.Also,theplayer'sshipwillbe"producing"newbullets
streakinginfrontoftheship,eventuallyrunningoutoffuelor
otherwisedisappearingfromthegamescene.Byputtingagood
amountofeffortintotheproblem,youwillbeabletouseRepeaterto
obtainthiseffect,butthereisabettertoolathand.
QMLoffersusanotherelementtypecalledComponent,whichis
anotherwaytoteachtheengineaboutanewelementtypeby
declaringitscontentsinQML.Therearebasicallytwoapproaches
todoingthis.
ThefirstapproachistodeclareaComponentelementinstanceinthe
QMLfileandinlinethedefinitionofthenewtypedirectlyinsidethe
element:
Component{
id:circleComponent
Item{
//...
}
}
Theotherapproachistoloadthecomponentdefinitionfroman
existingQMLfile.Let'ssaythatwehaveaCircle.qmlfilewiththe
followingcontent:
importQtQuick2.9
Item{
propertyintdiameter:20
propertyaliascolor:rect.color
propertyaliasborder:rect.border
implicitWidth:diameter
implicitHeight:diameter
Rectangle{
id:rect
width:radius
height:radius
radius:diameter/2
anchors.centerIn:parent
}
}
Suchcodedeclaresacomponentthatdefinesacircleandexposes
itsdiameter,color,andborderproperties.Let'sseehowwecancreate
instancesofthiscomponentdynamically.
QMLexposesaspecialglobalobjectcalledQt,whichprovidesaset
ofinterestingmethods.Oneofthemethodsallowsthecallerto
createacomponentpassingtheURLofanexistingQMLdocument:
varcircleComponent=Qt.createComponent("Circle.qml");
AninterestingnoteisthatcreateComponentcannotonlyacceptalocal
filepathbutalsoaremoteURL,andifitunderstandsthenetwork
scheme(forexample,http),itwilldownloadthedocument
automatically.Inthiscase,youhavetorememberthatittakestime
todothat,sothecomponentmaynotbereadyimmediatelyafter
callingcreateComponent.Sincethecurrentloadingstatusiskeptin
thestatusproperty,youcanconnecttothestatusChangedsignaltobe
notifiedwhenthishappens.Atypicalcodepathlookssimilartothe
following:
Window{
//...
Component.onCompleted:{
varcircleComponent=Qt.createComponent("Circle.qml");
if(circleComponent.status===Component.Ready){
addCircles(circleComponent);
}else{
circleComponent.statusChanged.connect(function(){
if(circleComponent.status===Component.Ready){
addCircles(circleComponent);
}
});
}
}
}
Inthisexample,weusetheComponent.onCompletedhandlertorunthe
codeassoonasthewindowobjectiscreated.Thishandleris
availableinallQtQuickitemsandisoftenusedtoperform
initialization.Youcanalsouseanyothersignalhandlerhere.For
example,youcanstartloadingthecomponentwhenabuttonis
pressedoratimerhasexpired.
Thecounterpartofthecompleted()signalofComponentisdestruction().Youcanuse
theComponent.onDestructionhandlertoperformactionssuchassavingthestateofthe
objecttopersistentstorageorotherwisecleaningtheobjectup.
Ifthecomponentdefinitionisincorrectorthedocumentcannotbe
retrieved,thestatusoftheobjectwillchangetoError.Inthatcase,
youcanmakeuseoftheerrorString()methodtoseewhattheactual
problemis:
if(circleComponent.status===Component.Error){
console.warn(circleComponent.errorString());
}
Onceyouaresurethatthecomponentisready,youcanfinallystart
creatingobjectsfromit.Forthis,thecomponentexposesamethod
calledcreateObject.Initssimplestform,itacceptsanobjectthatisto
becometheparentofthenewlyborninstance(similartowidget
constructorsacceptingapointertoaparentwidget)andreturnsthe
newobjectitselfsothatyoucanassignittosomevariable.Then,
youcanstartsettingtheobject'sproperties:
Window{
//...
ColumnLayout{
id:layout
anchors.fill:parent
}
functionaddCircles(circleComponent){
["red","yellow","green"].forEach(function(color){
varcircle=circleComponent.createObject(layout);
circle.color=color;
circle.Layout.alignment=Qt.AlignCenter;
});
}
//...
}
Amorecomplexinvocationletsusdoboththeseoperations(create
theobjectandsetitsproperties)inasinglecallbypassingasecond
parametertocreateObject:
varcircle=circleComponent.createObject(layout,
{diameter:20,color:'red'});
ThesecondparameterisaJavaScriptobjectwhosepropertiesareto
beappliedtotheobjectbeingcreated.Theadvantageofthelatter
syntaxisthatallpropertyvaluesareappliedtotheobjectasone
atomicoperationandtheywon'ttriggerpropertychangehandlers
(justlikewhentheitemisdeclaredinaQMLdocument)insteadof
aseriesofseparateoperations,eachofwhichsetsthevaluefora
singleproperty,possiblycausinganavalancheofchangehandler
invocationsintheobject.
Aftercreation,theobjectbecomesafirst-classcitizenofthescene,
actinginthesamewayasitemsdeclareddirectlyintheQML
document.Theonlydifferenceisthatadynamicallycreatedobject
canalsobedynamicallydestructedbycallingitsdestroy()method,
whichissimilartocallingdeleteonC++objects.Whenspeakingof
destroyingdynamicitems,wehavetopointoutthatwhenyou
assignaresultofcreateObjecttoavariable(likecircle,inourexample)
andthatvariablegoesoutofscope,theitemwillnotbereleasedand
garbagecollectedasitsparentstillholdsareferencetoit,
preventingitfrombeingrecycled.
Wedidn'tmentionthisexplicitlybefore,butwehavealreadyused
inlinecomponentdefinitionsearlierinthischapterwhenwe
introducedtheRepeaterelement.Therepeateditemdefinedwithin
therepeaterisinfactnotarealitem,butacomponentdefinition
thatisinstantiatedasmanytimesasneededbytherepeater.
Delayingitemcreation
Anotherrecurringscenarioisthatyoudoknowhowmanyelements
youwillneed,buttheproblemisthatyoucannotdetermineupfront
whattypeofelementstheywillbe.Atsomepointduringthe
lifetimeofyourapplication,youwilllearnthatinformationandwill
beabletoinstantiateanobject.Untilyougaintheknowledgeabout
thegivencomponent,youwillneedsomekindofitemplaceholder
whereyouwilllaterputtherealitem.Youcan,ofcourse,write
somecodetousethecreateObject()functionalityofthecomponent,
butthisiscumbersome.
Fortunately,QtQuickoffersanicersolutionintheformof
aLoaderitem.Thisitemtypeisexactlywhatwedescribedittobe—a
temporaryplaceholderforarealitemthatwillbeloadedon
demandfromanexistingcomponent.YoucanputLoaderinplaceof
anotheritemandwhenyouneedtocreatethisitem,onewayisto
settheURLofacomponenttothesourceproperty:
Loader{
id:loader
}
//...
onSomeSignal:loader.source="Circle.qml"
YoucanalsodirectlyattacharealcomponenttothesourceComponentof
aLoader:
Loader{
id:loader
sourceComponent:shouldBeLoaded?circleComponent:undefined
}
Immediatelyafterward,themagicbeginsandaninstanceofthe
componentappearsintheloader.IftheLoaderobjecthasitssizeset
explicitly(forexample,byanchoringorsettingthewidthand
height),thentheitemwillberesizedtothesizeoftheloader.Ifan
explicitsizeisnotset,thenLoaderwillinsteadberesizedtothesizeof
theloadedelementoncethecomponentisinstantiated.Inthe
followingcode,theloaderhasitssizesetexplicitly,sowhenitsitem
iscreated,itwillrespecttheanchorsandsizesdeclaredhere:
Loader{
anchors{
left:parent.left
leftMargin:0.2*parent.width
right:parent.right
verticalCenter:parent.verticalCenter
}
height:250
source:"Circle.qml"
}
ImperativepaintingonCanvas
usingJavaScript
Declaringgraphicalitemsisniceandeasy,butasprogrammers,
we'remoreusedtowritingimperativecode,andsomethingsare
easierexpressedasanalgorithmratherthanasadescriptionofthe
finalresulttobeachieved.ItiseasytouseQMLtoencodea
definitionofaprimitiveshapesuchasarectangleinacompactway
—allweneedistomarktheoriginpointoftherectangle,itswidth,
height,andoptionally,acolor.Writingdownadeclarative
definitionofacomplexshapeconsistingofmanycontrolpoints
positionedingivenabsolutecoordinates,possiblywithanoutlinein
somepartsofit,maybeaccompaniedbyanimageortwo,isstill
possibleinalanguagesuchasQML;however,thiswillresultina
muchmoreverboseandmuchlessreadabledefinition.Thisisa
casewhereusinganimperativeapproachmightprovemore
effective.HTML(beingadeclarativelanguage)alreadyexposesa
provenimperativeinterfacefordrawingdifferentprimitivescalled
aCanvasthatiswidelyusedinwebapplications.Fortunately,Qt
QuickprovidesuswithitsownimplementationofaCanvasinterface
similartotheonefromthewebbylettingusinstantiateCanvasitems.
Suchitemscanbeusedtodrawstraightandcurvedlines,simple
andcomplexshapes,andgraphsandgraphicimages.Itcanalso
addtext,colors,shadows,gradients,andpatterns.Itcaneven
performlow-levelpixeloperations.Finally,theoutputmaybesaved
asanimagefileorserializedtoaURLusableassourcefor
anImageitem.Therearemanytutorialsandpapersavailableout
thereonusinganHTMLcanvas,andtheycanusuallybeeasily
appliedtoaQtQuickcanvasaswell(thereferencemanualeven
includesalistofaspectsyouneedtopayattentiontowhenporting
HTMLcanvasapplicationstoaQtQuickCanvas),soherewewill
justgiveyoutheverybasicsofimperativedrawinginQtQuick.
Consideragamewheretheplayer'shealthismeasuredbythe
conditionofhisheart—theslowerthebeat,thehealthiertheplayer
is.Wewillusethiskindofvisualizationasourexerciseinpracticing
paintingusingtheCanvaselement.
Timeforaction–Preparing
Canvasforheartbeat
visualization
Let'sstartwithsimplethingsbycreatinganemptyQtQuickproject.
Addthefollowingcodetothemain.qmlfile:
Window{
//...
Canvas{
id:canvas
implicitWidth:600
implicitHeight:300
onPaint:{
varctx=canvas.getContext("2d");
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.strokeRect(50,50,100,100);
}
}
}
Whenyouruntheproject,youwillseeawindowcontaininga
rectangle:
Whatjusthappened?
Intheprecedingcode,wecreatedabasicboilerplatecodeforusing
acanvas.First,wecreatedaCanvasinstancewithanimplicitwidth
andheightset.There,wecreatedahandlerforthepaintsignalthatis
emittedwheneverthecanvasneedstoberedrawn.Thecodeplaced
thereretrievesacontextforthecanvas,whichcanbethoughtofas
anequivalenttotheQPainterinstanceweusedwhendrawingonQt
widgets.Weinformthecanvasthatwewantits2Dcontext,which
givesusawaytodrawintwodimensions.A2Dcontextistheonly
contextcurrentlypresentfortheCanvaselement,butyoustillhaveto
identifyitexplicitly—similartoHTML.Havingthecontextready,
wetellittoclearthewholeareaofthecanvas.Thisisdifferentfrom
thewidgetworldinwhichwhenthepaintEventhandlerwascalled,the
widgetwasalreadyclearedforusandeverythinghadtoberedrawn
fromscratch.WithCanvas,itisdifferent;thepreviouscontentiskept
bydefaultsothatyoucandrawoveritifyouwant.Sincewewantto
startwithacleansheet,wecallclearRect()onthecontext.Finally,we
usethestrokeRect()conveniencemethodthatdrawsarectangleon
thecanvas.
Timeforaction-drawinga
heartbeat
Wewillextendourcomponentnowandimplementitsmain
functionality—drawingaheartbeat-likediagram.
Addthefollowingpropertydeclarationstothecanvasobject:
propertyintlineWidth:2
propertyvarpoints:[]
propertyrealarg:-Math.PI
InsidetheCanvassection,addadeclarationforatimerthatwill
triggerupdatesofthepicture:
Timer{
interval:10
repeat:true
running:true
onTriggered:{
canvas.arg+=Math.PI/180;
while(canvas.arg>=Math.PI){
canvas.arg-=2*Math.PI;
}
}
}
Thenagain,insidetheCanvassection,definethehandlerforwhenthe
valueofargismodified:
onArgChanged:{
points.push(func(arg));
points=points.slice(-canvas.width);
canvas.requestPaint();
}
ThishandlerusesacustomJavaScriptfunction—func().Placethe
implementationofthefunctionjustabovethehandler:
functionfunc(argument){
vara=(2*Math.PI/10);
varb=4*Math.PI/5;
returnMath.sin(20*argument)*(
Math.exp(-Math.pow(argument/a,2))+
Math.exp(-Math.pow((argument-b)/a,2))+
Math.exp(-Math.pow((argument+b)/a,2))
);
}
Finally,modifytheonPaintsignalhandler:
onPaint:{
varctx=canvas.getContext("2d");
ctx.reset();
ctx.clearRect(0,0,canvas.width,canvas.height);
varpointsToDraw=points.slice(-canvas.width);
ctx.translate(0,canvas.height/2);
ctx.beginPath();
ctx.moveTo(0,-pointsToDraw[0]*canvas.height/2);
for(vari=1;i<pointsToDraw.length;i++){
ctx.lineTo(i,-pointsToDraw[i]*canvas.height/2);
}
ctx.lineWidth=canvas.lineWidth;
ctx.stroke();
}
Then,youcanrunthecodeandseeaheartbeat-likediagramappear
onthecanvas:
Whatjusthappened?
Weaddedtwokindsofpropertiestotheelement.By
introducinglineWidth,wecanmanipulatethewidthofthelinethat
visualizestheheartbeat.Thepointsvariablestoresanarrayof
alreadycalculatedfunctionvalues.Weinitializeittoanempty
array.Theargvariablestoresthefunctionargumentthatwaslast
evaluated.Theargumentofthefunctionshouldbeintherange
from−πto+π;thus,weinitializeargto-Math.PI.Then,weaddatimer
thatticksinregularintervals,incrementingargby1°untilitreaches
+π,inwhichcaseitisresettotheinitialvalue.
Changestoargareinterceptedinthehandlerweimplementnext.In
there,wepushanewitemtothearrayofpoints.Thevalueis
calculatedbythefuncfunction,whichisquitecomplicated,butitis
sufficienttosaythatitreturnsavaluefromwithinarange
of−1to+1.Theoldestrecordsareremovedfromthearrayofpoints
usingArray.slice()sothatatmost,thelastcanvas.widthitemsremainin
thearray.Thisissothatwecanplotonepointforeachpixelofthe
widthofthecanvas,andwedon'thavetostoreanymoredatathan
required.Attheendofthefunction,weinvokerequestPaint(),whichis
anequivalentofQWidget::update()andschedulesarepaintofthe
canvas.
That,inturn,callsouronPaintsignalhandler.There,afterretrieving
thecontext,weresetthecanvastoitsinitialstateandthencalculate
anarrayofpointsthatistobedrawnagainusingslice().Then,we
preparethecanvasbytranslatingandscalingitintheverticalaxis
sothattheoriginismovedtohalfoftheheightofthecanvas(that's
thereasonforcallingreset()atthebeginningoftheprocedure—to
revertthistransformation).Afterthat,beginPath()iscalledtoinform
thecontextthatwearestartingtobuildanewpath.Then,thepath
isbuiltsegmentbysegmentbyappendinglines.Eachvalueis
multipliedbycanvas.height/2sothatvaluesfromthepointarrayare
scaledtothesizeoftheitem.Thevalueisnegatedasthevertical
axisofthecanvasgrowstothebottom,andwewantpositivevalues
tobeabovetheoriginline.Afterthat,wesetthewidthofthepen
anddrawthepathbycallingstroke().
Timeforaction–Hiding
properties
IfweconvertourheartbeatcanvastoaQMLcomponent,
thepointsandargpropertieswillbethepublicpropertiesvisibleto
theuserofthecomponent.However,theyarereally
implementationdetailswewanttohide.Weshouldonlyexpose
propertiesthatmakesensetotheuserofthecomponent,such
aslineWidthorcolor.
SincetheTimerobjectinsidetheCanvasisnotexportedaspublic
property,thattimerobjectwillbeunavailablefromtheoutside,so
wecanattachpropertiestothetimerinsteadofattachingthemto
thetop-levelCanvasobject.However,thepropertiesdonotbelongto
thetimerlogically,sothissolutionwillbeconfusing.Forsuchcases,
thereisaconventionthatyoushouldcreateanemptyQtObjectchild
inthetop-levelobjectandmovepropertiesintoit:
Canvas{
id:canvas
propertyintlineWidth:2
//...
QtObject{
id:d
propertyvarpoints:[]
propertyrealarg:-Math.PI
functionfunc(argument){/*...*/}
onArgChanged:{/*...*/}
}
//...
}
QtObjectistheQMLrepresentationoftheQObjectclass.ItisaQML
typethatdoesn'thaveanyparticularfunctionality,butcanhold
properties.Aspartoftheconvention,wesetidofthisobjecttod.
TheonArgChangedhandlerismovedtotheprivateobjectaswell.In
theonTriggeredandonPainthandlers,weshouldnowrefertothe
internalpropertiesasd.pointsandd.arg.Considerthisexample:
onTriggered:{
d.arg+=Math.PI/180;
while(d.arg>=Math.PI){
d.arg-=2*Math.PI;
}
}
Thepointsandargpropertiesarenowunavailablefromtheoutside,
leadingtocleanpublicinterfaceofourheartbeatobject.
Timeforaction–Makingthe
diagrammorecolorful
Thediagramservesitspurpose,butitlooksabitdull.Addsome
shinetoitbydefiningthreenewcolorpropertiesinthecanvas
object—color,topColor,bottomColor—andsettingtheirdefaultvalues
toblack,red,andblue,respectively:
propertycolorcolor:"black"
propertycolortopColor:"red"
propertycolorbottomColor:"blue"
Then,let'smakeuseofthesepropertiesby
extendingonPaintimplementation:
onPaint:{
//...
//fill:
ctx.beginPath();
ctx.moveTo(0,0);
vari;
for(i=0;i<pointsToDraw.length;i++){
ctx.lineTo(i,-pointsToDraw[i]*canvas.height/2);
}
ctx.lineTo(i,0);
vargradient=ctx.createLinearGradient(
0,-canvas.height/2,0,canvas.height/2);
gradient.addColorStop(0.1,canvas.topColor);
gradient.addColorStop(0.5,Qt.rgba(1,1,1,0));
gradient.addColorStop(0.9,canvas.bottomColor);
ctx.fillStyle=gradient;
ctx.fill();
//stroke:
ctx.beginPath();
ctx.moveTo(0,-pointsToDraw[0]*canvas.height/2);
for(i=1;i<pointsToDraw.length;i++){
for(i=1;i<pointsToDraw.length;i++){
ctx.lineTo(i,-pointsToDraw[i]*canvas.height/2);
}
ctx.lineWidth=canvas.lineWidth;
ctx.strokeStyle=canvas.color;
ctx.stroke();
}
Uponrunningtheprecedingcodesnippet,yougetthefollowing
output:
Whatjusthappened?
ThemodificationstoonPaintthatweimplementedarecreating
anotherpathandusingthatpathtofillanareausingagradient.
Thepathisverysimilartotheoriginalone,butitcontainstwo
additionalpointsthatarethefirstandlastpointsdrawnprojected
ontothehorizontalaxis.Thisensuresthatthegradientfillsthearea
properly.Notethatthecanvasusesimperativecodefordrawing;
therefore,theorderofdrawingthefillandthestrokematters—the
fillhastobedrawnfirstsothatitdoesn'tobscurethestroke.
UsingC++classesasQML
components
Inthenextexercise,wewillimplementacardashboardthatcanbe
usedinaracinggameandwillshowanumberofparameterssuch
ascurrentspeedandmotorrevolutionsperminute.Theinputdata
willbeprovidedbyaC++object.We'llseehowtoincludethis
objectintotheQMLobjecttreeandusepropertybindingsto
implementthedashboard.
Thefinalresultwilllooksimilartothefollowing:
Timeforaction–Self-updating
cardashboard
WewillstartwiththeC++part.SetupanewQtQuickapplication.
Thiswillgeneratethemainfunctionforyouthat
instantiatesQGuiApplicationandQQmlApplicationEngineandsetsthemup
toloadaQMLdocument.
UsetheFilemenutocreateNewfileorProjectandcreateanewQt
Designerformclass.CallitCarInfoandchoosetheWidgettemplate.We
willusethisclassformodifyingvaluesofdifferentparametersso
thatwemayobservehowtheyinfluencewhattheQtQuickscene
displays.Intheclassdeclaration,addthefollowingproperties:
classCarInfo:publicQWidget{
Q_OBJECT
Q_PROPERTY(intrpmREADrpmNOTIFYrpmChanged)
Q_PROPERTY(intgearREADgearNOTIFYgearChanged)
Q_PROPERTY(intspeedREADspeedNOTIFYspeedChanged)
Q_PROPERTY(doubledistanceREADdistanceNOTIFYdistanceChanged)
//...
};
Thepropertiesareread-only,andtheNOTIFYclausedefinessignals
emittedwhentherespectivepropertyvalueschange.Goaheadand
implementtheappropriatefunctionsforeachproperty.Apartfrom
thegetter,alsoimplementasetterasapublicslot.Here'san
exampleforapropertycontrollingthespeedofthecar:
intCarInfo::speed()const{
returnm_speed;
}
voidCarInfo::setSpeed(intnewSpeed){
if(m_speed==newSpeed){
if(m_speed==newSpeed){
return;
}
m_speed=newSpeed;
emitspeedChanged(m_speed);
}
Youshouldbeabletofollowtheexamplefortheremaining
propertiesonyourown.
Sincewewanttousethewidgettotweakpropertyvalues,design
theuserinterfaceforitusingtheformeditor.Itcanlooklikethis:
Makeappropriatesignal-slotconnectionsinthewidgetsothat
modifyinganyofthewidgetsforagivenparameterorusingthe
setterslotdirectlyupdatesallthewidgetsforthatparameter.
InsteadofaddingmembervariablestotheCarInfoclassforpropertiessuch
asspeed,rpm,distance,orgear,youcanoperatedirectlyonthewidgetsplacedon
theuiform,asshownfurther.
Forexample,agetterforthedistancepropertywilllooklikethis:
qrealCarInfo::distance()const
{
returnui->distanceBox->value();
}
Thesetterwouldthenbemodifiedtothefollowing:
voidCarInfo::setDistance(qrealnewDistance)
{
ui->distanceBox->setValue(newDistance);
}
Youwillthenneedtoaddconnect()statementstotheconstructorto
ensurethatsignalsarepropagatedfromtheuiform:
connect(ui->distanceBox,SIGNAL(valueChanged(double)),
this,SIGNAL(distanceChanged(double)));
Next,youcantestyourworkbyrunningthewidget.Todothis,you
havetoalterthemainfunctiontolookasfollows:
intmain(intargc,char**argv){
QApplicationapp(argc,argv);
CarInfocinfo;
cinfo.show();
returnapp.exec();
};
Sinceweareusingwidgets,wehavetoreplaceQGuiApplicationwith
QApplicationandenablethewidgetsmodulebyplacingQT+=widgetsin
theprojectfile(remembertorunqmakefromtheproject'scontext
menuafterward).Ensurethateverythingworksasexpected(thatis,
thatmovingslidersandchangingspinboxvaluesreflectthechanges
towidgetproperties)beforemovingontothenextstep.
WewillnowaddQtQuicktotheequation,solet'sstartbyupdating
ourmainfunctiontodisplayourscene.Introducethehighlighted
changestothecode:
intmain(intargc,char**argv){
QApplicationapp(argc,argv);
CarInfocinfo;
QQmlApplicationEngineengine;
engine.rootContext()->setContextProperty("carData",&cinfo);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if(engine.rootObjects().isEmpty())
return-1;
cinfo.show();
returnapp.exec();
}
ThemodificationscreateaQMLengineforourscene,export
theCarInfoinstancetotheglobalcontextoftheQMLengine,and
loadanddisplaythescenefromafilelocatedinaresource.
Itisimportanttofirstexportalltheobjectsandonlythenloadthe
scene.Thisisbecausewewantallthenamestobealready
resolvablewhenthesceneisbeinginitializedsothattheycanbe
usedrightaway.Ifwereversedtheorderofcalls,wewouldgeta
numberofwarningsontheconsoleabouttheidentitiesbeing
undefined.
Finally,wecanfocusontheQMLpart.Lookatthepictureofthe
resultwewanttobeshownatthebeginningoftheexercise.Forthe
blackbackground,weusedabitmapimagecreatedinagraphical
editor(youcanfindthefileinthematerialsforthisbook),butyou
canobtainasimilareffectbycomposingthreeblackrounded
rectanglesdirectlyinQtQuick—thetwoouterpartsareperfect
circles,andtheinnermoduleisahorizontallystretchedellipse.
Ifyoudecidetouseourbackgroundfile(ormakeyourownprettier
image),youshouldaddittotheproject'sresourcesandputthe
followingcodeintomain.qml:
importQtQuick2.9
importQtQuick.Window2.3
Window{
visible:true
width:backgroundImage.width
height:backgroundImage.height
Image{
id:backgroundImage
source:"qrc:/dashboard.png"
source:"qrc:/dashboard.png"
Item{
id:leftContainer
anchors.centerIn:parent
anchors.horizontalCenterOffset:-550
width:400;height:width
}
Item{
id:middleContainer
anchors.centerIn:parent
width:700;height:width
}
Item{
id:rightContainer
anchors.centerIn:parent
anchors.horizontalCenterOffset:525
width:400;height:width
}
}
}
Whatwedohereisaddtheimagetothewindowandcreatethree
itemstoserveascontainersfordifferentelementsofthedashboard.
Thecontainersareallcenteredintheparent,andweuse
ahorizontalCenterOffsetpropertytomovethetwoouteritemssideways.
Thevaluesoftheoffsetaswellasthewidthsarebasedonthe
backgroundimage'sgeometry(notethatallthreecontainersare
perfectsquares).Ifinsteadofusingourfile,yousettleforcreating
thethreepartsyourselfusingQtQuickitems,thecontainerswill
simplybeanchoredtothecentersofthethreeblackitems.
Thedialslookcomplicated,butinreality,theyareveryeasyto
implement,andyouhavealreadylearnedeverythingyouneedto
designthem.
Let'sstartwiththeneedle.Usethecontextmenuoftheresourcefile
tocreateanewQMLfileandcallitNeedle.qml.Openthefileandplace
thefollowingcontent:
importQtQuick2.9
Item{
id:root
propertyintlength:parent.width*0.4
propertycolorcolor:"white"
propertycolormiddleColor:"red"
propertyintsize:2
Rectangle{//needle
width:root.size
height:length+20
color:root.color
anchors.horizontalCenter:parent.horizontalCenter
anchors.bottom:parent.bottom
anchors.bottomMargin:-20
antialiasing:true
}
Rectangle{//fixing
anchors.centerIn:parent
width:8+root.size
height:width
radius:width/2
color:root.color
Rectangle{//middledot
anchors{
fill:parent
margins:parent.width*0.25
}
color:root.middleColor
}
}
}
Thedocumentdefinesanitemwithfourattributes—thelengthof
theneedle(defaultsto80%ofthedial'sradius),thecolorofthe
needle,middleColor,whichstandsforthecoloroftheneedle'sfixing,
andthesize,whichdefineshowwidetheneedleis.Thecodeisself-
explanatory.Theitemitselfdoesnothaveanydimensionsandonly
actsasananchorforvisualelements—theneedleitselfisathin
rectangleorientedverticallywithafixing20unitsfromtheend.
Thefixingisacircleofthesamecolorastheneedlewithasmaller
circleinthemiddlethatusesadifferentfillcolor.Thesmaller
radiusoftheinnercircleisobtainedbyfillingtheoutercirclewitha
25%marginfromeachside.
Asforthedials,wewillputtheircodeinlineinthemainfilesince
wejusthavetwoofthemandtheydifferabit,sotheoverheadof
creatingaseparatecomponentwithawell-designedsetof
propertieswilloutweighthebenefitsofhavingnicelyencapsulated
objects.
Ifyouthinkaboutwhatneedstobedonetohavethedialdisplayed
andworking,itseemsthatthehardestthingistolayoutthe
numbersnicelyonthecircle,solet'sstartwiththat.Here'san
implementationofafunctionforcalculatingthepositionalonga
circlecircumference,basedontheradiusofthecircleandangle(in
degrees)whereanitemshouldbepositioned:
functioncalculatePosition(angle,radius){
if(radius===undefined){
radius=width/2*0.8;
}
vara=angle*Math.PI/180;
varpx=width/2+radius*Math.cos(a);
varpy=width/2+radius*Math.sin(a);
returnQt.point(px,py);
}
Thefunctionconvertsdegreestoradiansandreturnsthedesired
point.Thefunctionexpectsthewidthpropertytobeavailablethat
helpscalculatethecenterofthecircleandincasearadiuswasnot
given,servesasameanstocalculateafeasiblevalueforit.
Withsuchafunctionavailable,wecanusethealready
familiarRepeaterelementtopositionitemswherewewantthem.Let's
putthefunctioninmiddleContaineranddeclarethedialforcarspeed:
Item{
id:middleContainer
//...
functioncalculatePosition(angle,radius){/*...*/}
Repeater{
model:24/2
Item{
propertypointpt:
propertypointpt:
middleContainer.calculatePosition(120+index*12*2)
x:pt.x
y:pt.y
Label{
anchors.centerIn:parent
text:index*20
}
}
}
Needle{
anchors.centerIn:parent
length:parent.width*0.35
size:4
rotation:210+(carData.speed*12/10)
color:"yellow"
}
}
YoumighthavenotedthatweusedanelementcalledLabel.We
createdittoavoidhavingtosetthesamepropertyvaluesforallthe
textsweuseintheuserinterface:
importQtQuick2.9
Text{
color:"white"
font.pixelSize:24
}
Thedialconsistsofarepeaterthatwillcreate12elements.Each
elementisanitempositionedusingtheearlierdescribedfunction.
Theitemhasalabelanchoredtoitthatdisplaysthegivenspeed.
Weuse120+index*12*2astheangleexpressionaswewant"0"to
bepositionedat120degreesandeachfollowingitempositioned24
degreesfurther.
Theneedleisgivenrotationbasedonthevaluereadfrom
thecarDataobject.Sincetheangulardistancebetweenconsecutive20
kphlabelsis24degrees,thedistanceforonekphis1.2andthuswe
multiplycarData.speedbythatfactor.Itemrotationiscalculatedwith0
degrees"pointingright";therefore,weadd90totheinitial120
degreeoffsetofthefirstlabeltoobtainstartingcoordinates
matchingthoseofthelabelsystem.
Asyoucanseeintheimageofthefinalresultatthebeginningof
thissection,thespeeddialcontainssmalllinesevery2kph,with
thosedivisibleby10kphlongerthanothers.Wecanuse
anotherRepeatertodeclaresuchticks:
Repeater{
model:120-4
Item{
propertypointpt:middleContainer.calculatePosition(
120+index*1.2*2,middleContainer.width*0.35
)
x:pt.x
y:pt.y
Rectangle{
width:2
height:index%5?5:10
color:"white"
rotation:210+index*1.2*2
anchors.centerIn:parent
antialiasing:true
}
}
}
Finally,wecanputalabelforthedial:
Text{
anchors.centerIn:parent
anchors.verticalCenterOffset:40
text:"SPEED\n[kph]"
horizontalAlignment:Text.AlignHCenter
color:"#aaa"
font.pixelSize:16
}
Ensurethatthelabelisdeclaredbeforethedialneedle,orgivethe
needleahigherzvaluesothatthelabeldoesn'toverpaintthe
needle.
Next,repeattheprocessonyourownfortheleftcontainerby
creatinganRPMdialreadingvaluesfromthecarData.rpmproperty.
Thedialalsodisplaysthecurrentgearofthecar'sengine.Placethe
followingcodeinsidetheleftContainerobjectdefinition:
Item{
id:gearContainer
anchors.centerIn:parent
anchors.horizontalCenterOffset:10
anchors.verticalCenterOffset:-10
Text{
id:gear
propertyintvalue:carData.gear
propertyvargears:[
"R","N",
"1<sup>st</sup>","2<sup>nd</sup>","3<sup>rd</sup>",
"4<sup>th</sup>","5<sup>th</sup>"
]
text:gears[value+1]
anchors.left:parent.left
anchors.bottom:parent.bottom
color:"yellow"
font.pixelSize:32
textFormat:Text.RichText
}
}
Theonlypartneedingexplanationishighlighted.Itdefinesanarray
ofgearlabelsstartingwithreverse,goingthroughneutral,andthen
throughfiveforwardgears.Thearrayisthenindexedwiththe
currentgearandthetextforthatvalueisappliedtothelabel.Note
thatthevalueisincrementedby1,whichmeansthe0thindexofthe
arraywillbeusedwhencarData.gearissetto1.
Wewillnotshowhowtoimplementtherightcontainer.Youcando
thateasilyyourselfnowwiththeuseoftheGridpositionertolayout
thelabelsandtheirvalues.Todisplaytheseriesofcontrolsonthe
bottomoftherightcontainer(withtextsABS,ESP,BRK,andCHECK),you
canuseRowofLabelinstances.
Now,starttheprogramandbeginmovingtheslidersonthewidget.
SeehowtheQtQuickscenefollowsthechanges.
Whatjusthappened?
WehavecreatedaverysimpleQObjectinstanceandexposeditasour
"datamodel"toQML.Theobjecthasanumberofpropertiesthat
canreceivedifferentvalues.Changingavalueresultsinemittinga
signal,whichinturnnotifiestheQMLengineandcausesbindings
containingthosepropertiestobereevaluated.Asaresult,ouruser
interfacegetsupdated.
Timeforaction–Grouping
engineproperties
ThedatainterfacebetweentheQMLandC++worldsthatwe
createdisverysimpleandhasasmallnumberofproperties.
However,astheamountofdatawewanttoexposegrows,theobject
canbecomecluttered.Ofcourse,wecancounterthateffectby
dividingitintomultiplesmallerobjects,eachhavingseparate
responsibilitiesandthenexportingallthoseobjectstoQML,but
thatisnotalwaysdesirable.Inourcase,wecanseethatrpmandgear
arepropertiesoftheenginesubsystem,sowecanmovethemtoa
separateobject;however,inreality,theirvaluesaretightlycoupled
withthespeedofthecarandtocalculatethespeed,wewillneedto
knowthevaluesofthosetwoparameters.However,thespeedalso
dependsonotherfactorssuchastheslopeoftheroad,soputting
thespeedintotheenginesubsystemobjectjustdoesn'tseemright.
Fortunately,thereisanicesolutiontothatproblem.
QMLhasaconceptcalledgroupedproperties.Youalreadyknowa
numberofthem—theborderpropertyoftheRectangleelementorthe
anchorspropertyoftheItemelement,forexample.Let'sseehowto
definesuchpropertiesforourexposedobject.
CreateanewQObject-derivedclassandcallitCarInfoEngine.Movethe
propertydefinitionsofrpmandgeartothatnewclassalongwiththeir
getters,setters,andchangesignals.Addthefollowingproperty
declarationtoCarInfo:
Q_PROPERTY(QObject*engineREADengineNOTIFYengineChanged)
Implementthegetterandtheprivatefield:
QObject*engine()const{returnm_engine;}
private:
CarInfoEngine*m_engine;
Wewillnotusethesignalrightnow.However,wehadtodeclareit;
otherwise,QMLwouldcomplainwewerebindingexpressionsthat
dependonpropertiesthatarenon-notifiable:
signals:
voidengineChanged();
Initializem_engineintheconstructorofCarInfo:
m_engine=newCarInfoEngine(this);
Next,updatethecodeofCarInfotomodifyproperties
ofm_enginewheneverrespectiveslidersonthewidgetaremoved.
Providealinktheotherwayaswell—ifthepropertyvalueis
changed,updatetheuserinterfaceaccordingly.
UpdatetheQMLdocumentand
replacecarData.gearwithcarData.engine.gear.Dothesame
forcarData.rpmandcarData.engine.rpm.Youshouldendupwithsomething
alongthelinesofthefollowing:
Item{
id:leftContainer
//...
Item{
id:gearContainer
Text{
id:gear
propertyintvalue:carData.engine.gear
//...
}
}
Needle{
anchors.centerIn:parent
anchors.centerIn:parent
length:parent.width*0.35
rotation:210+(carData.engine.rpm*35)
}
}
Whatjusthappened?
Essentially,whatwedidisexposeapropertyinCarInfothatisitself
anobjectthatexposesasetofproperties.Thisobjectofthe
CarInfoEnginetypeisboundtotheCarInfoinstanceitrefersto.
Timeforaction–Registering
C++classasQMLtype
Sofar,whatwedidwasexposeourselvestoQMLsingleobjects
createdandinitializedinC++.However,wecandomuchmore—the
frameworkallowsustodefinenewQMLtypes.Thesecaneitherbe
genericQObject-derivedQMLelementsoritemsspecializedforQt
Quick.
Wewillstartwithsomethingsimple—exposingtheCarInfotypeto
QMLsothatinsteadofinstantiatingitinC++andthenexposingit
inQML,wecandirectlydeclaretheelementinQMLandstillallow
thechangesmadetothewidgettobereflectedinthescene.
Tomakeacertainclass(derivedfromQObject)instantiableinQML,
allthatisrequiredistoregisterthatclasswiththedeclarative
engineusingtheqmlRegisterTypetemplatefunction.Thisfunctiontakes
theclassasitstemplateparameteralonganumberoffunction
arguments:themoduleuri,themajorandminorversionnumbers,
andthenameoftheQMLtypeweareregistering.Thefollowingcall
willregistertheFooClassclassastheQMLtypeFoo,availableafter
importingfoo.bar.bazinVersion1.0:
qmlRegisterType<FooClass>("foo.bar.baz",1,0,"Foo");
YoucanplacethisinvocationanywhereinyourC++code;just
ensurethatthisisbeforeyoutrytoloadaQMLdocumentthat
mightcontaindeclarationsofFooobjects.Atypicalplacetoputthe
functioncallisintheprogram'smainfunction.Afterward,youcan
startdeclaringobjectsoftheFootypeinyourdocuments.Just
rememberthatyouhavetoimporttherespectivemodulefirst:
importQtQuick2.9
importfoo.bar.baz1.0
Item{
Foo{
id:foo
}
}
Timeforaction–Making
CarInfoinstantiablefromQML
First,wewillupdatetheQMLdocumenttocreateaninstance
ofCarInfopresentintheCarInfo1.0module:
importQtQuick2.9
importCarInfo1.0
Image{
source:"dashboard.png"
CarInfo{
id:carData
visible:true//makethewidgetvisible
}
//...
}
AsforregisteringCarInfo,itmightbetemptingtosimply
callqmlRegisterTypeonCarInfoandcongratulateourselvesforajobwell
done:
intmain(intargc,char**argv){
QGuiApplicationapp(argc,argv);
QQmlApplicationEngineengine;
//thiscodedoesnotwork
qmlRegisterType<CarInfo>("CarInfo",1,0,"CarInfo");
//...
}
Ingeneral,thiswouldwork(yes,itisassimpleasthat).However,it
willnotworkwithwidgets.It'snotpossibletoincludeQWidget-based
objectsintoaQMLobjecttreebecauseaQWidgetobjectcanonlyhave
anotherQWidgetobjectasitsparent,andQMLneedstosettheouter
QMLobjectastheparent.Toresolvethisconflict,weneedto
ensurethatwhatweinstantiateisnotawidget.Forthat,wewilluse
aproxyobjectthatwillforwardourcallstotheactualwidget.
Therefore,createanewclasscalledCarInfoProxyderived
fromQObjectandmakeithavethesamepropertiesasCarInfo.
Considerthisexample:
classCarInfoProxy:publicQObject{
Q_OBJECT
Q_PROPERTY(QObject*engineREADengineNOTIFYengineChanged)
Q_PROPERTY(intspeedREADspeedWRITEsetSpeedNOTIFY
speedChanged)
//...
Declareonemorepropertythatwillletusshowandhidethewidget
ondemand:
Q_PROPERTY(boolvisibleREADvisibleWRITEsetVisible
NOTIFYvisibleChanged)
Then,wecanplacethewidgetasamembervariableoftheproxyso
thatitiscreatedanddestroyedalongsideitsproxy:
private:
CarInfom_car;
Thisway,theCarInfowidgetwillhavenullptrparent,soitwillbe
displayedasatop-levelwindow.TheQMLenginewillcreatean
objectoftheCarInfoProxyclassandsetitsparenttobeanotherQML
object,butthiswillnotaffecttheparentofthewidget.
Next,implementthemissinginterface.Forsimplicity,weare
showingyoucodeforsomeoftheproperties.Theothersaresimilar,
soyoucanfillinthegapsonyourown:
public:
public:
CarInfoProxy(QObject*parent=nullptr):QObject(parent){
connect(&m_car,&CarInfo::engineChanged,
this,&CarInfoProxy::engineChanged);
connect(&m_car,&CarInfo::speedChanged,
this,&CarInfoProxy::speedChanged);
}
QObject*engine()const{
returnm_car.engine();
}
boolvisible()const{
returnm_car.isVisible();
}
voidsetVisible(boolv){
if(v==visible())return;
m_car.setVisible(v);
emitvisibleChanged(v);
}
intspeed()const{
returnm_car.speed();
}
voidsetSpeed(intv){
m_car.setSpeed(v);
}
signals:
voidengineChanged();
voidvisibleChanged(bool);
voidspeedChanged(int);
};
YoucanseethatwereusetheCarInfoEngineinstancefromthewidget
insteadofduplicatingitintheproxyclass.Finally,wecan
registerCarInfoProxyasCarInfo:
qmlRegisterType<CarInfoProxy>("CarInfo",1,0,"CarInfo");
Ifyourunthecodenow,youwillseethatitworks—CarInfohas
becomearegularQMLelement.Duetothis,itspropertiescanbe
setandmodifieddirectlyinthedocument,right?Ifyoutrysetting
thespeedorthedistance,itwillworkjustfine.However,trytoseta
propertygroupedintheengineproperty:
CarInfo{
id:carData
visible:true
engine.gear:3
}
QMLruntimewillcomplainwithamessagesimilartothefollowing
one:
Cannotassigntonon-existentproperty"gear"
engine.gear:3
^
Thisisbecausetheruntimedoesnotunderstandtheengineproperty
—wedeclareditasQObjectandyetweareusingapropertythisclass
doesn'thave.Toavoidthisissue,wehavetoteachtheruntime
aboutCarInfoEngine.
First,let'supdatethepropertydeclarationmacroto
useCarInfoEngineinsteadofQObject:
Q_PROPERTY(CarInfoEngine*engineREADengineNOTIFYengineChanged)
Also,thegetterfunctionitself:
CarInfoEngine*engine()const{
returnm_engine;
}
YoushouldmakethesechangesinboththeCarInfoandCarInfoProxy
classes.Then,weshouldteachtheruntimeaboutthetype:
QStringmsg=QStringLiteral("ObjectsoftypeCarInfoEnginecannotbe
created");
qmlRegisterUncreatableType<CarInfoEngine>("CarInfo",1,0,
"CarInfoEngine",msg);
Whatjusthappened?
Inthisexercise,welettheQMLruntimeknowabouttwonew
elements.OneofthemisCarInfo,whichisaproxytoourwidgetclass.
Wetoldtheenginethatthisisafull-featuredclassthatis
instantiablefromQML.Theotherclass,CarInfoEngine,alsobecame
knowntoQML;however,thedifferenceisthateveryattemptto
declareanobjectofthistypeinQMLfailswithagivenwarning
message.Thereareotherfunctionsavailableforregisteringtypesin
QML,buttheyarerarelyused,sowewillnotbedescribingthem
here.Ifyouarecuriousaboutthem,typeinqmlRegisterintheIndex
tabofCreator'sHelppane.
Popquiz
Q1.WhichQMLtypeallowsyoutocreateaplaceholderforan
objectthatwillbeinstantiatedlater?
1. Repeater
2. Loader
3. Component
Q2.WhichQMLtypeprovideslow-levelaccesstoindividualtouch
events?
1. PinchArea
2. MouseArea
3. MultiPointTouchArea
Q3.WhencanyouaccessacomponentdefinedinanotherQMLfile
withoutanimportstatement?
1. Youcandothatifthecomponentisregisteredusing
theqmlRegisterTypefunction
2. Youcandothatifthecomponentfileisaddedtotheproject
resources
3. Youcandothatifthecomponentfileisinthesame
directoryasthecurrentfile
Summary
Youarenowfamiliarwithmultiplemethodsthatcanbeusedto
extendQtQuickwithyourownitemtypes.Youlearnedtouse
JavaScripttocreatecustomvisualitems.Youalsoknowhowtouse
C++classesasnon-visualQMLelementsfullyintegratedwithyour
UI.Wealsodiscussedhowtohandlemouse,touch,keyboard,and
gamepadeventsinQtQuickapplications.However,sofar,despite
ustalkingabout"fluid"and"dynamic"interfaces,youhaven'tseen
muchofthem.Donotworry;inthenextchapter,wewillfocuson
animationsinQtQuickaswellasfancygraphicsandapplyingwhat
youlearnedinthischapterforcreatingnice-lookingandinteresting
games.So,readon!
AnimationsinQtQuickGames
Intheprevioustwochapters,weintroducedyoutothebasicsofQt
QuickandQML.Bynow,youshouldbefluentenoughwiththe
syntaxandunderstandthebasicconceptsofhowQtQuickworks.
Inthischapter,wewillshowyouhowtomakeyourgamesstandout
fromthecrowdbyintroducingdifferentkindsofanimationsthat
makeyourapplicationsfeelmoreliketherealworld.Youwillalso
learntotreatQtQuickobjectsasseparateentitiesprogrammable
usingstatemachines.Asignificantpartofthischapterwill
introducehowtoimplementanumberofimportantgaming
conceptsusingQtQuick.Allthiswillbeshownwhilewebuilda
simple2Dactiongameusingthepresentedconcepts.
Themaintopicscoveredinthischapterareasfollows:
AnimationframeworkinQtQuick
Statesandtransitionsindepth
ImplementinggamesinQtQuick
Spriteanimations
Usingstatemachinesforanimation
Parallaxscrolling
Collisiondetection
AnimationframeworkinQt
Quick
InChapter11,IntroductiontoQtQuick,weimplementedasimple
animationusingQtQuickstatesandtransitions.Wewillnow
deepenourknowledgeonthistopicandlearnhowtoaddsome
dynamicsintotheuserinterfaceswecreate.Thusfar,bookscannot
containmovingpictures,soyouwillhavetotestmostthingswe
describehereyourselfbyrunningtheprovidedQtQuickcode.
QtQuickprovidesaveryextensiveframeworkforcreating
animations.Bythat,wedon'tmeanonlymovingitemsaround.We
defineananimationaschanginganarbitraryvalueovertime.So,
whatcanweanimate?Ofcourse,wecananimateitemgeometry.
However,wecanalsoanimaterotation,scale,othernumericvalues,
andevencolors,butlet'snotstophere.QtQuickalsoletsyou
animatetheparent-childhierarchyofitemsoranchorassignments.
Almostanythingthatcanberepresentedbyanitempropertycanbe
animated.
Moreover,thechangesarerarelylinear—ifyoukickaballintheair,
itfirstgainsheightquicklybecauseitsinitialspeedwaslarge.
However,theballisaphysicalobjectbeingpulleddownbythe
earth'sgravity,whichslowstheclimbdownuntiltheballstopsand
thenstartsfallingdown,acceleratinguntilithitstheground.
Dependingonthepropertiesofboththegroundandtheball,the
objectcanbounceoffthesurfaceintotheairagainwithless
momentum,repeatingthespring-likemotionuntileventuallyit
fadesaway,leavingtheballontheground.QtQuickletsyoumodel
allthatusingeasingcurvesthatcanbeassignedtoanimations.
Genericanimations
QtQuickprovidesanumberofanimationtypesderivedfroma
genericAnimationelementthatyouwillneverusedirectly.Thetype
existsonlytoprovideanAPIcommontodifferentanimationtypes.
Let'stakeacloserlookattheanimationframeworkbylookingata
familyofanimationtypesderivedfromthemostcommon
animationtype—PropertyAnimation.Asthenameimplies,theyprovide
themeanstoanimatevaluesofobjectproperties.Despitethefact
thatyoucanusethePropertyAnimationelementdirectly,itisusually
moreconvenienttouseoneofitssubclassesthatspecialisesin
dealingwiththepeculiaritiesofdifferentdatatypes.
ThemostbasicpropertyanimationtypeisNumberAnimation,whichlets
youanimateallkindsofnumericvaluesofbothintegralandreal
numbers.Thesimplestwayofusingitistodeclareananimation,
tellittoanimateaspecificpropertyinaspecificobject,andthenset
thelengthoftheanimationandthestartingandendingvaluefor
theproperty:
importQtQuick2.9
Item{
id:root
width:600;height:width
Rectangle{
id:rect
color:"red"
width:50;height:width
}
NumberAnimation{
target:rect
property:"x"
from:0;to:550
duration:3000
duration:3000
running:true
}
}
Timeforaction–Sceneforan
actiongame
Let'strysomethingnewforournewproject.SelectNewFileor
ProjectfromtheFilemenuofQtCreator,switchtotheOther
ProjectcategoryandchoosetheQtQuickUIPrototypetemplate.Qt
CreatorwillcreateamainQMLfileandaprojectfilewith
the.qmlprojectextension.Thiskindofprojectfileisdifferentthan
regularprojectfileswiththe.proextension.ThisisapureQML
projectthatdoesnotcontainanyC++codeandthusdoesnot
requirecompilation.However,youneedaQMLruntime
environmenttorunthisproject.YourQtinstallationprovidessuch
anenvironment,soyoucanruntheprojectfromtheterminalusing
theqmlscenemain.qmlcommandorjustletQtCreatorhandlethat.Note
thattheQtresourcessystemisnotusedwiththeseprojects,andthe
QMLfilesareloadeddirectlyfromthefilesystem.
IfyouneedtoaddC++codetoyourprojectoryouintendtodistributecompiledbinariesof
theproject,usetheQtQuickApplicationtemplatesinstead.TheQtQuickUI
Prototypetemplate,asthenameimplies,isonlygoodforprototypes.
Intheprojectdirectory,makeasubdirectorycalledimagesandfrom
thegameprojectthatwehavecreatedusingGraphicsView,
copygrass.png,sky.png,andtrees.png.Then,putthefollowingcodeinto
theQMLdocument:
importQtQuick2.9
Image{
id:root
propertyintdayLength:60000//1minute
source:"images/sky.png"
Item{
id:sun
x:140
x:140
y:root.height-170
Rectangle{
id:sunVisual
width:40
height:width
radius:width/2
color:"yellow"
anchors.centerIn:parent
}
}
Image{
source:"images/trees.png"
x:-200
anchors.bottom:parent.bottom
}
Image{
source:"images/grass.png"
anchors.bottom:parent.bottom
}
}
Ifyoudon'tdeclarethetop-levelWindowobject,qmlscenewilldisplaythetop-levelQtQuick
iteminawindowautomatically.NotethatwhenwritingaQtQuickapplicationdrivenby
theQQmlApplicationEngineclass,youneedtodeclaretheWindowobjectexplicitly.
Whenyouruntheprojectnow,youwillseeascreensimilartothis
one:
Whatjusthappened?
Wesetupaverysimplesceneconsistingofthreeimagesstackedup
toformalandscape.Betweenthebackgroundlayer(thesky)and
theforeground(trees),weplacedayellowcirclerepresentingthe
sun.Sincewewillbemovingthesunaroundinamoment,we
anchoredthecenteroftheobjecttoanemptyitemwithoutphysical
dimensionssothatwecansetthesun'spositionrelativetoits
center.WealsoequippedthescenewithadayLengthproperty,which
willholdinformationaboutthelengthofonedayofgametime.By
default,wesetitto60secondssothatthingshappenreallyquickly
andwecanseetheanimation'sprogresswithoutwaiting.Afterall
thingsaresetcorrectly,thelengthofthedaycanbebalancedtofit
ourneeds.
Thegraphicaldesignletsuseasilymanipulatethesunwhile
keepingitbehindthetreeline.Notehowthestackingorderis
implicitlydeterminedbytheorderofelementsinthedocument.
Timeforaction–Animatingthe
sun'shorizontalmovement
Theeverydaycruiseofthesunintheskystartsintheeastand
continueswesttohidebeneaththehorizonintheevening.Let'stry
toreplicatethishorizontalmovementbyaddinganimationto
oursunobject.
OpentheQMLdocumentofourlastproject.Insidetherootitem,
addthefollowingdeclaration:
NumberAnimation{
target:sun
property:"x"
from:0
to:root.width
duration:dayLength
running:true
}
Runningtheprogramwithsuchmodificationswillproducearun
withahorizontalmovementofthesun.Thefollowingimageisa
compositionofanumberofframesoftherun:
Whatjusthappened?
WeintroducedaNumberAnimationelementthatissettoanimate
thexpropertyofthesunobject.Theanimationstartsat0andlasts
untilxreachestherootitem'swidth(whichistherightedgeofthe
scene).ThemovementlastsfordayLengthmilliseconds.
Therunningpropertyoftheanimationissettotruetoenablethe
animation.Sincewedidn'tspecifyotherwise,themotionislinear.
Youmaybethinkingthattheanimationrunsinthewrongdirection
—"west"isontheleftand"east"isontheright,yes?That'strue,
however,onlyiftheobserverfacesnorth.Ifthatwerethecasefor
ourscene,wewouldn'tbeseeingthesunatall—atnoon,itcrosses
thesouthdirection.
Composinganimations
TheanimationwemadeinthelastsectionlooksOKbutisnotvery
realistic.Thesunshouldriseinthemorning,reachitspeak
sometimebeforenoon,andthen,sometimelater,startsetting
towardtheevening,whenitshouldcrossthehorizonandhide
beneaththelandscape.
Toachievesuchaneffect,wecanaddtwomoreanimationsfor
theypropertyofthesun.Thefirstanimationwouldstartrightat
thebeginninganddecreasetheverticalpositionofthesun
(rememberthattheverticalgeometryaxispointsdown,so
decreasingtheverticalpositionmeanstheobjectgoesup).The
animationwouldbecompleteatone-thirdofthedaylength.We
wouldthenneedawaytowaitforsometimeandthenstarta
secondanimationthatwouldpulltheobjectdowntowardthe
ground.Startingandstoppingtheanimationiseasy—wecaneither
callthestart()andstop()functionsontheanimationitemordirectly
alterthevalueoftherunningproperty.EachAnimationobject
emitsstarted()andstopped()signals.Thedelaycanbeimplemented
usingatimer.Wecanprovideasignalhandlerforthestopped
signalofthefirstanimationtotriggeratimertostarttheotherone
likethis:
NumberAnimation{
id:sunGoesUpAnim
//...
onStopped:sunGoesDownAnimTimer.start()
}
Timer{
id:sunGoesDownAnimTimer
interval:dayLength/3
onTriggered:sunGoesDownAnim.start()
}
Evenignoringanysideproblemsthiswouldbring(forexample,
howtostoptheanimationwithoutstartingthesecondone),suchan
approachcouldn'tbecalled"declarative",couldit?
Fortunately,similartowhatwehadinC++,QtQuickletsusform
animation
groupsthatruneitherparalleltoeachotherorinsequence.There
aretheSequentialAnimationandParallelAnimationtypeswhereyoucan
declareanynumberofchildanimationelementsformingthegroup.
Toruntwoanimationsinparallel,wecandeclarethefollowing
hierarchyofelements:
ParallelAnimation{
id:parallelAnimationGroup
running:true
NumberAnimation{
target:obj1;property:"prop1"
from:0;to:100
duration:1500
}
NumberAnimation{
target:obj2;property:"prop2"
from:150;to:0
duration:1500
}
}
Thesametechniquecanbeusedtosynchronizealargergroupof
animations,evenifeachcomponenthasadifferentduration:
SequentialAnimation{
id:sequentialAnimationGroup
running:true
ParallelAnimation{
id:parallelAnimationGroup
NumberAnimation{
id:animation1
target:obj2;property:"prop2"
from:150;to:0
from:150;to:0
duration:1000
}
NumberAnimation{
id:animation2
target:obj1;property:"prop1"
from:0;to:100
duration:2000
}
}
PropertyAnimation{
id:animation3
target:obj1;property:"prop1"
from:100;to:300
duration:1500
}
}
Thegrouppresentedinthesnippetconsistsofthreeanimations.
Thefirsttwoanimationsareexecutedtogetherastheyforma
parallelsubgroup.Onememberofthegrouprunstwiceaslongas
theother.Onlyafterthewholesubgroupcompletesisthethird
animationstarted.ThiscanbevisualizedusingaUnified
ModelingLanguage(UML)activitydiagramwherethesizeof
eachactivityisproportionaltothedurationofthatactivity:
Timeforaction–Makingthe
sunriseandset
Let'saddverticalmovement(animationoftheyproperty)toour
sunbyaddingasequenceofanimationstotheQMLdocument.As
ournewanimationswillberunninginparalleltothehorizontal
animation,wecanencloseanimationsforbothdirectionswithina
singleParallelAnimationgroup.Itwouldwork,butinouropinion,this
willunnecessarilyclutterthedocument.Anotherwayofspecifying
parallelanimationsistodeclarethemasseparatehierarchiesof
elements,makingeachanimationindependentoftheother,and
thatiswhatwewilldohere.
Openourdocumentfromthelastexercise,andrightunderthe
previousanimation,placethefollowingcode:
SequentialAnimation{
running:true
NumberAnimation{
target:sun
property:"y"
from:root.height+sunVisual.height
to:root.height-270
duration:dayLength/3
}
PauseAnimation{duration:dayLength/3}
NumberAnimation{
target:sun
property:"y"
from:root.height-270
to:root.height+sunVisual.height
duration:dayLength/3
}
}
Runningtheprogramwillresultinthelightsourcerisinginthe
morningandsettingintheevening.However,thetrajectoryofthe
moveseemssomewhatawkward:
Whatjusthappened?
Wedeclaredasequentialanimationgroupconsistingofthree
animations,eachtakingone-thirdofthedaylength.Thefirst
memberofthegroupmakesthesungoup.Thesecondmember,
whichisaninstanceofanewelementtype—PauseAnimation—
introducesadelayequaltoitsduration.This,inturn,letsthethird
componentstartitsworkintheafternoontopullthesundown
towardthehorizon.
Theproblemwithsuchadeclarationisthatthesunmovesina
horriblyangularway,ascanbeseenintheimage.
Non-linearanimations
Thereasonforthedescribedproblemisthatouranimationsare
linear.Aswenotedatthebeginningofthischapter,linear
animationsrarelyoccurinnature,whichusuallymakestheiruse
yieldaveryunrealisticresult.
WealsosaidearlierthatQtQuickallowsustouseeasingcurvesto
performanimationsalongnon-linearpaths.Therearealarge
numberofcurvesoffered.Here'sadiagramlistingtheavailable
non-lineareasingcurves:
Youcanuseanyofthecurvesonanelementofthe
PropertyAnimationtypeoronederivedfromit(for
example,NumberAnimation).Thisisdoneusingtheeasingpropertygroup,
whereyoucansetthetypeofthecurve.Differentcurvetypesmay
furtherbetweakedbysettinganumberofpropertiesin
theeasingpropertygroup,suchasamplitude(forbounceandelastic
curves),overshoot(forbackcurves),orperiod(forelasticcurves).
DeclaringananimationalonganInOutBouncepathisveryeasy:
NumberAnimation{
target:obj
property:prop
from:startValue
to:endValue
easing.type:Easing.InOutBounce
running:true
}
Timeforaction–Improvingthe
pathofthesun
Thetaskathandwillbetoimprovetheanimationofthesunsothat
itbehavesinamorerealisticway.Wewilldothisbyadjustingthe
animationssothattheobjectmovesoveracurvedpath.
InourQMLdocument,replacethepreviousverticalanimationwith
thefollowingone:
SequentialAnimation{
running:true
NumberAnimation{
target:sun
property:"y"
from:root.height+sunVisual.height
to:root.height-270
duration:dayLength/2
easing.type:Easing.OutCubic
}
NumberAnimation{
target:sun
property:"y"
to:root.height+sunVisual.height
duration:dayLength/2
easing.type:Easing.InCubic
}
}
Thefollowingpictureshowshowthesunwillnowmove:
Whatjusthappened?
Thesequenceofthreeanimations(twolinearonesandapause)was
replacedbyanothersequenceoftwoanimationsthatfollowapath
determinedbyacubicfunction.Thismakesoursunriseprettyfast
andthenslowdowntoanamountalmostunnoticeablenearthe
momentwhenthesunapproachesnoon.Whenthefirstanimation
isfinished,thesecondonereversesthemotion,makingthesun
descendveryslowlyandthenincreaseitsvelocityasdusk
approaches.Asaresult,thefartherthesunisfromtheground,the
sloweritseemstomove.Atthesametime,thehorizontalanimation
remainslinear,asthespeedofearthinitsmotionaroundthesunis
practicallyconstant.Whenwecombinethehorizontalandvertical
animations,wegetapaththatlooksverysimilartowhatwecan
observeintherealworld.
Propertyvaluesources
FromtheQMLperspective,Animationandelementtypesderivedfrom
itarecalledpropertyvaluesources.Thismeanstheycanbe
attachedtoapropertyandgeneratevaluesforit.Whatisimportant
isthatitallowsustouseanimationsusingamuchsimplersyntax.
Insteadofexplicitlydeclaringthetargetandpropertyofan
animation,youcanattachtheanimationtoanamedpropertyofthe
parentobject.
Todothis,insteadofspecifyingtargetandpropertyforAnimation,use
theonkeyword,followedbythenameofapropertynameforwhich
theanimationistobeavaluesource.Forexample,toanimate
therotationpropertyofanobjectwithaNumberAnimationobject,the
followingcodecanbeused:
NumberAnimationonrotation{
from:0
to:360
duration:500
}
Itisvalidtospecifymorethanonepropertyvaluesourceforthe
samepropertyofanobject.
Timeforaction–Adjustingthe
sun'scolor
Ifyoulookatthesunatduskordawn,youwillseethatitisnot
yellowbutbecomesredthecloseritistothehorizon.Let'steach
ourobjectrepresentingthesuntodothesamebyprovidinga
propertyvaluesourceforit.
OpentheQMLdocument,findthedeclarationfor
thesunVisualobject,andextenditwiththehighlightedpart:
Rectangle{
id:sunVisual
//...
SequentialAnimationoncolor{
ColorAnimation{
from:"red"
to:"yellow"
duration:0.2*dayLength/2
}
PauseAnimation{
duration:2*0.8*dayLength/2
}
ColorAnimation{
to:"red"
duration:0.2*dayLength/2
}
running:true
}
}
Whatjusthappened?
Ananimationwasattachedtothecolorpropertyofourrectangle
modelingthevisualaspectsofthesun.Theanimationconsistsof
threeparts.First,weperformatransitionfromredtoyellowusing
theColorAnimationobject.ThisisanAnimationsubtypededicatedto
modifyingcolors.Sincetherectanglecolorisnotanumber,using
theNumberAnimationobjectwillnotwork,asthetypecannotinterpolate
colorvalues.Therefore,weeitherhavetousethePropertyAnimationor
theColorAnimationobject.Thedurationfortheanimationissetto20
percentofhalfthedaylengthsothattheyellowcolorisobtained
veryquickly.ThesecondcomponentisaPauseAnimationobjectto
provideadelaybeforethethirdcomponentisexecuted,which
graduallychangesthecolorbacktored.Forthelastcomponent,we
donotprovideavalueforthefromproperty.Thiscausesthe
animationtobeinitiatedwiththevalueofthepropertycurrentto
thetimewhentheanimationisexecuted(inthiscase,thesun
shouldbeyellow).
Notethatweonlyhadtospecifythepropertynameforthetop-level
animation.Thisparticularelementiswhatservesastheproperty
valuesource,andalldescendantanimationobjects"inherit"the
targetpropertyfromthatpropertyvaluesource.
Timeforaction–Furnishing
sunanimation
Theanimationofthesunlooksalmostperfectrightnow.Wecan
stillimproveit,though.Ifyoulookintotheskyintheearlymorning
andthenagainatnoon,youwillnotethatthesunappearsmuch
biggerduringsunriseorsunsetcomparedtoitssizewhenitisatits
zenith.Wecansimulatethateffectbyscalingtheobject.
Inourscenedocument,addanothersequentialanimationthat
operatesonthescalepropertyofthesun:
SequentialAnimationonscale{
NumberAnimation{
from:1.6;to:0.8
duration:dayLength/2
easing.type:Easing.OutCubic
}
NumberAnimation{
from:0.8;to:1.6
duration:dayLength/2
easing.type:Easing.InCubic
}
}
Let'sexaminetheresultagain:
Whatjusthappened?
Inthissection,wejustfollowedthepathsetforanearlier
declaration—theverticalmovementofthestellarbodyinfluencesits
perceivedsize;therefore,itseemslikeagooddecisiontobindthe
twoanimationstogether.Notethatinsteadofspecifyinganew
propertyvaluesourceforthescale,wemighthavemodifiedthe
originalanimationandmadethescaleanimationparalleltothe
animationthatoperatesontheyproperty:
SequentialAnimation{
ParallelAnimation{
NumberAnimation{
target:sun
property:"y"
from:root.height+sunVisual.height
to:root.height-270
duration:dayLength/2
easing.type:Easing.OutCubic
}
NumberAnimation{
target:sun
property:"scale"
from:1.6;to:0.8
duration:dayLength/2
easing.type:Easing.OutCubic
}
//...
}
}
Haveagohero–Animatingthe
sun'srays
Bynow,youshouldbeananimationexpert.Ifyouwanttotryyour
skills,here'sataskforyou.Thefollowingcodecanbeappliedto
thesunobjectandwilldisplayverysimpleredraysemittedfromthe
sun:
Item{
id:sunRays
propertyintcount:10
width:sunVisual.width
height:width
anchors.centerIn:parent
z:-1
Repeater{
model:sunRays.count
Rectangle{
color:"red"
rotation:index*360/sunRays.count
anchors.fill:parent
}
}
}
Theresultisshownonthefollowingpicture:
Thegoalistoanimatetherayssothattheoveralleffectlooksgood
andfitsthetunelikestyleofthescene.Trydifferentanimations—
rotations,sizechanges,andcolors.Applythemtodifferent
elements—allraysatonce(forexample,usingthesunRaysidentifier)
oronlyparticularrectanglesgeneratedbytherepeater.
Behaviors
Inthepreviouschapter,weimplementedadashboardforaracing
gamewherewehadanumberofclockswithneedles.Wecouldset
valuesforeachclock(forexample,carspeed)andarespective
needlewouldimmediatelysetitselftothegivenvalue.However,
suchanapproachisunrealistic—intherealworld,changesofa
valuehappenovertime.Inourexample,thecaracceleratesfrom10
mphto50mphbygoingthrough11mph,12mph,andsoon,until
aftersometimeitreachesthedesiredvalue.Wecallthis
thebehaviorofavalue—itisessentiallyamodelthattellshowthe
parameterreachesitsdestinedvalue.Definingsuchmodelsisa
perfectusecasefordeclarativeprogramming.Fortunately,QML
exposesaBehaviorelementthatletsusmodelbehaviorsofproperty
changesinQtQuick.
TheBehaviorelementsletusassociateananimationwithagiven
propertysothateverytimethepropertyvalueistobechanged,itis
donebyrunningthegivenanimationinsteadofbymakingan
immediatechangetothepropertyvalue.
Considerasimplescenedefinedbythefollowingcode:
importQtQuick2.9
Item{
width:600;height:width
Item{
id:empty
x:parent.width/2;y:parent.height/2
Rectangle{
id:rect
width:100;height:width
color:"red"
anchors.centerIn:parent
}
}
}
MouseArea{
anchors.fill:parent
onClicked:{
empty.x=mouse.x;
empty.y=mouse.y;
}
}
}
Thisscenecontainsaredrectangleanchoredtoanemptyitem.
Whenevertheuserclickssomewherewithinthescene,theempty
itemismovedthere,draggingalongtherectangle.Let'sseehowto
usetheBehaviorelementtosmoothlychangethepositionofthe
emptyitem.SimilartoAnimationandotherpropertyvaluesources,
theBehaviorelementcanbeusedwiththeon-propertysyntax:
Item{
id:empty
x:parent.width/2;y:parent.height/2
Rectangle{
id:rect
width:100;height:width
color:"red"
anchors.centerIn:parent
}
Behavioronx{
NumberAnimation{}
}
Behaviorony{
NumberAnimation{}
}
}
Byaddingthetwomarkeddeclarations,wedefinebehaviorsfor
thexandypropertiesthatfollowanimationsdefined
byNumberAnimation.Wedonotincludestartorendvaluesforthe
animationasthesewilldependontheinitialandfinalvalueforthe
property.Wealsodon'tsetthepropertynameintheanimation
becausebydefault,thepropertyforwhichthebehaviorisdefined
willbeused.Asaresult,wegetalinearanimationofanumerical
propertyfromtheoriginalvaluetothedestinedvalueoverthe
defaultduration.
Usinglinearanimationsforreal-worldobjectsrarelylooksgood.Usually,youwillget
muchbetterresultsifyousetaneasingcurvefortheanimationsothatitstartsslowlyand
thengainsspeedanddeceleratesjustbeforeitisfinished.
Animationsthatyousetonbehaviorscanbeascomplexasyou
want:
Behavioronx{
SequentialAnimation{
PropertyAction{
target:rect
property:"color"
value:"yellow"
}
ParallelAnimation{
NumberAnimation{
easing.type:Easing.InOutQuad
duration:1000
}
SequentialAnimation{
NumberAnimation{
target:rect
property:"scale"
from:1.0;to:1.5
duration:500
}
NumberAnimation{
target:rect
property:"scale"
from:1.5;to:1.0
duration:500
}
}
}
PropertyAction{
target:rect
property:"color"
value:"red"
}
}
}
Thebehavioralmodeldeclaredinthelastpieceofcodeperformsa
sequentialanimation.Itfirstchangesthecoloroftherectangleto
yellowusingthePropertyActionelement,whichperformsanimmediate
updateofapropertyvalue(wewilltalkaboutthismoreabitlater).
Thecolorwillbesetbacktoredafterthelaststepofthemodel.In
themeantime,aparallelanimationisperformed.Oneofits
componentsisaNumberAnimationclassthatexecutestheactual
animationofthexpropertyofempty(sincethetargetandpropertyof
theanimationarenotexplicitlyset).Thesecondcomponentisa
sequentialanimationofthescalepropertyoftherectangle,which
firstscalestheitemupby50percentduringthefirsthalfofthe
animationandthenscalesitbackdowninthesecondhalfofthe
animation.
Timeforaction–Animatingthe
cardashboard
Let'semploytheknowledgewejustlearnedtoimprovethecar
dashboardwecreatedinthepreviouschapter.Wewilluse
animationstoshowsomerealisminthewaytheclocksupdatetheir
values.
Openthedashboardprojectandnavigatetothemain.qmlfile.Findthe
declarationoftheNeedleobject,whichisresponsibleforvisualizing
thespeedofthevehicle.Addthefollowingdeclarationtotheobject:
Behavioronrotation{
SmoothedAnimation{
velocity:50
}
}
Repeattheprocessfortheleftclock.Setthevelocityofthe
animationto100.Buildandruntheproject.Seehowtheneedles
behavewhenyoumodifytheparametervaluesinspinboxes.Adjust
thevelocityofeachanimationuntilyougetarealisticresult.
Whatjusthappened?
Wehavesetthepropertyvaluesourcesonneedlerotationsthatare
triggeredwheneveranewvalueforthepropertyisrequested.
Insteadofimmediatelyacceptingthenewvalue,theBehaviorelement
interceptstherequestandstartstheSmoothedAnimationclassto
graduallyreachtherequestedvalue.TheSmoothedAnimationclassisan
animationtypethatanimatesnumericproperties.Thespeedofthe
animationisnotdeterminedbyitsduration;instead,
avelocitypropertyisset.Thispropertydictateshowfastavalueisto
bechanged.However,theanimationisusinganon-linearpath—it
startsslowly,thenacceleratestothegivenvelocity,and,nearthe
endoftheanimation,deceleratesinasmoothfashion.Thisyields
ananimationthatisattractiveandrealisticand,atthesametime,is
ofshorterorlongerduration,dependingonthedistancebetween
thestartingandendingvalues.
Youcanimplementcustompropertyvaluesourcesby
subclassingQQmlPropertyValueSourceandregisteringtheclassintheQMLengine.
States
Whenyoulookatreal-worldobjects,itisoftenveryeasytodefine
theirbehaviorbyextractinganumberofstatestheobjectmaytake
anddescribingeachofthestatesseparately.Alampcanbeturned
eitheronoroff.Whenitis"on",itisemittinglightofagivencolor,
butitisnotdoingthatwheninthe"off"state.Dynamicsofthe
objectcanbedefinedbydescribingwhathappensiftheobject
leavesoneofthestatesandentersanotherone.Consideringour
lampexample,ifyouturnthelampon,itdoesn'tmomentarilystart
emittinglightwithitsfullpower,butthebrightnessofthelight
graduallyincreasestoreachitsfinalpowerafteraveryshortperiod.
QtQuicksupportsstate-drivendevelopmentbylettingusdeclare
statesandtransitionsbetweenthemforitems.Themodelfitsthe
declarativenatureofQtQuickverywell.
Bydefault,eachitemhasasingleanonymousstate,andall
propertiesyoudefinetakevaluesoftheexpressionsyoubindor
assigntothemimperativelybasedondifferentconditions.Instead
ofthis,asetofstatescanbedefinedfortheobjectandforeachof
thestatepropertiesoftheobjectitself;inaddition,theobjects
definedwithinitcanbeprogrammedwithdifferentvaluesor
expressions.Ourexamplelampdefinitioncouldbesimilartothis:
Item{
id:lamp
propertyboollampOn:false
width:200
height:200
Rectangle{
id:lightsource
anchors.fill:parent
color:"transparent"
}
}
}
Wecould,ofcourse,bindthecolorpropertyoflightsourcetolamp.lampOn
?"yellow":"transparent";instead,wecandefinean"on"stateforthe
lampanduseaPropertyChangeselementtomodifytherectanglecolor:
Item{
id:lamp
propertyboollampOn:false
//...
states:State{
name:"on"
PropertyChanges{
target:lightsource
color:"yellow"
}
}
}
Eachitemhasastatepropertythatyoucanreadtogetthecurrent
state,butyoucanalsowritetoittotriggertransitiontoagiven
state.Bydefault,thestatepropertyissettoanemptystringthat
representstheanonymousstate.Notethatwiththepreceding
definition,theitemhastwostates—the"on"stateandthe
anonymousstate(whichisusedwhenthelampisoffinthiscase).
Rememberthatstatenameshavetobeuniqueasthenameparameter
iswhatidentifiesastateinQtQuick.
Toenterastate,wecan,ofcourse,useaneventhandlerfiredwhen
thevalueofthelampOnparameterismodified:
onLampOnChanged:state=lampOn?"on":""
Suchimperativecodeworks,butitcanbereplacedwitha
declarativedefinitioninthestateitself:
State{
name:"on"
when:lamp.lampOn
PropertyChanges{
target:lightsource
color:"yellow"
}
}
Whenevertheexpressionboundtothewhenpropertyevaluates
totrue,thestatebecomesactive.Iftheexpressionbecomesfalse,the
objectwillreturntothedefaultstateorwillenterastateforwhich
itsownwhenpropertyevaluatestotrue.
Todefinemorethanonecustomstate,itisenoughtoassignalistof
statedefinitionstothestatesproperty:
states:[
State{
name:"on"
when:lamp.lampOn
PropertyChanges{/*...*/}
},
State{
name:"off"
when:!lamp.lampOn
}
]
ThePropertyChangeselementisthemostoftenusedchangeinastate
definition,butitisnottheonlyone.Inexactlythesamewaythat
theParentChangeelementcanassignadifferentparenttoanitemand
theAnchorChangeelementcanupdateanchordefinitions,itisalso
possibletorunascriptwhenastateisenteredusing
theStateChangeScript
element.Alltheseelementtypesareusedbydeclaringtheir
instancesaschildreninaStateobject.
Transitions
Thesecondpartofthestatemachineframeworkisdefininghowan
objecttransitsfromonestatetoanother.Similarto
thestatesproperty,allitemshaveatransitionsproperty,whichtakes
alistofdefinitionsrepresentedbytheTransitionobjectsandprovides
informationaboutanimationsthatshouldbeplayedwhena
particulartransitiontakesplace.
Atransitionisidentifiedbythreeattributes—thesourcestate,the
destinationstate,andasetofanimations.Boththesourcestate
name(settothefromproperty)andthetargetstatename(setto
thetoproperty)canbeempty,inwhichcasetheyshouldbe
interpretedas"any".IfaTransitionexiststhatmatchesthecurrent
statechange,itsanimationswillbeexecuted.Amoreconcrete
transitiondefinition(whichisonewherefromand/ortoareexplicitly
set)hasprecedenceoveramoregenericone.
Supposethatwewanttoanimatetheopacityofthelamprectangle
from0to1whenthelampisswitchedon.Wecandoitasan
alternativetomanipulatingthecolor.Let'supdatethelamp
definition:
Item{
id:lamp
propertyboollampOn:false
Rectangle{
id:lightsource
anchors.fill:parent
color:"yellow"
opacity:0
}
MouseArea{
anchors.fill:parent
onPressed:{
lamp.lampOn=!lamp.lampOn;
lamp.lampOn=!lamp.lampOn;
}
}
states:State{
name:"on"
when:lamp.lampOn
PropertyChanges{
target:lightsource
opacity:1
}
}
transitions:Transition{
NumberAnimation{
duration:500
property:"opacity"
}
}
}
Thetransitionistriggeredforanysourceandanytargetstate—it
willbeactivewhenthelampgoesfromtheanonymoustothe"on"
stateaswellasintheoppositedirection.Itdefinesa
singleNumberAnimationelementthatworksonopacitypropertyandlasts
for500miliseconds.Theanimationdoesnotdefinethetarget
object;thus,itwillbeexecutedforanyobjectthatneedsupdating
aspartofthetransition—inthecaseofthelamp,itwillonlybe
thelightsourceobject.
Ifmorethanoneanimationisdefinedinatransition,allanimations
willruninparallel.Ifyouneedasequentialanimation,youneedto
explicitlyuseaSequentialAnimationelement:
Transition{
SequentialAnimation{
NumberAnimation{
target:lightsource
property:"opacity"
duration:500
}
ScriptAction{
script:{
console.log("Transitionhasended");
}
}
}
}
StatesareafeatureofallItemtypesaswellasitsdescendenttypes.Itis,however,possible
tousestateswithelementsnotderivedfromtheItemobjectusingaStateGroupelement,
whichisaself-containedfunctionalityofstatesandtransitionswithexactlythesame
interfaceaswhatisdescribedhereregardingItemobjects.
Moreanimationtypes
Theanimationtypeswediscussedearlierareusedformodifying
valuesoftypesthatcanbedescribedusingphysicalmetrics
(position,sizes,colors,angles).However,therearemoretypes
available.
Thefirstgroupofspecialanimationsconsistsof
theAnchorAnimationandParentAnimationelements.
TheAnchorAnimationelementisusefulifastatechangeshouldcausea
changetodefinedanchorsforanitem.Withoutit,theitemwould
immediatelysnapintoitsplace.ByusingtheAnchorAnimationelement,
wetriggerallanchorchangestobegraduallyanimated.
TheParentAnimationelement,ontheotherhand,makesitpossibleto
defineanimationsthatshouldbepresentwhenanitemreceivesa
newparent.Thisusuallycausesanitemtobemovedtoadifferent
positioninthescene.ByusingtheParentAnimationelementinastate
transition,wecandefinehowtheitemgetsintoitstargetposition.
Theelementcancontainanynumberofchildanimationelements
thatwillberuninparallelduringaParentChangeelement.
Thesecondspecialgroupofanimationsisactionanimations
—PropertyActionandScriptAction.Theseanimationtypesarenot
stretchedintimebutperformagivenone-timeaction.
ThePropertyActionelementisaspecialkindofanimationthat
performsanimmediateupdateofapropertytoagivenvalue.Itis
usuallyusedaspartofamorecomplexanimationtomodifya
propertythatisnotanimated.Itmakessensetouseitifaproperty
needstohaveacertainvalueduringananimation.
ScriptActionisanelementthatallowstheexecutionofanimperative
pieceofcodeduringananimation(usuallyatitsbeginningorend).
Quickgameprogramming
Here,wewillgothroughtheprocessofcreatingaplatformgame
usingQtQuick.ItwillbeagamesimilartoBenjamintheElephant
fromChapter6,QtCoreEssentials.Theplayerwillcontrolacharacter
thatwillbewalkingthroughthelandscapeandcollectingcoins.The
coinswillberandomlyappearingintheworld.Thecharactercan
accesshighlyplacedcoinsbyjumping.
Throughoutthischapteraswellasthepreviousone,weprepareda
numberofpiecesthatwewillbereusingforthisgame.Thelayered
scenethatwasarrangedwhenyoulearnedaboutanimationswill
serveasourgamescene.Theanimatedsunwillrepresentthe
passingoftime.
Wewillguideyouthroughimplementingthemainfeaturesofthe
game.Attheendofthechapter,youwillhaveachancetotestyour
skillsbyaddingmoregamemechanicstoourproject.
Gameloops
Mostgamesrevolvearoundsomekindofgameloop.Itisusually
somekindoffunctionthatiscalledrepeatedly,anditstaskisto
progressthegame—processinputevents,moveobjectsaround,
calculateandexecuteactions,checkwinconditions,andsoon.Such
anapproachisveryimperativeandusuallyresultsinavery
complexfunctionthatneedstoknoweverythingabouteverybody
(thiskindofanti-patternissometimescalledagod
objectpattern).InQML(whichpowerstheQtQuickframework),
weaimtoseparateresponsibilitiesanddeclarewell-defined
behaviorsforparticularobjects.Therefore,althoughitispossibleto
setupatimerthatwillperiodicallycallagameloopfunction,thisis
notthebestpossibleapproachinadeclarativeworld.
Instead,wesuggestusinganaturaltime-flowmechanismalready
presentinQtQuick—onethatcontrolstheconsistencyof
animations.Rememberhowwedefinedthesun'stravelacrossthe
skyatthebeginningofthischapter?Insteadofsettingupatimer
andmovingtheobjectbyacalculatednumberofpixels,wecreated
ananimation,definedatotalrunningtimeforit,andletQttake
careofupdatingtheobject.Thishasthegreatbenefitofneglecting
delaysinfunctionexecution.Ifyouusedatimerandsomeexternal
eventintroducedasignificantdelaybeforethetimeoutfunctionwas
run,theanimationwouldstartlaggingbehind.WhenQtQuick
animationsareused,theframeworkcompensatesforsuchdelays,
skippingsomeoftheframeupdatestoensurethattherequested
animationdurationisrespected.Thankstothat,youwillnothave
totakecareofitallbyyourself.
Toovercometheseconddifficultaspectofagameloop—thegod
objectanti-pattern—wesuggestencapsulatingthelogicofeachitem
directlyintheitemitselfusingthestatesandtransitionsframework
weintroducedearlier.Ifyoudefineanobjectusinganaturaltime
flowdescribingallstatesitcanenterduringitslifetimeandactions
causingtransitionsbetweenstates,youwillbeabletojustplopthe
objectwithitsincludedbehaviorwhereveritisneededandthus
easilyreusesuchdefinitionsindifferentgames,reducingthe
amountofworknecessarytomaketheobjectfitintothegame.
Inputprocessing
Ausualapproachingamesistoreadinputeventsandcallfunctions
responsibleforactionsassociatedwithparticularevents:
voidScene::keyEvent(QKeyEvent*event){
switch(event->key()){
caseQt::Key_Right:
player->goRight();break;
caseQt::Key_Left:
player->goLeft();break;
caseQt::Key_Space:
player->jump();break;
//...
}
}
This,however,hasitsdrawbacks,oneofwhichistheneedtocheck
eventsatevenperiodsoftime.Thismightbehardandiscertainly
notadeclarativeapproach.
WealreadyknowthatQtQuickhandleskeyboardinputvia
theKeysattachedproperty.ItispossibletocraftQMLcodesimilarto
theonejustpresented,buttheproblemwithsuchanapproachis
thatthefastertheplayertapskeysonthekeyboard,themore
frequentlythecharacterwillmove,jump,orshoot.However,it's
possibletoovercomethisproblem,aswe'llseeaswemoveon.
Timeforaction–Character
navigation
CreateanewQMLdocumentandcallitPlayer.qml.Inthedocument,
placethefollowingdeclarations:
Item{
id:player
y:parent.height
focus:true
Keys.onRightPressed:x=Math.min(x+20,parent.width)
Keys.onLeftPressed:x=Math.max(0,x-20)
Keys.onUpPressed:jump()
functionjump(){
jumpAnim.start();
}
Image{
source:"images/elephant.png"
anchors.bottom:parent.bottom
anchors.horizontalCenter:parent.horizontalCenter
}
Behavioronx{
NumberAnimation{duration:100}
}
SequentialAnimationony{
id:jumpAnim
running:false
NumberAnimation{
to:player.parent.height-50
easing.type:Easing.OutQuad
}
NumberAnimation{
to:player.parent.height
easing.type:Easing.InQuad
}
}
}
}
Next,openthedocumentcontainingthemainscenedefinitionand
declaretheplayercharacterneartheendofthedocumentafterall
thebackgroundlayershavebeendeclared:
Player{
id:player
x:40
}
Whatjusthappened?
Theplayeritselfisanemptyitemwithakeyboardfocusthat
handlespressesoftheright,left,anduparrowkeys,causingthem
tomanipulatethexandycoordinatesoftheplayer.Thexproperty
hasaBehaviorelementsetsothattheplayermovessmoothlywithin
thescene.Finally,anchoredtotheplayeritemistheactual
visualizationoftheplayer—ourelephantfriend.
Whentherightorleftarrowkeysarepressed,anewpositionforthe
characterwillbecalculatedandapplied.Thanksto
theBehaviorelement,theitemwilltravelgradually(duringone
second)tothenewposition.Keepingthekeypressedwilltrigger
autorepeatandthehandlerwillbecalledagain.Inasimilarfashion,
whentheuparrowkeyispressed,itwillactivateaprepared
sequentialanimationthatwillliftthecharacterupby50pixelsand
thenmoveitdownagaintotheinitialposition.
Thisapproachworks,butwecandobetter.Let'strysomething
different.
Timeforaction–Another
approachtocharacter
navigation
Replacethepreviouskeyhandlerswiththefollowingcode:
Item{
id:player
//...
QtObject{
id:flags
readonlypropertyintspeed:100
propertyinthorizontal:0
}
Keys.onRightPressed:{
recalculateDurations();
flags.horizontal=1;
}
Keys.onLeftPressed:{
if(flags.horizontal!=0){
return;
}
recalculateDurations();
flags.horizontal=-1;
}
Keys.onUpPressed:jump()
Keys.onReleased:{
if(event.isAutoRepeat)return;
if(event.key===Qt.Key_Right){
flags.horizontal=0;
}
if(event.key===Qt.Key_Left&&flags.horizontal<0){
flags.horizontal=0;
}
}
functionrecalculateDurations(){
xAnimRight.duration=(xAnimRight.to-x)*1000/
xAnimRight.duration=(xAnimRight.to-x)*1000/
flags.speed;
xAnimLeft.duration=(x-xAnimLeft.to)*1000/flags.speed;
}
NumberAnimationonx{
id:xAnimRight
running:flags.horizontal>0
to:parent.width
}
NumberAnimationonx{
id:xAnimLeft
running:flags.horizontal<0
to:0
}
}
Whatjusthappened?
Insteadofperformingactionsimmediately,uponpressingakey,we
arenowsettingflags(inaprivateobject)forwhichdirectionthe
charactershouldbemovingin.Inoursituation,therightdirection
haspriorityovertheleftdirection.Settingaflagtriggersan
animationthattriestomovethecharactertowardanedgeofthe
scene.Releasingthebuttonwillcleartheflagandstopthe
animation.Beforetheanimationisstarted,wearecalling
therecalculateDurations()function,whichcheckshowlongthe
animationshouldlastforthecharactertomoveatthedesired
speed.
Ifyouwanttoreplacekeyboard-basedinputwithsomethingelse,forexample,
accelerometerorcustombuttons,thesameprinciplecanbeapplied.Whenusingan
accelerometer,youcanevencontrolthespeedoftheplayerbymeasuringhowmuchthe
deviceistilted.Youcanadditionallystorethetiltintheflags.horizontalparameterand
makeuseofthatvariableintherecalculateDurations()function.
Haveagohero–Polishingthe
animation
Whatwehavedoneissufficientformanyapplications.However,
youcantrycontrollingthemovementevenmore.Asachallenge,try
modifyingthesysteminsuchawaythatduringajump,inertia
keepsthecurrenthorizontaldirectionandspeedofmovementof
thecharacteruntiltheendofthejump.Iftheplayerreleasesthe
rightorleftkeysduringajump,thecharacterwillstoponlyafter
thejumpiscomplete.
Despitetryingtodoeverythinginadeclarativefashion,some
actionswillstillrequireimperativecode.Ifsomeactionistobe
executedperiodically,youcanusetheTimeritemtoexecutea
functionondemand.Let'sgothroughtheprocessofimplementing
suchpatternstogether.
Timeforaction–Generating
coins
Thegoalofthegamewearetryingtoimplementistocollectcoins.
Wewillspawncoinsnowandtheninrandomlocationsofthescene.
CreateanewQMLDocumentandcallitCoin.qml.Intheeditor,enter
thefollowingcode:
Item{
id:coin
Rectangle{
id:coinVisual
color:"yellow"
border.color:Qt.darker(color)
border.width:2
width:30;height:width
radius:width/2
anchors.centerIn:parent
transform:Rotation{
origin.x:coinVisual.width/2
origin.y:coinVisual.height/2
axis{x:0;y:1;z:0}
NumberAnimationonangle{
from:0;to:360
loops:Animation.Infinite
running:true
duration:1000
}
}
Text{
color:coinVisual.border.color
anchors.centerIn:parent
text:"1"
}
}
}
Next,openthedocumentwherethesceneisdefinedandenterthe
followingcodesomewhereinthescenedefinition:
Component{
id:coinGenerator
Coin{}
}
Timer{
id:coinTimer
interval:1000
repeat:true
running:true
onTriggered:{
varcx=Math.floor(Math.random()*root.width);
varcy=Math.floor(Math.random()*root.height/3)
+root.height/2;
coinGenerator.createObject(root,{x:cx,y:cy});
}
}
Whatjusthappened?
First,wedefinedanewelementtype,Coin,consistingofayellow
circlewithanumbercenteredoveranemptyitem.Therectangle
hasananimationappliedthatrotatestheitemaroundavertical
axis,resultinginapseudothree-dimensionaleffect.
Next,acomponentabletocreateinstancesofaCoinelementis
placedinthescene.Then,aTimerelementisdeclaredthatfiresevery
secondandspawnsanewcoinatarandomlocationofthescene.
Spriteanimation
Theplayercharacteraswellasanyothercomponentofthegame
shouldbeanimated.Ifthecomponentisimplementedusingsimple
QtQuickshapes,itisquiteeasytodobychangingtheitem's
propertiesfluently,usingpropertyanimations(aswedidwith
theCoinobject).Thingsgetmoredifficultifacomponentiscomplex
enoughthatitiseasiertodrawitinagraphicsprogramandusean
imageinthegameinsteadoftryingtorecreatetheobjectusingQt
Quickitems.Then,youneedanumberofimages—oneforevery
frameofanimation.Imageswouldhavetokeepreplacingone
anothertomakeaconvincinganimation.
Timeforaction–Implementing
simplecharacteranimation
Let'strytomaketheplayercharacteranimatedinasimpleway.In
thematerialsthatcomewiththisbook,youwillfindanumberof
imageswithdifferentwalkingphasesforBenjamintheElephant.
Youcanusethem,oryoucandrawordownloadsomeotherimages
tobeusedinplaceofthoseprovidedbyus.
Putallimagesinonedirectory(forexample,images)andrename
themsothattheyfollowapatternthatcontainsthebaseanimation
namefollowedbyaframenumber,for
example,walking_01,walking_02,walking_03,andsoon.
Next,openthePlayer.qmldocumentandreplacetheimageelement
showingelephant.pngwiththefollowingcode:
Image{
id:elephantImage
propertyintcurrentFrame:1
propertyintframeCount:7
source:"images/walking_"+currentFrame+".png"
mirror:player.facingLeft
anchors.bottom:parent.bottom
anchors.horizontalCenter:parent.horizontalCenter
NumberAnimationoncurrentFrame{
from:1
to:frameCount
loops:Animation.Infinite
duration:elephantImage.frameCount*40
running:player.walking
}
}
IntherootelementofPlayer.qml,addthefollowingproperties:
propertyboolwalking:flags.horizontal!==0
propertyboolfacingLeft:flags.horizontal<0
StarttheprogramandusethearrowkeystoseeBenjaminmove.
Whatjusthappened?
Anumberofimageswerepreparedfollowingacommonnaming
patterncontaininganumber.Alltheimageshavethesamesize.
Thisallowsustoreplaceoneimagewithanotherjustbychanging
thevalueofthesourcepropertytopointtoadifferentimage.To
makeiteasier,weintroducedapropertycalled
thecurrentFrameelementthatcontainstheindexoftheimagetobe
displayed.WeusedthecurrentFrameelementinastring,formingan
expressionboundtothesourceelementoftheimage.Tomake
substitutingframeseasy,aNumberAnimationelementwasdeclaredto
modifythevaluesofthecurrentFrameelementinaloopfrom1tothe
numberofanimationframesavailable(representedby
theframeCountproperty)sothateachframeisshownfor40
milliseconds.
Theanimationisplayingifthewalkingpropertyevaluates
totrue(basedonthevalueoftheflags.horizontalelementintheplayer
object).Finally,weusethemirrorpropertyoftheImageparameterto
fliptheimageifthecharacteriswalkingleft.
Theprecedingapproachworks,butit'snotperfect.Thecomplexity
ofthedeclarationfollowingthispatterngrowsmuchfasterthan
requiredwhenwewanttomakemovementanimationmore
complex(forexample,ifwewanttointroducejumping).Thisisnot
theonlyproblem,though.Loadingimagesdoesnothappen
instantly.Thefirsttimeaparticularimageistobeused,the
animationcanstallforamomentwhilethegraphicsgetloaded,
whichmayruintheuserexperience.Lastly,itissimplymessyto
haveabunchofpictureshereandthereforeveryimageanimation.
Asolutiontothisistouseaspritesheet—asetofsmallimages
combinedintoasinglelargerimageforbetterperformance.Qt
Quicksupportsspritesheetsthroughitsspriteenginethathandles
loadingsequencesofspritesfromasingleimage,animatingthem,
andtransitioningbetweendifferentsprites.
InQtQuick,aspritesheetcanbeanimageofanytypesupportedby
Qtthatcontainsanimagestripwithallframesoftheanimation.
Subsequentframesshouldformacontinuouslineflowingfromleft
torightandfromthetoptothebottomoftheimage.However,they
donothavetostartinthetop-leftcornerofthecontainingimage,
nordotheyhavetoendinitsbottom-rightcorner—asinglefilecan
containmanysprites.Aspriteisdefinedbyprovidingthesizeofa
singleframeinpixelsandaframecount.Optionally,youcanspecify
anoffsetfromthetop-leftcornerwherethefirstframeofthesprite
istobereadfrom.Thefollowingdiagramcanbehelpfulin
visualizingthescheme:
QMLoffersaSpriteelementtypewithasourcepropertypointingto
theURLofthecontainerimage,
theframeWidthandframeHeightpropertiesdeterminingthesizeofeach
frame,andaframeCountpropertydefiningthenumberofframesinthe
sprite.Offsettingtheimagecanbeachievedbysettingvaluesof
theframeXandframeYproperties.Inadditiontothis,someadditional
propertiesarepresent;themostimportantthree
areframeRate,frameDuration,andduration.Alltheseservetodeterminethe
paceoftheanimation.IftheframeRateelementisdefined,itis
interpretedasanumberofframestocyclethroughpersecond.If
thispropertyisnotdefined,thentheframeDurationelementkicksin
andistreatedasaperiodoftimeinwhichtodisplayasingleframe
(thus,itisdirectlyaninverseoftheframeRateelement).Ifthis
propertyisnotdefinedaswell,thedurationelementisused,which
carriesthedurationofthewholeanimation.Youcansetanyof
thesethreeproperties,butyoudon'tneedtosetmorethanoneof
them.
Timeforaction–Animating
charactersusingsprites
Let'swaitnofurther.Thetaskathandistoreplacethemanual
animationfromthepreviousexercisewithaspritesheetanimation.
OpenthePlayer.qmldocument,removethewholeimageelement
responsiblefordisplayingtheplayercharacter,andaddthe
followingcode:
AnimatedSprite{
id:sprite
source:"images/sprite.png"
frameX:560
frameY:0
frameWidth:80
frameHeight:52
frameCount:7
frameRate:10
interpolate:true
width:frameWidth
height:frameHeight
running:player.walking
anchors.bottom:parent.bottom
anchors.horizontalCenter:parent.horizontalCenter
transform:Scale{
origin.x:sprite.width/2
xScale:player.facingLeft?-1:1
}
}
Whatjusthappened?
Wereplacedthepreviousstaticimagewithanever-changingsource
withadifferentitem.AstheSpriteparameterisnotanItemelement
butadatadefinitionofasprite,wecannotuseitinplaceof
theImageelement.Instead,wewillusetheAnimatedSpriteelement,
whichisanitemthatcandisplayasingleanimatedspritedefined
inline.ItevenhasthesamesetofpropertiesastheSpriteparameter.
Wedefinedaspriteembeddedinimages/sprite.pngwithawidth
of80andaheightof52pixels.Thespriteconsistsofsevenframes
thatshouldbedisplayedatarateof10framespersecond.The
running
propertyissetupsimilartotheoriginalAnimationelement.Asthe
AnimatedSprite
elementdoesnothaveamirrorproperty,weemulateitbyapplyinga
scaletransformationthatflipstheitemhorizontallyif
theplayer.facingLeftexpressionevaluatestotrue.Additionally,weset
theinterpolatepropertytotrue,whichmakesthespriteengine
calculatesmoothertransitionsbetweenframes.
Theresultweareleftwithissimilartoanearlierattempt,soifthese
twoaresimilar,whybotherusingsprites?Inmanysituations,you
wantmorecomplexanimationthanjustasingle-framesequence.
WhatifwewanttoanimatethewayBenjaminjumpsinadditionto
walking?Embeddingmoremanualanimations,althoughpossible,
wouldexplodethenumberofinternalvariablesrequiredtokeepthe
stateoftheobject.Fortunately,theQtQuickspriteenginecandeal
withthat.TheAnimatedSpriteelementweusedprovidesasubsetof
featuresofthewholeframework.Bysubstitutingtheitemwith
theSpriteSequenceelement,wegainaccesstothefullpowerofsprites.
Whilstwe'reonthesubjectofSprite,weneedtotellyouaboutone
additionalpropertyoftheobject,apropertycalledtothatcontainsa
mapofprobabilitiesoftransitioningfromthecurrentspriteto
anotherone.Bystatingwhichspritesthecurrentonemigratesto,
wecreateastatemachinewithweightedtransitionstoothersprites
aswellascyclingbacktothecurrentstate.
Transitioningtoanotherspriteistriggeredbysetting
thegoalSpritepropertyontheSpriteSequenceobject.Thiswillcausethe
spriteenginetotraversethegraphuntilitreachestherequested
state.Itisagreatwaytofluentlyswitchfromoneanimationto
anotherbygoingthroughanumberofintermediatestates.
Insteadofaskingthespritemachinetogracefullytransittoagiven
state,youcanaskittoforceanimmediatechangebycalling
theSpriteSequenceclass'sjumpTo()methodandfeedingitthenameofthe
spritethatshouldstartplaying.
Thelastthingthatneedstobeclarifiedishowtoactuallyattachthe
spritestatemachinetotheSpriteSequenceclass.Itisveryeasy—just
assignanarrayoftheSpriteobjectstothespritesproperty.
Timeforaction–Adding
jumpingwithspritetransitions
Let'sreplacetheAnimatedSpriteclasswiththeSpriteSequenceclassinthe
BejamintheElephantanimation,addingaspritetobeplayed
duringthejumpingphase.
OpenthePlayer.qmlfileandreplacetheAnimatedSpriteobjectwiththe
followingcode:
SpriteSequence{
id:sprite
width:80
height:52
interpolate:false
anchors.bottom:parent.bottom
anchors.horizontalCenter:parent.horizontalCenter
running:true
Sprite{
name:"still"
source:"images/sprite.png"
frameCount:1
frameWidth:80;frameHeight:52
frameDuration:100
to:{"still":1,"walking":0,"jumping":0}
}
Sprite{
name:"walking"
source:"images/sprite.png"
frameX:560;frameY:0
frameCount:7
frameWidth:80;frameHeight:52
frameRate:20
to:{"walking":1,"still":0,"jumping":0}
}
Sprite{
name:"jumping"
name:"jumping"
source:"images/sprite.png"
frameX:480;frameY:52
frameCount:11
frameWidth:80;frameHeight:70
frameDuration:50
to:{"still":0,"walking":0,"jumping":1}
}
transform:Scale{
origin.x:sprite.width/2
xScale:player.facingLeft?-1:1
}
}
Next,extendthejumpAnimobjectbyaddingthehighlightedchanges:
SequentialAnimation{
id:jumpAnim
running:false
ScriptAction{
script:{
sprite.goalSprite="jumping";
}
}
NumberAnimation{
target:player;property:"y"
to:player.parent.height-50
easing.type:Easing.OutQuad
}
NumberAnimation{
target:player;property:"y"
to:player.parent.height
easing.type:Easing.InQuad
}
ScriptAction{
script:{
sprite.goalSprite="";
sprite.jumpTo("still");
}
}
}
Whatjusthappened?
TheSpriteSequenceelementwehaveintroducedhasitsItemelements-
relatedpropertiessetupinthesamewayas
theAnimatedSpriteelement.Apartfromthat,aspritecalled"still"was
explicitlysetasthecurrentone.Wedefinedanumber
ofSpriteobjectsaschildrenoftheSpriteSequenceelement.Thisis
equivalenttoassigningthosespritestothespritespropertyofthe
object.Thecompletestatemachinethatwasdeclaredispresented
inthefollowingdiagram:
Aspritecalled"still"hasjustasingleframerepresentingasituation
whenBenjamindoesn'tmove.Thespritekeepsspinninginthe
samestateduetotheweightedtransitionbacktothe"still"state.
Thetworemainingtransitionsfromthatstatehavetheirweightsset
to0,whichmeanstheywillnevertriggerspontaneously,butthey
canbeinvokedbysettingthegoalSpritepropertytoaspritethatcan
bereachedbyactivatingoneofthosetransitions.
Thesequentialanimationwasextendedtotriggerspritechanges
whentheelephantliftsintotheair.
Haveagohero–Making
Benjaminwigglehistailin
anticipation
Topracticespritetransitions,yourgoalistoextendthestate
machineofBenjamin'sSpriteSequenceelementtomakehimwigglehis
tailwhentheelephantisstandingstill.Youcanfindtheappropriate
spriteinthematerialsthatcomeincludedwiththisbook.Thesprite
fieldiscalledwiggling.png.Implementthefunctionalitybymakingit
probablethatBenjaminspontaneouslygoesfromthe"still"stateto
"wiggling".Payattentiontoensurethattheanimalstopswiggling
andstartswalkingthemomenttheplayeractivatestherightorleft
arrowkeys.
Timeforaction–Revisiting
parallaxscrolling
WealreadydiscussedtheusefultechniqueofparallaxscrollinginCh
apter6,QtCoreEssentials.Itgivestheimpressionofdepthfor2D
gamesbymovingmultiplelayersofbackgroundatadifferentspeed
dependingontheassumeddistanceofthelayerfromtheviewer.
Let'sseehoweasyitistoapplythesametechniqueinQtQuick.
Wewillimplementparallaxscrollingwithasetoflayersthatmove
inthedirectionoppositetotheonetheplayerismovingin.
Therefore,wewillneedadefinitionofthesceneandamovinglayer.
CreateanewQMLFile(QtQuick2).CallitParallaxScene.qml.The
scenewillencompassthewholegame"level"andwillexposethe
positionoftheplayertothemovinglayers.Putthefollowingcodein
thefile:
importQtQuick2.9
Item{
id:root
propertyintcurrentPos
x:-currentPos*(root.width-root.parent.width)/width
}
Then,createanotherQMLfileandcallitParallaxLayer.qml.Makeit
containthefollowingdefinition:
importQtQuick2.9
Item{
propertyrealfactor:0
x:factor>0?-parent.currentPos/factor-parent.x:0
x:factor>0?-parent.currentPos/factor-parent.x:0
}
Now,let'susethetwonewelementtypesinthemainQML
document.We'lltakeelementsfromtheearlierscenedefinitionand
makethemintodifferentparallaxlayers—thesky,thetrees,andthe
grass:
Rectangle{
id:view
width:600
height:380
ParallaxScene{
id:scene
width:1500;height:380
anchors.bottom:parent.bottom
currentPos:player.x
ParallaxLayer{
factor:7.5
width:sky.width;height:sky.height
anchors.bottom:parent.bottom
Image{id:sky;source:"images/sky.png"}
Item{
id:sun
//...
}
}
ParallaxLayer{
factor:2.5
width:trees.width;height:trees.height
anchors.bottom:parent.bottom
Image{id:trees;source:"images/trees.png"}
}
ParallaxLayer{
factor:0
width:grass.width;height:grass.height
anchors.bottom:parent.bottom
Image{id:grass;source:"images/grass.png"}
}
Item{
id:player
//...
//...
}
Component{
id:coinGenerator
Coin{}
}
Timer{
id:coinTimer
//...
onTriggered:{
varcx=Math.floor(Math.random()*scene.width);
varcy=Math.floor(Math.random()*scene.height/3)
+
scene.height/2;
coinGenerator.createObject(scene,{x:cx,y:cy});
}
}
}
}
Youcannowrunthegameandobservethemovementof
backgroundlayerswhentheplayermovesaround:
Whatjusthappened?
TheParallaxSceneelementweimplementedisamovingplane.Its
horizontaloffsetdependsonthecharacter'scurrentpositionand
thesizeoftheview.Therangeofscrollofthesceneisdetermined
bythedifferencebetweenthescenesizeandtheviewsize—itsays
howmuchscrollingwehavetodowhenthecharactermovesfrom
theleftedgetotherightedgeofthescenesothatitisinviewallthe
time.Ifwemultiplythatbythedistanceofthecharacterfromthe
leftedgeofthesceneexpressedasafractionofthescenewidth,we
willgettheneededsceneoffsetintheview(orotherwisespeaking,a
projectionoffsetofthescene).
Thesecondtype—ParallaxLayer—isalsoamovingplane.Itdefinesa
distancefactorthatrepresentstherelativedistance(depth)ofthe
layerbehindtheforeground,whichinfluenceshowfasttheplane
shouldbescrolledcomparedtotheforeground(scene).Thevalue
of0meansthatthelayershouldbemovingwithexactlythesame
speedastheforegroundlayer.Thelargerthevalue,theslowerthe
layermovesascomparedtothecharacter.Theoffsetvalueis
calculatedbydividingthecharacter'spositioninthescenebythe
factor.Sincetheforegroundlayerisalsomoving,wehavetotakeit
intoconsiderationwhencalculatingtheoffsetforeachparallax
layer.Thus,wesubtractthehorizontalpositionofthescenetoget
theactuallayeroffset.
Havingthelayerslogicallydefined,wecanaddthemtothescene.
Eachlayerhasaphysicalrepresentationinourcase,staticimages
containingtexturesofthesky,trees,andgrass.Eachlayerisdefined
separatelyandcanliveitsownlife,containingstaticandanimated
elementsthathavenoinfluenceonremaininglayers.Forexample,
weputthesunobjectintotheskylayer,soitwillmovealongwith
theskylayerinadditiontoplayingitsownanimations.
Finally,sincewenolongerhavetherootelement,wemodified
thecoinTimerhandlertousethesceneelementinstead.
Haveagohero–Vertical
parallaxsliding
Asanadditionalexercise,youmaywanttoimplementavertical
parallaxslidinginadditiontoahorizontalone.Justmakeyour
scenebiggerandhaveitexposetheverticalscrollpositionin
additiontothehorizontalonereportedbythecurrentPoselement.
Then,justrepeatallthecalculationsfortheypropertyofeachlayer
andyoushouldbedoneinnotime.Rememberthatdistancefactors
forxandymaybedifferent.
Collisiondetection
Thereisnobuilt-insupportforcollisiondetectioninQtQuick,but
therearethreewaysofprovidingsuchsupport.First,youcanusea
readycollisionsystemavailableinanumberof2Dphysicsengines
suchasBox2D.Secondly,youcanimplementasimplecollision
systemyourselfinC++.Lastly,youcandocollisionchecking
directlyinJavaScriptbycomparingobjectcoordinatesand
boundingboxes.
Ourgameisverysimple;therefore,wewillusethelastapproach.If
wehadalargernumberofmovingobjectsinvolvedinourgame,we
wouldprobablychoosethesecondapproach.Thefirstapproachis
bestifyouhaveanobjectofnon-rectangularshapesthatcanrotate
andbounceoffotherobjects.Inthiscase,havingaphysicsengineat
handbecomesreallyuseful.
Timeforaction–Collecting
coins
FromQtCreator'smenu,accessFile—NewFileorProject.FromQt
category,choosetheJSFiletemplate.Callthecollisions.jsfile.Put
thefollowingcontentintothedocument:
.pragmalibrary
functionboundingBox(object1){
varcR=object1.childrenRect;
varmapped=object1.mapToItem(
object1.parent,cR.x,cR.y,cR.width,cR.height);
returnQt.rect(mapped.x,mapped.y,mapped.width,mapped.height);
}
functionintersect(object1,object2){
varr1=boundingBox(object1);
varr2=boundingBox(object2);
return(r1.x<=r2.x+r2.width&&//r1.left<=r2.right
r2.x<=r1.x+r1.width&&//r2.left<=r1.right
r1.y<=r2.y+r2.height&&//r1.top<=r2.bottom
r2.y<=r1.y+r1.height);//r2.top<=r1.bottom
}
CreateanotherJSFileandcallitcoins.js.Enterthefollowing:
.import"collisions.js"asCollisions
varcoins=[]
coins.collisionsWith=function(player){
varcollisions=[];
for(varindex=0;index<coins.length;++index){
varobj=this[index];
if(Collisions.intersect(player,obj)){
collisions.push(obj);
collisions.push(obj);
}
}
returncollisions;
};
coins.remove=function(obj){
vararr=Array.isArray(obj)?obj:[obj];
varL=arr.length;
varidx,needle;
while(L&&this.length){
needle=arr[--L];
idx=this.indexOf(needle);
if(idx!==-1){
this.splice(idx,1);
}
}
returnthis;
};
Finally,openthemain.qmlfileandaddthefollowingimportstatement:
import"coins.js"asCoins
Intheplayerobject,definethecheckCollisions()function:
functioncheckCollisions(){
varresult=Coins.coins.collisionsWith(player);
if(result.length===0)return;
result.forEach(function(coin){coin.hit()});
Coins.coins.remove(result)//preventthecoinfrombeinghit
again
}
Next,modifythecoinTimerhandlertopushnewcoinstothelist:
Timer{
id:coinTimer
//...
onTriggered:{
varcx=Math.floor(Math.random()*scene.width);
varcy=scene.height-60-Math.floor(Math.random()*60);
varcoin=coinGenerator.createObject(scene,{x:cx,y:cy});
Coins.coins.push(coin);
Coins.coins.push(coin);
}
}
Lastly,inthesameplayerobject,triggercollisiondetectionby
handlingthepositionchangesoftheplayer:
onXChanged:{
checkCollisions();
}
onYChanged:{
checkCollisions();
}
IntheCoin.qmlfile,defineananimationandahit()function:
SequentialAnimation{
id:hitAnim
running:false
NumberAnimation{
target:coin
property:"opacity"
from:1;to:0
duration:250
}
ScriptAction{
script:coin.destroy()
}
}
functionhit(){
hitAnim.start();
}
Whatjusthappened?
Thecollisions.jsfilecontainsfunctionsusedtodocollisionchecking.
Thefirstlineofthefileisa.pragmalibrarystatement,notingthatthis
documentonlycontainsfunctionsanddoesnotcontainany
mutableobject.Thisstatementmarksthedocumentasalibrary
thatcanbesharedbetweendocumentsthatimportit.Thisaidsin
reducedmemoryconsumptionandimprovedspeed,astheengine
doesn'thavetoreparseandexecutethedocumenteachtimeitis
imported.
Thefunctionsdefinedinthelibraryarereallysimple.Thefirstone
returnsaboundingrectangleofanobjectbasedonitscoordinates
andthesizeofitschildren.Itassumesthatthetop-levelitemis
emptyandcontainschildrenthatrepresentthevisualaspectofthe
object.ChildrencoordinatesaremappedusingthemapToItemfunction
sothattherectanglereturnedisexpressedintheparentitem
coordinates.Thesecondfunctiondoesatrivialcheckingof
intersectionbetweentwoboundingrectanglesandreturnstrueif
theyintersectandfalseotherwise.
Theseconddocumentkeepsadefinitionofanarrayofcoins.Itadds
twomethodstothearrayobject.Thefirstone—collisionsWith—
performsacollisioncheckbetweenanyoftheitemsinthearrayand
thegivenobjectusingfunctionsdefinedincollisions.js.That'swhy
weimportthelibraryatthestartofthedocument.Themethod
returnsanotherarraythatcontainsobjectsintersecting
theplayerargument.Theothermethod,calledremove,takesanobject
oranarrayofobjectsandremovesthemfromcoins.
Thedocumentisnotalibrary;therefore,eachdocumentthat
importscoins.jswouldgetitsownseparatecopyoftheobject.Thus,
weneedtoensurethatcoins.jsisimportedonlyonceinthegameso
thatallreferencestotheobjectsdefinedinthatdocumentrelateto
thesameinstanceoftheobjectinourprogrammemory.
Ourmaindocumentimportscoins.js,whichcreatesthearrayfor
storingcoinobjectsandmakesitsauxiliaryfunctionsavailable.This
allowsthedefinedcheckCollisions()functiontoretrievethelistof
coinscollidingwiththeplayer.Foreachcointhatcollideswiththe
player,weexecuteahit()method;asalaststep,allcollidingcoins
areremovedfromthearray.Sincecoinsarestationary;collisioncan
onlyoccurwhentheplayercharacterentersanareaoccupiedbya
coin.Therefore,itisenoughtotriggercollisiondetectionwhenthe
positionoftheplayercharacterchanges—weusetheonXChanged
andonYChanged
handlers.
Ashittingacoinresultsinremovingitfromthearray,welosea
referencetotheobject.Thehit()methodhastoinitiateremovalof
theobjectfromthescene.Aminimalisticimplementationofthis
functionwouldbetojustcallthedestroy()functionontheobject,but
wedomore—theremovalcanbemadesmootherbyrunningafade-
outanimationonthecoin.Asalaststep,theanimationcandestroy
theobject.
Thenumberofobjectswetrackonthesceneisreallysmall,andwesimplifytheshapeof
eachobjecttoarectangle.ThisletsusgetawaywithcheckingcollisionsinJavaScript.For
alargeramountofmovingobjects,customshapes,andhandlingrotations,itismuch
bettertohaveacollisionsystembasedonC++.Thelevelofcomplexityofsuchasystem
dependsonyourneeds.
Haveagohero–Extendingthe
game
Youcanpolishyourgamedevelopmentskillsbyimplementingnew
gamemechanicsinourjumpingelephantgame.Forexample,you
canintroduceaconceptoffatigue.Themorethecharacterjumps,
themoretiredtheygetandtheslowertheybegintomoveandhave
toresttoregainspeed.Tomakethegamemoredifficult,attimes
movingobstaclescanbegenerated.Whenthecharacterbumpsinto
anyofthem,theygetmoreandmoretired.Whenthefatigue
exceedsacertainlevel,thecharacterdiesandthegameends.
Theheartbeatdiagramwepreviouslycreatedcanbeusedto
representthecharacter'sleveloffatigue—themoretiredthe
charactergets,thefastertheirheartbeats.
Therearemanywaysthesechangescanbeimplemented,andwe
wanttogiveyoualeveloffreedom,sowewillnotprovideastep-by-
stepguideonhowtoimplementacompletegame.Youalready
knowalotaboutQtQuick,andthisisagoodopportunitytotest
yourskills!
Popquiz
Q1.Whichofthefollowingtypescannotbeusedwiththespecialon-
propertysyntax?
1. Animation
2. Transition
3. Behavior
Q2.WhichQMLtypeallowsyoutoconfigureaspriteanimation
withtransitionsbetweenmultiplestates?
1. SpriteSequence
2. Image
3. AnimatedSprite
Q3.WhichQMLtypeisabletopreventanyinstantchangeofthe
property'svalueandperformagradualchangeofvalueinstead?
1. Timer
2. Behavior
3. PropertyAction
Summary
Inthischapter,weshowedyouhowtoextendyourQtQuickskills
tomakeyourapplicationsdynamicandattractive.Wewentthrough
theprocessofrecreatingandimprovingagamecreatedearlierin
C++tofamiliarizeyouwithconceptssuchascollisiondetection,
state-drivenobjects,andtime-basedgameloops.Youarenow
familiarwithallthemostimportantconceptsrequiredtomake
gamesusingQtQuick.
Inthenextchapter,wewillturnourattentiontotechniquesthat
willmakeyourgamesevenmorevisuallyappealing.We'llexplore
thebuilt-ingraphicaleffectsQtQuickprovides.Youwillalsolearn
toextendQtQuickwithcustompainteditemsimplementedinC++.
Thiswillgiveyouthefreedomtocreateanyvisualeffectsyouhave
inmind.
AdvancedVisualEffectsinQt
Quick
Spriteanimationsandsmoothtransitionsarenotalwaysenoughto
makethegamevisuallyappealing.Inthischapter,wewillexplore
manywaystoaddsomeeyecandytoyourgames.QtQuickprovides
adecentamountofbuilt-invisualeffectsthatwillcomeinhandy.
However,fromtimetotime,youwillwanttodosomethingthatis
notpossibletodowithstandardcomponents—somethingunique
andspecifictoyourgame.Inthesecases,youdon'tneedtolimit
yourimagination.WewillteachyoutodivedeepintotheC++API
ofQtQuicktoimplementtrulyuniquegraphicseffects.
Themaintopicscoveredinthischapterarethese:
Auto-scalinguserinterfaces
Applyinggraphicaleffectstotheexistingitems
Particlesystems
OpenGLpaintinginQtQuick
UsingQPainterinQtQuick
Makingthegamemore
attractive
Agameshouldnotjustbebaseduponaninterestingidea,andit
shouldnotonlyworkfluentlyonarangeofdevicesandgive
entertainmenttothosepeopleplayingit.Itshouldalsolooknice
andbehavenicely.Whetherpeoplearechoosingfromanumberof
similarimplementationsofthesamegameorwanttospendmoney
onanothersimilarlypricedandentertaininggame,thereisagood
chancethatthey'llchoosethegamethatlooksthebest—havingalot
ofanimations,graphics,andflashycontent.Wealreadylearneda
numberoftechniquestomakeagamemorepleasingtotheeye,
suchasusinganimationsorimplementingparallaxeffect.Here,we
willshowyouanumberofothertechniquesthatcanmakeyourQt
Quickapplicationsmoreattractive.
Auto-scalinguserinterfaces
Thefirstextensionyoumayimplementismakingyourgameauto-
adjusttothedeviceresolutionitisrunningon.Therearebasically
twowaystoaccomplishthis.Thefirstistocentertheuserinterface
inthewindow(orscreen)andifitdoesn'tfit,enablescrolling.The
otherapproachistoscaletheinterfacetoalwaysfitthewindow(or
screen).Whichonetochoosedependsonanumberoffactors,the
mostimportantofwhichiswhetheryourUIisgoodenoughwhen
upscaled.Iftheinterfaceconsistsoftextandnon-imageprimitives
(basicallyrectangles),orifitincludesimagesbutonlyvectorones
orthosewithveryhighresolution,thenitisprobablyfinetotryand
scaletheuserinterface.Otherwise,ifyouusealotoflow-resolution
bitmapimages,youwillhavetochooseoneparticularsizeforthe
UI(optionallyallowingittodownscale,sincethequality
degradationshouldbelesssignificantinthisdirectionifyouenable
anti-aliasing).
Whetheryouchoosetoscaleortocenterandscroll,thebasic
approachisthesame—youputyourUIiteminanotheritemsothat
youhavefinecontrolovertheUIgeometry,regardlessofwhat
happenstothetop-levelwindow.Takingthecenteredapproachis
quiteeasy—justanchortheUItothecenteroftheparent.Toenable
scrolling,wraptheUIintheFlickableitemandconstrainitssizeif
thesizeofthewindowisnotbigenoughtofitthewholeuser
interface:
Window{
//...
Flickable{
id:uiFlickable
anchors.centerIn:parent
contentWidth:ui.width
contentHeight:ui.height
width:parent.width>=contentWidth?
contentWidth:parent.width
height:parent.height>=contentHeight?
contentHeight:parent.height
UI{
id:ui
}
}
}
YoucanputthefollowingsimplecodeintotheUI.qmlfiletoseehow
FlickablepositionstheUIitem:
importQtQuick2.0
Rectangle{
width:300
height:300
gradient:Gradient{
GradientStop{position:0.0;color:"lightsteelblue"}
GradientStop{position:1.0;color:"blue"}
}
}
Youshouldprobablydecoratethetop-levelitemwithanice
backgroundiftheUIitemdoesnotoccupythefullareaofits
parent.
Scalingseemsmorecomplicated,butitisreallyeasywithQtQuick.
Again,youhavetwochoices—eitherstretchorscale.Stretchingisas
easyasexecutingtheanchors.fill:parentcommand,whicheffectively
forcestheUItorecalculatethegeometryofallitsitems,butit
possiblyallowsustousethespacemoreefficiently.Itis,ingeneral,
verytime-consumingforthedevelopertoprovideexpressionsfor
calculatingthegeometryofeachandeveryelementintheuser
interfaceasthesizeoftheviewchanges.Thisisusuallynotworth
theeffort.AsimplerapproachistojustscaletheUIitemtofitthe
window,whichwillimplicitlyscalethecontaineditems.Insuchan
event,theirsizecanbecalculatedrelativetothebasesizeofthe
mainviewoftheuserinterface.Forthistowork,youneedto
calculatethescalethatistobeappliedtotheuserinterfacetomake
itfillthewholespaceavailable.Theitemhasascaleof1whenits
effectivewidthequalsitsimplicitwidthanditseffectiveheight
equalsitsimplicitheight.Ifthewindowislarger,wewanttoscale
uptheitemuntilitreachesthesizeofthewindow.
Therefore,thewindow'swidthdividedbytheitem'simplicitwidth
willbetheitem'sscaleinthehorizontaldirection.Thisisshownin
thefollowingdiagram:
Thesamecanbeappliedtotheverticaldirection,butiftheUIhasa
differentaspectratiothanthewindow,itshorizontalandvertical
scalefactorswillbedifferent.FortheUItolooknice,wehaveto
takethelowerofthetwovalues—toonlyscaleupasmuchasthe
directionwithlessspaceallows,leavingagapintheotherdirection:
Window{
//...
UI{
UI{
id:ui
anchors.centerIn:parent
scale:Math.min(parent.width/width,
parent.height/height)
}
}
Again,itmaybeagoodideatoputsomebackgroundonthe
windowitemtofillinthegaps.
Whatifyouwanttosavesomemarginbetweentheuserinterface
andthewindow?Youcan,ofcourse,takethatintoconsideration
whencalculatingthescale((window.width-2*margin)/width,andsoon)
butthereisaneasierway—simplyputanadditionaliteminsidethe
window,leavinganappropriatemargin,andputtheuserinterface
iteminthatadditionalitemandscaleituptotheadditionalitem's
size:
Window{
//...
Item{
anchors{
fill:parent
margins:10
}
UI{
id:ui
anchors.centerIn:parent
scale:Math.min(parent.width/width,
parent.height/height)
}
}
}
Whenyouscaleelementsalot,youshouldconsiderenablinganti-
aliasingforitemsthatcanlosequalitywhenrenderedinasize
differentthantheirnativesize(forexample,images).Thisisdone
veryeasilyinQtQuick,aseachIteminstancehasaproperty
calledantialiasingwhich,whenenabled,willcausetherendering
backendtotrytoreducedistortionscausedbythealiasingeffect.
Rememberthatthiscomesatthecostofincreasedrendering
complexity,sotrytofindabalancebetweenqualityandefficiency,
especiallyonlow-endhardware.Youmayprovideanoptiontothe
usertogloballyenableordisableanti-aliasingforallgameobjects
ortograduallyadjustqualitysettingsfordifferentobjecttypes.
Graphicaleffects
ThebasictwopredefineditemsinQtQuickarerectangleand
image.Youcanusetheminavarietyofcreativewaysandmake
themmorepleasant-lookingbyapplyingGLSLshaders.However,
implementingashaderprogramfromscratchiscumbersomeand
requiresin-depthknowledgeoftheshaderlanguage.Luckily,a
numberofcommoneffectsarealreadyimplementedandreadyto
useintheformoftheQtGraphicalEffectsmodule.
Toaddasubtleblackshadowtoourcanvas-basedheartbeat
elementdefinedintheHeartBeat.qmlfile,useacodesimilartothe
followingthatmakesuseoftheDropShadoweffect:
importQtQuick2.9
importQtQuick.Window2.2
importQtGraphicalEffects1.0
Window{
//...
HeartBeat{
id:heartBeat
anchors.centerIn:parent
visible:false
}
DropShadow{
source:heartBeat
anchors.fill:heartBeat
horizontalOffset:3
verticalOffset:3
radius:8
samples:16
color:"black"
}
}
Toapplyashadoweffect,youneedanexistingitemasthesourceof
theeffect.Inourcase,weareusinganinstanceoftheHeartBeatclass
centeredinatop-levelitem.Then,theshadoweffectisdefinedand
itsgeometryfollowsthatofitssourceusingtheanchors.fillelement.
JustastheDropShadowclassrenderstheoriginalitemaswellasthe
shadow,theoriginalitemcanbehiddenbysetting
itsvisiblepropertytofalse:
MostoftheDropShadowclass'spropertiesareself-explanatory,buttwo
properties—radiusandsamples—requiresomeadditionalexplanation.
Theshadowisdrawnasablurredmonochromaticcopyofthe
originalitemoffsetbyagivenposition.Thetwomentioned
propertiescontroltheamountofbluranditsquality—themore
samplesusedforblurring,thebettertheeffect,butalsothemore
demandingthecomputationthatneedstobeperformed.
Speakingofblur,theplainblurringeffectisalsoavailableinthe
graphicseffectsmodulethroughtheGaussianBlurelementtype.To
applyablurinsteadofashadowtothelastexample,simplyreplace
theoccurrenceoftheDropShadowclasswiththefollowingcode:
GaussianBlur{
source:heartBeat
anchors.fill:heartBeat
radius:12
samples:20
transparentBorder:true
}
Thischangewillproducethefollowingresult:
Here,youcanseetwoearliermentionedpropertiesaswellasa
vaguelynamedtransparentBorderone.Enablingthispropertyfixes
someartifactsontheedgesoftheblurandingeneral,you'llwantto
keepitthatway.
Haveagohero–Theblur
parallaxscrolledgameview
Theblurpropertyisaveryniceeffectthatcanbeusedinmany
situations.Forexample,youcantrytoimplementafeaturewithin
ourelephantgamewherebywhentheuserpausesthegame(for
example,bypressingthePkeyonthekeyboard),theviewgets
blurred.Maketheeffectsmoothbyapplyingananimationtothe
effect'sradiusproperty.
AnotherinterestingeffectisGlow.Itrendersacoloredandblurred
copyofthesourceelement.Anexampleusecaseforgamesis
highlightingsomepartsoftheuserinterface—youcandirectthe
user'sattentiontotheelement(forexample,buttonorbadge)by
makingtheelementflashperiodically:
Window{
//...
Badge{
id:importantBadge
anchors.centerIn:parent
}
Glow{
source:importantBadge
anchors.fill:source
samples:64
color:"red"
SequentialAnimationonradius{
loops:Animation.Infinite
running:true
NumberAnimation{from:0;to:30;duration:500}
PauseAnimation{duration:100}
NumberAnimation{from:30;to:0;duration:500}
PauseAnimation{duration:1000}
}
}
}
}
Thecompletemodulecontains20differenteffects.Wecannot
describeeacheffectindetailhere.Nevertheless,youcanlearn
aboutityourself.Ifyouclonethemodule'ssourcegitrepository
(foundunderhttps://code.qt.io/cgit/qt/qtgraphicaleffects.git/),in
thetests/manual/testbedsubdirectoryoftheclonedrepository,youwill
findaniceapplicationfortestingtheexistingeffects.Torunthe
tool,openthetestBed.qmlfilewithqmlscene:
Youcanalsoaccessacompletelistofeffectsandtheirshortdescriptionsbysearching
forQtGraphicalEffectsinthedocumentationindex.
Particlesystems
Acommonlyusedvisualeffectingamesisgeneratingalarge
numberofsmall,usuallyshort-lived,oftenfast-moving,fuzzy
objectssuchasstars,sparks,fumes,dust,snow,splinters,falling
leaves,orthelike.Placingtheseasregularitemswithinascene
wouldgreatlydegradeperformance.Instead,aspecialengineis
used,whichkeepsaregistryofsuchobjectsandtracks(simulates)
theirlogicalattributeswithouthavingphysicalentitiesinthescene.
Suchobjects,calledparticles,arerendereduponrequestinthe
sceneusingveryefficientalgorithms.Thisallowsustousealarge
numberofparticleswithouthavinganegativeimpactontherestof
thescene.
QtQuickprovidesaparticlesystemintheQtQuick.Particlesimport.
TheParticleSystemelementprovidesthecoreforthesimulation,which
usestheEmitterelementstospawnparticles.Theyarethenrendered
accordingtodefinitionsinaParticlePainterelement.Simulated
entitiescanbemanipulatedusingtheAffectorobjects,whichcan
modifythetrajectoryorlifespanofparticles.
Let'sstartwithasimpleexample.Thefollowingcodesnippet
declaresthesimplestpossibleparticlesystem:
importQtQuick2.0
importQtQuick.Window2.2
importQtQuick.Particles2.0
Window{
visible:true
width:360
height:360
title:qsTr("Particlesystem")
ParticleSystem{
id:particleSystem
anchors.fill:parent
Emitter{anchors.fill:parent}
ImageParticle{source:"star.png"}
}
}
Theresultcanbeobservedinthefollowingimage:
Let'sanalyzethecode.AfterimportingQtQuick.Particles2.0,
aParticleSystemitemisinstantiatedthatdefinesthedomainofthe
particlesystem.Wedefinetwoobjectswithinthatsystem.Thefirst
objectistheEmitteranddefinesanareawhereparticleswillbe
spawned.Theareaissettoencompassthewholedomain.The
secondobjectisanobjectoftheImageParticletype,whichis
aParticlePaintersubclass.Itdeterminesthatparticlesshouldbe
renderedasinstancesofagivenimage.Bydefault,theEmitterobject
spawns10particlespersecond,eachofwhichlivesforonesecond
andthendiesandisremovedfromthescene.Inthecodepresented,
theEmitterandImageParticleobjectsaredirectchildrenof
theParticleSystemclass;however,thisdoesn'thavetobethecase.The
particlesystemcanbeexplicitlyspecifiedbysetting
thesystemproperty.
Tuningtheemitter
Youcancontroltheamountofparticlesbeingemittedbysetting
theemitRatepropertyoftheemitter.Anotherproperty,called
thelifeSpan,determineshowmanymillisecondsittakesbeforea
particledies.Tointroducesomerandombehavior,youcanuse
thelifeSpanVariationpropertytosetamaximumamountoftime(in
milliseconds)thelifespancanbealteredbythesystem(inboth
directions):
Emitter{
anchors.fill:parent
emitRate:350
lifeSpan:1500
lifeSpanVariation:400//effective:1100-1900ms
}
Apossibleresultofthischangeisshowninthefollowingpicture:
Increasingtheemissionrateandlifespanofparticlescanleadtoasituationinwhicha
verylargenumberofparticleshavetobemanaged(andpossiblyrendered).Thiscan
degradeperformance;thus,anupperlimitofparticlesthatcanconcurrentlybealivecan
besetthroughthemaximumEmittedproperty.
Tweakingthelifespanofparticlesmakesthesystemmorediverse.
Tostrengthentheeffect,youcanalsomanipulatethesizeofeach
particlethroughthesizeandsizeVariationproperties:
Emitter{
anchors.fill:parent
emitRate:50
size:12
sizeVariation:6
endSize:2
}
Thiswillgiveyouparticlesofdifferentsizes:
Therangeoffunctionalitypresentedthusfarshouldbeenoughto
createmanynice-lookingandusefulparticlesystems.However,
particlesareemittedfromthewholeareaoftheemitter,whichisa
regularQQuickItemandthusisrectangular.Thisdoesn'thavetobethe
case,though.TheEmitterelementcontainsashapeproperty,whichisa
waytodeclaretheareathatistobegivingbirthtoparticles.
TheQtQuick.Particlesmoduledefinesthreetypesofcustomshapethat
canbeused—EllipseShape,LineShape,andMaskShape.Thefirsttwoarevery
simple,definingeitheranemptyorfilledellipseinscribedinthe
itemoralinecrossingoneofthetwodiagonalsoftheitem.
TheMaskShapeelementismoreinteresting,asitmakesitpossibleto
useanimageasashapefortheEmitterelement:
Emitter{
anchors.fill:parent
emitRate:1600
shape:MaskShape{source:"star.png"}
}
Particlescannowonlyspawnwithinthespecifiedarea:
Renderingparticles
Sofar,wehaveusedabareImageParticleelementtorenderparticles.
ItisonlyoneofthethreeParticlePaintersavailable,withtheothers
beingItemParticleandCustomParticle.However,beforewemoveonto
otherrenderers,let'sfocusontweakingtheImageParticleelementto
obtainsomeinterestingeffects.
TheImageParticleelementrenderseachlogicalparticleasanimage.
Theimagecanbemanipulatedseparatelyforeachparticleby
changingitscolorandrotation,deformingitsshape,orusingitasa
spriteanimation.
Toinfluencethecolorofparticles,youcanuseanyofthelarge
numberofdedicatedproperties
—alpha,color,alphaVariation,colorVariation,redVariation,greenVariation,
andblueVariation.Thefirsttwopropertiesdefinethebasevaluefor
therespectiveattributes,andtheremainingpropertiessetthe
maximumdeviationofarespectiveparameterfromthebasevalue.
Inthecaseofopacity,thereisonlyonetypeofvariationyoucan
use,butwhendefiningthecolor,youcaneithersetdifferentvalues
foreachofthered,green,andbluechannels,oryoucanusethe
globalcolorVariationproperty,whichissimilartosettingthesame
valueforallthreechannels.Allowedvaluesareanybetweenthe
rangeof0(nodeviationallowed)and1.0(100%ineitherdirection).
Notethatwhenacolorisappliedtoanimage,therespectivecomponentsofthecolors(red,
green,blue,andalpha)aremultiplied.Blackcolor(0,0,0,1)hasallcomponentssetto0
exceptforalpha,soapplyingasolidcolortoablackimagewillnothaveanyeffect.Onthe
contrary,ifyourimagecontainswhitepixels(1,1,1,1),theywillbedisplayedinexactlythe
specifiedcolor.Transparentpixelswillstaytransparentbecausetheiralphacomponent
willremainsetto0.
Inourexample,wecancreateparticleswithdifferentcolorsusing
thefollowingcode:
ImageParticle{
source:"star_white.png"
colorVariation:1
}
Theresultshouldlooklikethis:
Thepropertiesmentionedarestationary—theparticleobeysthe
constantvalueduringitswholelife.TheImageParticleelementalso
exposestwoproperties,lettingyoucontrolthecolorofparticles
relativetotheirage.Firstofall,thereisapropertycalled
entryEffectthatdefineswhathappenswiththeparticleatitsbirthand
death.ThedefaultvalueisFade,whichmakesparticlesfadeinfrom0
opacityatthestartoftheirlifeandfadesthembackto0justbefore
theydie.Youhavealreadyexperiencedthiseffectinalltheearlier
particleanimationswedemonstrated.Othervaluesfortheproperty
areNoneandScale.Thefirstoneisobvious—thereisnoentryeffect
associatedwithparticles.Thesecondonescalesparticlesfrom0at
theirbirthandscalesthembackto0attheendoftheirlife.
Theothertime-relatedpropertyiscolorTable.Youcanfeeditwitha
URLofanimagetobeusedasaone-dimensionaltexture
determiningthecolorofeachparticleoveritslife.Atthebeginning,
theparticlegetscolor-definedbytheleftedgeoftheimageandthen
progressesrightinalinearfashion.Itismostcommontosetan
imageherecontainingacolorgradienttoachievesmooth
transitionsbetweencolors.
Thesecondparameterthatcanbealteredistherotationofa
particle.Here,wecanalsoeitherusepropertiesthatdefineconstant
valuesforrotation(rotationandrotationVariation)specifiedindegrees
ormodifytherotationofparticlesintime
withrotationVelocityandrotationVelocityVariation.Thevelocitydefinesthe
paceorrotationindegreespersecond.
Particlescanalsobedeformed.The
xVectorandyVectorpropertiesallowbindingvectors,whichdefine
distortionsinhorizontalandverticalaxes.Wewilldescribehowto
setthevectorsinthenextsection.Lastbutnotleast,using
thespritesproperty,youcandefinealistofspritesthatwillbeused
torenderparticles.Thisworksinafashionsimilartothe
SpriteSequencetypedescribedinthepreviouschapter.
Makingparticlesmove
Apartfromfadingandrotating,theparticlesystemswehaveseen
sofarwereverystatic.Whilethisisusefulformakingstarfields,it
isnotatallusefulforexplosions,sparks,orevenfallingsnow.This
isbecauseparticlesaremostlyaboutmovement.Here,wewillshow
youtwoaspectsofmakingyourparticlesfly.
Thefirstaspectismodelinghowtheparticlesareborn.Bythat,we
meanthephysicalconditionsoftheobjectcreatingtheparticles.
Duringanexplosion,matterispushedawayfromtheepicenterwith
averylargeforcethatcausesairandsmallobjectstorushoutward
atanextremelyhighspeed.Fumesfromarocketengineareejected
withhighvelocitiesinthedirectionoppositetothatofthepropelled
craft.Amovingcometdrawsalongabraidofdustandgasesput
intomotionbytheinertia.
Alltheseconditionscanbemodeledbysettingthevelocityor
accelerationoftheparticles.Thesetwometricsaredescribedby
vectorsdeterminingthedirectionandamount(magnitudeor
length)ofthegivenquantity.InQtQuick,suchvectorsare
representedbyanelementtypecalledDirection,wherethetailofthe
vectorisattachedtotheobjectandthepositionoftheheadis
calculatedbytheDirectioninstance.Sincewehavenomeansof
settingattributesonparticlesbecausewehavenoobjects
representingthem,thosetwoattributes—velocityandacceleration—are
appliedtoemittersspawningtheparticles.Asyoucanhavemany
emittersinasingleparticlesystem,youcansetdifferentvelocities
andaccelerationsforparticlesofdifferentorigins.
Therearefourtypesofdirectionelementsrepresentingdifferent
sourcesofinformationaboutthedirection.First,there
isCumulativeDirection,whichactsasacontainerforotherdirection
typesandworkslikeasumofdirectionscontainedwithin.
Then,thereisPointDirection,whereyoucan
specifythexandycoordinatesofapointwheretheheadofthe
vectorshouldbeattached.Toavoidtheunrealisticeffectofall
particlesheadinginthesamedirection,youcanspecifyxVariation
andyVariationtointroducealloweddeviationfromagivenpoint:
Thethirdtypeisthemostpopulardirectiontype—AngleDirection,
whichdirectlyspecifiestheangle(indegreesclockwisefrom
straightright)andmagnitude(inpixelspersecond)ofthevector.
TheanglecanvaryfromthebasebyangleVariation,and
similarly,magnitudeVariationcanbeusedtointroducevariationtothe
lengthofthevector:
Thelasttypeissimilartothepreviousone.TheTargetDirectionvector
canbeusedtopointthevectortowardthecenterofagivenQt
Quickitem(setwiththetargetItemproperty).Thelengthofthe
vectoriscalculatedbygivingthemagnitudeandmagnitudeVariation,and
bothcanbeinterpretedaspixelspersecondormultiplesofdistance
betweenthesourceandtargetpoints(dependingonthevalueof
theproportionalMagnitudeproperty):
Let'sgetbacktosettingparticlevelocity.Wecanuse
theAngleDirectionvectortospecifythatparticlesshouldbemoving
left,spreadingatamaximumof45degrees:
Emitter{
Emitter{
anchors.centerIn:parent
width:50;height:50
emitRate:50
velocity:AngleDirection{
angleVariation:45
angle:180
magnitude:200
}
}
Thiscodewillproducetheeffectshownonthefollowingpicture:
Settingaccelerationworksthesameway.Youcanevensetboththe
initialvelocityandtheaccelerationeachparticleshouldhave.Itis
veryeasytoshoottheparticlesintheleftdirectionandstartpulling
themdown:
Emitter{
anchors.right:parent.right
anchors.verticalCenter:parent.verticalCenter
emitRate:15
lifeSpan:5000
velocity:AngleDirection{
angle:180
magnitude:200
}
acceleration:AngleDirection{
angle:90//localleft=globaldown
angle:90//localleft=globaldown
magnitude:100
}
}
Thiscodewillproduceparticlesmovingalongasinglecurve:
TheEmitterelementhasonemorenicepropertythatisusefulinthe
contextofmovingparticles.SettingthevelocityFromMovementparameter
toavaluedifferentfrom0makesanymovementof
theEmitterelementapplytothevelocityoftheparticles.The
directionoftheadditionalvectormatchesthedirectionofthe
emitter'smovement,andthemagnitudeissettothespeedofthe
emittermultipliedbythevaluesettovelocityFromMovement.Itisagreat
waytogeneratefumesejectedfromarocketengine:
Item{
Image{
id:image
source:"rocket.png"
}
Emitter{
anchors.right:image.right
anchors.verticalCenter:image.verticalCenter
emitRate:500
lifeSpan:3000
lifeSpanVariation:1000
velocityFromMovement:-20
velocity:AngleDirection{
magnitude:100
angleVariation:40
}
}
NumberAnimationonx{
...
}
}
Thisishowtheresultcouldlooklike:
Thesecondwayofaddressingthebehaviorofparticlesisto
influencetheirattributesaftertheyareborn—inanyparticular
momentoftheirlife.Thiscanbedoneusingaffectors.Theseare
itemsinheritingaffector,whichcanmodifysomeattributesof
particlescurrentlytravelingthoughtheareaoftheaffector.Oneof
thesimplestaffectorsisAge.Itcanadvanceparticlestoapointin
theirlifetimewheretheyonlyhavelifeLeftmillisecondsoftheirlife
left:
Age{
once:true
lifeLeft:500
shape:EllipseShape{fill:true}
anchors.fill:parent
}
Settingoncetotruemakeseachaffectorinfluenceagivenparticleonly
once.Otherwise,eachparticlecanhaveitsattributesmodified
manytimes.
AnotheraffectortypeisGravity,whichcanaccelerateparticlesina
givenangle.Frictioncanslowparticlesdown,andAttractorwillaffect
theparticle'sposition,velocity,oraccelerationsothatitstarts
travelingtowardagivenpoint.Wanderisgreatforsimulating
snowflakesorbutterfliesflyinginpseudo-randomdirections.
Therearealsootheraffectortypesavailable,butwewillnotgointo
theirdetailshere.Wewouldliketowarnyou,however,against
usingaffectorstoooften—theycanseverelydegradeperformance.
Timeforaction–Vanishing
coinsspawningparticles
Itisnowtimetopracticeourfreshlyacquiredskills.Thetaskisto
addaparticleeffecttothegamewecreatedinthepreviouschapter.
Whentheplayercollectsacoin,itwillexplodeintoasprinkleof
colorfulstars.
Startbydeclaringaparticlesystemasfillingthegamescene,along
withtheparticlepainterdefinition:
ParticleSystem{
id:coinParticles
anchors.fill:parent//sceneistheparent
ImageParticle{
source:"images/particle.png"
colorVariation:1
rotationVariation:180
rotationVelocityVariation:10
}
}
Next,modifythedefinitionofCointoincludeanemitter:
Emitter{
id:emitter
system:coinParticles
emitRate:0
lifeSpan:1500
lifeSpanVariation:100
velocity:AngleDirection{
angleVariation:180
magnitude:10
}
acceleration:AngleDirection{
acceleration:AngleDirection{
angle:270
magnitude:30
}
}
Finally,thehit()functionhastobeupdated:
functionhit(){
emitter.burst(50);
hitAnim.start();
}
RunthegameandseewhathappenswhenBenjamincollectscoins:
Whatjusthappened?
Inthisexercise,wedefinedasimpleparticlesystemthatfillsthe
wholescene.Wedefinedasimpleimagepainterfortheparticles
whereweallowparticlestotakeonallthecolorsandstartinall
possiblerotations.Weusedastarpixmapasourparticletemplate.
Then,anEmitterobjectisattachedtoeverycoin.ItsemitRateissetto0,
whichmeansitdoesnotemitanyparticlesonitsown.Weseta
varyinglifespanonparticlesandletthemflyinalldirectionsby
settingtheirinitialvelocitywithananglevariationof180degreesin
bothdirections(givingatotalof360degrees).Bysettingan
acceleration,wegivetheparticlesatendencytotraveltowardthe
topedgeofthescene.
Inthehitfunction,wecallaburst()functionontheemitter,which
makesitgiveinstantbirthtoagivennumberofparticles.
CustomOpenGL-basedQt
Quickitems
InChapter12,CustomizationinQtQuick,welearnedtocreatenew
QMLelementtypesthatcanbeusedtoprovidedynamicdata
enginesorsomeothertypeofnon-visualobjects.Nowwewillsee
howtoprovidenewtypesofvisualitemstoQtQuick.
Thefirstquestionyoushouldaskyourselfiswhetheryoureally
needanewtypeofitem.Maybeyoucanachievethesamegoalwith
thealreadyexistingelements?Veryoften,youcanusevectoror
bitmapimagestoaddcustomshapestoyourapplications,oryou
canuseCanvastoquicklydrawthegraphicsyouneeddirectlyin
QML.
Ifyoudecidethatyoudorequirecustomitems,youwillbedoing
thatbyimplementingsubclassesoftheQQuickItemC++class,whichis
thebaseclassforallitemsinQtQuick.Aftercreatingthenewtype,
youwillalwayshavetoregisteritwithQMLusingqmlRegisterType.
Thescenegraph
Toprovideveryfastrenderingofitsscene,QtQuickusesa
mechanismcalledscenegraph.Thegraphconsistsofanumberof
nodesofwell-knowntypes,eachdescribingaprimitiveshapetobe
drawn.Theframeworkmakesuseofknowledgeofeachofthe
primitivesallowedandtheirparameterstofindthemost
performance-wiseoptimalorderinwhichitemscanberendered.
RenderingitselfisdoneusingOpenGL,andalltheshapesare
definedintermsofOpenGLcalls.
ProvidingnewitemsforQtQuickboilsdowntodeliveringasetof
nodesthatdefinetheshapeusingterminologythegraph
understands.ThisisdonebysubclassingQQuickItemand
implementingthepurevirtualupdatePaintNode()method,whichis
supposedtoreturnanodethatwilltellthescenegraphhowto
rendertheitem.Thenodewillmostlikelybedescribingageometry
(shape)withamaterial(color,texture)applied.
Timeforaction–Creatinga
regularpolygonitem
Let'slearnaboutthescene-graphbydeliveringanitemclassfor
renderingconvexregularpolygons.Wewilldrawthepolygonusing
theOpenGLdrawingmodecalled"trianglefan".Itdrawsasetof
trianglesthatallhaveacommonvertex.Subsequenttrianglesare
definedbythesharedvertex,thevertexfromtheprevioustriangle,
andthenextvertexspecified.Takealookatthediagramtoseehow
todrawahexagonasatrianglefanusingeightverticesascontrol
points:
Thesamemethodappliesforanyregularpolygon.Thefirstvertex
definedisalwaysthesharedvertexoccupyingthecenterofthe
shape.Theremainingpointsarepositionedonthecircumferenceof
aboundingcircleoftheshapeatequalangulardistances.Theangle
iseasilycalculatedbydividingthefullanglebythenumberofsides.
Forahexagon,thisyields60degrees.
Let'sgetdowntobusinessandtheQQuickItemsubclass.Wewillgiveit
averysimpleinterface:
classRegularPolygon:publicQQuickItem
{
Q_OBJECT
Q_PROPERTY(intverticesREADverticesWRITEsetVertices
NOTIFYverticesChanged)
Q_PROPERTY(QColorcolorREADcolorWRITEsetColorNOTIFY
colorChanged)
public:
RegularPolygon();
~RegularPolygon();
intvertices()const;
voidsetVertices(intv);
QColorcolor()const;
voidsetColor(constQColor&c);
QSGNode*updatePaintNode(QSGNode*,UpdatePaintNodeData*);
signals:
voidverticesChanged(int);
voidcolorChanged(QColor);
private:
intm_vertexCount;
QColorm_color;
};
Ourpolygonisdefinedbythenumberofverticesandthefillcolor.
WealsogeteverythingweinheritedfromQQuickItem,includingthe
widthandheightoftheitem.Besidesaddingtheobviousgetters
andsettersfortheproperties,weoverridethevirtual
updatePaintNode()method,whichisresponsibleforbuildingthescene-
graph.
Beforewedealwithupdatinggraphnodes,let'sdealwiththeeasy
partsfirst.Implementtheconstructorasfollows:
RegularPolygon::RegularPolygon()
{
setFlag(ItemHasContents,true);
m_vertexCount=6;
}
Wemakeourpolygonahexagonbydefault.Wealsoseta
flag,ItemHasContents,whichtellsthescene-graphthattheitemisnot
fullytransparentandshouldaskushowtheitemshouldbepainted
bycallingupdatePaintNode().ExistenceofthisflagallowsQttoavoid
preparingthewholeinfrastructureiftheitemwouldnotbepainting
anythinganyway.
Thesettersarealsoquiteeasytograsp:
voidRegularPolygon::setVertices(intv){
v=qMax(3,v);
if(v==vertices())return;
m_vertexCount=v;
emitverticesChanged(v);
update();
}
voidRegularPolygon::setColor(constQColor&c){
if(color()==c)return;
m_color=c;
emitcolorChanged(c);
update();
}
Apolygonhastohaveatleastthreesides;thus,weenforcethisasa
minimum,sanitizingtheinputvaluewithqMax.Afterwechangeany
ofthepropertiesthatcouldinfluencethelookoftheitem,we
callupdate()toletQtQuickknowthattheitemneedstobe
rerendered.Let'stackleupdatePaintNode()now.We'lldisassembleit
intosmallerpiecessothatitiseasierforyoutounderstandhowthe
functionworks:
QSGNode*RegularPolygon::updatePaintNode(
QSGNode*oldNode,QQuickItem::UpdatePaintNodeData*){
QSGNode*oldNode,QQuickItem::UpdatePaintNodeData*){
Whenthefunctioniscalled,itmayreceiveanodeitreturnedduring
apreviouscall.Beawarethatthegraphisfreetodeleteallthe
nodeswhenitfeelslikeit,soyoushouldneverrelyonthenode
beingthereevenifyoureturnedavalidnodeduringtheprevious
runofthefunction.Let'smoveontothenextpartofthefunction:
QSGGeometryNode*node=nullptr;
QSGGeometry*geometry=nullptr;
Thenodewewillreturnisageometrynodethatcontains
informationaboutthegeometryandthematerialoftheshapebeing
drawn.Wewillbefillingthosevariablesaswegothroughthe
method.Next,wecheckwhetheroldNodewasprovided:
if(!oldNode){
node=newQSGGeometryNode;
geometry=newQSGGeometry(
QSGGeometry::defaultAttributes_Point2D(),m_vertexCount+
2);
geometry->setDrawingMode(GL_TRIANGLE_FAN);
node->setGeometry(geometry);
node->setFlag(QSGNode::OwnsGeometry);
Aswealreadymentioned,thefunctioniscalledwiththepreviously
returnednodeastheargument,butweshouldbepreparedforthe
nodenotbeingthereandweshouldcreateit.Thus,ifthatisthe
case,wecreateanewQSGGeometryNodeandanewQSGGeometryforit.The
geometryconstructortakesaso-calledattributesetasits
parameter,whichdefinesalayoutfordatainthegeometry.
Mostcommonlayoutshavebeenpredefined:
Attribut
eset Usage First
attribute
Second
attribute
Point2D
Solidcoloredshape
floatx,y
-
ColoredPoint
2D
Per-vertexcolor
floatx,y
ucharred,green,
blue,alpha
TexturedPoin
t2D
Per-vertextexture
coordinate
floatx,y
floattx,floatty
Wewillbedefiningthegeometryintermsof2Dpointswithoutany
additionalinformationattachedtoeachpoint;therefore,wepass
QSGGeometry::defaultAttributes_Point2D()toconstructthelayoutweneed.
Asyoucanseeintheprecedingtableforthatlayout,eachattribute
consistsoftwofloatingpointvaluesdenoting
thexandycoordinatesofapoint.
ThesecondargumentoftheQSGGeometryconstructorinformsusabout
thenumberofverticeswewillbeusing.Theconstructorwill
allocateasmuchmemoryasisneededtostoretherequirednumber
ofverticesusingthegivenattributelayout.Afterthegeometry
containerisready,wepassitsownershiptothegeometrynodeso
thatwhenthegeometrynodeisdestroyed,thememoryforthe
geometryisfreedaswell.Atthismoment,wealsomarkthatwewill
berenderingintheGL_TRIANGLE_FANmode.Theprocessisrepeatedfor
thematerial:
QSGFlatColorMaterial*material=newQSGFlatColorMaterial;
material->setColor(m_color);
node->setMaterial(material);
node->setFlag(QSGNode::OwnsMaterial);
WeuseQSGFlatColorMaterialasthewholeshapewillhaveonecolorthat
issetfromm_color.Qtprovidesanumberofpredefinedmaterial
types.Forexample,ifwewantedtogiveeachvertexaseparate
color,wewouldhaveusedQSGVertexColorMaterialalongwith
theColoredPoint2Dattributelayout.
ThenextpieceofcodedealswithasituationinwhicholdNodedid
containavalidpointertoanodethatwasalreadyinitialized:
}else{
node=static_cast<QSGGeometryNode*>(oldNode);
geometry=node->geometry();
geometry->allocate(m_vertexCount+2);
}
Inthiscase,weonlyneedtoensurethatthegeometrycanholdas
manyverticesasweneedincasethenumberofsideschangedsince
thelasttimethefunctionwasexecuted.Next,wecheckthe
material:
QSGMaterial*material=node->material();
QSGFlatColorMaterial*flatMaterial=
static_cast<QSGFlatColorMaterial*>(material);
if(flatMaterial->color()!=m_color){
flatMaterial->setColor(m_color);
node->markDirty(QSGNode::DirtyMaterial);
}
Ifthecolordiffers,weresetitandtellthegeometrynodethatthe
materialneedstobeupdatedbymarkingtheDirtyMaterialflag.
Finally,wecansetvertexdata:
QRectFbounds=boundingRect();
QSGGeometry::Point2D*vertices=geometry->vertexDataAsPoint2D();
//firstvertexisthesharedone(middle)
QPointFcenter=bounds.center();
vertices[0].set(center.x(),center.y());
//verticesaredistributedalongcircumferenceofacircle
qrealangleStep=360.0/m_vertexCount;
qrealradius=qMin(width(),height())/2;
for(inti=0;i<m_vertexCount;++i){
qrealrads=angleStep*i*M_PI/180;
qrealx=center.x()+radius*std::cos(rads);
qrealy=center.y()+radius*std::sin(rads);
vertices[1+i].set(x,y);
}
vertices[1+m_vertexCount]=vertices[1];
First,weaskthegeometryobjecttoprepareamappingforusfrom
theallocatedmemorytoaQSGGeometry::Point2Dstructure,whichcanbe
usedtoconvenientlysetdataforeachvertex.Then,actual
calculationsareperformedusingtheequationforcalculatingpoints
onacircle.Theradiusofthecircleistakenasthesmallerpartofthe
widthandheightoftheitemsothattheshapeiscenteredinthe
item.Asyoucanseeinthediagramatthebeginningoftheexercise,
thelastpointinthearrayhasthesamecoordinatesasthesecond
pointinthearraytoclosethefanintoaregularpolygon.
Attheveryend,wemarkthegeometryaschangedandreturnthe
nodetothecaller:
node->markDirty(QSGNode::DirtyGeometry);
returnnode;
}
Whatjusthappened?
RenderinginQtQuickcanhappeninathreaddifferentthatisthan
themainthread.BeforecallingtheupdatePaintNode()function,Qt
performssynchronizationbetweentheGUIthreadandthe
renderingthreadtoallowussafelyaccessouritem'sdataandother
objectslivinginthemainthread.Thefunctionexecutingthemain
threadisblockedwhilethisfunctionexecutes,soitiscrucialthatit
executesasquicklyaspossibleanddoesn'tdoanyunnecessary
calculationsasthisdirectlyinfluencesperformance.Thisisalsothe
onlyplaceinyourcodewhereatthesametimeyoucansafelycall
functionsfromyouritem(suchasreadingproperties)andinteract
withthescene-graph(creatingandupdatingthenodes).Trynot
emittinganysignalsnorcreatinganyobjectsfromwithinthis
methodastheywillhaveaffinitytotherenderingthreadrather
thantheGUIthread.
Havingsaidthat,youcannowregisteryourclasswithQML
usingqmlRegisterTypeandtestitwiththefollowingQMLdocument:
Window{
width:600
height:600
visible:true
RegularPolygon{
id:poly
anchors{
fill:parent
bottomMargin:20
}
vertices:5
color:"blue"
}
}
Thisshouldgiveyouanicebluepentagon.Iftheshapelooks
aliased,youcanenforceanti-aliasingbysettingthesurfaceformat
fortheapplication:
intmain(intargc,char**argv){
QGuiApplicationapp(argc,argv);
QSurfaceFormatformat=QSurfaceFormat::defaultFormat();
format.setSamples(16);//enablemultisampling
QSurfaceFormat::setDefaultFormat(format);
qmlRegisterType<RegularPolygon>("RegularPolygon",1,0,
"RegularPolygon");
QQmlApplicationEngineengine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if(engine.rootObjects().isEmpty())
return-1;
returnapp.exec();
}
Iftheapplicationproducesablackscreenafterenablinganti-aliasing,trytolowerthe
numberofsamplesordisableit.
Haveagohero–Creatinga
supportingborderfor
RegularPolygon
WhatisreturnedbyupdatePaintNode()maynotjustbea
singleQSGGeometryNodebutalsoalargertreeofQSGNodeitems.Eachnode
canhaveanynumberofchildnodes.Byreturninganodethathas
twogeometrynodesaschildren,youcandrawtwoseparateshapes
intheitem:
Asachallenge,extendRegularPolygontodrawnotonlytheinternal
filledpartofthepolygonbutalsoanedgethatcanbeofadifferent
color.YoucandrawtheedgeusingtheGL_QUAD_STRIPdrawingmode.
Coordinatesofthepointsareeasytocalculate—thepointscloserto
themiddleoftheshapearethesamepointsthatformtheshape
itself.Theremainingpointsalsolieonacircumferenceofacircle
thatisslightlylarger(bythewidthoftheborder).Therefore,you
canusethesameequationstocalculatethem.
TheGL_QUAD_STRIPmoderendersquadrilateralswitheverytwovertices
specifiedafterthefirstfour,composingaconnectedquadrilateral.
Thefollowingdiagramshouldgiveyouaclearideaofwhatweare
after:
UsingQPainterinterfaceinQt
Quick
ImplementingitemsinOpenGLisquitedifficult—youneedtocome
upwithanalgorithmofusingOpenGLprimitivestodrawtheshape
youwant,andthenyoualsoneedtobeskilledenoughwithOpenGL
tobuildaproperscenegraphnodetreeforyouritem.However,
thereisanotherway—youcancreateitemsbypaintingthem
withQPainter.Thiscomesatacostofperformanceasbehindthe
scenes,thepainterdrawsonanindirectsurface(aframebuffer
objectoranimage)thatisthenconvertedtoOpenGLtextureand
renderedonaquadbythescene-graph.Evenconsideringthat
performancehit,itisoftenmuchsimplertodrawtheitemusinga
richandconvenientdrawingAPIthantospendhoursdoingthe
equivalentinOpenGLorusingCanvas.
Tousethatapproach,wewillnotbesubclassingQQuickItemdirectly
butQQuickPaintedItem,whichgivesustheinfrastructureneededtouse
thepainterfordrawingitems.
Basically,allwehavetodo,then,isimplementthepure
virtualpaint()methodthatrenderstheitemusingthereceived
painter.Let'sseethisputintopracticeandcombineitwiththeskills
wegainedearlier.
Timeforaction–Creatingan
itemfordrawingoutlinedtext
Thegoalofthecurrentexerciseistobeabletomakethefollowing
QMLcodework:
importQtQuick2.9
importQtQuick.Window2.3
importOutlineTextItem1.0
Window{
visible:true
width:800
height:400
title:qsTr("HelloWorld")
Rectangle{
anchors.fill:parent
OutlineTextItem{
anchors.centerIn:parent
text:"Thisisoutlinedtext"
fontFamily:"Arial"
fontPixelSize:64
color:"#33ff0000"
antialiasing:true
border{
color:"blue"
width:2
style:Qt.DotLine
}
}
}
}
Then,itproducesthefollowingresult:
StartwithanemptyQtQuickapplicationproject.CreateanewC++
classandcallitOutlineTextItemBorder.Placethefollowingcodeintothe
classdefinition:
classOutlineTextItemBorder:publicQObject{
Q_OBJECT
Q_PROPERTY(intwidthMEMBERm_widthNOTIFYwidthChanged)
Q_PROPERTY(QColorcolorMEMBERm_colorNOTIFYcolorChanged)
Q_PROPERTY(Qt::PenStylestyleMEMBERm_styleNOTIFYstyleChanged)
public:
OutlineTextItemBorder(QObject*parent=0);
intwidth()const;
QColorcolor()const;
Qt::PenStylestyle()const;
QPenpen()const;
signals:
voidwidthChanged(int);
voidcolorChanged(QColor);
voidstyleChanged(int);
private:
intm_width;
QColorm_color;
Qt::PenStylem_style;
};
ThisisasimpleQObject-basedclassholdinganumberof
properties.YoucanseethatQ_PROPERTYmacrosdon'thave
theREADandWRITEkeywordswe'vebeenusingthusfar.Thisisbecause
wearetakingashortcutrightnow,andweletmocproducecode
thatwilloperateonthepropertybydirectlyaccessingthegiven
classmember.Normally,wewouldrecommendagainstsuchan
approachaswithoutgetters;theonlywaytoaccessthepropertiesis
throughthegenericproperty()andsetProperty()calls.However,inthis
case,wewillnotbeexposingthisclasstothepublicinC++sowe
won'tneedthesetters,andweimplementthegettersourselves,
anyway.ThenicethingabouttheMEMBERkeywordisthatifwealso
providetheNOTIFYsignal,thegeneratedcodewillemitthatsignal
whenthevalueofthepropertychanges,whichwillmakeproperty
bindingsinQMLworkasexpected.Wealsoneedtoimplementthe
methodthatreturnstheactualpenbasedonvaluesofthe
properties:
QPenOutlineTextItemBorder::pen()const{
QPenp;
p.setColor(m_color);
p.setWidth(m_width);
p.setStyle(m_style);
returnp;
}
Theclasswillprovideagroupedpropertyforourmainitemclass.
CreateaclasscalledOutlineTextItemandderiveitfromQQuickPaintedItem,
asfollows:
classOutlineTextItem:publicQQuickPaintedItem
{
Q_OBJECT
Q_PROPERTY(QStringtextMEMBERm_text
NOTIFYtextChanged)
Q_PROPERTY(QColorcolorMEMBERm_color
NOTIFYcolorChanged)
Q_PROPERTY(OutlineTextItemBorder*borderREADborder
NOTIFYborderChanged)
Q_PROPERTY(QStringfontFamilyMEMBERm_fontFamily
NOTIFYfontFamilyChanged)
Q_PROPERTY(intfontPixelSizeMEMBERm_fontPixelSize
NOTIFYfontPixelSizeChanged)
public:
OutlineTextItem(QQuickItem*parent=0);
voidpaint(QPainter*painter);
OutlineTextItemBorder*border()const;
QPainterPathshape(constQPainterPath&path)const;
privateslots:
voidupdateItem();
signals:
voidtextChanged(QString);
voidcolorChanged(QColor);
voidborderChanged();
voidfontFamilyChanged(QString);
voidfontPixelSizeChanged(int);
voidfontPixelSizeChanged(int);
private:
OutlineTextItemBorder*m_border;
QPainterPathm_path;
QRectFm_boundingRect;
QStringm_text;
QColorm_color;
QStringm_fontFamily;
intm_fontPixelSize;
};
Theinterfacedefinespropertiesforthetexttobedrawn,inaddition
toitscolor,font,andthegroupedpropertyfortheoutlinedata.
Again,weuseMEMBERtoavoidhavingtomanuallyimplementgetters
andsetters.Unfortunately,thismakesourconstructorcodemore
complicated,aswestillneedawaytorunsomecodewhenanyof
thepropertiesaremodified.Implementtheconstructorusingthe
followingcode:
OutlineTextItem::OutlineTextItem(QQuickItem*parent):
QQuickPaintedItem(parent)
{
m_border=newOutlineTextItemBorder(this);
connect(this,&OutlineTextItem::textChanged,
this,&OutlineTextItem::updateItem);
connect(this,&OutlineTextItem::colorChanged,
this,&OutlineTextItem::updateItem);
connect(this,&OutlineTextItem::fontFamilyChanged,
this,&OutlineTextItem::updateItem);
connect(this,&OutlineTextItem::fontPixelSizeChanged,
this,&OutlineTextItem::updateItem);
connect(m_border,&OutlineTextItemBorder::widthChanged,
this,&OutlineTextItem::updateItem);
connect(m_border,&OutlineTextItemBorder::colorChanged,
this,&OutlineTextItem::updateItem);
connect(m_border,&OutlineTextItemBorder::styleChanged,
this,&OutlineTextItem::updateItem);
updateItem();
}
Webasicallyconnectallthepropertychangesignalsfromboththe
objectanditsgroupedpropertyobjecttothesameslotthatwill
updatethedatafortheitemifanyofitscomponentsaremodified.
Wealsocallthesameslotdirectlytopreparetheinitialstateofthe
item.Theslotcanbeimplementedlikethis:
voidOutlineTextItem::updateItem(){
QFontfont(m_fontFamily,m_fontPixelSize);
m_path=QPainterPath();
m_path.addText(0,0,font,m_text);
m_boundingRect=borderShape(m_path).controlPointRect();
setImplicitWidth(m_boundingRect.width());
setImplicitHeight(m_boundingRect.height());
update();
}
Atthebeginning,thefunctionresetsapainterpathobjectthat
servesasabackendfordrawingoutlinedtextandinitializesitwith
thetextdrawnusingthefontset.Then,theslotcalculatesthe
boundingrectforthepathusingtheborderShape()functionthatwe
willshortlysee.WeusecontrolPointRect()tocalculatethebounding
rectangleasitismuchfasterthanboundingRect()andreturnsanarea
greaterthanorequaltotheoneboundingRect(),whichisOKfor
us.Finally,itsetsthecalculatedsizeasthesizehintfortheitemand
askstheitemtorepaintitselfwiththeupdate()call.Implement
theborderShape()functionusingthefollowingcode:
QPainterPathOutlineTextItem::borderShape(constQPainterPath&path)
const
{
QPainterPathStrokerpathStroker;
pathStroker.setWidth(m_border->width());
QPainterPathp=pathStroker.createStroke(path);
p.addPath(path);
returnp;
}
TheborderShape()functionreturnsanewpainterpaththatincludes
boththeoriginalpathanditsoutlinecreatedwith
theQPainterPathStrokerobject.Thisissothatthewidthofthestrokeis
correctlytakenintoaccountwhencalculatingthebounding
rectangle.
Whatremainsistoimplementthepaint()routineitself:
voidOutlineTextItem::paint(QPainter*painter){
if(m_text.isEmpty())return;
painter->setPen(m_border->pen());
painter->setBrush(m_color);
painter->setRenderHint(QPainter::Antialiasing,true);
painter->translate(-m_boundingRect.topLeft());
painter->drawPath(m_path);
}
Thecodeisreallysimple—webailoutearlyifthereisnothingto
draw.Otherwise,wesetupthepainterusingthepenandcolor
obtainedfromtheitem'sproperties.Weenableanti-aliasingand
calibratethepaintercoordinateswiththatofthebounding
rectangleoftheitem.Finally,wedrawthepathonthepainter.
Whatjusthappened?
Duringthisexercise,wemadeuseofthepowerfulAPIofQt'sraster
graphicsenginetocomplementanexistingsetofQtQuickitems
withasimplefunctionality.Thisisotherwiseveryhardtoachieve
usingpredefinedQtQuickelementsandevenhardertoimplement
usingOpenGL.Weagreedtotakeasmallperformancehitin
exchangeforhavingtowritejustaboutahundredlinesofcodeto
haveafullyworkingsolution.Remembertoregistertheclasswith
QMLifyouwanttouseitinyourcode:
qmlRegisterUncreatableType<OutlineTextItemBorder>(
"OutlineTextItem",1,0,"OutlineTextItemBorder","");
qmlRegisterType<OutlineTextItem>(
"OutlineTextItem",1,0,"OutlineTextItem");
Popquiz
Q1.WhichQMLtypecanbeusedtoenablescrollingofalargeitem
insideasmallerviewport?
1. Rectangle
2. Flickable
3. Window
Q2.WhatisthepurposeoftheAffectorQMLtype?
1. AffectorallowsyoutochangepropertiesofQMLitemsduring
ananimation
2. Affectorinfluencespropertiesofparticlesspawnedbya
particlesystem
3. Affectorallowsyoutocontrolinitialpropertiesofparticles
spawnedbyaparticlesystem
Q3.WhathappenswhenyouuseQPaintertodrawonaQtQuick
item?
1. EverycalltotheQPainterAPIistranslatedtoanequivalent
OpenGLcall
2. QPainterpaintsonaninvisiblebufferthatisthenloadedasan
OpenGLtexture
3. TheitempaintedbyQPainterisdisplayedwithouthardware
acceleration
Summary
YouarenowfamiliarwithQtQuick'scapabilitiesthatallowyouto
addastonishinggraphicaleffectstoyourgames.Youcanconfigure
particlesystemsandimplementOpenGLpaintingintheQtQuick's
scenegraph.Youarealsoabletoutilizetheskillsacquiredinthe
firstpartsofthebooktoimplementpaintedQtQuickitems.
Ofcourse,QtQuickismuchricherthanallthis,butwehadtostop
somewhere.Thesetofskillswehavehopefullypassedontoyou
shouldbeenoughtodevelopmanygreatgames.However,manyof
theelementshavemorepropertiesthanwehavedescribedhere.
Wheneveryouwanttoextendyourskills,youcancheckthe
referencemanualtoseewhethertheelementtypehasmore
interestingattributes.QtQuickisstillinactivedevelopment,soit's
agoodideatogothroughthechangelogsoftherecentQtversions
toseethenewfeaturesthatcouldnotbecoveredinthisbook.
Inthenextchapter,we'llturnourattentiontotheQt3Dmodule,
whichisarelativelyrecentadditiontotheQtframework.Qt3D
providesarichQMLAPIthatwillallowustousemanyoftheskills
welearnedwhileworkingwithQtQuick.However,insteadofuser
interfaceand2Dgraphics,youwillnowcreategamesthatdisplay
hardwareaccelerated3Dgraphics.WhenyoulearntouseQt3D,
youwillbeabletotakeyourgamestoacompletelynewlevel!
3DGraphicswithQt
Manymoderngamestakeplacein3Dworlds.Graphicsprocessing
unitsareconstantlyevolving,allowingdeveloperstocreatemore
andmorevisuallyappealinganddetailedworlds.Whileyoucanuse
OpenGLorVulkandirectlytorender3Dobjects,thiscanproveto
bequitechallenging.Luckily,theQt3Dmoduleprovidesan
implementationof3Drenderingwithahigh-levelAPI.Inthis
chapter,we'lllearntouseitscapabilitiesandseehowwecancreate
a3DgamewithQt.
Qt3Disnotlimitedtorendering.You'llalsolearntohandleuser
inputandimplementgamelogicina3Dgame.Qt3Dwasdesigned
tobehighlyefficientandfullyextensible,soitallowsyouto
implementyourownadditionstoallQt3Dsystems.
Qt3DoffersbothC++andQMLAPIwithmostlyequivalent
functionality.WhiletheC++APIallowsyoutomodifyandextend
theimplementation,wewillusetheQMLapproach,thatwillallow
ustowritecleananddeclarativecodeandusethetechniqueswe've
learnedinthepreviouschapters.BycombiningQt3Dwiththe
powersofQML,youwillbeabletomakeamazinggamesinnotime!
Themaintopicscoveredinthischapterare:
Rendering3Dobjects
Handlinguserinput
Performinganimations
Integrationwith3Deditors
WorkingwithQt3DusingC++
IntegrationwithQtWidgetsandQtQuick
Qt3Doverview
BeforeweseeQt3Dinaction,let'sgothroughtheimportantparts
ofitsarchitecture.
Entitiesandcomponents
Qt3Disnotjusta3Drenderingtool.Whensufficientlyevolved,it
canbecomeafull-featuredgameengine.Thisissupportedbyits
originalarchitecture.Qt3Dintroducesanewsetofabstractions
thatareparticularlyusefulforitstask.
YoumayhavenoticedthatmostoftheQtAPIheavilyuses
inheritance.Forexample,eachwidgettypeisderivedfromQWidget,
whichinturnisderivedfromQObject.Qtformslargefamilytreesof
classestoprovidecommonandspecializedbehavior.Incontrast,
elementsofaQt3Dsceneareconstructedusingcomposition
insteadofinheritance.AsinglepartofaQt3Dsceneiscalledan
entityandrepresentedbytheEntitytype.However,anEntityobject
byitselfdoesn'thaveanyparticulareffectorbehavior.Youcanadd
piecesofbehaviortoanentityintheformofcomponents.
Eachcomponentcontrolssomepartoftheentity'sbehavior.For
example,theTransformcomponentcontrolstheentity'sposition
withinthescene,theMeshcomponentdefinesitsshape,andthe
Materialcomponentcontrolsthepropertiesofthesurface.This
approachallowsyoutoassembleentitiesfromonlythecomponents
thatyouneed.Forexample,ifyouneedtoaddalightsourcetothe
scene,youcancreateanentitywiththePointLightcomponent.You
stillwanttochoosealocationofthelightsource,soyou'llneedthe
Transformcomponentaswell.However,MeshandMaterialcomponents
donotmakesenseforalightsource,soyoudon'tneedtousethem.
Entitiesarearrangedinaclassicparent–childrelationship,likeany
otherQMLobjectsorQObjects.AtreeofentitiesformaQt3D
scene.Thetopmostentityisusuallyresponsiblefordefiningscene-
wideconfiguration.Thesesettingsarespecifiedbyattachingspecial
components(suchasRenderSettingsandInputSettings)tothetoplevel
Entity.
Qt3Dmodules
Qt3Dissplitintoanumberofmodulesthatyoucanchoosetouse
inyourproject.Itmaybehardtoseewhichofthemyouneed,so
let'sseewhateachmoduleismadefor.
Stablemodules
TheQt3DCoremoduleimplementsthebasestructureofQt3D.It
providesEntityandComponenttypes,aswellasbaseclassesforotherQt
3Dsystems.Qt3DCoreitselfdoesnotimplementanybehavior,
providingonlytheframeworkthat'susedbyothermodules.
TheQt3DRendermoduleimplements3Drendering,soit'soneofthe
mostfeature-richmodules.Let'slistsomeimportantpiecesofits
functionality:
GeometryRendereristhebasecomponenttypethatdefinesthe
visibleshapeofanentity
TheMeshcomponentallowsyoutoimporttheentity's
geometryfromafile
TheMaterialcomponentisthebasecomponenttypethat
definesvisiblepropertiesoftheentity'ssurface
TheSceneLoadercomponentallowsyoutoimportahierarchyof
entitieswithmeshesandmaterialsfromafile
Lightcomponents(DirectionalLight,EnvironmentLight,PointLight,
andSpotLight)allowyoutocontrolthescene'slighting
TheFrameGraphAPIprovidesawayofdefininghowexactly
yoursceneshouldberendered.Itallowsyoutosetupthe
camera,implementmultipleviewports,shadowmapping,
customshaders,andmuchmore
TheObjectPickercomponentallowsyoutofindoutwhich
entitiesarepositionedataparticularwindowpoint.
Next,Qt3DLogicisaverysmallmodulethatprovidestheFrameAction
component.Thiscomponentallowsyoutoexecuteanarbitrary
actionforeachframeofyourentity.
Finally,theQt3DInputmoduleisfocusedonuserinput.Itprovidesa
fewcomponentsthatallowyoutohandlekeyboardandmouse
eventsinyourgame.Qt3DInputalsocontainstypesthatcanbeusedto
configuretheinputdevices.
Experimentalmodules
Atthetimeofwriting,alltheotherQt3Dmodulesarestillintech
preview,sotheirAPImaybeincomplete.FutureQtversionsmay
introduceincompatiblechangesinthesemodules,sodon'tbe
surprisedifyouneedtomakeafewchangesintheprovidedcodeto
makeitwork(ourcodewastestedonQt5.10).Thesemodules
shouldeventuallybestabilizedinthefuture,soyoushouldcheck
theQtdocumentationtofindouttheircurrentstatus.
TheQt3DAnimationmodule,asthenameimplies,isresponsiblefor
animationsintheQt3Dscene.It'sabletohandlekeyframe
animationsontheentity'sTransformcomponent,aswellasblend-
shapeandvertex-blendanimations.However,wewon'tbeusing
thismoduleinthischapter,asthealreadyfamiliaranimation
frameworkofQtQuickismorethanenoughforus.
TheQt3DExtrasmoduleprovidescomponentsthatarenotstrictly
necessaryforworkingwithQt3D,butareveryusefulforbuilding
firstsimpleprojects.Theyare:
Meshgeneratorsforbasicgeometricshapes(cubes,spheres,
andsoon)
TheExtrudedTextMeshcomponentthatallowsyoutoshow3D
textinthescene
Manystandardmaterialcomponents,such
asDiffuseSpecularMaterialandGoochMaterial
Additionally,Qt3DExtrasprovidestwoconvenienceclassesthatallow
theusertocontrolthepositionofthecamerausingmouseand
keyboard:
OrbitCameraControllermovesthecameraalonganorbitalpath
FirstPersonCameraControllermovesthecameraasinafirst-person
game
TheQt3DQuickExtrasmoduleprovidestheQt3DExtras::Quick::Qt3DQuickWindow
C++class.ThisisawindowthatdisplaysaQML-basedQt3D
scene.
Finally,theQt3DQuickScene2DmoduleprovidestheabilitytoembedQt
QuickitemsintotheQt3Dscene,andtheQtQuick.Scene3DQML
moduleallowsyoutoembedaQt3DsceneintoaQtQuick
application.
Asyoucansee,thecapabilitiesofQt3Darenotlimitedby
rendering.Youcanalsouseittohandleuserinputandimplement
thegamelogicforyourentities.Qt3Disfullyextensible,soyoucan
useitsC++APItoimplementyourowncomponents,ormodify
existingones.However,wewillmainlyusetheQML-basedAPIin
thischapter.
NotethatQt3DobjectsarenotQtQuickitems,sonotallQtQuickcapabilitiesareopenfor
youwhenyouworkwithQt3D.Forexample,youcan'tuseRepeatertoinstantiatemultiple
Qt3Dentities.However,youcanstilluseQtQuickanimationsbecausetheycanhandle
anyQMLobjects.It'salsopossibletoembedaQt3DsceneintoaQtQuickinterfaceusing
theScene3DQMLtype.
Usingmodules
BeforeusingeachoftheQt3Dmodules,youhavetoenablethe
moduleseparatelyintheprojectfile.Forexample,thefollowingline
willenableallcurrentlydocumentedmodules:
QT+=3dcore3drender3dinput3dlogic3danimation\
qmlquick3dquick3dquickextras3dquickscene2d
WhenusingQML,eachmodulemustalsobeseparatelyimported:
importQt3D.Core2.10
importQt3D.Render2.10
importQt3D.Extras2.10
importQt3D.Input2.0
importQt3D.Logic2.0
importQtQuick.Scene2D2.9
importQtQuick.Scene3D2.0
YoucanseethatdifferentQt3DmoduleshavedifferentQMLmoduleversions.Some
moduleswereupdatedinQt5.10andhavenewfeaturesthatwe'dliketouseinourcode,so
youhavetospecifythelastversion(2.10)tomakenewQMLtypesavailable.Ontheother
hand,somemodulesweren'tupdated,so2.0istheonlyavailableversion.Theup-to-date
versionswillchangeinthefutureasnewQtreleasescomeout.TheQtdocumentation
shouldhopefullycontainthecorrectimportstatements.
AllC++typesofaQt3Dmoduleareplacedinanamespace.In
otherregards,Qtnamingconventionsapply.Forexample,theEntity
QMLtypecorrespondstotheQEntityC++classintheQt3DCore
namespace.Thecorrespondingincludedirectiveis#include<QEntity>.
Qt3Dalsointroducesaconceptofaspects.Anaspectissimplya
pieceofbehaviorthatcanbeaddedtotheQt3Dengine.
TheQt3DQuickWindowclasscontainsabuilt-inaspectenginethat
automaticallyenablesQRenderAspect,QInputAspect,andQLogicAspectaspects,
allowingQt3Dtorenderthescene,processuserinput,andexecute
frameactions.IfyoudecidetousetheQt3DAnimationmodule,you
shouldalsoenableQAnimationAspect.Youcandothatusing
theQt3DWindow::registerAspect()method.OtherQt3Dmodulesdon't
requireaseparateaspect.It'salsopossibletocreateanewaspect,
butit'susuallynotnecessary.
Rendering3Dobjects
EachitemoftheQt3DsceneisrepresentedbytheEntitytype.
However,notallentitiesarevisible3Dobjects.Inorderforan
entitytobevisible,ithastohaveameshcomponentanda
materialcomponent.
Mesh,material,andtransform
Themeshdefinesthegeometricalshapeoftheentity.Itcontains
informationaboutvertices,edges,andfacesrequiredtorenderthe
object.ThebasetypeofallmeshcomponentsisGeometryRenderer.
However,you'llusuallyuseoneofitsdescendants:
Meshimportsgeometrydatafromafile
ConeMesh,CuboidMesh,CylinderMesh,PlaneMesh,SphereMesh,
andTorusMeshprovideaccesstoprimitivegeometricshapes
ExtrudedTextMeshdefinestheentity'sshapebasedonaspecified
textandfont
Whilethemeshdefineswheretheobject'ssurfacewillbedrawn,the
materialdefineshowexactlyitwillbedrawn.Themostobvious
propertyofasurfaceisitscolor,butdependingonthereflection
model,therecanbeallsortsofproperties,suchascoefficientsof
diffuseandspecularreflection.Qt3Dprovidesalotofdifferent
materialtypes:
PerVertexColorMaterialallowsyoutosetcolorpropertiesforeach
vertexandrendersambientanddiffusereflections
TextureMaterialrendersatextureandignoreslighting
DiffuseSpecularMaterialimplementsthePhongreflectionmodel
andallowsyoutosetambient,diffuse,andspecular
componentsofthereflection
GoochMaterialimplementstheGoochshadingmodel
MetalRoughMaterialrendersametal-likesurfaceusingPBR
(physicallybasedrendering)
MorphPhongMaterialalsofollowsthePhongreflectionmodelbut
alsosupportsmorphanimationsoftheQt3DAnimationmodule
Thethirdcommoncomponentofavisible3DobjectisTransform.
Whilenotstrictlyrequired,it'susuallynecessaryforsettingthe
positionoftheobjectinthescene.Youcansetthepositionusing
thetranslationproperty.It'salsopossibletoscaletheobjectusingthe
scale3Dpropertythatallowsyoutosetdifferentscalecoefficientsfor
eachaxis,orthescalepropertythatacceptsasinglecoefficientthat
appliestoallaxes.Similarly,youcaneithersettherotation
quaternionusingtherotationpropertyorsetindividualEulerangles
usingrotationX,rotationY,androtationZproperties.Finally,youcanset
thematrixpropertytoapplyanarbitrarytransformationmatrix.
Notethattransformationsapplynotonlytothecurrententity,buttoallitschildrenas
well.
Lighting
Someoftheavailablematerialswilltakethelightingintoaccount.
Qt3Dallowsyoutoadddifferenttypesoflightstothesceneand
configurethem.YoucandothatbyaddinganewEntitytothescene
andattachingaDirectionalLight,PointLight,orSpotLightcomponenttoit.
Eachofthesecomponentshasthecolorpropertythatallowsyouto
configurethecolorofthelightandtheintensitypropertythatallows
youtochoosehowbrightthelightis.Therestofthepropertiesare
specifictothelighttype.
Directionallight(alsocalled"distantlight"or"sunlight")casts
parallelraysinthedirectiondefinedbytheworldDirectionpropertyof
theDirectionalLighttype.Positionandrotationoftheentityhaveno
influenceonthelightingeffectofadirectionallight,sothereisno
needforaTransformcomponent.
Pointlightemitslightfromitspositioninalldirections.The
positionofthelightcanbechangedviatheTransformcomponent
attachedtothesameentity.ThePointLightcomponentallowsyouto
configurehowbrightthelightwillbeatadistancebysetting
theconstantAttenuation,linearAttenuation,andquadraticAttenuationproperties.
Whilepointlightcanbeinterpretedasasphereoflight,spotlight
isaconeoflight.Itemitslightfromitsposition,butthedirections
arelimitedbythelocalDirectionpropertythatdefineswherethe
spotlightisfacingandthecutOffAnglepropertythatconfigureshow
widethelightconeis.Thepositionanddirectionofthespotlight
canbeinfluencedbythetranslationandrotation
oftheTransformcomponentattachedtothesameentity.SpotLightalso
hasthesameattenuationpropertiesasPointLight.
Ifnolightsarepresentinthescene,Qtwillautomaticallyaddanimplicitpointlightsothat
thesceneissomewhatvisible.
Thefourthtypeoflightisdifferentfromtheothers.It'scalled
environmentlightandcanbeconfiguredbyadding
theEnvironmentLightcomponenttoanentity.Itdefinesthesurrounding
lightingofthesceneusingtwotexturesassignedtoitsirradiance
andspecularproperties.Unlikeotherlighttypes,thiscomponent
doesnothavecolororintensityproperties.Therecanonlybeone
environmentlightinascene.
Notethatlightsourcesthemselvesareinvisible.Theironlypurpose
istoinfluencetheappearanceof3Dobjectsthatusecertain
materialtypes.
Timeforaction–creatinga3D
scene
Inthischapter,wewillcreateanimplementationofthefamous
TowerofHanoigame.Thispuzzlegamecontainsthreerodsand
multipledisksofdifferentsizes.Thediskscanslideontotherods,
butadiskcannotbeplacedontopofasmallerdisk.Atthestarting
position,alltherodsareplacedononeofthedisks.Thegoalisto
movethemalltoanotherrod.Theplayercanonlymoveonediskat
atime.
Asusual,youwillfindthecompleteprojectintheresourcesthatcomewiththebook.
CreateanewQtQuickApplication-Emptyprojectandcallithanoi.
WhilewewillusesomeQtQuickutilities,ourprojectwillnotreally
bebasedonQtQuick.Qt3Dwilldomostofthework.Nevertheless,
theQtQuickApplication-Emptyisthemostsuitableofcurrently
presenttemplates,sowechoosetouseit.Editthehanoi.profileto
enabletheQtmodulesthatwe'llneed:
QT+=3dcore3drender3dinputquick3dquickextras
WewillusetheQt3DQuickWindowclasstoinstantiateourQMLobjects
insteadoftheQQmlApplicationEngineclassweusuallyusewithQtQuick.
Todothat,replacethemain.cppfilewiththefollowingcode:
#include<QGuiApplication>
#include<Qt3DQuickWindow>
intmain(intargc,char*argv[])
{
QGuiApplicationapp(argc,argv);
Qt3DExtras::Quick::Qt3DQuickWindowwindow;
Qt3DExtras::Quick::Qt3DQuickWindowwindow;
window.setSource(QUrl("qrc:/main.qml"));
window.show();
returnapp.exec();
}
Next,replacethemain.qmlfilewiththefollowingcode:
importQt3D.Core2.10
importQt3D.Render2.10
importQt3D.Input2.0
importQt3D.Extras2.10
Entity{
components:[
RenderSettings{
activeFrameGraph:ForwardRenderer{
clearColor:"black"
camera:Camera{
id:camera
projectionType:CameraLens.PerspectiveProjection
fieldOfView:45
nearPlane:0.1
farPlane:1000.0
position:Qt.vector3d(0.0,40.0,-40.0)
upVector:Qt.vector3d(0.0,1.0,0.0)
viewCenter:Qt.vector3d(0.0,0.0,0.0)
}
}
},
InputSettings{}
]
}
ThiscodedeclaresasingleEntityobjectthatcontainstwo
components.TheRenderSettingscomponentdefineshowQt3Dshould
renderthescene.TheactiveFrameGraphpropertyofRenderSettingscanhold
atreeofrenderoperations,butthesimplestpossibleframegraphis
asingleForwardRendererobjectthattakescareofallthe
rendering.ForwardRendererrendersobjectsonebyonedirectlytothe
OpenGLframebuffer.WeusetheclearColorpropertytosetthe
backgroundcolorofourscenetoblack.Thecamerapropertyof
theForwardRendererholdstheCameraobjectitwilluseforcalculatingthe
transformationmatrix.Let'sgothroughthepropertiesoftheCamera
objectwe'veusedinourcode:
TheprojectionTypepropertydefinesthetypeoftheprojection.
BesidesthePerspectiveProjection,youcan
useOrthographicProjection,FrustumProjection,orCustomProjection.
ThefieldOfViewpropertycontainsthefieldofviewparameter
oftheperspectiveprojection.Youcanchangeittoachievea
zoomin/outeffect.
ThenearPlaneandfarPlanepropertiesdefinethepositionsofthe
nearestandthefurthestplanesthatwillbevisibleinthe
camera(theycorrespondtothevisiblezaxisvaluesinthe
viewportcoordinates).
Thepositionvectordefinesthepositionofthecamerainthe
worldcoordinates.
TheupVectorvectorintheworldcoordinatesisthevectorthat
wouldappearpointingupwhenviewingitthroughthe
camera.
TheviewCentervectorintheworldcoordinatesisthepointthat
willappearinthecenteroftheviewport.
Whenusingtheperspectiveprojection,youusuallyneedtosetthe
aspectratioaccordingtothewindowsize.TheCameraobjecthas
theaspectRatiopropertyforthat,butwedon'tneedtosetit,since
theQt3DQuickWindowobjectwillupdatethispropertyautomatically.
Youcandisablethisfeaturebyadding
window.setCameraAspectRatioMode(Qt3DExtras::Quick::Qt3DQuickWindow::UserAspectRatio)to
themain.cppfile.
Ifyouwanttouseanorthographicprojectioninsteadofa
perspectiveone,youcanusethetop,left,bottom,andrightproperties
oftheCameraobjecttosetthevisiblearea.
Finally,thesecondcomponentofourEntityistheInputSettings
component.ItseventSourcepropertyshouldpointtotheobjectthat
providestheinputevents.AswithaspectRatio,wedon'tneedtoset
thispropertymanually.TheQt3DQuickWindowwillfindtheInputSettings
objectandsetitselfastheeventSource.
Youcanruntheprojecttoverifythatitcompilessuccessfullyand
doesn'tproduceanyruntimeerrors.Youshouldreceiveanempty
blackwindowasaresult.
Nowlet'saddsomethingvisibletoourscene.Editthemain.qmlfileto
addafewchildobjectstotherootEntity,asshown:
Entity{
components:[
RenderSettings{/*...*/},
InputSettings{}
]
FirstPersonCameraController{
camera:camera
}
Entity{
components:[
DirectionalLight{
color:Qt.rgba(1,1,1)
intensity:0.5
worldDirection:Qt.vector3d(0,-1,0)
}
]
}
Entity{
components:[
CuboidMesh{},
DiffuseSpecularMaterial{ambient:"#aaa";shininess:100;
},
Transform{scale:10}
]
}
}
Asaresult,youshouldseeacubeatthecenterofthewindow:
Morethanthat,youcanusethearrowkeys,thePageUpandPage
Downkeys,andtheleftmousebuttontomovethecameraaround.
Whatjusthappened?
Weaddedafewobjectstoourscenegraph.First,
theFirstPersonCameraControllerobjectallowstheusertofreelycontrolthe
camera.Thisisquiteusefulfortestingthegamewhileyoudon't
haveyourowncameracontrollingcodeyet.Next,anentitywitha
singleDirectionalLightcomponentworksasalightsourceinthescene.
Weusethepropertiesofthiscomponenttosetthecolor,intensity,
anddirectionofthelight.
Finally,weaddedanentitythatrepresentsaregular3Dobject.Its
shapeisprovidedbytheCuboidMeshcomponentthatgeneratesaunit
cube.Theappearanceofitssurfaceisdefinedby
theDiffuseSpecularMaterialcomponentthatconformstothewidely
usedPhongreflectionmodel.Youcanuseambient,diffuse,
andspecularcolorpropertiestocontroldifferentcomponentsofthe
reflectedlight.Theshininesspropertydefineshowsmooththesurface
is.WeusetheTransformcomponenttoscalethecubetoalargersize.
Timeforaction–constructing
theTowerofHanoiscene
Ournexttaskwillbetocreateafoundationandthreerodsforour
puzzle.WewilltakeadvantageofQML'smodularsystemandsplit
ourcodeintomultiplecomponents.First,let'sleavecameraand
lightingsettingsinthemain.qmlandputouractualscenecontenttoa
newScenecomponent.Inordertodothat,putthetextcursoronto
theEntitydeclarationofthecube,pressAlt+EnterandselectMove
ComponentintoSeparateFile.InputSceneasthecomponentname
andconfirmtheoperation.QtCreatorwillcreateanewScene.qmlfile
andaddittotheproject'sresources.Themain.qmlnowcontainsjust
aninstantiationofourscenecomponent:
Entity{
//...
Scene{}
}
TheactualpropertiesoftheentityaremovedtotheScene.qmlfile.
Let'sadjustittothefollowingform:
Entity{
id:sceneRoot
Entity{
components:[
DiffuseSpecularMaterial{
ambient:"#444"
},
CuboidMesh{},
Transform{
scale3D:Qt.vector3d(40,1,40)
}
]
}
}
}
Ourscenewillcontainmultipleitems,soweintroducedanewEntity
objectandcalleditsceneRoot.Thisentitydoesn'thaveany
components,soitwillnothaveanyvisibleeffectonthescene.This
issimilartohowanobjectofItemtypeusuallyservesasacontainer
forQtQuickitemswithoutprovidinganyvisualcontent.
ThecubeentityisnowachildofsceneRoot.Weusethescale3Dproperty
oftheTransformcomponenttochangethedimensionsofourcube.
Nowitlookslikeatabletopthatwillserveasafoundationforthe
restoftheobjects.
Nowlet'sworkontherods.Naturally,wewanttohaveaRod
componentbecauseitisarepeatingpartofourscene.Invokethe
contextmenuofqml.qrcintheprojecttreeandchooseAddNew.
FromtheQtcategory,chooseQMLFile(QtQuick2)andinputRod
asthefilename.Let'sseehowwecanimplementthiscomponent:
importQt3D.Core2.10
importQt3D.Render2.10
importQt3D.Extras2.10
Entity{
propertyintindex
components:[
CylinderMesh{
id:mesh
radius:0.5
length:9
slices:30
},
DiffuseSpecularMaterial{
ambient:"#111"
},
Transform{
id:transform
translation:{
varradius=8;
varstep=2*Math.PI/3;
returnQt.vector3d(radius*Math.cos(index*step),
mesh.length/2+0.5,
radius*Math.sin(index*step));
}
}
]
}
Similartothecubeentity,ourrodconsistsofamesh,amaterial,
andaTransformcomponent.InsteadoftheCubeMesh,weuse
theCylinderMeshcomponentthatwillcreateacylinderforus.Theradius
andlengthpropertiesdefinethedimensionsoftheobject,andthe
slicespropertyimpactsthenumberofgeneratedtriangles.Wechose
toincreasethenumberofslicestomakethecylinderslookbetter,
butbeawarethatithasaperformanceimpactthatmaybecome
noticeableifyouhavemanyobjects.
OurRodcomponenthastheindexpropertythatcontainsthe
positionalnumberoftherod.Weusethispropertytocalculatethex
andzcoordinatesoftherodsothatallthreerodsareplacedona
circlewithaneightradius.Theycoordinateissettoensurethatthe
rodispositionedontopofthefoundation.Weassignthecalculated
positionvectortothetranslationpropertyoftheTransformcomponent.
Finally,addthreeRodobjectstotheScene.qmlfile:
Entity{
id:sceneRoot
//...
Rod{index:0}
Rod{index:1}
Rod{index:2}
}
Whenyouruntheproject,youshouldseethefoundationandthe
rods:
Timeforaction–repeating3D
objects
Ourcodeworks,butthewaywecreatetherodsisnotideal.
First,enumeratingrodsandtheirindicesinScene.qmlisinconvenient
anderror-prone.Second,we'llneedtohaveawaytoaccessaRod
objectbyitsindex,andthecurrentapproachdoesn'tallowthat.In
thepreviouschapters,wedealtwithrepeatingQtQuickobjects
usingtheRepeaterQMLtype.However,Repeaterdoesn'tworkforEntity
objects.It'sonlyabletohandletypesthatinheritfromQtQuickItem.
Thesolutiontoourproblemisalreadyfamiliartoyousince
Chapter12,CustomizationinQtQuick.WecancreateQMLobjects
usingimperativeJavaScriptcode.RemoveRodobjectsfromthe
Scene.qmlfileandmakethefollowingadditionsinstead:
Entity{
id:sceneRoot
propertyvariantrods:[]
Entity{/*...*/}
Component.onCompleted:{
varrodComponent=Qt.createComponent("Rod.qml");
if(rodComponent.status!==Component.Ready){
console.log(rodComponent.errorString());
return;
}
for(vari=0;i<3;i++){
varrod=rodComponent.createObject(sceneRoot,{index:i
});
rods.push(rod);
}
}
}
Whatjusthappened?
First,wecreatedapropertycalledrodsthatwillholdanarrayof
createdRodobjects.Next,weusedtheComponent.onCompletedattached
propertytorunsomeJavaScriptcodeaftertheQMLengine
instantiatesourrootobject.OurfirstactionwastoloadtheRod
componentandcheckwhetheritwasloadedsuccessfully.After
obtainingafunctioningcomponentobject,weuseditscreateObject()
methodtocreatethreenewrods.Weusedtheargumentsofthis
functiontopasstherootobjectandvalueoftheindexproperty.
Finally,wepushedtheRodobjectintothearray.
Timeforaction–creatingdisks
Ournexttaskistocreateeightdisksthatwillslideontorods.We'll
doitinasimilarwaytohowwehandledrods.First,createanew
filecalledDisk.qmlforournewcomponent.Putthefollowingcontent
intothefile:
importQt3D.Core2.10
importQt3D.Render2.10
importQt3D.Extras2.10
Entity{
propertyintindex
propertyaliaspos:transform.translation
components:[
DiffuseSpecularMaterial{
ambient:Qt.hsla(index/8,1,0.5)
},
TorusMesh{
minorRadius:1.1
radius:2.5+1*index
rings:80
},
Transform{
id:transform
rotationX:90
scale:0.45
}
]
}
Likerods,disksareidentifiedbytheirindex.Inthiscase,index
influencesthecolorandsizeofthedisk.Wecalculatethedisk's
colorusingtheQt.hsla()functionthattakeshue,saturation,and
lightnessvaluesandreturnsacolorvaluethatcanbeassignedtothe
ambientpropertyofthematerial.Thisformulawillgiveuseight
colorfuldisksofdifferenthues.
Thepositionofthediskisdefinedbythetranslationpropertyof
theTransformcomponent.Wewanttobeabletoreadandchangethe
positionofthediskfromtheoutside,sowesetupapropertyalias
calledposthatexposesthetransform.translationpropertyvalue.
Next,weusetheTorusMeshcomponenttodefinetheshapeofour
disks.AtorusshapeisnotreallysuitableforplayingtheTowerof
Hanoigameinreality,butitwillhavetodofornow.Laterinthis
chapter,we'llreplaceitwithamoresuitableshape.Theproperties
oftheTorusMeshcomponentallowustoadjustsomeofits
measurements,butwealsohavetoapplyrotationandscaletothe
objecttoachievethedesiredsizeandposition.
Insteadofputtingallthediskobjectsintoasinglearray,let'screate
anarrayforeachrod.Whenwemoveadiskfromonerodto
anotherone,we'llremovethediskfromthefirstrod'sarrayandadd
ittothesecondrod'sarray.Wecandothatbyaddingapropertyto
theRodcomponent.Whilewe'reatit,weshouldalsoexposetherod's
positiontotheoutside.We'llneedittopositionthedisksonthe
rods.DeclarethefollowingpropertiesinthetoplevelEntityinRod.qml:
readonlypropertyaliaspos:transform.translation
propertyvardisks:[]
Thepospropertywillfollowthevalueofthetranslationpropertyofthe
Transformcomponent.Sincethisvalueiscalculatedbasedontheindex
property,wedeclarethepospropertyasreadonly.
Next,weneedtoadjusttheComponent.onCompletedhandleroftheScene
component.InitializethediskComponentvariable,justlikewedid
withrodComponent.Thenyoucancreatedisksusingthefollowingcode:
varstartingRod=rods[0];
for(i=0;i<8;i++){
vardisk=diskComponent.createObject(sceneRoot,{index:i});
disk.pos=Qt.vector3d(startingRod.pos.x,8-i,
startingRod.pos.z);
startingRod.disks.unshift(disk);
startingRod.disks.unshift(disk);
}
Aftercreatingeachdisk,wesetitspositionbasedonitsindexand
thepositionofthechosenrod.Weaccumulatealldisksinthedisks
propertyoftherod.Wechoosetheorderofdisksinthearrayso
thatthebottomdiskisatthebeginningandthetopdiskisatthe
end.Theunshift()functionaddstheitemtothearrayatthe
beginning,givingthedesiredorder.
Ifyouruntheproject,youshouldseealleighttoriononeofthe
rods:
Thenextpieceoffunctionalitywe'llneedistheabilitytomovedisks
fromonerodtoanother.However,it'stheplayerwhomakesthe
decision,sowe'llalsoneedsomewaytoreceiveinputfromtheuser.
Let'sseewhatoptionswehaveforhandlinguserinput.
Handlinguserinput
ThefirstwayofreceivingeventsinQt3DistouseQtGUI
capabilities.TheQt3DQuickWindowclassweuseinheritsfromQWindow.That
allowsyoutosubclassQt3DQuickWindowandreimplementsomeofits
virtualfunctions,suchaskeyPressEvent()ormouseMoveEvent().Youare
alreadyfamiliarwiththispartofQtAPIbecauseit'sroughlythe
sameasprovidedbyQtWidgetsandGraphicsView.Qt3Ddoesn't
introduceanythingspecialhere,sowewon'tgivethisapproach
muchattention.
SimilartoQtQuick,Qt3Dintroducesahigher-levelAPIfor
receivinginputevents.Let'sseehowwecanuseit.
Devices
Qt3Disfocusedonprovidingagoodabstractionforeveryaspectit
handles.Thisappliestoinputaswell.IntermsofQt3D,an
applicationmayhaveaccesstoanarbitrarynumberofphysical
devices.TheyarerepresentedbytheAbstractPhysicalDevicetype.Atthe
timeofwriting,therearetwobuilt-intypesofphysicaldevices:
keyboardandmouse.Youcanaccessthembydeclaringanobject
ofKeyboardDeviceorMouseDevicetypeinyourQMLfile.
Youcanusepropertiesofthedeviceobjecttoconfigureitsbehavior.
Thereiscurrentlyonlyonesuchproperty:theMouseDevicetypehas
asensitivitypropertythataffectshowmousemovementisconverted
toaxisinputs.
It'sallowedtocreatemultipleobjectsofthesamedevicetypeina
singleapplication.Alldeviceswillhandleallreceivedinputs,but
youcansetdifferentvaluesofpropertiesfordifferentdevice
objects.
Youtypicallydon'twanttohandleeventsdirectlyfromthephysical
devices.Instead,youshouldsetupalogicaldevicethatreceives
eventsfromthephysicaldevicesandconvertsthemtoactionsand
inputsthatmakesenseforyourapplication.Youcanspecifyasetof
actionsandaxesforyourdeviceusingtheactionsandaxes
propertiesoftheLogicalDevicetype,andQt3Dwillrecognizethe
describedinputsandnotifyyourobjectsaboutthem.
Wewillprovideafewcodeexamplestodemonstratehowtohandlevariouskindsofinput
inQt3D.Youcantestthecodebyputtingitintothemain.qmlfileofthehanoiprojector
createaseparateprojectforthat.
Keyboardandmousebuttons
AnactionisrepresentedbytheActiontype.Anactioncanbe
triggeredbypressingasinglekey,akeycombination,orakey
sequence.ThisisdefinedbytheinputspropertyoftheActiontype.The
mostsimplekindofinputisActionInputwhichreactstoasinglekey.
Whentheactionistriggered,itsactivepropertywillchangefrom
falsetotrue.Whenthecorrespondingkeyorkeycombinationis
released,activechangesbacktofalse.YoucanusetheusualQML
featurestotrackchangesoftheproperty:
Entity{
//...
KeyboardDevice{id:keyboardDevice}
MouseDevice{id:mouseDevice}
LogicalDevice{
actions:[
Action{
inputs:ActionInput{
sourceDevice:keyboardDevice
buttons:[Qt.Key_A]
}
onActiveChanged:{
console.log("Achanged:",active);
}
},
Action{
inputs:ActionInput{
sourceDevice:keyboardDevice
buttons:[Qt.Key_B]
}
onActiveChanged:{
console.log("Bchanged:",active);
}
},
Action{
inputs:ActionInput{
sourceDevice:mouseDevice
sourceDevice:mouseDevice
buttons:[MouseEvent.RightButton]
}
onActiveChanged:{
console.log("RMBchanged:",active);
}
}
]
}
}
Asyoucansee,keyboardandmousebuttoneventsarehandledin
thesameway.However,theycomefromdifferentphysicaldevices,
somakesureyouspecifythecorrectdeviceinthesourceDevice
propertyofActionInput.
YoucanspecifymultiplebuttonsforActionInput.Inthiscase,the
actionwilltriggerifanyofthespecifiedbuttonsarepressed.For
example,usethefollowingcodetohandleboththemainEnterkey
andtheEnterkeyonthekeypad:
Action{
inputs:ActionInput{
sourceDevice:keyboardDevice
buttons:[Qt.Key_Return,Qt.Key_Enter]
}
onActiveChanged:{
if(active){
console.log("enterwaspressed");
}else{
console.log("enterwasreleased");
}
}
}
Notethatit'snotrequiredtoputtheinputhandlingcodeintotherootobjectofthescene.
YoucanputitintoanyEntity.It'salsopossibletohavemultipleentitieshandlinginput
eventsatthesametime.
Inputchords
TheInputChordtypeallowsyoutotriggeranactionwhenmultiplekeys
arepressedatthesametime:
Action{
inputs:InputChord{
timeout:500
chords:[
ActionInput{
sourceDevice:keyboardDevice
buttons:[Qt.Key_Q]
},
ActionInput{
sourceDevice:keyboardDevice
buttons:[Qt.Key_W]
},
ActionInput{
sourceDevice:keyboardDevice
buttons:[Qt.Key_E]
}
]
}
onActiveChanged:{
console.log("changed:",active);
}
}
TheonActiveChangedhandlerwillbecalledwhenQ,W,andEkeysare
pressedwithin500millisecondsandheldtogether.
Analog(axis)input
AxisinQt3Disanabstractionofanalogone-dimensionalinput.A
typicalsourceofaxisinputisananalogstickofagamepad.Asthe
nameimplies,Axisonlyrepresentsmovementalongasingleaxis,so
astickcanberepresentedbytwoaxes—oneforverticalmovement
andoneforhorizontalmovement.Apressure-sensitivebuttoncan
berepresentedbyasingleaxis.Anaxisinputproducesafloatvalue
rangingfrom−1to1,withzerocorrespondingtotheneutral
position.
Thatbeingsaid,thereisnogamepadsupportinQt3Datthetimeofwriting.It'spossible
thatitwillbeaddedinfutureversions.YoucanalsousetheextensibleC++APIofQt3Dto
implementthegamepaddeviceusingQtGamepad.However,thesimplestsolutionistouse
QtGamepaddirectly.NothingpreventsyoufromusingQMLorC++APIofQtGamepadin
anapplicationthatusesQt3D.
TheinputspropertyoftheAxistypeallowsyoutospecifywhichinput
eventsshouldberedirectedtothisaxis.Youcanuse
theAnalogAxisInputtypetoaccesstheaxisdataprovidedbyaphysical
device.TheMouseDeviceprovidesfourvirtualaxesthatareemulated
basedonthemouseinput.Twoofthemarebasedonthevertical
andhorizontalscroll.Twoothersarebasedonverticaland
horizontalpointermovement,buttheyonlyworkwhileanymouse
buttonispressed.
TheButtonAxisInputtypeallowsyoutoemulateanaxisbasedonthe
buttonpressed.Youcanusethescalepropertytosettheaxisvalue
correspondingtoeachbutton.Whenmultiplebuttonsarepressed
together,themeanoftheiraxisvaluesisused.
Bothmouse-basedandbutton-basedaxesaredemonstratedinthe
followingexample:
LogicalDevice{
axes:[
axes:[
Axis{
inputs:[
AnalogAxisInput{
sourceDevice:mouseDevice
axis:MouseDevice.X
}
]
onValueChanged:{
console.log("mouseaxisvalue",value);
}
},
Axis{
inputs:[
ButtonAxisInput{
sourceDevice:keyboardDevice
buttons:[Qt.Key_Left]
scale:-1.0
},
ButtonAxisInput{
sourceDevice:keyboardDevice
buttons:[Qt.Key_Right]
scale:1
}
]
onValueChanged:{
console.log("keyboardaxisvalue",value);
}
}
]
}
Objectpicker
Objectpickerisacomponentthatallowsanentitytointeractwith
themousepointer.Thiscomponentdoesnotinteractwiththe
previouslydescribedinputAPIdirectly.Forexample,youdon't
needtoprovideamousedeviceforit.Allyouneedtodoistoattach
theObjectPickercomponenttoanentitythatalsocontainsamesh.
ThesignalsfromObjectPickerwillnotifyyouaboutinputevents
relatedtothisentity:
Signal Explanation
clicked(pic
k) Theobjectwasclicked.
pressed(pic
k) Themousebuttonwaspressedovertheobject.
released(pi
ck)
Themousebuttonwasreleasedafterpressed(pick)was
triggered.
moved(pick) Themousepointerwasmoved.
entered() Themousepointerenteredtheobject'sarea.
exited() Themousepointerexitedtheobject'sarea.
Additionally,thepressedpropertywillbesettotruewhileamouse
buttonispressedovertheobject,andthecontainsMousepropertywill
besettotruewhilethemousepointerisovertheobject'sarea.You
canattachchangehandlerstothesepropertiesorusethemina
propertybinding,aswithanyotherpropertyinQML:
Entity{
components:[
DiffuseSpecularMaterial{/*...*/},
TorusMesh{/*...*/},
ObjectPicker{
hoverEnabled:true
onClicked:{
console.log("clicked");
}
onContainsMouseChanged:{
console.log("containsmouse?",containsMouse);
}
}
]
}
Dependingonyourscene,pickingcanbeaheavycomputational
task.Bydefault,themostsimpleandefficientoptionsareused.The
defaultobjectpickerwillonlyhandlemousepressandrelease
events.YoucansetitsdragEnabledpropertytotruetohandlemouse
movementsafterpressed(pick)wastriggered.Youcanalsoset
thehoverEnabledpropertytotruetohandleallmousemovements,even
whenmousebuttonsaren'tpressed.Therepropertiesbelongto
theObjectPickercomponent,soyoucansetthemseparatelyforeach
entity.
Therearealsoglobalpickingsettingsthataffectthewholewindow.
TheyarestoredinthepickingSettingspropertyoftheRenderSettings
componentthatisnormallyattachedtotherootentity.Thesettings
canbechangedlikethis:
Entity{
components:[
RenderSettings{
activeFrameGraph:ForwardRenderer{/*...*/}
pickingSettings.pickMethod:
PickingSettings.TrianglePicking
},
InputSettings{}
]
//...
}
Let'sgothroughthepossiblesettings.ThepickResultModeproperty
definesthebehaviorofoverlappingpickers.Ifit'sset
toPickingSettings.NearestPick,onlytheobjectnearesttothecamerawill
receivetheevent.IfPickingSettings.AllPicksisspecified,allobjectswill
receivetheevent.
ThepickMethodpropertyallowsyoutochoosehowpickersdecide
whetherthemousepointeroverlapswiththeobject.Thedefault
valueisPickingSettings.BoundingVolumePicking,meaningthatonlythe
boundingboxoftheobjectistakenintoaccount.Thisisafastbut
inaccuratemethod.Toachievehigheraccuracy,youcanset
thePickingSettings.TrianglePickingmethod,whichtakesallmesh
trianglesintoaccount.
Finally,thefaceOrientationPickingModepropertyallowsyoutochooseif
thefrontface,backface,orbothfaceswillbeusedforthetriangle
picking.
Frame-basedinputhandling
Inallthepreviousexamples,weusedpropertychangesignal
handlerstoexecutecodewhenthestateofthelogicaldeviceor
objectpickerchanges.Thisallowsyou,forexample,toexecutea
functionatthemomentabuttonispressedorreleased.However,
sometimesyouwanttoexecuteacontinuousaction(forexample,
accelerateanobject)whileabuttonispressed.Thisiseasytodo
withjustafewchangestothecode.
First,youneedtoattachanidtotheobjectwithinteresting
properties(forexampleAction,Axis,orObjectPicker):
LogicalDevice{
actions:[
Action{
id:myAction
inputs:ActionInput{
sourceDevice:keyboardDevice
buttons:[Qt.Key_A]
}
}
]
}
Thiswillallowyoutorefertoitsproperties.Next,youneedtouse
theFrameActioncomponentprovidedbytheQt3DLogicmodule.This
componentwillsimplyemitthetriggered()signaleachframe.You
canattachittoanyentityandusetheinputdataasyouwant:
Entity{
components:[
//...
FrameAction{
onTriggered:{
console.log("Astate:",myAction.active);
console.log("Astate:",myAction.active);
}
}
]
YoucanusetheFrameActioncomponenttorunanycodethatshouldbe
executedonceperframe.However,don'tforgetthatQMLallows
youtousepropertybindings,soyoucansetpropertyvaluesbased
onuserinputwithouthavingtowriteimperativecodeatall.
Timeforaction–receiving
mouseinput
Ourgameisprettysimple,sotheonlyactiontheplayerhastodois
picktworodsforamove.Let'suseObjectPickertodetectwhenthe
playerclicksonarod.
First,setthepickingSettings.pickMethodpropertyoftheRenderSettings
objecttoPickingSettings.TrianglePickinginthemain.qmlfile(youcanuse
thecodeexamplefromtheprevioussection).Oursceneisvery
simple,andtrianglepickingshouldn'tbetooslow.Thissettingwill
greatlyincreasethepicker'saccuracy.
ThenextsetofchangeswillgototheRod.qmlfile.First,addanIDto
therootentityanddeclareasignalthatwillnotifytheoutsideworld
thattherodwasclicked:
Entity{
id:rod
propertyintindex
readonlypropertyaliaspos:transform.translation
propertyvardisks:[]
signalclicked()
//...
}
Next,addanObjectPickertothecomponentsarrayandemitthepublic
clicked()signalwhenthepickerreportsthatitwasclicked:
Entity{
//...
components:[
//...
ObjectPicker{
ObjectPicker{
id:picker
hoverEnabled:true
onClicked:rod.clicked()
}
]
}
Finally,let'sgivetheplayerahintthattherodisclickableby
highlightingitwhenitintersectswiththemousepointer:
DiffuseSpecularMaterial{
ambient:{
returnpicker.containsMouse?"#484":"#111";
}
},
Whentheplayerputsthemousepointeroverarod,
thepicker.containsMousepropertywillbecometrue,andQMLwillupdate
thematerial'scolorautomatically.Youshouldseethisbehavior
whenrunningtheproject.Thenexttaskistoaccesstherod's
clicked()signalfromtheScenecomponent.Todothat,you'llneedto
makethefollowingchangestothecode:
Component.onCompleted:{
//...
varsetupRod=function(i){
varrod=rodComponent.createObject(sceneRoot,{index:i});
rod.clicked.connect(function(){
rodClicked(rod);
});
returnrod;
}
for(vari=0;i<3;i++){
rods.push(setupRod(i));
}
//...
}
functionrodClicked(rod){
console.log("rodclicked:",rods.indexOf(rod));
}
}
Asaresultofthesechanges,thegameshouldprintamessagetothe
applicationoutput,wheneverarodisclicked.
Whatjusthappened?
First,weaddedasetupRod()helperfunctionthatcreatesanewrod
andconnectsitssignaltothenewrodClicked()function.Thenwe
simplycalledsetupRod()foreachindexandaccumulatedtherod
objectintotherodsarray.TherodClicked()functionwillcontainthe
restofourgamelogic,butfornowitonlyprintstheindexofthe
clickedrodtotheapplicationoutput.
NotethatthecontentofthesetupRod()functioncannotbeplaceddirectlyintothebodyof
theforloopoveri.Theclicked()signalisconnectedtoaclosurethatcaptures
therodvariable.Withinthefunction,eachrodwillconnecttoaclosurethatcapturesthe
correspondingRod
object.Withintheforloop,allclosureswouldcapturethecommon
rodvariablethatwillholdthelastRodobjectforalltheclosures.
Performinganimations
Animationsareessentialformakingagoodgame.Qt3Dprovidesa
separatemoduleforperforminganimations,butatthetimeof
writingit'sstillexperimental.Luckily,Qtalreadyprovidesmultiple
waystoplayanimations.WhenusingC++API,youcanusethe
AnimationFramework(welearnedaboutitinChapter5,Animations
inGraphicsView).WhenusingQML,youcanusethepowerfuland
convenientanimationsystemprovidedbyQtQuick.Wealready
workedwithitalotinpreviouschapters,soherewe'llseehowwe
canapplyourknowledgetoQt3D.
QtQuickanimationscanbeappliedtoalmostanypropertyofany
QMLobject(strictlyspeaking,therearepropertytypesitcan't
handle,butwewon'tdealwiththosetypeshere).Ifyoulookatthe
QMLfilesofourproject,you'llseethatbasicallyeverythinginour
sceneisdefinedbyproperties.Thatmeansthatyoucananimate
positions,colors,dimensionsofobjectsandalmosteverythingelse.
Ourcurrenttaskwillbetocreateananimationofthedisksliding
upfromtherod,movingacrossthetabletotheotherrod,and
slidingdownthatrod.Thepropertywe'llanimateisposwhichisthe
propertyaliasfortransform.translation.
Timeforaction–animating
diskmovements
Ouranimationwillconsistofthreeparts,soitwillrequireafair
amountofcode.Insteadofputtingallthatcodedirectlyintothe
Scenecomponent,let'sputtheanimationintoaseparatecomponent.
CreateanewfilecalledDiskAnimation.qmlandfillitwiththefollowing
code:
importQtQuick2.10
SequentialAnimation{
id:rootAnimation
propertyvarianttarget:null
propertyvector3drod1Pos
propertyvector3drod2Pos
propertyintstartY
propertyintfinalY
propertyintmaxY:12
Vector3dAnimation{
target:rootAnimation.target
property:"pos"
to:Qt.vector3d(rod1Pos.x,maxY,rod1Pos.z)
duration:30*(maxY-startY)
}
Vector3dAnimation{
target:rootAnimation.target
property:"pos"
to:Qt.vector3d(rod2Pos.x,maxY,rod2Pos.z)
duration:400
}
Vector3dAnimation{
target:rootAnimation.target
property:"pos"
to:Qt.vector3d(rod2Pos.x,finalY,rod2Pos.z)
duration:30*(maxY-finalY)
}
}
Whatjusthappened?
Ouranimationhasalotofpropertiesbecauseitshouldbeflexible
enoughtohandleallthecasesweneed.First,itshouldbeableto
animateanydisk,soweaddedthetargetpropertythatwillcontain
thediskwecurrentlymove.Next,therodsthatparticipateinthe
movementinfluencetheintermediateandfinalcoordinatesofthe
disk(morespecifically,itsxandzcoordinates).Therod1Pos
androd2Pospropertieswillholdthecoordinatesoftherodsinplay.
ThestartYandfinalYpropertiesdefinethestartingandfinal
coordinatesofthedisk.Thesecoordinateswilldependonthe
currentnumberofdisksstoredoneachrod.Finally,themaxY
propertysimplydefinesthemaximumheightthediskwillraiseat
whilemoving.
Thepropertyweanimatedisofthevector3dtype,soweneededtouse
theVector3dAnimationtypethatisabletocorrectlyinterpolateallthree
componentsofthevector.Wesetthesametargetandpropertyforall
threepartsoftheanimation.Then,wecarefullycalculatedthefinal
positionofthediskaftereachstageandassignedittotheto
property.Thereisnoneedtosetthefromproperty,astheanimation
willautomaticallyusethecurrentpositionofthediskasthestarting
point.Finally,wecalculatedthedurationofeachsteptoensuresteady
movementofthedisk.
Ofcourse,wewanttotestthenewanimationrightaway.Add
aDiskAnimationobjecttotheScenecomponentandinitializethe
animationattheendoftheComponent.onCompletedhandler:
DiskAnimation{id:diskAnimation}
Component.onCompleted:{
//...
vardisk1=rods[0].disks.pop();
diskAnimation.rod1Pos=rods[0].pos;
diskAnimation.rod1Pos=rods[0].pos;
diskAnimation.rod2Pos=rods[1].pos;
diskAnimation.startY=disk1.pos.y;
diskAnimation.finalY=1;
diskAnimation.target=disk1;
diskAnimation.start();
}
Whenyouruntheapplication,youshouldseethetopdiskmoving
fromonerodtoanother.
Timeforaction–implementing
gamelogic
Mostoftherequiredpreparationsaredone,andnowit'stimeto
makeourgamefunctional.Theplayershouldbeabletomakea
movebyclickingonarodandthenclickingonanotherrod.After
thefirstrodisselected,thegameshouldrememberitandshowitin
adifferentcolor.
First,let'spreparetheRodcomponent.Weneedittohaveanew
propertythatindicatesthatthisrodwasselectedasthefirstrodfor
thenextmove:
propertyboolisSourceRod:false
It'seasytomaketherodchangecolordependingontheisSourceRod
valueusingapropertybinding:
DiffuseSpecularMaterial{
ambient:{
if(isSourceRod){
returnpicker.containsMouse?"#f44":"#f11";
}else{
returnpicker.containsMouse?"#484":"#111";
}
}
},
Nowlet'sturnourattentiontotheScenecomponent.We'llneeda
propertythatcontainsthecurrentlyselectedfirstrod:
Entity{
id:sceneRoot
propertyvariantrods:[]
propertyvariantsourceRod
//...
}
AllthatremainsistheimplementationoftherodClicked()function.
Let'sgothroughitintwosteps:
functionrodClicked(rod){
if(diskAnimation.running){return;}
if(rod.isSourceRod){
rod.isSourceRod=false;
sourceRod=null;
}elseif(!sourceRod){
if(rod.disks.length>0){
rod.isSourceRod=true;
sourceRod=rod;
}else{
console.log("nodisksonthisrod");
}
}else{
//...
}
}
First,wecheckwhetherthemoveanimationisalreadyrunning,and
ignoretheeventifitis.Next,wecheckwhethertheclickedrodwas
alreadyselected.Inthiscase,wesimplydeselecttherod.This
allowstheplayertocancelthemoveiftheyaccidentallyselectedan
incorrectrod.
IfsourceRodisunset,thatmeansthatwe'reinthefirstphaseofthe
move.Wecheckthattheclickedrodhassomedisksonit,otherwise
amovewouldnotbepossible.Ifeverythingisallright,weset
thesourceRodpropertyandtherod'sisSourceRodproperty.
Therestofthefunctionhandlesthesecondphaseofthemove:
vartargetRod=rod;
if(targetRod.disks.length>0&&
if(targetRod.disks.length>0&&
targetRod.disks[targetRod.disks.length-1].index<
sourceRod.disks[sourceRod.disks.length-1].index)
{
console.log("invalidmove");
}else{
vardisk=sourceRod.disks.pop();
targetRod.disks.push(disk);
diskAnimation.rod1Pos=sourceRod.pos;
diskAnimation.rod2Pos=targetRod.pos;
diskAnimation.startY=disk.pos.y;
diskAnimation.finalY=targetRod.disks.length;
diskAnimation.target=disk;
diskAnimation.start();
}
sourceRod.isSourceRod=false;
sourceRod=null;
Inthisbranch,wealreadyknowthatwehavethefirstrodobject
storedinthesourceRodproperty.Westoretheclickedrodobjectin
thetargetRodvariable.Next,wecheckwhethertheplayertriestoput
alargerdiskontopofthesmallerone.Ifthat'sthecase,werefuse
tomaketheinvalidmove.
Ifeverythingiscorrect,wefinallyperformthemove.Weusethe
pop()functiontoremovethediskfromtheendofthesourceRod.disks
array.Thisisthediskthatwillbemovedtotheotherrod.We
immediatelypushthediskobjecttothedisksarrayoftheotherrod.
Next,wecarefullysetupallpropertiesoftheanimationandstartit.
Attheendofthefunction,wecleartherod'sisSourceRodpropertyand
thescene'ssourceRodpropertytoallowtheplayertomakethenext
move.
Haveagohero–improvingthe
game
Trytomakeyourownmodificationstothegame.Forexample,you
cannotifytheplayeraboutaninvalidmovebyflashingthe
backgroundcolororthecolorofthefoundationobject.Youcan
evenadda3DtexttothesceneusingtheExtrudedTextMeshcomponent.
Trytoplaywithdifferenteasingmodestomaketheanimationslook
better.
ThepropertiesandfunctionsoftheSceneobjectarevisibletothe
outsideworld,buttheyreallyareimplementationdetails.Youcan
fixthatbyputtingthemintoaninternalQtObject,aswedescribedinC
hapter12,CustomizationinQtQuick.
Qt3Disveryflexiblewhenitcomestorendering.Whileit'sstraightforwardtousewiththe
simpleForwardRenderer,youcancreateamuchmorecomplexrendergraphifyouwant.It's
possibletorendertomultipleviewports,useoff-screentextures,applycustomshaders,and
createyourowngraphicseffectsandmaterials.Wecan'tdiscussallthesepossibilitiesin
thisbook,butyoucanlookatQtexamplestoseehowthiscanbedone.Someoftherelevant
examplesareQt3D:MultiViewportQML,Qt3D:ShadowMapQML,andQt3D:Advanced
custommaterialQML.
Integrationwith3Dmodeling
software
GeometricshapesprovidedbytheQt3DExtrasmodulearegreatfor
prototyping.Aswesaw,thesemeshgeneratorscomeinhandywhen
youwanttocreateandtestanewgamequickly.However,areal
gameusuallycontainsmorecomplexfiguresthanspheresand
cubes.Themeshesareusuallypreparedusingspecialized3D
modellingsoftware.Qt3Dprovideswidecapabilitiesforimporting
3Ddatafromexternalfiles.
ThefirstwayofimportingsuchdataistheMeshcomponent.Youonly
needtoattachthiscomponenttoanentityandspecifythepathto
thefileusingthesourceproperty.AsofQt5.10,MeshsupportsOBJ,
PLY,STL,andAutodeskFBXfileformats.
Asalways,youcanusearealfilenameoraQtresourcepath.However,notethatthe
sourcepropertyexpectsanURL,notapath.Acorrectabsoluteresourcepathshouldstart
withqrc:/,andanabsolutefilepathshouldstartwithfile://.Youcanalsouserelative
pathsthatwillberesolvedrelativelytothecurrentQMLfile.
Ifyou'reusingOBJfiles,Meshprovidesyouwithanadditionaloption
toonlyloadasub-meshfromthesourcefile.Youcandoitby
specifyingthenameofthesub-meshinthemeshNamepropertyofthe
Meshcomponent.Insteadoftheexactname,youcanalsospecifya
regularexpressiontoloadallsub-meshesmatchingthatexpression.
Timeforaction–usingOBJ
filesforthedisks
Qt3Ddoesn'tprovideasuitablemeshforthedisks,butwecanuse
a3Dmodellingsoftwaretomakeanyshapewewantandthenuseit
inourproject.YouwillfindtherequiredOBJfilesintheresources
thatcomewiththebook.Thefilesarenamedfromdisk0.obj
todisk7.obj.Ifyouwanttopracticewitha3Dmodellingsoftware,
youcanpreparethefilesyourself.
Createasubdirectorynamedobjinyourprojectdirectoryandput
theOBJfilesthere.Invokethecontextmenuofqml.qrcintheQt
Creator'sprojecttreeandselectAddExistingFiles.AddallOBJ
filestotheproject.Toputthesefilestowork,weneedtoeditthe
Disk.qmlfile.RemovescaleandrotationfromtheTransformcomponent.
ReplaceTorusMeshwithMeshandspecifytheresourcepathtotheOBJ
fileasthesourceproperty:
components:[
DiffuseSpecularMaterial{/*...*/},
Mesh{
source:"qrc:/obj/disk"+index+".obj"
},
Transform{
id:transform
}
]
Qt3Dwillnowuseournewmodelsforthedisks:
Loadinga3Dscene
TheMeshcomponentisusefulwhenyouwanttoimportasingle
object'sshapefromanexternalfile.However,sometimesyouwant
toimportmultipleobjectsfromasinglefile.Forexample,youcould
preparesomedecorationssurroundingyourgameactionandthen
importthemallatonce.ThisiswheretheSceneLoadercomponent
becomesuseful.
ItcanbeusedsimilartotheMeshcomponent:
Entity{
components:[
SceneLoader{
source:"path/to/scene/file"
}
]
}
However,insteadofprovidingshapeforitsentity,SceneLoadercreates
awholetreeofEntityobjectsthatbecomechildrenoftheSceneLoader's
entity.Eachnewentitywillbeprovidedwithamesh,amaterial,
andatransformaccordingtothefiledata.SceneLoaderusesAssimp
(OpenAssetImportLibrary)toparsethesourcefiles,soitsupports
manycommon3Dformats.
WorkingwithQt3DusingC++
WhileQMLisapowerfulandconvenientwayofusingQt3D,
sometimesyoumayhavereasonstopreferC++overQML.For
example,ifyourprojecthasalargeC++codebaseoryourteamis
notfamiliarwithJavaScript,stickingwithC++mightbetheright
solution.IfyouwanttoextendaQt3Dclasswithyourcustom
implementation,you'llhavetousetheC++approach.Additionally,
ifyoudealwithlargeamountsofobjects,processingtheminC++
maybenoticeablyfasterthandoingthatinQML.Qtallowsyouto
choosebetweenC++andQMLfreely.
TheQMLAPIofQt3DforthemostpartconsistsofC++classes
exposedwithoutmanychanges.Thatmeansthatmostofthecode
you'veseeninthischaptersofarcanbetransparentlytranslatedto
theequivalentC++codewithminimaleffort.Whenyouelectnotto
useQML,youloseitspropertybindings,syntaxsugar,andthe
abilitytodeclaretreesofobjectsthatareautomaticallyinstantiated.
However,aslongasyou'refamiliarwiththecoreofQtC++API,
youshouldn'thaveanyissues.You'llhavetocreateobjects
manuallyandassignparentstothem.Inplaceofpropertybindings,
you'llhavetoconnecttopropertychangesignalsandperformthe
requiredupdatesmanually.Ifyoustudiedtheearlierchaptersof
thisbook,youshouldhavenoproblemswithdoingthat.
Timeforaction–creatinga3D
sceneusingC++
Let'sseehowwecanrecreateourfirstQt3DsceneusingonlyC++
code.Ourscenewillcontainalightsource,acube,andafirst
personcameracontroller.YoucanusetheQtConsoleApplication
templatetocreatetheproject.Don'tforgettoenabletheQt3D
modulesyouwanttouseintheprojectfile:
QT+=3dextras
CONFIG+=c++11
ThefirstchangecomparedtotheQMLapproachisthatyouneedto
usetheQt3DWindowclassinsteadofQt3DQuickWindow.TheQt3DWindowclass
performsafewactionsthataretypicallyneededinaQt3D
application.ItsetsupaQForwardRenderer,acamera,andinitializes
theQInputSettingsobjectneededforprocessingevents.Youcanaccess
thedefaultframegraphusingthedefaultFrameGraph()method.The
defaultcameraisavailableusingthecamera()method.Theaspect
ratioofthedefaultcameraisupdatedautomaticallyaccordingto
thewindowsize.Ifyouwanttosetupacustomframegraph,use
thesetActiveFrameGraph()method.
Allthecodefromoursmallexamplewillbeputintothemain()
function.Let'sgothroughitpiecebypiece.First,weinitializethe
usualQGuiApplicationobject,createaQt3DWindowobject,andapplyour
preferredsettingstoitsframegraphandcamera:
intmain(intargc,char*argv[]){
QGuiApplicationapp(argc,argv);
Qt3DExtras::Qt3DWindowwindow;
window.defaultFrameGraph()->setClearColor(Qt::black);
window.defaultFrameGraph()->setClearColor(Qt::black);
Qt3DRender::QCamera*camera=window.camera();
camera->lens()->setPerspectiveProjection(45.0f,16.0f/9.0f,
0.1f,1000.0f);
camera->setPosition(QVector3D(0,40.0f,-40.0f));
camera->setViewCenter(QVector3D(0,0,0));
//...
}
Next,wecreatearootentityobjectthatwillholdallourother
entitiesandcreateacameracontrollerattachedtothecamera:
Qt3DCore::QEntity*rootEntity=newQt3DCore::QEntity();
Qt3DExtras::QFirstPersonCameraController*cameraController=
newQt3DExtras::QFirstPersonCameraController(rootEntity);
cameraController->setCamera(camera);
Next,wesetupalightentity:
Qt3DCore::QEntity*lightEntity=newQt3DCore::QEntity(rootEntity);
Qt3DRender::QDirectionalLight*lightComponent=new
Qt3DRender::QDirectionalLight();
lightComponent->setColor(Qt::white);
lightComponent->setIntensity(0.5);
lightComponent->setWorldDirection(QVector3D(0,-1,0));
lightEntity->addComponent(lightComponent);
It'simportantthatwepasstherootentitytotheQEntityconstructor
toensurethatthenewentitywillbeapartofourscene.Toadda
componenttotheentity,weusetheaddComponent()function.Thenext
stepistosetupthecube3Dobject:
Qt3DCore::QEntity*cubeEntity=newQt3DCore::QEntity(rootEntity);
Qt3DExtras::QCuboidMesh*cubeMesh=newQt3DExtras::QCuboidMesh();
Qt3DExtras::QDiffuseSpecularMaterial*cubeMaterial=
newQt3DExtras::QDiffuseSpecularMaterial();
cubeMaterial->setAmbient(Qt::white);
Qt3DCore::QTransform*cubeTransform=newQt3DCore::QTransform();
cubeTransform->setScale(10);
cubeEntity->addComponent(cubeMesh);
cubeEntity->addComponent(cubeMaterial);
cubeEntity->addComponent(cubeMaterial);
cubeEntity->addComponent(cubeTransform);
Asyoucansee,thiscodesimplycreatesafewobjectsandsetstheir
propertiestothesamevaluesweusedinourQMLexample.The
finallinesofcodecompleteoursetup:
window.setRootEntity(rootEntity);
window.show();
returnapp.exec();
Wepasstherootentitytothewindowandshowitonscreen.That's
all!Qt3Dwillrendertheconstructedsceneinthesamewayit
workedinourQMLproject.
AllpropertiesofQt3Dclassesareequippedwithchange
notificationsignals,soyoucanuseconnectstatementstoreactto
externalchangesproperties.Forexample,ifyouuse
theQt3DInput::QActioncomponenttoreceivekeyboardormouseevents,
youcanuseitsactiveChanged(boolisActive)signaltogetnotifications
abouttheevent.Youcanalsoperformanimationsinthe3Dscene
usingC++animationclassessuchasQPropertyAnimation.
IntegrationwithQtWidgets
andQtQuick
WhileQt3Disaverypowerfulmodule,sometimesit'snotenough
tomakeacompletegameorapplication.OtherQtmodulessuchas
QtQuickorQtWidgetscanbeveryhelpful,forexample,when
workingontheuserinterfaceofyourgame.Luckily,Qtprovidesa
fewwaystousedifferentmodulestogether.
WhenitcomestoQtWidgets,yourbestbet
istheQWidget::createWindowContainer()
function.Itallowsyoutosurroundyour3Dviewwithwidgetsand
displaythemallinasinglewindow.Thisapproachwasalready
discussedinChapter9,OpenGLandVulkaninQtApplications,and
canbeappliedtoQt3Dwithoutanychanges.
However,thecapabilitiesofQtWidgetsarestilllimitedintheworld
ofhardware-acceleratedgraphics.QtQuickismuchmore
promisinginthisarea,andthesynergybetweenQMLAPIsofQt
QuickandQt3Dcanprovetobeverystrong.Qtprovidestwoways
tocombineQtQuickandQt3Dinasingleapplicationwithout
significantperformancecosts.Let'stakeacloserlookatthem.
EmbeddingQtQuickUIintoa
3Dscene
Qt3DallowsyoutoembedanarbitraryQtQuickitemintoyour3D
sceneusingtheScene2Dtype.Howdoesthatwork?First,youneedto
putyourQtQuickcontentintoanewScene2Dobject.Next,youneed
todeclareatexturethatwillbeusedasarendertargetfortheform.
WheneverQtQuickdecidestoupdateitsvirtualview,theScene2D
objectwillrenderitdirectlytothespecifiedtexture.Youonlyneed
todisplaythistextureasyouwant.Themostsimplewayofdoing
thatistopassittoaTextureMaterialcomponentattachedtooneof
your3Dobjects.
However,thisisonlyonepartofthejob.It'snicetoallowusersto
seeyourform,buttheyshouldalsobeabletointeractwithit.This
isalsosupportedbyScene2D!Tomakeitwork,youneedtodothe
following:
1. SetpickMethodtoTrianglePickingintheRenderSettings.Thiswill
allowobjectpickerstoretrievemoreaccurateinformation
aboutmouseevents.
2. AttachanObjectPickercomponenttoallentitiesthatusethe
texturecreatedbyScene2D.It'sagoodideatosetthehoverEnabled
anddragEnabledpropertiesoftheobjectpickertotruetomake
mouseeventsworkasexpected.
3. SpecifyalltheseentitiesintheentitiespropertyoftheScene2D
object.
ThiswillallowScene2DtoforwardmouseeventstotheQtQuick
content.Unfortunately,forwardingkeyboardeventsisnotavailable
yet.
Let'sseeanexampleofthisapproach:
importQt3D.Core2.0
importQt3D.Render2.0
importQt3D.Input2.0
importQt3D.Extras2.10
importQtQuick2.10
importQtQuick.Scene2D2.9
importQtQuick.Controls2.0
importQtQuick.Layouts1.0
Entity{
components:[
RenderSettings{
activeFrameGraph:ForwardRenderer{/*...*/}
pickingSettings.pickMethod:
PickingSettings.TrianglePicking
},
InputSettings{}
]
Scene2D{
output:RenderTargetOutput{
attachmentPoint:RenderTargetOutput.Color0
texture:Texture2D{
id:texture
width:200
height:200
format:Texture.RGBA8_UNorm
}
}
entities:[cube,plane]
Rectangle{
color:checkBox1.checked?"#ffa0a0":"#a0a0ff"
width:texture.width
height:texture.height
ColumnLayout{
CheckBox{
id:checkBox1
text:"Togglecolor"
}
CheckBox{
id:checkBox2
text:"Togglecube"
}
}
CheckBox{
id:checkBox3
checked:true
text:"Toggleplane"
}
}
}
}
//...
}
ThiscodesetsupaQt3DscenethatcontainsaScene2Dobject.Scene2D
itselfisnotvisibleinthe3Dscene.Wedeclareatexturethatwill
receivetherenderedQtQuickcontent.Youcanchoosewidthand
heightofthetexturedependingonthesizeofthedisplayedcontent.
Next,wedeclarethatwe'llrenderthistextureintwoentities(we'll
createtheminthenextpieceofcode).Finally,weplaceaQtQuick
itemdirectlyintotheScene2Dobject.Makesureyousetthissizefor
yourQtQuickitemaccordingtothesizeofthetexture.Inour
example,wecreatedaformcontainingthreecheckboxesinalayout.
ThenextpartofcodecreatestwoitemsfordisplayingtheQtQuick-
basedtexture:
Entity{
id:cube
components:[
CuboidMesh{},
TextureMaterial{
texture:texture
},
Transform{
scale:10
rotationY:checkBox2.checked?45:0
},
ObjectPicker{
hoverEnabled:true
dragEnabled:true
}
]
}
}
Entity{
id:plane
components:[
PlaneMesh{
mirrored:true
},
TextureMaterial{
texture:texture
},
Transform{
translation:checkBox3.checked?Qt.vector3d(-20,0,0):
Qt.vector3d(20,0,0)
scale:10
rotationX:90
rotationY:180
rotationZ:0
},
ObjectPicker{
hoverEnabled:true
dragEnabled:true
}
]
}
Thefirstitemisacube,andtheseconditemisaplane.Mostofthe
propertiesarejustarbitraryvaluesthatmakethescenelookgood.
TheimportantpartisthateachitemhasaTextureMaterialcomponent,
andwepassedthetextureobjectintoit.Eachitemalsohas
anObjectPickercomponentthatallowstheusertointeractwiththe
item.NotethatweusedthemirroredpropertyofPlaneMeshtodisplaythe
textureinitsoriginal(notmirrored)orientation.
Oneplaneobjectisusuallyenoughtodisplayyourform.Weusedtwoobjectspurelyfor
demonstrationpurposes.
WhileQtQuickitemsandQt3Dentitiesliveindifferentworldsand
don'tseemtointeractwitheachother,theyarestilldeclaredina
singleQMLfile,soyoucanusepropertybindingsandotherQML
techniquestomakealltheseitemsworktogether.Inourexample,
notonlyisthebackgroundcoloroftherootQtQuickitem
controlledbythecheckbox,butthe3Dobjectsarealsoinfluenced
bycheckboxes:
EmbeddingaQt3Dsceneinto
aQtQuickform
Nowlet'sseehowwecanperformtheoppositetask.Thisapproach
isusefulifyourapplicationisbuiltmainlyaroundQtQuick.This
meansthatyouusetheQQmlApplicationEngineclassinthemain()function,
andtherootobjectofyourmain.qmlfileisusuallytheWindowobject.It's
veryeasytoextendyourQtQuickapplicationwithabitof3D
action.
Wecouldplaceallthecodeintothemain.qmlfile,butit'smore
convenienttosplititbecausesettingupa3Dscenerequiresquitea
bitofcode.Let'ssayyouhaveafilenamedMy3DScene.qmlthatcontains
theusualcontentofa3Dscene:
Entity{
components:[
RenderSettings{
activeFrameGraph:ForwardRenderer{/*...*/},
InputSettings{}
]
Entity{/*...*/}
Entity{/*...*/}
//...
}
Toaddthis3Dsceneintothemain.qmlfile(oranyotherQtQuick-
basedQMLfile),youshouldusetheScene3DQMLtypethatcanbe
importedfromtheQtQuick.Scene3Dmodule.Forexample,thisishow
youcancreateaformwithabuttonanda3Dview:
importQtQuick2.10
importQtQuick.Layouts1.0
importQtQuick.Controls1.0
importQtQuick.Window2.0
importQtQuick.Scene3D2.0
Window{
visible:true
Button{
id:button1
text:"button1"
anchors{
top:parent.top
left:parent.left
right:parent.right
margins:10
}
}
Scene3D{
focus:true
anchors{
top:button1.bottom
bottom:parent.bottom
left:parent.left
right:parent.right
margins:10
}
aspects:["input","logic"]
My3DScene{}
}
}
MostofthecodeistheusualcontentofaQtQuickform.TheScene3D
itemdoesallthemagic.Theroot3Dentityshouldbeaddedtothis
itemdirectlyor,asinourcase,intheformofacustomcomponent.
TheScene3DitemsetsupaQt3Dengineandrendersthepassed
scene:
IfyouwanttouseQt3DInputorQt3DLogicmodules,youneedtoenable
thecorresponding3DaspectsusingtheaspectspropertyofScene3D,as
showninthescreenshot.Additionally,themultisampleBoolean
propertycanbeusedtoenablemultisampling.ThehoverEnabled
propertycanbeusedtoenablehandlingofmouseeventswhen
mousebuttonsarenotpressed.
SimilartoQt3DQuickWindow,Scene3Dsetsthecamera'saspectratio
automaticallybydefault.Youcandisableitbysetting
itscameraAspectRatioModepropertytoScene3D.UserAspectRatio.
ThisapproachcanalsobeusedtodisplaysomeUIcontrolsontop
ofthe3Dview.ThiswillallowyoutousethefullpowerofQtQuick
tomaketheUIofyourgameamazing.
Popquiz
Q1.Whichcomponentcanbeusedtorotatea3Dobject?
1. CuboidMesh
2. RotationAnimation
3. Transform
Q2.Whichcomponentisthemostsuitableforemulatingthelightof
thesun?
1. DirectionalLight
2. PointLight
3. SpotLight
Q3.WhatisaQt3Dmaterial?
1. Anobjectthatallowsyoutoloadatexturefromafile.
2. Acomponentthatdefinesthephysicalpropertiesofthe
object.
3. Acomponentthatdefinesthevisiblepropertiesofthe
object'ssurface.
Summary
Inthischapter,welearnedtocreate3DgameswithQt.Wesawhow
tocreateandposition3Dobjectsinthesceneandconfigurethe
cameraforrendering.Next,weexaminedhowwecanhandleuser
inputusingQt3D.Morethanthat,youlearnedtoapplyyour
existinganimationskillstoQt3Dobjects.Finally,wefoundout
howtouseQt3DtogetherwithotherQtmodules.
LikeQtQuick,Qt3Disrapidlyevolving.Atthetimeofwriting,
someofthemodulesarestillexperimental.Youshouldexpectthe
APIofQt3Dtobeimprovedandextended,somakesureyoucheck
theQtdocumentationfornewerreleases.
ThisconcludesourbookongameprogrammingusingQt.Wehave
taughtyouthegeneralbasicsofQt,describeditswidgetrealmto
you,andintroducedyoutothefascinatingworldofQtQuickandQt
3D.Widgets(includingGraphicsView),QtQuick,andQt3Darethe
mainpathsyoucantakewhencreatinggamesusingtheQt
framework.Wehavealsoshownyouwaysofmergingthetwo
approachesbymakinguseofanyOpenGLorVulkanskillsyou
mighthave,goingbeyondwhatQtalreadyofferstoday.Atthis
point,youshouldstartplayingaroundandexperimenting,andifat
anypointyoufeellostorsimplylacktheinformationonhowtodo
something,theveryhelpfulQtreferencemanualshouldbethefirst
resourceyoudirectyourselfto.
Goodluckandhavelotsoffun!
Popquizanswers
Chapter3
Q1:2
Q2:2
Q3:2
Q4:1
Chapter4
Q1:1
Q2:4
Q3:2
Q4:3
Chapter5
Q1:3
Q2:1
Q3:3
Chapter6
Q1:1
Q2:2
Q3:3
Q4:3
Chapter7
Q1:1
Q2:2
Q3:2
Q4:3
Chapter8
Q1:1
Q2:2
Q3:3
Chapter9
Q1:3
Q2:1
Q3:3
Chapter10
Q1:2
Q2:2
Q3:1
Q4:4
Q5:2
Chapter11
Q1:2
Q2:2
Q3:3
Chapter12
Q1:2
Q2:3
Q3:3
Chapter13
Q1:2
Q2:1
Q3:2
Chapter14
Q1:2
Q2:2
Q3:2
Chapter15
Q1:3
Q2:1
Q3:3
Chapter16
Q1:3
Q2:2
Q3:1
Q4:3
OtherBooksYouMayEnjoy
Ifyouenjoyedthisbook,youmaybeinterestedintheseotherbooks
byPackt:
LearnQT5
NicholasSherriff
ISBN:978-1-78847-885-4
InstallandconfiguretheQtFrameworkandQtCreatorIDE
Createanewmulti-projectsolutionfromscratchandcontrol
everyaspectofitwithQMake
ImplementarichuserinterfacewithQML
LearnthefundamentalsofQtTestandhowtointegrateunit
testing
Buildself-awaredataentitiesthatcanserializethemselves
toandfromJSON
ManagedatapersistencewithSQLiteandCRUDoperations
ReachouttotheinternetandconsumeanRSSfeed
Produceapplicationpackagesfordistributiontootherusers
Qt5Projects
MarcoPiccolino
ISBN:978-1-78829-388-4
LearnthebasicsofmodernQtapplicationdevelopment
DevelopsolidandmaintainableapplicationswithBDD,
TDD,andQtTest
MasterthelatestUItechnologiesandknowwhentouse
them:QtQuick,Controls2,Qt3DandCharts
BuildadesktopUIwithWidgetsandtheDesigner
TranslateyouruserinterfaceswithQTranslatorandLinguist
Getfamiliarwithmultimediacomponentstohandlevisual
inputandoutput
Exploredatamanipulationandtransfer:themodel/view
framework,JSON,Bluetooth,andnetworkI/O
TakeadvantageofexistingwebtechnologiesandUI
componentswithWebEngine
Leaveareview-letother
readersknowwhatyouthink
Pleaseshareyourthoughtsonthisbookwithothersbyleavinga
reviewonthesitethatyouboughtitfrom.Ifyoupurchasedthe
bookfromAmazon,pleaseleaveusanhonestreviewonthisbook's
Amazonpage.Thisisvitalsothatotherpotentialreaderscansee
anduseyourunbiasedopiniontomakepurchasingdecisions,we
canunderstandwhatourcustomersthinkaboutourproducts,and
ourauthorscanseeyourfeedbackonthetitlethattheyhaveworked
withPackttocreate.Itwillonlytakeafewminutesofyourtime,but
isvaluabletootherpotentialcustomers,ourauthors,andPackt.
Thankyou!