Tutorial

Release: 16/10/2001

Introduction

This document aims to show the developer how to construct a simple application using the library. It will cover the main features provided and give advice on how to write custom objects to expand the functionality of the library.

Importing the app module

Before an application can use the library, it has to import the app module. This is achieved by including the line

import app

at the beginning of the Python program. The functions and classes can now be accessed in the app namespace. Additionally, the appobjects module, which contains some useful objects to put inside windows, can also be imported using either

import appobjects

or

from appobjects import *

We could instead just import the objects we require, e.g.

from appobjects import Square, Text

would just import the Square and Text objects from the appobjects module.

Starting the application

To start a new application that uses the RISC OS window manager, we need to include the line

app.start_task(<name>)

where <name> is a string containing the name of the application. Before the application finishes running we need to use the corresponding end_task function.

Creating a window

Window objects are instances of the Window class. To illustrate the creation of a window, let us use the example

window = app.Window(0, 100, 1000, 500, 400, 200, "cbt", 1, None)

which would create a window that, when opened, has its lower left corner positioned at (0, 100) in screen coordinates. The window has a visible width of 400 screen units and height of 200 screen units. The total width and height of the window are 1000 and 500 screen units respectively. Positions and dimensions should all be given as integers.

The string "cbt" indicates that the window will have a close icon, a back icon and a title bar. The characters in the string indicate pieces of window furniture or features of the window:

	c		close icon
	b		back icon
	t		title bar
	T		toggle size icon
	a		adjust size icon
	h		horizontal scroll bar
	v		vertical scroll bar
	r		window is redrawn by the window manager
			(the application never receives redraw events)
	k		the window can have the input focus

Use of the "k" character in the string is mandatory if notification of key presses is required.

The background of the window is taken to be colour 1, the standard background which is tiled on later versions of RISC OS. A number from 0 to 15 can be given, or None may be used to indicate that the window has no background.

The final argument in the instantiation of a Window object is the name of a parent window. This is usually None, but for nested windows a string containing the name of a window is passed.

Now that the window has been created, the app module must be informed if it is to be used. Let us use

app.add_window(window, "my window")

where "my window" is the name we have given the window for future reference. The app module stores the window object in a dictionary under the name given. The window can now be opened, or used as part of a menu, for instance.

Note: The window object can be retrieved by an assignment such as

window = app.windows[<name>]

Removing a window

To just close a window, it is only necessary to call the close method of the relevant window object, e.g.

app.windows["my_window"].close()

but to destroy it completely requires that it be removed using the app module's remove_window function. This is called with the name of the window as its argument:

app.remove_window("my_window")

The window will ask its objects to close themselves if appropriate then close itself before being removed. If the removal was successful then None is returned. However, objects may try to keep the window open by returning values other than None. If this occurs then the value returned by such an object is returned to the caller of remove_window. Generally, objects don't try to keep windows open.

Configuration windows

Since the standard Window class is used to create general purpose windows for the application, the behaviour of the mouse pointer when the mouse buttons are clicked allows clicking, double clicking and dragging of objects. This is not really suitable behaviour for configuration windows where instantaneous single clicks over standard window manager icons are required. Therefore, the Config_Window class, derived from Window may be used to create configuration windows for applications. We use the same approach to creating an instance of this class as we used when creating a general purpose window, e.g.

config = app.Config_Window(0, 0, 768, 768, 768, 512, "cbtTha", 1, None)

It is intended that icons are used to present Style Guide compliant configuration windows.

Opening a window

Once registered with the app module, the window can be opened at its current position on the screen by the use of the window object's open method, e.g.

window.open()

or, if the window is called "my window" then the following will also work:

app.windows["my window"].open()

Alternatively, the to_front method can be used to open the window at the front of the window stack, obscuring other windows, e.g.

window.to_front()

or the centred method may be used to open the window in the centre of the screen, e.g.

window.centred()

Creating objects

Objects can be created for use in windows. There is a generic object class fron which all other objects are derived: the app module's Object class. Instances of this class are created in the following manner:

object = app.Object(<x1>, <y1>, <x2>, <y2>)

The x and y coordinates given specify the positions of the lower left and upper right corners of the object's bounding box. The coordinates are relative to the work area origin of the window in which the object is placed; this is at the top left of the work area so that most objects will have positions with positive x coordinates and negative y coordinates.

Some objects allow additional attributes to be specified on creation. For instance, the Square object (in the appobjects library) allows the colour of the square to be set:

