Programming for non programmers
Part 8 - Working with Windows
David Holden continues his series.
In the last session I described the outlines of a multi-tasking program. As this program will need to open a window on screen, the first step will be to explain how that is done.
Windows, with all their icons, are defined in a block of RAM, then the window is actually created by calling the SWI 'Wimp_CreateWindow' with R1 holding a pointer to the definition. This doesn't open the window, it just tells the Wimp about it, and the SWI then returns a window handle in R0. It is this window handle which is then used to refer to the window both by the program and the Wimp.
The data that describes a window is quite complicated, and although it's useful to know how it's made up, it's not strictly necessary because there's a much easier way of defining a window. The definitions for the windows your program will need can be produced in a separate program called a Template Editor and saved from this as a Template file. This file, which has its own special filetype, &FEC, is then read by the program when it starts up and the window definitions extracted from it and passed directly to the Wimp to create the windows. As Template Editors are WYSIWYG programs you don't have to get involved in the intricacies of window and icon definitions. You just need to have a general understanding of how they work.
Each window in a Template file is given a name, and there are some SWIs to read the window definitions from the Template file and create the windows, returning a window handle in the usual way.
Some icons in a window, and some parts of the window, such as the title, may contain data, like a line of text, which is too big to fit into the very limited space allocated within the window definition. This data can therefore be indirected, which means that the data is held outside the actual window definition in a separate block of RAM. The window or icon definition then, instead of the actual data, contains a pointer to the place where it is actually held.
This is particularly useful to us for two reasons. The first is that it lets us use much longer text fields that would otherwise be the case, because the 'internal storage' is limited to just 12 characters.
Secondly it makes it easy for our program to read this data or modify it without making any changes to the actual window itself.
So, assuming we have a template file called Templates in our application directory, and in it a pre-defined window named 'main' and we want to turn this into a window we would first need two things; a temporary workplace for the Wimp to load the templates and work on them and some RAM to store indirected data. Both of these can be created by DIMing an integer array. The first needs to be big enough to hold the largest window definition in the Template file, the second big enough to hold all the indirected data.
Assume we have created these and called them respectively tmp% and indirected%. and that indirected% is 512 bytes, and that we want to load a window definition called 'main'. We then proceed like this.
SYS "Wimp_OpenTemplate",,"<Obey$Dir>.Templates" SYS "Wimp_LoadTemplate",,tmp%,indirected%,indirected%+512,-1,"main" SYS "Wimp_CreateWindow",,tmp% TO main_handle% SYS "Wimp_CloseTemplate"
Let's go through this line by line.
The first line tells the Wimp that we want it to 'open' or use the Template file whose name is pointed to by R1. As the Template file is going to be in our application directory and we know that the OS variable 'Obey$Dir' will be set to this when the program is Run we can use this to define where our template file (called 'Templates') can be found.
The next line tells the Wimp to extract a particular window definition, named 'main', from the Template file and load it into the buffer 'tmp%'. The parameters passed to the Wimp by this SWI are -
The third line tells the Wimp to get the window definition it will find at 'tmp%' and turn it into a window. This it will do, returning the window handle in R0. We can then store this window handle for future use by assigning it to a variable, in this case, 'main_handle%'.
Obviously when there is more than a single window this procedure requires some adjustments, but we will come to that in due course.
The final line just tells the Wimp that we have finished with this Template file.
Opening a window
The previous section described how to define a window and get the Wimp to give us a window handle so that we can communicate with the Wimp about it. Now we have to actually open it on screen. This is done by calling 'Wimp_OpenWindow'. This SWI takes a single parameter in R1 which is a pointer to a data block. This data block must contain the following data -
You will notice that each parameter requires 4 bytes, so they're all integers. But what are all these things, and how do you find them out?
Luckily you don't have to bother about them too much, at least, not at this stage, because there is a SWI, Wimp_GetWindowInfo, to read them all from the data held by the Wimp and put them in a block of memory in exactly the order required. (In fact, this SWI reads the complete window definition, including all the icon data). So to open a window you would first call Wimp_GetWindowInfo to read the window data, then Wimp_OpenWindow to actually open it.
Wimp_GetWindowInfo takes just one parameter in R1, which is a pointer to a block of RAM to hold the data. before you call the SWI you must put the window handle into the first word of this block.
You might think it would be simpler if Wimp_OpenWindow could read the data for itself and open the window, but there's a very good reason for this slightly roundabout way of doing things. Wimp_GetWindowInfo returns the current state of the window. If we are opening a window, then this will normally mean the state the window was in when it was last closed (or if it's being opened for the first time, its state when it was defined). You may want to change its position, or alter the scroll offset or size. You can do this before you call Wimp_OpenWindow and the window will then open with the changes you've made.
We shall only bother setting one of these parameters at this stage, that is the value at R1 + 28. Usually, when you open a window, you want it to open in front of any other windows that might be open, even if it was behind another window when it was last closed. To do this all you need to do is call Wimp_GetWindowInfo and than make sure that this word is set to -1, so when you call Wimp_OpenWindow it will open on top no matter where it was when it was last closed.
So let's write a simple procedure to open a window exactly where and how it was when it was last closed except that we want to make sure it always opens on top.
In this and other bits of program I describe later I'm going to use the array tmp% as temporary workspace.
DEFPROCopen_window (handle%) !tmp% = handle% :REM put handle at start of data block SYS "Wimp_GetWindowInfo",,tmp% :REM get window data tmp%!28 = -1 :REM make sure it opens on top SYS "Wimp_OpenWindow",,tmp% :REM open the window ENDPROC
As you can see, the window handle is passed to this procedure as a parameter and it does the rest.
Windows and Wimp_Poll
As you saw from the last instalment there are two events returned by Wimp_Poll which concern the opening and closing of windows directly. These are 2, (open window request) and 3 (close window request).
An open window request returns a data block exactly as required by Wimp_OpenWindow, so all you have to do (and must do) is call Wimp_OpenWindow using this block. If your program is more complex you may well decide to have a special routine that handles this wimp event. For example if your window has a toolbar attached, moving one window will require the toolbar to be moved as well.
A close window request usually means that someone has clicked on the window's Close icon. The only parameter returned is the window handle in the first word of the data block. Once again this is exactly the parameter required by the SWI which closes windows, Wimp_CloseWindow, so again it's very easy to implement. Again as your program gets more complicated you may well want to create a special routine for this event. For example suppose the user shuts the main window if your program without having saved some data. You would want to provide them with a warning instead of just closing the window as the wimp requested.
A mouse click returned via Wimp_Poll (event type 6) returns a data block containing the following information -
You will know what a window handle is, but I haven't described icon handles.
Unlike window handles these aren't assigned by the Wimp. Instead they are a simple number, starting at 0, assigned as each icon is created. Using a template editor you can change these numbers, but if you want to be able to decode clicks on buttons and other icons you will need to know what they are.
Luckily it's not too difficult to find out. Good template editors like TemplEd and WinEd, both of which are provided on this months CD, will open a small window when you are editing a window which indicates the number of the icon the mouse pointer is over.
As Wimp_Poll returns the handles of the window and icon the mouse was clicked on we can use a CASE structure to 'decode' this information and decide what action (if any) we have to take.
The template for the VAT calculator program looks like this.
You will find this in the file 'Templates' in the BEGINPRG in the SOFTWARE directory of the CD. Here you will also find the !TemplEd and !WinEd template editors. Double-click on !TemplEd to run it and then double-click on the 'Templates' file to load it into !Templed. Two windows will open. On the top left of the screen is the window showing all the templates in the file. In this case there's only one, called 'main'. Double-click on this and it will open ready for you to edit. Now look at the top right corner of the screen and you'll see another small window, like the one shown below.
As you move the mouse pointer over the VAT calculator window so you will see that this window shows both the name of the window (main) and the number of the icon the pointer is over. You should see that the icon labelled 'Quit' is number 4, 'Add' is number 5, and 'Subtract' is number 6.
These are the only three icons we are interested in at the moment, so having noted their numbers we can now write the code to act on a mouse click on any of them.
Assume that we have already checked that the click occurred over the right window and that the icon number is held in the variable 'icon_no%'.
CASE icon_no% OF WHEN 4 : PROCquit WHEN 5 : PROCadd_vat WHEN 6 : PROCsubtract_vat ENDCASE
That should ensure that a mouse click on any of the three buttons is directed to the appropriate routine to deal with it.
Wimp error handling
In earlier programs we used a simple error trapping routine which just PRINTed an error message on screen. With a true Wimp program this is no longer possible; we need some sort of window to display an error message.
Luckily the Wimp has its own error window. We all know about that, it's the one that pops up when things go wrong. There are various ways to use this but with an uncomplicated Basic program we can employ a very simple error handling method. This is done using the usual 'ON ERROR' command to set the error handler followed by 'ERROR EXT'. This means that any error will be 'thrown back' to an external error handler, in this case (since the Wimp is the 'parent' of our program) the standard Wimp error handler. This takes two parameter, an error number, and the error text.
The error text can be the same as in the simple PRINT error handler, displaying the Basic error message (REPORT$) and the line number where the error occurred.
ON ERROR ERROR EXT 0,REPORT$+"at line "+STR$(ERL):END
The zero after EXT is the error number. In this case I'm using zero because that is the number of a fatal error, and in this program any error is likely to be a mistake in the program code and so we may as well quit the program after displaying the error message, which also explains the END instruction at the end of the line.
Initialising the program
Before a multi-tasking Wimp program can do anything it must start itself up and 'register' its existence with the Wimp. To do this it calls Wimp_Initialise and gets allocated a task handle. We have already seen (in Part 6) how important the task handle is. It is the 'name' by which your task is known and identified by the Wimp.
Wimp_Initialise takes three parameters -
To explain what these mean in practice;
The minimum acceptable Wimp version in R0. A program that will work on RISC OS 2 would use 200, one that required RISC OS 3.5 or better 350, etc. Certain functions of the Wimp are not available on earlier versions and so this provides a way for the Wimp to refuse to start up a program that will only work on a later version. (In fact, the program we are about to write uses 200, even though it won't work on RISC OS 2, but that's just so that we can simplify the program initialising code slightly and is not significant at this point).
The rather strange value in R1 is actually the ASCII values of the four characters of the word TASK. It just tells the Wimp that this is a genuine message from a program (or Task) that wants to start up.
The name pointed to by R3 is the name that will appear in the Task Manager window when the program is running and on the title bar of any error windows that appear.
The values returned by the SWI are -
The value in R0 could be important because your program may be able to offer additional facilities or use different code for different functions on a later version of the OS. This enables it to know what facilities are available and make the appropriate choices.
Putting it together
Having dealt with most of the pieces, we can now plug them in to our skeleton program from the last session.
REM Program to add or subtract VAT ON ERROR ERROR EXT 0,REPORT$+"at line "+STR$(ERL):PROCdie PROCsetup PROCopen_window(main_handle%) REPEAT SYS "Wimp_Poll",0,blk% TO event% CASE event% OF WHEN 2:SYS "Wimp_OpenWindow",,blk% WHEN 3:SYS "Wimp_CloseWindow",,blk% WHEN 6:PROCmouse_click(blk%!12,blk%!16) WHEN 17,18:PROCmessage_received(blk%!16) ENDCASE UNTIL FALSE END : DEFPROCmouse_click(win%,icon%) IF win%<>main_handle% THEN ENDPROC CASE icon% OF WHEN 4:PROCdie WHEN 5: WHEN 6: ENDCASE ENDPROC : DEFPROCmessage_received(type%) IF type%=0 THEN PROCdie ENDPROC : DEFPROCdie SYS "Wimp_CloseDown",task_handle%,&4B534154 ENDPROC : DEFPROCopen_window(handle%) !tmp%=handle% SYS "Wimp_GetWindowInfo",,tmp% tmp%!28=-1 SYS "Wimp_OpenWindow",,tmp% ENDPROC : DEFPROCsetup DIM tmp% 1024, blk% 255, indirected% 512 SYS "Wimp_Initialise",200,&4B534154,"VAT Calculator" TO ,task_handle% SYS "Wimp_OpenTemplate",,"<Obey$Dir>.Templates" SYS "Wimp_LoadTemplate",,tmp%,indirected%,indirected%+512,-1,"main" SYS "Wimp_CreateWindow",,tmp% TO main_handle% SYS "Wimp_CloseTemplate" ENDPROC
There are a few changes and additions, so I'll go through them now.
There is a new procedure, PROCdie, which is called when the program is going to shut down. Instead of just performing an END a Wimp program should call Wimp_CloseDown with its task handle in R0 and the same value (ie. the text TASK) in R1 as is used when calling Wimp_Initialise. In a more complex program PROCdie might contain various things to 'tidy up' before quitting, such as closing any open files, etc.
PROCmessage_received is called when Wimp Poll receives a message for the program. The message action code is returned in the data block at offset 16, so this is passed to the procedure as a parameter.
The only message our program is likely to receive is a type '0', which is the instruction to close down, so, if it receives such a message, that's what it will do. This is exactly what happens when a program is on the receiving end of something like the program described in Part 6, or when you select 'Quit' from the task's menu in the Task Manager window.
Your task must respond to and obey this command. It has no choice, it must quit. However, it is allowed to shut down gracefully, which is why PROCdie is called.
PROCsetup now contains the various things we have talked about. It DIMs the main arrays the program needs, tmp% for temporary workspace, blk% as a data block for Wimp Poll to use, indirected% for the window's indirected data. It next calls Wimp_Initialise to start up the task and get the task handle, and then loads the Template file.
Wimp events type 6, mouse clicks, are directed to PROCmouse_click. The window and icon handles where the click occurred are in the data block at offsets 12 and 16 respectively, so these are passed as parameters to the procedure. As there's only one window we are interested in if the window handle is anything else the procedure is simply exited. The CASE statement then directs the program to whichever procedure is appropriate for the button clicked. At this stage we have only implemented code for button number 4, Quit, so that goes to PROCdie. The other two buttons haven't had any code written for them yet so they're left blank.
The program begins by calling PROCsetup and then opens the window. It then enters a REPEAT - UNTIL loop. The UNTIL FALSE condition at the end of this loop effectively means 'carry on forever', so although there is an END statement after this the program should never actually get to this point.
The call to Wimp Poll takes two parameters. In R1 is a pointer to a data block in which parameters are passed to and from the Wimp, in this case I've used blk%. R0 would normally have a 'mask', which is a way of telling the Wimp not to bother returning from Wimp Poll unless something of interest to the program has happened. The 'things' the program wants to know about are set in this mask. The reason for this is to make the desktop run much faster by avoiding the need for the Wimp to waste time visiting a program when nothing of interest to that program has happened. We will go into this later, but for the present a value of 0 will assure that all events are passed to the program.
The event code returned by Wimp Poll in R0 is placed in event%, and the required action decided upon in the following CASE structure.
The finished program so far is in the BASICPRG archive in the SOFTWARE directory. At this stage it doesn't do very much. When you double-click on it the window will open and all you can do is drag it about the screen or click on the Quit button when it will do just that. Alternatively you can make it quit using the Task Manager or with the program described in Part 6.
This might not be very exciting, but then there are only just over 40 lines of code, and we have a program that can start itself up in the Wimp, open a window, respond to a mouse click, and shut itself down on request.
In the next instalment we will add the 'real' functionality and a few more refinements.