Step By Step PyGTK Tutorials

PyGTK tutorial

This is PyGTK tutorial. In this tutorial, we will learn the basics of GUI programming in PyGTK. The PyGTK tutorial is suitable for beginners and more advanced programmers.

Table of contents

PyGTK

PyGTK is a set of Python wrappers for the GTK+ GUI library. It offers a comprehensive set of graphical elements and other useful programming facilities for creating desktop applications. It is a part of the GNOME project. PyGTK is free software and licensed under the LGPL.
Continue Reading

Custom widget in PyGTK

Custom widget in PyGTK

Have you ever looked at an application and wondered, how a particular gui item was created? Probably every wannabe programmer has. Then you were looking at a list of widgets provided by your favourite gui library. But you couldn't find it. Toolkits usually provide only the most common widgets like buttons, text widgets, sliders etc. No toolkit can provide all possible widgets.
There are actually two kinds of toolkits. Spartan toolkits and heavy weight toolkits. The FLTK toolkit is a kind of a spartan toolkit. It provides only the very basic widgets and assumes, that the programemer will create the more complicated ones himself. wxWidgets is a heavy weight one. It has lots of widgets. Yet it does not provide the more specialized widgets. For example a speed meter widget, a widget that measures the capacity of a CD to be burned (found e.g. in nero). Toolkits also don't have usually charts.
Programmers must create such widgets by themselves. They do it by using the drawing tools provided by the toolkit. There are two possibilities. A programmer can modify or enhance an existing widget. Or he can create a custom widget from scratch.

Burning widget

This is an example of a widget, that we create from scratch. This widget can be found in various media burning applications, like Nero Burning ROM.
burning.py
#!/usr/bin/python
# -*- coding: utf-8 -*-

# ZetCode PyGTK tutorial
#
# This example creates a burning
# custom widget
#
# author: Jan Bodnar
# website: zetcode.com
# last edited: April 2011


import gtk
import cairo

class Burning(gtk.DrawingArea):

def __init__(self, parent):

self.par = parent
super(Burning, self).__init__()

self.num = ( "75", "150", "225", "300",
"375", "450", "525", "600", "675" )

self.set_size_request(-1, 30)
self.connect("expose-event", self.expose)


def expose(self, widget, event):

cr = widget.window.cairo_create()
cr.set_line_width(0.8)

