Previous: 3 It’s been a long road, getting from there to here ...
Next: 5 Going back to the File menu
In this section:
Every application I have written (bar one) uses the entire client area as one big graphics pane, or drawing surface. If I am in a hurry, then I usually make this a fixed size and with no graphical interaction, but when I am producing something more substantial, I make it re-sizable and with some degree of graphical interaction. In the following description, I would assume that the graphics are not re-sizable and have no interaction. Then, I will go on to explain how both of those facilities are implemented.
I have an acquaintance whose idea is to keep one half of a %gr drawing surface free of any detail so that his dialogs can pop up without obscuring any background. Finally, when his program reaches the end of the input stage and some analysis is done, the results can be presented in that available space. It’s a good idea, but it doesn’t conform to a normal Windows approach, which is to put the dialog boxes up above the master window and if the master window contains graphics, then the dialog boxes are movable so that they can be repositioned where they obscure less detail. The way Windows works is that anything which has been obscured is repainted after the overlying dialog is closed.
With a small amount of coding it is possible to make dialogs remember where they were when they were closed and the following time they are invoked to appear in the same place. How this is done is covered below in section 6.17.
In Example_01, we put in a simple %gr graphics area, initially coloured blue (so we could see where it was) sized 400 (H) x 300 (V) pixels. The coordinates in pixels of this area start in the top left corner, so the x coordinates increase from left to right and the y coordinates increase from top to bottom. This coordinate system is not one that lends itself to the imagination of many people, although professional programmers are used to it and find it less disconcerting. The coordinates start at (0,0) and finish at 399 and 299.
The graphics primitives in ClearWin+ are relatively few in number, and include:
In addition, a single pixel may be coloured, and the line style (dotted and dashed) and line width (in pixels) may be set. The names of the routines are self-explanatory because they all start with the word ‘draw’, for example: DRAW_LINE_BETWEEN@, DRAW_FILLED_POLYGON@, and so on. For the details of the routines please consult the online help file, where they are consecutive entries in the Library reference.
The colour used to draw any one of these primitives is set by the integer function RGB@ with parameters in the order red, green and blue, as in:
KOLOUR = RGB@ (NRED, NGREEN, NBLUE)
The parameters are INTEGERs with the value 0..255. The RGB colour model is additive, and so black is RGB@(0, 0, 0) and white is RGB@(255, 255, 255). If all the parameters have the same value, you get a shade of grey. The original versions of FTN95 were configured by default to use the VGA 16-colour model with all of those 16 colours named, and the RGB colour model could be set as an option. Some versions of FTN95 ago the default was changed. However, the old VGA colour model persists from time to time, for example when the initial colour of the %gr area was set to blue. (You can change back to the VGA 16-colour model using the routine USE_APPROXIMATE_COLOURS@, but I do not recommend it).
The simplest way to use the graphics primitives is with the use of pixel coordinates. If you draw anything which goes outside the size of the graphics area, it is simply ignored although it is drawn up to the boundary.
Here is an example of the code to draw a white flag with the cross of St George on it:
CALL DRAW_FILLED_RECTANGLE@ (100, 50, 300, 250, RGB@(255,255,255)) CALL DRAW_FILLED_RECTANGLE@ (180, 50, 220, 250, RGB@(255,0,0)) CALL DRAW_FILLED_RECTANGLE@ (100, 130, 300, 170, RGB@(255,0,0))
The coordinates would usually be variables rather than the constants I have used in the above. When you draw text, a text string may be sized and the font selected, but it is positioned relative to the top left of the character string. You can rotate text although the effect is often not very desirable, and works best with text that is rotated by 90° or is horizontal. You could add text to the flag with:
CALL DRAW_CHARACTERS@ (100, 260,'Flag of St George', RGB@(255,255,255))
and it would be left justified. To centre it would require working out the length of the text string using:
CALL GET_TEXT_SIZE@ ('Flag of St George', NPIX_X, N_PIX_Y)
where the last two parameters are the width and height of the text string in pixels. Then replace the 100 in the DRAW_CHARACTERS@ call with 200-NPIX_X/2.
My suggestion to you is that you include all the graphics with every variation all dealt with by only calling a particular routine with a set of integers (or ‘flags’) as to whether things may be drawn or not. If that makes the graphics drawing routine very long and complicated, there is no problem whatsoever about splitting the routine into a set of subroutines, but with that single point of contact, as it were. If you like MODULEs and CONTAINS, then you can have all your subsidiary subprograms subordinate to a master drawing routine.
Although the example made the graphics area initially blue, and drawing yellow on blue is a highly effective colour combination, as is white on blue, but red on blue (or vice versa) is a really bad combination, and black on a blue background does not show very well. You make your program conform much better to typical Windows standards if you use a white background. A black background makes your program look rather old-fashioned.
Quite often, the objects that are drawn exist in the real world with coordinates in metres, millimetres or feet and inches and some way of mapping these into the pixel coordinate system is required. The way I do it is to interrogate windows to find out the current pixel size of the graphics area into which I will draw my objects centred and scaled to fit. Out of the two possible scales with respect to the X size and Y size, I then have to choose the one that enables the entire object to fit. Then, knowing the real-world coordinate values for any point, I transform them to the pixel coordinates for plotting. Those pixel coordinates are always INTEGER.
The routine proceeds as follows:
Firstly, find the maximum and minimum of both X and Y coordinates, from which the range of those coordinates can be determined and also where the midpoint of the object to be drawn is located in real-world coordinates. The centre position of the graphics area is obviously at half of the maximum coordinates in X and Y.
IXRES = CLEARWIN_INFO@ ('GRAPHICS_WIDTH') IYRES = CLEARWIN_INFO@ ('GRAPHICS_DEPTH') N_PIXELS_IN_X = MAX (1, IXRES) N_PIXELS_IN_Y = MAX (1, IYRES) REAL_WORLD_X_RANGE = WORLD_X_MAX – WORLD_X_MIN REAL_WORLD_Y_RANGE = WORLD_Y_MAX – WORLD_Y_MIN WORLD_X_MID = (WORLD_X_MAX + WORLD_X_MIN) / 2 WORLD_Y_MID = (WORLD_Y_MAX + WORLD_Y_MIN) / 2 XSCALE = REAL_WORLD_X_RANGE / N_PIXELS_IN_X YSCALE = REAL_WORLD_Y_RANGE / N_PIXELS_IN_Y SCALE = MAX (XSCALE, YSCALE) MID_SCREEN_X = N_PIXELS_IN_X / 2 MID_SCREEN_Y = N_PIXELS_IN_Y / 2
To position yourself on the screen with your graphics nicely centred you must transform your real-world coordinates to pixels. I tend to do this by declaring two statement functions at the head of every graphics routine to do the transformation. Statement functions are a bit of a curse, but the code is included in line without you having to write it explicitly every time, and there’s no reason if you prefer to do it that way not to have the transformation done in other functions. I find statement functions mildly confusing and so ordinarily after I have included them, I make some very explicit comments in my source code to the effect that the following lines are in fact statement functions. Statement functions are in the sights of the Fortran committee as being obsolete or worse, but that just shows in my view just how much the Fortran committee really dislikes Fortran as it is and wishes that it was something else!
IPOSX(XX) = MID_SCREEN_X + (XX - WORLD_X_MID)/ SCALE + 0.5 IPOSY(YY) = MID_SCREEN_Y - (YY - WORLD_Y_MID)/ SCALE + 0.5
The addition of the 0.5 make sure that when the REAL result of the calculation is transformed to an INTEGER, it rounds up or down to the nearest one. You could, of course, use the function NINT (nearest integer).
It is possible to have more than one %gr area in your program. It is easy to forget that you may put one in a dialog box as well as having a big one in your parent window.
One use of another %gr area is to show a colour that the user selects by entering the red, green and blue values. You switch between them using the INTEGER FUNCTION SELECT_GRAPHICS_OBJECT@. This FUNCTION takes a single parameter, which is the user ID of the graphics area (UID). This UID is something that you choose and give to ClearWin+ when you put %gr into a WINIO@ call:
ID_SCREEN = 1 IW = WINIO@ ('%`gr[white]&', 400, 300, ID_SCREEN )
You will see that the grave accent has been used to tell WINIO@ that the %gr has a UID. Incidentally, the UID in the documentation (especially in older versions) is sometimes called a handle, and this creates confusion with other Windows controls for which Windows generates a handle when that control is created. The UID handle is an INTEGER that the user specifies. If there is only a single %gr, then you don’t need to bother with giving it a UID.
However, should you wish to produce hardcopy graphics, that drawing surface also counts as a graphics object, and so it is a good idea to get into the habit of giving each its own, unique, UID. Don’t give the same UID to two separate graphics drawing surfaces – which is basically what you are doing if you produce hardcopy while the screen is displaying graphics and you haven’t given a UID to either!
IRET = SELECT_GRAPHICS_OBJECT@ (ID_SCREEN)
The return value, IRET in the example, will be returned as 1 if the selection has worked, or 0 if it failed, for example by that particular Window having been closed.
In the very first versions of ClearWin (i.e. before the + was added), onscreen drawing areas had to be defined by the use of the %dw format code. In particular, the graphics primitives that were provided were evolved very slightly from the graphics supplied with pre-Windows versions of FTN77. %dw has been improved somewhat since those early days, but my advice is simple: don’t use %dw, use %gr. In the odd situation where you are trying to read a very early ClearWin+ program that does use %dw, think of it as a foreign language version of %gr! I also don’t5 use %dw.
Two comparatively recent developments with the graphics system in ClearWin+ are firstly the introduction of the ability to use the graphics primitives with REAL coordinates (which are DOUBLE PRECISION if you use the compiler directive DREAL), and secondly, the ability to use GDI+ instead of the simple GDI.
Using REAL coordinates somewhat simplifies the transformation from world to pixel coordinates. In particular, the addition of 0.5, needed to actually yield the true nearest pixel, is taken care of inside the graphics primitives.
The names of the graphics primitives with REAL coordinates are the same as the original names, with a ‘D’ (for DREAL or Double Precision) added before the @.
GDI graphics can sometimes look blocky, especially where the pixels on the screen in use are large, as in a large format but low-resolution display. The effect is best seen if horizontal and vertical lines are drawn, and then compared to one drawn at an angle – say 45 degrees – across the screen. In comparison, GDI+ uses antialiasing, where some extra pixels are drawn in a colour range midway between the foreground and background colours. Various sampling schemes can be selected. You would think that antialiasing would produce a fuzzy image, but the eye is fooled, and a better-quality visual effect is generally achieved, with the exception of one pixel thick lines that don’t look good.
Even though the subroutines work in terms of REAL coordinates that may imply fractional pixels, screens work in terms of INTEGER coordinates, and the resulting image will look the same – it just makes the coordinates-to-pixels conversion easier (allegedly). I say allegedly because if you do the conversion using statement functions, then the precise mechanics of the conversion is hidden in the source code (but you have to remember that the Fortran committee hates statement functions and they may be removed from a later standard).
When ClearWin+ was introduced as an enhancement to FTN77 in the early 1990s, personal computer graphics were slow, and high pixel-count screens were rare, but nowadays the graphics systems are fast and even the most basic screen is likely to have counted as high-resolution in the past. Therefore, although ClearWin+ has the facility to define a restricted area in which graphics are updated, its use is optional
The FTN95 helpfile points out that by default calling any graphics function does not result in an instantaneous update of the screen, although on modern computers it is generally quite fast. Instead, ClearWin+ waits until the current sequence of graphics calls from within your callback has been completed, (i.e. until the application is idle). However, calling the routine PERFORM_GRAPHICS_UPDATE@ will cause an immediate update. PERFORM_GRAPHICS_UPDATE@ is a subroutine, and as it has no parameters, it updates only the current drawing surface, meaning that if for example, there are 2 %gr areas, only the active one is updated, but of course that will be the one where the most recent graphics operations will have been drawn.
When your application is in an idle state or a call to PERFORM_GRAPHICS_UPDATE@ is made, changes to the graphics buffer are copied to the screen. When using %gr, ClearWin+ keeps track of the smallest rectangle to enclose the changed areas and updates only that region, although with modern graphics systems I have found it more convenient to update the whole lot, starting by blanking everything out with the appropriately sized DRAW_FILLED_RECTANGLE@ in white, and then calling the routine to redraw everything.
On the other hand, if you use %dw, ClearWin+ cannot automatically detect the smallest rectangle to be updated and so by default it updates the whole of the graphics region whenever a change is made.
If you wish to update only a small rectangular area of either a %gr or a %dw area, then you should make a call to the subroutine SET_UPDATE_RECTANGLE@ (IX1,IY1,IX2,IY2) in order to set the update rectangle limits (which are defined in pixels, so must be integer). Once used, this routine must be called whenever the update rectangle changes.
The helpfile advice is to remember to reset the limits after you have used the local update rectangle limits, but once you go down that road you have become addicted to it, and must remember to do it if the graphics area is resized! Alternatively, you may find that inserting periodic calls to PERFORM_GRAPHICS_UPDATE@ so that the graphic is updated as you go along. Personally, I prefer to draw everything and do a single update at the end, and think that the advice in the helpfile harks back to a time of much slower computers.
There are many issues with drawing text in Windows on any drawing surface particularly onscreen. ClearWin+ has many routines to do with drawing text. Life is easier with monospaced fonts, and more complicated with proportionally spaced fonts, but even so, a call to the subroutine:
FONT_METRICS@
will return 20 values for different parameters relating to the font!
An important thing to remember is that font selection and effects relate to the drawing surface and not to dialog boxes where the font selection mechanism is completely different. Because a proportionally spaced font has different widths for every character, and even these ratios will vary between fonts, then to determine how much space a particular character string will occupy you can use the routine GET_TEXT_SIZE@ which you feed with a string and it returns the width and depth in pixels that it will occupy if drawn with the current settings. The calculation includes any leading or trailing blanks, so the dimensions of the area occupied by the text itself must be determined by feeding the routine with only the relevant substring.
Text size on the drawing surface is set either by calling the subroutine SIZE_IN_PIXELS@ or SIZE_IN_POINTS@, both requiring height and width as integer parameters. The side effect of giving a width to a font is to possibly distort it, so be careful with that parameter. Note that when using GDI+, attempting to set the width parameter has no effect.
ClearWin+ does not have separate routines for writing numeric values and so these must be converted to strings by means of a WRITE statement that uses the appropriate character variable (LABEL) instead of a unit number, as in the following where NUMBER is the INTEGER to be turned into characters:
CHARACTER*(8) LABEL WRITE (LABEL, '(I8)') NUMBER
Text is positioned on the screen relative to its top left corner. This takes a bit of getting used to, as most people think of the critical position being the bottom left corner.
Oh Dear! There’s a problem here. If you start by drawing text, sure, it is positioned using the top left hand corner. But the minute you change some feature of the text, the location point moves to the bottom left hand corner of the string, neglecting any drops.
The basic graphics primitive DRAW_CHARACTERS@ draws characters using the current font which on entry to graphics is a system font that works quite well for labelling even at the default size and shape, and even then, a number of routines can affect how it is drawn, for example, using the subroutines:
ROTATE_FONT@, SCALE_FONT@, SELECT_FONT@, UNDERLINE_FONT@, BOLD_FONT@
ROTATE_FONT@ takes one parameter, which is the anticlockwise rotation in degrees from the horizontal.
SCALE_FONT@ and SCALE_FONT1@ each take a single parameter that is integer in the first case and real in the latter, They do what they say, scaling the font accordingly and although SCALE_FONT1@ has finer control everything is predicated by rounding to the nearest pixel and so the effects are only really obtained with very high DPI settings such as you get on printers rather than on-screen.
UNDERLINE_FONT@, BOLD_FONT@, ITALIC_FONT@ all take an integer parameter that is set to 1 to enable the effect, and when set to zero and the routine run again, the effect is cancelled.
SELECT_FONT@ allows you to select a font based on its name. Good advice is to take a great deal of care in this because fonts are not always available on every machine. If you specifically require a particular font then when your application is installed it should also install that font. By default, Windows’ system fixed font is used. Different versions of Windows use different fonts by default in dialog boxes, and these are found by reference to Windows documentation. If you check which version of Windows your program is running under then you have some certainty that the appropriate font will be found on the system. These default font names for the most recent versions of Windows are listed below.
Windows version | Default proportionally spaced font |
1,2, 3 | Helv |
3.1 to 98 second edition | MS Sans Serif |
2000 & XP | Tahoma |
Me | MS Sans Serif |
7 | Tahoma |
Vista, 8, 8.1, 10 | Segoe UI |
The change in default font has very small effects, but it does change the appearance of dialogs and may result in some misalignment. A useful device is to include Segoe UI in the installation package for you application, and stick with that, or be prepared to do an update of your software for new Windows versions.
It is possible to allow user selection of a font, its attributes, and its colour. This selection requires the subroutine CHOOSE_FONT@ with a parameter for the colour selected by the user from the standard Windows dialogue that allows the user to choose a font. The attributes of italic, or underlined can be selected by the user and when that is done, the current font changes. However, the colour selected by the user is returned as the integer value equivalent to that which you would get from RGB@.
If it is required to return to the original font then before calling CHOOSE_FONT@, the ID of the font can be found with GET_FONT_ID@ and reset with a call to SELECT_FONT_ID@.
My own Windows applications regard the choice of fonts as something that I do programmatically, and users do not have access to this capability. However, if the application is more about text and its display, then this is a very useful facility.
One of the reasons that you may wish to resize a graphics area is when the user resizes the parent or master window of the application. First of all, consider how that is enabled. The fixed size window from HOMER.FOR and can be made into a re-sizable window by adding the format code %ww or %sy. The former is used with a master window, and the latter with a dialog box. You also have to modify %gr to tell it how to handle graphic interaction. ClearWin+ offers two possibilities in how to handle resize events. The method that I recommend and deal with here is to handle the resize yourself
The format code %ww has lots of possible options, but used without any of them, it puts minimise and maximise icons on the caption bar adjacent to the close box. The window is then also resizable.
To make the %gr drawing surface resizable, you have to give it a pivot (Windows terminology, sorry). You do that by preceding the %gr with the format code %pv. That means that when the window containing the graphics object is resized, the graphics object will shrink or grow with it. Basically, if you don’t redraw the graphics, that means that you may lose part of the graphics on the right or bottom of the area, or you may find that some white space appears to the right or beneath your graphic. That behaviour may well suit you, but if it doesn’t and you want to resize the graphic when the graphic area changes size or shape, then you have to provide a graphics callback function.
IW = WINIO@ ('%pv%`^gr[white,user_resize]&', 400, 300, ID_SCREEN, & KB_GRAPHICS )
Note that in the above, the code would have gone over the 72 character limit and so I have gone to a continuation line and done so without breaking up the callback function name. I have also used the & that is preferred these days as a continuation marker. In free format code, continuation is signified by having an & at the end of one line and at the beginning of the following one, and you can make even fixed format code dual-purpose by adding an & in column 73 of the line to be continued. That may be something you feel like doing when the code is stable!
A typical, but minimalist, graphics callback function may look something like this:
INTEGER FUNCTION KB_GRAPHICS() C ============================== INCLUDE <WINDOWS.INS> CHARACTER*(20) REASON REASON = CLEARWIN_STRING@ ('CALLBACK_REASON') IF (REASON .EQ. 'RESIZE') THEN IXRES0 = CLEARWIN_INFO@ ('GRAPHICS_WIDTH') IYRES0 = CLEARWIN_INFO@ ('GRAPHICS_DEPTH') IXRES = MAX (1, IXRES0) IYRES = MAX (1, IYRES0) CALL PLOT_ROUTINE (IXRES, IYRES) KB_GRAPHICS = 2 RETURN ENDIF END
CLEARWIN_STRING@ is a CHARACTER FUNCTION which returns a text message that gives the reason why the callback was invoked, in the case of a resize, it is ‘RESIZE’ (surprise, surprise!). Immediately, we must find the current size of the graphics area, check that no dimension is zero (which would throw the scaling), and then rescale and replot, which I have suggested here is done in the SUBROUTINE PLOT_ROUTINE. Sometimes I find it better to put the scaling in a separate routine from the plotting, and to pass appropriate sizes and scale factors via COMMON. The choice, really, as always, is yours.
I put the RETURN inside the loop for a good reason: it could have been omitted there, and then under modern Fortran rules would not have been required at all. However, a graphics callback function will also need to handle various mouse clicks and actions where the user interacts with the graphics area and in that case, I find it very desirable to complete the actions as quickly as possible and to exit the graphics callback function without needing to trawl through a lot of other possible actions that have already been shown not to apply.
ClearWin+ can open a drawing surface for you to produce OpenGL graphics. The format code is
%og. OpenGL graphics can be rather beautiful but programming that system can be a trial. Besides which, OpenGL is not ClearWin+, so programming it is something you should learn elsewhere. Possibly online. For most purposes, GDI is fine, and GDI+ is even better - sometimes. The problem with both GDI+ and OpenGL is that single-pixel wide lines look rather ‘weedy’ because of the anti-aliasing or smoothing algorithm. Many of us will find that GDI is good enough. I certainly do. I recommend learning how to use (or implementing) the fancy graphics systems to a later date.
A great milestone is to be able to read one of those old-fashioned datafiles, run your program and get the results, then draw something that shows those results in the appropriate context.
I once had a program that did my analysis using data on punched cards on one mainframe computer, with its results transferred via paper tape to a different mainframe computer that could provide graphics output. A second program not only read the paper tape, but also had its own data file containing instructions on what to do with the information on the paper tape. Part of the reason was that computer A (an ICL 1900 series machine) would run the analysis, but computer B (an Elliot/NCR 4120) had a hardcopy graphics plotter while computer A didn’t Sadly, computer B wouldn’t run the analysis program because the program took up too much memory – computer A stored the executable in a different way. The whole procedure took a couple of days, because cards and tapes had to be couriered across not just building to building but also across the town of Kingston upon Thames. It is all doable on a single PC nowadays, and not only that, the run time is negligible. What is more, the analysis and the graphics output can all be implemented in a single program. It therefore makes sense to do the following.
Steps 4 and 5 can be repeated as often as I like. The way to do the above procedure is to have a graphics subprogram (or set of subprograms) that in principle can draw everything, along with a set of switches so that most options are initially turned off. An example screen grab from such a program is given below in Figure 4.1, where before doing the screen grab, I moved the dialog from centre screen to near the menu bar. The Options menu launches this dialog, and the Plot menu command makes a hard copy.
To program such a dialog yourself, you need to master the following ClearWin+ facilities:
We will get to all of these in due course, but there’s nothing to stop you looking them up in the online help file even now.
Another way of doing it is to have the plot options somewhere in the input file, but then if you want to change them you still need the appropriate dialog(s) – or change the input file and run the program again, which is rather old-fashioned, and certainly not user friendly.
Assuming that you got your program to draw something in a fixed size %gr drawing surface, the exercise recommended at this stage is to make the main program window resizable, give the drawing surface a pivot and a graphics callback function. Once you have done that, you can countenance adding a dialog to allow the user control over what is plotted. Hardcopy we need to leave until Chapters 11 and 12.
It’s probably about time to revisit the File menu, and make that old-fashioned externally-prepared datafile user selectable from a list in standard Windows fashion. This may mean having a look back at Chapter 2.
Once you have completed the drawing part, we move on to saving your files with any changes – something that you need to master before you think about implementing mechanisms to make those changes.
Footnotes:
5. In fact, I did use %dw originally. It was very primitive then, and it rather put me off the whole of ClearWin+ for getting on for a decade! When I returned to the fray, there was %gr, thank heavens.
FORTRAN and the ART of Windows Programming, Copyright © Eddie Bromhead, 2023.