0

First off, I want to say, for anyone who is going to say "why am I reinventing the wheel", I am doing this for fun, and for a project of mine I'm currently working on.

As you can see from the code below, I am trying to dynamically create a window and a button, but something I'm having trouble with is adding a function to that button when it's clicked.

I know it would be really simple to go into the window procedure in WM_COMMAND and do so there, but that's missing the whole point of what I'm trying to accomplish here, have it so I can simple call btn.add(params) to a certain window, and add a certain function to that button by calling say btn.click(function); without having to go into the window procedure itself when adding the controls.

How would I accomplish this?

#include <Windows.h>
#include <vector>
#include <thread>

using namespace std;

WNDCLASSEX defWndClass = { 0 };

class WinForm
{
private:
    HWND WindowHandle;
    std::thread Thread;
    std::vector<std::tuple<std::string, std::size_t, HWND>> ControlHandles;

public:
    ~WinForm();
    WinForm(std::string ClassName, std::string WindowName, bool Threaded = false, int Width = CW_USEDEFAULT,
        int Height = CW_USEDEFAULT, WNDPROC WindowProcedure = nullptr, WNDCLASSEX WndClass = defWndClass);
    bool AddButton(std::string ButtonName, POINT Location, int Width, int Height);
};

WinForm::~WinForm()
{
    if (Thread.joinable())
    {
        Thread.join();
    }
}

