Previous: 6 Entering and editing data
Next: 8 Bitmaps and icons, cursors and other resources
In this section:
By graphical interaction I mean responding to mouse operations taking place in the area occupied by a %gr drawing surface. The ability to have this interaction is part of what makes the Windows graphical user interface both graphical and user-centred.
It isn’t necessary to have a full graphical interface in certain classes of program, even though they present an image within a drawing surface. For example, imagine that the drawing is of an object that has different temperatures over its surface. You might wish to draw line contours of those temperatures, or coloured contour bands. In that case, you could switch between the line contours or the contoured bands with a selection in the menu bar. All that would be necessary in that case is to redraw the object and the surface contours in the appropriate format as a response in the callback to those menu items. You don’t necessarily need to do anything directly on the drawing surface.
The way that graphical interaction is programmed is via the mechanism of the callback to the graphics drawing area. If a lot of different types of interaction occurs, then this callback function can become extremely long and complicated. What is fairly important is to exit the callback as quickly as possible in the event that there is nothing to do, or in any case, as soon as possible.
Most Windows mice have at least three buttons: left, right, and centre, with the centre button usually provided with a scroll wheel. A button can be pressed and released which constitutes a single click, or two clicks in close succession to signify something different. A click may be accompanied by one of the keys on a keyboard being pressed at the same time, and facilities are made within ClearWin+ for the keys Shift or Ctrl (or indeed, in combination) to be pressed along with a particular form of mouse click.
All of the above can be styled mouse events. They are all static events, because the mouse pointer (ideally) is not moved while the buttons are depressed and released, but there are also dynamic events, which occur when the user moves the mouse while holding one or more buttons in the down position. With a computer that has a touch sensitive screen, various gestures that the user may make are interpreted into the equivalent mouse movements. A final type of dynamic event is when the user rotates a scroll wheel. In order to respond to mouse events, the user must decypher within the graphics callback what sort of event it is responding to. ClearWin+ has two categories of a response to a mouse event, a simple but standard mode and a more complete and complicated mode, the latter being selected when the %gr is specified with the optional parameter full_mouse_input.
In the standard mode, the mouse events are filtered and presented to the callback only in terms of clicks associated with each of the three buttons, or as a double-click on the left button. In full_mouse_input mode, the graphics callback is presented with many more mouse events in a sort of torrent, and it is left to the graphics callback to discard meaningless events. This need to discard many of the mouse events is a significant drawback to using the more complete mode, but even the standard mode has its drawback and that is that a double-click event is also preceded by a left click event, which means that the left click has to be associated with a fairly benign action or one that is undoable if it is followed by a double-click event.
In standard mode, the reason for a graphics callback having been made is determined by means of the same mechanism used to determine if the callback is in response to the resize event:
= CLEARWIN_STRING@ ('CALLBACK_REASON')
The responses will be one of:
MOUSE_LEFT_CLICK
MOUSE_MIDDLE_CLICK
MOUSE_RIGHT_CLICK
Or
MOUSE_DOUBLE_CLICK
The next step is to determine the position within the drawing surface of the mouse pointer at the time the button was clicked. The mechanism for this determination uses the CLEARWIN_INFO@ function as follows:
IXP = CLEARWIN_INFO@ ('GRAPHICS_MOUSE_X') IYP = CLEARWIN_INFO@ ('GRAPHICS_MOUSE_Y')
To obtain the equivalent coordinates in the real world system I do a reverse of the original scaling function:
RX = (IXP-ixres/2)* SCALE + p2x RY = p2y - (IYP-iyres/2)* SCALE
to sort out the button state, consider a little bit of code as follows:
IFL = CLEARWIN_INFO@ ('GRAPHICS_MOUSE_FLAGS') IF (and(IFL,MK_SHIFT) .EQ. MK_SHIFT) THEN ISHFT = 1 ELSE ISHFT = 0 ENDIF IF (and(IFL,MK_CONTROL) .EQ. MK_CONTROL) THEN ICTRL = 1 ELSE ICTRL = 0 ENDIF ! some users may like less indentation for ENDIF !
On review of this book, the question was asked as to whether the indentation for ENDIF is correct. The code fragment was extracted verbatim from a working program, and that is what it is there, including the indentation of the whole IF sequences. I’m not a fan of indentation anyway, much preferring blank lines. You must program in your own preferred style – my rules are often honoured more in the breach than in the observance!7
In the above fragment of code you will see another of my programming styles in action, and that is while I always try to program in capitals if I want something to really stand out I do it in lowercase. Apart from this single usage in sorting out the various flags, I never did bitwise operations and the use of lowercase for the name of the ‘AND’ function highlights to me that it is something out of the ordinary. Apart from ClearWin+, I also never use external functions or subroutines except in WINIO@, but I’ve highlighted those by pre-fixing the names with my special character sequence KB_. Silverfrost generally highlight their own functions and subroutines by finishing the names with the ‘@’ symbol. There are multiple ways of drawing your attention to these things. The values of MK_SHIFT and MK_CONTROL are defined in WINDOWS.INS
What you do in response to that particular click and button combination is then entirely up to you. What I do is to jump to the appropriate bit of code and execute it.
The welter of mouse events that cause the invocation of graphics callbacks have to be dealt with as quickly as possible. A simple way of dealing with them is to record the previous event’s parameters and compare them to the current event. If nothing has changed, then the current event can be discarded and control return using the return code 2. If, on the other hand, something has changed, even then it is possible that no action needs to be taken other than to save the current event’s parameters and wait for the next mouse event. When something changes that creates the need to respond, then you must determine what the appropriate response is and do it.
The simplest way to select an item is to move the mouse pointer onto the item, and click the left button. In standard mode, that produces a MOUSE_LEFT_CLICK event, but in full_mouse_input mode, may create several events, first as the mouse pointer is moved, then as the left button is depressed, and finally as the button is released, followed by a set of events that tell you where the mouse pointer is located.
Let’s consider the standard mode first, in the context of that network problem. Suppose that the nodes were marked by a 5 pixel square, then the click would be ‘on’ the node if it was anywhere within plus or minus 2 pixels of the node’s pixel coordinates, which might require the callback function to work through a list of those coordinates to find the one that is most likely and to select that. If the node was identified as lying within a circle, then the formula would probably be a little more complicated.
It will also be possible to select one of the links in a ‘nodes and links’ graphic by moving the mouse pointer approximately onto it and again clicking the mouse left button. You could differentiate between selecting a node and selecting a link by using different mouse buttons, or simply by where on the drawing surface the mouse pointer was when the left button was clicked. You could, for instance, select a node with a left-click, and a link with a right-click or a double-click. If you were in standard mode, then you will only be able to select something with a double-click if the left-click did not do anything because you weren’t close enough to a node for example.
The strategy must be left to you. However, it normally requires a great deal of thought because if you considered adding a link, then you would for example imagine that a left-click to select one node followed by a right-click to select the other would define a new link. That would leave you the problem of how you created a new node. Perhaps that could be a left-click but with the shift key depressed. Then, you have two problems: firstly, that you cannot define the coordinates of that new node to better than a single pixel resolution on the screen, and secondly, it will start life unconnected to anything.
As far as the connection is concerned, that may well require you to inhibit any file saving until it is connected, or to ask the user via a dialog whether or not unconnected nodes should be
simply discarded when the file is saved. That option might even be something to set up in a menu - probably one where the top-level option is simply Options. The choice is always yours.
As far as the coordinates are concerned, any reverse scaling from pixels back into real world units will inevitably give you a very long set of decimal digits. I have one program where the user can enforce rounding, say to 0.1m, and another option is simply to stick with those values for the time being.
What I described in the previous paragraph is free selection, where all the alternatives are available using a combination of a particular mouse button press and one or both of the modifier keys. There is an alternative that enables the use of the particularly common clicks for multiple purposes. I’ll call that tool selection.
The way that tool selection works is that you have a (graphical) toolbox, and that it has ‘tools’ that we can imagine at the moment include ‘Select node’, Select link’, Create node’, ‘Create link’, ‘Delete node’ and ‘Delete link’. You could set these up as menu commands, but using a graphical toolbox would be better. In principle, you click first on the tool, and that activates what happens with your next click. For example, if you chose ‘Select node’ then your next click would be identified with the next nearest node on the drawing surface, or if you weren’t close enough then nothing would happen and you could go on until you actually selected a node.
A toolbox is a special case of a toolbar that I will describe in two chapters, 9 and 10. You cannot have a graphical toolbar of any sort until you have drawn or otherwise acquired the necessary graphical bitmaps to depict the tools although Chapter 9 does describe text toolbars that do not necessarily need the descriptive graphic. Unfortunately, although they are a good first step, text toolbars ordinarily just don’t look good enough for a finished application.
Sometimes an on-screen graphic is simply too congested, or the detail is too fine, and the user needs to zoom in to gain some clarity in what they are viewing. Then, if they want to see something that is offscreen, the image needs to be panned, or moved side-to-side or up-and-down, or perhaps even a bit of both. The issues therefore are how to specify how much to zoom in or out, what area to display, and how to actually code that, with an equal set of problems relating to the issue of panning. You can get ideas on how to design the interface by seeing how it is done in other applications, decide which you like, and follow that lead.
Take, for example, Microsoft Word, which I am using to write this. My document is A4 portrait format, and when I am zoomed in to see what I have written, I only see part of the height of the current page. On the right side of the client area, there is a scroll bar (although the scroll bar disappears when it isn’t being used), and which moves the page up or down. Zooming is done with a slider control located in a status bar. If the page gets wider than the available drawing surface then a second scroll bar appears so that the user can also scroll sideways.
A second paradigm is to use the scroll wheel on the mouse to zoom in or out, and a click-and-drag to move the image around, although by way of contrast, there is a third paradigm where the mouse is used to block-select an area which is then zoomed to fit and fill the drawing surface.
Once one of these (or anything else, for that matter) is chosen, then the issue becomes one of how to implement it, with a major issue being whether to have the zoom and pan functions permanently enabled (as in free selection), or only to enable then following selection of a toolbox button or tool. The second of these options fits best with ClearWin+’s way of working, especially if the block select approach is chosen.
When zooming in, you have a choice as to whether or not to scale things defined ab initio in terms of pixels, such as line thicknesses and font sizes. In the case of line thicknesses, many line drawings work well with a small range of line thicknesses even when zoomed in. Text heights are determined by the settings you choose with the subroutines:
SIZE_IN_PIXELS@ SIZE_IN_POINTS@
For some applications it may be worth not scaling the text height, for example where a plethora of labels obscures what each one is, and zooming in without changing the font size allows you to read those labels, whereas scaling the fonts would simply enlarge the overlaps.
When you set the size in points, remember that the so-called logical DPI (dots per inch) setting is 96 for a standard screen and more for the large fonts settings, but that does not change when you zoom in. Both of the sizing routines affect both the height and width of characters, and you need to be careful not to stretch characters too much laterally or the effect is strange and sometimes ugly.
Zooming is comparatively easy to implement, simply by changing the scale factor SCALE and then replotting the whole graphic. Panning is done by again replotting, but after changing the real-this is world coordinates that correlate with the drawing surface centre. Part of the plot will overspill the extents of the drawing surface but will not appear on screen as ClearWin+ does all the necessary cropping automatically.
My personal preference is not to keep block selection available always by specifying it as an option with the relevant %gr format code, but to enable it when it is required, usually following a click on a toolbar icon. A second icon on the toolbar is required to return the graphic to being entirely displayed with in the drawing surface.
A good toolbar icon for zooming is the magnifying glass, a la Sherlock Holmes, although some applications use it for an ‘object inspector’ (also relevant to the great detective). Personally, I think that the object inspector icon should be a silhouette of Holmes in his iconic deerstalker hat rather than just his magnifying glass. An icon for panning is usually the ‘grabber hand’. On completion of implementing things from this chapter you will have created an enhanced graphics callback function for the drawing surface on your programs client area so that you can zoom in to an area that has been block selected. Initially this action will be linked to a menu item Zoom, although later it will be relocated to a toolbar icon. The milestone is reached when your graphic can be zoomed and restored to its original size.
I have given a complete example program for graphical interaction in Appendix E, which draws a rectangle with some control points that you can select and drag to change the shape. It is worth typing that program in and running it, then programming some of the additional features suggested in the Appendix.
The Appendix example uses some nice cursors that you can download from the internet for free, although there are plenty of standard cursors available in Windows and ClearWin+ if you prefer. The use of cursors is covered in the following Chapter.
Sometimes you want a mouse click on an object on the drawing surface to bring up a menu. There is a specific control for that which is defined with one of the options to %pm, with a callback that includes the routine DISPLAY_POPUP_MENU@. All the options must be text, and it takes a right-click in a %gr drawing surface which has FULL_MOUSE_INPUT set in order to to invoke it. If that sounds complicated – it is.
The fundamental problems with the approach are manifold. Firstly, the WINIO@ call is horrendously long and complicated, and secondly, it needs FULL_MOUSE_INPUT. Perhaps thirdly, it works with a right mouse click, and you might want that for something else. There is a fourth issue, and that is that all the options are text items. It is certainly not something that I would recommend in the early stages of program development. On the plus side, those pop-up menus look great, and conform to the standard current Windows appearance.
The explanation for pop-up menus is covered in the online help files. You can get this style of pop-up menus to appear anywhere, although they won’t pop-up over an actual control.
When I wanted pop-up menus in response to mouse clicks on a drawing surface I wanted them to respond to a double-click, and also – my fault – wasn’t using FULL_MOUSE_INPUT. Understandably, they didn’t work. Instead, I developed my own alternative. Here is an example with a segment of code taken from the graphics callback routine:
JXP = MIN (IXP+40, IXRES-125) ! so it isn't off-screen JYP = MIN (IYRES-200, IYP+50) IGNORE_GMOUSE = 1 IA=WINIO@('%ww[no_maxminbox,no_caption,no_border,volatile]&') IA=WINIO@('%sy[toolwindow]%bg[btnface]&') IA=WINIO@('%sp&', JXP, JYP) WRITE(DUMMY,'(I2)') NREGION IA=WINIO@('%cnSoil No. '//DUMMY//'%bx%dy&', 1.0D0, 0.2D0) IA=WINIO@(' %12`~tt[Modify Line]%nl&', IGREYMODI) IA=WINIO@(' %~12tt[Remove Zone]%nl&', IGREYREM) IA=WINIO@(' %12tt[Add ceiling line]%nl&') IA=WINIO@(' %12tt[Split Zone]%nl&') IA=WINIO@(' %12tt[Add floor line]%nl&') IA=WINIO@(' %12tt[Zone colour]%nl&') IA=WINIO@(' %12tt[Properties]%nl&') IA=WINIO@(' %12tt[Help]%nl&') IA=WINIO@(' %12tt[Deselect Zone]') IGNORE_GMOUSE = 0
IXP and IYP are the pixel cords of the mouse, IXRES and IYRES give the size of the screen. JXP and JYP give the coordinates for the top left corner of the pop-up window. The format codes %ww and %sy set the style of the window. %bx and %dy give a line under the heading, and two of the options can be greyed out. IGNORE_GMOUSE is set to 1 while this dialog is displayed just in case another mouse event is detected – at an early point in the callback routine its value is checked and if it is set to 1 then there is an immediate return. The layout looks best with a single space leading in each WINIO@ format string.
If the user selects any option, then the WINIO@ return code IA is checked and the appropriate action taken. The results looks like the following in Figure 7.1 (where the red is simply the background colour from the screen grab). Although this example is composed entirely from %tt (textual toolbar buttons) format codes, it is possible to use data input boxes and other controls, although I do caution against too much complexity.
Under Windows XP I used the option CASTS_SHADOW, but as the versions of Windows progressed the result became less and less pleasing and in the end ClearWin+ no longer supported the option. VOLATILE means that the pop-up disappears if you move the cursor off it, and TOOLWINDOW means that the window does not show up in the Windows taskbar. This approach doesn’t need FULL_MOUSE_INPUT. As these pop-ups are in all respects simply ordinary dialogs, then all the formatting options are available.
If you don’t use FULL_MOUSE_INPUT, then detecting a double-click also detects a left click before and after it, which means that if your application does have a programmed response to a double-click then what it does to a single left-click has to be pretty benign. It probably means that your application has a greater need for right-clicks and so their use for standard ClearWin+ pop-up menus has to be avoided.
If, on the other hand, you do use FULL_MOUSE_INPUT, your graphics callback routine will get drowned in mouse events, which means that you have to very early on in the callback filter out all the mouse events where the mouse pointer hasn’t moved at all (or not by more than a few pixels) and the button settings haven’t changed. Note that in the previous section I have used the IGNORE_GMOUSE flag to throw away mouse events I don’t want.
It also gets quite involved to detect a double-click for yourself, although you can experiment with the delay between clicks.
Footnotes:
7. Hamlet, apparently. Act 1, scene iv, if you must pursue the origins of the phrase.
FORTRAN and the ART of Windows Programming, Copyright © Eddie Bromhead, 2023.