Page 1 of 1

Better Windows Code Organization Using C++ classes Rate Topic: -----

#1 Skydiver  Icon User is offline

  • Code herder
  • member icon

Reputation: 3622
  • View blog
  • Posts: 11,290
  • Joined: 05-May 12

Post icon  Posted 21 August 2012 - 10:33 AM

Better Windows Code Organization Using C++ classes

This tutorial focuses on how take advantage of C++ classes in your Windows code to improve the code organization. C++ classes lets us encapsulate data and behavior that should be only relevant to the class.

Although this code picks up from the previous tutorial Using WindowsX.h for Better Code Organization and Message Cracking, all the concepts presented here can apply equally everywhere. Again, kudos should go to snoopy11 for putting together the initial code base. It's his amazingly clearly written code that allows me to do this code reorganization without getting distracted with other issues.

Like previously, 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 the 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 key to making this all this work is being able to instantiate a class and be able to consistently get access to it again. To do that we'll be using the extra slot of memory that is always available to every Window class and custom dialog: GWLP_USERDATA and DWLP_USER, respectively. This slot can be read using the GetWindowLongPtr() and written using SetWindowLong(). Our class' this pointer will be stored in this user slot.

Let's start off by declaring an empty class that will eventually hold member variables and functions.
class MainWindow
{
public:
    MainWindow();
    ~MainWindow();
};


And
MainWindow::MainWindow()
{
}

MainWindow::~MainWindow()
{
}



(The changes have been applied to the old WinProc.cpp/.h files instead of creating a new class file and header because I find that deltas are easier to see within a file instead of across files.)

The first problem we need to tackle is how to get the this pointer put into the slot at the right time. There are several techniques, but the one I'll discuss here will probably cover 95% of your needs. (For cases where you need much earlier or later access to the this pointer, I invite you to look at the source code in your ATLMFC directory.) To do this, we'll pass the pointer as the lpParam to CreateWindow() or CreateWindowEx(). For a dialog, we'll pass it along as the dwInitParam to DialogBoxParam(), CreateDialogBoxParam(), etc.

Dealing With Windows
Here we instantiate the MainWindow class and pass it in to CreateWindowEx():
int WINAPI WinMain (...)
{
    :
    MainWindow mainwindow;
    :
    hwnd = CreateWindowEx (
               0,                   /* Extended possibilites for variation */
               szClassName,         /* Classname */
               szClassName,         /* Title Text */
               WS_OVERLAPPEDWINDOW, /* default window */
               CW_USEDEFAULT,       /* Windows decides the position */
               CW_USEDEFAULT,       /* where the window ends up on the screen */
               544,                 /* The programs width */
               375,                 /* and height in pixels */
               HWND_DESKTOP,        /* The window is a child-window to desktop */
               NULL,                /* menu */
               hThisInstance,       /* Program Instance handler */
               &mainwindow          /* MainWindow instance */
           );
    :
}



Next, we'll move the global WindowProcedure to be a public static method on MainWindow:
class MainWindow
{
public:
    :
    static LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
};


LRESULT CALLBACK MainWindow::WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    :
}


int WINAPI WinMain (...)
{
    :
    wincl.lpfnWndProc = MainWindow::WindowProcedure;      /* This function is called by windows */
    :
    /* Register the window class, and if it fails quit the program */
    if (!RegisterClassEx (&wincl))
        return 0;
    :
}



Now we'll get into trying to retrieve the this pointer. Obviously, it's not there until we have a chance to install it when we see the WM_CREATE message:
LRESULT CALLBACK MainWindow::WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    MainWindow * pThis = reinterpret_cast<MainWindow *>(GetWindowLongPtr(hwnd, GWLP_USERDATA));

    if (!pThis && message == WM_CREATE)
    {
        LPCREATESTRUCT pcreatestruct = reinterpret_cast<LPCREATESTRUCT>(lParam);
        pThis = reinterpret_cast<MainWindow *>(pcreatestruct->lpCreateParams);
        SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LPARAM>(pThis));
    }
    :
}



In most cases, you can get away with not removing the pointer, since the lifetime of the class should outlast the window, but for the sake of symmetry, we'll also remove the pointer.
    if (pThis && message == WM_DESTROY)
    {
        SetWindowLongPtr(hwnd, GWLP_USERDATA, NULL);
    }



The public static MainWindow::WindowProcedure() we have is still essentially a global function. We'll want per instance function so that we can have C++ subclasses.
class MainWindow
{
    :
protected:
    virtual LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
};


LRESULT CALLBACK MainWindow::WndProc(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);
    :
    }

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



In MainWindow::WindowProcedure(), we'll call the per instance function:
    if (pThis)
    {
        return pThis->WndProc(hwnd, message, wParam, lParam);
    }



Converting the global functions to class methods
We now convert the global functions into class methos. As an example, we'll start off with the OnCreate handler:
class MainWindow
{
    :
Protected:
    :
    virtual BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct);
};


BOOL MainWindow::OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
    :
}
:
LRESULT CALLBACK MainWindow::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    HANDLE_MSG(hwnd, WM_CREATE,             OnCreate);
    :
    }
    :
}


Notice that essentially what has happened is that all the "MainWindow_" prefixes have become "MainWindow::" class scopes? The same applies to the WM_COMMAND handlers:
void MainWindow::OnCommand_Open_BM(HWND hwnd)
{
    :
}
:
void MainWindow::OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
    switch (id)
    {
    case IDM_OPEN_BM:
        OnCommand_Open_BM(hwnd);
        break;
    :
    }
}



