含有章节索引的中文 文章模板

::-- ehu4ever [2005-09-15 01:50:25]

1. Creating GUI Widgets with pyGTK

学习一个GUI框架,首先要了解的是widget模型。widget是GUI的组件,有些widget直接显示在屏幕上,而有些是用来组织和排列其它的widget的。GUI就是由widget前后左右上下搭起来的。下面我们来看一个简单的例子。

Try it out: writing s simple pygtk program

下列的这个程序创建的界面是一个窗口,里面有一个按钮,按钮上有一个文本。

   1 #!/usr/bin/env python
   2 import findgtk
   3 import gtk
   4 
   5 class SingleButtonGUI:
   6         def __init__(self, msg="Hello World"):
   7                 "Set up the window and the button within."
   8                 self.window=gtk.Window()
   9                 self.button=gtk.Button(msg)
  10                 self.window.add(self.button)
  11                 #Show the GUI
  12                 self.button.show()
  13                 self.window.show()
  14 
  15 if __name__ == '__main__':
  16         SingleButtonGUI()
  17         gtk.main()

这就是我们的“Hello world”,很简单吧~ 运行一下。

How it work

首先是为每个widget创建相应的pyGTK对象,接着将子widget与父widget相联,最后就是把所有的widget都显示show()出来。注意一下button和window的显示顺序,试着将它们交替或是去掉一个看看会有什么效果。

当然这个程序没有那么完美,比如不能用右上角的×按钮关闭窗口,而是用Ctrl+C在terminal中将其关闭,或是杀掉Python进程。还有就是这个按钮点它没有反映,这可不好。总之就是这个程序不会处理GUI事件。

1.1. 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.

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.