WinForm::WinForm(std::string ClassName, std::string WindowName, bool Threaded, int Width, int Height, WNDPROC WindowProcedure, WNDCLASSEX WndClass)
    :WindowHandle(nullptr)
{
    if (WindowProcedure == nullptr)
    {
        WindowProcedure = [](HWND window, UINT msg, WPARAM wp, LPARAM lp)->LRESULT __stdcall
        {
            switch (msg)
            {
                /*
                case WM_PAINT:
                    break;
                    */

            case WM_DESTROY:
                PostQuitMessage(0);
                return 0;

            case WM_CREATE:
                break;

            default:
                return DefWindowProc(window, msg, wp, lp);
            }
            return 0;
        };
    }

    if (WndClass.cbSize == 0)
    {
        WndClass.cbSize = sizeof(WNDCLASSEX);
        WndClass.style = CS_DBLCLKS;
        WndClass.lpfnWndProc = WindowProcedure;
        WndClass.cbClsExtra = 0;
        WndClass.cbWndExtra = 0;
        WndClass.hInstance = GetModuleHandle(nullptr);
        WndClass.hIcon = LoadIcon(nullptr, IDI_APPLICATION);
        WndClass.hCursor = LoadCursor(nullptr, IDC_ARROW);
        WndClass.hbrBackground = HBRUSH(COLOR_WINDOW + 1);
        WndClass.lpszMenuName = nullptr;
        WndClass.lpszClassName = ClassName.c_str();
        WndClass.hIconSm = LoadIcon(nullptr, IDI_APPLICATION);
    }

    if (RegisterClassEx(&WndClass))
    {
        if (Threaded)
        {
            // can't do that!
        }
        else
        {
            WindowHandle = CreateWindowEx(0, ClassName.c_str(), WindowName.c_str(), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, Width, Height, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
            if (WindowHandle)
            {
                ShowWindow(WindowHandle, SW_SHOWDEFAULT);

                // don't put message loop here!
            }
        }
    }
}

bool WinForm::AddButton(std::string ButtonName, POINT Location, int Width, int Height)
{
    for (std::vector<std::tuple<std::string, std::size_t, HWND>>::iterator it = ControlHandles.begin(); it != ControlHandles.end(); ++it)
    {
        auto& tu = *it;
        auto& str = std::get<0>(tu);
        if (ButtonName.compare(str) == 0) {
            return false;
        }
    }

    std::size_t ID = 1;
    for (std::vector<std::tuple<std::string, std::size_t, HWND>>::iterator it = ControlHandles.begin(); it != ControlHandles.end(); ++it, ++ID)
    {
        if (std::get<1>(*it) != ID)
        {
            break;
        }
    }

    HWND ButtonHandle = CreateWindowEx(
        0, "button", ButtonName.c_str(), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, Location.x, Location.y, Width, Height,
        WindowHandle, (HMENU)ID, (HINSTANCE)GetWindowLong(WindowHandle, GWL_HINSTANCE), nullptr);
    ShowWindow(ButtonHandle, SW_SHOW);
    ControlHandles.push_back(std::make_tuple(ButtonName, ID, ButtonHandle));

    //SendMessage(WindowHandle, WM_CREATE, 0, 0);
    return true;
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
    WinForm Form("Class", "Title", false);
    POINT pt = { 50, 50 };
    Form.AddButton("NewButton", pt, 80, 50);

    MSG msg = { nullptr };
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

}
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Joshua
  • 56
  • 8
  • 2
    One way to do this is to have the window procedure consult a map from buttons to functions. When you add a button, add an entry to the map. – Raymond Chen Nov 17 '18 at 02:45
  • 1
    No matter what you do, the parent window of the button is going to receive a [`BN_CLICKED`](https://learn.microsoft.com/en-us/windows/desktop/controls/bn-clicked) notification via `WM_COMMAND` when the button is clicked. If the parent window doesn't handle the message, it ends up going to `DefWindowProc()` and gets ignored. So, the parent window needs to handle `WM_COMMAND`. When the notification code is `BN_CLICKED`, the parent can look up the provided `HWND` in its list of buttons, and if found then call the corresponding function if one has been assigned. – Remy Lebeau Nov 17 '18 at 02:51
  • Could you possibly give an example of what you're talking about? – Joshua Nov 17 '18 at 03:12
  • 1
    Hate to be the one, but I'll be the one to throw cold water on this project. To create a C++ wrapper for the Windows API, you better know the Windows API itself from its `C`-based roots. That means having concrete `C`-based examples that work, plus having books is the way to learn the API. Otherwise you totally miss the things mentioned by @RemyLebeau. – PaulMcKenzie Nov 17 '18 at 03:22
  • https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/NativeMethods.cs,c038d2f6b223a3ca,references – Hans Passant Nov 17 '18 at 07:43
  • Not a problem to reinvent the wheel but why is this ugly mix of C and C++? Maybe you cannot replace 3-4 "C++" lines with pure C? For that reason I will not try to find the problem. – i486 Jul 06 '23 at 09:35

1 Answers1

3

The parent window of the button is going to receive a BN_CLICKED notification via WM_COMMAND when the button is clicked. If the parent window doesn't handle the message, it ends up going to DefWindowProc() and gets ignored. So, the parent window needs to handle WM_COMMAND. When the notification code is BN_CLICKED, simply lookup the provided button HWND in your list of buttons, and if found then call the corresponding function, if one has been assigned.

#include <Windows.h>
#include <vector>
#include <thread>
#include <algorithm>
#include <functional>

class WinForm
{
public:
    using ControlActionFunc = std::function<void(const std::string &)>;

    WinForm(std::string ClassName, std::string WindowName, bool Threaded = false, int Width = CW_USEDEFAULT, int Height = CW_USEDEFAULT);

    ~WinForm();

    bool AddButton(const std::string &ButtonName, POINT Location, int Width, int Height, ControlActionFunc OnClick);

private:
    HWND WindowHandle;
    std::thread Thread;

    using ControlInfo = std::tuple<std::string, std::size_t, HWND, ControlActionFunc>;
    using ControlInfoVector = std::vector<ControlInfo>;
    ControlInfoVector Controls;

    static LRESULT __stdcall StaticWindowProcedure(HWND window, UINT msg, WPARAM wp, LPARAM lp);

protected:
    virtual LRESULT WindowProcedure(UINT msg, WPARAM wp, LPARAM lp);
};

class MainAppWinForm : public WInForm
{
public:
    using WinForm::WinForm;

protected:
    LRESULT WindowProcedure(UINT msg, WPARAM wp, LPARAM lp) override;
};

WinForm::WinForm(std::string ClassName, std::string WindowName, bool Threaded, int Width, int Height)
    : WindowHandle(nullptr)
{
    HINSTANCE hInstance = GetModuleHandle(nullptr);
    WNDCLASSEX WndClass = {};

    bool isRegistered = GetClassInfoEx(hInstance, ClassName.c_str(), &WndClass);
    if ((!isRegistered) || (WndClass.lpfnWndProc != &WinForm::StaticWindowProcedure))
    {
        if (isRegistered)
            UnregisterClass(ClassName.c_str(), hInstance);

        WndClass.cbSize = sizeof(WNDCLASSEX);
        WndClass.style = CS_DBLCLKS;
        WndClass.lpfnWndProc = &WinForm::StaticWindowProcedure;
        WndClass.cbClsExtra = 0;
        WndClass.cbWndExtra = sizeof(WinForm*);
        WndClass.hInstance = hInstance;
        WndClass.hIcon = LoadIcon(nullptr, IDI_APPLICATION);
        WndClass.hCursor = LoadCursor(nullptr, IDC_ARROW);
        WndClass.hbrBackground = HBRUSH(COLOR_WINDOW + 1);
        WndClass.lpszMenuName = nullptr;
        WndClass.lpszClassName = ClassName.c_str();
        WndClass.hIconSm = LoadIcon(nullptr, IDI_APPLICATION);

        if (!RegisterClassEx(&WndClass))
            return;
    }

    if (Threaded)
    {
        // can't do that!
        return;
    }

    WindowHandle = CreateWindowEx(0, ClassName.c_str(), WindowName.c_str(), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, Width, Height, nullptr, nullptr, hInstance, this);
    if (!WindowHandle)
        return;

    ShowWindow(WindowHandle, SW_SHOWDEFAULT);

    // don't put message loop here!
}

WinForm::~WinForm()
{
    if (Thread.joinable())
    {
        Thread.join();
    }
}

LRESULT __stdcall WinForm::StaticWindowProcedure(HWND window, UINT msg, WPARAM wp, LPARAM lp)
{
    WinForm *This;
    if (msg == WM_NCCREATE)
    {
        This = static_cast<WinForm*>(reinterpret_cast<CREATESTRUCT*>(lp)->lpCreateParams);
        This->WindowHandle = window;
        SetWindowLongPtr(window, 0, reinterpret_cast<LONG_PTR>(This));
    }
    else
        This = reinterpret_cast<WinForm*>(GetWindowLongPtr(window, 0));

    if (This)
        return This->WindowProcedure(msg, wp, lp);

    return DefWindowProc(window, msg, wp, lp);
}

LRESULT WinForm::WindowProcedure(UINT msg, WPARAM wp, LPARAM lp)
{
    switch (msg)
    {
        /*
        case WM_PAINT:
            break;
        */

        case WM_COMMAND:
        {
            if (lp != 0)
            {
                if (HIWORD(wp) == BN_CLICKED)
                {
                    HWND ControlWindow = reinterpret_cast<HWND>(lp);

                    auto it = std::find_if(Controls.begin(), Controls.end(),
                        [](ControlInfo &info){ return (std::get<2>(info) == ControlWindow); }
                    );

                    if (it != Controls.end())
                    {
                        auto &tu = *it;
                        auto actionFunc = std::get<3>(tu);
                        if (actionFunc) actionFunc(std::get<0>(tu));
                        return 0;
                    }
                }
            }
            break;
        }

        case WM_CREATE:
            break;
    }

    return DefWindowProc(WindowHandle, msg, wp, lp);
}

bool WinForm::AddButton(const std::string &ButtonName, POINT Location, int Width, int Height, ControlActionFunc OnClick)
{
    auto it = std::find_if(Controls.begin(), Controls.end(),
        [&](ControlInfo &info){ return (std::get<0>(info).compare(ButtonName) == 0); }
    );

    if (it != Controls.end()) {
        return false;
    }

    std::size_t ID = 1;
    auto matchesID = [&](ControlInfo &info){ return (std::get<1>(tu) == ID); };
    while (std::find_if(Controls.begin(), Controls.end(), matchesID) != Controls.end()) {
        ++ID;
    }

    HWND ButtonHandle = CreateWindowEx(
        0, "button", ButtonName.c_str(), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, Location.x, Location.y, Width, Height,
        WindowHandle, (HMENU)ID, (HINSTANCE)GetWindowLong(WindowHandle, GWL_HINSTANCE), nullptr);
    if (!ButtonHandle)
        return false;

    Controls.push_back(std::make_tuple(ButtonName, ID, ButtonHandle, std::move(OnClick)));
    return true;
}

LRESULT MainAppWinForm::WindowProcedure(UINT msg, WPARAM wp, LPARAM lp)
{
    if (msg == WM_DESTROY)
    {
        PostQuitMessage(0);
        return 0;
    }
    return WinForm::WindowProcedure(msg, wp, lp);
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
    MainAppWinForm Form("Class", "Title", false);

    POINT pt = { 50, 50 };
    Form.AddButton("NewButton", pt, 80, 50,
        [](const std::string &ButtonName){ MessageBox(NULL, ButtonName.c_str(), "button clicked", MB_OK); }
    );

    MSG msg = { nullptr };
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}

Also see

Class method for WndProc

Win32: More "object oriented" window message handling system

Best method for storing this pointer for use in WndProc

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • I couldn't compile your code on VS2017. I made some changes but there is a main issue, in that when `This->WindowProcedure(msg, wp, lp);` is called at first, there is no `WindowHandle`, subsequently `CreateWindowEx` fails – Barmak Shemirani Nov 17 '18 at 06:55
  • 1
    @BarmakShemirani I wrote the code from memory, I didn't try to compile it. Wouldn't surprise me if it needs further tweaking. But it should be clear what the intent of the code is trying to demonstate. I fixed the `WindowHandle` issue, though. – Remy Lebeau Nov 17 '18 at 07:11
  • 1
    for top-level windows first message is `WM_GETMINMAXINFO` (before `WM_NCCREATE`), as result will be ignored. if want handle it, and any possible messages before `WM_NCCREATE`, too save pointer to `WinForm` in tls, in `StaticWindowProcedure` - get it from tls, assign to window via `SetWindowLongPtr` and change `GWLP_WNDPROC` to say `StaticWindowProcedure2`. inside this we already unconditionally got pointer to `WinForm` via `GetWindowLongPtr` - never will be 0. – RbMm Nov 17 '18 at 09:53
  • I see in order for this to work you would have to use WndClass.cbWndExtra = sizeof(WinForm*) or it will not work. Now that's an issue because when you close one window with this method every window you have will also close. – Joshua Nov 17 '18 at 21:16
  • 1
    @Joshua you don't have to use `cbWndExtra`, `GWLP_USERDATA` will work, too. All HWNDs have `GWLP_USERDATA` available for free, which makes it appealing for users to use. In this case, I chose to use `cbWndExtra` to keep the `WinForm*` pointer private and leave `GWLP_USERDATA` free for the caller to use for its own purposes. And no, closing one window does not affect other windows. Every individual `WinForm` HWND allocated in this code has its own distinct `WinForm*` pointer assigned to it. There is no linkage between multiple `WinForm` instances – Remy Lebeau Nov 17 '18 at 22:21
  • When I create another instance of the window and close it, with the proper destruction in WM_DESTROY being PostQuitMessage(0); Every instance of the window destroys along with the one I distinctly closed. Run the code yourself and you will see if you create another window and close one of those two, they will both close. – Joshua Nov 17 '18 at 22:28
  • @Joshua if you run multiple `WinForm` instances, then you should not be calling `PostQuitMessage()` in response to `WM_DESTROY` unconditionally. That posts a `WM_QUIT` message that ends the message loop in `main()`. Don't call `PostQuitMessage()` until you are ready to exit your app, such as when your "main window" is closed. See [Closing the window](https://docs.microsoft.com/en-us/windows/desktop/learnwin32/closing-the-window): "*In your **main application window**, you will typically respond to WM_DESTROY by calling PostQuitMessage.*" I tweaked my example to distinguish this responsibility. – Remy Lebeau Nov 18 '18 at 01:17