Click here to Skip to main content
13,196,730 members (71,753 online)
Click here to Skip to main content
Add your own
alternative version

Stats

6.9K views
5 downloads
18 bookmarked
Posted 26 Apr 2017

WinLamb: using C++11 lambdas to handle Win32 messages

Rate this:
Please Sign up or sign in to vote.
Introducing WinLamb, a modern C++11 object-oriented library to write native Windows programs.

Contents

  1. Spoilers
  2. Getting started
    1. Concept
    2. Setting up a new project
  3. Creating and using windows
    1. Creating the main window
    2. Handling messages
    3. Dialog as main window
    4. Using controls
    5. A modal popup dialog
    6. A modeless popup dialog
    7. A custom control
    8. Dialog as a control
  4. Cracking messages
    1. Message parameters unpacked
    2. Handling command messages
    3. Handling Common Controls messages
    4. Multithread and UI
  5. Subclassing controls
    1. Installation and message handling
    2. Using specific handlers
  6. Final topics
    1. Window types summary
    2. Default message processing
    3. More
  7. History

1. Spoilers

First of all, this article assumes the reader is familiar with native Win32 programming and C++11.

Before explaining what it is, I’ll start showing what a Win32 program can look like with WinLamb. The following is a full program with a single window. Note that there’s no message loop, no window class registering, no switch statement or message maps. And two messages are handled, each with a C++11 lambda:

// Declaration: SimpleMainWindow.h

#include "winlamb/window_main.h"

class SimpleMainWindow : public wl::window_main
{
public:
  SimpleMainWindow();
};
// Implementation: SimpleMainWindow.cpp

#include "SimpleMainWindow.h"
RUN(SimpleMainWindow);

SimpleMainWindow::SimpleMainWindow()
{
  setup.wndClassEx.lpszClassName = L"SOME_CLASS_NAME";
  setup.title = L"This is my window";
  setup.style |= WS_MINIMIZEBOX;

  on_message(WM_CREATE, [&](wl::params p)->LRESULT
  {
    SetWindowText(hwnd(), L"A new title for the window");
    return 0;
  });

  on_message(WM_LBUTTONDOWN, [&](wl::params p)->LRESULT
  {
    SetWindowText(hwnd(), L"Window clicked!");
    return 0;
  });
}

To compile this code, you’ll need a C++11 compiler. All examples on this article have been compiled and tested with Visual C++ 2015 Update 3, under 32 and 64-bit architectures.

2. Getting started

2.1. Concept

The raw & usual way to create a native Windows C program is described in depth by Charles Petzold in his classic Programming Windows book. Since then, many object-oriented libraries – like MFC and WTL – have been written offering C++ approaches to deal with native Windows programming.

WinLamb – an uninspired acronym of Windows and lambda – is another object-oriented C++ library. It’s a header-only library which depends on nothing but pure Win32 and the C++ Standard Template Library, and it heavily relies upon modern C++11 features (well, technically, maybe one or two C++14’s too).

WinLamb is a thin layer over Win32 API. It’s designed as a basic infrastructure to organize window and dialog handling, providing scalability, modularity and readability to applications with many windows which handle many messages. It’s not a big, monolithic does-it-all library to “save the world”, it’s a basic framework that can be extended. It does not provide classes to handle file I/O, native Windows controls or system routines – you can use any other library or your own classes to do so. (Although there’s an addendum.)

With WinLamb, each window of your program has a class. Each message is handled through a C++11 lambda – no macros or message maps. WinLamb’s classes are enclosed within wl namespace. Also, WinLamb is fully implemented using Unicode Win32. All string-related functions use std::wstring class and/or wchar_t type.

Important: WinLamb isn’t an excuse to not learn Win32 – before start using WinLamb, I strongly advice the reader to learn the basics on how to write a Win32 program using plain C.

Caveat: when dealing with lambda functions, variable scope becomes very important, and demands special attention. Take some time to fully understand it, there are plenty of resources around.

WinLamb source is available on GitHub as open-source under the MIT license, and the reader is encouraged to take a look and understand how the code works behind the scenes.

2.2. Setting up a new project

WinLamb is a header-only C++ library. The most up-do-date code can be found at GitHub, you can clone the repository or simply download the files. The most straightforward way to have the library in your project is simply keep all the files in a subfolder called, for example, “winlamb”, then #include them in your sources.

The present article is accompanied by a ZIP file with complete Visual C++ 2015 projects. Anyway, here it is presented how to create a fresh new WinLamb Win32 project using Visual C++ IDE, from scratch. You can skip this section straight into the code.

To start, first create the new project:

Choose “Win32 Project”. The “.NET Framework” version doesn’t matter, since it won’t be used. Name your project and choose where to save it:

Choose “Windows application” and “Empty project”, so no other library will be included:

Unzip (or copy) all WinLamb files into a subfolder within your project folder. In the example, my project folder is “Example1”, with a subfolder called “winlamb” which will contain the library files.

