Contents
Introduction
The W Widget set is a collection of python modules which overlay the Framework application module. They are the principally used in the Mac python IDE, but can potentially be used to implement any python Application on the Mac.
To give a flavour of how you can use the W widgets, consider the following example (which you can type into the interactive window of the IDE):
import W w = W.Window((600, 400), "Hello!", minsize = (240, 200)) def buttonCallback(): print "Hello World!" w.button = W.Button((20,20,100,50), "Hello World!", buttonCallback) w.open()
This creates a window, with a button in it. When you click on the button "Hello World!" appears in the output window.
The window will persist until you close it, either by closing the window, or by typing:
>>> w.close()
Because the IDE is written using the W widget set, all the support is built-in and easy to access interactively. This allows quick testing of widgets in the best tradition of the interactive Python interpreter - if you want to know if something works, open up a window and try it out.
The same code in the regular Python interpreter will not work, however, because W is not automatically enabled; you'd need to write a full application to use it. We'll see how to do that later.
Using W within the IDE
While typing at the interactive prompt can be useful quick and simple things, anything even remotely complex should be written in an editor window and run, so that you can save your code for reuse and debug more easily.
To access the W widgets, all you need to do is
import Wbefore you use them. If you are planning to go beyond the capabilities of the standard widgets, you may need to import some of the Carbon toolbox interfaces as well. There are also a couple of modules in the W package which provide useful and essential functions, such as
Wkeys
(which holds useful constants for key events) and Wapplication
(which holds base application and menu classes).
Windows
Typically, you don't want to do things in windows that the IDE controls, so
you most likely will want to create a new window to work in - a blank
document window is an instance of class W.Window
. The creation step
looks like:
W.Window((600, 400), "Hello!")A window must be given a position/size argument and a title. Position/size arguments are required by all widgets, and consist of a tuple (left, top, width, height). In the case of a window you can also use a pair (width, height) and the window will be automatically positioned. The title is just the text which appears at the top of the window frame. Windows take other arguments as well, the most notable being minsize, which is a (width, height) pair which gives the minimum size of the window, and also makes the window resizable.
Once you've created a window you can then add other widgets to it and build
up its contents, however the actual Macintosh window will not be created
until you tell it to open()
. Opening a window automatically
opens the heirarchy of widgets it contains at the same time.
There are two variant types of window, Dialog
and ModalDialog
which create
dialog windows. The most critical thing about these is that they have no
close box and deactivate the Close menu item, so you must build in some way
of closing them (usually a Cancel button bound to w.close
does
this nicely). This is particularly important for ModalDialog
boxes, as they will block access to all other windows (including the
interpreter window) while open.
Creating and Deleting Widgets
Once you have a window, you can create widgets to work with. Unless you
want to modify your window dynamically, you should create your widgets
before you open()
the window. You add widgets to a window
simply by setting a new attribute of the window containing the new widget,
or by assigning as you would to a dictionary. In other words, if
w
is a window, you would attach a new button to it either by:
w.button = W.Button((20,20,100,50), "New Button")or by:
w["New Button"] = W.Button((20,20,100,50), "New Button")If you use the first method, be careful not to use the name of a class method for any of your widgets.
You can close a widget by deleting it from the parent widget, either by:
del w.buttonor by:
del w["New Button"]Closing the containing window will also delete all its widgets. All subwidgets of a given widget are also deleted automatically on close().
Types of Widget
The W widget set has a number of standard widgets which you can use with little effort. These include:
Button
(possize[, title, callback])BevelButton
(possize[, title, callback])-
This creates a standard Macintosh push button or bevelled push button.
CheckBox
(possize[, title, callback, value])-
This creates a standard Macintosh check box. The default for value is 0, which is unchecked.
RadioButton
(possize, title, thebuttons[, callback, value])-
This creates a standard Macintosh radio button. Thebuttons is a list of other radio buttons which act in concert with the new radio button. The default for value is 0, which is unchecked.
ScrollBar
(possize[, callback, value, min, max, livefeedback])-
This creates a standard Macintosh scroll bar.
List
(possize[, items, callback, flags, cols, typingcasesens])-
This creates a standard Macintosh list box.
TextBox
(possize[, text, align, fontsettings, backgroundcolor])-
Creates a static text widget.
EditText
(possize[, text, callback, inset, fontsettings, tabsettings, readonly])TextEditor
(possize[, text, callback, wrap, inset, fontsettings, tabsettings, readonly])-
Creates an editable text widget. The first is intended for simple entry fields, the second for editing substantial quantities of text. If there are subwidgets of its parent widget called
_barx
and_bary
, then these will be used as scrollbars for the window. PopupWidget
(possize[, items, callback])PopupMenu
(possize[, items, callback])-
Creates a small popup menu button. The first dynamically creates the menu when the widget is clicked, the second pre-generates the menu.
HorizontalLine
(possize[, thickness])VerticalLine
(possize[, thickness])-
A horizontal or vertical line.
Frame
(possize[, pattern, color])-
A rectangular box.
BevelBox
(possize[, color )])-
A box with a beveled appearance.
Group
(possize)-
This creates a widget which groups together its subwidgets as a unit.
HorizontalPanes
(possize[, panesizes, gutter])VerticalPanes
(possize[, panesizes, gutter])-
Creates a group of widgets in a horizontal row or vertical column. The space available to each widget can be resized by dragging the dividers between them.
For more complete documentation of what they do, and what the parameters mean, read the reference portion of this document.
Some simple demonstrations of these widgets can be found in the Scripts menu of the IDE; hold down the option key when selecting a menu item to see the source code.
Callbacks
For some simple widgets, all that you want to do is to create and delete them, but often you will want them to do something. Most widgets have a way of linking a function which will be called when a major event occurs. Such a function is called a callback. For example, a button widget can have a callback which is run whenever the button is clicked. You define a function or method (or more generally, any callable Python object), and then pass it as a parameter when creating the widget:
def callback(): print "Look at me! I'm a callback!" w.button = W.Button((20,20,100,50), "Callback", callback)Remember that, in Python, functions are first class objects, and so you can pass
callback
as you would any other variable.
The number of parameters that a callback takes depends upon what is required by the widget. Clicking a button, for example, is a simple event, so no extra parameters are needed. Interacting with a scroll bar is much more complex, with a range of different outcomes, and callbacks for scroll bars are required to take a parameter which tells them what has just happened.
A CheckBox widget, for example, passes an optional parameter to its callback which is 1 if it is checked, and 0 otherwise. We can use this information to set a global variable based on the value:
myvar = 0 def setmyvar(value): global myvar myvar = value print myvar w.checkbox((20,20,100,24), "My Variable", setmyvar)Notice how checking and unchecking the box changes the value.
By using callbacks to change other widgets, you can get quite sophisticated behaviour for comparatively little effort. However there is a limit to what you can conveniently do with callbacks; for very complex behaviour, you will probably want to subclass the widget.
Events and Bindings
Typically, callbacks sent during object creation respond to just one event - usually the main one that the widget is intended to handle. There is another way that W allows you to respond to effects.
Every widget has a list of bindings, which associate an
event - in W just a short descriptive string - with a callback. You create
these by calling the bind()
method of a widget like so:
mywidget.bind('cmdc', do_copy)This binding would call the
do_copy
callback whenever the
widget received a "Command-C" keystroke. (Actually, this would be better
handled via a do_menu
handler.)
Events are usually sent to the front window and the currently selected widget in that window, but some events may be propagated down the hierarchy to other widgets.
The callback may take arguments, depending on the type of event. The
events you can bind to include particular keystrokes, general keystrokes, <click>
events, <idle>
events and
<select>
events . In addition, some events which get propagated universally,
like the activate
event, and are normally handled by class
methods, can also be handled by bindings to their names, or to
<name>
. These events include
open
, close
, draw
and
activate
.
Bindings of the form <name>
have the option
of returning 1 to halt further processing of the event.
Menus
Menus are not quite as nice to work with interactively as other parts of W
Widgets - they are essentially the same as the menu classes discussed in
the FrameWork
documentation. You can add simple menu items, however; more complex
behaviour will be discussed later.
A menu is created by instantiating Wapplication.Menu
. You
need to pass it the menubar it is to be attached to, and its title. An
optional parameter determines where it is to be inserted. Once it is
created, you need to populate the menu with menu items.
You can find the current application's menubar by calling:
app = W.getapplication()to get the application, and then:
menubar = app.menubaris the menu bar. Adding a menu is then as simple as:
test = Wapplication.Menu(menubar, "Test")
A menu item is added by instantiating a FrameWork.MenuItem
object. You must pass it the menu it is to be instantiated in, the title of
the menu item; optionally you can pass a command key equivalent and a
callback. At this stage if you want a menu item to do anything, you must
pass a callback. Menu items must be added sequentially to the menu, you
can only add a menu item to the end of a menu.
For example:
def callback(): print "You just selected a menu item!" newitem = FrameWork.MenuItem(test, "A menu item", "B", callback)
To delete a menu, you need to import the Menu
module, and call
the delete method of each object:
newitem.delete()removes the item from the menu (again, you must delete from the bottom of the menu only).
test.delete() Menu.DrawMenuBar()deletes the menu from the menubar, and redraws the menubar. You should delete each menu item - deleting the menu will break circular references to them, but will not call their
delete()
methods.
Examples
Here are some examples which illustrate what is discussed above.
- Number Dialog
A dialog box which validates that the entry is a number.
- Calculator
A calculator in an IDE window. Uses several tricks and techniques that are generally useful.
Writing a New Widget Class
Writing a new widget is a matter of subclassing the appropriate widget class and redefining methods appropriately. Unfortunately, depending on your widget, this could be just a few lines of code, or it could be hundreds.
New Widgets
Before starting, you should have a reasonable idea how the widget you are subclassing works, so you have some idea what needs to be changed. Read the reference section and, if needs be, have a look at the source code.
Typical methods you will need to override include:
__init__
()- Creates the widget, and sets up the data structures.
open
()- Opens up the widget. Often this is the stage at which you should create any controls that the widget uses.
close
()- This is called when the widget is being destroyed. Make sure you delete any new instance variables you define which could contain circular references, so that Python can free the memory your widget uses.
click
(point, modifiers)- This is called whenever a click occurs in the widget. If you need your widget to respond to clicks in some manner, you should use this.
draw
([visRgn =None
])- This is called whenever your widget needs to be drawn. visRgn is the region of the window which is visible, so you can test this to avoid unneeded drawing.
adjust
(oldbounds)- This is called whenever your widget's bounding rectangle is changed.
If you override this, you should call
W.Widget.adjust()
so that the window updates correctly. rollover
(onoff)- A widget's parent window will invoke a rollover method of a widget if the mouse moves in into or out of the widget at idle time. Usually used to adjust the cursor.
key
(char, event)- This is called whenever the widget is selected
and the user presses a key. The widget should be a subclass of
SelectableWidget
to make full use of this.
Other common methods are get()
and set()
for
widgets that have some sort of state; enable()
and
activate()
for widgets that change behaviour and appearance
based on the state of other widgets; and methods which simulate user
actions such as clicking on the widget.
More on Menus
Now that you are writing your own classes, you have the opportunity to take
advantage of a more elegant way of using menus. If a menu item is passed a
string as its callback rather than a callable object, it will look for a
handler of the form domenu_string
and call that.
It will look for this special handler in the application's methods first,
and then in the frontmost window. However, if the frontmost window has a
SelectableWidget
(one
which is designed to take keyboard input) active, then that widget will
receive any unhandled menu hits. If you are writing a widget which is
selectable, you may want to take advantage of this feature.
In particular, the IDE lets the following menu items be handled by either
the window or the current widget: save
, saveas
,
close
(usually handled by the window), undo
,
cut
, close
, paste
,
clear
, and selectall
(usually handled by the
widget).
W also provides a way for menu items to be enabled or disabled
automatically. If a menu item is in the application's
_menustocheck
instance variable, and there is a
can_string
method available, then W will call this
when the menu bar is clicked; the method should return 1 or 0 depending on
whether or not the item should be enabled. Save, Undo and Paste menus are
particularly appropriate for this. W will also automatically disable any
menu item which it can't find a callback for.
Examples
- Bevel Button Widget
A new class of pushbuttons using the System 8 beveled appearance.
- Styled Text Editor Widget
A text editor widget which allows text with many different styles.
- Validated Field
A validating field widget based on the
editText
widget. (XXX Works, but is not optimal; no comments.) - Quickdraw Picture Widget
A simple picture widget: takes a QucikDraw Picture in the form of a resource handle (such as you would get from
Qd.GetPicture()
) and displays it.
Applications with W Widgets
The basic strategy for writing a full-blown application using W Widgets (or FrameWork, for that matter, is to provide subclasses for the Application class (to handle application-wide issues), and the Window class (to handle document-level processing).
Writing an Application
An application is a subclass of Wapplication.Application
. To
make a working application the methods you must provide in the application
class are:
- An
__init__()
method: this should at least call the base class__init__()
method andself.mainloop()
. It should probably also install Apple Event handlers, load documents passed through sys.argv, and display a splash screen if appropriate. - A
makeusermenus()
method: this sets up the menus and callbacks for the application; you should have a File and Edit menu, even if they do nothing more than offer Quit as an option. Any menu items which need to by dynamically updated (which will be most of them) should be added to theself._menustocheck
instance variable here too. - A
checkopenwindowsmenu()
method: If you have a Windows menu which keeps a list of open windows, this should update that menu; if not it should justpass
. Look at the PythonIDEMain.py to see how you would implement it. (XXX a stub method should really be part ofWapplication.Application
.) - Handlers for Menu Items: you should provide
domenu_quit()
or equivalent at an absolute minimum; most likely, you will want to writedomenu_new()
anddomenu_open()
methods as well; and if there are other menu items which are application-wide, like preferences, you should handle them as well. - Handlers for Apple Events: if you are using Apple Events, then you will need to write handlers for them. Python's support for Apple Events is fairly rudimentary, so most Apple Events which can be handled will be application-wide.
- A call which creates an instance of your application so that the whole thing runs.
This will get you as far as having an application which runs, but can't do
anything much. You will need to write document classes to represent the
data and how to manipulate it. The easiest way to do this is to subclass
W.Window
, however if your data
is very complex, or there is more than one way to look at it, you may want
to separate the representation of the data (the document) from its visual
appearance (the window).
Your document must provide:
- An
__init__()
method: this should take an optional parameter of the path or URL (or equivalent location information) of the document and be able to read it in and create an appropriate window and widgets. If the user invokes the New menu item, then most likely there will be no relevant path information, and so the method should create a blank document. - A
close()
method: this should close the document gracefully, offering the user the opportunity to save if needed. It should also carefully break any circular references in data structures related to the document to avoid memory leaks. Finally it should close up the actual window and widgets by calling the window'sclose()
method. - Menu Handlers: some menus are best handled at the document level -
domenu_save()
anddomenu_close()
are the obvious ones, but there may be many more. You'll likely want to track whether the document has been changed and provide acan_save()
method so that your menu items enable correctly.
Some menu handlers may be best placed in the window's currently active widget, rather than the document window. Edit menu items often fall into this category.
Ideally, it should be possible to design a document class so that it is independent of the application - any application which knows how to instantiate it, and provides the right menu items, should be able to run it. This sort of modularity can make the creation of applications very fast if you have a collection of document classes and widgets to hand; just plug them together in the right way.
A Template Application
Probably the easiest way to understand what you need to do to write an application is to have a look at a simple application which does the minimum required to get up and running, and does what it has to do.
Then have a look at this basic document class, which fits in with the above application.
You can use these templates as a starting point for simple applications.
Bundling the Application
At this point we have all the Python code for a working application, but we can't quite run it yet - you can't run an application from within the IDE because they compete for resources and clobber one another; and we can't run it from the Python Interpreter, because there are resources which W needs which it won't be able to find.
To run it, you need to build an applet or an application. Fortunately these are pretty straightforward. Make sure that:
- That
$(PYTHON):Mac:Tools:IDE
folder is in Python's default path (use the "Edit Python Prefs" applet to set it). - That the file "MyApp.rsrc" is in the same folder as "MyApp.py". This is a resource file containing all the needed resources to make a standalone application.
BuildApplet creates a very lightweight application, but it requires
PythonCore and all the libraries to be available for it to run.
BuildApplication creates a full standalone application which can be run on
computers which do not have Python installed (this the same as
freeze
on Unix Python). Which method you choose depends
heavily on the environment you wish to run in, but for experimentation, an
applet is probably fine.
For your application, you will need your own version of "MyApp.rsrc" - you
could copy it as a starting point, or you can create it from scratch. The
way "MyApp.rsrc" was created was to copy the "Widgets.rsrc" file from the
IDE folder, and use BuildApplet. This creates an applet which runs, but
has the console window in the background. To get rid of that, the applet
was dropped on Edit Python Prefs, and "Default Startup Options" and then
"Delay console window until needed" were selected. To avoid having to make
this change every time we rebuilt, we used ResEdit to copy the
GU∑I
resource into the application .rsrc
file.
For a serious application, you will want more information in your
.rsrc
file, such as custom dialog resources, icons, menus, and
so on; as well as customised BNDL
, FREF
and owner
resources; just as you would with any other Mac application.
Examples
(XXX It'd be nice to have some examples here, even if they aren't fully explained)
Conclusion
Hopefully this will have given you sufficient overview to get started. The reference contains more specific details of what various handlers and parameters are for.