Page 1 of 1

Using WindowsX.h for Better Code Organization and Message Cracking Rate Topic: -----

#1 Skydiver  Icon User is online

  • Code herder
  • member icon

Reputation: 3628
  • View blog
  • Posts: 11,320
  • Joined: 05-May 12

Post icon  Posted 25 July 2012 - 03:55 PM

Using WindowsX.h for Better Code Organization and Message Cracking

This tutorial shows how you can use all the macros defined in WindowsX.h to organize your code better, as well as get a more consist and bug free way of cracking Windows messages. It'll delve a little bit into some sofware development engineering practices and philosophies, but I'll try not wander too far astray.

As a starting point, I'll start off with Snoopy11's excellent tutorial Win32 Image Editting Program - Part 1. This is not to intended to say that his code is poorly organized. In fact, it is actually amazing well organized for his objective of teaching how to use the Win32 API to do basic image editing. Another one of his objectives is to make it easy for anyone reading his tutorial to throw the code into their respective files and get up and running quickly without fighting with makefiles. If you like his program, be sure to let him know. I'm quite sure he'll appreciate it.

This good starting point lets me, and hopefully you the reader as well, to focus on the "hows" and "whys" of doing the code reorg using WindowsX.h without getting bogged down by other concerns.

I'll be showing one or two examples of changes as I go along in this post, but I'll skip the repetitive tasks. If you want to follow along with all tiny changes and see them on your machine, you can get to the Mercurial repository by:
hg clone https://ants@bitbucket.org/ants/sied-reorg


The steps to better organization:

0. Move code to source files, and declarations to headers.
If you are only interested in learning about code organization and message cracking move on to step 1. This step is just minor housekeeping.

Spoiler


1. Message Parameter Macros
Very typically in Windows code, you'll get a parameters passed to you in the wParam or lParam parameters of a Windows message. If you aren't cutting and pasting somebody else's code, then you need to peek in MSDN to find out how to pull out the information which maybe stashed in the high word or low word of one of the parameters. This tends to get error prone and some people won't even bother masking off the high word if they know the data they have is in the low word and the high word is normally zero. Generally, this is a source for bugs for the careless.

All the message parameter macros in WindowsX.h follow the naming convention GET_message_parameter. message can be WM_COMMAND, WM_VSCROLL, etc. parameter is the special parameter for that message like POS, CMD, ID, etc.