cr.select_font_face("Courier",
cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
cr.set_font_size(11)

width = self.allocation.width

self.cur_width = self.par.get_cur_value()

step = round(width / 10.0)

till = (width / 750.0) * self.cur_width
full = (width / 750.0) * 700

if (self.cur_width >= 700):

cr.set_source_rgb(1.0, 1.0, 0.72)
cr.rectangle(0, 0, full, 30)
cr.save()
cr.clip()
cr.paint()
cr.restore()

cr.set_source_rgb(1.0, 0.68, 0.68)
cr.rectangle(full, 0, till-full, 30)
cr.save()
cr.clip()
cr.paint()
cr.restore()

else:
cr.set_source_rgb(1.0, 1.0, 0.72)
cr.rectangle(0, 0, till, 30)
cr.save()
cr.clip()
cr.paint()
cr.restore()


cr.set_source_rgb(0.35, 0.31, 0.24)

for i in range(1, len(self.num) + 1):
cr.move_to(i*step, 0)
cr.line_to(i*step, 5)
cr.stroke()

(x, y, width, height, dx, dy) = cr.text_extents(self.num[i-1])
cr.move_to(i*step-width/2, 15)
cr.text_path(self.num[i-1])
cr.stroke()



class PyApp(gtk.Window):

def __init__(self):
super(PyApp, self).__init__()

self.set_title("Burning")
self.set_size_request(350, 200)
self.set_position(gtk.WIN_POS_CENTER)
self.connect("destroy", gtk.main_quit)

self.cur_value = 0

vbox = gtk.VBox(False, 2)

scale = gtk.HScale()
scale.set_range(0, 750)
scale.set_digits(0)
scale.set_size_request(160, 40)
scale.set_value(self.cur_value)
scale.connect("value-changed", self.on_changed)

fix = gtk.Fixed()
fix.put(scale, 50, 50)

vbox.pack_start(fix)

self.burning = Burning(self)
vbox.pack_start(self.burning, False, False, 0)

self.add(vbox)
self.show_all()


def on_changed(self, widget):
self.cur_value = widget.get_value()
self.burning.queue_draw()


def get_cur_value(self):
return self.cur_value


PyApp()
gtk.main()
We put a DrawingArea on the bottom of the window and draw the entire widget manually. All the important code resides in the expose() method of the Burning class. This widget shows graphically the total capacity of a medium and the free space available to us. The widget is controlled by a scale widget. The minimum value of our custom widget is 0, the maximum is 750. If we reach value 700, we began drawing in red colour. This normally indicates overburning.
self.num = ( "75", "150", "225", "300", 
"375", "450", "525", "600", "675" )
These numbers are shown on the burning widget. They show the capacity of the medium.
self.cur_width = self.par.get_cur_value()
These two lines get the current number from the scale widget. We get the parent widget and from the parent widget, we get the current value.
till = (width / 750.0) * self.cur_width
full = (width / 750.0) * 700
The till parameter determines the total size to be drawn. This value comes from the slider widget. It is a proportion of the whole area. The full parameter determines the point, where we begin to draw in red color.
cr.set_source_rgb(1.0, 1.0, 0.72)
cr.rectangle(0, 0, till, 30)
cr.save()
cr.clip()
cr.paint()
cr.restore()
This code here, draws a yellow rectangle up to point, where the medium is full.
(x, y, width, height, dx, dy) = cr.text_extents(self.num[i-1])
cr.move_to(i*step-width/2, 15)
cr.text_path(self.num[i-1])
cr.stroke()
This code here draws the numbers on the burning widget. We calculate the TextExtents to position the text correctly.
def on_changed(self, widget):
self.cur_value = widget.get_value()
self.burning.queue_draw()
We get the value from the scale widget, store it in the cur_value variable for later use. We redraw the burning widget.

Burning widget
Figure: Burning widget

In this chapter, we created a custom widget in PyGTK.
Continue Reading

Snake game in PyGTK

Snake game in PyGTK

In this part of the PyGTK programming tutorial, we will create a Snake game clone.

Snake game

Snake is an older classic video game. It was first created in late 70s. Later it was brought to PCs. In this game the player controls a snake. The objective is to eat as many apples as possible. Each time the snake eats an apple, its body grows. The snake must avoid the walls and its own body. This game is sometimes called Nibbles.

Development

The size of each of the joints of a snake is 10px. The snake is controlled with the cursor keys. Initially the snake has three joints. The game starts immediately. If the game is finished, we display "Game Over" message in the middle of the Board.
snake.py
#!/usr/bin/python

# ZetCode PyGTK tutorial
#
# This is a simple snake game
# clone
#
# author: jan bodnar
# website: zetcode.com
# last edited: February 2009

import sys
import gtk
import cairo
import random
import glib


WIDTH = 300
HEIGHT = 270
DOT_SIZE = 10
ALL_DOTS = WIDTH * HEIGHT / (DOT_SIZE * DOT_SIZE)
RAND_POS = 26

x = [0] * ALL_DOTS
y = [0] * ALL_DOTS


class Board(gtk.DrawingArea):

def __init__(self):
super(Board, self).__init__()

self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(0, 0, 0))
self.set_size_request(WIDTH, HEIGHT)

self.connect("expose-event", self.expose)

self.init_game()

def on_timer(self):

if self.inGame:
self.check_apple()
self.check_collision()
self.move()
self.queue_draw()
return True
else:
return False

def init_game(self):

self.left = False
self.right = True
self.up = False
self.down = False
self.inGame = True
self.dots = 3

for i in range(self.dots):
x[i] = 50 - i * 10
y[i] = 50

try:
self.dot = cairo.ImageSurface.create_from_png("dot.png")
self.head = cairo.ImageSurface.create_from_png("head.png")
self.apple = cairo.ImageSurface.create_from_png("apple.png")
except Exception, e:
print e.message
sys.exit(1)

self.locate_apple()
glib.timeout_add(100, self.on_timer)




def expose(self, widget, event):

cr = widget.window.cairo_create()

if self.inGame:
cr.set_source_rgb(0, 0, 0)
cr.paint()

cr.set_source_surface(self.apple, self.apple_x, self.apple_y)
cr.paint()

for z in range(self.dots):
if (z == 0):
cr.set_source_surface(self.head, x[z], y[z])
cr.paint()
else:
cr.set_source_surface(self.dot, x[z], y[z])
cr.paint()
else:
self.game_over(cr)



def game_over(self, cr):

w = self.allocation.width / 2
h = self.allocation.height / 2

(x, y, width, height, dx, dy) = cr.text_extents("Game Over")

cr.set_source_rgb(65535, 65535, 65535)
cr.move_to(w - width/2, h)
cr.show_text("Game Over")
self.inGame = False



def check_apple(self):

if x[0] == self.apple_x and y[0] == self.apple_y:
self.dots = self.dots + 1
self.locate_apple()


def move(self):

z = self.dots

while z > 0:
x[z] = x[(z - 1)]
y[z] = y[(z - 1)]
z = z - 1

if self.left:
x[0] -= DOT_SIZE

if self.right:
x[0] += DOT_SIZE

if self.up:
y[0] -= DOT_SIZE

if self.down:
y[0] += DOT_SIZE



