Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning Python (2005)

.pdf
Скачиваний:
177
Добавлен:
17.08.2013
Размер:
15.78 Mб
Скачать

Writing a GUI with Python

The author’s experience with GUI toolkits, and with pyGTK specifically, stems from developing Immunity CANVAS, a cross-platform commercial product written completely in Python. Note that the same techniques described here are the basis for the new large projects being written by the Ximian team (now part of Novell) as they build the next-generation SuSe desktop application suite.

Of course, not all pyGTK applications have to be complex. Your application may be a simple dialog box that you’ve written to automate a business process you often do. The same things that made large applications like CANVAS, Dashboard, and PythonCAD quick and easy to write make simple applications nearly trivial.

pyGTK Resources

You’ll first need to make sure you have pyGTK installed. If you did a complete install on a modern Linux distribution, you’ll have pyGTK 2.0 installed already. If you’re running Windows, you can install the latest pyGTK with two clicks.

The latest Win32 installations of pyGTK are available at www.pcpm.ucl.ac.be/~gustin/win32_ ports/pygtk.html.

If you don’t have pyGTK installed on your Linux system, you’ll likely find that the platform-specific packaging commands will quickly produce them for you. For gentoo, use “emerge pyGTK”. On debian or Red Hat installations with apt, invoking “apt-get pygtk-devel” will remedy the situation. Even if you do have it installed, it doesn’t hurt to make sure that it’s the latest pyGTK package your distribution offers. See Appendix B and the web site for more information on installing pyGTK.

After you have pyGTK installed, you can make sure it works by importing the pygtk module:

>>>import pygtk

>>>pygtk.require(“2.0”)

>>>import gtk

A more reliable method of importing pyGTK follows. This code is more complex but also more portable across the different versions of pyGTK that exist in the wild. Put it into a file called findgtk.py and you can just import findgtk to ensure that Python loads the right version of pyGTK, and that import gtk will work anytime afterwards. findgtk.py is used by all the examples in this chapter.

#!/usr/bin/env python “””

findgtk.py - Find the pyGTK libraries, wherever they are. “””

import os import sys

sys.path.append(“/usr/local/lib/python2.3/site-packages/”)

def try_import(): import sys

“””tries to import gtk and if successful, returns 1””” #print “Attempting to load gtk...Path=%s”%sys.path

# To require 2.0

211

TEAM LinG

Chapter 13

try:

import pygtk pygtk.require(“2.0”)

except:

print “pyGTK not found. You need GTK 2 to run this.”

print “Did you \”export PYTHONPATH=/usr/local/lib/python2.2/sitepackages/\” first?”

print “Perhaps you have GTK2 but not pyGTK, so I will continue to try loading.”

try:

import gtk,gtk.glade

import atk,pango #for py2exe import gobject

except:

import traceback,sys traceback.print_exc(file=sys.stdout)

print “I’m sorry, you apparently do not have GTK2 installed - I tried” print “to import gtk, gtk.glade, and gobject, and I failed.”

return 0 return 1

if not try_import(): site_packages=0 #for k in sys.path:

#if k.count(“site-packages”):

#print “existing site-packages path %s found\n”%k

#site_packages=1

if site_packages == 0: from stat import *

#print “no site-packages path set, checking.\n” check_lib = [ “/usr/lib/python2.2/site-packages/”,

“/usr/local/lib/python2.2/site-packages/”, “/usr/local/lib/python2.3/site-packages/” ]

for k in check_lib: try:

path=os.path.join(k,”pygtk.py”) #print “Path=%s”%path

if open(path)!=None: #print “appending”, k sys.path=[k]+sys.path if try_import():

break

except: pass

if not try_import(): sys.exit(0)

212

TEAM LinG

Writing a GUI with Python

pyGTK Resources

The pyGTK FAQ is really more of a Wiki. This has everything you need to know and is actively maintained. Often, when people post questions to the pyGTK mailing list, the maintainers simply reply with a FAQ number and URL:

www.async.com.br/faq/pygtk/index.py?req=index

The pyGTK mailing list is actively used. You’ll find the authors of both pyGTK and this chapter on pyGTK on this list, actively helping newcomers:

www.daa.com.au/mailman/listinfo/pygtk

This list of tutorials can be handy for beginners. Some are unfinished, but they all present useful information:

