Класс динамической оболочки C++ WINAPI

Прежде всего, я хочу сказать всем, кто собирается сказать «зачем я изобретаю велосипед», я делаю это для развлечения и для своего проекта, над которым я сейчас работаю.

Как видно из приведенного ниже кода, я пытаюсь динамически создать окно и кнопку, но у меня возникают проблемы с добавлением функции к этой кнопке при ее нажатии.

Я знаю, что было бы очень просто войти в оконную процедуру в WM_COMMAND и сделать это там, но здесь отсутствует вся суть того, что я пытаюсь выполнить здесь, так что я могу просто вызвать btn.add(params) в определенное окно, и добавить определенную функцию к этой кнопке, вызвав, скажем, btn.click(function);, без необходимости входить в саму оконную процедуру при добавлении элементов управления.

Как бы я этого добился?

#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);
    }

}

Один из способов сделать это - заставить оконную процедуру сверяться с картой от кнопок к функциям. Когда вы добавляете кнопку, добавьте запись на карту.

Raymond Chen 17.11.2018 03:45

Независимо от того, что вы делаете, родительское окно кнопки будет получать уведомление BN_CLICKED через WM_COMMAND при нажатии кнопки. Если родительское окно не обрабатывает сообщение, оно переходит к DefWindowProc() и игнорируется. Итак, родительское окно должно обрабатывать WM_COMMAND. Когда код уведомления - BN_CLICKED, родитель может найти предоставленный HWND в своем списке кнопок и, если он найден, вызвать соответствующую функцию, если она была назначена.

Remy Lebeau 17.11.2018 03:51

Не могли бы вы привести пример того, о чем говорите?

Joshua 17.11.2018 04:12

Ненавижу быть одним из них, но я тот, кто проливает холодную воду на этот проект. Чтобы создать оболочку C++ для Windows API, вам лучше знать сам Windows API по его корням на основе C. Это означает, что наличие конкретных работающих примеров на основе C, а также наличие книг - способ изучить API. В противном случае вы полностью упустите то, что упоминает @RemyLebeau.

PaulMcKenzie 17.11.2018 04:22
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
792
1

Ответы 1

Родительское окно кнопки получит уведомление BN_CLICKED через WM_COMMAND при нажатии кнопки. Если родительское окно не обрабатывает сообщение, оно переходит к DefWindowProc() и игнорируется. Итак, родительское окно должно обрабатывать WM_COMMAND. Если код уведомления - BN_CLICKED, просто найдите предоставленную кнопку HWND в своем списке кнопок, и, если она найдена, вызовите соответствующую функцию, если она была назначена.

#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;
}

Также см

Метод класса для WndProc

Win32: более "объектно-ориентированная" система обработки оконных сообщений.

Лучший способ сохранить этот указатель для использования в WndProc

Я не смог скомпилировать ваш код на VS2017. Я внес некоторые изменения, но есть основная проблема, заключающаяся в том, что при первом вызове This->WindowProcedure(msg, wp, lp);WindowHandle отсутствует, а затем CreateWindowEx выходит из строя.

Barmak Shemirani 17.11.2018 07:55

@BarmakShemirani Я писал код по памяти, компилировать не пробовал. Не удивлюсь, если потребуется дополнительная настройка. Но должно быть ясно, что пытается продемонстрировать код. Однако я исправил проблему с WindowHandle.

Remy Lebeau 17.11.2018 08:11

для окон верхнего уровня первое сообщение - WM_GETMINMAXINFO (до WM_NCCREATE), поэтому результат будет проигнорирован. если вы хотите обработать это и любые возможные сообщения перед WM_NCCREATE, также сохраните указатель на WinForm в tls, в StaticWindowProcedure - получите его из tls, назначьте окну через SetWindowLongPtr и измените GWLP_WNDPROC на StaticWindowProcedure2. внутри мы уже безоговорочно получили указатель на WinForm через GetWindowLongPtr - никогда не будет 0.

RbMm 17.11.2018 10:53

Я вижу, чтобы это сработало, вам придется использовать WndClass.cbWndExtra = sizeof (WinForm *), иначе это не сработает. Теперь это проблема, потому что, когда вы закрываете одно окно с помощью этого метода, каждое ваше окно также закрывается.

Joshua 17.11.2018 22:16

@Joshua, вам не обязательно использовать cbWndExtra, GWLP_USERDATA тоже подойдет. GWLP_USERDATA доступен для всех HWND бесплатно, что делает его привлекательным для пользователей. В этом случае я решил использовать cbWndExtra, чтобы сохранить указатель WinForm* в секрете и оставить GWLP_USERDATA свободным для использования вызывающей стороной в своих целях. И нет, закрытие одного окна не влияет на другие окна. Каждому индивидуальному WinForm HWND, выделенному в этом коде, назначен свой собственный индивидуальный указатель WinForm*. Нет связи между несколькими экземплярами WinForm

Remy Lebeau 17.11.2018 23:21

Когда я создаю еще один экземпляр окна и закрываю его, с правильным уничтожением в WM_DESTROY является PostQuitMessage (0); Каждый экземпляр окна разрушается вместе с тем, что я закрыл отчетливо. Запустите код самостоятельно, и вы увидите, что если вы создадите другое окно и закроете одно из этих двух, они оба закроются.

Joshua 17.11.2018 23:28

@Joshua, если вы запускаете несколько экземпляров WinForm, вам не следует безоговорочно вызывать PostQuitMessage() в ответ на WM_DESTROY. Это отправляет сообщение WM_QUIT, которое завершает цикл сообщений в main(). Не вызывайте PostQuitMessage(), пока не будете готовы выйти из приложения, например, когда ваше «главное окно» закрыто. См. Закрытие окна: «В вашем главное окно приложения вы обычно отвечаете на WM_DESTROY, вызывая PostQuitMessage.» Я изменил свой пример, чтобы выделить эту ответственность.

Remy Lebeau 18.11.2018 02:17

Другие вопросы по теме