(written by John Salerno, firstname.lastname@example.org )
I've decided to write this tutorial to explain the basics of using XRC (XML Resource) for the construction of wxPython GUIs (Graphical User Interfaces). I spent a few days looking for documentation on this topic but couldn't find too much, so I thought it would be helpful to create a step-by-step guide to getting started with XRC. But I have to preface this tutorial by saying that I am not a wxPython or XRC expert. What I plan to explain here are the essentials for getting started, but I won't be covering advanced topics yet. I learned a lot from UsingXmlResources, but the intention of this page is to present the information in a clearer and more structured manner, whereas that site simply gives an example with little explanation.
Before I begin, I also want to thank Robin Dunn, creator of wxPython, for providing such great help on the mailing list. Without his help and advice, I wouldn't have grasped these concepts so quickly. Now, let's get started...
What is XRC?
First of all, a quick explanation of what XRC is: XRC is a method of defining the layout of a GUI application in a separate XML file rather than as wxPython code within the program itself. The benefit of this process is that the design is kept separate from the logic (similar to the distinction between HTML and CSS). For example, this is a traditional start to a wxPython program:
1 #GUI and logic combined in the same module 2 3 import wx 4 5 6 class MyFrame(wx.Frame): 7 8 def __init__(self): 9 wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, title='My Frame') 10 panel = wx.Panel(self) 11 label1 = wx.StaticText(panel, wx.ID_ANY, 'First name:') 12 label2 = wx.StaticText(panel, wx.ID_ANY, 'Last name:') 13 self.text1 = wx.TextCtrl(panel, wx.ID_ANY) 14 self.text2 = wx.TextCtrl(panel, wx.ID_ANY) 15 button = wx.Button(panel, wx.ID_ANY, 'Submit') 16 sizer = wx.FlexGridSizer(rows=2, cols=2, vgap=5, hgap=5) 17 self.Bind(wx.EVT_BUTTON, self.OnSubmit, button) 18 sizer.Add(label1) 19 sizer.Add(self.text1) 20 sizer.Add(label2) 21 sizer.Add(self.text2) 22 sizer.Add((0,0)) #filler for the grid cell 23 sizer.Add(button) 24 panel.SetSizer(sizer) 25 sizer.Fit(self) 26 27 def OnSubmit(self, evt): 28 wx.MessageBox('Your name is %s %s!' % 29 (self.text1.GetValue(), self.text2.GetValue()), 'Feedback') 30 31 32 class MyApp(wx.App): 33 34 def OnInit(self): 35 frame = MyFrame() 36 self.SetTopWindow(frame) 37 frame.Show() 38 return True 39 40 41 if __name__ == '__main__': 42 app = MyApp(False) 43 app.MainLoop()
The important point to notice here is that almost all of this code is involved in GUI creation. This is usually the case with the __init__ method of the custom Frame class, because it is used to set up the layout of the frame. The only real logic in this example is the Bind call and the event handler OnSubmit. As you can imagine, any non-trivial application will quickly pile up with GUI code and it becomes more difficult to see the logic.
So how does XRC solve this problem? To put it in broad terms first, when you use XRC to construct an application, you will be using two separate files: the Python file, as above, that will now contain only the logic of your program, e.g. event handlers; and the XRC file, which is an XML document that describes the physical layout of your program. Here are these two files for the above program, refactored to take advantage of the separation of design and logic. There are some new concepts here, but I will explain them below. For now, just notice the difference in the content of the Python file and take a look at the structure of the XRC file.
1 #logic by itself in module 2 3 import wx 4 from wx import xrc 5 6 7 class MyApp(wx.App): 8 9 def OnInit(self): 10 self.res = xrc.XmlResource('gui.xrc') 11 self.init_frame() 12 return True 13 14 def init_frame(self): 15 self.frame = self.res.LoadFrame(None, 'mainFrame') 16 self.panel = xrc.XRCCTRL(self.frame, 'panel') 17 self.text1 = xrc.XRCCTRL(self.panel, 'text1') 18 self.text2 = xrc.XRCCTRL(self.panel, 'text2') 19 self.frame.Bind(wx.EVT_BUTTON, self.OnSubmit, id=xrc.XRCID('button')) 20 self.frame.Show() 21 22 def OnSubmit(self, evt): 23 wx.MessageBox('Your name is %s %s!' % 24 (self.text1.GetValue(), self.text2.GetValue()), 'Feedback') 25 26 27 if __name__ == '__main__': 28 app = MyApp(False) 29 app.MainLoop()
Although there is some new code here, it's obvious that the bulky GUI code has been removed, making this file easier to read and maintain.
<?xml version="1.0" encoding="utf-8"?> <!-- design layout in a separate XML file --> <resource> <object class="wxFrame" name="mainFrame"> <title>My Frame</title> <object class="wxPanel" name="panel"> <object class="wxFlexGridSizer"> <cols>2</cols> <rows>3</rows> <vgap>5</vgap> <hgap>5</hgap> <object class="sizeritem"> <object class="wxStaticText" name="label1"> <label>First name:</label> </object> </object> <object class="sizeritem"> <object class="wxTextCtrl" name="text1"/> </object> <object class="sizeritem"> <object class="wxStaticText" name="label2"> <label>Last name:</label> </object> </object> <object class="sizeritem"> <object class="wxTextCtrl" name="text2"/> </object> <object class="spacer"> <size>0,0</size> </object> <object class="sizeritem"> <object class="wxButton" name="button"> <label>Submit</label> </object> </object> </object> </object> </object> </resource>
This file may seem difficult to read (as XML can sometimes be), but there is a fairly obvious pattern that you will soon learn to pick up on. I'll describe it in more detail below. And of course it isn't necessary to write the XRC file by hand. You can use an editor such as XRCed to generate the above output.
So now I hope you have a general idea of the point of XRC. Remember, keeping design and logic separate is a good thing!
Creating an XRC file
So how does one go about writing an XRC file? Can you just make up your own tags? An XRC document follows a fairly simple structure with more or less predefined tags. You will notice that the representation of an object in XRC contains tags that correspond to the keyword arguments of that object's constructor. Here is the constructor for a Button:
wx.Button(parent, id, label='', pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, validator=wx.DefaultValidator, name='button')
We don't normally use all of these parameters, so here is a realistic example of creating a button:
button = wx.Button(panel, wx.ID_ANY, 'Submit')
If we created this button in an XRC file, it would look like this:
<object class="wxButton" name="button"> <label>Submit</label> </object>
There are a few things to notice here:
- XRC uses an object node to represent the particular widget you are making.
- Instead of the call to wx.Button, XRC uses a class attribute with the value "wxButton."
- Note that XRC can only use the C++ class names, i.e. there is no dot operator.
XRC uses an attribute called name to refer to this object in your Python program. The name attribute actually corresponds to the id parameter in the widget's constructor. You assign it a string value in the XRC file and wxPython will create a unique ID for it internally.
This value does not have to be the same as the name of the variable you will use to refer to this widget in your program. The value of name is not the name you will use to refer to this object in your program.
Once this object node is created, the keyword parameters of the Button's constructor become child nodes, each with the value of that particular argument. So here we have the label node created with the text that will appear in the Button, just as if we had manually written the Button constructor with the label argument.
The parent argument is not created as a child node. Instead, the entire Button object node will be a child node of a Panel object, which in turn will be a child node of a Frame object. This is how wxPython knows the parent-child relationship. Below you can see the entire structure of a Frame with a Button.
<?xml version="1.0" encoding="utf-8"?> <resource> <object class="wxFrame" name="mainFrame"> <title>Test Frame</title> <object class="wxPanel" name="panel"> <object class="wxButton" name="button"> <label>Submit</label> </object> </object> </object> </resource>
Notice also that all of the objects are wrapped in the <resource> root node. This simply means that the file is an XML Resource.
Almost all programs will begin with a Frame and a child Panel. From there the Panel will most likely have a Sizer, as in our original example above. Fortunately you don't have to worry about typing out an XRC file by hand (although I recommend creating a small one by hand, just to get the feel for the structure of it). There are several programs you can use to generate the XML for you, such as XRCed. Below is a screenshot of the XRCed session that was used to create the simple Button example, as well as a screenshot for the larger program above. You add to the hierarchy by clicking on the objects in the left panel, and you edit the properties (arguments) of those objects by changing the options in the right panel. When the file is saved, a well-formed XML document is created, as above.
So now I hope you have a basic understanding of the structure of an XRC file. Using XRCed makes creating these files almost trivial, so the trickier part, in my opinion, is figuring out what to do with it once it's been created.
Using the XRC file in your Python program
Ok, now we are at the important stage of loading your XRC file into your application and creating any references to widgets that you might need over the course of the program run. If you took a careful look at the program above that uses the XRC file, you might have figured out how it works already, but let's do it step-by-step, just to be sure.
Obviously we need to import wx in order to take advantage of wxPython. But the second line of our program will also import the xrc module, which is necessary for calling the XRC functions that interact with the XRC file. You could also write this as import wx.xrc, but xrc must be imported explicitly -- you cannot simply import wx and then refer to xrc as wx.xrc.
I'm sure you already understand the concept of subclassing wx.App and defining the OnInit method, so we will start with the first line of this method. self.res will be the variable used to store the contents of the XRC file. It is not necessary to use "res" as your variable name -- this can be anything. xrc.XmlResource is the name of the class used to create an XRC object, and it takes as it's argument the path of the XRC file to be used for the program. So this single line serves the purpose of "importing" the XRC file's contents into your program. The XRC is now stored in self.res and this variable will be used to extract the widgets that we need later.
self.init_frame() is also not a necessary call -- you can do all of your Frame initialization here in the OnInit method -- but it helps to separate these tasks into their own methods. Assuming that you want to break up these tasks, you would then define this method: def init_frame(self): This is where you begin to work with the XRC file, i.e. load your main frame, obtain references to other widgets, etc.
self.frame = self.res.LoadFrame(None, 'mainFrame')
You will most likely have this line in all of your programs. self.frame will become the reference to your main Frame object, just as if you had subclassed wx.Frame as in the original example at the top of the page. You then call the LoadFrame method of the XmlResource object. This is a special method that loads a top-level Frame object into your Python program. The first argument is the parent, which is always None for the main Frame, and the second argument is the ID you created when writing the XRC file. After this line executes, you will have a reference to your application's parent Frame. This line will also initialize all of the children of the Frame, so it is not necessary to explicitly "create" the Panel, Static Text, Textboxes, or Button, because this is now the job of XRC to do for you. All you had to do was define them in the XRC file.
As you can probably tell from the left-hand side of these assignments, you are getting more references to your GUI objects. First the Panel, then the two Textboxes that will be used for input. To get a reference for a widget, you use the XRCCTRL function of the xrc module. This function returns the object just as if you had typed self.panel = wx.Panel(self.frame). (But it is important to understand the difference as well: the above three lines are not creating these widgets, they are simply obtaining references; self.panel = wx.Panel(self.frame) actually does create and show the panel widget.)
The first argument is the parent object -- for the Panel, the Frame is the parent; for the Textboxes, the Panel is the parent. The second argument is the ID you used when you created the XRC file.
Note: It is not necessary to get an explicit reference to the Panel. When getting references to the Textboxes, you can simply use self.frame as the parent argument. XRCCTRL uses the FindWindowById method, which is recursive, and therefore will automatically find the Panel as the parent of the widgets even if self.frame is used.
At this point you now have all the references you need for this program. You then use these when you write the logic of your application.
self.frame.Bind(wx.EVT_BUTTON, self.OnSubmit, id=xrc.XRCID('button'))
This is a simple Bind call, just as you would normally make in your custom Frame classes. The only difference is that in a custom Frame, you would just need to write self.Bind because self already refers to the Frame instance, whereas in this case it refers to the custom App instance and so needs further qualification. The first two arguments are also the same as usual -- the event type and the event handler -- but the third argument involves a little more XRC know-how.
Just as XRCCTRL was used to return the actual widget object, XRCID is used to return that object's ID. It takes the string ID you assigned when creating the XRC file and returns an integer using wxPython's wx.NewID() call. So this line of code hooks up the given event handler to the object whose ID is given in the third argument.
An important distinction about event binding
There are two things to notice about the previous section, both of which are related to the wxPython constructor for event binding:
Bind(event, handler, source=None, id=wx.ID_ANY, id2=wx.ID_ANY)
We can safely ignore the id2 parameter because it won't concern us. But as you can see, the third parameter of the Bind method -- source -- actually takes the object itself. Since XRCID returns the object's ID rather than the object, we skip the source argument and use the id keyword argument to pass in the object's ID.
You might have already asked yourself why we used the id argument instead of just passing in the object itself, as Bind calls usually do. In this particular case it would have worked. So instead of:
self.frame.Bind(wx.EVT_BUTTON, self.OnSubmit, id=xrc.XRCID('button'))
We could have created a reference to the Button object, just as we did with the Panel and Textboxes:
self.button = xrc.XRCCTRL(self.panel, 'button')
And then used that reference as the source argument in the Bind call:
self.frame.Bind(wx.EVT_BUTTON, self.OnSubmit, self.button)
So why not just do this all the time? It certainly makes a cleaner Bind call. One downside -- if you consider it as such -- is that you are adding an extra line to your code to get the reference to the object. A lot of programmers like this explicitness, though.
But the real reason for even introducing the concept of XRCID is because sometimes it is necessary to use it rather than passing the object itself. XRCCTRL can only return objects that derive from the wxWindow class -- that is, objects that have a GetId method. In this case, wxButton does derive from wxWindow so you can safely pass the object itself to the Bind call.
However, another frequent use of Bind is to hook up event handlers with menu items, but the wxMenuItem class does not derive from wxWindow. Therefore, you cannot use XRCCTRL to return a MenuItem object. Instead, None would be returned and the Bind method would default to the id parameter, which would be wx.ID_ANY. The result of this would be that the event would be bound to all objects with the id wx.ID_ANY, and this is definitely not what you want.
In these cases, you must use XRCID to get the object's ID instead, and pass that as a keyword argument to the Bind call.
Finally you call self.frame.Show() as normal.
The rest of the code involves the logic of the program, but the event handler for the Button makes use of our other two widget references:
We simply use the references to the Textboxes as normal, calling the GetValue method on each to get their string values.
Using XRC with dynamically generated widgets
One side effect of loading a Frame into your program with XRC is, as stated earlier, that it automatically initializes all the children of that Frame. This is great most of the time, but occasionally we need to have widgets defined in our XRC file but not appear in the program until we (or the user) is ready for them. Other times we want to allow the user to customize the GUI to his liking, which means our layout can't be set in stone.
In order to do this, you define these objects as top-level nodes in your XRC file, just as you define the Frame as top-level. Frames, Dialogs, Panels, Toolbars, Menus, and MenuBars can be top-level objects, and each of these objects has a method to load them into your Python program, just as we used self.res.LoadFrame to load our parent Frame object above.
Let's see an example using a Frame. Our program will consist of an initial Frame that has a Button, and when this button is clicked it will generate a new Frame object. Since we don't want this second frame to appear immediately, and since this button may never be clicked at all, we don't define the secondary frame within the first frame (which you can't do anyway). Here is the XRC file for this layout. Notice that the two Frame objects are on the same level.
<?xml version="1.0" encoding="utf-8"?> <resource> <object class="wxFrame" name="mainFrame"> <title>Primary Frame</title> <object class="wxPanel" name="mainPanel"> <object class="wxButton" name="button"> <label>New Frame</label> </object> </object> </object> <object class="wxFrame" name="nextFrame"> <title>Secondary Frame</title> <object class="wxPanel" name="nextPanel"> <object class="wxStaticText" name="label"> <label>This is a dynamically created frame.</label> </object> </object> </object> </resource>
Using what we've learned so far, we can simply load the main Frame object in our Python program and this will display when the program runs. We then hook up an event handler to the button to deal with the second frame, if necessary.
1 import wx 2 from wx import xrc 3 4 5 class MyApp(wx.App): 6 7 def OnInit(self): 8 self.res = xrc.XmlResource('dynamic.xrc') 9 self.init_frame() 10 return True 11 12 def init_frame(self): 13 self.frame = self.res.LoadFrame(None, 'mainFrame') 14 self.panel = xrc.XRCCTRL(self.frame, 'mainPanel') 15 self.button = xrc.XRCCTRL(self.panel, 'button') 16 self.frame.Bind(wx.EVT_BUTTON, self.OnNewFrame, self.button) 17 self.frame.Show() 18 19 def OnNewFrame(self, evt): 20 self.frame2 = self.res.LoadFrame(None, 'nextFrame') 21 self.frame2.Show() 22 23 24 if __name__ == '__main__': 25 app = MyApp(False) 26 app.MainLoop()
There's really nothing new here. All that we've done in the OnNewFrame method is load the second frame, just as we've done with the first. But because it was listed in the XRC file as a top-level object, it must be explicitly invoked in your program -- it will not initialize with the main frame.
End of the line for now
I hope this tutorial was useful in explaining the groundwork of using XRC. As I learn more, I may expand this page with advanced concepts. But with what I've described here you can get most, if not all, GUI tasks done. It's really rather easy to use XRC once you get the hang of the method calls, and the leanness of your Python files will be immediately evident once you learn to move your GUI code into a separate file.