Wednesday, July 21, 2010

Python (GUI) Programming Tutorial using WxPython w/ Videos

wxPython
One of the GUI toolkits available for Python is called wxPython. The current incarnation is fairly new to the Python scene and is rapidly gaining popularity amongst Python developers. wxPython is a Python extension module that encapsulates the wxWidgets C++ class library.


wxWidgets

wxWidgets is a free C++ framework designed to make cross-platform programming child's play. Well, almost. wxWidgets 2.0 supports Windows 3.1/95/98/NT, Unix with GTK/Motif/Lesstif, with a Mac version underway. Other ports are under consideration.
wxWidgets is a set of libraries that allows C++ applications to compile and run on several different types of computers, with minimal source code changes. There is one library per supported GUI (such as Motif, or Windows). As well as providing a common API (Application Programming Interface) for GUI functionality, it provides functionality for accessing some commonly used operating system facilities, such as copying or deleting files. wxWidgets is a framework in the sense that it provides a lot of built-in functionality, which the application can use or replace as required, thus saving a great deal of coding effort. Basic data structures such as strings, linked lists and hash tables are also supported.
Native versions of controls, common dialogs, and other window types are used on platforms that support them. For other platforms suitable alternatives are created using wxWidgets itself. For example, on Win32 platforms the native list control is used, but on GTK a generic list control with similar capabilities was created for use in the wxWidgets class library.
Experienced Windows programmers will feel right at home with the wxWidgets object model. Many of the classes and concepts will be very familiar. For example, the Multiple Document Interface, drawing on Device Contexts with GDI objects such as brushes, pens, etc., and so on.


Putting the two together

wxPython is a Python extension module that provides a set of bindings from the wxWidgets library to the Python language. In other words, the extension module allows Python programers to create instances of wxWidgets classes and to invoke methods of those classes.
The wxPython extension module attempts to mirror the class hierarchy of wxWidgets as closely as possible. This means that there is a wxFrame class in wxPython that looks, smells, tastes and acts almost the same as the wxFrame class in the C++ version. Unfortunately, not every class or method matches exactly because of differences in the languages, but the differences should be easy to absorb because they are natural to Python. For example, some methods that return multiple values via argument pointers in C++ will instead return a tuple of values in Python. wxPython is close enough to the C++ version that the majority of the wxPython documentation is actually just notes attached to the C++ documents that describe the places where wxPython is different. There is also a series of sample programs included, and a series of documentation pages that assist the programmer in getting started with wxPython.


Where to get wxPython

The latest version of wxPython can always be found at http://alldunn.com/wxPython/. From this site you can download a self-installer for Win32 systems that includes a pre-built extension module, documentation in html-help format and a set of demos.
Also available from this site is a Linux RPM, wxPython sources, documentation in raw HTML, and pointers to other sites, mail lists, etc.
If you will be building wxPython from sources yourself, you will also need the wxWidgets sources, available from http://www.wxwidgets.org/


Using wxPython
I've always found that best way to learn is by doing and then experimenting and tweaking with what's been done. So download and install wxPython, fire up your favorite text editor and get ready to play along as you read the next few sections.


A Simple Example

Familiarize yourself with this little wxPython program, and refer back to it as you read through the explanations that follow.
    from wxPython.wx import *
    
    class MyApp(wxApp):
        def OnInit(self):
            frame = wxFrame(NULL, -1, "Hello from wxPython")
            frame.Show(true)
            self.SetTopWindow(frame)
            return true
    
    app = MyApp(0)
    app.MainLoop()
    

