This is not really part of my PyQt by Example series * but since it's a totally unrelated topic that would be impossible to connect to it, but is still a PyQt tutorial and follows the same concept... whatever, here it is.
In this tutorial I will explain QGraphicsScene (QGS for short) and its related classes. What are they for, you may ask? Well, the answer is: they are for cool stuff.
You don't need QGS for CRUD. You don't need QGS for office applications. You don't need QGS for boring stuff. Just for fun stuff, like games or anything that needs fancy graphics and a non-conventional UI.
How does it work
You can think of a QGraphicsScene as a stage, filled with actors. The actors are called QGraphicsItems and they can do things in the stage.
An item can be of different kinds:
A shape (like a circle or a polygon)
A widget (yes, any Qt widget works!)
Then, you can have one or more "cameras" looking at the stage. That's a QGraphicsView.
Keep this in mind, because it's very important:
One QGraphicsScene can contain any number of QGraphicsItems.
Any number of QGraphicsViews can be linked to a QGraphicsScene
Whatever the items do in one scene, all views will show the same thing.
So, having said that, let's start doing code.
I don't believe in many things, but I do believe one thing: It's almost never a good idea to do your PyQt UI without using designer.
In this specific case, our first example application has the silliest UI you can think of: an empty window with a QGraphicsView in it. Still, I did it using designer, and here is the file, which is as trivial as it sounds: window.ui
As usual, I will start with my basic template for a PyQt app, and add a little bit of code in the main widget's __init__ method so it does something interesting ( don't worry much about what populate and animate do, yet):
# -*- coding: utf-8 -*- """The user interface for our app""" import os,sys,time # Import Qt modules from PyQt4 import QtCore,QtGui, QtOpenGL # Import the compiled UI module from ui_clock import Ui_Form from random import randint, shuffle # Create a class for our main window class Main(QtGui.QWidget): def __init__(self): QtGui.QWidget.__init__(self) # This is always the same self.ui=Ui_Form() self.ui.setupUi(self) # From here until the end of this method is # the only interesting part! # Since the UI is a QGraphicsView, I create a Scene # so it has something to show self.scene=QtGui.QGraphicsScene() self.ui.view.setScene(self.scene) self.scene.setSceneRect(0,0,600,400) # This makes the view OpenGL-accelerated. Usually makes # things much faster, but it *is* optional. self.ui.view.setViewport(QtOpenGL.QGLWidget()) # populate fills the scene with interesting stuff. self.populate() # Make it bigger self.setWindowState(QtCore.Qt.WindowMaximized) # Well... it's going to have an animation, ok? # So, I set a timer to 1 second self.animator=QtCore.QTimer() # And when it triggers, it calls the animate method self.animator.timeout.connect(self.animate) # And I animate it once manually. self.animate() def main(): # Again, this is boilerplate, it's going to be the same on # almost every app you write app = QtGui.QApplication(sys.argv) window=Main() window.show() # It's exec_ because exec is a reserved word in Python sys.exit(app.exec_()) if __name__ == "__main__": main()
Here's what that code does, in a nutshell:
Setup a QGraphicsScene
Connect our view to it
Fill the scene with something (populate)
Make the items do something (animate)
Now, let's see the missing pieces. First, populate:
def populate(self): self.digits= self.animations= # This is just a nice font, use any font you like, or none font=QtGui.QFont('White Rabbit') font.setPointSize(120) # Create three ":" and place them in our scene self.dot1=QtGui.QGraphicsTextItem(':') self.dot1.setFont(font) self.dot1.setPos(140,0) self.scene.addItem(self.dot1) self.dot2=QtGui.QGraphicsTextItem(':') self.dot2.setFont(font) self.dot2.setPos(410,0) self.scene.addItem(self.dot2) # Create 6 sets of 0-9 digits for i in range(60): l = QtGui.QGraphicsTextItem(str(i%10)) l.setFont(font) # The zvalue is what controls what appears "on top" of what. # Send them to "the bottom" of the scene. l.setZValue(-100) # Place them anywhere l.setPos(randint(0,500),randint(150,300)) # Make them semi-transparent l.setOpacity(.3) # Put them in the scene self.scene.addItem(l) # Keep a reference for internal purposes self.digits.append(l)
As you can see, populating a scene is as simple as creating whatever items you want to be in the scene, put them in their positions, set their attributes any way you want, and adding them to the scene itself.
Now, the "tricky" one, animate:
def animate(self): # Just a list with 60 positions self.animations=range(0,60) # This is the only "hard" part # Given an item, and where you want it to be # it moves it there, smoothly, in one second. def animate_to(t,item,x,y,angle): # The QGraphicsItemAnimation class is used to # animate an item in specific ways animation=QtGui.QGraphicsItemAnimation() # You create a timeline (in this case, it is 1 second long timeline=QtCore.QTimeLine(1000) # And it has 100 steps timeline.setFrameRange(0,100) # I want that, at time t, the item be at point x,y animation.setPosAt(t,QtCore.QPointF(x,y)) # And it should be rotated at angle "angle" animation.setRotationAt(t,angle) # It should animate this specific item animation.setItem(item) # And the whole animation is this long, and has # this many steps as I set in timeline. animation.setTimeLine(timeline) # Here is the animation, use it. return animation # Ok, I confess it, this part is a mess, but... a little # mistery is good for you. Read this carefully, and tell # me if you can do it better. Or try to something nicer! offsets=range(6) shuffle(offsets) # Some items, animate with purpose h1,h2=map(int,'%02d'%time.localtime().tm_hour) h1+=offsets*10 h2+=offsets*10 self.animations[h1]=animate_to(0.2,self.digits[h1],-40,0,0) self.animations[h2]=animate_to(0.2,self.digits[h2],50,0,0) m1,m2=map(int,'%02d'%time.localtime().tm_min) m1+=offsets*10 m2+=offsets*10 self.animations[m1]=animate_to(0.2,self.digits[m1],230,0,0) self.animations[m2]=animate_to(0.2,self.digits[m2],320,0,0) s1,s2=map(int,'%02d'%time.localtime().tm_sec) s1+=offsets*10 s2+=offsets*10 self.animations[s1]=animate_to(0.2,self.digits[s1],500,0,0) self.animations[s2]=animate_to(0.2,self.digits[s2],590,0,0) # Other items, animate randomly for i in range(60): l = self.digits[i] if i in [h1,h2,m1,m2,s1,s2]: l.setOpacity(1) continue l.setOpacity(.3) self.animations[i]=animate_to(1,l,randint(0,500),randint(0,300),randint(0,0)) [ animation.timeLine().start() for animation in self.animations ] self.animator.start(1000)
Sadly I can't show you the end result in a video, it's pretty hard to capture. But here's a teaser (95% of the fun is the animation, so try it yourself ;-):
Coming in session 2 of this detour: interaction and more