www.pygtk.org/articles.html

Creating GUI Widgets with pyGTK

The first thing to understand is that most GUI frameworks, including pyGTK, are based on a widget model. A widget is a component of a GUI — buttons, labels, and text boxes are all widgets. Most widgets have graphical representations on screen, but some widgets, such as tables and boxes, exist only to contain other widgets and arrange them on the screen. A GUI is constructed out of an arrangement of widgets. In the following section, you’ll create a simple GUI by defining some widgets and placing them inside each other.

Try It Out

Writing a Simple pyGTK Program

With pyGTK in place, you’re ready to write a real GUI application. This script, SingleButtonGUI, creates a GUI of two widgets: a window, which contains a button. The label of the button displays a message:

#!/usr/bin/env python import findgtk import gtk

class SingleButtonGUI:

def __init__(self, msg=”Hello World”):

“Set up the window and the button within.” self.window=gtk.Window() self.button=gtk.Button(msg)

self.window.add(self.button)

#Show the GUI self.button.show() self.window.show()

if __name__ == ‘__main__’: SingleButtonGUI() gtk.main()

Run this program and you’ll see the Hello World button in the window, as shown in Figure 13-1.

213

TEAM LinG

Chapter 13

Figure 13-1

If you’re running Windows, you can use Cygwin’s bash to execute this script, but don’t use Cygwin’s Python; it doesn’t come linked with pyGTK. Try this instead:

$ /cygdrive/c/Python24/python.exe SingleButtonGUI.py

How It Works

The first thing to do is to create pyGTK objects for each widget. Then, the child widget (the button) is associated with its parent (the window). Finally, both widgets are displayed. It’s important to call the show method on every widget in your GUI. In this example, if you call show on the button but not the window, the window will show up but nothing will be inside it. If you call show on the window but not the button, nothing will show up on the screen at all.

One problem with this script is that you can’t kill this window by clicking the Close window in the GUI. You’ll need to press Ctrl+C (that is, the control key and the c key, together) in the script terminal to close it, or otherwise kill the Python process. Another problem with this script is that unlike most buttons in GUI applications, the button here doesn’t actually do anything when you click it. Both problems share a cause: The script as it is doesn’t handle any GUI events.

GUI Signals

GUI programs aren’t just about putting widgets up on the screen. You also need to be able to respond to the user’s actions. GUIs generally handle this with the notion of events, or (in pyGTK terminology) signals.

Each GUI widget can generate a number of different signals in response to user actions: For instance, a button may be clicked, or a window destroyed. In pyGTK, these would correspond to signals named clicked and destroy. The other half of GUI programming is setting up handlers for GUI signals: pieces of code that are triggered each time a corresponding signal is sent by the framework.

If no piece of code is listening for a signal, nothing happens when the user triggers the signal. That’s why in the previous example you couldn’t close the window through the GUI, and why nothing happened when you clicked the button. Signals could have been spawned, but they wouldn’t have gone anywhere.

In pyGTK, you register a function with a signal handler by calling the connect method on the widget whose signals you want to capture. Pass in the name of the signal you want to receive and the function you want to be called every time that widget emits that signal.

The following script, ClickCountGUI.py, presents a similar interface to the previous example. The difference is that this GUI application responds to some signals. You can see ClickCountGUI.py working in Figure 13-2.

214

TEAM LinG

Writing a GUI with Python

#!/usr/bin/env python import findgtk import gtk

class ClickCountGUI:

“When you click, it increments the label.”

CLICK_COUNT = ‘Click count: %d’

def __init__(self):

“Set up the window and the button within.” self.window=gtk.Window() self.button=gtk.Button(self.CLICK_COUNT % 0) self.button.timesClicked = 0 self.window.add(self.button)

#Call the buttonClicked method when the button is clicked. self.button.connect(“clicked”, self.buttonClicked)

#Quit the program when the window is destroyed. self.window.connect(“destroy”, self.destroy)

#Show the GUI self.button.show() self.window.show()

def buttonClicked(self, button):

“This button was clicked; increment the message on its label.” button.timesClicked += 1

button.set_label(self.CLICK_COUNT % button.timesClicked)

def destroy(self, window):

“Remove the window and quit the program.” window.hide()

gtk.main_quit()

if __name__ == ‘__main__’: ClickCountGUI() gtk.main()

Figure 13-2

