Programming for the OS/2 PM in C:The File Dialog and Help

From EDM2
Jump to: navigation, search

by Rick Papo

<< Window State & Some Graphics - Index - Printing >>

Part VI. - The File Dialog and Help

Before we get into this months enhancements to the painter program we've been making, there were two errors in last month's program which need to be fixed. One error was in the left-button-down logic, and affected the storage of the starting point of the last possible line the program could accept. One person caught this problem, believe it or not. The other error was an oversight, but one that anybody working with the Presentation Manager must remember: the WM_MOUSEMOVE message is not only for advising the program of changes in the position of the mouse pointer, but is also used to adjust the shape of the mouse pointer. If you do not wish to change the normal behaviour, then you must allow the system default window message processor the opportunity to process the message. This can be done by replacing the 'return(0);' statements in the WM_MOUSEMOVE processing case with 'break;' statements.

The Standard File Dialog

The sample program is very simple, and a cute toy, but even if its drawing capabilities were much greater it would hardly be useful because it does not provide the ability to save a drawing, nor to load a previously saved drawing. The OS/2 Presentation Manager provides a very nice helper for this: The Standard File Dialog. We will add this abilities to the program now.

Before we use the File Dialog, we will need a pair of helper functions first: one to save the current drawing to disk, another to load one from disk. These functions are given in the sample code, and as they have little, if anything, to do with how the OS/2 Presentation Manager works, I will not elaborate on them here. Suffice it to know that we have two helper functions, Save(filename) and Load(filename), and that they attach a default extension to their files of .PNT.

In dealing with files, editor programs of any type normally have four standard commands related to saving and loading files: New, Open, Save and SaveAs. 'New' clears the working area in preparation for building a new file. 'Open' loads an already existing file. 'Save' saves the changes made to the last file loaded. 'SaveAs' saves the current file with a new name.

The 'Save' function is the easiest. If what you are working on came from a file, or has been saved to a file at any point, then it has a file name associated with it. All that needs doing is to call the 'Save' helper function already mentioned above. If the data has no file name, then a direct save cannot be done, and some sort of error should be reported to the user. If you want to be creative, you could invoke the SaveAs function automatically at this point.

The 'SaveAs' function needs some way for the user to specify in what directory and under what file name to data will be saved. Under the original versions of both OS/2 and Windows there were many people who built their own versions of this kind of dialog, so many, in so many ways, that both IBM and Microsoft decided that a common file dialog was needed. OS/2's File Dialog is quite powerful and flexible, and can be adapted to both simple and complex needs. In our case, we will use it in a rather simple way:

static int SaveAs ( HWND hwnd, char Filename [] ) {
           FILEDLG DialogData ;
           memset ( &DialogData, 0, sizeof(DialogData) );
           DialogData.cbSize = sizeof(DialogData);
           DialogData.fl = FDS_SAVEAS_DIALOG | FDS_CENTER;
           DialogData.pszTitle = (PSZ) "Save As";
           if ( Filename[0] )
              strcpy ( DialogData.szFullFile, Filename );
           else
              strcpy ( DialogData.szFullFile, "*.PNT" );
           WinFileDlg ( HWND_DESKTOP, hwnd, &DialogData );
           if ( DialogData.lReturn != DID_OK)
              return (FALSE);
           strcpy (Filename, DialogData.szFullFile);
           return (TRUE);
        }

This function takes as input the handle of the application window, which will become the owner of the File Dialog, and the address of an area into which the new file name will be saved. The File Dialog is invoked with a single function call, WinFileDlg, which takes as parameters three things: the handle of the window to be considered the parent of the dialog, the handle of the window to be considered the owner of the dialog, and the address of a parameter block. A window's parent is the window upon which the child window lives. It cannot extend beyond that window, and always appears on top of it. When the parent window moves, the child window moves with it. When invoking the File Dialog, the parent window will usually be the desktop, whose handle is HWND_DESKTOP. A window's owner is something more subtle. It is often the window that requested the child window's creation, and often wants to be notified as to the window's status. In the case of the File Dialog, the owner window and all its children is usually disabled so as to not interfere with the operation of the File Dialog.