The following step is optional, if you want to browse WinLamb files within your project. Add a new filter and name it “WinLamb”, or any other name you want:

Now, on Windows Explorer, select all WinLamb library files and drag & drop them into the filter you’ve just created:

WinLamb files now can be browsed inside your project:

Finally, it’s time to create your first source code file:

Now your source file should be able to include WinLamb headers:

In the rest of the article, we’ll create new source and header files for our programs. Just follow the above steps to create them, when needed.

You may have noticed that many WinLamb library files are prefixed with “base_”. These are internal library files, with classes buried within wl::base namespace. You should not #include these files in your code. All the other files will be included when needed.

3. Creating and using windows

Once the project is ready, it’s time to use WinLamb to wrap up our windows.

3.1. Creating the main window

Under the most common cases, the first thing you must design when creating a Win32 program is the main window. So let’s start with the declaration of the main window class – in WinLamb, each window has a class. It’s a good idea to have 1 header and (at least) 1 source file for each window. Technically the main window doesn’t need a header, but for consistency, let’s write one.

All WinLamb library classes belong to the wl namespace. Our main window class will inherit from window_main class. Let’s also declare the constructor:

// Declaration: MyWindow.h

#include "winlamb/window_main.h"

class MyWindow : public wl::window_main
{
public:
  MyWindow();
};

For the program entry point, you can write your WinMain function and instantiate MyWindow manually, if you want. However, if you aren’t doing anything special on WinMain, you can simply use WinLamb’s RUN macro – the only macro in the whole library, I promise –, which will simply expand into a WinMain call instantiating your class on the stack. This is the macro call:

RUN(MyWindow);

And then implement the class constructor. Thus, that’s what we have in our source file, so far:

// Implementation: MyWindow.cpp

#include "MyWindow.h"
RUN(MyWindow); // will generate a WinMain function

MyWindow::MyWindow()
{
}

If you compile and run this code, the window will fail to show, because we didn’t specify the window class name. When the class is instantiated, the base window_main will call RegisterClassEx internally, and it will use a WNDCLASSEX structure with a few predetermined values – these values, however, don’t specify the class name to be registered.

The base class provides a setup member variable which holds all initialization values for the class. All these fields are optional – they have default values but the class name. The setup has a wndClassEx member, which is the WNDCLASSEX object passed to RegisterClassEx internally. We must define the class name at lpszClassName member, do this inside the constructor:

// Implementation: MyWindow.cpp

#include "MyWindow.h"
RUN(MyWindow);

MyWindow::MyWindow()
{
  setup.wndClassEx.lpszClassName = L"HAPPY_LITTLE_CLASS_NAME";
}

The other fields of the WNDCLASSEX structure can be filled at your will. However, some fields like cbSize, hInstance and lpfnWndProc will always be overwritten with the correct values, regardless of what you assign to them.

The other members of setup are the parameters passed to CreateWindowEx. Specifically, the two style members are already filled with default flags. For the window_main class, these are the predefined values of style members, already set by WinLamb:

setup.wndClassEx.style = CS_DBLCLKS;
setup.style = WS_CAPTION | WS_SYSMENU | WS_CLIPCHILDREN | WS_BORDER;

You can overwrite these values, of course. However, they are common to most windows, and most of the time you’ll just want to add a flag. For example, if you want your main window to be resizable and minimizable, you just need this:

setup.style |= (WS_SIZEBOX | WS_MINIMIZEBOX);

Therefore, also adding the window title, we have:

// Implementation: MyWindow.cpp

#include "MyWindow.h"
RUN(MyWindow);

MyWindow::MyWindow()
{
  setup.wndClassEx.lpszClassName = L"HAPPY_LITTLE_CLASS_NAME";
  setup.title = L"My first window";
  setup.style |= (WS_SIZEBOX | WS_MINIMIZEBOX);
}

All available window styles can be seen here.

The program should compile and run fine now. Yes, this is a fully functional Win32 program, including window class registering, window creation, message loop dispatching and final cleanup – all this infrastructure code is transparent.

Note: WinLamb classes make use of Set/GetWindowLongPtr with GWLP_USERDATA and DWLP_USER flags for storing context data. Since you have a class for your window, I can’t really imagine a reason for using these on your code, but I’m warning you just in case: don’t use GWLP_USERDATA and DWLP_USER to store your data.

3.2. Handling messages

The traditional way to handle window messages is a big switch statement inside the WNDPROC function. Some libraries define macros to avoid the “big switch”.

In our program using WinLamb, we’ll use C++11 lambdas. The window_main base class provides the on_message member function, which receives two arguments: the message to be handled and a function to handle it:

void on_message(UINT message, msg_funcT callback);

The most straightforward way to use it is to pass an unnamed lambda function inline. For example, let’s handle the WM_CREATE message in our main window class constructor:

MyWindow::MyWindow()
{
  setup.wndClassEx.lpszClassName = L"HAPPY_LITTLE_CLASS_NAME";

  on_message(WM_CREATE, [&](wl::params p)->LRESULT
  {
    return 0;
  });
}

The lambda receives a params argument, which has the WPARAM and LPARAM members – more on this later. Note that the lambda must return an LRESULT value, just like any ordinary WNDPROC message processing.

Note: in an ordinary window, you would have to handle WM_DESTROY in order to call PostQuitMessage. WinLamb implements default message processing for a few messages, using the default behavior, so you don’t have to worry about them. But they can be orverwritten if you need something specific – in the above example, if we write a handler to WM_DESTROY, the default library code would be completely bypassed. More on this later.

Now, if you happen to have two messages which will demand the same processing, on_message also accepts an initializer_list as the first argument:

on_message({WM_LBUTTONUP, WM_RBUTTONUP}, [&](wl::params p)->LRESULT
{
  UINT currentMsg = p.message;
  return 0;
});

This is functionally equivalent of:

switch (LOWORD(wParam))
{
  case WM_LBUTTONUP:
  case WM_RBUTTONUP:
    // some code...
    return 0;
}

Tip: if your window handles too many messages, the class constructor can become quite large and hard to follow. In these situations, breaking the handlers into member functions is helpful:

// Declaration: MyWindow.h

class MyWindow : public wl::window_main
{
public:
  MyWindow();
private:
  void attachHandlers();
  void evenMoreHandlers();
};
// Implementation: MyWindow.cpp

#include "MyWindow.h"
RUN(MyWindow);

MyWindow::MyWindow()
{
  setup.wndClassEx.lpszClassName = L"HAPPY_LITTLE_CLASS_NAME";

  attachHandlers();
  evenMoreHandlers();
}

void MyWindow::attachHandlers()
{
  on_message(WM_CREATE, [&](wl::params p)->LRESULT
  {
    return 0;
  });

  on_message(WM_CLOSE, [&](wl::params p)->LRESULT
  {
    return 0;
  });
}

void MyWindow::evenMoreHandlers()
{
  on_message(WM_SIZE, [&](wl::params p)->LRESULT
  {
    WORD width = LOWORD(p.lParam);
    return 0;
  });
}

To finally work with the window, the hwnd member function can be used to retrieve the window’s HWND:

on_message(WM_CREATE, [&](wl::params p)->LRESULT
{
  SetWindowText(hwnd(), L"New window title");
  return 0;
});

In order to keep the classes simple, WinLamb base classes have no specific window methods – for example, you won’t find member funcions wrapping SetWindowPos or ScreenToClient. Just use the hwnd member to call any native function. But if you feel the need to have a window class providing many methods, just make your own shim: inherit another base class and add the methods you want.

3.3. Dialog as main window

In the previous example, we created an ordinary main window – in pure Win32, it would be the equivalent of calling RegisterClassEx and CreateWindowEx, among other proceedings.

But it is also possible to have a dialog box as the main window of your program. This possibility is covered in WinLamb, if you inherit your main window from the dialog_main class:

// Declaration: FirstDialog.h

#include "winlamb/dialog_main.h"

class FirstDialog : public wl::dialog_main
{
public:
  FirstDialog();
};

With dialogs you don’t deal with WNDCLASSEX directly, there’s no need to register a window class name. That’s why the setup member variable doesn’t have the WNDCLASSEX field, instead it allows you to specify the ID of the dialog resource to be loaded.

Usually, dialog resources are created with resource editors, like the one Visual Studio has. An example of a dialog creation can be seen here. Now, assuming the dialog resource is already created, let’s use the dialog ID:

// Implementation: FirstDialog.cpp

#include "FirstDialog.h"
#include "resource.h" // contains the dialog resource ID
RUN(FirstDialog);

FirstDialog::FirstDialog()
{
  setup.dialogId = IDD_MY_FIRST_DIALOG; // specify dialog ID

  on_message(WM_INITDIALOG, [&](wl::params p)->INT_PTR
  {
    SetWindowText(hwnd(), L"A new title for the dialog");
    return TRUE;
  });
}

Here, on_message has some minor differences to follow the dialog box DLGPROC message processing, like the INT_PTR return type, and returning TRUE instead of zero.

3.4. Using controls

Still on the previous example, let’s say the dialog resource has an edit box, with IDC_EDIT1 as the resource ID. WinLamb has the native_control class, which is a generic wrapper to any native control. To use it, declare the object as a member of the parent class:

// Declaration: FirstDialog.h

#include "winlamb/dialog_main.h"
#include "winlamb/native_control.h"

class FirstDialog : public wl::dialog_main
{
private:
  wl::native_control edit1; // our control object
public:
  FirstDialog();
};

On the control object, call assign method, which wraps GetDlgItem and stores HWND inside the object. After that, the widget is ready to be used:

// Implementation: FirstDialog.cpp

#include "FirstDialog.h"
#include "resource.h" // contains the dialog resource IDs
RUN(FirstDialog);

FirstDialog::FirstDialog()
{
  setup.dialogId = IDD_MY_FIRST_DIALOG;

  on_message(WM_INITDIALOG, [&](wl::params p)->INT_PTR
  {
    edit1.assign(this, IDC_EDIT1);
    SetWindowText(edit1.hwnd(), L"This is the edit box.");
    return TRUE;
  });
}

The native_control class is really basic, and it does nothing beyond storing the HWND of the control. The idea is to use this class as the base for specialized control classes. WinLamb brings no specialized controls, but I’ve implemented some more, in case you’re not interested in writing your own.

And, of course, you can use any other control library you like.

3.5. A modal popup dialog

A modal popup dialog is a window created via DialogBoxParam. Let’s implement a modal dialog with its header and source files. Then, we will instantiate this modal in a parent window.

This is the header of our modal dialog, which inherits from dialog_modal class:

// Declaration: MyModal.h

#include "winlamb/dialog_modal.h"

class MyModal : public wl::dialog_modal
{
public:
  MyModal();
};

The implementation is pretty much like any other dialog window – you must inform the ID of the dialog resource to be loaded –, but modal dialogs are destroyed by calling EndDialog, with the second parameter of this function being the return value of the original dialog call.

// Implementation: MyModal.cpp

#include "MyModal.h"
#include "resource.h" // contains dialog resource ID

MyModal::MyModal()
{
  setup.dialogId = IDD_DIALOG2;

  on_message(WM_COMMAND, [&](wl::params p)->INT_PTR
  {
    if (LOWORD(p.wParam) == IDCANCEL) // the ESC key
    {
      EndDialog(hwnd(), 33); // modal will return 33, see the next example
      return TRUE;
    }
    return FALSE;
  });
}

Now let’s revisit the implementation of our main window, which will now use the modal dialog by instantiating the object and calling the show method. Since the dialog is modal, the show method will block the execution and will return only after the dialog is closed.

// Implementation: MainWindow.cpp

#include "MainWindow.h"
#include "MyModal.h" // our modal dialog header
RUN(MainWindow);

MainWindow::MainWindow()
{
  on_message(WM_COMMAND, [&](wl::params p)->LRESULT
  {
    if (LOWORD(p.wParam) == IDC_SHOWMODAL) // some button to open the modal
    {
      MyModal modalDlg;
      int retVal = modalDlg.show(this); // blocks until return; retVal receives 33
      return 0;
    }
    return DefWindowProc(hwnd(), p.message, p.wParam, p.lParam);
  });
}

If the modal asks user input, it’s common for the modal to return constants like IDOK or IDCANCEL.

The modal dialog can also receive any parameters on the constructor, just like any class, and have public methods to return something:

class MyModal : public wl::dialog_modal
{
public:
  MyModal(std::wstring name, int number);
  std::wstring getName();
};

Then the instantiation by the parent window, with a more elaborated example:

on_message(WM_COMMAND, [&](wl::params p)->LRESULT
{
  if (LOWORD(p.wParam) == IDC_BTNSHOWMODAL)
  {
    MyModal modalDlg(L"Hello modal", 800); // instantiate the modal
    if (modalDlg.show(this) != IDCANCEL)
    {
      std::wstring name = modalDlg.getName();
    }
    return 0;
  }
  return DefWindowProc(hwnd(), p.message, p.wParam, p.lParam);
});

Modal dialogs can pop other modal dialogs, as well.

3.6. A modeless popup dialog

A modeless popup dialog differs from the modal, since modeless dialogs are created via CreateDialogParam. This is an example of a declaration:

// Declaration: MyModeless.h

#include "winlamb/dialog_modeless.h"

class MyModeless : public wl::dialog_modeless
{
public:
  MyModeless();
};

And the implementation, pretty much like the previous examples:

// Implementation: MyModeless.cpp

#include "MyModeless.h"
#include "resource.h"

MyModeless::MyModeless()
{
  setup.dialogId = IDD_DIALOG3;

  on_message(WM_INITDIALOG, [&](wl::params p)->INT_PTR
  {
    return TRUE;
  });
}

Very important: once a modeless dialog is created, it will live alongside its parent window. For this reason, attention must be paid to the declaration scope. The modeless object must be declared as a member of its parent:

// Declaration: MainWindow.h

#include "winlamb/window_main.h"
#include "MyModeless.h" // our modeless dialog header

class MainWindow : public wl::window_main
{
private:
  MyModeless mless; // modeless as a member
public:
  MainWindow();
};

This way, after we created it, the variable won’t go out of scope after the caller function returns:

// Implementation: MainWindow.cpp

#include "MainWindow.h"
RUN(MainWindow);

MainWindow::MainWindow()
{
  on_message(WM_CREATE, [&](wl::params p)->LRESULT
  {
    mless.show(this); // modeless dialog is now alive
    return 0;
  });
}

If we had declared mless object inside on_message’s lambda – just like we did on the previous example with the modal dialog –, mless would go out of scope right after the lambda returns, thus being destroyed while the modeless window is still alive. Then, as the modeless would come to process its first message, mless object would no longer exist.

As said in the beginning of this article: scope is very important with lambdas.

A modeless dialog is destroyed with a DestroyWindow call. By default, WinLamb handles WM_CLOSE with a call to DestroyWindow, so if you send WM_CLOSE to your modeless, it will be destroyed right away.

Now if you have used modeless windows with pure Win32, you may be wondering about the problems they introduce in the window message dispatching. Worry not: WinLamb was designed to treat these problems internally – this pain is gone.

3.7. A custom control

A custom control is a window designed to be a child of another window. It will inherit from window_control:

// Declaration: MyWidget.h

#include "winlamb/window_control.h"

class MyWidget : public wl::window_control
{
public:
  MyWidget();
};

The implementation can look like this:

// Implementation: MyWidget.cpp

#include "MyWidget.h"

MyWidget::MyWidget()
{
  setup.wndClassEx.lpszClassName = L"HAPPY_LITTLE_WIDGET";
  setup.wndClassEx.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_BTNFACE + 1);
  setup.exStyle |= WS_EX_CLIENTEDGE;
  setup.style |= WS_TABSTOP | WS_GROUP | WS_HSCROLL;

  on_message(WM_PAINT, [&](wl::params p)->LRESULT
  {
    PAINTSTRUCT ps = { 0 };
    HDC hdc = BeginPaint(hwnd(), &ps);
    EndPaint(hwnd(), &ps);
    return 0;
  });

  on_message(WM_ERASEBKGND, [&](wl::params p)->LRESULT
  {
    return 0;
  });
}

For the very same reasons of the aforementioned modeless dialog example, you must declare the child window as a member of the parent:

// Declaration: ParentWindow.h

#include "winlamb/window_main.h"
#include "MyWidget.h" // our custom control header

class ParentWindow : public wl::window_main
{
private:
  MyWidget widgetFoo1, widgetFoo2; // let's have two of them
public:
  ParentWindow();
};

To create the control, the parent must call its create member function, which receives: the this pointer of the parent, the control ID we want to give it, a SIZE for the control size, and a POINT for the position within the parent:

// Implementation: ParentWindow.cpp

#include "ParentWindow.h"
RUN(ParentWindow);

#define WIDG_FIRST  40001
#define WIDG_SECOND WIDG_FIRST + 1

ParentWindow::ParentWindow()
{
  setup.wndClassEx.lpszClassName = L"BEAUTIFUL_PARENT";

  on_message(WM_CREATE, [&](wl::params p)->LRESULT
  {
    widgetFoo1.create(this, WIDG_FIRST,  {10,10},  {150,100});
    widgetFoo2.create(this, WIDG_SECOND, {10,200}, {150,320});
    return 0;
  });
}

These are the predefined values of style for the window_control:

setup.wndClassEx.style = CS_DBLCLKS;
setup.style = CS_DBLCLKS | WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS;

3.8. Dialog as a control

It is also possible to embed a dialog as a child of a window, or as a child of another dialog. To build such a child dialog, inherit it from dialog_control class:

// Declaration: MyDlgWidget.h

#include "winlamb/dialog_control.h"

class MyDlgWidget : public wl::dialog_control
{
public:
  MyDlgWidget();
};

To work properly, a control dialog must have some specific styles – this is a requirement from Win32, not from WinLamb. With Visual Studio resource editor, these are the styles:

  • Border: none;
  • Control: true;
  • Style: child;
  • Visible: true (otherwise will start invisible);
  • Client Edge: true if you want a border (will add WS_EX_CLIENTEDGE).

Given the previous examples, I believe the implementation of a control dialog is trivial at this point of the article.

4. Cracking messages

Beyond providing the ability to use lambdas to handle messages, WinLamb also offers facilities to deal with the message contents.

4.1. Message parameters unpacked

When handling a message, your lambda always receive a single params argument:

on_message(WM_MENUSELECT, [&](wl::params p)->LRESULT
{
  return 0;
});

The params is a simple struct with 3 members, which are familiar to anyone who ever wrote a Win32 program:

struct params {
  UINT   message;
  WPARAM wParam;
  LPARAM lParam;
};

For Windows messages, the WPARAM and LPARAM members contain packed data, varying accordingly to the message being handled. For example, for WM_MENUSELECT, this is what they carry:

  • WPARAM, low-order word – menu item index;
  • WPARAM, high-order word – item state flag;
  • LPARAM – handle to clicked menu.

To retrieve these data, you must perform casts, extract bitflags, and be sure of what you’re doing. To alleviate this burden, WinLamb provides unpackers for (hopefully) all documented Windows messages. These unpackers are simply structs derived from params, adding the unpacking methods. They are enclosed within the wl::wm namespace.

For WM_MENUSELECT, this is an example of unpacking some parameters. The declaration of params p has the type replaced, becoming wm::menuselect p, and that’s all. Notice the methods being called on p:

on_message(WM_MENUSELECT, [&](wl::wm::menuselect p)->LRESULT
{
  if (p.is_checked() || p.has_bitmap()) {
    HMENU hMenu = p.hmenu();
    WORD itemIndex = p.item();
  }
  return 0;
});

This is functionally equivalent of writing:

on_message(WM_MENUSELECT, [&](wl::params p)->LRESULT
{
  if ((HIWORD(p.wParam) & MF_CHECKED) || (HIWORD(p.wParam) & MF_BITMAP)) {
    HMENU hMenu = reinterpret_cast<HMENU>(p.lParam);
    WORD itemIndex = LOWORD(p.wParam);
  }
  return 0;
});

4.2. Handling command messages

When handling WM_COMMAND messages, you must write a nested “big switch” to pick the control identifier. On a lambda message handler, it would look like this:

on_message(WM_COMMAND, [&](wl::params p)->LRESULT
{
  switch (LOWORD(p.wParam))
  {
  case IDC_BUTTON1: // pick control identifier
    // button click code...
    return 0;
  }
  return DefWindowProc(hwnd(), p.msg, p.wParam, p.lParam);
});

With the wm::command parameter unpacker, it looks slightly less ugly:

on_message(WM_COMMAND, [&](wl::wm::command p)->LRESULT
{
  switch (p.control_id())
  {
  case IDC_BUTTON1: // pick control identifier
    // button click code...
    return 0;
  }
  return DefWindowProc(hwnd(), p.msg, p.wParam, p.lParam);
});

But the “big switch” is still there. Enter the specific WM_COMMAND message handler. To use it, your window class must also inherit from msg_command class. Retaking the previous main window example, this is what we need to do:

// Declaration: MyWindow.h

#include "winlamb/window_main.h"
#include "winlamb/msg_command.h" // header for specific handler

class MyWindow :
  public wl::window_main,
  public wl::msg_command // inherit from this class too
{
public:
  MyWindow();
};

Now, besides on_message, our class has another method:

void on_command(WORD cmd, command_funcT func);

From now on, you must not handle WM_COMMAND anymore. The reason is: when you inherit from msg_command class, it adds a handler to WM_COMMAND, so it will handle this message for you. If you write another handler to WM_COMMAND, you will overwrite the previous one, thus on_command method will become useless.

And now we can handle WM_COMMAND messages at the same level of ordinary Windows messages:

// Implementation: MyWindow.cpp

#include "MyWindow.h"
#include "resource.h"
RUN(MyWindow);

MyWindow::MyWindow()
{
  setup.wndClassEx.lpszClassName = L"HAPPY_LITTLE_CLASS_NAME";

  on_message(WM_CREATE, [&](wl::params p)->LRESULT
  {
    // ordinary Windows message...
    return 0;
  });

  on_command(IDC_BUTTON1, [&](wl::wm::command p)->LRESULT
  {
    // here the IDC_BUTTON1 click code...
    return 0;
  });
}

Just like on_message, on_command also acceps an initializer_list to handle many messages at the same time:

on_command({IDC_BUTTON1, IDC_BUTTON2}, [&](wl::wm::command p)->LRESULT
{
  // click code for both buttons...
  return 0;
});

4.3. Handling Common Controls messages

Another message plagued by a nested “big switch” is WM_NOTIFY, which is fired by Common Controls. However, instead of one identifier, it has two, straight from NMHDR structure: idFrom and code. WinLamb also provides a specific message handler to it: msg_notify, which works in the same way of msg_command. This is the method signature:

void on_notify(UINT_PTR idFrom, UINT code, notify_funcT func);

NMHDR also has a hwndFrom member, but it’s not relevant for on_notify internal handling.

Now an example of declaration in our main window:

// Declaration: MyWindow.h

#include "winlamb/window_main.h"
#include "winlamb/msg_notify.h"

class MyWindow :
  public wl::window_main,
  public wl::msg_notify
{
public:
  MyWindow();
};

Handling a notification from a hypothetical listview control:

// Implementation: MyWindow.cpp

#include "MyWindow.h"
#include "resource.h"
RUN(MyWindow);

MyWindow::MyWindow()
{
  setup.wndClassEx.lpszClassName = L"HAPPY_LITTLE_CLASS_NAME";

  on_notify(IDC_LIST1, LVN_ITEMCHANGED, [&](wl::wm::notify p)->LRESULT
  {
    // listview itemchanged code...
    return 0;
  });
}

Just like on_command, on_notify also acceps an initializer_list to handle many messages at the same time. You must pass the identifiers as pairs, like this:

on_notify({ {IDC_LIST1, LVN_ITEMCHANGED},
            {IDC_LIST2, LVN_DELETEITEM} }, [&](wl::wm::notify p)->LRESULT
{
  // code for both events...
  return 0;
});