The first thing to do in any program is import the classes and other items necessary for the application. The first line above imports all of wxPython into the current namespace. You can do more restrictive imports if you like, but the typical wxPython program will just import everything.
Every wxPython application needs to derive a class from wxApp and provide an OnInit method for it. The system calls this method as part of its startup/initialization sequence. The primary purpose of OnInit is to create the windows, etc. necessary for the program to begin operation. In our sample we create a frame with no parent, with a title of "Hello from wxPython" and then show it. We could have also specified a position and size for the frame in its constructor, but since we didn't defaults will be used. The last two lines of the OnInit method will probably be the same for all applications. The SetTopWindow method simply informs wxWidgets that this frame is one of the main frames (in this case the only one) for the application. When all top-level windows have been closed then the application terminates. Returning true from OnInit implies that OnInit was successful, if false had been returned then the application would exit.
The final two lines of the script again will probably be the same for all your wxPython applications. We simply create an instance of our application class, and then call its MainLoop method. MainLoop is the heart of the application and is where events are processed and dispatched to the various windows in the application. Fortunately wxWidgets insulates us from the differences in event processing in the various GUI toolkits.
Most of the time you will want to customize the main frame of the application, and so using the stock wxFrame will not be sufficient. The basic principles of Object Oriented Programming also suggest that the attributes and behaviors of the objects in your application should be encapsulated in a class rather than hodge-podged together in a place like OnInit. The next example derives a custom frame from wxFrame and creates an instance of it in OnInit. Notice that except for the name of the class created in OnInit, that the rest of the MyApp code is identical to the previous example.

    from wxPython.wx import *
    
    ID_ABOUT = 101
    ID_EXIT  = 102
    
    class MyFrame(wxFrame):
        def __init__(self, parent, ID, title):
            wxFrame.__init__(self, parent, ID, title,
                             wxDefaultPosition, wxSize(200, 150))
            self.CreateStatusBar()
            self.SetStatusText("This is the statusbar")
    
            menu = wxMenu()
            menu.Append(ID_ABOUT, "&About",
                        "More information about this program")
            menu.AppendSeparator()
            menu.Append(ID_EXIT, "E&xit", "Terminate the program")
    
            menuBar = wxMenuBar()
            menuBar.Append(menu, "&File");
    
            self.SetMenuBar(menuBar)
    
    
    class MyApp(wxApp):
        def OnInit(self):
            frame = MyFrame(NULL, -1, "Hello from wxPython")
            frame.Show(true)
            self.SetTopWindow(frame)
            return true
    
    app = MyApp(0)
    app.MainLoop()
    
    

This example shows off some of the built-in capabilities of the wxFrame class. For example, creating a status bar for the frame is as simple as calling a single method. The frame itself will automatically manage its placement, size and drawing. On the other hand, if you want to customize the status bar you can, simply by creating an instance of your own wxStatusBar derived class and attaching it to the frame.
Creating a simple menu bar and a drop-down menu is also demonstrated in this example. The full range of expected menu capabilities is supported, cascading submenus, checkable items, popup menus, etc. All you have to do is create a menu object and append menu items to it. The items can be text as shown here, or can be other menus. With each item you can optionally specify some short help text, as we have done, which will automatically be shown in the status bar when the menu item is selected.

Events in wxPython