1.2. 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.

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.

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.

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:

   1 def append(self,command,args):
   2         '''
   3         Append can be called by any thread
   4         '''
   5         #print "about to acquire..."
   6         self.mylock.acquire()
   7         self.myqueue.append((command,args))
   8         #this won’t work on a host with a ZoneAlarm firewall
   9         #or no internet connectivity...
  10         s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  11         #small timeout will wake up the gui thread, but not
  12         #cause painful pauses if we are already in the gui thread.
  13         #important to note that we use timeoutsocket and it
  14         #is already loaded.
  15         s.set_timeout(0.01)
  16         #wakey wakey!
  17         #print "Connecting to port %d"%self.listenport
  18         try:
  19                 s=s.connect(("localhost",self.listenport))
  20         except:
  21                 #ignore timeouts
  22                 pass
  23         #print "About to release"
  24         self.mylock.release()
  25         return
  26 
  27 
  28 def clearqueue(self, socket, x):
  29         """
  30         Clearqueue is only called by the main GUI thread
  31         Don’t forget to return 1
  32         """
  33         #print "Clearing queue"
  34         #clear this...TODO: add select call here.
  35         newconn,addr=self.listensocket.accept()
  36         for i in self.myqueue:
  37                 (command,args)=i
  38                 self.gui.handle_gui_queue(command,args)
  39         self.myqueue=[]
  40         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.

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.

   1 class Queued:
   2         def __init__(self):
   3                 self.gui_queue=gui_queue(self) #our new gui queue
   4                 #for older pyGTK:
   5                 #gtk.input_add(self.gui_queue.listensocket,
   6                 # gtk.gdk.INPUT_READ, self.gui_queue.clearqueue)
   7                 #
   8                 #for newer pyGTK (2.6):
   9                 import gobject
  10                 gobject.io_add_watch(self.gui_queue.listensocket, gobject.IO_IN,
  11                                 self.gui_queue.clearqueue)
  12 
  13         def handle_gui_queue(self, command, args):
  14                 """
  15                 Callback the gui_queue uses whenever it receives a command for us.
  16                 command is a string
  17                 args is a list of arguments for the command
  18                 """
  19                 gtk.threads_enter()
  20                 #print "handle_gui_queue"
  21                 method = getattr(self, command, None)
  22                 if method:
  23                         apply(method, args)
  24                 else:
  25                         print "Did not recognize action to take %s: %s"%(command,args)
  26                 #print "Done handling gui queue"
  27                 gtk.threads_leave()
  28                 return 1
  29 
  30         def gui_queue_append(self,command,args):
  31                 self.gui_queue.append(command,args)
  32                 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:

   1 #!/usr/bin/env python
   2 import time
   3 from threading import Thread
   4 
   5 import findgtk
   6 import gtk
   7 from gui_queue import Queued
   8 
   9 class CountUpGUI(Queued):
  10         """Does counting in a separate thread. To be safe, the other
  11         thread puts calls to threads_enter() and threads_leave() around
  12         all GTK code."""
  13         START = "Click me to start counting up."
  14         STOP = "I've counted to %s (click me to stop)."
  15 
  16         def __init__(self):
  17                 Queued.__init__(self)
  18                 self.window=gtk.Window()
  19                 self.button=gtk.Button(self.START)
  20                 self.button.timesClicked = 0
  21                 self.window.add(self.button)
  22                 self.thread = None
  23 
  24                 #Call the toggleCount method when the button is clicked.
  25                 self.button.connect("clicked", self.toggleCount)
  26 
  27                 #Quit the program when the window is destroyed.
  28                 self.window.connect("destroy", self.destroy)
  29 
  30                 #Show the GUI
  31                 self.button.show()
  32                 self.window.show()
  33 
  34         def destroy(self, window):
  35                 "Remove the window and quit the program."
  36                 window.hide()
  37                 gtk.main_quit()
  38 
  39         def toggleCount(self, button):
  40                 if self.thread and self.thread.doCount:
  41                         #Stop counting.
  42                         self.thread.doCount = False
  43                 else:
  44                         #Start counting.
  45                         self.thread = self.CountingThread(self, self.button)
  46                         self.thread.start()
  47 
  48         def incrementCount(self, button, count):
  49                 button.set_label(self.STOP % count)
  50 
  51         def resetCount(self, button):
  52                 button.set_label(self.START)
  53 
  54 class CountingThread(Thread):
  55         """Increments a counter once per second and updates the button
  56         label accordingly. Updates the button label by putting an
  57         event on the GUI queue, rather than manipulating the GUI
  58         directly."""
  59 
  60         def __init__(self, gui, button):
  61                 self.gui = gui
  62                 Thread.__init__(self)
  63                 self.button = button
  64                 self.doCount = False
  65                 self.count = 0
  66                 self.setDaemon(True)
  67 
  68         def run(self):
  69                 self.doCount = True
  70                 while self.doCount:
  71                         self.gui.gui_queue_append("incrementCount",
  72                         [self.button, self.count])
  73                         self.count += 1
  74                         time.sleep(1)
  75                 self.gui.gui_queue_append("resetCount", [self.button])
  76                 self.count = 0
  77 
  78 if __name__ == '__main__':
  79 CountUpGUI()
  80 try:
  81         gtk.threads_init()
  82 except:
  83         print "No threading was enabled when you compiled pyGTK!"
  84         import sys
  85         sys.exit(1)
  86 gtk.threads_enter()
  87 gtk.main()
  88 gtk.threads_leave()

How It Works

When you click the button the first time, it initializes a CountingThread object. This thread object spends most of its time sleeping, but it wakes up every second to update the label with a new number. If it were updating the label directly, then to avoid freezing the program, it would have to know to call gtk.threads_enter before calling incrementCount, and to call gtk.threads_leave afterward.

Instead, it puts an incrementCount command onto the gui_queue object. The main thread (which called gtk.threads_enter before entering the main body of the code) retrieves this command from the queue and executes it. The other thread can change the GUI without having to know any of the details of pyGTK or its thread handling.

1.3. Widget Packing

So far, all of our examples have explored GUI concepts with a GUI consisting only of a single button. Needless to say, most real GUI applications are more complicated. You might be tempted to create a GUI with multiple widgets by simply creating the widgets and attaching them to the window:

   1                 #This is bad code! Don’t actually try it!
   2                 button1=gtk.Button("Button 1")
   3                 window.add(button1)
   4                 button2=gtk.Button("Button 2")
   5                 window.add(button2)

