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

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