Archive

Using the Acorn Toolbox



Tony Houghton

A number of people have asked for a tutorial on programming with the Toolbox supplied with Acorn C/C++. I will be aiming this at programmers who are competent in C but have never used it to write a RISC OS application. Experience of programming the Wimp in Basic or Assembler will help, because to write your own applications, you will probably need to know many essential aspects of Wimp programming that I cannot cover here.

I would guess that anyone serious enough about their programming to buy Acorn C++ will also have the PRM, so I will assume that, at the very least, you are familiar with RISC OS and know what files should be inside an application directory and how they work; also you should have played around with ResEd and got the hang of it.

I have used C rather than C++ for the tutorial. This may seem a strange decision after my last series of articles, but there are good reasons. It means those of you new to C++ will not have to struggle with language issues as well as the Toolbox; and there are currently no C++ class libraries suitable for Toolbox programming.

The example application

I decided not to simply write a program that would open a few windows and do nothing; I wanted to write a small, but real, application to do a real job. It is called FormText and formats plain text files ready for printing. It adds margins, headers and footers, and even a contents page. The complete application is provided on the monthly disc so that you can examine what it does. I have not provided the Toolbox modules, because you should have them already if you're reading this. However, you may need some newer modules than were originally provided with Acorn C/C++, so you should get the C/C++ Update Disc 1 from Acorn-by-Post for £5.00. The upgrade is also available from Acorn's FTP site (ftp.acorn.co.uk) as archive cupdate1 in directory riscos/releases. The FTP site is reproduced on the Archimedes World and second Acorn User cover CD-ROMs. You will also need the upgrade for flexlib which we'll come to next month.

The directories Step1, etc, on the monthly disc show different stages of FormText's development to accompany this article.

Getting started

You really need to have a good idea of exactly what your program will do before you start writing it. However, the advent of the Toolbox at last means that you can design the whole interface of a program interactively if you wish, provided you are clear about what will go where.

If you want to provide interactive help, give all your objects help text as you go along - it is probably quicker in the long run, and certainly less tedious than leaving it as a final chore.

Event reason codes

For most events generated by the Toolbox, you have a choice between using the default reason codes allocated by Acorn or supplying your own. For this, Acorn have allocated numbers in the range 1-&FFFF. I prefer to supply my own for menu selection, and button icon click events in particular. This makes it much easier to make abstractions between a program and its user interface, and also makes it unnecessary to explicitly read which menu entry or button has been clicked. Thus, menus can be completely rearranged and common functions can be duplicated with keyboard shortcuts without changing any code. If you do use your own reason codes, you will obviously have to keep track of them in a text file or on paper. It is a good idea to allocate your own chunks, say 1-255 for a particular window, 256-512 for a menu tree, etc.

Step 1

I like to have something pretty to look at as early as possible when writing an application, so the first thing I did was create the application sprite files, !Sprites and !Sprites22.

I decided to make FormText use an iconbar icon with the most basic of menus, just containing Info and Quit entries. Dragging a text file to FormText's icon will pop up a window, allowing the user to choose how the file should be formatted. When formatting is complete, the window will be replaced by a 'Save as' dialogue box to save the processed file.

I created a new file in ResEd and opened the Prototypes window. From there I selected Iconbar, Menu, ProgInfo, SaveAs and Window and dragged them to my file. I didn't bother to rename them because I'm only using one of each.

I then edited the Iconbar object to use the sprite name !formtext with no text. We don't need it to do anything in response to Adjust or Select, but it does have a menu, so I selected the relevant options and dragged the Menu object to the 'Show menu' writable icon; many of ResEd's writable icons allow you to drag objects to them instead of typing. No help text is necessary for the iconbar because the Wimp provides its own messages here if you don't.

For the menu, I set the title to 'FormText' and stopped it from generating show and hide events. No help text is necessary if all the entries have help. I changed the existing entry to "Quit" and made it generate event 1. I then opened the Menu entries dialogue and dragged an entry with a submenu to above my Quit entry. I set the text to "Info" and dragged the ProgInfo object to the Submenu action 'Show object' field.

In the ProgInfo object, I left the title as default to be given the task name and filled in the main fields. I use version numbers of 0.xx for software I am developing and update it to 1.00 for the first release.

In the SaveAs dialogue box, the default title will do; we want a filetype of text, and no selection will be available. The client (our program) does not need to participate, but it needs to know when the dialogue box is closing to clear the file from memory when it has finished with it.

For the main dialogue box, 'Window', I changed the title and added all the gadgets required. The top group is for all the page dimensions - I used writable number ranges and linked them together and later with the other writable icons in the window.

The header and footer fields are for entering text to add at the top and bottom of every page respectively. %p and |M are special sequences to allow newlines and the current line number to be inserted. There are no buttons to enable/disable generation of headers and footers, because this stage of processing is required to get the page dimensions right. If a user doesn't want headers and footers, the fields can be left blank.

Any line beginning with a top-bit-set character corresponding to the character entered in the Heading flag field is treated as a heading. It will be underlined with as many Underline chars as necessary and appear in the contents if enabled.

Contents generation can be switched on and off, and the title to use on the contents page will behave like a header. The leadering character is used to pad the gap between a contents entry and its page number to guide the reader's eye.

I gave the action buttons appropriate event numbers from the 'chunk' &100-&1FF that I allocated for the main window.

The file must be saved as "Res" (with optional territory suffix) to be automatically loaded by the Toolbox.

All Toolbox applications require a Messages or Messagesn file where n is the territory number; 1 is the UK's territory number. The file must contain at least one entry that has a token _TaskName. This is read by the Toolbox when the task starts up and used as the task's name.

Step2

Time to start writing code. Have a look in the h directory. err.h contains declarations for error handling functions, using Wimp_ReportError to report messages in a Wimp error box. err_set_taskname() sets the string to use in the error box's title - I have simplified things by just using the task name instead of "Message from task name", when reporting messages that aren't program errors. The err_check() functions check the result of functions that return an OS error block pointer (_kernel_oserror *), zero meaning no error. If there is no error, the function returns (with a value of zero for the non-fatal version), but if there is an error, it reports it and returns with 1 for the non-fatal version or exits for the fatal version. err_report() is for reporting messages that aren't really errors, err_complain() and err_complain_fatal() are for things that should be considered user errors and might (or when forced by the fatal version) lead to the program being abandoned. E and EF are handy short-cuts to save typing when checking countless SWI veneer calls.

msgtrans.h contains functions for MessageTrans handling. It supports one file and relies on this being opened by another function, because the Toolbox opens the default file for us. msgs_get_ descriptor() returns a pointer to its descriptor for us to give the Toolbox to initialise. msgs_lookup() returns a pointer to the string corresponding to the supplied token. The string is not copied out of the original file, so it may not be relied on to be zero-terminated.

tbchunks.h shows which groups of Toolbox reason codes I have allocated to which objects.

After writing err.c and msgtrans.c, I started building the skeleton of the program in main.c. The first thing main() does is call initialise_task() to set up the program as a Toolbox task. Have a look at the entry for toolbox_initialise() in the User Interface Toolbox manual (currently pp33-34) and compare it with the call in function initialise_ task(). Throughout the manual, any flags not specifically mentioned should be left as zero. We should pass 310 as the Wimp version if we want the program to run on all versions of RISC OS from 3.1 onwards.

Next is a list of Wimp messages we want to receive - we're only interested in the ones concerned with transferring data and the Task manager signalling our program to quit. Next we need a list of Toolbox events we want to receive. A Toolbox application is likely to want to receive so many different Toolbox event codes that we might as well accept them all. It doesn't make it less efficient, because the Toolbox will only try to send us events that we've enabled in the Res file apart from a few that we want anyway.

