Programming for the OS/2 PM in C:Window State & Some Graphics
by Rick Papo
Part V. - Window State & Some Graphics
So far, we have talked about how to create a standard Presentation Manager window. The sample program creates a window, paints the flag in it, and waits for any one of several commands to arrive through the menu or accelerator key mechanisms. It doesn't do much, and we will now begin to change it to be more useful and interesting.
Window State
The window we have created so far is a 'stateless' window. That is, its behaviour does not depend on any past events or memory of them. There are very few truly useful windows of this type. The example program we have been building up until now can do little but decorate the screen. The edit menu options can accomplish little either, except that Copy could possibly put a copy of the window contents on the system clipboard. For these reasons, we will change the nature of the sample program a bit: it will now become a simple paint program.
There are several ways for a window to remember its state. The simplest way is to declare static variables outside of the window message processor and use these. This works fine, provided you do not want to create a second instance of the window within the application. For instance, if you had created a button control class, and wanted to have more than one of them, each with its own state and characteristics, sharing a single set of state information declared outside of the window message processing function will not work easily.
There is a better way, however. When a window class is registered, it is possible to tell the system that every instance of the class is to have some extra memory allocated for its own private use. We will allocate space to store a pointer to the window's state information. Registering the client window class is now done like this:
WinRegisterClass ( Anchor, CLASS_NAME, MessageProcessor, CS_SIZEREDRAW, sizeof(void*));
Allocating space to store the address of state data does not allocate the state data itself, nor initialize it. This needs to be done while processing the WM_CREATE message, which is the first message a window receives upon being created. The state information itself can be allocated any way you want (malloc, new, etc), but its location is stored with the function WinQueryWindowPtr, in this way:
struct WindowState *State = whatever; WinSetWindowPtr ( hwnd, QWL_USER, State );
Having done this, the address of the state information must be retrieved before processing any particular message, in this way:
struct WindowState *State = (struct WindowState *) WinQueryWindowPtr ( hwnd, QWL_USER );
Enough of that. A slightly different way of doing this is illustrated in the sample code. The only real difference there lies in how the state information is allocated.
Some Graphics
In the previous example programs there was some simple programming to clear the window and paint the Hungarian flag on it. This month we will make something a little more difficult and useful: a simple window for drawing lines on with the mouse. This will also demonstrate a use for a window state information, as described above.
To draw graphics within a window on the display, the program must first get permission from the system to do so. This is done by obtaining a 'handle' to a 'Presentation Space' associated to your program's window. A Presentation Space (PS), in general, is a place to paint graphics on, and under the OS/2 Presentation Manager, a PS has (in general) nothing to do with the physical device that will be the eventual destination of the graphics. A PS, however, can be associated with a thing called a Device Context (DC), which describes the physical device. You can create a PS that is not associated with a DC, and establish the connection later, but this is what could be considered an advanced topic, and we will not try anything like that right now. For our purposes, there are two normal means of getting a Presentation Space, and which method you use depends on what you are trying to do. If your program is reacting to a system Paint request, the method required is like this:
RECTL Rectangle; HPS hPS = WinBeginPaint ( hwnd, 0, &Rectangle ); // Perform graphics here. WinEndPaint ( hPS );
When OS/2 determines that a portion of the window needs to be repainted, like when it is uncovered, or when the window becomes visible, or it gets expanded somehow, the message WM_PAINT is sent to the window message processor. The program must call WinBeginPaint to get a handle to the Presentation Space. If he wishes to know which portion of the window needs repainting, the address of a RECTL structure can be passed as the third parameter of the function. Any normal graphics setup or drawing function can be called afterwards using the returned handle (more about this in a moment). When the painting is complete, the program must call the WinEndPaint function to release the Presentation Space.
If the program wants to alter the window graphics under nearly any other circumstance, the following procedure is followed:
HPS hPS = WinGetPS (hwnd); // Perform graphics here. WinReleasePS (hPS);
To create presentation spaces for the printer, for a plotter, or for drawing on a bitmap held in memory, there is yet another method to be used, but we will not talk about this at this time.
Once you have a handle to the presentation space, you will might want to change some of the default setup for it. For instance, the default handling of colours works similar to the old 16 colour text mode of IBM PCs. That is, color 0 was black, color 1 was blue, and so on, to color 15, which was white. I prefer to work with true colours, which on PCs are specified in three-byte sets: one byte each for red, green and blue, which each byte able to have a value from 0 (fully off) to 255 (fully on). The function to do this looks like this:
GpiCreateLogColorTable ( hPS, LCOL_RESET, LCOLF_RGB, 0, 0, 0 ) ;
This function can be used for some fairly elaborate configuration of how the Presentation Space handles colours, and there some times when you want to use the parameters zeroed out here, but the sample program here is not one of those times. In fact, this kind of setup suffices for nearly all programs. The only ones which need something more complicated are those which must show many colours simultaneously and correctly (like Peter Nielsen's graphic image viewer, PMVIEW).
When the program is responding to a WM_PAINT message, the first drawing action it will probably want to take will be to clear the invalid rectangle to whatever the correct background colour should be. A good sequence of program code to do this might look like this:
GpiSetColor (hPS, RGB_WHITE); WinQueryWindowRect (hwnd, &Rectangle); GpiMove (hPS, (PPOINTL)&Rectangle.xLeft); GpiBox (hPS, DRO_FILL, (PPOINTL)&Rectangle.xRight, 0, 0);
The first statement sets the current colour to white, specified in Red-Green-Blue notation. The second statement asks the system for the size of the whole window. The third line moves the graphic cursor (an invisible point used for reference) to the lower-left corner of the rectangle, and the last line fills the rectangle marked by the graphic cursor at one corner, and the upper-right corner of the rectangle at the other. This may look rather strange, but it is a quick and simple method for clearing the window to a colour. The other suggested way to do this, the function GpiErase, has the drawback of not clearing the window to a colour that can be specified by the programmer.
We said we would be making the program into a simple paint program, one supporting straight lines that could be drawn by the user on the window with the mouse pointer. That means we need to know when the user has clicked the mouse button over the window, when he moves the mouse pointer, and when he releases the mouse button, and then record this information in the window state area. What's more, the window should remember all the lines currently drawn, just in case it needs to repaint them all for any reason.
A little bit of preparation and planning is necessary for all this. First of all, the window state information (where the list of lines will be stored) must be defined. After a little bit of thought, the following state structure was defined for this month's sample program:
struct WindowState { int ButtonDown; int NumberOfLines; POINTL Lines [MAX_LINES] [2]; };
The window needs to remember whether or not the mouse button is currently pressed (we'll see why in a moment), how many lines are currently drawn, and the starting and ending points of each line. Initially, the ButtonDown state is set to FALSE, and the number of lines to zero.
What we want to see happening is this: when the mouse button is pressed down over the window, we record the beginning point of a new line segment. The ending point is set to the same location. As the mouse pointer is moved around, the ending point gets updated continuously, and a tentative line should be drawn on the window. When the mouse button gets released, the line is completed and drawn. This sequence should be repeated for every button click, move and release sequence, until the program can store no more lines (see MAX_LINES, above). Let's see how we would implement this procedure.
When the mouse button is pressed down over a window, the WM_BUTTON1DOWN is sent to the message. The program can get the mouse pointer location from the message and use this to update the window state, like this:
POINTL Point; Point.x = SHORT1FROMMP (mp1); Point.y = SHORT2FROMMP (mp1); State->ButtonDown = TRUE; if ( State->NumberOfLines < MAX_LINES-1 ) { State->Lines[State->NumberOfLines][0] = Point; State->Lines[State->NumberOfLines][1] = Point; } /* endif */
The mouse pointer location is passed to the window procedure in the first message parameter, as two 16-bit numbers. The window state is updated to show that the button click has been detected. If there is room for yet another line segment, the starting and ending points of a new segment are set to the mouse pointer position.
As the mouse pointer moves around over the window, the message WM_MOUSEMOVE will be sent to the window message procedure. Like the WM_BUTTON1DOWN message, it contains the location of the mouse pointer, and this information is obtained from the message in the same way as before. The following program code would now be executed:
if (State->ButtonDown == FALSE)
return (0);
hPS = WinGetPS (hwnd);
GpiCreateLogColorTable (hPS, LCOL_RESET, LCOLF_RGB, 0, 0, 0);
GpiSetColor (hPS, RGB_BLACK);
GpiSetMix (hPS, FM_INVERT);
GpiMove (hPS, &State->Lines[State->NumberOfLines][0]);
GpiLine (hPS, &State->Lines[State->NumberOfLines][1]);
if ( State->NumberOfLines < MAX_LINES)
State->Lines[State->NumberOfLines][1] = Point;
GpiMove (hPS, &State->Lines[State->NumberOfLines][0]);
GpiLine (hPS, &State->Lines[State->NumberOfLines][1]);
WinReleasePS (hPS);
The first statement causes the mouse motion to be ignored if the mouse button is not currently pressed down. The next block of statements gets a handle to the window presentation space, sets the current colour to black, and sets the line drawing mode to 'invert', which means that any line drawn will invert every colour in its path. This has two useful effects: the line is visible along its entire length, regardless of the background colours, and if the line is drawn twice, the result is that nothing remains of the line first drawn. As the mouse moves around, the result is an elastic line which moves smoothly over everything below it, but leaves no trace behind as it moves onwards.
When the user releases the mouse button, the message WM_BUTTON1UP is sent to the message procedure. As before, the message contains the mouse pointer position, and it is extracted from the message in the same way as last time. The procedure for reacting to this message looks like this:
if ( State->ButtonDown == FALSE )
return (0);
hPS = WinGetPS (hwnd);
GpiCreateLogColorTable ( hPS, LCOL_RESET, LCOLF_RGB, 0, 0, 0);
GpiSetColor ( hPS, RGB_BLACK);
GpiSetMix ( hPS, FM_INVERT);
GpiMove ( hPS, &State->Lines[State->NumberOfLines][0]);
GpiLine ( hPS, &State->Lines[State->NumberOfLines][1]);
if ( State->NumberOfLines < MAX_LINES)
State->Lines[State->NumberOfLines++][1] = Point;
GpiSetMix (hPS, FM_DEFAULT);
GpiMove (hPS, &State->Lines[State->NumberOfLines-1][0]);
GpiLine (hPS, &State->Lines[State->NumberOfLines-1][1]);
WinReleasePS (hPS);
State->ButtonDown = FALSE;
As before, the first statement causes the rest to be ignored if the window never saw the mouse button getting pressed, and the second block of statements sets up the presentation space. The current elastic line is then erased. The final ending point of the line segment is updated from the pointer position in the message, and the number of defined line segments is incremented. Finally, the line-drawing mode is restored to the default condition, that of drawing solid lines, and the line segment is redrawn. That done, the presentation space is released and the window state is updated to show that the mouse button is no longer down.
If you combine all the above mouse message processing with logic to draw all the stored lines upon receiving the WM_PAINT message, we are nearly done for this month. Only one more problem, a rather obscure one, needs to be dealt with: what happens if the mouse button is pressed while the mouse pointer is over the window, and then the pointer is moved over some other window before the button is released. Without taking special steps, the window would never see the mouse button getting released. To fix this situation, the system function WinSetCapture is called upon detecting the mouse button down, causing the window to 'own' the mouse until it releases it, which will happen when the mouse button release is detected. Try clicking on the sample program window, keeping the button pressed, and moving the mouse outside the window, and see what happens now.