square = appobjects.Square(32, -64, 96, -32, 11)

Of course, the shape defined is actually 64 work area units in width and 32 in height, so is therefore not a square! The colour given as the fifth argument is the standard desktop colour number for red.

Adding an object to a window

Newly created objects need to be told which windows they reside in. This is done by using the add_object method of a window object. For example, we could add the square to our window by writing

window.add_object(square, "my square")

The window object adds the object to its internal dictionary under the name "my square". The dictionary can be used in the same way as the app module's windows dictionary to retrieve and manipulate objects, e.g.

object = window.objects["my square"]

By default, the object will immediately appear, if the window is already open. To suppress this behaviour, we use the update keyword argument:

window.add_object(square, "my square", update = 0)

or just window.add_object(square, "my square", 0)

Note: The add_object method for objects does not currently support the update keyword argument.

Removing an object from a window

Objects can be removed from windows using the remove_object method, allowing us to remove our square from the window we have created. Perhaps, at some later stage, we need to access the window object through the app module:

window = app.windows["my window"]
window.remove_object("my square", update = 0)

where we suppress immediate redraw of the area where the object used to be by using the update keyword argument.

Note: Again, the update keyword argument is not supported in the remove_object method for objects.

Receiving events

At some point, we need to enable the user to interact with the interface we have constructed. This interaction could have been built into a library function called mainloop or similar, which only returned when the application is about to exit. However, it is sometimes useful to examine events passed to the application within the body of the application rather than only deal with them through objects. Therefore, we use a traditional loop structure, during which we call the library function poll which returns the raw event code and some details.

quit = 0

while quit == 0:

	code, details = app.poll()

The poll function can take two keyword arguments: null is set to 1 if the application wants to perform actions when not responding to other events, time is the minimum time the application wants to wait before receiving another null event. Applications which want to incorporate objects which are dragged around in windows must set the null argument to 1.

Types of event

The poll function returns a number of different codes for different events that occur. Many of these concern particular windows and are passed by the library to the relevant objects which represent them. Others are not intended for a specific application and it is up to an object within the application's object hierarchy to claim the event. In addition, even events which can be passed by the library to a particular window object must be offered to all objects in the window to determine which was the intended recipient. This arises as a result of the mismatch between the philosophies followed by the library and the window manager. In practice, the application developer does not need to worry too much about the differences between these types of events.

Direct events

Objects which can be placed in windows can associate functions with particular events. Those events which are generally passed directly to the window and are then offered to objects contained in that window include mouse clicks, key presses and some general events. To tell an object to execute a function with certain arguments when clicked on with a particular mouse button requires the use of the add_clickaction method of the object. For example, to tell our square object to respond to a click with the select button, we would use

square.add_clickaction(app._select_, function, args)

where function is a function object which takes a list or tuple as its only argument and args is a list (not tuple) of items we wish to pass to function when it is executed. The reason that args is a list is that the object (a square object in this case) will append the window manager's window handle to the end of this argument list; this wouldn't work if args was a tuple. The window object to which the handle refers can be found using the app modules window dictionary in the same way as we would find it using the window's name, e.g.

window = app.windows[handle]

The mouse buttons or actions which can be trapped are:

	app._select_		select button
	app._menu_		menu button
	app._adjust_		adjust button

	app._select_drag_	drag using select button
	app._menu_drag_		drag using menu button
	app._adjust_drag_	drag using adjust button

	app._select_double_	double click with select button
	app._menu_double_	double click with menu button
	app._adjust_double_	double click with adjust button

Similarly, key presses are trapped by assigning a function and arguments to a key code, as in

square.add_keyaction(ord("h"), print_message, ["Hello"])

The key codes are standard codes for the window manager. Again, the window handle is appended to the list of arguments.

Some general events, such as the DataLoad event are also sent in reference to a particular window. To set up a handler for this event we use

square.add_messageaction("DataLoad", handler_function, ["Some info."])

and write a function called handler_function which will take a list of arguments. Details specific to the particular type of message are appended to this list of arguments, so for DataLoad the handler function should also expect x and y screen coordinates, the filetype of the file to load, its pathname and the window name of the window in which the file was dropped. The function returns None if it didn't act on the event otherwise it returns some other value.

Examples of events, the information appended to the arguments list and the return values expected from the function on success are:

	Message		Extra arguments		Expects if successful

	DataSave	screen x		Pathname of temporary file
			screen y
			filetype
			pathname
			name of parent window

	DataLoad	screen x		Any value other than None
			screen y
			filetype
			pathname
			name of parent window

	Help		work area x		Help text string
	(object in	work area y
	a window)	name of parent window

	Help		item number		Help text string
	(item in a	menu name
	menu)

	MenuWarning	screen x		Any value
			screen y
			menu name

