Advanced GPI: Retained Segments and Transformations
Written by Steve Lacy
When you say "Graphic Program Interface" what first comes to mind is a set of about a dozen and a half system calls to draw lines, boxes, circles, and other kinds of graphic elements. But, I may ask you, did you know that by using OS/2 Presentation Manager, you could refresh your entire window, whatever you draw, with one call? Did you know that you can take graphic "segments" and do things like rotate them, translate them, and scale them, without actually having to write the code to do the matrix math yourself? Well, that's what we're going to be talking about. There is a whole host of functions that are built into the GPI of OS/2's presentation manager that perform these features, plus more. Unfortunately, these functions are not normally used by the common user, and that's why I'm going to be talking about them here. This is an introduction to the Advanced GPI functions that many of us don't usually think about.
Just as an introduction, I'm assuming that you've programmed OS/2 presentation manager programs before. Therefore, I'm not going to go into the specifics of the calls such as WinCreateStdWindow, or any of the other Window functions, since this article mainly deals with Gpi functions. But, if you've never programmed PM before, I do think that you should still be able to follow this article through to the end, and by looking at the source code, which I hope is clear and straightforward, you should be able to pick up with what I'm talking about. So, I would also recommend that you have a copy of the source code around while you're reading this, since I'll be talking about distinct parts of the code, and to get a better view of what's going on, you should see the context of how the call is used.
First, we're going to talk about presentation spaces. What exactly is a presentation space and how can we use it to help us? Very generally, a presentation space is the part of the system that takes your calls to the GPI functions, and generates the pictures that you see on the screen. In most cases, this mapping of graphic calls to displayed results is normally a pass-through operation, that is, the things that you do aren't retained in memory, other than the results that you see on the screen. This is the default behavior of the presentation space that is associated with the client window that would be created by a WinCreateStdWindow call, this type of presentation space is called the "Cached Micro-PS." But, there are other types of presentation spaces, the most important of these being the "Normal PS." What a Normal PS allows you to do is take the graphic calls that you're sending it, and remember them for later operations, like refreshing the window, or rotating your objects, or whatever you like. A Normal PS also supports correlation functions that will let you know if any of your graphic segments pass through a certain rectangle of the screen, that you define. This correlation is wonderful for user interface tasks, such as clicking on an object to select it. We'll see how to use these functions later in the article.
The program that is developed here displays nine objects on the screen, each of which is its own segment in the presentation space. By clicking on an object, with either the right or left button, you can select an object. When an object is selected, you can use the '+' and '-' keys to rotate it, or drag with the right mouse button to translate it. The '=' key does the same thing as the '+' key, for ease of use.
Since we know that the default Presentation Space create with the WinCreateStdWindow call isn't a Normal PS, we have to figure out how to create the PS that we really want. Looking through our list of the GPI functions, we might take notice of GpiCreatePS.
HPS GpiCreatePS(HAB hab, HDC hdc, PSIZEL pszl, LONG loptions)
"hab" is the Handle to our Anchor Block that we got when we did our first WinInitialize call. This should be readily available as a global variable to our program. HDC is the Handle of the Device Context that is associated with a window, or in layman's terms, a handle that describes what type of device we're working with. This is readily available by calling the WinOpenWindowDC function. So, all we need now are the pszl that defines the size of the presentation space that we want to create, and some options. Looking at the functional description of the call, we see that if we specify (0,0) as the size of the PS we're creating, a default size for our device will be used instead. So, we're left with some options, that describe what type of PS we're going to be creating. In our case, we want the Page Units to be pixels (since we're drawing on the screen), we want a Normal PS, and we want to associate the PS with the hdc that we're dealing with, so we tell it to automatically do this for us. So, our final version of the call ends up being:
sizel.cx=0; sizel.cy=0; hps=GpiCreatePS(hab, hdc, &sizel, PU_PELS | GPIF_DEFAULT | GPIT_NORMAL | GPIT_ASSOC);
And, we can use this HPS throughout our program, as long as we follow a couple simple conventions, the main one being that we destroy it at the end of our program. Normal PS's take up a lot of memory, and you don't want to be using them where you don't have to. Use a normal PS only when you want to use the functions that go along with it. You'll quickly run out of memory if you use a normal PS for every window that you create.
Looking at our WM_CREATE message in our client window procedure, we see the above call to GpiCreatePS, along with a couple other suspicious looking calls, the first of these being the GpiSetDrawingMode command. Before explaining the call, I'll explain a little more about the process that will be going on in the program.
We know that we have now a Normal PS instead of a Cached Micro PS and we know that we can have retained graphics using this normal PS. Well, it would seem fairly obvious that there has to be some way of more readily organizing the information (graphics) that we're going to be retaining. What the Gpi can do for us is break our individual calls to Gpi primitives into larger blocks, each of which is a segment. Each segment has a "tag" that goes along with it. This "tag" is just a number that we assign to the segment so that we can remember which one is which. The segments also have numbers, and we'll order them sequentially. In our program, the segment number and the tag are the same number, for simplicity's sake. Note that to do the functions that we're doing, each segment must have its own unique nonzero tag. Along with the tag, a segment transform matrix is also stored. If you're not familiar with computer graphics and transform matrices, just think of this as something that will specify the position and rotation angle of the segment. It can also specify the scaling value of a segment, but we won't deal with those calls in this example program. The calls to the scaling routines are very similar to the ones for translation and rotation, so if you have a reference manual that describes these functions, you should be able to use them with no problem. So, we've got our segments. For this program, we're going to have nine segments. Two will be five pointed stars, one a square with rounded corners, one an oval, and the rest will be squares. They'll be arranged in a 3x3 grid on the screen. We also must mote that matrices in OS/2 are mainly composed of fixed point numbers instead of floating point. This allows the Gpi to not require a floating point coprocessor and still run at a reasonable speed on slow computers. Thus, there is a macro called MAKEFIXED that takes two arguments, an integer portion and a fractional portion of the number. As a little background, to represent a number as a fixed point, call our number 'n'. You would use the following code:
i=(INT)n; f=(n*65536)-((INT)n*65536); fixed=MAKEFIXED(i,f);
So, back to talking about the GpiSetDrawingMode command. What this does is tell our presentation space to do one of the following things: Draw only, draw and retain, or retain only. We're going to be using the retain only function, so that we can enter the segments into the presentation space, then draw them ourselves with other specialized calls later. So, we use the call
To set the drawing mode. The other suspicious looking call is the GpiSetPickApertureSize call. I'll come back to this later. What it does is set the size of the rectangle that we'll be using as our correlation area.
Now, the last thing that's there is the SetupSegments call. This is a procedure that was written to draw the nine segments, and set their initial segment transform matrices so that they're not rotated, and so that they're arranged in a 3x3 grid. Looking at this procedure, we see some calls that look somewhat unfamiliar. GpiSetInitialSegmentAttrs. Oh, this must have to do something with the segment attributes. Yes, it does. Every segment has some flags that go along with it, this just tells the system "for every new segment that you make, set these options from now on" The options that are most commonly used are:
- ATTR_VISIBLE The segment will be visible -- i.e. will be drawn
- ATTR_DETECTABLE The segment will be detectable by the correlation functions
- ATTR_CHAINED The segment will be in the segment chain. The segment chain is the list of segments that will be drawn by the GpiDrawChain call. So, in our case, we want all of our segments to be in the segment chain.
Now, the second unusual looking call is the GpiScale call. What we're doing here is initializing the variable 'identity' so that it contains the identity matrix, so that we can set our viewing transformation to be the identity. What this means is that if we say our square is 30x30, it will come out as 30 pixels on a side, exactly. So, we call GpiSetViewingTransformMatrix with the appropriate arguments. The one that might look suspicious is the second one, the 9L. This specifies that there are nine elements in our matrix. This is because for two dimensional transformations, you deal with 3x3 matrices. The rest of the code is organized as follows:
GpiOpenSegment(); GpiSetTag(); GpiSetColor(); GpiTranslate(); GpiSetSegmentTransformMatrix(); ... Draw our segment ... GpiSetModelTransformMatrix(); GpiCloseSegment();
What we're doing is keeping a counter of the segments that we've drawn, so that each segment will have a unique number. We open that segment, set its tag, and its color. The GpiTranslate call sets the matrix 'translate' to be matrix that will translate the image drawn from (0,0) to the point specified in ptlTranslate. Then, using the GpiSetSegmentTransformMatrix call, we set the matrix for the current segment, to be the translation matrix that we just computed. Along with the individual segment transform matrices for each segment, there is a model transform matrix which is applied to all segments drawn. When a segment is drawn, its transform matrix gets multiplied to the model transform matrix, resulting in an accumulation of the transforms. To get around this effect, at the end of each segment we set the model transform to be the identity. We then close the segment we're working with.
So, after the SetupSegments call, we've got nine retained segments, each with its own individual transform matrix to put it in the right spot on the screen. The hard part is through. We've already drawn the innards of our segments, an operation that only has to happen once, and we've also set their initial values, something that only has to happen once. From here on out, its just the accumulation of the deltas that make up the other operations we can do on the segments.
Looking at the WM_PAINT message, which is the next things that happens, we see that it is very short and concise. We erase the presentation space, set the foreground mix to XOR mode, so that we can easily erase a segment once we've drawn it, then we call the infamous GpiDrawChain, which draws all the chained segments, along with applying their individual transform matrices. Note that the WinBeginPaint call in our WM_PAINT message is probably a little different than those that you've seen before. Because WinBeginPaint usually returned a hps, but we have a PS already, we have to tell it that, so the second argument to the call is our current hps. This way it doesn't get obliterated by the new PS that would have been created.
The next message that I'll look at is the WM_BUTTON?DOWN messages, they're both the same, so I'll just talk about WM_BUTTON1DOWN. What we're going to be doing here is correlating the retained segments to see if we've clicked on one or not. To do this we have to do a couple of setup things first. Intuitively, we have to set the size and position of the rectangle that we're going to be correlating with, then do the actual correlation. Because the size of the correlation rectangle doesn't change throughout the program, I've placed it in the WM_CREATE message, so it only gets executed once. Take a look at the code, it sets the variable 'size' so that its 10 pixels square, then calls GpiSetPickApertureSize. This calls are fairly obvious, first the hps, then the options, then the pointer to the variable 'size'. The option PICKAP_REC tells the Gpi system that we want to use the value specified in the third argument, as opposed to using some default value. Now for the position of the pick aperture. Back to the WM_BUTTON1DOWN message, we see that the variable 'ptl' is set to be the current position of the mouse, and we call GpiSetPickAperturePosition. Its arguments are self explanatory. Next comes the neat call, the correlation.
lNumHits=GpiCorrelateChain( HPS hps, LONG lType, POINTL pptlPick, LONG lMaxHits, LONG lMaxDepth, PLONG alSegTag);
lType specifies what type of segments you want to correlate with. In our case, we want to do all segments, so we specify PICKSEL_ALL. The pptlPick argument is again the position of the pick aperture. lMaxHits and lMaxDepth together specify the maximum number of hits that will be recorded by this function. In my case, as in most cases, both of these arguments will have the value one, since we only are interested in one segment at a time. The PLONG alSegTag is an output array of segment number and tag value pairs. For example, on return, if we've hit a segment alSegTag will be the segment number of the selected segment, and alSegTag will be its tag value. The routine returns the number of segments hit, so in our case, this will be either zero or one, and we're only interested in the case where its nonzero. If the call returns nonzero, we set our variable 'selected' to be the index of the currently selected segment, and continue, otherwise we set 'selected' to be zero, to signify that nothing is currently selected.
Now, when we get a WM_MOUSEMOVE message, and when button two is down, we do some more magic. What we want to accomplish here is to figure out how far the mouse was moved at each step, and then add that increment to the current position of the currently selected segment. The first thing that we do is draw the segment in XOR mode, which will erase it from the screen. We compute the position delta, and then use GpiTranslate to get the matrix that represents that translation. Using GpiSetSegmnetTransformMatrix with the TRANSFORM_ADD parameter, we then add this delta translation to the total translation. The TRANSFORM_ADD parameter tells the Gpi to post-multiply the matrix onto the current matrix. Because we want all rotations to come before all translations, we post-multiply translations, and pre-multiply rotations. The last thing that we do here is draw the segment again, so that it appears in its new place. The GpiDrawSegment call draws just one segment, which is specified as the second argument to the call.
The last part of our program that we haven't looked at yet is the WM_CHAR message. It too is fairly simple. It sets the rotation angle to be 3.0 degrees, then if the '-' key has been pressed, negates this value. It then pre-multiplies the right matrix onto the segment transform matrix with the GpiRotate and GpiSetSegmentTransformMatrix calls. Note that the parameter to GpiSetSegmentTransformMatrix is TRANSFORM_PREEMPT.
I've described all the Gpi functions that allow you to create segments, and modify the segment transform matrices so that you can rotate and translate them. Hopefully I've given enough background so that you can write your own programs that use these advanced Gpi functions, along with the others that deal with retained segments and segment transform matrices. In most cases, these calls aren't as hard as they seem, and it is guaranteed that using these calls will both save you programming time, and increase the speed of your own code. Have fun!