def check_collision(self):

z = self.dots

while z > 0:
if z > 4 and x[0] == x[z] and y[0] == y[z]:
self.inGame = False
z = z - 1

if y[0] > HEIGHT - DOT_SIZE:
self.inGame = False

if y[0] < 0:
self.inGame = False

if x[0] > WIDTH - DOT_SIZE:
self.inGame = False

if x[0] < 0:
self.inGame = False


def locate_apple(self):

r = random.randint(0, RAND_POS)
self.apple_x = r * DOT_SIZE
r = random.randint(0, RAND_POS)
self.apple_y = r * DOT_SIZE


def on_key_down(self, event):

key = event.keyval

if key == gtk.keysyms.Left and not self.right:
self.left = True
self.up = False
self.down = False


if key == gtk.keysyms.Right and not self.left:
self.right = True
self.up = False
self.down = False


if key == gtk.keysyms.Up and not self.down:
self.up = True
self.right = False
self.left = False


if key == gtk.keysyms.Down and not self.up:
self.down = True
self.right = False
self.left = False


class Snake(gtk.Window):

def __init__(self):
super(Snake, self).__init__()

self.set_title('Snake')
self.set_size_request(WIDTH, HEIGHT)
self.set_resizable(False)
self.set_position(gtk.WIN_POS_CENTER)

self.board = Board()
self.connect("key-press-event", self.on_key_down)
self.add(self.board)

self.connect("destroy", gtk.main_quit)
self.show_all()


def on_key_down(self, widget, event):

key = event.keyval
self.board.on_key_down(event)


Snake()
gtk.main()
First we will define some globals used in our game.
The WIDTH and HEIGHT constants determine the size of the Board. The DOT_SIZE is the size of the apple and the dot of the snake. The ALL_DOTS constant defines the maximum number of possible dots on the Board. The RAND_POS constant is used to calculate a random position of an apple. The DELAY constant determines the speed of the game.
 x = [0] * ALL_DOTS
y = [0] * ALL_DOTS
These two lists store x, y coordinates of all possible joints of a snake.
The init_game() method initializes variables, loads images and starts a timeout function.
 self.left = False
self.right = True
self.up = False
self.down = False
self.inGame = True
self.dots = 3
When the game starts, the snake has three joints. And it is heading to the right.
In the move() method we have the key algorithm of the game. To understand it, look at how the snakeis moving. You control the head of the snake. You can change its direction with the cursor keys. The rest of the joints move one position up the chain. The second joint moves where the first was, the third joint where the second was etc.
 while z > 0:
x[z] = x[(z - 1)]
y[z] = y[(z - 1)]
z = z - 1
This code moves the joints up the chain.
 if self.left:
x[0] -= DOT_SIZE
Move the head to the left.
In the checkCollision() method, we determine if the snake has hit itself or one of the walls.
 while z > 0:
if z > 4 and x[0] == x[z] and y[0] == y[z]:
self.inGame = False
z = z - 1
Finish the game, if the snake hits one of its joints with the head.
 if y[0] > HEIGHT - DOT_SIZE: 
self.inGame = False
Finish the game, if the snake hits the bottom of the Board.
The locate_apple() method locates an apple randomly on the form.
 r = random.randint(0, RAND_POS)
We get a random number from 0 to RAND_POS - 1.
 self.apple_x = r * DOT_SIZE
...
self.apple_y = r * DOT_SIZE
These line set the x, y coordinates of the apple object.
     self.connect("key-press-event", self.on_key_down)
...

def on_key_down(self, widget, event):

key = event.keyval
self.board.on_key_down(event)
We catch the key press event in the Snake class, and delegate the processing to the board object.
In the on_key_dow() method of the Board class, we deternime which keys the player hit.
 if key == gtk.keysyms.Left and not self.right: 
self.left = True
self.up = False
self.down = False
If we hit the left cursor key, we set self.left variable to True. This variable is used in the move()method to change coordinates of the snake object. Notice also, that when the snake is heading to the right, we cannot turn immediately to the left.

Snake
Figure: Snake

This was the Snake computer game programmed using PyGTK programming library.
Continue Reading

Continue drawing with Cairo in PyGTK

Continue drawing with Cairo in PyGTK

In this part of the PyGTK programming tutorial, we will continue drawing with the Cairo library.

Donut

In the following example we create a complex shape by rotating a bunch of ellipses.
donut.py
#!/usr/bin/python

# ZetCode PyGTK tutorial
#
# This program creates a donut
# with cairo library
#
# author: jan bodnar
# website: zetcode.com
# last edited: February 2009


import gtk
import math

class PyApp(gtk.Window):

def __init__(self):
super(PyApp, self).__init__()

self.set_title("Donut")
self.set_size_request(350, 250)
self.set_position(gtk.WIN_POS_CENTER)