But there’s another particularity to WM_NOTIFY messages: each Common Control has its own messages. We already saw the message unpackers for ordinary Windows messages, and WinLamb also has unpackers to (hopefully) all documented Common Controls messages. They become available as you #include the header for msg_notify, and they’re also enclosed within wl::wm namespace.

Within wl::wm namespace, Common Controls messages have the same prefixes found in the original notifications themselves. For example listview notifications are prefixed with lvn_, month calendar with mcn_, and so on.

When using the unpacker for the bare WM_NOTIFY message, the p.nmhdr() method returns a reference to the NMHDR structure. When using an unpacker for a specific Common Control notification, the same nmhdr() method will return a reference to the specific structure of that notification – for example, LVN_ITEMCHANGED structure’s nmhdr() will bring NMLISTVIEW:

on_notify(IDC_LIST1, LVN_ITEMCHANGED, [&](wl::wm::lvn_itemchanged p)->LRESULT
{
  int itemIndex = p.nmhdr().iItem; // iItem is a member of NMLISTVIEW
  return 0;
});

Note that your window can inherit from msg_command and msg_notify at the same time.

WinLamb has only two specific message handlers, for WM_COMMAND and WM_NOTIFY. However, it’s possible to write specific handlers to any other message (I wrote some). If you want to do so, take a look at msg_command code – it would be pretty much a Ctrl+C/Ctrl+V plus adjustments.

4.4. Multithread and UI

Sometimes your program needs to perform a lengthy task. In order to not freeze the user interface (UI), leaving the program in an unresponsive state, it’s a good idea to run the task in background with a separated thread. However, no matter which function or library you use to launch the parallel tread, you should always update the UI from the original thread.

Discussion of the possible ways to launch and manage a separated thread is beyond the scope of this article, so I’ll show just one, which I consider to be very simple. Using the C++11 thread support, this is how you can launch a parallel detached thread, with your code inside a lambda, which will run free. This will allow the UI thread to remain responsive:

std::thread([]()->void
{
  // all the code inside this block
  //  will run in a parallel thread...
}).detach();

After your task is complete, however, you’ll almost certainly want to update the UI in some way. And you must do this in the original UI thread. WinLamb can ease this process: to achieve this, inherit your window class from msg_ui_thread class. With the main window example, that’s what we do:

// Declaration: MyWindow.h

#include "winlamb/window_main.h"
#include "winlamb/msg_ui_thread.h"

class MyWindow :
  public wl::window_main,
  public wl::msg_ui_thread
{
public:
  MyWindow();
};

The new method added to your class receives just a function, which will immediately execute in the same original thread of the UI:

void on_ui_thread(ui_thread_funcT func);

The call to on_ui_thread is synchronous – subsequent code won’t execute until on_ui_thread returns. This allows you to write code on a parallel thread updating the UI at multiple moments. An ellaborated example:

// Implementation: MyWindow.cpp

#include <thread>
#include "MyWindow.h"
RUN(MyWindow);

MyWindow::MyWindow()
{
  setup.wndClassEx.lpszClassName = L"HAPPY_LITTLE_CLASS_NAME";

  on_message(WM_LBUTTONDOWN, [&](wl::params p)->LRESULT
  {
    // preliminar code can go here...

    std::thread([]()->void
    {
      // parallel thread, code for the slow task goes here...
      Sleep(10000);

      on_ui_thread([&]()->void
      {
        // this code runs on UI thread...
        SetWindowText(hwnd(), L"Processing things");
      });

      // more parallel code, will run only after on_ui_thread() returns...
      Sleep(10000);

      on_ui_thread([&]()->void
      {
        // more updates on UI thread...
        SetWindowText(hwnd(), L"Process finished");
      });
    }).detach();

    // code written in this area will run in parallel with the thread,
    //  so don’t write anything here unless you know what you’re doing...

    return 0;
  });
}

Note: the code above is simplified, and doesn’t check for potential bugs.

5. Subclassing controls

Control subclassing is an operation normally done with the aid of SetWindowSubclass Win32 function. You provide a SUBCLASSPROC callback function, which is very similar to WNDPROC and DLGPROC, and handle specific messages from there.

5.1. Installation and message handling

WinLamb’s approach to control subclassing is to instantiate an object of subclass type, then attach it to an existing control. For example, retaking the edit control example, let’s subclass the edit control. First, we add a subclass member to our class:

// Declaration: FirstDialog.h

#include "winlamb/dialog_main.h"
#include "winlamb/native_control.h"
#include "winlamb/subclass.h"

class FirstDialog : public wl::dialog_main
{
private:
  wl::native_control edit1; // our control object
  wl::subclass edit1sub; // edit subclasser object
public:
  FirstDialog();
};

To handle the methods within the subclassing, we just call on_message on the subclass object. It works the same way of windows and dialogs, except we’re handling a message from SUBCLASSPROC callback procedure:

edit1sub.on_message(WM_RBUTTONDOWN, [&](wl::params p)->LRESULT
{
  // subclass code...
  return 0;
});

