WinApi obiektowo - organizacja kodu

0

Cześć, uczę się WinApi i zacząłem się zastanawiać czy jest możliwe zorganizowanie kodu w ten sposób, by pisać w nim (a raczej z nim) obiektowo?
Problem oczywiście dot. głównie procedury zdarzeniowej, która musi być funkcją globalną lub statyczną...
Czy macie jakieś ciekawe pomysły jak można to rozwiązać?
Jak waszym zdaniem mogłoby to wyglądać?

Z góry dziękuję za pomoc.

0

Uzyc gotowego wrappera (click)? Lub jakiekojkolwiek innej biblioteki obiektowej?

0

Nah, to jest za proste ;D -> chciałem to zrobić po swojemu ;-)
Ale ta procedura zdarzeń mnie dobija i nie wiem co z nią począć... Komplikuje ona trochę sprawę...

0
WojtekMS napisał(a):

Nah, to jest za proste ;D -> chciałem to zrobić po swojemu ;-)
Ale ta procedura zdarzeń mnie dobija i nie wiem co z nią począć... Komplikuje ona trochę sprawę...

To są po prostu funkcje dynamiczne, czyli wołane przez numer - identyfikator.

Nie ma takich rzeczy w C++, więc trzeba to zrobić samodzielnie.
Zwykle tworzy się tablicę (dla danej class), w której się szuka danego identyfikatora i pobiera adres funkcji.

A np. w delphi, i w free pascalu, są metody dynamiczne, więc można od razu to deklarować... jako message;
ale tam to też jest realizowane poprzez tablice - można sprawdzić w rtl jak to działa.

2

Ale ta procedura zdarzeń mnie dobija i nie wiem co z nią począć... Komplikuje ona trochę sprawę...

Za pomocą SetWindowLongPtr/GetWindowLongPtr możesz do okna przypisać wskaźnik - może być to wskaźnik na this żeby z metody statycznej przejść na metodę obiektu.

0

Dzięki @Azarien za ciekawą koncepcję ;-) -> chyba rozumiem o co chodzi, chociaż muszę się jeszcze bliżej przyjrzeć tym funkcją. Czyli rozumiem, że w klasie bazowej będzie jedna funkcja statyczna procedury zdarzeń, która będzie przyjmować wskaźnik na wywołującą klasę a stamtąd będzie wywoływana jakaś metoda virtualna, tak?
Innymi słowy jedna procedura będzie obsługiwać wiele okien? ;)

2

jedna funkcja statyczna, która będzie wyciągać wskaźnik z okna przez GetWindowLongPtr, i traktując ten wskaźnik jako wskaźnik na nasz obiekt odpalać funkcję niestatyczną (wirtualną bądź nie).

ale zastanów się, czy chcesz pisać bibliotekę do okienek, czy po prostu „zobiektywizować” program.
bo może się okazać że skupisz się na dziedziczeniu, funkcjach wirtualnych odpowiedzialnych za zdarzenia, a cały program będzie miał okno jedno...

0

Ogólnie to chcę "zobiektyzować" program, ale z myślą o przyszłości ;-)
Dzięki za pomoc!

0

@Azarien znalazłem w necie taki przykład (lekko go zmodyfikowałem): Czy możesz mi napisać czy o to Ci mniej więcej chodziło?
Bo w tym przykładzie autor używa tylko funkcję GetWindowLongPtr() (bez używania SetWindowLongPtr()).

class Window
{
public:
    static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
    {
         Window* wp= static_cast<Window*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
         if (wp) return me->realWndProc(hwnd, msg, wParam, lParam);
         return DefWindowProc(hwnd, msg, wParam, lParam);
    }
protected:
    virtual LRESULT CALLBACK realWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) = 0;
};

class MyWindow : public Window
{
   public:
   virtual LRESULT CALLBACK realWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
   {
   //...
   }
}

Takie rozwiązanie jest w mojej ocenie bardzo proste i przyjemne - przynajmniej na pierwszy rzut oka, ale czy wystarczające?

7

Cały temat jest bardzo ciekawy, można go rozwiązać na wiele różnych sposobów. Każdy z nich ma swoje plusy i minusy.

Metoda, którą przedstawił Azarien (statyczny WndProc, SetWindowLongPtr i GWLP_USERDATA) jest prosta, ale ma kilka minusów. Po pierwsze używa GWLP_USERDATA, co może powodować problemy. Istnieją programy, które są na tyle zdolne, że potrafią ustawić GWLP_USERDATA dla nieswoich okien i wtedy zepsują nam program. Po drugie nie pozwala nam obsłużyć wszystkich komunikatów. System wysyła już komunikaty w momencie wywołania CreateWindow i w tym wariancie nie ma możliwości podpiąć się pod pierwszy z nich. Po trzecie, co jest chyba najważniejsze, nie jest tak wydajna jak można to zrobić - z każdym komunikatem musimy wywołać GetWindowLongPtr i wywołać procedurę okna w naszej klasie.

MFC oraz Qt, upraszczając, mają mapę HWND -> obiekt, do której jest robiony lookup przy każdej wiadomości. Rozwiązuje to pierwszy z problemów, które opisałem na początku.

Sam Microsoft obecnie używa innej metody, z tzw. thunkiem. Jest ona zastosowana w ATL/WTL oraz pośrednio, w .NET jako element ogólnego marshallingu delegat do pointerów, co jest również użyte w obsłużeniu procedury okna. Thunk to najogólniej mówiąc kawałek kodu, który pomaga nam w wywołaniu innej funkcji. Natomiast jest to o tyle ciekawy kawałek kodu, że stworzymy go sami podczas działania programu. Z tego też względu jest to sposób nieprzenośny, tj. trzeba zapewnić implementację dla każdej architektury. W przypadku Windows nie jest to wielki problem, bo wystarczy zrobić to dla x86, x64 i ARM. Adres do naszego thunka podamy jako procedurę okna. W przypadku x86 thunk zamieni nam pierwszy parametr na stosie, czyli esp+4 (hWnd), na pointer do obiektu okna. Potem skoczymy do funkcji, która wywoła nam już metodę w klasie okna (zakładając, że jest ona wirtualna - ale tutaj jest kolejny element, w którym możemy pobawić się w optymalizacje, np. ze statycznym dziedziczeniem jak to w ATL/WTL zorganizowano - wtedy kompilator wyoptymalizuje nam to wywołanie w całości).
Cała procedura wygląda tak:
Przygotowujemy mapę "thread id" -> "pointer na obiekt okna". Mapa ta jest używana tylko i wyłącznie przy pierwszym otrzymanym komunikacie, który dostaniemy już przy wywołaniu CreateWindow. Do mapy dodajemy nasz pointer tuż przed wywołaniem CreateWindow i zaraz po tym możemy z niej nasze mapowanie usunąć. Dzięki temu jesteśmy pewni, że w tym momencie w naszym wątku nie będzie tworzone inne okno. Do tego dodajemy mutex. Przy rejestracji klasy okna początkowo ustawiamy procedurę okna na funkcję, która przy pierwszym komunikacie pobierze wskaźnik do obiektu okna z mapy oraz zmieni GWLP_WNDPROC na nasz thunk.
Tadam. Mamy procedurę okna jako metodę klasy przy jedynym overheadzie w postaci jednego mov i jednego jmp.

Tak wygląda przygotowanie thunku dla x86:

unsigned char payload[] = "\xC7\x44\x24\x04\xDE\xAD\xBA\xBE\xE9\xDE\xAD\xBA\xBE";
memcpy(thunk, payload, _countof(thunk));

int32_t wndProcAddress = (int32_t)&RedirectWndProc;

*(int32_t*)(&thunk[4]) = (int32_t)this;
*(int32_t*)(&thunk[9]) = wndProcAddress - ((int32_t)&thunk + _countof(thunk));
		
DWORD dummy;
VirtualProtect(thunk, _countof(thunk), PAGE_EXECUTE_READWRITE, &dummy);
FlushInstructionCache(GetCurrentProcess(), thunk, _countof(thunk));

Który ustawiamy potem zwykłym:

SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)&window->thunk);
0

Oraju, daje do myślenia... ;-)
Dzięki wielkie @Rev!
Wygląda dość zaawansowanie, ale spróbuję na spokojnie to przeanalizować i zrozumieć ;)
Ale teraz idę spać :P

2

System wysyła już komunikaty w momencie wywołania CreateWindow i w tym wariancie nie ma możliwości podpiąć się pod pierwszy z nich.

Jeżeli robisz SetWindowLongPtr po CreateWindow to rzeczywiście przegapisz WM_NCCREATE i WM_CREATE.
Ale można przekazać this do CreateWindow w ostatnim parametrze (LPVOID lpParam). Wskaźnik będzie możliwy do odczytania w WM_NCCREATE i WM_CREATE, i tam można zrobić SetWindowLongPtr.

pseudokod: piszę z pamięci.

HWND hWnd = CreateWindow(..., this);
// StaticWndProc
{
    if (msg == WM_NCCREATE)
    {
        MyWindow *that = (MyWindow*)(((CREATESTRUCT*)lParam)->lpCreateParams);
        SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)that);
    }

    MyWindow *that = (MyWindow*)GetWindowLongPtr(hWnd, GWLP_USERDATA);
    if (that!=nullptr)
        return that->NonStaticWndProc(...);
    else
        return DefWindowProc(...);
}
0

Jeszcze raz dziękuję za pomoc! ;-)

0

@Azarien, WM_GETMINMAXINFO jest jeszcze. Niby tylko jeden komunikat, ale przez niego ten warunek potrzebny.

0

Można jeszcze użyć CBTHook. Wklepanie w google z frazą "static wndproc" powinno pomóc. Dla mnie już trochę za późno :P

0
Rev napisał(a):

Cały temat jest bardzo ciekawy, można go rozwiązać na wiele różnych sposobów. Każdy z nich ma swoje plusy i minusy.

Metoda, którą przedstawił Azarien (statyczny WndProc, SetWindowLongPtr i GWLP_USERDATA) jest prosta, ale ma kilka minusów. Po pierwsze używa GWLP_USERDATA, co może powodować problemy. Istnieją programy, które są na tyle zdolne, że potrafią ustawić GWLP_USERDATA dla nieswoich okien i wtedy zepsują nam program. Po drugie nie pozwala nam obsłużyć wszystkich komunikatów. System wysyła już komunikaty w momencie wywołania CreateWindow i w tym wariancie nie ma możliwości podpiąć się pod pierwszy z nich. Po trzecie, co jest chyba najważniejsze, nie jest tak wydajna jak można to zrobić - z każdym komunikatem musimy wywołać GetWindowLongPtr i wywołać procedurę okna w naszej klasie.

MFC oraz Qt, upraszczając, mają mapę HWND -> obiekt, do której jest robiony lookup przy każdej wiadomości. Rozwiązuje to pierwszy z problemów, które opisałem na początku.

Sam Microsoft obecnie używa innej metody, z tzw. thunkiem. Jest ona zastosowana w ATL/WTL oraz pośrednio, w .NET jako element ogólnego marshallingu delegat do pointerów, co jest również użyte w obsłużeniu procedury okna. Thunk to najogólniej mówiąc kawałek kodu, który pomaga nam w wywołaniu innej funkcji. Natomiast jest to o tyle ciekawy kawałek kodu, że stworzymy go sami podczas działania programu. Z tego też względu jest to sposób nieprzenośny, tj. trzeba zapewnić implementację dla każdej architektury. W przypadku Windows nie jest to wielki problem, bo wystarczy zrobić to dla x86, x64 i ARM. Adres do naszego thunka podamy jako procedurę okna. W przypadku x86 thunk zamieni nam pierwszy parametr na stosie, czyli esp+4 (hWnd), na pointer do obiektu okna. Potem skoczymy do funkcji, która wywoła nam już metodę w klasie okna (zakładając, że jest ona wirtualna - ale tutaj jest kolejny element, w którym możemy pobawić się w optymalizacje, np. ze statycznym dziedziczeniem jak to w ATL/WTL zorganizowano - wtedy kompilator wyoptymalizuje nam to wywołanie w całości).
Cała procedura wygląda tak:
Przygotowujemy mapę "thread id" -> "pointer na obiekt okna". Mapa ta jest używana tylko i wyłącznie przy pierwszym otrzymanym komunikacie, który dostaniemy już przy wywołaniu CreateWindow. Do mapy dodajemy nasz pointer tuż przed wywołaniem CreateWindow i zaraz po tym możemy z niej nasze mapowanie usunąć. Dzięki temu jesteśmy pewni, że w tym momencie w naszym wątku nie będzie tworzone inne okno. Do tego dodajemy mutex. Przy rejestracji klasy okna początkowo ustawiamy procedurę okna na funkcję, która przy pierwszym komunikacie pobierze wskaźnik do obiektu okna z mapy oraz zmieni GWLP_WNDPROC na nasz thunk.
Tadam. Mamy procedurę okna jako metodę klasy przy jedynym overheadzie w postaci jednego mov i jednego jmp.

Tak wygląda przygotowanie thunku dla x86:

unsigned char payload[] = "\xC7\x44\x24\x04\xDE\xAD\xBA\xBE\xE9\xDE\xAD\xBA\xBE";
memcpy(thunk, payload, _countof(thunk));

int32_t wndProcAddress = (int32_t)&RedirectWndProc;

*(int32_t*)(&thunk[4]) = (int32_t)this;
*(int32_t*)(&thunk[9]) = wndProcAddress - ((int32_t)&thunk + _countof(thunk));
		
DWORD dummy;
VirtualProtect(thunk, _countof(thunk), PAGE_EXECUTE_READWRITE, &dummy);
FlushInstructionCache(GetCurrentProcess(), thunk, _countof(thunk));

Który ustawiamy potem zwykłym:

SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)&window->thunk);

No, ale to jest zaledwie fragmencik tego co należy zrobić, żeby obsługiwać sprawnie te komunikaty.

To że dostanę ten this wraz z msg niewiele pomaga... bo z tym nadal trzeba by tworzyć
funkcję w stylu wndproc w każdej klasie osobno, no a to jest tu właśnie najgorsze.

1

Ten kod to już około połowa tego co trzeba zrobić.

To że dostanę ten this wraz z msg niewiele pomaga... bo z tym nadal trzeba by tworzyć
funkcję w stylu wndproc w każdej klasie osobno, no a to jest tu właśnie najgorsze.

Jak to? To właśnie chyba chcemy tutaj osiągnąć?

0
Rev napisał(a):

Jak to? To właśnie chyba chcemy tutaj osiągnąć?

Sprawdź sobie jak to ostatecznie działa w mfc, w delphi (znaczy to vcl), albo w starym owl borlanda.

2

Wiem jak to tam działa. Chodzi ci o już o docelowy dispatch do handlerów typu OnMove? To już osobna kwestia i jak mamy WndProc w klasie to handlery są kwestią jednego switcha w najprostszym rozwiązaniu.

0
Rev napisał(a):

Wiem jak to tam działa. Chodzi ci o już o docelowy dispatch do handlerów typu OnMove? To już osobna kwestia i jak mamy WndProc w klasie to handlery są kwestią jednego switcha w najprostszym rozwiązaniu.

wndproc ze swichem masz już gotowe, nawet w c - też tam definiujemy te wndproce ze swichami dla każdego okna indywidualnie.
Zero postępu...

0

to zrób sobie

unordered_map<UINT, MessageProc_t> messages;

gdzie UINT to komunikat WM_COŚTAM, a MessageProc_t to typ wskaźnika na funkcję (albo na metodę) która ma obsługiwać komunikat.

mapę budujesz dodając poszczególne funkcje:

messages[WM_CREATE] = OnCreate;
messages[WM_DESTROY] = OnDestroy;
etc.

potem w WndProc coś takiego:

auto proc = messages[uMsg];
if (proc != nullptr)
    return proc(hWnd, uMsg, wParam, lParam);
else
    return DefWindowProc(hWnd, uMsg, wParam, lParam);

ale zapewne będzie to wolniejsze w działaniu niż zwykły switch...

kiedyś zastosowałem taką mapę funkcyj co prawda nie do obsługi WindowProc ale do rozbicia WM_COMMAND na poszczególne kontrolki.

1 użytkowników online, w tym zalogowanych: 0, gości: 1