Dynamic Control Formatting

From EDM2
Jump to: navigation, search

Making your Frame Controls Dockable

Written by Alger Pike

Introduction

[NOTE: Here is a link to a zip of the source code for this article. Ed]

This article will assume the user has a working knowledge of frame controls and how to format them in a frame window. As such, I will not review how to format a frame window here. To see an example of the required concepts read "EDM/2 April 1995 - Building Custom Controls", or "EDM/2 December 1995 - Bubble Help". The code presented in this article will take the code in the April article and expand upon it. Eric Slaats, the author, has granted me permission to do this.

One of the many concepts behind programming a GUI interface is "choice". The more pathways a user has to do useful work the better. Sometimes however, we can present the user too much information on the screen. By giving the user too many buttons and other gadgets on the screen, less prime real estate is available, for the data the user is working on. One solution to this problem has been the advent of the dockable control. Such a control, like a button bar or menu, can be detached from the frame window or even hidden out of view. By doing so, the user makes more screen space for their data. Of course when the control is needed again it can easily be made part of the main frame window again, simply by clicking.

Let's us first start implementing our dockable frame control by outlining the required components: 1) a main frame window, 2) a floating control window, and 3) dynamic formatting support to handle frame formatting for each window type. The main frame window handles formatting for all the application controls. The floating window will format only the control that it docks. And somehow, when the control is in the floating window, we must make the main frame window aware of this. Otherwise our SWP structure ends up having a dual entry, one for the main frame and one for the floating frame (PM does not like this).

The main frame window can be thought of as the base of operations. It needs to know about all the controls in the application and what state they are in. There are four states a dockable control can take. They are 1) docked, 2) floating, 3) docked minimized and 4) floating minimized. The frame window takes a specific action for each state the control might be in. If the control is in the docked state formatting for the control proceeds normally. If however, the control is in the floating state, the main window now has one less item to format. We must make it aware of this, so that if the control is floating, the main window will not attempt to format it again. In this way, the formatting becomes dynamic. The number of formatted controls is variable, depending upon which if any, of the frame controls are docked.

The floating control window is easier to implement, but no less important. It is the job of this floating window, to display the control, when it is not docked. Each floating window handles only one frame control. This makes the actual job of formatting, for this window much easier; it is static. The floating control can also be told to minimize itself. This way the control is not visible at all. Of course it can be made visible again with a single click of the mouse.

Let's now proceed to make some controls dockable. We will take both the toolbar and the statusbar of the MLE editor and make them dockable. The first thing to do is to setup a structure with all the variables that we need for each window:

  typedef struct windowstruct
  {
    PFNWP oldFrameProc;
    HWND* mainFrame;
    BOOL* dockState;
    BOOL  isMenu;
    HWND menu;
    ULONG item;
    HWND control;
  }   WinParms;

Figure 1. Parameters required by the floating frame window,

The first variable is a pointer to the old window procedure of our floating frame window. The second is a pointer to the main frame window. The third is the dockState. The main frame window will use this variable to decide if the control is docked or not. The next three items have to do with menus. The bool helps us determine if the formatted control is a menu or not. (Special care must be taken when docking menus.) The next two are an HWND and itemID to a menu and menu item on the main frame window. We will use these to check the menuitems that tell us if our controls are in view or not. The last item control is an HWND to the actual control that we are docking.

At this point now we can make all the necessary modifications to the main() function of the application. (Now would be a good time to dig up the MLE code. This way you can see exactly where the changes I made are placed.) Since the initial state of our controls is known we can tell the application what each state is. Since we also know if each is a menu we can also initialize these fields as well:

   BOOL dockMenuState = 1;
   BOOL dockStatusState = 1; 

   //Initialize the current dock State
   MenuParms.dockState = &dockMenuState;
   MenuParms.isMenu = TRUE;
   StatusParms.dockState = &dockStatusState;
   StatusParms.isMenu = FALSE;

Figure 2. Initializing parameters for the dockable controls,

Since the controls are visible when docked we check the corresponding menu items:

  //If items are visible at startup check them
  WinCheckMenuItem(MenuParms.menu,
    MenuParms.item,
    TRUE);
  WinCheckMenuItem(StatusParms.menu,
    StatusParms.item,
    TRUE);

Figure 3. Checking menu items for visible controls,

We will also need to register a new class:

  WinRegisterClass (hab,
    szFloatClient,
    NewDocClientProc,
    CS_SIZEREDRAW,
    sizeof(PVOID));

Figure 4. Registering the floating window class,

This class will handle all requests that are made to the floating control windows. This step completes all the required changes needed for the main() function. (Since the formatting for the floating control is static I will not discuss it, but you can check out the NewDocClientProc and NewDocProc for details.)

The next step is to make the changes required for the main frame window procedure. I added four new menu items to the main window. These items, two for each control, control the behavior of our dockable control, i.e. they dock and view each control. Add the message handlers as follows (The only dockable support will be through the menu. I leave it up to each user to define double clicking and other standard functionality for their dockable controls):

  case IDM_DOCKMENU:
    {
    ToggleDockState(hwndFrame,
      &hwndMenuDockFrame,
      hwndToolbar,
      &MenuParms,
      FALSE);
    }
  break;

  case IDM_VIEWMENU:
    {
    ToggleDockState(hwndFrame,
      &hwndMenuDockFrame,
      hwndToolbar,
      &MenuParms,
      TRUE);
    }
  break;

  case IDM_DOCKSTATUS:
    {
    ToggleDockState(hwndFrame,
      &hwndStatusDockFrame,
      hwndStatus,
      &StatusParms,
      FALSE);
    }
  break;

  case IDM_VIEWSTATUS:
    {
    ToggleDockState(hwndFrame,
      &hwndStatusDockFrame,
      hwndStatus,
      &StatusParms,
      TRUE);
    }
  break;

Figure 5. Adding new menu items,

Up to this point, the changes required have been fairly straightforward. All that has been needed has been initializing a few variables, and making a few function calls. At this point, we are now ready to put these variables to use by adding dynamic formatting support. Only those controls that are docked get counted in the PSWP structure and go on to get formatted. To take care of this provision, we need to make some changes to the WM_QUERYFRAMECTLCOUNT message in our main frame control. Remember back to out dockState variable in WinParms. We will check this variable for each control that is dockable. If the control is docked, it needs formatting (dockState = TRUE). We increment the count of items that need to be formatted for controls that are docked. If the control is not docked the number of formatted items remains where it is.

  case WM_QUERYFRAMECTLCOUNT:
    {
      USHORT   itemCount;

    //get count of original frame
    itemCount = SHORT1FROMMR(oldFrameProc(hwnd,
        msg,
        mp1,
        mp2));

    if(MenuParms.dockState)
      itemCount++;
    if(StatusParms.dockState)
      itemCount++;
    return (MRESULT) itemCount;
          }
  break;

Figure 6. Making item count control dynamic,

In this way then, if one of the controls is docked we add a one to our frame items, if they are both docked we add a two, and so on.

The next step is to add the support for dynamic formatting to the WM_FORMATFRAME message. Several new variables are required to help us keep everything straight:

USHORT items[ITEMS], maparray[ITEMS], i, itemindex;

The first array, items[ITEMS], contains the SWP array numbers for all the controls. In our case, there are two controls; so items[0] = itemcount and items[1] = itemcount + 1, etc. The items[index] is static; In a pure static setting this number is always equal to the control number, i.e. formatted control x has an item count of y and a PSWP index of y. In a dynamic setting these two numbers are not always equal. Formatted control x still has an item count of y but its PSWP index may or may not be y depending upon whether the control is docked or not.