Indirect events

These events are not associated with particular windows, and are usually broadcast to all applications. The only supported event of this type which is handled by the library at present is the DataOpen event:

	DataOpen	filetype		Any value other than None
			pathname
			name of parent window

Note that the window name passed to the handler function does not indicate that the event was intended for objects in that window.

Menus

The Menu is derived from the basic Object class. This means that it is possible to add menus to windows using a window object add_object method. However, it is useful to be able to specify that a menu should be accessible from any point in the window rather than just from within a bounding box. In such a case, the bounding box coordinates can all be set to None. In addition to the bounding box, the Menu class requires two more arguments to create a menu object:

my_menu = app.Menu(None, None, None, None, "My application", items)

The string "My application" is the title we have given the menu and will appear inits title bar. The items argument is a list of items which will appear in the menu and can be changed by accessing my_menu.items.

Each item is itself a list or tuple containing five items: the item text, flags, submenu pointer, a function and a list of arguments. For example, an item which leads to a submenu may be defined as

item[0] = ["Names", "dotted", names_submenu, None, None]

Here, the item "Names" appears as the first item in the menu. It is followed by a dotted line, and there is an arrow to its right leading to a submenu which has previously been defined. If the submenu has not been defined then it is a good idea to refer to it by name and define it later:

item[0] = ["Names", "dotted", "submenu of names", None, None]

An item which leads to a window rather than a submenu could be written as

item[1] = ["Style", "", "styles window", None, None]

where "styles window" could have been replaced by a direct reference to a window object.

If an action is to be performed when an item is clicked then we define a function to perform the action and decide what arguments it needs to do this. We could write

item[2] = ["Quit", "", None, quit_fn, ["some", "information"]]

where quit_fn is a function object which takes a list as an argument. Unlike mouse clicks and key presses, the function only receives what is contained in the arguments list. [This could change in the future.]

It is now necessary to add the menu object to a window, or the icon bar icon. This is simply achieved in the same way as for any object:

window.add_object(my_menu, "A main menu")

Submenus are defined in the same way, except that they are not added to a window, or to their parent menu. Instead, they are added to a menu dictionary in the app module in a similar manner to the practice of adding windows:

app.add_menu(my_submenu, "submenu of names")

This non-hierarchical approach is partly due to the way the window manager informs applications about submenus. [This could be worked around in the future.]

Since menu objects can respond to events as any other object, it is possible to dynamically alter submenu items as a submenu is opened. This is useful when the information to be presented needs to be up-to-date and is achieved in the following way:

my_submenu.add_messageaction("MenuWarning", last_minute, [])

Now, when the submenu previously named as "submenus of names" is opened, the last_minute function object is called. This function must have been written to accept a single list argument which will not be empty, as we noted before, but will actually contain the x and y screen coordinates of the submenu and its name. The function might contain code to change the submenu title and each item, e.g.

def last_minute(args):

	# Read the arguments
	x, y, menu_name = args[:3]

	# Find the menu object
	menu = app.menus[menu_name]

	# Change the submenu title
	menu.title = "Menu opened at (%i, %i)" % (x, y)

	# Change the submenu items

	menu.items = [ ["x = %i" % x, "", None, None, None],
		       ["y = %i" % y, "", None, None, None] ]

	# The return value is unimportant

The second string in each menu item can contain recognised keywords separated by commas to change the appearance and behaviour of the item. These require some explaining:

ticked
The item is ticked. Changing this requires either the use of the toggle method, e.g. my_menu.toggle(1) to tick/untick the second menu item, or manipulation of the items attribute.
writable
The item can be modified and text entered. The original text in this item must be long enough to contain any possible input and this is usually achieved by padding a string to the required length with "\0" characters.
shaded
The item is shaded and cannot be selected.
dotted
The item is followed by a dotted line.
shared
This property does not affect the menu item, but affects any submenu to the right of it. If the shared flag is not used then the submenu will be removed from the app module's menu dictionary when its parent menu is removed. For submenus which are shared by many menus this flag prevents removal of the submenu, although this means that the submenu can only be removed manually using the remove_menu function.

Fonts

Fonts objects are used to encapsulate the properties of typefaces and provide services related to their use by objects. To use a particular style of text of a given size, we instantiate a font object. For example, the line

my_font = app.Font("Trinity.Medium", 12, 12)

produces an object which can be used in the appropriate circumstances to render 12 point text in the Trinity.Medium style. Font objects are usually used by objects which handle text rather than by the application itself.

Sprites

Sprites, like fonts, are not usually rendered directly by the application but are used by objects to provide images in windows. However, instances of the Sprite class deal with collections of sprites rather than single sprites and therefore need some explaining.

To use sprites from a file, we need to first create a sprite area object:

sprites = app.Sprite()

At this point, the sprite area is undefined and unusable. To load the sprites from a file called "my_file", we would write

sprites.load("my_file")

Now, we may obtain some information on the sprites present in the area by accessing the object's internal dictionary:

names = sprites.sprites.keys()

producing a list of sprite names. We can obtain information on a particular sprite by looking it up in the dictionary using its name as the key. For example,

info = sprites.sprites["my_sprite"]

assuming that "my_sprite" is present.

If we require a sprite from the system area, such as a directory icon or a sprite for a common filetype, then we create a sprite object and inform it that it refers to the system area using the system method:

system_sprites = app.Sprite()
system_sprites.system()

Since many objects only require the sprite object and a sprite name to function, it is possible to use system sprites and custom sprites without specific code for each case.

Icons

Icon objects provide an interface to the icons used by the window manager. They are only really intended for use in configuration windows since they cannot be scaled arbitrarily and respond to mouse clicks differently to other objects.

We create an icon object in the following manner:

my_icon = app.Icon(64, -128, 192, -64, ("Some text", "", 7, 1))

This creates an icon which displays the string "Some text" and has foreground colour number 7 (black) and background colour number 1 (light grey). The empty string is the icon's validation string (see relevant documentation on the window manager).

To display sprites in icons, we need to have created a sprite object before we create the icon. For example,

sprites = app.Sprite()
sprites.system()
my_sprite_icon = app.Icon(64, -192, 192, -128, (sprites, "directory", ""))

Note how the icon class only requires three items in the list passed to create the icon. The second item is the name of the sprite to be displayed from the sprite area specified. The third item is the icon's validation string.

To create icons which are used to specify choices, such as option or radio icons, a text icon needs to be created with an appropriate validation string. For an option icon, we could write

option = app.Icon(64, -256, 192, -192, ("Click me", "Soptoff,opton", 7, 1))

Alternatively, we could use the Option or Radio objects in the appobjects module.

The icon bar

The icon bar object is not like the other icon objects in that, although the icon it represents resides in a window (the icon bar), the library has no control over that window. Therefore, it is something of a special case, having the properties of both a window and an icon.

To put an icon on the icon bar, we need to have created a sprite object before we can refer to the sprite we wish to display:

sprites = app.Sprite()
sprites.load("my_sprites")
icon_bar = app.IconBar((sprites, "my_icon", ""), location = "right")

Here we have loaded the sprites in the file "my_sprites" and put the "my_icon" sprite on the right hand side of the icon bar. The possible locations for the icon are: "right", "left", "far left", "near left", "near right" and "far right".

To remove the icon just requires that the object is deleted. However, a reference to it also resides in the app module's window dictionary. In most cases it is just sufficient to remove the window object corresponding to the icon bar icon using, for example

app.remove_window("__icon_bar__")

Multiple views and scaling

The procedure of creating objects then adding them to windows allows us to share the same objects between a number of windows. This is achieved as one would expect, e.g.

my_window.add_object(square, "my square")
my_other_window.add_object(square, "my square again")

Note that in this case each window knows the object by a different name. This practice should be avoided as it will complicate future manipulation of the object.

Consider the case where my_other_window has been created for the purpose of providing a magnified view onto the objects in my_window. It will need to have been created larger than my_window by a factor equal to the scaling factor required; it also needs to be informed that the objects it contains need to be scaled when they are displayed. We achieve this by setting internal attributes of my_other_window. For example, to magnify the objects so that they are displayed at twice their usual size, we write

my_other_window.zoom_x = 2.0
my_other_window.zoom_y = 2.0

The attributes should be assigned floating point values. To decrease the display size of objects in the window we would choose values between 0.0 and 1.0.

Note: A scale factor of 0.0 should be avoided.

Closing down the application

To close down the application use the following function call:

app.end_task()

The application's interaction with the window manager is terminated by this function. Usually, the application closes itself down using the sys.exit function immediately after this call, although it could first save configuration details to a file, for example.


David Boddie
david@boddie.org.uk