The one thing that the last sample doesn't do is show you how to make the menus actually do something. If you run the sample and select "Exit" from the menu, nothing happens. Our next sample will take care of that little problem, but first a bit about events.Most, if not all, GUI systems and toolkits are designed to be event driven. This means that programs written using these GUI toolkits are basically a collection of event handlers. The event handlers determine the functionality of the program. They can open or update windows, access a database, whatever...
In non object-oriented toolkits, the event handlers are usually standalone functions that are attached to the event by calling a toolkit function. Object Oriented Programmers dislike these standalone event handler functions because they prevent complete encapsulation of the window's functionality into a single class.
To overcome this limitation many object oriented GUI toolkits, such as Java's AWT, use class methods for the event handlers. Basically events of a certain type are always sent to methods of a certain name. This fixes the encapsulation problem, but means that in order to catch an event you have to derive a new class and implement that method. Very often that is not an ideal situation, and typically causes a lot of clutter in the application as more and more classes are created just to handle certain types of events in different ways.
wxPython utilizes the best of both paradigms. Any method (or standalone function for that matter) can be attached to any event by using a helper function from the toolkit. wxPython also provides a wxEvent class and a whole bunch of derived classes for containing the details of the event. Each time a method is invoked due to an event, an object derived from wxEvent is sent as a parameter, the actual type of the event object depends on the type of event. wxSizeEvent for when the window changes size, wxCommandEvent for menu selections and button clicks, wxMouseEvent for, um…, mouse events, etc.
To solve our little problem with the last sample, all we have to do is add two lines to the MyFrame constructor, and add some methods to handle the events. We'll also demonstrate one of the Common Dialogs, the wxMessageDialog. Here's the code, with the new parts in bold.

    from wxPython.wx import *
    
    ID_ABOUT = 101
    ID_EXIT  = 102
    
    class MyFrame(wxFrame):
        def __init__(self, parent, ID, title):
            wxFrame.__init__(self, parent, ID, title,
                             wxDefaultPosition, wxSize(200, 150))
    
            self.CreateStatusBar()
            self.SetStatusText("This is the statusbar")
            menu = wxMenu()
            menu.Append(ID_ABOUT, "&About",
                        "More information about this program")
            menu.AppendSeparator()
            menu.Append(ID_EXIT, "E&xit", "Terminate the program")
            menuBar = wxMenuBar()
            menuBar.Append(menu, "&File");
            self.SetMenuBar(menuBar)
    
            EVT_MENU(self, ID_ABOUT, self.OnAbout)
            EVT_MENU(self, ID_EXIT,  self.TimeToQuit)
    
        def OnAbout(self, event):
            dlg = wxMessageDialog(self, "This sample program shows off\n"
                                  "frames, menus, statusbars, and this\n"
                                  "message dialog.",
                                  "About Me", wxOK | wxICON_INFORMATION)
            dlg.ShowModal()
            dlg.Destroy()
    
    
        def TimeToQuit(self, event):
            self.Close(true)
    
    
    
    class MyApp(wxApp):
        def OnInit(self):
            frame = MyFrame(NULL, -1, "Hello from wxPython")
            frame.Show(true)
            self.SetTopWindow(frame)
            return true
    
    app = MyApp(0)
    app.MainLoop()
    
    

The EVT_MENU function called above is one of the helper functions for attaching events to methods. Sometimes it helps to understand what is happening if you translate the function call to English. The first one is saying, "For any menu item selection event sent to the window self with an ID of ID_ABOUT, invoke the method self.OnAbout."
There are many of these EVT_* helper functions, all of which correspond to a specific type of event, or events. Some of the more popular ones are listed here:

EVT_SIZESent to a window when its size has changed, either interactively by the user or programmatically.
EVT_MOVESent to a window when it has been moved, either interactively by the user or programmatically.
EVT_CLOSESent to a frame when it has been requested to close. Unless the close is being forced, it can be canceled by calling event.Veto(true)
EVT_PAINTThis event is sent whenever a portion of the window needs to be redrawn.
EVT_CHARSent for each non-modifier (shift key, etc.) keystroke when the window has the focus.
EVT_IDLEThis event is sent periodically when the system is not processing other events.
EVT_LEFT_DOWNThe left mouse button has been pressed down.
EVT_LEFT_UPThe left mouse button has been let up.
EVT_LEFT_DCLICKThe left mouse button has been double-clicked.
EVT_MOTIONThe mouse is in motion.
EVT_SCROLLA scrollbar has been manipulated. This one is actually a collection of events, which can be captured individually if desired.
EVT_BUTTONA button has been clicked.
EVT_MENUA menu tem has been selected.
The list goes on an on... See the wxPython documentation for details.