So let's go for a concrete example. The WindowProcedure() has these bits of code:
case WM_VSCROLL:
    {
        int xDelta = 0;
        int yDelta;     // yDelta = new_pos - current_pos
        int yNewPos;    // new position

        switch (LOWORD(wParam))
        {
            // User clicked the scroll bar shaft above the scroll box.
        case SB_PAGEUP:
            yNewPos = yCurrentScroll - 50;
            break;

            // User clicked the scroll bar shaft below the scroll box.
        case SB_PAGEDOWN:
            yNewPos = yCurrentScroll + 50;
            break;

            // User clicked the top arrow.
        case SB_LINEUP:
            yNewPos = yCurrentScroll - 5;
            break;

            // User clicked the bottom arrow.
        case SB_LINEDOWN:
            yNewPos = yCurrentScroll + 5;
            break;

            // User dragged the scroll box.
        case SB_THUMBPOSITION:
            yNewPos = HIWORD(wParam);
            break;

        default:
            yNewPos = yCurrentScroll;
        }



This is where the macros start to shine because you just pass both the wParam and lParam to the macro and it does the right thing:
    case WM_VSCROLL:
    {
        int xDelta = 0;
        int yDelta;     // yDelta = new_pos - current_pos
        int yNewPos;    // new position

        switch (GET_WM_VSCROLL_CODE(wParam, lParam))
        {
            // User clicked the scroll bar shaft above the scroll box.
        case SB_PAGEUP:
            yNewPos = yCurrentScroll - 50;
            break;

            // User clicked the scroll bar shaft below the scroll box.
        case SB_PAGEDOWN:
            yNewPos = yCurrentScroll + 50;
            break;

            // User clicked the top arrow.
        case SB_LINEUP:
            yNewPos = yCurrentScroll - 5;
            break;

            // User clicked the bottom arrow.
        case SB_LINEDOWN:
            yNewPos = yCurrentScroll + 5;
            break;

            // User dragged the scroll box.
        case SB_THUMBPOSITION:
            yNewPos = GET_WM_VSCROLL_POS(wParam, lParam);
            break;

        default:
            yNewPos = yCurrentScroll;
        }



For even more fun, if you get used to Windows programming, Window handles are typically passed in the wParam, but not for WM_COMMAND, it's passed in the lParam. So a message macro really helps out because WindowsX.h provides:
#define GET_WM_COMMAND_ID(wp, lp)               LOWORD(wp)
#define GET_WM_COMMAND_HWND(wp, lp)             (HWND)(lp)
#define GET_WM_COMMAND_CMD(wp, lp)              HIWORD(wp)



So isn't this more readable:
    case WM_COMMAND:
        switch (GET_WM_COMMAND_ID(wParam, lParam))


as compared to:
    case WM_COMMAND:
        switch (LOWORD(wParam))



A little variance from the naming convention, but still useful:
    case WM_LBUTTONDOWN:
    {
        StartxPos = LOWORD(lParam);
        StartyPos = HIWORD(lParam);

        return 0;
    }

    case WM_LBUTTONUP:
    {
        EndxPos = LOWORD(lParam);
        EndyPos = HIWORD(lParam);

        mmov = true;
        InvalidateRect(hwnd,&rect,true);
        return 0;
    }



Using the macros in WindowsX.h we can have something more meaningful:
    case WM_LBUTTONDOWN:
    {
        StartxPos = GET_X_LPARAM(lParam);
        StartyPos = GET_Y_LPARAM(lParam);

        return 0;
    }

    case WM_LBUTTONUP:
    {
        EndxPos = GET_X_LPARAM(lParam);
        EndyPos = GET_Y_LPARAM(lParam);

        mmov = true;
        InvalidateRect(hwnd,&rect,true);
        return 0;
    }



The macros do the same thing, but they've been vetted by Microsoft to do The Right Thing™ in terms of casting and sign extensions, etc. From WindowsX.h:
#define GET_X_LPARAM(lp)                        ((int)(short)LOWORD(lp))
#define GET_Y_LPARAM(lp)                        ((int)(short)HIWORD(lp))



2. Message Crackers
Most people have seen how the WindowProcedure or DialogProcedure can become one huge switch statement to handle the various messages, and if you have WM_COMMAND message handling, it becomes even worse as you get nesting levels.

So one huge switch statement is completely against the advice we usually give people which is trying to keep functions short. A screenful or pageful is the normal limit which would be somewhere in the 30-50 line range (unless you like really small fonts or are lucky enough to have a really large screen in portrait mode). None of us would really like to look at a 900-2000 line switch statement, but that is the reality for some legacy Windows programs.

Message crackers in WindowsX.h help us trim down the huge switch statement by encouraging us to write functions for the message handlers instead of throwing everything into the switch statement. Additionally, they ensure that the right parameters get passed to the function as meaningful parameters instead of just seeing wParam and lParams.

So let's start out by tackling WM_CREATE in WindowProcedure(). It currently looks like this:
    case WM_CREATE:
        InitialiseDialog(hwnd);
        hwndViewer = SetClipboardViewer(hwnd);

        menu = GetMenu(hwnd);


        // Initialize the flags.
        fBlt = FALSE;
        fScroll = FALSE;
        fSize = FALSE;

        // Initialize the horizontal scrolling variables.
        xMinScroll = 0;
        xCurrentScroll = 0;
        xMaxScroll = 0;

        // Initialize the vertical scrolling variables.
        yMinScroll = 0;
        yCurrentScroll = 0;
        yMaxScroll = 0;
        return 0;



We'll replace all of that with:
    HANDLE_MSG(hwnd, WM_CREATE, MainWindow_OnCreate);



I just arbitrarily chose the function name MainWindow_OnCreate, but it usually a good idea to have the part before the underscore be the window class name, dialog name, or functional group name. A good convention for the part after the underscore is to use OnXXX where XXX is the event the function is handling.

The HANDLE_MSG macro is defined to be:
#define HANDLE_MSG(hwnd, message, fn)    \
    case (message): return HANDLE_##message((hwnd), (wParam), (lParam), (fn))



This matches perfectly with the standard Windows pattern where a message is handled, and the handler returns a value which is either a brush, a success or failure code, etc.

Since the ##message was WM_CREATE, we go looking in WindowsX.h we find the cracker for WM_CREATE:
/* BOOL Cls_OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct) */
#define HANDLE_WM_CREATE(hwnd, wParam, lParam, fn) \
    ((fn)((hwnd), (LPCREATESTRUCT)(lParam)) ? 0L : (LRESULT)-1L)



The first line gives us the idea of how to build the prototype for the function. So we'll write the function by moving the code:
BOOL MainWindow_OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
    InitialiseDialog(hwnd);
    hwndViewer = SetClipboardViewer(hwnd);

    menu = GetMenu(hwnd);

    // Initialize the flags.
    fBlt = FALSE;
    fScroll = FALSE;
    fSize = FALSE;

    // Initialize the horizontal scrolling variables.
    xMinScroll = 0;
    xCurrentScroll = 0;
    xMaxScroll = 0;

    // Initialize the vertical scrolling variables.
    yMinScroll = 0;
    yCurrentScroll = 0;
    yMaxScroll = 0;
    return TRUE;
}



Notice that the the original case statement returned 0. This new code returns TRUE to indicate success, and the message cracker will return 0 for success, or -1 for failure as required by WM_CREATE.

Moving the code, though has revealed something interesting when we try to compile. Most of the variables being initialised in the WM_CREATE handler are static variables declared in the scope of the WindowProcedure. We have even more "virtual" globals. I call them "virtual" globals because they act just like globals as in keeping state from call to call to WindowProcedure, but they just happen to be hidden away by the function scope. For now, we'll make them into real globals and deal with them in a later tutorial when I discuss using C++ classes with Windows to encapsulate data and finally get rid of globals that tend to plague Windows programming.

Most of the functions return void. So for example, let's tackle the WM_VSCROLL handler:
    case WM_VSCROLL:
    {
        int xDelta = 0;
        int yDelta;     // yDelta = new_pos - current_pos
        int yNewPos;    // new position

        switch (GET_WM_VSCROLL_CODE(wParam, lParam))
        {
            // User clicked the scroll bar shaft above the scroll box.
        case SB_PAGEUP:
            yNewPos = yCurrentScroll - 50;
            break;

            // User clicked the scroll bar shaft below the scroll box.
        case SB_PAGEDOWN:
            yNewPos = yCurrentScroll + 50;
            break;

            // User clicked the top arrow.
        case SB_LINEUP:
            yNewPos = yCurrentScroll - 5;
            break;

            // User clicked the bottom arrow.
        case SB_LINEDOWN:
            yNewPos = yCurrentScroll + 5;
            break;

            // User dragged the scroll box.
        case SB_THUMBPOSITION:
            yNewPos = GET_WM_VSCROLL_POS(wParam, lParam);
            break;

        default:
            yNewPos = yCurrentScroll;
        }

        // New position must be between 0 and the screen height.
        yMaxScroll = (int)bxHeight2;
        yNewPos = max(0, yNewPos);
        yNewPos = min(yMaxScroll, yNewPos);

        // If the current position does not change, do not scroll.
        if (yNewPos == yCurrentScroll)
            break;

        // Set the scroll flag to TRUE.
        fScroll = TRUE;

        // Determine the amount scrolled (in pixels).
        yDelta = yNewPos - yCurrentScroll;

        // Reset the current scroll position.
        yCurrentScroll = yNewPos;

        // Scroll the window. (The system repaints most of the
        // client area when ScrollWindowEx is called; however, it is
        // necessary to call UpdateWindow in order to repaint the
        // rectangle of pixels that were invalidated.)
        ScrollWindowEx(hwnd, -xDelta, -yDelta, (CONST RECT *) NULL,
                       (CONST RECT *) NULL, (HRGN) NULL, (PRECT) NULL,
                       SW_INVALIDATE);
        InvalidateRect(hwnd,&WinRect, true);

        // Reset the scroll bar.
        si.cbSize = sizeof(si);
        si.fMask  = SIF_POS;
        si.nPos   = yCurrentScroll;
        SetScrollInfo(hwnd, SB_VERT, &si, TRUE);

        break;
    }



This gets moved into a function:
void MainWindow_OnVScroll(HWND hwnd, HWND hwndCtl, UINT code, int pos)
{
    int xDelta = 0;
    int yDelta;     // yDelta = new_pos - current_pos
    int yNewPos;    // new position

    switch (code)
    {
        // User clicked the scroll bar shaft above the scroll box.
    case SB_PAGEUP:
        yNewPos = yCurrentScroll - 50;
        break;

        // User clicked the scroll bar shaft below the scroll box.
    case SB_PAGEDOWN:
        yNewPos = yCurrentScroll + 50;
        break;

        // User clicked the top arrow.
    case SB_LINEUP:
        yNewPos = yCurrentScroll - 5;
        break;

        // User clicked the bottom arrow.
    case SB_LINEDOWN:
        yNewPos = yCurrentScroll + 5;
        break;

        // User dragged the scroll box.
    case SB_THUMBPOSITION:
        yNewPos = pos;
        break;

    default:
        yNewPos = yCurrentScroll;
    }

    // New position must be between 0 and the screen height.
    yMaxScroll = (int)bxHeight2;
    yNewPos = max(0, yNewPos);
    yNewPos = min(yMaxScroll, yNewPos);

    // If the current position does not change, do not scroll.
    if (yNewPos == yCurrentScroll)
        return;

    // Set the scroll flag to TRUE.
    fScroll = TRUE;

    // Determine the amount scrolled (in pixels).
    yDelta = yNewPos - yCurrentScroll;

    // Reset the current scroll position.
    yCurrentScroll = yNewPos;

    // Scroll the window. (The system repaints most of the
    // client area when ScrollWindowEx is called; however, it is
    // necessary to call UpdateWindow in order to repaint the
    // rectangle of pixels that were invalidated.)
    ScrollWindowEx(hwnd, -xDelta, -yDelta, (CONST RECT *) NULL,
                    (CONST RECT *) NULL, (HRGN) NULL, (PRECT) NULL,
                    SW_INVALIDATE);
    InvalidateRect(hwnd,&WinRect, true);

    // Reset the scroll bar.
    si.cbSize = sizeof(si);
    si.fMask  = SIF_POS;
    si.nPos   = yCurrentScroll;
    SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
}


Notice that we get nice parameters to play with now instead of trying to get values out of wParam.

Sometimes, a message needs to be forwarded like in WM_CHANGECBCHAIN. Here is the old code:
    case WM_CHANGECBCHAIN:
    {
        if((HWND)wParam == hwndViewer)
        {
            hwndViewer = (HWND)lParam;
        }
        else if(hwndViewer)
        {
            SendMessage(hwndViewer,message,wParam, lParam);
        }

        return 0;
    }


And the new code uses a message forwarder:
void MainWindow_onchangeCBChain(HWND hwnd, HWND hwndRemove, HWND hwndNext)
{
    if(hwndRemove == hwndViewer)
    {
        hwndViewer = hwndNext;
    }
    else if(hwndViewer)
    {
        FORWARD_WM_CHANGECBCHAIN(hwnd, hwndRemove, hwndNext, SendMessage);
    }
}



All of this lets us have a nice looking WindowProcedure:
LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    HANDLE_MSG(hwnd, WM_CREATE,             MainWindow_OnCreate);
    HANDLE_MSG(hwnd, WM_LBUTTONDOWN,        MainWindow_OnLButtonDown);
    HANDLE_MSG(hwnd, WM_LBUTTONUP,          MainWindow_OnLButtonUp);
    HANDLE_MSG(hwnd, WM_QUERYNEWPALETTE,    MainWindow_OnQueryNewPalette);
    HANDLE_MSG(hwnd, WM_PALETTECHANGED,     MainWindow_OnPaletteChanged);
    HANDLE_MSG(hwnd, WM_DRAWCLIPBOARD,      MainWindow_OnDrawClipboard);
    HANDLE_MSG(hwnd, WM_CHANGECBCHAIN,      MainWindow_onchangeCBChain);
    HANDLE_MSG(hwnd, WM_COMMAND,            MainWindow_OnCommand);
    HANDLE_MSG(hwnd, WM_PAINT,              MainWindow_OnPaint);
    HANDLE_MSG(hwnd, WM_SIZE,               MainWindow_OnSize);
    HANDLE_MSG(hwnd, WM_HSCROLL,            MainWindow_OnHScroll);
    HANDLE_MSG(hwnd, WM_VSCROLL,            MainWindow_OnVScroll);
    HANDLE_MSG(hwnd, WM_DESTROY,            MainWindow_OnDestroy);
    }

    /* for messages that we don't deal with */
    return DefWindowProc (hwnd, message, wParam, lParam);
}



