How do I? - Part 5

From EDM2
Jump to: navigation, search
How Do I? / Part
1 2 3 4 5 6 7 8 9
10 11 12 13 14 15 16 17 18 19

by Eric Slaats

Hi, welcome to the next lesson on PM programming. In this column simple Presentation manager programming problems and philosophies will be discussed. This Column is aimed at people who are interested in PM programming or are simply curious what makes PM programs tick. To understand this column a little programming experience (preferably in C) is recommended.

As promised we will finish the 'menu' discussion this month. Last month's article and this one can be viewed as one in this respect. Last month the simple menu stuff was discussed. Everything discussed there could be done in a resource editor or by writing simple resource (RC) files. This month we'll focus on the heavy stuff that needs actual programming to accomplish. The Sample program presented this time is essentially the extended program of last month's column.

We will be looking at:

  • Checked menu items
  • Conditional cascades
  • Disabling and Enabling menu items

As I have said, the items presented here need a little programming. Remember, we're working in an environment that is ruled by events. So the first thing we need to know is on what event (message) we have to act regarding these items.

It isn't very handy to constantly change the menus every time an action dictates a change. For example, for copying text, a menu selection has to be made active. This would mean that every time a selection is made, the copy item (in the invisible submenu) will be enabled. Every time the selection is undone, the menu-item has to de disabled. This isn't very efficient because we only want this to happen when we really need this information on the menu-item. In other words when the menu is showing.

If you think about it, all the above actions that can be done to menu items only appear in submenus. Submenus aren't displayed continuously. If we want to change a submenu to reflect the current status, a perfect time would be when the menu is going to be displayed. This way we don't have to act on every event that dictates a change; instead we just change the menu before it's displayed.

And, of course, (how convenient) OS/2 has a perfect message for this. This is the WM_INITMENU message. This message is sent to your main window whenever a menu attached to the frame will become active. The nice thing about this message is that it will also work for pop-up menus. This means we can handle pop-up items the same as normal menus in this respect.

The WM_INITMENU message has the following structure:

param1   SHORT  smenuid     //  Menu-control identifier
param2   HWND   hwnd        //  Menu-window handle
returns  ULONG  ulReserved  //  Reserved value, should be

The parameters of this message are just what we need: the ID of the submenu that is becoming active as well as the handle of this submenu so we can interact with it. With this info we can set up the code for handling the WM_INITMENU message. It looks just like the handling of the WM_COMMAND message; it's a large case statement in which the menu that is becoming active is selected. This gives the following structure:

WM_INITMENU
        {
        switch (SHORT1FROMMP(mpParm1))
                {
                case IDM_MENU-1:
                {
                // Actions
                        }
                break;
                .
                .
                .
                case IDM_MENU-N:
                {
                // Actions
                        }
                break;
                }
     }
break;

Where the "// Actions" are placed we can work our magic to change the menu that is going to be displayed. For example, this can be the items that are mentioned at the start of the article. Remember that it can go a lot further too; items can even be added or removed from a menu.

Now that we know where to perform our magic, let's handle the checked menu items first. What do we use these for? In most cases, checked menu items are used for those options that perform a toggle between two states. An example could be turning 'wrap mode' on or off. In the 'on' state, the Wrap menu item has a check mark, in the 'off' state no, check mark. This kind of use is often seen in menus where a number of settings can be turned on or off. For example, take a look at the 'view' menu of the Works word processor (in the BonusPak).

There is also another use for checked menu items. Essentially, it's the same as showing the state of an item but in this case there are more items involved. It can be used to show which item of an array of items is active. An example of this can be found in the same word processor mentioned above. (Check out the "Character" menu.) In this menu, items like bold, underline, etc. can be chosen. These are mutually exclusive (only one item can be active) and the active item has a check mark beside it. If another item is chosen, the check mark will be on this item the next time the menu is opened.

We will build a small example of the last situation. If this situation can be handled, the simple situation should be trivial.

First things first: before we can set/reset the check mark on a menu item, the item has to be defined as a CHECKED menu item. This means we have to add a Menu Item Attribute to the menu items we want to have the ability to appear checked. This can be done with the MIA_CHECKED keyword. Check the SAMPLE5.RC file (in this month's sample zip file) and you'll find the following is added:

SUBMENU "~Checked", IDM_MENU

BEGIN
     MENUITEM "Checked ~1", IDM_CHECKED1, MIA_CHECKED
     MENUITEM "Checked ~2", IDM_CHECKED2, MIA_CHECKED
     MENUITEM "Checked ~3", IDM_CHECKED3, MIA_CHECKED
     MENUITEM "Checked ~4", IDM_CHECKED4, MIA_CHECKED
     MENUITEM "Checked ~5", IDM_CHECKED5, MIA_CHECKED
END

It would be nice if that code alone would handle everything. Unfortunately it won't. A menu defined this way displays the menu items simply if there is no MIA_CHECKED attribute. To have the menu item display a check mark, it has to be 'turned on'. This can be done two ways: the hard way, using a MENU message, or the easy way, using a predefined macro. Of course we will use the easy way (KISS and be LAZY remember). On the other hand, it always pays to know what you're doing, so we'll also take a look at how the Macro expands.

The macro we will use is WinCheckMenuItem. This macros needs three parameters:

  • The window handle of the menu that's becoming active
  • The ID of the menu item to be handled
  • The checkstate wanted (True of False)

We already have all these items. The menu handle is mp2 from the WM_INITMENU, the ID of the menu item we simply know. When used, the WinCheckMenuItem macro expands to:

((BOOL)WinSendMsg(hwndMenu,
                  MM_SETITEMATTR,
                  MPFROM2SHORT(usId, TRUE),
                  MPFROM2SHORT(MIA_CHECKED, (BOOL)(fCheck) ? MIA_CHECKED : 0)))

Now to the last part of creating the checked items we want. We wanted to set up a menu that has only one of the items checked. This means they must be mutually exclusive. To handle this we create a global SHORT that gets the number of the men uitem that is to be activated. This way we know which menu item to check and which to uncheck when the menu is activated. (Check out the extra code for this in WM_COMMAND. I guess (hope) that this needs no further explanation).

We can now fill in the code for handling the IDM_CHECKEDMENU menu in WM_INITMENU. This gives us the following piece of code.

case IDM_CHECKEDMENU:
        {
        WinCheckMenuItem((HWND)mp2, IDM_CHECKED1, usChecked==1);
        WinCheckMenuItem((HWND)mp2, IDM_CHECKED2, usChecked==2);
        WinCheckMenuItem((HWND)mp2, IDM_CHECKED3, usChecked==3);
        WinCheckMenuItem((HWND)mp2, IDM_CHECKED4, usChecked==4);
        WinCheckMenuItem((HWND)mp2, IDM_CHECKED5, usChecked==5);
        }
break;

As I said, the WM_INITMENU event can also be used to enable/disable menu items. What do we use this for? When a menu-item is disabled, it can't be activated. So if we want to prevent users from activating a certain menu option, we simply disable it. And (here we go again) we can do it the simple way or the hard way. Just as with checking menu items, we can use a macro to handle the dirty work. This is the WinEnableMenuItem macro. The parameters it needs are identical to the WinCheckMenuItem macro:

  • The window handle of the menu that's becoming active
  • The ID of the menu item to be handled
  • The checkstate wanted (True of False)

Again, just as with the WinCheckMenuItem, we will take a peek on how the WinEnableMenuItem macro is expanded.

((BOOL)WinSendMsg(hwndMenu,
                   MM_SETITEMATTR,
                   MPFROM2SHORT(usId, TRUE),
                   MPFROM2SHORT(MIA_DISABLED, (BOOL)(fEnable) ? 0 : MIA_DISABLED)))

Now let's handle it in a sample. Just for the sake of this example, we'll define the following problem: if a menu item is checked in the checked menu, this menu item must be disabled. We'll use the WinEnableMenuItem menu item for this. The following piece of code takes care of business:

WinEnableMenuItem((HWND)mp2, IDM_CHECKED1, usChecked==1);
WinEnableMenuItem((HWND)mp2, IDM_CHECKED2, usChecked==2);
WinEnableMenuItem((HWND)mp2, IDM_CHECKED3, usChecked==3);
WinEnableMenuItem((HWND)mp2, IDM_CHECKED4, usChecked==4);
WinEnableMenuItem((HWND)mp2, IDM_CHECKED5, usChecked==0);

We still have one thing left to discuss. The conditional cascade menu. A conditional cascade menu is a cascade menu where the arrow is a little button. If this button is selected, the submenu becomes visible. If the menu item is activated, the top element of the (non displayed) submenu is activated. The default item can be changed though.

To do this, we use the MM_SETDEFAULTITEMID message. Using this message also creates a check mark in front of the default-item (check after the 'Open' menu item on the Desktop menu for an example of this).

Normally this is the way to go. However, I can think of some situations that I like to do things in a different manner. Personally I like to use conditional cascades for mutually exclusive situations. The default (first) item in the submenu is used to toggle between the possible values while the check mark indicates the active choice. This means we can't set a default because that would create a check mark making it impossible to only check the current choice. For an example of this, check out how Smalled handles its Wrap and Indent settings.

Which of the two choices you use will depend on the situation and your personal taste. I only show you how to create a conditional cascade menu; the handling of setting the default I leave to your vivid imagination.

To make a menu item conditional, a style has to be attached to this menu. This is done by attaching the MS_CONDITIONALCASCADE style to the submenu that has to become conditional. Unfortunately this can't be done directly by handling the RC file, we've got to change the style by hand. To change the style of a window (any window), first we must know how this is maintained in a window.

Information over a window is kept in the so called 'window words'. (I'll be looking into these in some future column). Window words are a set of bytes that are reserved for every open window. In this piece of memory a lot of useful info is stored among which are the style info, the window ID, the pointer to the window procedure, the window flags, etc.

For now, we're interested in changing the STYLE of a window. The LONG in which the STYLE is kept can be changed with several calls. We only want to change some bits in this variable and to do this we can first query this LONG, change it and write it back. Sounds good, but there is an even easier way. OS/2 has a function that will change a specified number of bits in a specified area of window words. This is the WinSetWindowBits function. This function takes the following parameters:

HWND     hwnd;    //  Window handle.
LONG     index;   //  Zero-based index of the window word to be set.
ULONG    flData;  //  Bit data to store in the window words.
ULONG    flMask;  //  Bits to be written indicator.

In our case this means we can do the following:

WinSetWindowBits(hwndSubMenu, QWL_STYLE, MS_CONDITIONALCASCADE, MS_CONDITIONALCASCADE);

The hwndSubMenu speaks for itself. However, we've got a little problem here. How do we get this window handle? For this we've got to make a slight detour. There is a function through which window handles of controls can be obtained when we know the ID of the control and the handle of the parent window. In the case of the main menu, we've got to have the handle of the frame-window. Fortunately, we've got this handle; it is available after we create the frame window. But how do we get the ID of the Main-menu? OS/2 assigns default identifiers to the standard frame-controls (also something that will be handled in a future column). For the main menu this is FID_MENU. This means the following line will return the handle of the main-menu:

WinWindowFromID(hwndFrame, FID_MENU)

This can be substituted in the place where we must enter the menu hwnd.

The next parameter in WinSetWindowBits, QWL_STYLE, is a constant that indicates the index in the window words for the STYLE bits. The following two values declare what we want to store in the STYLE LONG. So, in fact, it would be enough to call only this function once to set the conditional cascade. There is one pitfall to this though: we don't know the window-handle of the submenu.

How do we handle this? Here we also take the easy way out, we simply query all the information for the submenu. For this we'll use the MM_QUERYITEM message and send it to the menu item that is formed by the conditional cascade. This will fill a MENUITEM structure. This structure contains a window handle to the submenu and voila, we've got all we need. This means we've only got to call the following code at initiation time to set the conditional cascade for the duration of the session. The following code will take care of business:

MENUITEM mi;

WinSendMsg(WinWindowFromID(hwndFrame, FID_MENU),
           MM_QUERYITEM,
           MPFROM2SHORT(IDM_CASCMENU,TRUE),
           &mi);
WinSetWindowBits(mi.hwndSubMenu,
                 QWL_STYLE,
                 MS_CONDITIONALCASCADE,
                 MS_CONDITIONALCASCADE);

OK, now we know how the conditional cascade can be set, but where do we set it? The easiest place is right after the creation of the frame window, before the message loop starts (check out the sample). In the sample file this month (ZIP, 17.8k) one of the cascaded menus is made conditional with the above code.

This article started with mentioning that a little programming was needed. If you take a look at the sample, you can see that it isn't too much. All the new code is marked with // NEW.

Next month we will start on a completely new topic. See you then!