Window Layout
Before going any further I should mention something about the various methods of managing the layout of windows and sub-windows in wxPython. There are several alternative mechanisms provided, and potentially several ways to accomplish the same thing. This allows the programmer to work with whichever mechanism works best in a particular situation, or whichever they are most comfortable with.
  1. Constraints: There is a class called wxLayoutConstraints that allows the specification of a window's position and size in relationship to its siblings and its parent. Each wxLayoutContraints object is composed of eight wxIndividualLayoutConstraint objects, which define different sorts of relationships such as which window is above this window, what is the relative width of this window, etc. You usually have to specify four of the eight individual constraints in order for the window to be fully constrained. For example this button will be positioned in the center of its parent, and will always be 50 percent of the parent's width.





    b = wxButton(self.panelA, 100, ' Panel A ')
            lc = wxLayoutConstraints()
            lc.centreX.SameAs   (self.panelA, wxCentreX)
            lc.centreY.SameAs   (self.panelA, wxCentreY)
            lc.height.AsIs      ()
            lc.width.PercentOf  (self.panelA, wxWidth, 50)
            b.SetConstraints(lc)
    
  2. Layout Algorithm: The class named wxLayoutAlgorithm implements layout of sub-windows in MDI or SDI frames. It sends a wxCalculateLayoutEvent to children of the frame, asking them for information about their size. Because the event system is used this technique can be applied to any window, even those which are not necessarily aware of the layout classes. However, you may wish to use wxSashLayoutWindow for your sub-windows since this class provides handlers for the required events, and accessors to specify the desired size of the window. The sash behavior in the base class can be used, optionally, to make the windows user-resizable. wxLayoutAlgorithm is typically used in IDE (integrated development environment) style of applications, where there are several resizable windows in addition to the MDI client window, or other primary editing window. Resizable windows might include toolbars, a project window, and a window for displaying error and warning messages.
  3. Sizers: In an effort to simplify the programming of simple layouts, a family of wxSizer classes have been added to the wxPython library. These are classes that are implemented in pure Python instead of wrapping C++ code from wxWidgets. They are somewhat reminiscent of the Layout Managers from Java in that you select the type of sizer you want and then add windows or other sizers to it and they all follow the same rules for layout. For example this code fragment creates five buttons that are laid out horizontally in a box, and the last button is allowed to stretch to fill the remaining space allocated to the box:





    box = wxBoxSizer(wxHORIZONTAL)
        box.Add(wxButton(win, 1010, "one"), 0)
        box.Add(wxButton(win, 1010, "two"), 0)
        box.Add(wxButton(win, 1010, "three"), 0)
        box.Add(wxButton(win, 1010, "four"), 0)
        box.Add(wxButton(win, 1010, "five"), 1)
    
  4. Resources: The wxWidgets library has a simple Dialog Editor available which can assist with the layout of controls on a dialog and will generate a portable cross platform resource file. This file can be loaded into a program at runtime and transformed on the fly into a window with the specified controls on it. The only downfall with this approach is that you don't have the opportunity to sub-class the windows that are generated, but if you can do everything you need with existing control types and event handlers then it should work out great. Eventually there will probably be a wxPython specific application builder tool that will generate either a resource type of file, or actual Python source code for you.
  5. Brute Force: And finally there is the brute force mechanism of specifying the exact position of every component programmatically. Sometimes the layout needs of a window don't fit with any of the sizers, or don't warrant the complexity of the constraints or the layout algorithm. For these situations you can fall back on doing it "by hand" but you probably don't want to attempt it for anything much more complex than half a dozen sub-windows or controls. wxPython does give you some tools to help though. One typical pitfall of specifying pixel coordinates for controls on a dialog box is what happens if the font size changes? Suddenly everything seems scrunched together or otherwise out of whack. By using dialog units instead of pixels for positions and sizes then the dialog is somewhat insulated from font size changes. A dialog unit is based on the default character size of the default font for the window, so if the font size changes then the size of a dialog unit changes too. The helper functions wxDLG_SZE and wxDLG_PNT turn dialog units into actual pixel sizes or positions.

1 comment: