Нестатическая callback функция-член

283
24 апреля 2017, 04:58

Начал разбираться с WinAPIи столкнулся со следующей проблемой:
При создании класса (WNDCLASS) ему нужно указать функцию-обработчик событий (вероятно это по другому называется, но вы поняли) LRESULT CALLBACK wndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam). Для чистоты кода я решил вынести все, что связано с этим окном в отдельный класс MainWindow.
Тут-то и возникла проблема. Если сделать данную функцию членом класса, то компилятор начинает ругаться на каст:

WNDCLASS wc;
...
wc.lpfnWndProc = static_cast<WNDPROC> (wndProc);  

Ошибка C2440 static_cast: невозможно преобразовать "overloaded-function" в "WNDPROC" MyProjectName c:\...\mainwindow.cpp 22

Перебрал несколько очевидных вариантов решения данной проблемы: сделать функцию статической, дружественной, или использовать лямбду, но дело в том, что в данном окне должны рисоваться объекты(например круг). Соответственно, их экземпляры должны храниться в классе окна, чтобы при WM_PAINT просто вежливо попросить их заново нарисоваться и никаких проблем не было. Тогда это отметает все 3 моих варианта т.к. ни один из них не может напрямую взаимодействовать с нестатическими свойствами класса.
Есть еще 2 варианта, но они ужасные: сделать поля, нужные для перерисовки, статическими или заново создавать их при перерисовки. Даже комментировать это не буду.
Как можно решить эту проблему?
P.S. Вот код реализации класса, если кому-то нужно

Answer 1

Никак нельзя сделать обратный вызов на метод. Можно запоминать для каждого окна Windows указатель на соответствующий ему объект MainWindow с помощью функции SetWindowLongPtr с параметром GWLP_USERDATAи сделать общую функцию WNDPROC для всех окон, внутри которой получать с помощью функции GetWindowLongPtr этот указатель, преобразовывать к типу MainWindow* и запускать на нем какой-то метод.

То есть, вызов метода класса будет осуществляться примерно так:

LRESULT MainWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    MainWindow *mainwin = static_cast<MainWindow*>(
        GetWindowLongPtr(hwnd, GWLP_USERDATA));
    return mainwin->wndProc(hwnd, uMsg, wParam, lParam);
}

Тем не менее, я сталкивался с одним показательным случаем, когда это не работает. Моя программа (VST-плагин) получала готовое окно HWND от VST-хоста (сторонняя программа). Все работало, пока я не наткнулся на VST-хост, разработчик которого тоже захотел использовать GWL_USERDATA.

То есть более общий вариант решения - использовать хэш (окно=>объект).

Answer 2

Тем не менее, я сталкивался с одним показательным случаем, когда это не работает. Моя программа (VST-плагин) получала готовое окно HWND от VST-хоста (сторонняя программа). Все работало, пока я не наткнулся на VST-хост, разработчик которого тоже захотел использовать GWL_USERDATA.

Я, собственно, поддерживаю ответ @Alexander Zonov, он верный. Добавлю только как быть ситуации, когда нет возможности использовать GWL_USERDATA.

В WNDCLASS есть поля:

int       cbClsExtra;
int       cbWndExtra;

они используются как раз тогда, когда необходимо в объекте класса или в объекте окна хранить дополнительные данные. По умолчанию, любое окно уже имеет некоторые данные и мы можем к ним обратиться через GetWindowLongPtr/SetWindowLongPtr. Константы GWLP_xxx - это всего лишь смещения внутри объекта окна на эти данные. Для того, чтобы их не путать с пользовательскими данными, все служебные смещения сделали отрицательными (просто сдвинули все служебные данные в отрицательную плоскость).

В нашем случае, для обеспечения стабильной работы необходимо зарезервировать в объекте окна размер указателя на объект С++:

wc.cbWndExtra = sizeof(void*);

после регистрации такого класса окна, любое окно этого класса будет иметь дополнительные 4/8 байт, к которым можно будет обратиться через GetWindowLongPtr/SetWindowLongPtr, передавая в качестве смещения значение 0.

Таким образом, без GWL_USERDATA можно вполне обойтись.

Есть еще вариант, когда и этот сценарий не сработает: когда необходимо наследовать класс от другого класса, который уже использует дополнительные данные окна. Это достаточно распространенный вариант, за которым даже закреплен устоявшийся термин - superclassing.

Но тут тоже все решается довольно просто: надо запомнить значение cbWndExtra предка, использовать его в качестве смещения для GetWindowLongPtr/SetWindowLongPtr, а перед регистрацией класса окна просто расширить размер объекта окна:

GetClassInfo(..., &wc);
...
wc.cbWndExtra += sizeof(void*);

Т.е. оконные классы Windows достаточно гибкие. При желании, можно даже полиморфизм организовать, например: хранить в дополнительных данных класса окна указатель на функцию, которую потомки будут переписывать. Этот функционал аналогично работает через GetClassLongPtr/SetClassLongPtr.

Answer 3

Ну, хоть и не совсем по делу, вынужден как ответ из-за объема.
Рассказываю о синглтоне. Если у вас есть класс, который должен иметь единственный экземпляр - то запрещаем его копирование, а конструктор делаем закрытым. Этот единственный экземпляр делается статической переменной:

class Singleton
{
    Singleton(){};
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    ~Singleton(){}
public:
    static Singleton& get()
    {
        static Singleton s;
        return s;
    }
    void out() { cout << "It's me :)\n"; }
};

int main(int argc, const char * argv[])
{
    Singleton& s = Singleton::get();
    s.out();
}

Примерно так. Вы не сможете ни создать, ни скопировать новый объект Singleton. Зато любая функция знает, как к нему обратиться - через вызов Singleton::get().

Ну нет дополнительного параметра в обработчике сообщений...

Еще вариант - привязать объект к дескриптору окна, но, по-моему, это хоть и несколько обобщеннее, но ничуть не лучше и уж точно сложнее...
P.S. Пока писал - этот последний способ привязки предложил в своем ответе @Alexander Zonov.

READ ALSO
Где здесь ошибка?

Где здесь ошибка?

Пожалуйста помогите найти мою ошибку

321
C++ iterator для своего контейнера

C++ iterator для своего контейнера

Здравствуйте! Я написал свой класс-контейнерИ теперь мне нужно написать класс iterator, чтобы мой контейнер смог взаимодействовать с алгоритмами...

295
Структуры с++ Visual Studio

Структуры с++ Visual Studio

Создал стуктуруВвожу 2 элемента f

218
Проблема с CLion, Makefile

Проблема с CLion, Makefile

При попытки собрать проект, я поучаю следующую ошибку

473