If you try this code, you’ll notice that only the first button shows up. This is because a window can only contain one widget. Once you associate the first button with the window, you can’t associate anything else with it. How, then, are complex GUIs possible? The answer is a technique called widget packing.

Widget packing makes use of boxes and tables, virtual widgets that don’t necessarily show up on the screen the way a button does. A window can only have one child widget, but if that widget happens to be a box or table, it can contain a number of child widgets, and display them all, either beside or on top of each other. As you’ll see, you can put boxes inside boxes to get the exact layout you need.

Here’s TwoButtonsGUI.py, a script that is like our original “Hello World” application SingleButtonGUI.py (in which clicking the button does nothing), but this time there are two buttons instead of one:

   1 #!/usr/bin/env python
   2 import findgtk
   3 import gtk
   4 
   5 class TwoButtonsGUI:
   6         def __init__(self, msg1="Hello World" , msg2="Hello Again"):
   7                 #Set up the window and the button within.
   8                 self.window=gtk.Window()
   9                 self.box = gtk.VBox()
  10                 self.window.add(self.box)

The window widget only has space for one child widget: If we put one of our buttons directly in the window, there wouldn’t be anywhere to put the other one. Instead, we put a box widget in the window. This widget doesn’t show anything onscreen, but it can contain more than one child widget. We use a VBox, which means that widgets will be packed vertically into the box, one on top of the other. The alternative is an HBox, which packs widgets next to each other horizontally:

   1                 self.button1 = gtk.Button(msg1)
   2                 self.button2 = gtk.Button(msg2)
   3                 self.box.pack_start(self.button1)
   4                 self.box.pack_start(self.button2)

We create our two buttons and put each one in the box. Next we must show all four widgets: the two buttons, the box, and the window. Remember that if you don’t show a widget, neither it nor any of its children appear on the screen:

   1                 #Show the GUI
   2                 self.button1.show()
   3                 self.button2.show()
   4                 self.box.show()
   5                 self.window.show()
   6 if __name__ == '__main__':
   7         TwoButtonsGUI()
   8         gtk.main()

As you can see, adding just one more GUI widget greatly increased the amount of design and programming work we had to do. For complex layouts, it gets even worse. Writing GUIs by hand in this day and age is insane. Fortunately, we don’t have to: The best way to lay out a GUI is graphically. This is as far as we’re going to go with hand-coded GUIs. From this point on, we’ll use a GUI builder tool called Glade.

1.4. Glade: a GUI Builder for pyGTK

The great thing about pyGTK is that you almost never have to write the GUI by hand. That’s what Glade is for. Glade is a GUI construction kit: It provides a GUI you can use to design your own GUI. Once you’re done, it writes a description of a GUI layout to an XML file (see Chapter 15 for more information about XML). A library called libglade can then read that XML file and render the corresponding GUI. Instead of instantiating a bunch of Python objects and calling show on all of them, you just feed a file describing your GUI into libglade.

To give you some idea of the sorts of complex applications you can build with Glade, Figure 13-5 shows a screenshot of CANVAS 5.4 running on Windows XP. It runs identically on Linux—and, of course, integrates with the standard GTK themes.

Here you can see many of the widgets you’ll shortly know how to use: a notebook container for the log and debug information panes, several list and tree views, a horizontal scale for the covertness bar, text entries, a menu bar, and a button with an icon in it.

Given a little practice, you too could lay out this complex application in Glade in just a few moments. Of course, the real payoff is when you want to move parts of your application around. Rather than regenerate code, you simply change the pieces you want changed in Glade, and click Save. No code changes need to be made at all.

canvas1.jpg

1.5. GUI Builders for Other GUI Frameworks

Writing GUI code is painful—no matter which toolkit you use, you’re going to want to use some sort of generator for as much of it as possible. All the non-TK toolkits include a construction kit. If you’re using wxPython to run your GUI, you should consider using Boa Constructor, wxDesigner, or wxGlade to build it. QT has the KDE builder tools to work with, and of course pyGTK has Glade.

PyGTK2_widgets (last edited 2009-12-25 07:16:55 by localhost)

Loading