Converting the global variables to class variables
And lastly, we move the global variables to become class variables . The static "virtual" globals simply lose the static modifier and pasted into the class definition. All the other global variables, also get pasted into the class definition. Those variables that were being initialized, now get initialized in the constructor:
class MainWindow
{
    :
protected:
    :
    HPALETTE hpal;
    HMENU menu;
    HBITMAP hBitmap;
    double bxWidth, bxHeight;
    double bxWidth2, bxHeight2;
    double zoom;
    bool fZoom;
    int cxsize, cxpage;
    int cysize, cypage;
    :
    BOOL fBlt;
    BOOL fLoad;           // TRUE if BitBlt occurred
    BOOL fScroll;         // TRUE if scrolling occurred
    BOOL fSize;           // TRUE if fBlt & WM_SIZE
    :
};


MainWindow::MainWindow()
    : zoom(0)
    , fZoom(false)
    , cxsize(0)
    , cxpage(0)
    , cysize(0)
    , cypage(0)
{
}



And then we do the same transplant of the functions and globals from functions.cpp/.h which really should have been also part of the window class.

Window registration should also be encapsulated
Now that we've nice encapsulated the functions and variables used by the MainWindow, we notice that we still have some leaks in our encapsulation. WinMain() knows about the intimate details of Mainwindow. We remedy this by making a static method that registers the class. We also create a method for creating the window since the class name doesn't really need to be public in this case.
class MainWindow
{
public:
    MainWindow();
    ~MainWindow();

    HWND Create(HINSTANCE hinstance);

    static BOOL RegisterClass(HINSTANCE hinstance);

protected:
    static LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
    :
    static char szClassName[];

    HWND hwnd;
    :
};


char MainWindow::szClassName[] = "Snoopy's Image Editing Demo";
:
BOOL MainWindow::RegisterClass(HINSTANCE hinstance)
{
    WNDCLASSEX wincl;        /* Data structure for the windowclass */

    /* The Window structure */
    wincl.hInstance = hinstance;
    wincl.lpszClassName = szClassName;
    wincl.lpfnWndProc = WindowProcedure;      /* This function is called by windows */
    :
    return RegisterClassEx (&wincl);
}

HWND MainWindow::Create(HINSTANCE hinstance)
{
    hwnd = CreateWindowEx (
               0,                   /* Extended possibilites for variation */
               szClassName,         /* Classname */
               szClassName,         /* Title Text */
               WS_OVERLAPPEDWINDOW, /* default window */
               CW_USEDEFAULT,       /* Windows decides the position */
               CW_USEDEFAULT,       /* where the window ends up on the screen */
               544,                 /* The programs width */
               375,                 /* and height in pixels */
               HWND_DESKTOP,        /* The window is a child-window to desktop */
               NULL,                /* menu */
               hinstance,           /* Program Instance handler */
               this                 /* MainWindow instance */
           );
    return hwnd;
}


Notice the MainWindow::WindowProcedure() is no longer public, and nor is the window class name. Additionally, notice the call to CreateWindowEx() now really passes a this pointer.

Dealing With Dialogs
Similar to how we worked with windows, we do the same thing with dialogs.
class MyDialog
{
public:
    :
    INT_PTR Show();
    :
protected:
    static INT_PTR CALLBACK DialogProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);

    INT_PTR CALLBACK DlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
    :
};


INT_PTR MyDialog::Show()
{
    return DialogBoxParam(
        hinstance,
        MAKEINTRESOURCE(IDD_MyDialog),
        hwnd,
        MyDialog::DialogProcedure,
        reintepret_cast<LPARAM>(this));
}


int MainWindow::OnCommand_CustomZoom(HWND hwnd)
{
    MyDialog mydialog;
    
    INT_PTR result = mydialog.Show();
    :
}



For a dialog, saving the this pointer would look something like this:
INT_PTR CALLBACK MyDialog::DialogProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    MyDialog * pThis = reinterpret_cast<MyDialog *>(GetWindowLongPtr(hwnd, DWLP_USER));

    if (!pThis && message == WM_INITDIALOG)
    {
        pThis = reinterpret_cast<MyDialog *>(lParam);
        SetWindowLongPtr(hwnd, DWLP_USER, reinterpret_cast<LPARAM>(pThis));
    }

    if (pThis && message == WM_DESTROY)
    {
        SetWindowLongPtr(hwnd, DWLP_USER, NULL);
    }

    if (pThis)
    {
        return pThis->DlgProc(hwnd, message, wParam, lParam);
    }

    /* for messages that we don't deal with */
    return FALSE;
}



Further Improvements
Notice that almost all of the functions still take an HWND as the first parameter. This is a side effect of using the message crackers in WindowsX.h. How ironic that have used it has facilitated on easy conversion to using C++ classes, and now it is holding us back from making further improvements. In the WM_CREATE/WM_INITDIALOG, we could have another member variable that simply holds on to the window handle and all the other per instance methods can skip that parameter much like the ATL and MFC message dispatchers do. Since the various MainWindow::OnCommand_* don't actually use message crackers, this would be an easy one to tackle.

Also notice that as you add more windows and dialogs, the static WindowProcedure() and DialogProcedure() methods are boiler plate and could be written to take advantage of templates.

Conclusion
We can take advantage of C++ classes to make our Windows code more organized. Additionally, the use of global variables and functions are minimized, or at the very least encapsulated away from general access.

Some of the techniques shown here for windows and dialogs will also work well with property sheets and other Windows event message based constructs.

Complete final source code follows:
Spoiler


Is This A Good Question/Topic? 0
  • +

Page 1 of 1