10: A Simple Wimp Shell Program
We have now covered enough of Wimp programming for you to be able to write your own fairly simple Wimp-based programs. You found out how to create a window containing a variety of icons by using a template editor and how a menu can be created from a set of DATA statements, provided that you have a function for doing this in place. You also saw how the program may respond to a mouse click on a particular icon or a particular menu selection. You can now create procedures which do something when you do these things to produce a multi-tasking application of your own.
You'll be wanting to go ahead and experiment but, if you were to start from scratch with a blank file, you would have a great deal of typing to do before the program did anything worthwhile at all. The !RunImage file of our !Test application had 236 lines of Basic by the time we'd finished with it (including REMs and spacers between procedures), and it never did very much. Fortunately, you don't need to type in all that lot again.
A Foundation For Our ProgramAs you looked at or typed in the listings in the previous sections, you probably realised that many of the routines were not specific to that particular application but would be the same whatever Wimp-based program you were writing.
PROCmouseclick, for example, checks the window and icon handles and the button state every time you click the mouse. You would use a similar routine whatever your application was doing, and just make alterations so that it called the appropriate procedures according to what you did with the mouse.
We're now going to look at a shell program, on which you can base your applications. All you'll need to do is to add the new routines that make your program do its job, and the appropriate lines in the shell's routines to call them.
As it stands, the shell is an application that puts its icon on the icon bar, and gives you a menu with two items - Info and Quit. Info leads to an Info box. It's not a lot of use on its own, but it contains a lot of the necessary routines to do more complicated tasks. It also acts as a reminder of how to do various things such as loading a window definition from a template file and creating a window from it. It does have a main window which beeps when you click the mouse over its work area but this is just included for demonstration purposes. The procedures and functions have been rearranged so that most of the ones which are more likely to be modified are near the end of the program to make them easier to find.
Using the Shell ProgramTo create an application, copy the shell, renaming it appropriately. Design yourself a new sprite, remembering to give it the same name as your application (all lower case and not forgetting the '!'), and modify the !Run file, as appropriate. You can then start work on modifying the !RunImage file. (If you're just doing a quick experiment for your own purposes, you could, of course, leave the name as "!Shell" and not bother with a new sprite. If you do this, just copy the shell to another directory and work on the copy.)
Your application will be a stand-alone program, requiring no additional software to run it, apart, of course, from RISC OS, with its Basic interpreter, which is built into the machine.
You will notice that the version number at the beginning of the program is now a string, instead of a REM statement. That's so it will be incorporated into your Info box automatically - we don't want to have to redesign the box every time we write a new version of the program! Line 1270 in PROCload_templates does this for us.
To use the shell, of course, you have to understand how it works. That has been the whole point of this guide up to now, and the shell contains many of the functions and procedures which were in the !Test application. No new concepts or routines have been introduced, with one exception. We will, however, be developing several new techniques later on, which will feature in a more advanced version of the shell, presented near the end of the guide in Section 21.
Creating the Shell Program YourselfThis first, simple version of the shell is provided with the software that accompanies this guide, but to save space it is not listed here. Should you wish to type it in for yourself, just enter the longer version listed in Section 21. (The functions that you don't recognise will of course be discussed in forthcoming sections of this guide.) You can start off with the !RunImage file from the existing !Test application if you want to save yourself some typing.
You will need to create a new !Shell application directory in which to save the new !RunImage and other files. If you are starting off with the !RunImage from the !Test application, be sure to change all references to 'Test' so that they become references to 'Shell'. The search and replace functions in Edit (or your choice of text editor) will make these changes easy to do.
Your !Shell application directory also needs a !Run file, which you can also copy from the !Test application. Again, remember to change references to 'Test$Dir' into 'Shell$Dir', and make the appropriate change to the comment line at the start, then save it in your new directory. Design an appropriate sprite with the name '!shell' and save it in the shell directory under the filename '!Sprites'. For completeness, you may also like to include an Obey file called !Boot; it should contain the single line:
A Memory StackThe one new concept that the simple shell introduces, and which we will use in subsequent sections of this guide, is the way in which we allocate blocks of memory to procedures and functions that need them temporarily. This usually happens when they call a SWI such as Wimp_GetPointerInfo which requires a data block.
In our test program, PROCget_origin and PROCmenuclick used a data sub-block. This consisted of the upper part of the 1024-byte block pointed to by b%, and was itself pointed to by local variable c%. This technique worked well when we only used it in two routines, neither of which called the other one, but think what would happen if we had a lot of procedures and functions, all using the same bit of memory to store data. If one routine called another, the second routine might overwrite the data being used by the first routine, which would obviously cause the program to malfunction.
What we need is a block of memory from which a routine can claim a portion when it needs it, and give it back when it's finished with it. If, in the meantime, another routine needs some memory, it claims a different part of the block. If we arrange things properly, neither routine need know about the other one, which will make them easier to write. We call this sort of arrangement a stack.
Making Space For Our StackFirst of all, we need a block of memory to act as our stack. This is set up in PROCinit which, in the shell, now looks like this:
1410 DEFPROCinit 1420 REM initialisation before polling loop starts 1430 DIM b% 255,ws% 1023,menspc% 1023,stack% 1023 1440 wsend%=ws%+1024:stackend%=stack%+1024:stackptr%=stack% 1450 quit%=FALSE 1460 PROCload_templates 1470 PROCmenus 1480 ENDPROC 1490 :
Note that this is one of the routines which have been moved to the end of the !RunImage file.
As well as allocating 1024 bytes to the stack, pointed to by stack%, we've also reduced the size of the Wimp_Poll data block pointed to by b% to 256 bytes. This is the size of block officially required by Wimp_Poll. We don't need it to be any larger now because routines such as window creation can use memory from the stack instead.
In line 1440, we create two more variables. We set up stackend% to point to the end of the stack. The stack pointer, stackptr%, always points to the first unallocated byte in the stack. We increase stackptr% when we allocate memory to a routine and decrease it when the routine gives memory back again. If stackptr% is ever increased to a higher figure than stackend%, it would indicate that the program had tried to use too much memory from the stack and an error message would result. If you were working on a program and this happened, you would need to modify line 1430 to increase the amount of memory allocated to the stack and make a corresponding modification to stackend%.
Claiming and Releasing Stack MemoryTo see how the stack is used, take a look at a new version of PROCget_origin, which now looks like this in the shell,:
510 DEFPROCget_origin(handle%,RETURN xorig%,RETURN yorig%) 520 REM returns coordinates of window work area origin 530 LOCAL c% 540 c%=FNstack(36) 550 !c%=handle% 560 SYS "Wimp_GetWindowState",,c% 570 xorig%=c%!4-c%!20:yorig%=c%!16-c%!24 580 PROCunstack(c%) 590 ENDPROC 600 :
We use a local variable, c%, to point to the block of memory that we get from the stack. Doing this means that we can use the same variable name for this purpose in all routines that use stack space. This makes it easier to remember what we're doing.
Line 540 claims 36 bytes from the stack by calling FNstack and passing the number of bytes we need as a parameter. This is a new function in the Wimp shell. It returns the address of the start of the block allocated to this procedure. When we've finished with the block, in line 580, we call PROCunstack, passing the address of the block which we're relinquishing.
Let's look first at PROCstack:
610 DEFFNstack(size%) 620 REM allocates temporary memory from stack block 630 REM stack must be cleared after use with PROCunstack 640 IF stackptr%+size%>stackend% ERROR 1,"No room in stack" 650 stackptr%+=size% 660 =stackptr%-size% 670 :
Before this function is called, the stack pointer, stackptr%, points to the first free byte in the stack. All that this routine really has to do is return this address to the calling routine as a pointer to the block that the routine may use, and also increment the stack pointer by an appropriate amount so that it points to the first byte after the end of the block it's just allocated. It will then be ready in case another routine calls for some memory before the first routine has finished. Before it does this, though, it checks to see whether or not it's about to allocate memory outside the stack block, which we set up in PROCinit, due to too many routines wanting too much stack memory at the same time. Line 640 checks for this eventuality and issues an error message if it happens, to warn us to modify the program.
Line 650 increments the stack pointer and line 660 works out what the previous value was so that it can be returned when the function exits.
Returning memory with PROCunstack is an even simpler process:
680 DEFPROCunstack(old_ptr%) 690 REM removes temporary memory from stack 700 stackptr%=old_ptr% 710 IF stackptr%<stack% stackptr%=stack% 720 ENDPROC 730 :
We saw how line 580 in PROCget_origin called this routine, passing it the address of the block which it had finished with. All we need to do is set the stack pointer back to this value, ready for the same bit of memory to be re-allocated to the next routine that wants it. Line 700 does this and the following line checks just in case a routine tries to give back more memory than it claimed. This might set the stack pointer to a lower value than the start of the stack block, which would mean that the next routine to call for some memory would get a portion that it wasn't supposed to use, possibly overwriting a Basic variable.
In the next section, we'll start to use this shell program to create a more complicated application.