Previous: 4 Graphics in the client area
Next: 6 Entering and editing data
In this section:
In Chapter 2, I left the File menu details hanging, having only dealt with Open. So what about Save and Save As ? What about New and Print? These are the first things that I am going to deal with in this chapter. Should you wish to follow my suggestion that you divide your source code between a number of files, each based on top-level menu items, then if you haven’t already done so it is a good time to start.
Remember, that every menu item at the lowest level will require its own callback function. Top-level or higher-level menu items that lead to further sub-menus do not get their own callback function for the obvious reason that when selected they do not do anything other than show the next level down in the menu structure.
As pointed out several chapters ago, there are at least six items in a Windows file menu, and once you go down the route of building the structure of that particular menu in order to run a test you must have at the very least dummy callback functions in your source code so that when the program is compiled and linked there are no missing references. My advice is to quite definitely advance slowly and try not to write too much code before you can test it. That simplifies debugging.
My suggestion is that for the time being you leave the callbacks for New and Print as dummy routines that do nothing except return the value of 2. New requires you to write a number of dialogs involved in the creation of a new dataset, and Print requires consideration of what you actually want to produce in the way of hardcopy. These issues will be dealt with later in the book.
The option Save in the File menu is intended to allow you to save changes that you have made to your data set into the file that it originally came from. In the simplest way possible, all you need to do is to keep the file open, perform a REWIND operation on it, and write the dataset in its entirety back into the file. If you do that, then you do not need to bother about a new OPEN statement, but at some point you do need to concern yourself with whether or not the file is read-only.
The other problem that will come up and bite you is that if the data originally came from a removable drive, and the user has removed the media, then your program will crash. If you read it from somewhere that was write-protected, then your program will crash. Since part of the Windows paradigm is that the program should never crash, it’s the reason why I originally suggested that after reading your data off the file you use a CLOSE statement. It does mean that you will have to go through the rigmarole of reopening the file, and part of that is checking that the file is still available, and that it is (in this case) writable. The Fortran OPEN statement does have various things in it that will allow you to field certain errors but there are better ways of checking that the file is still available, and if it is not, allowing you to do something about it.
Right from the time of FTN77, the compiler writers provided some SUBROUTINEs and FUNCTIONs to do various things that weren’t in the Fortran standard. If I am allowed a small rant at this time, then I will say that the originators of FTN77 had discovered that the biggest problem with Fortran 77 was that it had no graphics and no way of generating a reasonable user interface, as the various things like READ, WRITE and PRINT were fairly hopeless. Even the current Fortran standard has not provided either of those two rather essential facilities, which is why FTN95 is so brilliant.
A function that has been present in FTN since those early times is the LOGICAL FUNCTION FEXISTS@ , which allows you to check that a file exists. I would recommend using this function before you respond to a Save request. it isn’t just a matter of checking that the file exists before opening it, it’s a question of finding out whether the user really did mean to save over the top of the existing data set. You need to give the function your file name and its complete path and it will return the value .TRUE.- if the file exists - and .FALSE. for a variety of reasons including that you set a wildcard and more than one file matched it or if any other kind of error occurred. Do be aware that there is possibly an obsolete version of this function with the more rational name FILE_EXISTS@. However, even if a file actually exists that is no guarantee that you can write to it, as it might be in a read-only folder or even the file itself might be read-only. In that case you need to use the standard Fortran intrinsic INQUIRE, documented in full in the online help file.
An anecdote about why this is important is that I once wrote a program for student use on field courses when the computers were not attached to the University network. In their wisdom, the IT technicians had made all the computers check periodically whether or not they were connected to the University network, which they obviously could not be where there were some distance away from the University and being used on a field course. If the computers found that they were not connected in the appropriate way they would shut down, which was hard lines on the student using the computer wondering what the heck happened. Incidentally, the check was whether or not they were connected to a segment of the University network where none of the students ever went! It resulted in a sort of shouting match, as a result of which the IT section re-imaged all the machines. Whether or not it was done deliberately, we’ll never know, but the default saving location for student files was write-protected. That would have been fine if everybody knew that was the case because instead of using the saving function they should have used Save As and saved their work to a removable USB drive. However, it caused a second lot of mayhem (and a continuation of the shouting match)!
It is possible to determine whether a file is write-protected by using another of the FTN95 special routines, this time one called FILES@, which returns a whole lot of information including the date and time the file was created and various other pieces of information. If you find that a file is write-protected, then that probably means that the original creator does not intend it to be overwritten and you should then give the current user the opportunity to save the file with a different name or in a different place, or, you need to reset the file attributes with the routine SET_FILE_ATTRIBUTE@. I will leave you to explore these particular facilities which you will find in the help file for FTN95.
The point is that you do need to make sure that the file exists, and can be overwritten, and if not, then do something about it in the way of letting the user make sensible choices from there on.
Save does not need your program to use the standard Windows file selection dialog, unless something has gone wrong, and the user has chosen to save the file to another location, but that is Save As.
If the user has selected Save As this generally means that it is wished to save the current dataset to a different filename and therefore one of the first steps must be to launch the standard Windows file selection dialog, but this time with the file parameter set to 0, because it does not matter whether the file exists or not and the dialog is equally at home with the specification of a completely new filename. If the user has selected an existing file name, then it is very sensible to ask them whether they are content for the original file contents to be overwritten.
It should be fairly obvious that what is actually written to a data file or ’document’ is the same regardless of whether Save or Save As was initially invoked, and the WRITE statements can therefore go into a subprogram that is called from either of the callback functions.
There is one very obvious drawback to using the standard Windows file selection dialog, and that is because the acceptance button is labelled Open, whereas you might wish it to be labelled Save. In that case you should use the standard callback ‘FILE_OPENW’ as this labels the acceptance button appropriately. It is still necessary to do the checks as to whether the file is writable, and then to connect it to a Fortran unit number via an OPEN statement.
The traditional approach to writing a data file is to prepare it with a text editor that emulates the former use of a card punch or tape punch, so that the numeric values are laid out as plain text. Whether or not the data set is read using formatted input or unformatted, plaintext is always used when the user creates a data file. The problem with a user-prepared plaintext data file is that the user can make mistakes and if one of those is encountered when the file is read, then without the necessary END= and ERR= lines in every READ statement, any error will terminate the program with a Fortran error message and that is not the way that Windows programs are expected to operate. Even when a data file is created by a program, should it be in plaintext then the user may edit it and thus introduce errors.
To make your Windows program stable, then the use of END= and ERR= is essential when reading a plaintext data file. Moreover, the format statements must provide fields that are long enough to contain the largest conceivable number to the requisite degree of precision and should always ensure that there is an appropriate separator between the data items.
Rather than to give further advice I think it is probably better that I give an example where I got it wrong. For a student survey, I was saving coordinates in a file. These coordinates were in metres and had three decimal places. For over 30 years, student field courses had used a local coordinate system for their surveys where the maximum coordinates were measured in thousands of metres. All coordinates were positive and to allow 4 digits before the decimal point and three after, a format of F8.3 would have been perfectly adequate. But, in order to provide a substantial separator, I had used F12.3. One of my colleagues then made the unilateral decision to use UK National Grid coordinates which as well as metres involved hundreds of kilometres and therefore also filled the format completely, removing the separator, so that files could not be reread unless they were edited by hand. The lesson to learn from this is to anticipate such things and program accordingly.
The problem of the survey coordinates would have been resolved had the program saved its data in an unformatted file, and unformatted files have the advantage that they will not be edited by a user who does not possess some fairly detailed knowledge and skills. The downside of using unformatted files for saving data is precisely the difficulty of reading them outside the application that created them all without the knowledge of how they are structured.
My recommendation is to stick with plaintext Fortran formatted files while you are developing your application and only consider changing things if you really don’t want any easier editing of those files later. Even then, you may still want plain text, and if you use it, please make sure that the data fields are adequately separated. This means explicitly providing separators in the FORMATs, and not just relying on the format being larger than the anticipated value.
The menu command Save is intended to overwrite the original dataset. Some programs, including my favourite, CorelDRAW!, take one level of backup when a file is saved. After several saves, the original dataset is long gone. An accounting program that I use takes 10 levels of back up, which is rather better at not losing your work. How you implement backups is up to you and I just raise it here because it can be an issue.
If you consider each change your program will eventually do to the dataset as a transaction, then an alternative approach is to automatically save your work after each transaction. It may be wise to save a copy of any original data file that you opened, but if you have saved as you progressed, then the standard callbacks to File/Exit will be perfectly satisfactory.
There is a certain logic to keeping Save and Save As disabled until after a data file (or ‘document’) has been opened, read successfully, and checked for errors and inconsistencies. At that point, Save As might be enabled because the combination of Open followed by Save As is a way of making a copy of the original dataset within your program. However, it is only logical to enable Save if there have been changes to the dataset. Moreover, once a dataset has been saved, it is sensible to disable the menu command Save until other changes have been made. Save As logically should stay enabled.
Similarly, once an Open menu command has been successfully executed, it is sensible to disable both Open and New.
The above suggestions work if it is intended that the user should only be able to work on one dataset at a time and should then exit the program once they have finished with that dataset. That is not the way that Windows programs normally operate. Windows programs usually have options to permit the user to work on multiple datasets or documents. What I have described previously is a very clear example of a single document interface (SDI). A program that operates under the SDI paradigm can be made to deal with multiple documents simultaneously by simply launching a second copy of the program, which will be complete with its own toolbars and menus and will function completely stand-alone. Naturally, a Windows conversion of a preexisting older Fortran program is likely not to have very great memory demands and multiple copies can be launched without stressing the total availability of RAM. It may be possible to launch second and subsequent copies of any program (and that includes itself) from a menu using the START_PPROCESS@ facility described in section 1.4 in the context of Help, or the user may start those extra copies from the desktop. Conversely, there are ways for a program that uses a very great deal of available RAM to inhibit starting of a second copy of itself. This requires Windows messaging which I discuss later in the book in Chapter 27.
The explicit multiple document interface (MDI) approach usually contains the different activities on each of the multiple documents in child windows contained within a frame defined in the master window. Personally, I find that MDI program development is much more difficult than the SDI case, and prefer the ‘multiple invocations of the same application’ route. The business of frames and child windows is rather complicated, and I feel that certainly you will not want to be involved with that degree of complexity until you are adept with ClearWin+.
In order to see the look and feel of SDI when you have multiple instances of a program, you need look no further than Microsoft Word, when each time you select a new document, you generate another instance of the program. In contrast, Microsoft Excel gives you the choice of running a second instance of itself or of putting a second sheet into your main document.
The choice of how to handle multiple documents is rather fundamental to how you manage the File menu, especially in respect of enabling or disabling the options. Indeed, the current versions of Microsoft Office applications do not respond directly to the File menu but in fact launch a completely different window with rather more options than are traditional in the Windows paradigm.
I have found it helpful to also add a Close option to the File menu in an SDI program, with the option disabled initially, and only enabled after a successful Save or Save As, and being disabled again if your user changes their dataset. Alternatively, once Close has been enabled, you may prefer to keep it enabled but if the user has changed their dataset to pop up a window that asks them if they are sure that is what they wish to do, i.e. to close that particular run without saving a file. Once Close has been operated, New and Open are enabled, and Save and Save As need to be disabled. Other possibilities are to keep New and Open available at all times, but to give the user the opportunity to save current datasets first.
Part of the reason why I suggested doing all of the initialisation for your program in a subroutine (see Section 17.1) is that it makes it easy to reinitialise everything when a new dataset is contemplated simply by calling the subroutine. At the risk of duplicating what is said later, BLOCK DATA isn’t a good idea, and a subroutine containing all the initialisation statements can be called repeatedly. My subroutines that do the initialisation are called BLOCK_DATA for historical reasons!
My programs work generally with the idea that I have an input dataset that I run through a program to generate some results, or output(s). That means I have a file (or document) that contains the input dataset, and so Save or Save As generally imply saving that input dataset and changes I or another user have made to it. Inevitably, that means my output(s) have to be read off the screen, printed out, or saved to a different file. I imagine that my standard approach is therefore similar to many other engineers in particular, but also scientists, and indeed, in my early years as an academic when processing exam results each year was done on a department-by-department basis and not for the whole University, the tabulating and listing program ran the same way. If the results were saved in a file system, it created a naming issue, partly, but not wholly, solved by having input and output files with the same root name but just a different extension. There is a big advantage to having the same root name and different extensions in that you have to give only that root, and the application can append the appropriate file name extensions. I suppose that it has a certain inevitability that input file names were often given the extension .DAT and results files .RES, and because the lack of imagination is endemic, eventually a whole mess of DATs and RESs files from different applications clog up the users’ systems.
There are, of course, different paradigms, and one of those that most Windows users will be familiar with is the document being not just the user inputs, but how the application has dealt with them. Examples abound, but to cite just 2, we have Microsoft Word and CorelDRAW! In the former, the document is a file that is formatted, and when it is saved it no longer contains just the user’s keystrokes, but how they have been formatted, perhaps into sentences and paragraphs, different fonts and so on. Similarly, in the latter, the document is the drawing. There’s a chance that your application conforms to that paradigm, and not my input and output separated into different files.
There is, of course, at least a third way, and that is simply to append the results (using my approach) to the end of the input file. It is a simple matter to write the inputs again before the results, and to prefix the whole lot with some form of header that says ‘This file contains data and results’. Even in 32-bit Windows the available file sizes will allow just about any length of information the average Fortran programmer is likely to save. A disadvantage of the approach is the risk of overwriting something you wanted to keep, and of course that means being careful about letting the user do it too easily, using pop up ‘Are you sure?’ messages in a way that you simply wouldn’t need to in, say, Word. It also means that you have to equip your application with the facilities to read the results portion of the file: not always necessary, but handy if the processing takes a long time, and by reading a file with the results pre-computed it can save a long wait.
As always, the choice of what you do is yours, and yours alone (unless you work in a team, when it is a joint decision) and nothing to do with any preferences I might have. However, saving the recomputation time is only really useful if that implies a significant wait, and many applications that I know used to run overnight now run surprisingly quickly.
A pet hate of mine is the fact that output to a file is asynchronous, and a program can stop before the file is written. The way to enforce some order in this is to actually close the file when you have finished writing it using a CLOSE statement. Then, if the program is still running in Windows mode, pop up a message to tell the user that the file has been properly saved. If you think that it will annoy the user, then add a tick box labelled ‘Do not show this again’, and act on the user’s preference.
Print files which go to the Spooler part of Windows need to be labelled so that the user can see if his job is in the queue, and that is done with a call to the subroutine SET_CLEARWIN_STRING@ with the appropriate parameters (see Section 12.1, or the online help file). There isn’t the equivalent for file saving, even though files go through the disk cache, but they do so at a much faster rate than through the printer queue.
In the early days (or should that be years, or perhaps even decades) of Windows, it was a habit of Windows applications to append a list of most recently used files to the File menu. Sometimes it was a little as the 4 most recently opened files, or sometimes more. When files were limited to that 8.3 format (up to 8 characters in the name, and up to 3 in the extension), typically subdirectories were only nested 1 or 2 levels deep, and users kept so few files that their names tended to be unique despite their brevity, the MRU list fitted naturally into the bottom of the File menu. ClearWin+ can do it that way if you like, using a facility called dynamic menus. The principle is to use a special form of the %mn format string that returns a handle to the relevant part of the menu, and with the aid of that handle and the subroutines ADD_MENU_ITEM@ and REMOVE_MENU_ITEM@ it is possible to add or to remove submenu items. Doing so creates complexities because the callback function must pick which dynamically added entry has been invoked via the use of a function CLEARWIN_STRING@(‘CURRENT_MENU_ITEM’).
There are several reasons why I do not advocate this route. One reason is aesthetic, and that is because file names may be very long, particularly if a path (or even a truncated form of path) is given. The second reason is that although users may be familiar with having the MRU files list in the Files menu, dynamic menus are not generally a good idea. For example, it is quite possible to construct all menus dynamically so that there are never ‘greyed-out’ items. Microsoft says that following that approach you confuse users, and it is better that menus always present the same general appearance, with greying-out there to point out that selections are unavailable. The alternative is that users may wonder how to get to the selections they want, and hunt fruitlessly through menu after menu, getting more frustrated every time.
So if we don’t use the dynamic menu route, how do we use the MRU list? The answer is to do something that is still fairly obvious, but where the MRU list is presented in a dialog. One option may be to have submenus to File/Open, such as Recent file (which brings up a dialog) and Search (which brings up the standard file selection dialog). Alternatively, a completely new command in the Files menu – say Recent files or Open Recent might be provided. CorelDRAW! presents its MRU list in multiple ways, one of them being as part of the initial ‘splash’, and that is also an option for you.
If the MRU files list is part of the initial splash screen, then it mustn’t fade away. It also needs careful design with not too much space devoted to logos etc. All of the file names when clicked should essentially launch OPEN, and as some of those files may not be available (through being on removable media that has, in fact, been removed), their status regarding availability must be checked before offering the file as a selection. INQUIRE is helpful here, or FEXISTS@ (see section 5.1) and I recommend keeping the file in the list even if it isn’t available, but marking it in some way equivalent to ‘greying-out’.
You will probably find that putting an action button next to the filename is easier to program than making the name itself clickable, which probably requires a Listview with only one column (see Chapter 14). Alternatively, each file can be associated with a set of ganged radio buttons.
In general, you will find it best to leave the whole business of the MRU list to a late stage in program development.
FORTRAN and the ART of Windows Programming, Copyright © Eddie Bromhead, 2023.