Beginner's Guide to WIMP Programming
Everything you need to know to start writing your own applications.
11: Windows Redrawn
Hopefully, you have been experimenting with the shell program from the previous section to create some applications of your own. In this section, we will start creating an application which involves the next major step in Wimp programming.
All the windows we've created so far have contained nothing but icons. This means that they can be redrawn by the Wimp, without any direct help from our program.
Windows frequently have to be redrawn; for example, when you scroll them, or when another window or menu is drawn in front of them, and then removed.
We're now going to look at the other kind of window - one which contains information that only our program can provide.
An Application Which Draws ShapesWe are going to call our new application !Shapes. Although you can find a copy of it in the files, read the following account of how to start creating it so that you will be able to create your own applications in the same way.
Start by making a copy of the shell application directory, with all its contents, calling the new directory "!Shapes". You'll immediately notice that it is represented in the Filer window by a standard application icon. This is because, although it has a !Sprites file, the file doesn't contain a sprite with the same name as the application directory.
You may wish to correct this before you go any further. You could create new sprites in the way that we saw in Section 2, or you may wish to borrow the ones in the files. At the very least, you will need a !Sprites file containing a sprite called "!shapes" and possibly a small version called "sm!shapes". You may also wish to create the other two files, !Sprites22 and !Sprites23, also as we did in Section 2.
Your new application should now be represented in the Filer window by an icon with your new sprite, though you may have to close and reopen the directory window, or wait until you first run your application, for this to happen.
Modifying the ShellWe now have to change the identity of the program so that it becomes our !Shapes application rather than the Wimp shell program.
Begin with the !Run file. Load it into your text editor and replace "Shell" with "Shapes" throughout, so that the file now looks like this:
|Run file for !Shapes Set Shapes$Dir <Obey$Dir> IconSprites <Shapes$Dir>.!Sprites WimpSlot -min 64k -max 64k Run <Shapes$Dir>.!RunImage
Note particularly that the system variable that we create is now called "Shapes$Dir". This is important because this variable is referred to within the !RunImage file.
Next, load the Templates file into your template editor and modify the details in the progInfo template to something more appropriate. The version of the window in the accompanying files looks like this:
Note that the blank box where the version number should be must be icon 0 so that the line in PROCload_templates which inserts the version number into that icon's indirected text string will work properly. Although the icon contains no text in the template, it should be indirected and its maximum text length set to 20 bytes. This will ensure that there is room for the version number text string in the indirected data buffer.
While you have the Templates file open, open the main window and change the text in the title bar to something appropriate, such as "Shapes window", then go to the Work area dialogue box. We want to set the window's work area so that it measures 1020 × 1020 units. To do this, leave the X0 and Y1 boxes set to zero, set the X1 box to 1020 and the Y0 box to -1020. Open the window to its full size with its toggle size icon, then close it and re-save the Templates file.
Modifying the !RunImage FileAt this stage, we're just establishing the new identity of our application and changes to the !RunImage file mostly consist of replacing "Shell" with "Shapes". As well as the REM statements at the start of the program, there is also the name passed to Wimp_Initialise, the menu title in PROCmain_menu and the system variable in PROCload_templates. Note that the sprite name in PROCcreateicon is all lower case.
What you now have is an application which calls itself !Shapes but is still just the Wimp shell program. Before we proceed further, we'll make a couple of small improvements to it.
The first is another new line in PROCinit, so that it now looks like this:
1420 DEFPROCinit 1430 REM initialisation before polling loop starts 1440 DIM b% 255,ws% 1023,menspc% 1023,stack% 1023 1450 wsend%=ws%+1024:stackend%=stack%+1024:stackptr%=stack% 1460 quit%=FALSE 1470 PROCload_templates 1480 PROCmenus 1490 !b%=main%:SYS "Wimp_GetWindowState",,b%:SYS "Wimp_OpenWindow",,b% 1500 ENDPROC 1510 :
The extra line, line 1490, may look familiar. It's part of the line in PROCmouseclick which is conditionally executed when we click Select on the icon bar icon and which opens the main window. The effect of including it in PROCinit is that the window opens automatically when the program starts.
For our second improvement, we have to refer back to Section 5, in which we laboriously constructed a window data block so that we could see what was in it. This is where we put some of that information to use.
If we click Select on the icon bar icon, we open the main window. If the window is already open, the click has no effect. What, though, if the window were covered by other windows? It would be convenient if a click on the icon bar icon would bring the window to the front. As things stand in the shell, this won't happen.
If we look at the window data block, we see that it includes the following:
This is part of the data block as it is when the window is first created. It is, however, also part of the first 32 bytes of the block which get modified when various things happen to the window, such as scrolling and resizing. The data must be available to the Wimp when we call Wimp_OpenWindow, which is why we call Wimp_GetWindowState first. This SWI fetches the information about the current state of the window, which is used to set up the data block, ready to be used by Wimp_OpenWindow. In this way, if the window is currently closed, it can be re-opened in exactly the same position and state as it was before. If the window is already open, the data fetched refers to the state of the window at the time, so Wimp_OpenWindow has no effect unless, of course, we modify the data before we call it.
When we call either of these two SWIs, the data block at b% starts with the window handle in the first four bytes, so the whole of the window data is four bytes further on in the block than it was when we created the window definition. This means that the "handle to open window behind" word which was at b%+24 is now at b%+28. If we call Wimp_GetWindowState while the window is open but not at the front of the window stack, this word will contain the handle of the window immediately in front of ours. If we change it to -1 before we call Wimp_OpenWindow. the effect will be to make our window jump to the front, which is what we want.
In our program, we change the line in PROCmouseclick to:
460 WHEN 4:!b%=main%:SYS "Wimp_GetWindowState",,b%:b%!28=-1:SYS "Wimp_OpenWindow",,b%
This gives us the first version of our new application's !RunImage file, which appears in the Steps directory within the !Shapes application as page_099. The modified Templates file is also in the Steps directory as "Templates2".
Windows That Need RedrawingWe will now change the main window of our application to one which needs to be redrawn by routines in our program.
Load the Templates file into WinEd, open the main window and then open the Edit window dialogue box. Find the "Auto redraw" option and turn it off. When you click on the "Update" icon, the window will become criss-crossed with diagonal lines. This indicates that the window needs to be redrawn by the program.
... the window will become criss-crossed with diagonal lines
Save the template file again, under its old filename. There is also a copy in the Steps directory, called "Templates3". Do not try to run your application at the moment, though, or it will crash.
Now load up your !RunImage file. When the Wimp wishes our program to redraw part of its window, it calls it through a return from Wimp_Poll with a reason code of 1. The first thing for us to do, therefore, is to add a line to PROCpoll to handle this:
270 DEFPROCpoll 280 REM main program Wimp polling loop 290 SYS "Wimp_Poll",,b% TO r% 300 CASE r% OF 310 WHEN 1:PROCredraw(b%) 320 WHEN 2:SYS "Wimp_OpenWindow",,b% 330 WHEN 3:SYS "Wimp_CloseWindow",,b% 340 WHEN 6:PROCmouseclick 350 WHEN 8:PROCkeypress 360 WHEN 9:PROCmenuclick 370 WHEN 17,18:PROCreceive 380 ENDCASE 390 ENDPROC 400 :
This almost completes our Wimp polling routine, though we could delete one line that we used earlier: we no longer need to look for a reason code of 8, as we will not be using keypresses in this application.
Drawing in RectanglesLine 310 calls the first of two new procedures:
2070 DEFPROCredraw(b%) 2080 REM redraws window contents 2090 LOCAL xorig%,yorig%,more% 2100 PROCget_origin(!b%,xorig%,yorig%) 2110 SYS "Wimp_RedrawWindow",,b% TO more% 2120 WHILE more% 2130 PROCdraw(b%,xorig%,yorig%) 2140 SYS "Wimp_GetRectangle",,b% TO more% 2150 ENDWHILE 2160 ENDPROC 2170 : 2180 DEFPROCdraw(b%,xorig%,yorig%) 2190 REM called when all or part of window needs redrawing 2200 REM xorig% and yorig% are coordinates of work area origin (top left-hand corner of window work area) 2210 REM b% points to block: 2220 REM b%!0 : window handle 2230 REM b%!4 : visible area minimum x coordinate 2240 REM b%!8 : visible area minimum y coordinate 2250 REM b%!12 : visible area maximum x coordinate 2260 REM b%!16 : visible area maximum y coordinate 2270 REM b%!20 : scroll x offset relative to work area origin 2280 REM b%!24 : scroll y offset relative to work area origin 2290 REM b%!28 : current graphics window minimum x coordinate 2300 REM b%!32 : current graphics window minimum y coordinate 2310 REM b%!36 : current graphics window maximum x coordinate 2320 REM b%!40 : current graphics window maximum y coordinate 2330 ENDPROC 2340 :
This new version is supplied as page_101. You can now run the program again without it crashing.
To follow what is happening in these procedures, you first have to understand what is involved in redrawing a window. The actual redrawing of information on the screen will be done by PROCdraw, which we've left empty for the moment, apart from a lot of REM statements which tell us what is in the data block when it's called.
We cannot simply draw our picture, or whatever is in our window, on the screen, even if we knew exactly where to draw it. The window may not be open to full size, or part of it may be covered up, and, if we're not careful, some of our drawing may go outside the window and mess up other parts of the screen.
Fortunately, the Wimp comes to our aid. The whole secret of drawing in windows is the graphics window, which has nothing whatever to do with Wimp windows.
While you were learning about Basic, you probably experimented with the MOVE and DRAW commands, which enable you to draw lines and other shapes on the screen. To refresh your memory, try the following simple exercise. If you are following this guide on the screen, you'll have to write down a few notes to refer to while you're doing it because you won't be able to see the desktop.
Start up Basic in its immediate mode by pressing F12 and typing:
Now change screen mode. If you're not sure which mode to select, just type:
which will reset everything to do with the screen but leave it in the same mode. Now type:
You should get a diagonal line, from the bottom left-hand corner of the screen towards the top right-hand corner. Our graphics cursor was set to coordinates 0,0 when we changed mode, which is the bottom left-hand corner, and we have drawn a line from there to 1280,1024. Of course, if you know the resolution of your screen, you can change these figures so that the line goes all the way to the corner, if you wish. To convert from pixels to graphics units, multiply the x coordinate by 2. If you are using a square-pixel mode, multiply the y coordinate by 2; if you're using a rectangular-pixel mode, multiply it by 4.
Altering the Graphics WindowEverything we put on the screen using the graphics commands has to fall inside the graphics window - anything we attempt to draw outside leaves the screen untouched. The graphics window initially covers the whole screen. Change mode again to clear the screen and resets the graphics cursor to 0,0, then type in the following:
Be sure to type semi-colons between most of the figures, as shown, and do not miss out the semi-colon following the last figure. These tell Basic that the numbers to be sent to the VDU consist of two bytes.
This alters the graphics window so that, instead of occupying the whole screen, it now occupies a rectangle whose bottom left-hand corner is at 300,200 and whose top right-hand corner is at 900,800. You can see it by typing:
This changes the graphics window's background colour, then clears the graphics window. Depending on how many colours your screen mode has, you may get a dark red or bright red background.
Now type the DRAW command again, exactly as before. You will find that the line only appears on the part of the screen which has changed colour, although you had drawn it across much more of the screen. The remainder of the line falls outside the graphics window, and so was not drawn.
One further experiment, before we leave this exercise. Don't change screen mode or alter the setting of the graphics window, but type:
You will see the Basic prompt appear at position 600,600 which is roughly in the middle of the screen. The VDU 5 command means that text will be put on the screen at the graphics cursor, not the text cursor position.
Now try typing in a line of text. When the text reaches the right-hand side of the graphics window, below where the line disappears, it will vanish.
Text entered at the graphics cursor also has to fall inside the graphics window for it to be visible. Any text that we put into a Wimp window has to be entered in this way. You don't need to send a 'VDU 5' to the screen, though; it's all taken care of by the Wimp. Just remember to use a MOVE command, instead of TAB, to position the text.
Divide and DrawNow to return to our program, and the two new procedures.
When we call Wimp_RedrawWindow, the Wimp sets up a graphics window for us to draw in. Unfortunately, it's not quite as simple as that. The graphics window is always a rectangle, but the visible part of the Wimp window may not be a complete rectangle, as something could be covering part of it, such as the corner of another window.
The Wimp gets round this problem by dividing our window into a number of rectangles which do not overlap each other, but which, together, cover the entire visible portion of the window. It then sets a graphics window for each one in turn. When we call Wimp_RedrawWindow, it returns with the first rectangle set up as a graphics window, and a number in R0, which we put into variable more%. This number is zero if this is the only rectangle to be drawn, and a different number if there is at least one more to follow.
We call our drawing procedure, PROCdraw, once for each rectangle that has to be redrawn. After each call, we call Wimp_GetRectangle. This sets up the next rectangle as a graphics window, and returns with a new number in R0, which we again put into more%, ready to repeat the process. As long as more% is not zero, there is another rectangle to draw. We may have to call PROCdraw several times if there are several rectangles; once for each one. It is now a matter of going round the loop, calling PROCdraw and Wimp_GetRectangle, until more% is zero (there is 'no more'), and the process is finished.
An Illustration of RedrawingWe can see this process in action by adding a few experimental lines to PROCdraw. It doesn't conform to good RISC OS practice, but it will let you see how the visible portion of the window is divided into rectangles.
This version is called page_104 in the Steps and makes the latter part of PROCdraw (after the REM statements) look like this:
2330 LOCAL t% 2340 SYS "Wimp_SetColour",(1<<7):CLG 2350 VDU 7:t%=TIME:REPEAT UNTIL TIME>=t%+100 2360 SYS "Wimp_SetColour",(1<<7)+7:CLG 2370 ENDPROC 2380 :
Every time PROCdraw is called, the current graphics window is turned white. The program then beeps, waits one second, then changes the colour to black. This slows down the redrawing process to one rectangle per second, so you can see it happening.
When the program first starts up, its entire window has to be drawn; hence the fact that the window turns first white, then black. The use of Wimp_SetColour instead of a GCOL command ensures that the appropriate colour numbers are used for black and white, regardless of how many colours are used in the current screen mode.
With these lines in place, run the application and try scrolling the window, or dragging other windows and menus across it. You will see various rectangles in the window change colour as the Wimp calls for them to be redrawn. The effect is particularly noticeable if you reduce the window to less than full size, open a smaller window on top so that it is completely surrounded by your window, then scroll your window.
This procedure is for experimental purposes only. It is very bad practice, because it holds up the operation of the entire machine for a whole second, every time PROCdraw is called, So when you've finished experimenting with it, delete lines 2330 to 2360, or go back to the previous version of the file.
... the current graphics window is cleared to a black background ...
As we shall see later, the data block after Wimp_RedrawWindow or Wimp_GetRectangle contains, among other things, the coordinates of the current graphics window. This can be useful in some applications - it gives you a chance to work out which parts of your window need to be redrawn on each individual call to PROCdraw, and not waste time redrawing all the detail in the window each time. We will not use this facility in the application we are going to write on this occasion, though. Let's try to keep things fairly simple.
Now we need something to draw.
Our New ApplicationHere is where we start creating our simple drawing application. We will not go in for Bézier curves, like Draw, but we'll settle for something that will let us draw lines and other shapes in different colours.
You may recall that MOVE and DRAW are special examples of the PLOT command: for example, PLOT 4,100,100 means MOVE 100,100, and PLOT 5,200,200 means DRAW 200,200.
Our drawing application will enable us to use PLOT commands to move, and to draw lines, circles and rectangles.
It is not simply a matter of drawing on the screen. All the details have to be stored in memory, so that the window can be redrawn, as required.
We will store each PLOT operation as a four-byte word, using the bits as follows:
It's a tight squeeze, getting an eight-bit PLOT code, a four-bit colour code and two coordinates into 32 bits. We have achieved this by allocating 10 bits to each coordinate, and using them to store a 12-bit coordinate, with the two lowest bits missing. This enables us to handle coordinates in steps of four, up to 1020, which explains why we reduced the work area extent of our window to this figure.
The x and y coordinates, by the way, refer to the position of a point in the window, not on the screen. We will store the y coordinate as a positive number, measured downwards from the top of the window, to make the maths easier. The position on the screen of a point with coordinates x%,y% can be found as follows:
where xorig% and yorig% are the screen coordinates of the window's work area origin. If you look at PROCredraw, you'll see that we call PROCget_origin to get these figures each time we redraw the window.
We now need somewhere to store the codes for these operations, so we add a further statement to the list of DIMs at the start of PROCinit, plus a new line:
1430 DEFPROCinit 1440 REM initialisation before polling loop starts 1450 DIM b% 255,ws% 1023,menspc% 1023,stack% 1023,list% 1023 1460 wsend%=ws%+1024:stackend%=stack%+1024:stackptr%=stack% 1470 quit%=FALSE 1480 !list%=&4067C32:list%!4=&9D043C32:list%!8=-1 1490 PROCload_templates 1500 PROCmenus 1510 !b%=main%:SYS "Wimp_GetWindowState",,b%:SYS "Wimp_OpenWindow",,b% 1520 ENDPROC 1530 :
This sets up a 1,024-byte buffer at list%, which will be enough to store 255 PLOT operations, plus a word containing -1. This will mark the end of the list of words, rather like the list of menu items returned by the Wimp when we click the mouse on a menu.
There are two procedures we have to write: one to interpret mouse clicks and put numbers into the buffer; the other to read the buffer and do the drawing on the screen. We will deal with the former later, but, so that our drawing procedure already has something to draw, line 1480 puts some numbers into the buffer for it to use for now.
Don't worry about how these numbers were produced for now. Just note that, at list% and list%+4, we have codes for two PLOT commands, and, at list%+8, a minus 1 to mark the end of the series.
Let's refer to this list of words as the Plot Instruction List.
Now for our drawing procedure, PROCdraw. The initial version of this routine (before our redraw experiment) did nothing, containing only REMs to remind us of the information being passed to it. We must now add commands which are specific to our application. The following listing also shows a new procedure which is called from PROCdraw, PROCplot_shape.
This new listing is in the Steps directory as page_108.
2190 DEFPROCdraw(b%,xorig%,yorig%) 2200 REM called when all or part of window needs redrawing 2210 REM xorig% and yorig% are coordinates of work area origin (top left-hand corner of window work area) 2220 REM b% points to block: 2230 REM b%!0 : window handle 2240 REM b%!4 : visible area minimum x coordinate 2250 REM b%!8 : visible area minimum y coordinate 2260 REM b%!12 : visible area maximum x coordinate 2270 REM b%!16 : visible area maximum y coordinate 2280 REM b%!20 : scroll x offset relative to work area origin 2290 REM b%!24 : scroll y offset relative to work area origin 2300 REM b%!28 : current graphics window minimum x coordinate 2310 REM b%!32 : current graphics window minimum y coordinate 2320 REM b%!36 : current graphics window maximum x coordinate 2330 REM b%!40 : current graphics window maximum y coordinate 2340 LOCAL coords%,colour%,plot% 2350 MOVE xorig%,yorig% 2360 coords%=list% 2370 WHILE !coords%<>-1 2380 PROCplot_shape(!coords%,x%,y%,colour%,plot%) 2390 SYS "Wimp_SetColour",colour% 2400 PLOT plot%,xorig%+x%,yorig%-y% 2410 coords%+=4 2420 ENDWHILE 2430 ENDPROC 2440 : 2450 DEFPROCplot_shape(word%,RETURN x%,RETURN y%,RETURN colour%,RETURN plot%) 2460 REM returns parameters of object to be plotted, decoded from word% 2470 x%=(word% AND &3FF)*4:y%=(word%>>12) AND &FFC 2480 colour%=(word%>>10) AND &F 2490 plot%=(word%>>24) AND &FF 2500 ENDPROC 2510 :
Before we look at this procedure, we have to understand what's in the data block at b% when we call PROCdraw from PROCredraw. The program has just returned from a call to either Wimp_RedrawWindow or Wimp_GetRectangle. In both cases, the contents of the data block are the same and are shown in the REMs between lines 2230 and 2330. The first seven words are the same as for Wimp_GetWindowState. PROCredraw also passes the coordinates of the window's work area origin, which it found by calling PROCget_origin at the start of the redrawing routine.
If we wished, we could work out which parts of the window to redraw by using the information in bytes b%+28 onwards, which show us the position of the current graphics window. In more complex programs, we could potentially save a lot of execution time by redrawing only the parts of the window that actually needed to be updated. We will not do this in our current example, however.
Carrying Out the PLOT CommandsWe will use a variable called coords% to hold the address of the word containing the details of our current PLOT command. Line 2360 sets it to the start of the Plot Instruction List, and line 2410 in the WHILE... ENDWHILE loop steps it through the list, carrying out PLOT commands, until it finds -1 indicating the end of the data.
PROCplot_shape extracts the required numbers from the word. A considerable amount of bit shifting and masking goes on here. Do not worry if you don't follow it all exactly - understanding it is not essential at this stage.
Having retrieved the numbers, we can now go ahead with the PLOT command, in line 2400. As we saw earlier, we turn the window coordinates into screen coordinates by adding them to or subtracting them from the position of the window's work area origin.
And that's all there is to it. If you have typed in the numbers correctly, you should have the equivalent of a MOVE command to 200,100, followed by a CIRCLE FILL command, and you should be seeing a light blue circle near the top left-hand corner of your window.
... a light blue circle near the top left-hand corner ...
So that's half the story over. We will shortly be looking at the procedure for turning mouse clicks into lines and shapes.
Before we can do that, though, we need a dialogue box.