This GUI responds to the destroy signal of the window object, which means you can close the window through the GUI. It also responds to the clicked signal of the button object, so the button can change to display the number of times you’ve clicked it.

215

TEAM LinG

Chapter 13

GUI Helper Threads and the GUI Event Queue

One common problem GUIs must deal with is handling long-running events, such as data reads from the network. It doesn’t take much time to change the label on a button, so our click-counting program is safe. However, what if clicking a button started a process that took a minute to finish? A script like the one shown in the previous example would freeze the GUI until the process finished. There would be no processor time allocated to sending out the GUI signals triggered by the user. To the end user, it would look like your application had frozen.

Even worse, what if clicking a button started a process that would stop only in response to another GUI action? For example, consider a stopwatch-like application in which clicking a button starts a counter, and clicking the button again stops it. It wouldn’t do to write code that started counting after receiving the first signal and stopped counting after receiving a second signal. Once you clicked the button, you’d never be able to click it again; the program would be busy doing the count, not listening for signals. Any GUI program that performs a potentially long-running task needs to delegate that task to a separate thread for the duration. A GUI is always doing two things: It’s doing whatever job is specified for that particular program, and it’s constantly gathering signals from the user.

With pyGTK, you can run code in other threads without disrupting the GUI, so long as each thread calls the gtk module’s threads_enter function before calling any pyGTK code, and calls threads_leave afterwards. Make one mistake, though, and your application will truly freeze. That’s why it’s better to keep all the pyGTK code in the main thread, and have other threads request changes to the GUI by putting them into a GUI event queue.

Note that pyGTK under Linux is pretty forgiving of threading mistakes. Nonetheless, having to debug a random freeze in your application that happens only after running it for several hours can make for a frustrating week. Getting threading right is difficult in any GUI framework, and the concepts listed below are applicable to C programming as well as Python programming.

Let’s start with some basics. The problem of cross-platform threading under pyGTK is complicated by some architectural difficulties on Windows. But if you keep to the strict design decisions outlined below, you’ll have no problems on any platform. A bonus payoff is that your program will become more organized in general, and you won’t have to learn all the intricacies of managing threads yourself.

1.Your *GUI.py is the only Python file allowed to call GTK functions.

2.Only one thread is allowed to run in *GUI.py.

3.The thread in *GUI.py will read from and clear a GUI queue object; other threads will add actions to the queue object.

4.For any operation that might take a long time to complete, your GUI will start another worker thread. This especially includes network calls.

The term *GUI.py means that once you’ve decided on a name for your program, you’ll create nameGUI.py so that you know it will be the file that follows these rules.

216

TEAM LinG

Writing a GUI with Python

This simple design will prevent you from eons of nearly impossible debugging problems as your project gets more complicated. The following library module (placed in gui_queue.py) will accomplish this for you. There are several ways to do this sort of queue, but this is the only way that I can absolutely guarantee works:

This module requires the timeoutsocket module: www.steffensiebert.de/soft/python/ timeoutsocket.py. See Appendix B for details.

#!/usr/bin/env python “””

gui_queue.py

This Python modules does what we need to do to avoid threading issues on both Linux and Windows.

Your other modules can include this file and use it without knowing anything about gtk.

“””

#Python License for Beginner’s Python book

import findgtk import gtk import random import socket import time

from threading import RLock

import timeoutsocket #used for set_timeout()

class gui_queue:

“””wakes up the gui thread which then clears our queue””” def __init__(self,gui,listenport=0):

“””If listenport is 0, we create a random port to listen on””” self.mylock=RLock()

self.myqueue=[] if listenport==0:

self.listenport=random.randint(1025,10000)

else:

self.listenport=listenport

print “Local GUI Queue listening on port %s”%self.listenport s=socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((“”, self.listenport))

self.listensocket=s

self.listensocket.listen(300) #listen for activity. #time.sleep(15)

self.gui=gui return

217

TEAM LinG

Chapter 13

Above, we use initialize the self.mylock with the Rlock function which we will use to create a “mutex” to ensure that certain parts of the code are only run by one thread at a time. (This is what mutex means: mutually exclusive. If one thread is holding on to the mutex, that excludes the other threads from doing the same action). The code listens for GUI events on a network socket (see Chapter 16 for more information on sockets and ports). If no listening port is specified, this code will choose a random high port on which to listen. Other threads will add an item to the GUI queue by connecting to that socket over the operating system’s local network interface:

def append(self,command,args): “””

Append can be called by any thread “””

#print “about to acquire...” self.mylock.acquire() self.myqueue.append((command,args))

#this won’t work on a host with a ZoneAlarm firewall #or no internet connectivity...

s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)

#small timeout will wake up the gui thread, but not #cause painful pauses if we are already in the gui thread. #important to note that we use timeoutsocket and it

#is already loaded. s.set_timeout(0.01) #wakey wakey!

#print “Connecting to port %d”%self.listenport try:

s=s.connect((“localhost”,self.listenport))

except:

#ignore timeouts pass

#print “About to release” self.mylock.release() return

def clearqueue(self, socket, x): “””

Clearqueue is only called by the main GUI thread Don’t forget to return 1

“””

#print “Clearing queue”

#clear this...TODO: add select call here. newconn,addr=self.listensocket.accept() for i in self.myqueue:

(command,args)=i self.gui.handle_gui_queue(command,args)

self.myqueue=[] return 1

The preceding code’s clearqueue function will be called periodically by the main GUI thread, which will then get each of the gui_queue’s new commands sent to the GUI’s handle_gui_queue function in turn.

218

TEAM LinG

Writing a GUI with Python

Your GUI application will need to set up a GUI queue, and have its signal hook methods append items to the GUI queue instead of handling the signals directly. Here’s a class you can subclass that sets up a GUI queue and provides a method for appending to it, and handling what comes out of it. Note that the code to connect the queue to the network differs between versions of pyGTK.

class Queued:

def __init__(self):

self.gui_queue=gui_queue(self) #our new gui queue #for older pyGTK: #gtk.input_add(self.gui_queue.listensocket,

#

gtk.gdk.INPUT_READ, self.gui_queue.clearqueue)

#

 

#for newer pyGTK (2.6): import gobject

gobject.io_add_watch(self.gui_queue.listensocket, gobject.IO_IN, self.gui_queue.clearqueue)

def handle_gui_queue(self, command, args): “””

Callback the gui_queue uses whenever it receives a command for us. command is a string

args is a list of arguments for the command “””

gtk.threads_enter() #print “handle_gui_queue”

method = getattr(self, command, None) if method:

apply(method, args) else:

print “Did not recognize action to take %s: %s”%(command,args) #print “Done handling gui queue”

gtk.threads_leave() return 1

def gui_queue_append(self,command,args): self.gui_queue.append(command,args) return 1

Try It Out

Writing a Multithreaded pyGTK App

Here’s an application, CountUpGUI.py, that implements the stopwatch idea mentioned earlier. It uses a separate thread to count off the seconds, a thread that modifies the GUI by putting items on the gui_queue for the main thread to process:

#!/usr/bin/env python import time

from threading import Thread

import findgtk import gtk

from gui_queue import Queued

219

TEAM LinG

Chapter 13

class CountUpGUI(Queued):

“””Does counting in a separate thread. To be safe, the other thread puts calls to threads_enter() and threads_leave() around all GTK code.”””

START = “Click me to start counting up.”

STOP = “I’ve counted to %s (click me to stop).”

def __init__(self): Queued.__init__(self) self.window=gtk.Window() self.button=gtk.Button(self.START) self.button.timesClicked = 0 self.window.add(self.button) self.thread = None

#Call the toggleCount method when the button is clicked. self.button.connect(“clicked”, self.toggleCount)

#Quit the program when the window is destroyed. self.window.connect(“destroy”, self.destroy)

#Show the GUI self.button.show() self.window.show()

def destroy(self, window):

“Remove the window and quit the program.” window.hide()

gtk.main_quit()

def toggleCount(self, button):

if self.thread and self.thread.doCount: #Stop counting. self.thread.doCount = False

else:

#Start counting.

self.thread = self.CountingThread(self, self.button) self.thread.start()

def incrementCount(self, button, count): button.set_label(self.STOP % count)

def resetCount(self, button): button.set_label(self.START)

class CountingThread(Thread):

“””Increments a counter once per second and updates the button label accordingly. Updates the button label by putting an event on the GUI queue, rather than manipulating the GUI directly.”””

def __init__(self, gui, button): self.gui = gui

220

TEAM LinG