FormText$Dir will be set up by the !Run file to point to FormText's application directory which the Toolbox needs to load its resource files. Then we pass it the MessageTrans descriptor used by the msgs functions and a block to use for Toolbox IDs. This block must stay available for as long as the task is running, but it does not need global access, so I have used a static local variable. We don't need to know the return values from toolbox_ initialise(), so the rest of the parameters are zero. The manual doesn't say that it is safe to do this, but the example sources provided by Acorn do so, so I have assumed it is safe.

The event library (event.h and eventlib.o) is provided to deal with polling the Wimp and passing events to client functions; you should read the chapter describing it in the Acorn C/C++ manual. After initialising, a program should register any handlers it needs with event and then keep calling event_ poll() in an infinite loop to sustain multitasking until it receives an event telling it to quit. Each call to event_poll() allows other applications to have a share of processor time, and the Wimp will respond with an event. event_poll() then tries to find a handler for that event, and when the handler has finished, event_poll() returns. Thus, it must be in an infinite loop to allow more events to be received.

Certain events can be masked out so that they will not be sent to our application. In particular, it is important to mask out null events to save our application being continually activated for no reason. event_set_mask() sets the mask for us. event_initialise() needs to be called so that event can pass a pointer to the same Toolbox ID block that we provided for the Toolbox to any handlers. Finally, initialise_task() looks up the task name in the Messages file and passes it to the error handling code.

Back in main(), it registers some handlers for general events. The first two are to deal with the two types of quit event we can receive; one is a message from the Task manager and one is the toolbox event that we told the menu to send us. It is also possible for the Toolbox to generate errors some time after we have called one of its SWIs, e.g. if we tell it to open a menu and, later on, the user attempts to open a submenu with an invalid ID. The Toolbox tells us about these errors with a Toolbox_Error event, so I have claimed this. Now all that's left to add to main.c, for now, is the main loop of calling event_poll, and we can produce a working task.

The interactive Make tool is fine for simple applications like this, so I loaded it and created a new file to build a !RunImage file with the Link tool. I dragged all the local c files to it as well as the clib Stubs, and from tboxlibs, eventlib, toolboxlib and wimplib. After clicking the Make button, I had a !RunImage file. To tell the truth, I had to do a little debugging most times I compiled something, but I won't clutter up the article with any of those details.