The other huge switch statement we'll end up with is in the body of the WM_COMMAND handler. Unfortunately, there isn't a ready made message cracker for the specific WM_COMMAND id or notification messages. With the pattern shown in WindowsX.h, there is nothing stopping you from making a message cracker for the different id's. For now, the WM_COMMAND handler looks like this where I broke out each of the different command handlers:
void MainWindow_OnCommand_Open_BM(HWND hwnd)
{
    // Code hidden for brevity

}

void MainWindow_OnCommand_Print_BM(HWND hwnd)
{
    // Code hidden for brevity

}

// Other MainWindow_OnCommand_* command handlers hidden for brevity

void MainWindow_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
    switch (id)
    {
    case IDM_OPEN_BM:
        MainWindow_OnCommand_Open_BM(hwnd);
        break;

    case IDM_PRINT_BM:
        MainWindow_OnCommand_Print_BM(hwnd);
        break;

    case IDM_SAVE_BM:
        MainWindow_OnCommand_Save_BM(hwnd);
        break;

    case IDM_EXIT:
        MainWindow_OnCommand_Exit(hwnd);
        break;

    case IDM_COPY:
        MainWindow_OnCommand_Copy(hwnd);
        break;

    case IDM_CUT:
        MainWindow_OnCommand_Cut(hwnd);
        break;

    case IDM_PASTE:
        MainWindow_OnCommand_Paste(hwnd);
        break;

    case IDM_ZOOMOUT:
        MainWindow_OnCommand_ZoomOut(hwnd);
        break;

    case IDM_ZOOMIN:
        MainWindow_OnCommand_ZoomIn(hwnd);
        break;
    }
}



Other than code organization, one of the other side benefits of moving code out of the huge WindowProcedure is that if you have a crash and a callstack, you'll have a better chance of narrowing it down to a specific event handler rather than just a crash in WindowProcedure.

Conclusion
There are a lot of hidden gems stashed away in the WindowsX.h. One of the high value ones is how the macros defined there help in making code more readable and more organized.

Full source after all the changes:
Spoiler

This post has been edited by Skydiver: 20 August 2012 - 09:55 PM


Is This A Good Question/Topic? 2
  • +

Page 1 of 1