After adding the messages, we’ll have a subclass object stuffed with handlers, but inert. We install the subclass after we initialize the control. This is the full implementation:

// Implementation: FirstDialog.cpp

#include "FirstDialog.h"
#include "resource.h"
RUN(FirstDialog);

FirstDialog::FirstDialog()
{
  setup.dialogId = IDD_MY_FIRST_DIALOG;

  edit1sub.on_message(WM_RBUTTONDOWN, [&](wl::params p)->LRESULT
  {
    // subclass code for edit1...
    return 0;
  });

  on_message(WM_INITDIALOG, [&](wl::params p)->INT_PTR
  {
    edit1.assign(this, IDC_EDIT1); // init control
    edit1sub.install_subclass(edit1); // subclass installed and ready
    return TRUE;
  });
}

Note that, in this example, we added the edit1sub handler before the WM_INITIDALOG, but this is not required. Since the lambdas are called asynchronously, the order we attach them doesn’t matter. We can attach the edit1sub messages after WM_INITDIALOG as well, in any order.

5.2. Using specific handlers

When adding handlers to windows and dialogs, we optionally have two specific addendums to dig into WM_COMMAND and WM_NOTIFY messages. It’s also possible to have these extensions with the subclass object, but in order to do so, we need to declare a new class, extended with the new functionalities we want.

In an exaggerated example, if we want to subclass a control, and within this subclass we want to handle both WM_COMMAND and WM_NOTIFY messages, we can declare this on our code:

class MyCommandNotifySubclass :
  public subclass,
  public msg_command, // adds on_command() method
  public msg_notify   // adds on_notify() method
{
public:
  using msg_command::on_command;
  using msg_notify::on_notify;
};

The we declare the objects:

native_control edit1;
MyCommandNotifySubclass edit1sub;

Install the subclass normally:

edit1sub.install_subclass(edit1);

And add the handlers, for example:

edit1sub.on_message(WM_RBUTTONDOWN, [&](wl::params p)->LRESULT
{
  return 0;
});

edit1sub.on_command(IDOK, [&](wl::params p)->LRESULT
{
  return 0;
});

edit1sub.on_command(IDCANCEL, [&](wl::wm::command p)->LRESULT
{
  return 0;
});

edit1sub.on_notify(SOME_CRAZY_ID, LVN_ANYMSG, [&](wl::params p)->LRESULT
{
  return 0;
});

And, of course, if you have any other specific message handlers, they can be used as well.

6. Final topics

6.1. Window types summary

Summing up, these are all WinLamb window base classes your window can inherit from:

You also have native_control, which is a simple wrapper to any native Windows control and can be used as a base class to specialized widget classes, and subclass, which subclasses a control.

To handle specific WM_COMMAND messages, inherit your window also from msg_command. To handle specific WM_NOTIFY messages, use msg_notify. And to add multithreading support, use msg_ui_thread.

6.2. Default message processing

The window classes provide default processing for some messages. If you write a handler to one of these messages, the default processing will be overwritten. Below is a list of the messages which have a default processing, and what they do:

6.3. More

Originally, WinLamb started a humble collection of classes for my own code reuse. As the time passed by, it started evolving into a huge, monolythic “save the world” library. I started willing to share the code, but the way it was going, it would be never ready, and it would always have too many holes. So I took the core, as simple and concise as it could be, and made a library out of it: WinLamb was born.

All the other classes were dumped into another library, which can be seen as an addendum to WinLamb. It’s also available on GitHub: it’s named WinLamb More. To be used, the directory must be side-by-side with WinLamb’s. Once downloaded, it should work right away. However I consider it to be very incomplete, and honestly I don’t know if it will ever be “complete” some day, because it grows only as I need it to, like a perpetual “work in progress” – Win32 API is way too vast. But you may be interested in one class or another, so they’re all there.

Finally, in this article, no discussion has been made upon design choices within the library. That would make the article excessively long. I believe this topic deserves a whole article for itself, which I can consider writing if there’s enough interest. However, I’m open to discuss my architectural and implementation decisions.

7. History

2017.04.26 – First version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Rodrigo Cesar de Freitas Dias
Systems Engineer
Brazil Brazil
No Biography provided

You may also be interested in...

Pro
Pro

Comments and Discussions

 
QuestionCan WinLamb be used in a DLL? Pin
Jon Summers27-Apr-17 4:11
memberJon Summers27-Apr-17 4:11 
AnswerRe: Can WinLamb be used in a DLL? Pin
Rick York27-Apr-17 5:25
memberRick York27-Apr-17 5:25 
AnswerRe: Can WinLamb be used in a DLL? Pin
Rick York27-Apr-17 6:36
memberRick York27-Apr-17 6:36 
GeneralWykobi Pin
Jon Summers1-May-17 3:05
memberJon Summers1-May-17 3:05 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.171019.1 | Last Updated 26 Apr 2017
Article Copyright 2017 by Rodrigo Cesar de Freitas Dias
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid