Skip to main content

Ralsina.Me — Roberto Alsina's website

PyQt by Example detour: QGraphicsScene and friends session1

Introduction

This is not re­al­ly part of my PyQt by Ex­am­ple se­ries [*] but since it's a to­tal­ly un­re­lat­ed top­ic that would be im­pos­si­ble to con­nect to it, but is still a PyQt tu­to­ri­al and fol­lows the same con­cep­t... what­ev­er, here it is.

In this tu­to­ri­al I will ex­plain QGraph­ic­sS­cene (QGS for short) and its re­lat­ed class­es. What are they for, you may ask? Well, the an­swer is: they are for cool stuff.

You don't need QGS for CRUD. You don't need QGS for of­fice ap­pli­ca­tion­s. You don't need QGS for bor­ing stuff. Just for fun stuff, like games or any­thing that needs fan­cy graph­ics and a non-­con­ven­tion­al UI.

How does it work

You can think of a QGraph­ic­sS­cene as a stage, filled with ac­tors. The ac­tors are called QGraph­ic­sItems and they can do things in the stage.

An item can be of dif­fer­ent kind­s:

  • A text

  • A shape (like a cir­­cle or a poly­­gon)

  • A pic­­ture

  • A wid­get (yes, any Qt wid­get work­s!)

Then, you can have one or more "cam­eras" look­ing at the stage. That's a QGraph­icsView.

Keep this in mind, be­cause it's very im­por­tan­t:

  • One QGraph­ic­sS­cene can con­­tain any num­ber of QGraph­ic­sItem­s.

  • Any num­ber of QGraph­icsViews can be linked to a QGraph­ic­sS­cene

  • What­ev­er the items do in one scene, all views will show the same thing.

So, hav­ing said that, let's start do­ing code.

I don't be­lieve in many things, but I do be­lieve one thing: It's al­most nev­er a good idea to do your PyQt UI with­out us­ing de­sign­er.

In this spe­cif­ic case, our first ex­am­ple ap­pli­ca­tion has the sil­li­est UI you can think of: an emp­ty win­dow with a QGraph­icsView in it. Stil­l, I did it us­ing de­sign­er, and here is the file, which is as triv­ial as it sound­s: win­dow.ui

As usu­al, I will start with my ba­sic tem­plate for a PyQt ap­p, and add a lit­tle bit of code in the main wid­get's __init__ method so it does some­thing in­ter­est­ing ( don't wor­ry much about what pop­u­late and an­i­mate 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 nut­shel­l:

  • Set­up a QGraph­ic­sS­cene

  • Con­nect our view to it

  • Fill the scene with some­thing (pop­u­late)

  • Make the items do some­thing (an­i­­mate)

Now, let's see the miss­ing pieces. First, pop­u­late:

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, pop­u­lat­ing a scene is as sim­ple as cre­at­ing what­ev­er items you want to be in the scene, put them in their po­si­tion­s, set their at­tributes any way you wan­t, and adding them to the scene it­self.

Now, the "trick­y" one, an­i­mate:

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[0]*10
    h2+=offsets[1]*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[2]*10
    m2+=offsets[3]*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[4]*10
    s2+=offsets[5]*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)

Sad­ly I can't show you the end re­sult in a video, it's pret­ty hard to cap­ture. But here's a teas­er (95% of the fun is the an­i­ma­tion, so try it your­self ;-):

clock1

Com­ing in ses­sion 2 of this de­tour: in­ter­ac­tion and more

You can find the code and this text in github:

New sport / 2010-02-01 18:12:

I agree with this blog, this comment is very interesting and I want to visit it more frequently.

Mihai / 2011-06-09 17:18:

I've downloaded the source for graphicsscene (this is my main interest now),
and it doesn't work
from ui_clock import Ui_Formwhat's Ui_Form? or ui_clock?I didn't find anything as these,I've downloaded all sources in a zip file.Am I missing something?

Noname / 2011-09-17 09:14:

You need to generate it first: "pyuic clock.ui > ui_clock.py"


Contents © 2000-2024 Roberto Alsina