How do I? - Part 19

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

Summary: More adventures with pop-up menus, as this time we look at subclassing and window words techniques.

I'm getting ready to leave for several weeks for vacation. The house is filled with all kinds of stuff (which my wife thinks she might need) that we're taking on our camping trip. This means no computer-stuff. I even got a prohibition on books about computer stuff (she thinks I'm a workaholic). Luckily I can take a guitar and a load of other books.

Last month we looked at the inner workings of a popup menu. I had stated that in principle every control can have its own popup. This is as long as the context popup event (normally right-click) doesn't interfere with the button's native action. For example a left-click on a button will activate it, so it's unwise to have the context-popup-event defined as the same action.

Before we can handle events on a control ourselves, we've got to take a brief look at a technique called subclassing. In the slipstream of this we'll take a short detour and look briefly at so-called window-words. I love both techniques and use them a lot. Smalled for example uses subclassing and window words to: add bubble help to the button-bar, add pop-ups to the statusbar, add extra controls to the frame window, and add a drag-n-drop facility to the titlebar icon. This example shows that both techniques have a lot going for them. I'm sure I will refer to them a lot more in future articles.

What are window words? When we create a window, we pass along a number of parameters to define the way the window should look and behave. All this information is stored in a structure that is created at the moment a window is born. This information can also be queried by another application (In fact it's queried all the time by the OS to know how a window should look and react.) These elements associated with a certain instance of a window are called window words. The nice thing about window words is that they reserve 4 bytes for use by the user (more is possible, but that's beyond the scope of this month's problem.) The user contents of the window words can be filled with the WinSetWindowULong, WinSetWindowUShort and WinSetWindowPtr functions. These can store an Unsigned long, an Unsigned short or a Pointer. With the associating WinQueryWindowXXXX, the values in the window words can be queried (not just the user data, but also the data put there by your program or the OS.) Now 4 bytes may seem very little, but it's more that enough. You could store a pointer to a specific memory area in there. That pointer can point to a massive amount of data. So these window words are actually very flexible.

We will look at what subclassing can do for us in this month's problem. In earlier columns we saw that every window has its events handled by a so-called window procedure. This was no more than a big case statement which handled all the WM_* messages. Until now we only bothered with the messages sent to our main window so that the menu events and such could be handled. It's only logical that every window (such as buttons and listboxes) also have a window procedure. The standard window procedures for these controls handle the way the controls function. If we could break into these window-procedures, we could add special functionality such as starting a special popup on the WM_CONTEXTENU message and adding a handler for the WM_COMMAND message. We could also interfere with the WM_CONTROL message etc. By using subclassing we can modify the behaviour of virtually any control. For example, the MLE that Smalled uses is subclassed so that it supports Drag-n-Drop, several wrap modes etc.

Now how does this subclassing work? It's actually a very simple trick, in theory. Provide a custom window procedure for the messages you want to give special treatment. When the window procedure is needed, first call the custom procedure and when this procedure exits, call the default procedure. This means we need three things.

  1. A custom window procedure for the control we'd like to subclass
  2. The default window procedure for the control so that it can be called after the custom procedure
  3. A way to tell the control that after some event, it must first call the custom procedure

Item 1 isn't that difficult. We simply have to define a window procedure, the same as we've constantly done for our applications. Only this time this window procedure is used for the control. But the syntax for defining it is the same. We will be looking at such a window procedure in a moment.

Item 2 is somewhat harder. A procedure is usually called by using a name (with some parameters, if required). Internally in the program this name represents a pointer to the memory area where the procedure resides. In a program we can use the PFNWP (pointer to a window procedure) type for this. The bottom line is that if we know the pointer, we can call the procedure. Now we need a way to extract this pointer from a window. Solving this is closely connected to Item 3.

Item 3. There is an API which will tell a window that it has to call a custom window procedure instead of the one that is assigned to it by default. It won't be a surprise to learn that this API is called WinSubclassWindow. This function accepts the window handle of the window we want to subclass, along with a pointer to the new window procedure. It's very convenient, but the function will return the pointer to the current windowprocedure.

This seems to solve most of our problems. (The exact functioning will be explained in a minute) For this month's example, I reused last month's dialog and added three extra controls; an entryfield, a MLE and a listbox. As we've seen before, these controls have an ID in the RC file by which they are known (Check the rc file with this months example). If we want to be able to call the WinSubclassWindow API, we must have the window handle of the control we would like to subclass. If we get the ID, we can query the HWND by using the WinWindowFromID API.

The code for this would look something like this:

PFNWP   *pfnwOldProc;
HWND	 hwndControl;
//------------------------------------------------------------------
// Subclass the entryfield
//------------------------------------------------------------------
hwndControl = WinWindowFromID(hwndDlg,ENTRYFIELD);	 // Get Entryfield HWND
pfnwOldProc = WinSubclassWindow(hwndControl,NewEFProc); // subclass Statusbar

There are some problems left however. We now have the old window procedure, but how do we tell the new custom window procedure how to call it? Sure, we could use a global variable that is also known in the custom window procedure. This however, is far from elegant! Imagine a medium application that has about 50 controls that we all want to subclass. This gives us a boatload of global variables, which I hate, especially when a program gets more complicated.

Well, you guessed it. I didn't mention Window words for nothing. We could store the pointer to the old window procedure in the user window word of the control. This way we can extract this pointer in the custom window procedure so that the old window procedure can be called as a default. Sounds difficult? Let's see what the code looks like.

The complete set that initiates the control can be placed in the WM_INITDLG message handler. We are sure this message is sent before any of the controls start generating events that we want to act upon. The thing missing from the above is storing the oldproc pointer in the user window word. This is done with the WinSetWindowULong API. The complete code in the WM_INITDLG for one control looks like this:


PFNWP    pfnwOldProc;
HWND	 hwndControl;
//------------------------------------------------------------------
// Subclass the entryfield
//------------------------------------------------------------------
hwndControl = WinWindowFromID(hwndDlg,ENTRYFIELD);	    // Get Entryfield HWND
pfnwOldProc = WinSubclassWindow(hwndControl,NewEFProc);    // subclass Statusbar
WinSetWindowULong(hwndControl,QWL_USER,(ULONG)pfnwOldProc);// Pointer to oldwinproc in winword

Now lets take a look at the window procedure. Two things have to be done:

  1. Extracting the pointer to the old window procedure.
  2. Calling this procedure as a default action.

Item 1 is the reverse of putting data in a window word. The API through which this is done is called WinQueryWindowULong. This result can be put in a PFNWP variable so that we can use it to call the default window procedure. This will give us a subclass window procedure for a control that looks like:

MRESULT EXPENTRY NewProc(HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2)
  {
    PFNWP oldProc = (PFNWP) WinQueryWindowULong( hwnd, QWL_USER ); // Pointer from windowword
    switch (msg)
	 {
	    case ................. break;
	    case ................. break;
	 }
     return oldProc(hwnd, msg, mp1, mp2);
   }

This takes care of all our problems, except that it doesn't do what we've started off with: a popup on the controls. Remember from the column last month that the popup can be best started by intercepting the WM_CONTEXTMENU message? It's in the eventhandler for this message that we can put the WinLoadMenu and WinPopupMenu calls. If we assign the control's window handle to the popup as owner and as parent, we can also put a WM_COMMAND message in the custom window procedure to handle the specific WM_COMMAND calls.

For this month's example (.ZIP, 16K) I created a procedure that starts the popup for all the controls if called. This makes the code a bit more lean. I also added a beep in the WM_COMMAND to prove that it's called. The popup on the client window of the dialog as we created last month is also still available (with the beeps).

I hope this was clear since I've told a lot in a really short time. I will return to this material in later columns. But for now, I'm off on vacation!