Explaining the wxBrowser 0.3 Version: 0.1 Date: 2005-08-28 Author: Johan Lindberg, Pulp Introduction The wxBrowser is a program that lets you run a desktop application via HTTP. It works like a regular web browser but instead of reading and "executing" HTML/JavaScript to generate a document. It reads wxwML (wxWidgets Markup Language) and generates and executes wxPython code. The wxBrowser gives you the benefits of having your application's logic in one place, and one place only, just like a regular web application. But it also gives you access to the large collection of widgets in the wxWidgets GUI library. What it is and what it isn't The wxBrowser is an experiment. I think it's important to let you know that straight away. I've used it for four different applications. One of them was made just for show (My contacts). Which means that it's far from thoroughly tested. I am, however, using frequently both at home and at work so I hope that quality will improve over time. The wxBrowser is unfortunately not a solution to the rexec problem. Even though you may think it's very similar to Java's applet technology there are several differences and most notably, there is no sandbox in the wxBrowser. Why use it? I don't expect many programmers will want to convert their existing web applications into wxwML but I think there can be a point to try it out if you've got a wxPython-based desktop application and you've been annoyed at least once at the distribution and installation problems that seem to become worse with each released version. How it works When you start the wxBrowser you must also supply a URL pointing to the application you want to start. If you haven't got one, you can try out my sample application (My contacts) which you'll find at http://www.pulp.se/wx/contacts/v2. To start the application, just type (at the command prompt): python wxBrowser.py http://www.pulp.se/wx/contacts/v2/ What happens now is that the wxBrowser generates and loads a lot of constructors and handler functions. After that an HTTP Request is sent to the URL, triggering the web application to send an HTTP Response. It's important that the web application keeps track of whether or not this is the first request from this particular wxBrowser. The reason for this is because unlike HTML-based web applications a wxwML document does NOT contain a description of the whole application. It only sends the commands neccessary to put it in the desired state. So the first request must build, if not the whole, at least most of the GUI to be used. The first five lines of the sample applications first response looks like this: 01: 02: 03: 04: Hopefully, you'll feel rather comfortable reading this. Don't worry about the first two lines at the moment. They have to be there, but there's really no other way they can be written that will change what happens when they're interpreted by the wxBrowser, so just copy them for your application. Let's instead look at line 03. It's the opening element of a wx:Frame. The attributes match the wx.Frame constructor function in wxPython. In fact, this is so for all of the supported widgets, except for validators. There is no support for validators at all at the moment. The name attribute is mandatory, for all widgets. If you leave out name, the application won't work and the wxBrowser will crash. All other attribues are optional, if they're optional in the wxPython constructor. NOTE! You can see what code has been executed in the wxBrowser frame. If it's hidden, make it visible by double-clicking on the wxBrowser icon in your taskbar. At the top you'll see all of the constructors and handlers that are used to help convert the wxwML to wxPython GUI code. If you scroll down to the bottom you'll see the last executed statement The wx:Frame element will be converted to the follwing wxPython code: 01: MainFrame= wx.Frame(parent= None, id= -1, style= wx.DEFAULT_FRAME_STYLE, title= "My contacts", ) Hopefully you won't have any trouble reading the wxPython code and you'll see how everything in the wxwML maps to a place in the constructor. The id attribute is something I've added, if you leave it out it will still pop up in the constructor code set to -1. The same thing, basically, will happen for all elements. Let's look at line 04. On my computer it's converted to: 02: FrameIcon= wx.Icon(name= "c:\\docume~1\\...\\temp\\tmpsnsboq.ico", type= wx.BITMAP_TYPE_ICO, ) 03: MainFrame.SetIcon(FrameIcon) wx.Icon is a bit special not only because the constructor uses the name attribute completely differently from all other widgets, but also because the wxwML element's file attribute points to another URL. What happens is that the icon, located at the URL, is downloaded to the local system and the file attribute is rewritten as name in the constructor. Going further down in the wxwML we find a MenuBar definition: 01: 02: 03: 04: 05: The above is converted to: 01: MenuBar1= wx.MenuBar() 02: mFile= wx.Menu() 03: MenuBar1.Append(mFile, title= "File", ) 04: miNew= wx.MenuItem(parentMenu= mFile, text= "New Contact\tCtrl+N", id= -1, ) 05: mFile.AppendItem(miNew) 06: wxBrowser.Bind(event= wx.EVT_MENU, handler= wxBrowser.MakeHandler(url= "new.asp", args= wxBrowser.GetArgumentsAsDict(_node)), source= miNew) The wx:MenuBar element and corresponding constructor function is not much to talk about. The wx:Menu element includes a rather strange looking attribute: append.title. It is used as an attribute when calling the function Append, which is called directly after the constructor in the case of wx.Menu. The wx:MenuItem is also rather uninteresting, but please note how the parent/child relationsships between different elements are reflected in the generated wxPython code. What's interesting however is the bind element. The bind element does not have the wx prefix. It's not going to be translated into an object in the wx namespace. If you look at line 06 you can see that there's quite a lot of code that doesn't look like something you'd ever write in your app. The wxBrowser variable refers to the wxBrowser class which is a subclass of wx.App. The MakeHandler function generates a function that takes care of the request/response part. The GetArgumentsAsDict function with _node as parameter is used to get which parts of the GUI should be sent as arguments to the server. There are none in this case, but we'll get to that later. The rest of the generated code (event and source) are just like any other Bind. Let's look even further down in the wxwML: 01: Closing the wx:MenuBar element causes the following code to be generated and executed: 01: MainFrame.SetMenuBar(MenuBar1) There are several diferent handlers for an element. So far we've only looked at constructors which are generated and executed when an opening element is found. There are also Close handlers that are called when the closing element is found. Most code can be placed in the constructor but there are times when the Close handler is absolutely neccessary. The wx:Frame's Close function, for instance, performs: Frame.Fit() and Frame.Show() functions. So far we've got the Frame and a MenuBar with a Menu and some MenuItems. Let's look at sizers: This is what's defined after the wx:MenuBar's closing tag: 01: 03: 04: 05: 08: 09: 10: 11: The first couple of lines all generate the expected code. Note the use of add.flag and add.border which are used when adding widgets to the sizer. I guess you've already figured out what the generated code looks like: 01: anon1= wx.BoxSizer(orient= wx.VERTICAL, ) 02: anon0= wx.Panel(parent= MainFrame, id= -1, ) 03: anon1.Add(anon0, flag= wx.ALIGN_LEFT | wx.ALL, border= 0, ) 04: sbLabel= wx.StaticBox(parent= anon0, id= -1, label= "Johan Lindberg, Nordea Liv & Pension", ) 05: anon2= wx.StaticBoxSizer(box= sbLabel, orient= wx.VERTICAL, ) 06: anon3= wx.BoxSizer(orient= wx.HORIZONTAL, ) 07: anon2.Add(anon3, flag= wx.ALL, border= 2, ) Worth mentioning is that the function attributes (for example add.flag) are searched upwards in the wxwML structure until a value is found so if you wish to use a different value for border on one of your widgets, just add the attribute add.border with an appropriate value in that element, it will be used instead of the parent's value. Going further down in the wxwML, we can see that there are more wx:BoxSizer elements and after that, almost last is the bind element for the update menu item: 01: 02: 03: 04: 05: 06: 07: 08: The reason it's been put down here is because when the generated code is executed the widgets that are referenced need to exist otherwise we'll get an exception. I mentioned earlier the GetArgumentsAsDict function which took one parameter: _node. What it does is that it scans the current xml.dom.Node for children. If the children are named argument and of type reference they're included as arguments (HTTP POST) in the request that is sent to the server when the event is triggered. In this case you can't see any of that in the generated code: 01: wxBrowser.Bind(event= wx.EVT_MENU, handler= wxBrowser.MakeHandler(url= "update.asp?id=1", args= wxBrowser.GetArgumentsAsDict(_node)), source= miUpdate) But the whole thing results in that if the user selects the "Update Contact" menu item. The dictionary returned from GetArgumentsAsDict will be used to generate key/value pairs which will be included in the HTTP POST request. The key will be the name attribute from the argument element and the value will be fetched from the GUI by calling either GetValue(), GetLabel() or GetChoice() on the referenced. Which function to call is selected by comparing the output of the class function with the contents of a pre-defined list of widgets. You can add and remove items from these lists dynamically, we'll look at that later on. Except for bind there is also unbind which, not surprisingly, unbinds an event handler. It's very similar to bind and shouldn't cause any problems whatsoever. Calling functions The call element is used to perform a function call. If you click on the button with the right arrow in the sample application and watch the corresponding wxwML (either use the --debug flag and watch wxBrowser.log or go to whatever URL is specified in the bNext handler, in my case it's default.asp?id=16.) Notice that most of the wxwML is made up of function calls and (re)bindings of menu elements and buttons. Take a look at one of the call elements: 01: 02: 03: It's corresponding wxPython code is not much of a surprise: 01: teName.SetValue(value= """Sofia Jonsson""", ) But a few things should be mentioned. In the above, argument is specified in a child element. It can also be written as an attribute in the call element itself but in that case the argument is un-named and will not be quoted, so that won't work for strings. There are currently three different types available: string, number and reference. The difference is that strings are triple-quoted and references are looked up in the local namespace to point to the corresponding widget. If you want to use a multiple line string you need to place the value as a text node child instead of as an attribute: 01: 02: This is a 03: multiline string 03: The above will be translated into: 01: teName.SetValue(value= """This is a multiline string""", ) Adding and/or customizing handlers for widgets, even unsupported ones. There is a highly experimental part of the wxBrowser that allows you to customize the behaviour of the wxBrowser from the server. It uses the wx:Data element. To be able to use it ou need to understand how the wxwML document is traversed. When an element is found it's constructor-generator (wx_[Name]) is called. For example, the wx_Frame function is called when the XML parser finds a wx:Frame element. All generator functions are called with two arguments, the first is node which is a reference to the XML node and the second is app which is a reference to the wxBrowser class. After the constructor, two combination-generators are called, first wx_[ParentName]_Any and then wx_[ParentName]_[Name]. When an element's closing tag is found, the close-generator (wx_[Name]_Close) is called. By defining a new function within a wx:Data element (in a CDATA section) you can override default handling of a widget. You can also define your own handler for an unsupported widget. The following code: 01: borrows heavily from the default implementation but also forces the background colour to be set to red (line 09). NOTE! There's really nothing stopping you here from adding any Python code you can think of. In coming versions I'll try to implement the possibility to add local handlers as well as those bound to a URL. That would mean that you could write code that saves state to a local file or similar. Anyway, this is all very experimental and it's really meant more to be used for adding things like wx_lib_iewin_IEHtmlWindow and similar constructors. I hope this rather brief tutorial gives you enough information to experiment with the wxBrowser and make it work for you. If you think there's something missing or if you find any bugs or weird behaviour, please let me know by sending an e-mail to johan@pulp.se Johan Lindberg, Pulp 2005-08-29