self.connect("destroy", gtk.main_quit)

darea = gtk.DrawingArea()
darea.connect("expose-event", self.expose)
self.add(darea)

self.show_all()

def expose(self, widget, event):

cr = widget.window.cairo_create()

cr.set_line_width(0.5)

w = self.allocation.width
h = self.allocation.height

cr.translate(w/2, h/2)
cr.arc(0, 0, 120, 0, 2*math.pi)
cr.stroke()


for i in range(36):
cr.save()
cr.rotate(i*math.pi/36)
cr.scale(0.3, 1)
cr.arc(0, 0, 120, 0, 2*math.pi)
cr.restore()
cr.stroke()


PyApp()
gtk.main()
In this example, we create a donut. The shape resembles a cookie, hence the name donut.
 cr.translate(w/2, h/2)
cr.arc(0, 0, 120, 0, 2*math.pi)
cr.stroke()
In the beginning there is an ellipse.
  for i in range(36):
cr.save()
cr.rotate(i*math.pi/36)
cr.scale(0.3, 1)
cr.arc(0, 0, 120, 0, 2*math.pi)
cr.restore()
cr.stroke()
After several rotations, there is a donut. We insulate each rotate and scale operations from one another with the save() and restore() methods.

Donut
Figure: Donut

Gradients

In computer graphics, gradient is a smooth blending of shades from light to dark or from one color to another. In 2D drawing programs and paint programs, gradients are used to create colorful backgrounds and special effects as well as to simulate lights and shadows. (answers.com)
gradients.py
#!/usr/bin/python

# ZetCode PyGTK tutorial
#
# This program works with
# gradients in cairo
#
# author: jan bodnar
# website: zetcode.com
# last edited: February 2009

import gtk
import cairo

class PyApp(gtk.Window):

def __init__(self):
super(PyApp, self).__init__()

self.set_title("Gradients")
self.set_size_request(340, 390)
self.set_position(gtk.WIN_POS_CENTER)

self.connect("destroy", gtk.main_quit)

darea = gtk.DrawingArea()
darea.connect("expose-event", self.expose)
self.add(darea)

self.show_all()

def expose(self, widget, event):

cr = widget.window.cairo_create()
lg1 = cairo.LinearGradient(0.0, 0.0, 350.0, 350.0)

count = 1

i = 0.1
while i < 1.0:
if count % 2:
lg1.add_color_stop_rgba(i, 0, 0, 0, 1)
else:
lg1.add_color_stop_rgba(i, 1, 0, 0, 1)
i = i + 0.1
count = count + 1


cr.rectangle(20, 20, 300, 100)
cr.set_source(lg1)
cr.fill()

lg2 = cairo.LinearGradient(0.0, 0.0, 350.0, 0)

count = 1

i = 0.05
while i < 0.95:
if count % 2:
lg2.add_color_stop_rgba(i, 0, 0, 0, 1)
else:
lg2.add_color_stop_rgba(i, 0, 0, 1, 1)
i = i + 0.025
count = count + 1

cr.rectangle(20, 140, 300, 100)
cr.set_source(lg2)
cr.fill()

lg3 = cairo.LinearGradient(20.0, 260.0, 20.0, 360.0)
lg3.add_color_stop_rgba(0.1, 0, 0, 0, 1)
lg3.add_color_stop_rgba(0.5, 1, 1, 0, 1)
lg3.add_color_stop_rgba(0.9, 0, 0, 0, 1)

cr.rectangle(20, 260, 300, 100)
cr.set_source(lg3)
cr.fill()


PyApp()
gtk.main()
In our example, we draw three rectangles with three different gradients.
lg1 = cairo.LinearGradient(0.0, 0.0, 350.0, 350.0)
Here we create a linear gradient pattern. The parameters specify the line, along which we draw the gradient. In our case it is a vertical line.
 lg3 = cairo.LinearGradient(20.0, 260.0,  20.0, 360.0)
lg3.add_color_stop_rgba(0.1, 0, 0, 0, 1)
lg3.add_color_stop_rgba(0.5, 1, 1, 0, 1)
lg3.add_color_stop_rgba(0.9, 0, 0, 0, 1)
We define color stops to produce our gradient pattern. In this case, the gradient is a blending of black and yellow colors. By adding two black and one yellow stops, we create a horizontal gradient pattern. What do these stops actually mean? In our case, we begin with black color, which will stop at 1/10 of the size. Then we begin to gradually paint in yellow, which will culminate at the centre of the shape. The yellow color stops at 9/10 of the size, where we begin painting in black again, until the end.

Gradients
Figure: Gradients

Puff

In the following example, we create a puff effect. The example will display a growing centered text, that will gradually fade out from some point. This is a very common effect, which you can often see in flash animations.
puff.py
#!/usr/bin/python

# ZetCode PyGTK tutorial
#
# This program creates a puff
# effect
#
# author: jan bodnar
# website: zetcode.com
# last edited: February 2009


import gtk
import glib
import cairo


class PyApp(gtk.Window):

def __init__(self):
super(PyApp, self).__init__()

self.set_title("Puff")
self.resize(350, 200)
self.set_position(gtk.WIN_POS_CENTER)

self.connect("destroy", gtk.main_quit)

self.darea = gtk.DrawingArea()
self.darea.connect("expose-event", self.expose)
self.add(self.darea)

self.timer = True
self.alpha = 1.0
self.size = 1.0

glib.timeout_add(14, self.on_timer)

self.show_all()

def on_timer(self):
if not self.timer: return False

self.darea.queue_draw()
return True


def expose(self, widget, event):

cr = widget.window.cairo_create()

w = self.allocation.width
h = self.allocation.height

cr.set_source_rgb(0.5, 0, 0)
cr.paint()

cr.select_font_face("Courier", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)

self.size = self.size + 0.8

if self.size > 20:
self.alpha = self.alpha - 0.01

cr.set_font_size(self.size)
cr.set_source_rgb(1, 1, 1)

(x, y, width, height, dx, dy) = cr.text_extents("ZetCode")

cr.move_to(w/2 - width/2, h/2)
cr.text_path("ZetCode")
cr.clip()
cr.stroke()
cr.paint_with_alpha(self.alpha)

if self.alpha <= 0:
self.timer = False


PyApp()
gtk.main()
The example creates a growing and fading text on the window.
 glib.timeout_add(14, self.on_timer)
Every 14 ms the on_timer() method is called.
 def on_timer(self):
if not self.timer: return False

self.darea.queue_draw()
return True
In the on_timer() method, we call the queue_draw() method upon the drawing area, which triggers the expose signal.
 cr.set_source_rgb(0.5, 0, 0)
cr.paint()
We set the background color to dark red color.
 self.size = self.size + 0.8
Each cycle, the font size will grow by 0.8 units.
 if self.size > 20:
self.alpha = self.alpha - 0.01
The fading out begins after the font size is bigger than 20.
 (x, y, width, height, dx, dy) = cr.text_extents("ZetCode")
We get the text metrics.
 cr.move_to(w/2 - width/2, h/2)
We use the text metrics to center the text on the window.
 cr.text_path("ZetCode")
cr.clip()
We get the path of the text and set the current clip region to it.
 cr.stroke()
cr.paint_with_alpha(self.alpha)
We paint the current path and take alpha value into account.

Puff
Figure: Puff

Reflection

In the next example we show a reflected image. This beautiful effect makes an illusion as if the image was reflected in water.
reflection.py
#!/usr/bin/python
# -*- coding: utf-8 -*-

# ZetCode PyGTK tutorial
#
# This program creates an
# image reflection
#
# author: Jan Bodnar
# website: zetcode.com
# last edited: April 2011


import gtk
import cairo
import sys

class PyApp(gtk.Window):

def __init__(self):
super(PyApp, self).__init__()

self.set_title("Reflection")
self.resize(300, 350)
self.set_position(gtk.WIN_POS_CENTER)

self.connect("destroy", gtk.main_quit)

darea = gtk.DrawingArea()
darea.connect("expose-event", self.expose)
self.add(darea)

try:
self.surface = cairo.ImageSurface.create_from_png("slanec.png")
except Exception, e:
print e.message
sys.exit(1)


self.imageWidth = self.surface.get_width()
self.imageHeight = self.surface.get_height()
self.gap = 40
self.border = 20

self.show_all()

def expose(self, widget, event):

cr = widget.window.cairo_create()


w = self.allocation.width
h = self.allocation.height

lg = cairo.LinearGradient(w/2, 0, w/2, h*3)
lg.add_color_stop_rgba(0, 0, 0, 0, 1)
lg.add_color_stop_rgba(h, 0.2, 0.2, 0.2, 1)

cr.set_source(lg)
cr.paint()

cr.set_source_surface(self.surface, self.border, self.border)
cr.paint()

alpha = 0.7
step = 1.0 / self.imageHeight

cr.translate(0, 2 * self.imageHeight + self.gap)
cr.scale(1, -1)

i = 0


while(i < self.imageHeight):

cr.rectangle(self.border, self.imageHeight-i, self.imageWidth, 1)

i = i + 1

cr.save()
cr.clip()
cr.set_source_surface(self.surface, self.border, self.border)
alpha = alpha - step
cr.paint_with_alpha(alpha)
cr.restore()


PyApp()
gtk.main()
The example shows a reflected castle.
 lg = cairo.LinearGradient(w/2, 0, w/2, h*3)
lg.add_color_stop_rgba(0, 0, 0, 0, 1)
lg.add_color_stop_rgba(h, 0.2, 0.2, 0.2, 1)

cr.set_source(lg)
cr.paint()
The background is filled with a gradiet paint. The paint is a smooth blending from black to dark gray.
 cr.translate(0, 2 * self.imageHeight + self.gap)
cr.scale(1, -1)
This code flips the image and translates it below the original image. The translation operation is necessary, because the scaling operation makes the image upside down and translates the image up. To understand what happens, simply take a photograph and place it on the table. Now flip it.
cr.rectangle(self.border, self.imageHeight-i, self.imageWidth, 1)

i = i + 1

cr.save()
cr.clip()
cr.set_source_surface(self.surface, self.border, self.border)
alpha = alpha - step
cr.paint_with_alpha(alpha)
cr.restore()
This is the final part. We make the second image transparent. But the transparency is not constant. The image gradually fades out. The reflected image is draw line by line. The clip() method restricts the drawing to the rectangle of height 1. The paint_with_alpha() takes the transparency into account when painting the current clip of the image surface.

Reflection
Figure: Reflection

Waiting

In this examle, we use transparency effect to create a waiting demo. We will draw 8 lines that will gradually fade out creating an illusion, that a line is moving. Such effects are often used to inform users, that a lengthy task is going on behind the scenes. An example is streaming video over the internet.
waiting.py
#!/usr/bin/python

# ZetCode PyGTK tutorial
#
# This program creates an
# waiting effect
#
# author: jan bodnar
# website: zetcode.com
# last edited: February 2009


import gtk
import glib
import math
import cairo


trs = (
( 0.0, 0.15, 0.30, 0.5, 0.65, 0.80, 0.9, 1.0 ),
( 1.0, 0.0, 0.15, 0.30, 0.5, 0.65, 0.8, 0.9 ),
( 0.9, 1.0, 0.0, 0.15, 0.3, 0.5, 0.65, 0.8 ),
( 0.8, 0.9, 1.0, 0.0, 0.15, 0.3, 0.5, 0.65 ),
( 0.65, 0.8, 0.9, 1.0, 0.0, 0.15, 0.3, 0.5 ),
( 0.5, 0.65, 0.8, 0.9, 1.0, 0.0, 0.15, 0.3 ),
( 0.3, 0.5, 0.65, 0.8, 0.9, 1.0, 0.0, 0.15 ),
( 0.15, 0.3, 0.5, 0.65, 0.8, 0.9, 1.0, 0.0, )
)


class PyApp(gtk.Window):

def __init__(self):
super(PyApp, self).__init__()

self.set_title("Waiting")
self.set_size_request(250, 150)
self.set_position(gtk.WIN_POS_CENTER)

self.connect("destroy", gtk.main_quit)

self.darea = gtk.DrawingArea()
self.darea.connect("expose-event", self.expose)
self.add(self.darea)

self.count = 0

glib.timeout_add(100, self.on_timer)

self.show_all()

def on_timer(self):
self.count = self.count + 1
self.darea.queue_draw()
return True


def expose(self, widget, event):

cr = widget.window.cairo_create()

cr.set_line_width(3)
cr.set_line_cap(cairo.LINE_CAP_ROUND)

w = self.allocation.width
h = self.allocation.height

cr.translate(w/2, h/2)

for i in range(8):
cr.set_source_rgba(0, 0, 0, trs[self.count%8][i])
cr.move_to(0.0, -10.0)
cr.line_to(0.0, -40.0)
cr.rotate(math.pi/4)
cr.stroke()


PyApp()
gtk.main()
We draw eight lines with eight different alpha values.
 glib.timeout_add(100, self.on_timer)
We use a timer function to create animation.
trs = (
( 0.0, 0.15, 0.30, 0.5, 0.65, 0.80, 0.9, 1.0 ),
...
)

This is a two dimensional tuple of transparency values used in this demo. There are 8 rows, each for one state. Each of the 8 lines will continuously use these values.
 cr.set_line_width(3)
cr.set_line_cap(cairo.LINE_CAP_ROUND)
We make the lines a bit thicker, so that they are better visible. We draw the lines with rounded caps. They look then better.
 cr.set_source_rgba(0, 0, 0, trs[self.count%8][i]
Here we define the transparency value for a line.
 cr.move_to(0.0, -10.0)
cr.line_to(0.0, -40.0)
cr.rotate(math.pi/4)
cr.stroke()
These code lines will draw each of the eight lines.

Waiting
Figure: Waiting

In this chapter of the PyGTK programming library, we did some more advanced drawing with the Cairo library.
Continue Reading

Drawing with Cairo in PyGTK

Drawing with Cairo in PyGTK

In this part of the PyGTK programming tutorial, we will do some drawing with the Cairo library.
Cairo is a library for creating 2D vector graphics. We can use it to draw our own widgets, charts or various effects or animations.

Simple drawing

The stroke operation draws the outlines of shapes and the fill operation fills the insides of shapes. Next we will demonstrate these two operations.
simpledrawing.py
#!/usr/bin/python

# ZetCode PyGTK tutorial
#
# This code example draws a circle
# using the cairo library
#
# author: jan bodnar
# website: zetcode.com
# last edited: February 2009


import gtk
import math

class PyApp(gtk.Window):

def __init__(self):
super(PyApp, self).__init__()

self.set_title("Simple drawing")
self.resize(230, 150)
self.set_position(gtk.WIN_POS_CENTER)

self.connect("destroy", gtk.main_quit)

darea = gtk.DrawingArea()
darea.connect("expose-event", self.expose)
self.add(darea)

self.show_all()

def expose(self, widget, event):

cr = widget.window.cairo_create()

cr.set_line_width(9)
cr.set_source_rgb(0.7, 0.2, 0.0)

w = self.allocation.width
h = self.allocation.height

cr.translate(w/2, h/2)
cr.arc(0, 0, 50, 0, 2*math.pi)
cr.stroke_preserve()

cr.set_source_rgb(0.3, 0.4, 0.6)
cr.fill()


PyApp()
gtk.main()
In our example, we will draw a circle and will it with a solid color.
 darea = gtk.DrawingArea()
We will be doing our drawing operations on the DrawingArea widget.
 darea.connect("expose-event", self.expose)
We do all drawing in a method, that is a handler for the expose-event signal.
 cr = widget.window.cairo_create()
We create the cairo context object from the gdk.Window of the drawing area. The context is an object that is used to draw on all Drawable objects.
 cr.set_line_width(9)
We set the width of the line to 9 pixels.
 cr.set_source_rgb(0.7, 0.2, 0.0)
We set the color to dark red.
 w = self.allocation.width
h = self.allocation.height

cr.translate(w/2, h/2)
We get the width and height of the drawing area. We move the origin into the middle of the window.
 cr.arc(0, 0, 50, 0, 2*math.pi)
cr.stroke_preserve()
We draw the outside shape of a circle. In red color. The stroke_preserve() strokes the current path according to the current line width, line join, line cap, and dash settings. Unlike the stroke(), it preserves the path within the cairo context.
 cr.set_source_rgb(0.3, 0.4, 0.6)
cr.fill()
This fills the interior of the circle with some blue color.

Simple drawing
Figure: Simple drawing

Basic shapes

The next example draws some basic shapes onto the window.
basicshapes.py
#!/usr/bin/python

# ZetCode PyGTK tutorial
#
# This code example draws basic shapes
# with the cairo library
#
# author: jan bodnar
# website: zetcode.com
# last edited: February 2009

import gtk
import math

class PyApp(gtk.Window):

def __init__(self):
super(PyApp, self).__init__()

self.set_title("Basic shapes")
self.set_size_request(390, 240)
self.set_position(gtk.WIN_POS_CENTER)

self.connect("destroy", gtk.main_quit)

darea = gtk.DrawingArea()
darea.connect("expose-event", self.expose)
self.add(darea)

self.show_all()

def expose(self, widget, event):

cr = widget.window.cairo_create()
cr.set_source_rgb(0.6, 0.6, 0.6)

cr.rectangle(20, 20, 120, 80)
cr.rectangle(180, 20, 80, 80)
cr.fill()

cr.arc(330, 60, 40, 0, 2*math.pi)
cr.fill()

cr.arc(90, 160, 40, math.pi/4, math.pi)
cr.fill()

cr.translate(220, 180)
cr.scale(1, 0.7)
cr.arc(0, 0, 50, 0, 2*math.pi)
cr.fill()


PyApp()
gtk.main()
In this example, we will create a rectangle, a square, a circle, an arc and an ellipse.
 cr.rectangle(20, 20, 120, 80)
cr.rectangle(180, 20, 80, 80)
cr.fill()
These lines draw a rectangle and a square.
 cr.arc(330, 60, 40, 0, 2*math.pi)
cr.fill()
Here the arc() method draws a full circle.
 cr.scale(1, 0.7)
cr.arc(0, 0, 50, 0, 2*math.pi)
cr.fill()
If we want to draw an oval, we do some scaling first. Here the scale() method shrinks the y axis.

Basic shapes
Figure: Basic shapes

Colors

A color is an object representing a combination of Red, Green, and Blue (RGB) intensity values. Cairo valid RGB values are in the range 0 to 1.
colors.py
#!/usr/bin/python

# ZetCode PyGTK tutorial
#
# This program shows how to work
# with colors in cairo
#
# author: jan bodnar
# website: zetcode.com
# last edited: February 2009


import gtk

class PyApp(gtk.Window):

def __init__(self):
super(PyApp, self).__init__()

self.set_title("Colors")
self.resize(360, 100)
self.set_position(gtk.WIN_POS_CENTER)

self.connect("destroy", gtk.main_quit)

darea = gtk.DrawingArea()
darea.connect("expose-event", self.expose)
self.add(darea)

self.show_all()

def expose(self, widget, event):

cr = widget.window.cairo_create()

cr.set_source_rgb(0.2, 0.23, 0.9)
cr.rectangle(10, 15, 90, 60)
cr.fill()

cr.set_source_rgb(0.9, 0.1, 0.1)
cr.rectangle(130, 15, 90, 60)
cr.fill()

cr.set_source_rgb(0.4, 0.9, 0.4)
cr.rectangle(250, 15, 90, 60)
cr.fill()

PyApp()
gtk.main()
We draw three rectangles in three different colors.
 cr.set_source_rgb(0.2, 0.23, 0.9)
The set_source_rgb() method sets a color for the cairo context. The three parameters of the method are the color intensity values.
 cr.rectangle(10, 15, 90, 60)
cr.fill()
We create a rectangle shape and fill it with the previously specified color.

Colors
Figure: Colors

Transparent rectangles

Transparency is the quality of being able to see through a material. The easiest way to understand transparency is to imagine a piece of glass or water. Technically, the rays of light can go through the glass and this way we can see objects behind the glass.
In computer graphics, we can achieve transparency effects using alpha compositing. Alpha compositing is the process of combining an image with a background to create the appearance of partial transparency. The composition process uses an alpha channel. (wikipedia.org, answers.com)
transparentrectangles.py
#!/usr/bin/python

# ZetCode PyGTK tutorial
#
# This program shows transparent
# rectangles using cairo
#
# author: jan bodnar
# website: zetcode.com
# last edited: February 2009


import gtk

class PyApp(gtk.Window):

def __init__(self):
super(PyApp, self).__init__()

self.set_title("Transparent rectangles")
self.resize(590, 90)
self.set_position(gtk.WIN_POS_CENTER)

self.connect("destroy", gtk.main_quit)

darea = gtk.DrawingArea()
darea.connect("expose-event", self.expose)
self.add(darea)

self.show_all()

def expose(self, widget, event):

cr = widget.window.cairo_create()

for i in range(1, 11):
cr.set_source_rgba(0, 0, 1, i*0.1)
cr.rectangle(50*i, 20, 40, 40)
cr.fill()


PyApp()
gtk.main()
In the example we will draw ten rectangles with different levels of transparency.
 cr.set_source_rgba(0, 0, 1, i*0.1)
The last parameter of the set_source_rgba() method is the alpha transparency.

Transparent rectangles
Figure: Transparent rectangles

Soulmate

In the next example, we draw some text on the window.
soulmate.py
#!/usr/bin/python

# ZetCode PyGTK tutorial
#
# This program draws text
# using cairo
#
# author: jan bodnar
# website: zetcode.com
# last edited: February 2009


import gtk
import cairo

class PyApp(gtk.Window):

def __init__(self):
super(PyApp, self).__init__()

self.set_title("Soulmate")
self.set_size_request(370, 240)
self.set_position(gtk.WIN_POS_CENTER)

self.connect("destroy", gtk.main_quit)

darea = gtk.DrawingArea()
darea.connect("expose-event", self.expose)
self.add(darea)

self.show_all()

def expose(self, widget, event):

cr = widget.window.cairo_create()

cr.set_source_rgb(0.1, 0.1, 0.1)

cr.select_font_face("Purisa", cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
cr.set_font_size(13)

cr.move_to(20, 30)
cr.show_text("Most relationships seem so transitory")
cr.move_to(20, 60)
cr.show_text("They're all good but not the permanent one")
cr.move_to(20, 120)
cr.show_text("Who doesn't long for someone to hold")
cr.move_to(20, 150)
cr.show_text("Who knows how to love without being told")
cr.move_to(20, 180)
cr.show_text("Somebody tell me why I'm on my own")
cr.move_to(20, 210)
cr.show_text("If there's a soulmate for everyone")


PyApp()
gtk.main()
We display part of the lyrics from the Natasha Bedingfields Soulmate song.
  cr.select_font_face("Purisa", cairo.FONT_SLANT_NORMAL, 
cairo.FONT_WEIGHT_NORMAL)
Here we specify the font, that we use.
 cr.set_font_size(13)
We specify the size of the font.
 cr.move_to(20, 30)
We move to the point, where we will draw the text.
 cr.show_text("Most relationships seem so transitory")
The show_text() method draws text onto the window.

Soulmate
Figure: Soulmate

In this chapter of the PyGTK programming library, we were drawing with the Cairo graphics library.
Continue Reading