We now need a !Run file to ensure all the Toolbox modules are loaded (it checks for the versions as supplied with RISC OS 3.6; some of them have been updated, but the bugs don't seem to affect FormText and it would be too inconvenient to load the replacements). Set the FormText$Dir variable, allocate a small memory slot and run !RunImage. We now have an application that will put itself on the iconbar, show its menu and Info dialogue box and quit when asked.

Step 3

Now we need some way of getting a file into the program. The Toolbox makes it easy to export or save a file, but not to import or load one. I've written a general purpose 'component' (in the sense of a set of C functions and structures or C++ classes, rather than a Toolbox component) for loading and importing files; have a look at import.h. You will be able to use this in other programs you write.

It starts off with three types of functions that its 'client' (in this case other functions in FormText) should provide for handling the importing of files. An ImportLoader should load a file from a filing system (usually a disc); an ImportBufferHandler should handle memory allocation for a file being transferred in memory. An ImportComplete will be called when a data transfer has been completed. In each case, the handle argument is provided as a pointer to some data that the client can specify to keep track of what it's loading.

In case you have little RISC OS Wimp programming experience before learning the Toolbox, I had better digress slightly to explain data transfer from the receiving task's point of view. I should point out that the PRM has a more complete description than mine on pages 3­249 to 3-256. A task can be asked to load something with one of three Wimp messages; I'll use the names as defined in wimp.h supplied with Acorn C/C++.

Wimp_MDataOpen means a file has been double-clicked. FormText won't be responding to this - it should only be claimed by editors that 'own' a particular filetype.

Wimp_MDataLoad means the program should load a file from disc. It is usually sent if a file is dragged from a filer window to an application.

Wimp_MDataSave means a file has been dragged from the Save as dialogue box of another application to our application. This can be dealt with in one of two ways. The simpler way is to reply to the sending task with Wimp_MDataSaveAck, asking it to save the file as <Wimp$Scrap> and then send us Wimp_MDataLoad. Or we can reply with Wimp_MRAMFetch, asking for the file to be sent by RAM transfer. Not all applications will be able to do this, so if we don't receive a Wimp_MRAMTransmit reply from it, we should then resort to scrap file transfer.

RAM transfer consists of a series of Wimp_MRAMTransmit and Wimp_MRAMFetch messages until the sending task sends a block of data that doesn't fill the receiving task's buffer; then the receiving task knows the transfer is complete. Data is not actually sent with the Transmit messages, but by the sending task calling wimp_transfer_block(). The message is needed to tell the receiving task that this has been done.

Most of the above work is done by import; all the client has to do is wait for one of the three initiating messages, and call import_start, supplying pointers to its three functions. import's client is textfile - have a look at textfile.c to see how it works. The textfile variable is used to point to a file in memory, but the value of the pointer may change as memory is moved around by flex; a pointer to textfile is used as the handle for the import functions. Using a pointer to a pointer is confusing, but necessary because of the way flexlib works.

It is a shifting heap manager, meaning that it shifts blocks of memory around to make sure there are never any unused areas in the middle of the heap. Therefore, pointers to flex blocks do not have fixed values; they may be changed at any time by another call to flex. This means that there must only ever be one copy of the pointer, or the master copy could change without updating its copies.

So, for flex block pointers to be passed around safely, we are forced to use a pointer to the master pointer. flex uses the area of memory immediately above what was available to the task before calling flex_init(), and calls wimp_slot_size() during each operation to make sure it has just enough memory. You can get flexlib as part of the C/C++ Upgrade described in my previous article.

textfile_initialise() is called once at the beginning of main(). It initialises the flex memory manager, and registers an event handler for the two messages that it will receive when a file is dragged to FormText; textfile_importer() can be used for both because import does so much of the work. The former just checks that the file is text and calls import_start().

textfile_loader() allocates memory for the file and loads it. Although it knows at this point whether it has loaded a file, it is best to wait until textfile_complete() is called before taking any further action - that's its purpose.

textfile_bufferer() checks whether any memory has been allocated yet, and allocates a new flex block if not, otherwise it extends the block. It then updates import's pointer to point to the empty part of the block just made available, and returns with its size.

textfile_complete() at the moment just shows a message saying what file it has loaded and how big it is, then clears the memory ready to receive another file.

To add new files to a Makefile, Make should be loaded with the Auto run option off, then the Makefile loaded. The new files (in this case textfile.c, import.c and flexlib) should then be dragged to Make's Insert field and OK clicked. This adds the files to the list of those to be compiled and linked to form the RunImage.

Step 4

The first thing we want to happen when a file is dragged to FormText is for the main window to open. There are two new files, settings.h and settings.c to deal with the window. settings.h just contains a struct definition for how the settings are stored internally and in the preferences file, and function declarations for initialising and showing the window. All other functions concerned with the window are called from these two, or by event handlers.

Open settings.c and look at the function settings_initialise(). This is called once at the beginning of main() to prepare the settings window's handlers. Most of the operations on the window will need to know its ObjectId, but how do we find this out? We set the window's auto-create flag for convenience when testing the interface with ResTest, so we can't simply call toolbox_create_ object() and get the id that way.

In this situation, it might be more sensible to clear the auto-create flag and create it 'by hand', but it is important to know how to deal with auto-created objects, because more sophisticated applications will make heavy use of objects that are automatically created by virtue of their being attached to other objects.

One strategy is to ensure that all events generated by an object have reason codes unique to that object and only refer to the object's id in its handlers, where the id is available as the self_id field of the IdBlockœ passed to event_initialise(). This may not always be possible.

To allow an auto-created object's id to be found as soon as possible and used globally (the strategy used by FormText for Window), we use the Toolbox_ObjectAutoCreated event. When objects are auto-created, these events are queued up until they can be returned by event_poll() (or simply Wimp_Poll if not using eventlib). As far as I know, Toolbox_ObjectAutoCreated event data is generated after the object, and all its attached objects have been created, so you can find out the ids of attached objects when handling the event.

settings_initialise() registers a handler called settings_created to detect when the main (settings) window has been created. The other three handlers registered by settings_initialise() are for events connected with the window, but they have unique event numbers so can be registered before knowing the id.

Now have a look at settings_created(). First of all, it casts the event to the right type of struct - in this case, ToolboxObjectAutoCreatedEvent *toace. This sort of casting is a frequent chore when using the Toolbox. Then it checks that the name of the object that has just been created is "Window". If not, it returns zero to indicate it has not handled the event, and some other handler might want to do so.

If it is "Window", it reads its id from the self_id field of the IdBlock, and stores it in the variable settings_window. Then it checks for a settings preferences file that may have been created by a user saving the settings, and loads the settings from it. If no file is present, it reads the current settings from the window. In this way, the default settings can be edited by editing the Res file. The handler also politely deregisters itself, because it is only needed once.

The functions write_window() and read_window() are for writing the settings to the window and reading them from the window, respectively. I haven't room in this article to explain all the gadget-handling methods, but they should be fairly easy to understand from the manual; I suggest you look at how FormText uses them. set_fades() reads the current state of the Contents option button and fades, or unfades, other connected gadgets accordingly. There is no SWI to do this explicitly, so it has to rather inelegantly manipulate each gadget's flag word.

Let's consider the other event handlers registered by settings_initialise(). cancel_clicked() clears the text file from memory. It doesn't need to close the window, because unlike with Wimp applications, an action button closes a window unless the button's Local option is set. An action button's Cancel option doesn't currently seem to do anything, but I suspect it's supposed to force <adjust> to close a window as well as <select>.

save_clicked() reads the settings from the window and saves them in a preferences file (see above).

contents_click() responds to changes in state of the Contents option button and calls set_fades().

At this stage, I have modified textfile_complete() to call settings_show() instead of showing a message about the file that has been loaded then clearing it. settings_show() uses toolbox_show_object() to show the settings window as a static window/dialogue box, opening it at the position defined in the Res file.

A static window/dialogue box remains open until explicitly closed, whereas a transient dialogue box behaves like a menu and closes when the user presses <escape> or clicks outside it. Those familiar with Wimp programming will know that the former is opened by Wimp_OpenWindow and the latter by Wimp_CreateMenu.

Step 5

We now need to consider that the file will have to be saved once processed, using the SaveAs object. In settings.c, I have added a Toolbox_ObjectAutoCreated handler for "SaveAs", similar to the one for "Window". It stores the id in the variable saveas_id.

There is also a handler for a click on the Format button, format_clicked(). This handler reads the settings from the window, then calls textfile_process, passing saveas_id so the function can set the SaveAs pointers to the file. If the processing is successful, the main window is hidden and SaveAs is opened. I have chosen to make it a static dialogue box, because it's a nuisance if it vanishes while you hunt down that directory you forgot to open beforehand. Once SaveAs is open, the Toolbox handles all its functionality for us, because its Client participates option isn't ticked.

There is just one final handler registered by settings_initialise(). The saveas_done() clears the file from memory when it has been saved.

The main work of the program is done by textfile_process() in textfile.c. It returns 1 if successful, or 0 if not. It works with a simple multi-pass technique, applying one sub-process to the whole file at each stage. This can take a while, so I've added some simple Hourglass macros in hourglass.h.

I won't describe how the file is processed because it is irrelevant to the Toolbox, but feel free to examine the code if you're interested. Once the file has been processed, saveas_set_data_address() is called to inform SaveAs of the file's address and size before returning control to the function that opens SaveAs.

Conclusion

I hope you now know enough to write your own, larger, Toolbox applications. Obviously, I haven't been able to cover the whole Toolbox, but I have tried to describe its most important concepts. Please don't consider FormText to be a well-designed and well-coded application. In fact, its user interface is quite weak. It was just something I cooked up in a hurry to demonstrate some of the more important parts of the Toolbox, but it happens to do a useful job, albeit no more than adequately.


Contents - The Archives - Archive Articles