The function of the next array, maparray[], becomes very important especially when some of your controls depend on other controls for formatting. It is the maparray[]'s job to keep track of the dynamic PSWP array elements. Each control is assigned a position in the maparray[], equal to its item count. So for example maparray[0] will always refer to the toolbar and maparray[1] will always refer to the statusbar. The value of the map array then takes the index to the items PSWP array. For example let's say that both of the controls are docked. In this case for the toolbar, maparray[0] = 0 and items[maparray[0]] = items[0] = itemcount. For the status bar, maparray[1] = 1 and items[maparray[1]] = items[1] = itemcount + 1. If the toolbar is not docked maparray[0] = -1, and now the control is not formatted. For the status bar (which is still docked) maparray[1] = 0 and items[maparray[1]] = items[0] = itemcount. So maparray[1], always refers to the status bar. Its value is then used to obtain the correct PSWP index. Now maparray[1] always refers correctly back to control one (the statusbar. This comes in very handy if for instance you have yet another control which depends on the location of maparray[1]. The entire WM_FORMATFRAME message follows:

  case WM_FORMATFRAME :
    {
    USHORT itemCount = SHORT1FROMMR( oldFrameProc( hwnd,
        msg,
        mp1,
        mp2 ));
    USHORT usClient  = 0,
      usMenu    = 0;

  //These variables now become dynamic and cannot be set to a value
  //The new variable itemindex holds the running count of the number
  //of controls based on who is currently docked to the main frame.
  //
  //The PSWP index of the toolbar (items[0]) may or may not be itemCount.
  //If the control is docked its PSWP index is itemcount. If it's not
  //docked the value is ignored and the status bar takes on a value of
  //itemcount.
  //    usToolbar = itemCount,
  //    usStatus  = itemCount+1;

  //These variables keep track of whose docked and what
  //their index is if they are
    USHORT items[ITEMS], maparray[ITEMS], i, itemindex;

    PSWP pSWP = (PSWP)PVOIDFROMMP(mp1);

    while (pSWP[usClient].hwnd != WinWindowFromID(hwnd,
      FID_CLIENT))
      ++usClient;
    while (pSWP[usMenu].hwnd != hwndMenu)
      ++usMenu;

    //Arrays start from zero so this becomes the index of
    //our first control
    for(i=0; i < ITEMS; i++)
      items[i] = itemCount + i;

    //Set our index to zero this is needed for docking control
    itemindex = 0;

    // Fill in values for the Toolbar
    //The toolbar is assigned items[0]
    if(dockMenuState)
      {
      maparray[0] = itemindex;
      pSWP[items[maparray[0]]].fl = pSWP[usMenu].fl
      pSWP[items[maparray[0]]].cy = pSWP[usClient].cy;
      pSWP[items[maparray[0]]].cx = pSWP[usClient].cx ;
      pSWP[items[maparray[0]]].hwndInsertBehind = HWND_TOP;
      pSWP[items[maparray[0]]].hwnd = hwndToolbar;

      WinSendMsg(pSWP[items[maparray[0]]].hwnd
        WM_ADJUSTWINDOWPOS,
        MPFROMP(pSWP+items[maparray[0],
        (MPARAM) 0L );

      pSWP[items[maparray[0]]].x = pSWP[usMenu].x ;
      pSWP[items[maparray[0]]].y = pSWP[usMenu].y ;
      pSWP[items[maparray[0]]].cy ;

      // adjust client window size for 2nd menu
      // If not, the client window will be placed
      // over the Toolbar
      pSWP[usClient].cy= pSWP[usClient].cy ;
      pSWP[items[maparray[0]]].cy ;

      //If this item has been added increment the count
      //Notice how the array index does NOT get incremented
      //if this control is not docked. This is a key point.
      itemindex++;
      }
    else
      maparray[0] = -1;

    if(dockStatusState)
           {

      //Also notice how this control is designated control[1]
      //ITS INDEX IN THE PSWP ARRAY MAY OR MAY NOT BE ONE.
      //This therefore allows you to refer back and get the
      //proper PSWP index for controls whose position depends
      //on the placement of other controls. Very useful for
      //when you have 30 controls and they all depend on each
      //others placement. This is the case in my Spectra 4.3
      //application.
      maparray[1] = itemindex;

      // Fill in values for the statusbar
      pSWP[items[maparray[1]]].fl = SWP_SIZE | SWP_MOVE;
      pSWP[items[maparray[1]]].cy = pSWP[usMenu].cy;
      pSWP[items[maparray[1]]].cx = pSWP[usMenu].cx ;
      pSWP[items[maparray[1]]].x = pSWP[usClient].x ;
      pSWP[items[maparray[1]]].y = pSWP[usClient].y ;
      pSWP[usStatus].hwndInsertBehind = HWND_TOP ;
      pSWP[items[maparray[1]]].hwnd = hwndStatus;

      // adjust client window size for 2nd control
      // If not, the client window will be placed over
      // the statusbar
      pSWP[usClient].cy= pSWP[usClient].cy ;
      pSWP[items[maparray[1]]].cy ;
      pSWP[usClient].y = pSWP[items[maparray[1]]].y  +
      pSWP[items[maparray[1]]].cy ;
      }
    else
      maparray[1] = -1;

    // return total count of frame controls
    //return( MRFROMSHORT(itemCount+2));
    //Old code above: When you dock controls you do not know
    //ahead of time what the number to add is. You must calculate
    //this on the fly based on whether or not the control is to
    //be floated or docked

    if(dockMenuState)
      itemCount++;
    if(dockStatusState)
      itemCount++;
    return (MRESULT) itemCount;
}

Figure 7. The new WM_FORMATFRAME message handler with added dynamic formatting support,

The final step in making the controls dockable is to write the toggleDockState function. This function will do the actual docking and hiding of the controls. By passing the function the current state of the control and the new control state it will be able to take the appropriate action. The function is prototyped as follows:

INT ToggleDockState(HWND mainFrame, HWND *floatFrame,
                    HWND control, WinParms *controlParms,
                    BOOL dockView);

At first this may seem a little scary, but by having so many variables passed to this function, it becomes very generic and can be used to dock any control to and from any frame window. The first parameter is in fact a handle to the frame window of your application. The second parameter is a pointer to the floating frame window. The function will later use this pointer to determine if the window is currently docked. The third parameter is a handle to the control that is to be docked. As far as I know any control you can put into a frame window can be docked in this way including menus. The fourth parameter is a pointer to the window parameters that the floatFrame will need. The structure contains all the important information that the floatFrame needs to know. The final parameter is a BOOL that has to do with viewing. If view is TRUE you are hiding/restoring the control from its current dockState. If the BOOL is FALSE you are changing the dockState of the control.

I think the body of the function is relatively simple to go through. Notice how the if statements take care of all four of the states our dockable control can take. Most of the code is just API calls which take care of some of the details with a dockable control (menu checking etc). The most important thing to notice is that when formatting passes to the floating window, that the control must also be made a child of that window. The control is always formatted using its parents coordinates. If you do not reset the parent, the control is still formatted but it is formatted in the main frame window. Also when formatting of the control passes back to the main frame window, make sure to set the parent back to the main frame. Do this before you destroy the floating window. If you do not, you will destroy the control window since PM destroys all children when destroying a frame window.

Hopefully this article has made you aware of how to format a control to and from its parent window. Making the formatting of controls dynamic adds several complexities to the code. However, the added work is well worth the effort. I have just presented the tip of the iceberg when it comes to this topic. Now that you have the basic understanding it should be quite easy to extend the code even further. Two enhancements I can think of include double click support, and automatic docking based on the movement of the dockable control. The user will appreciate the addition of dockable controls to your application. By using them wisely, the user will be able to hide and show the control at will making more for their valuable data.