The parameter block passed to the WinFileDialog function is of type FILEDLG, and has many fields, most of which we will not be using right now. The first step in preparing the block is to zero it and to set the byte-count field at its start, which serves as a sort of version indicator, so that if IBM wishes to enhance this block, it will know whether or not the user program knows about the enhancements. For the 'Save As' dialog, we set the 'fl' field to the flag values FDS_SAVEAS_DIALOG and FDS_CENTER, which indicate what type of file dialog will be required, and that we want the dialog centered over its parent window. We set the dialog title to "Save As", and set the initial file name if we have one specified already, and if not we set it to "*.PNT", which will cause all the painter files in the current directory to be displayed in the dialog's file list area. The FILEDLG structure has many other fields to customize the behaviour of the File Dialog, but we will not use more of them here. We've already used enough for our purposes.

Upon returning, the WinFileDlg function will have a value indicating the final result. In our case, we only care that everything was OK, and that a valid file name was selected. In this case, the return value of the function will be DID_OK. If it is not, we return an error from the SaveAs function, as it has failed to accomplish its purpose. If it is DID_OK, then the 'szFullFile' field will contain the full name of the file selected, from the drive letter onwards. In our case, this name is saved for use with the 'Save' helper function.

Having written the 'SaveAs' function, writing the 'Open' function is almost exactly the same. The only difference is that instead of specifying the style flag FDS_SAVEAS_DIALOG, we use FDS_OPEN_DIALOG instead. Once we have the full name of the file to be loaded, we pass it to the 'Load' helper function.

At a higher level, processing the Save command is easy: simply call the Save helper function and report any error from it. Processing the SaveAs command is a little more complicated, but only because we want to do something fancy: showing the current file name in the program's titlebar.

Dealing with the New and Open commands is more complicated, simply because you might have a data with unsaved changes in memory, and the user should be given the opportunity to save them. If this is the case, the WinMessageBox function can be used to decide what to do. In the sample program, we ask the user if he wants to save his data. If the user answers Yes, then the Save or SaveAs functions are used, depending on whether or not the data has a file name yet. If No, then the New or Open procedure continues, discarding the old information. If Cancel, the entire New/Open procedure is aborted.

Help

The OS/2 Presentation Manager, when it was originally shipped in revision 1.1, had all the pieces from which a clever programmer could build a help subsystem for his application, but had no standard way of using those pieces. When OS/2 1.2 was shipped in the fall of 1989, this was fixed, at least in the IBM version of the system. It appears than even then Microsoft and IBM were at odds, because when I was finally able to obtain of the programmer's toolkit for OS/2 1.2, the IBM version had tools for building help screens, but the Microsoft version did not. What's more, the new language that was introduced for describing help screens strongly resembled some earlier IBM rich text description languages I had seen some years before.

Anyway, attaching or associating a collection of help screens with an application and its components is not particularly difficult. Building those help screens is another matter, as it involves learning the above-mentioned hypertext description language and using it. One thing at a time. First, the easy part: attaching your application to an existing set of panels contained in a HLP file.

The first thing you have to do in attaching a help file to your application is to modify your 'main' function to build a help window, which is a very special kind of system window. The following lines will need to be added to the main function's data declarations:

HELPINIT HelpInit;
HWND HelpWindow;

Then, later in the main function, after having created the frame window, but before starting the message processing loop, you will need to insert code similar to the following:

memset ( &HelpInit, 0, sizeof(HelpInit) );
HelpInit.cb = sizeof(HelpInit);
HelpInit.phtHelpTable = (PHELPTABLE)MAKEULONG(ID_RESOURCES,0xFFFF);
HelpInit.hmodHelpTableModule = ResourceLibrary;
HelpInit.pszHelpWindowTitle = Title;
HelpInit.fShowPanelId = CMIC_HIDE_PANEL_ID;
HelpInit.pszHelpLibraryName = "ARTICLE6.HLP";
HelpWindow = WinCreateHelpInstance ( Anchor, &HelpInit );
WinAssociateHelpInstance ( HelpWindow, FrameWindow );

The first line clears the HELPINIT structure, which will describe the set of help panels. The second line sets the structure's byte-count field, which serves as a sort of version control so that if a new version of the structure should be defined, programs using the old version will continue to run. The third parameter is a pointer to another structure referred to as the Help Table, which is used to relate window IDs with the help topics to be displayed for them. In this case, the table will be loaded from the application's resource segment. We'll add it to the resource segment momentarily. The fourth parameter is the handle to the EXE or DLL from which the help table will be loaded. The fifth parameter sets the title for the help window, and the sixth sets an option for showing help panel ID numbers (or hiding them, as in this case). The seventh and last parameter being set tells the system where to find the actual help file. Once all these parameters have been set, a call to the system function WinCreateHelpInstance is performed to create the help window. Finally, the help window needs to be associated, or linked, to the application frame window, which is done with the WinAssociateHelpInstance function.

This is all fine, but unless you add that Help Table stuff into the resource segment, none of this will work. In the RC file we add the following:

HELPTABLE ID_RESOURCES
        {
        //
        //         Window ID      Help Sub-Table ID   Help Panel ID
        //
          HELPITEM ID_RESOURCES,  ID_RESOURCES,       ID_RESOURCES
          HELPITEM FID_CLIENT,    ID_RESOURCES,       ID_RESOURCES
        }

This table lists all the windows of the application by their ID, and indicates in which Help Subtable it is to look for specific help topic information. You must have a subtable, so we also insert the following into the RC table:

HELPSUBTABLE ID_RESOURCES
        {
        //
        //            Item ID                  Help Panel ID
        //
        }

For the moment, we will put nothing in the table, which will cause the Help Manager to select the most general help topic it can, generally the first one in the help file. If we build the application as it now stands, and run it, we now find that pressing the F1 key causes the first help panel to be displayed, and that the Help Index, Extended Help, Using Help menu options now work.

Unfortunately, we are not done. Keys Help still does not work, and we really don't have context sensitive help at all, just general help. Adding Keys Help is not hard: all you have to do is provide processing for the HM_QUERY_KEYS_HELP message. When the Help Manager's Keys Help function is requested, the help window sends this message to our window, which must respond by returning the number of the help panel to be displayed. In our case, the processing looks like this:

case HM_QUERY_KEYS_HELP: {
         return ( (MRESULT) IDM_KEYS_HELP ) ; } /* endcase */

The standard menu item for Keys Help is serviced by the following code:

case IDM_KEYS_HELP: {
         HWND hwndHelp = WinQueryHelpInstance (hwnd);
         if ( hwndHelp ) 
            WinSendMsg (hwndHelp, HM_KEYS_HELP, 0, 0);
         break; } /* endcase */

The above code is executed when the Keys Help menu item is selected, and its processing is done by (1) obtaining the help window handle, either by recovering it from somewhere where it had been saved, or, as in this case, by asking the system for the handle of the current window's help instance. In a similar fashion we also implement the other Help Menu items, in every case asking the Help Window itself to perform the function:

case IDM_HELP_INDEX: {
         HWND hwndHelp = WinQueryHelpInstance (hwnd);
         if ( hwndHelp )
            WinSendMsg (hwndHelp, HM_HELP_INDEX, 0L, 0L);
         break; } /* endcase */

case IDM_EXTENDED_HELP: {
         HWND hwndHelp = WinQueryHelpInstance (hwnd);
         if (hwndHelp)
            WinSendMsg (hwndHelp, HM_EXT_HELP, 0L, 0L);
         break; } /* endcase */

case IDM_HELP_FOR_HELP: {
         HWND hwndHelp = WinQueryHelpInstance (hwnd);
         if (hwndHelp)
            WinSendMsg (hwndHelp, HM_DISPLAY_HELP, 0, 0);
            break; } /* endcase */

Context sensitive help is not difficult to implement. Each separate window of the application can have a specific help panel assigned to it. The assignment is done through the Help Subtable, which so far we have left blank. Each entry in the table contains two numbers, or defined constants: the first being the identifier of the window whose help panel is being defined, and the second being the identifier of the help panel. It can be convenient to define them both the same, using the same numbers to identify windows and to identify their corresponding help panels, and in fact that is what I have done in the sample program.

An example IPF file defining the help panels is included with the sample program. It is written in the version of IPF current before the OS/2 Warp 3.0 Toolkit was released. The latest version of IPF is slightly different. There is only one thing special about this file, compared to IBM's specifications: I normally use a special preprocessor for IPF files, IPFCOMP. This preprocessor allows me to use the #include directive to include the application identifier numbers as defined constants, and converts these defined constants to their numeric equivalents before passing the converted IPF file to IBM's IPFC compiler program. This allows me to synchronize the window IDs and help panels IDs very easily, and IPFCOMP is free and reliable, so there's no good reason -not- to use it. My thanks to Rick Fishman, currently or formerly of Code Blazer, Inc, for this contribution to the OS/2 developer's community.

<< Window State & Some Graphics - Index - Printing >>