Warsztat.GDCompo!ProjektyMediaArtykułyQ&AForumOferty pracyPobieranie

Opisz napotkaną sytuację, a redakcja niezwłocznie znajdzie rozwiązanie!

wyślij anuluj

Szkielet aplikacji okienkowej dla gier

Tekst został importowany z Warsztatowych artykułów. Jego oryginalnym autorem jest Wojciech Skóra. Jeżeli został importowany poprawnie, usuń ten szablon!

Prolog

Czy pisząc kiedyś kolejną grę nie pomyślałeś sobie - „ej, zaraz, przecież ja już pisałem podobny kod!”? Zapewne nie raz. Zwłaszcza jeśli to stwierdzenie dotyczyło procesu tworzenia okna i jego obsługi. Zwykle w takim momencie programista staje przed zadaniem napisania jakiegoś szkieletu (ang. framework). Zbioru niezmiennych funkcji, które są wykorzystywane właściwie w każdym projekcie. Rejestracja klas, zarządzanie oknami, obsługa systemowych komunikatów, czy też główna pętla aplikacji. Dobrze jest napisać taki zestaw raz i mieć go zawsze pod ręką, aby móc włączyć do kolejnej produkcji. Wszystko to ma na celu zaoszczędzenie czasu, który można przeznaczyć na pisanie właściwego, nowego kodu.

W tym artykule przedstawię jak taki framework napisać. Będę się opierał na moim autorskim projekcie G WinAPI Framework 2.0. Kompletny kod oraz przykładową aplikację można znaleźć na stronie tegoż projektu.

Jeśli Cię zainteresowałem - zapraszam do dalszej lektury.

Założenia

Na szkielet mogą się składać najróżniejsze funkcje. Wszystko zależy od tego, co danemu programiście jest potrzebne. Dlatego najpierw należy sprecyzować, czego będzie się oczekiwać od frameworka. Ja kierowałem się następującymi kryteriami:
  • Kod zorientowany obiektowo.
  • Hermetyzacja procedur okienkowych.
  • Zarządzanie wieloma oknami.
  • Dwa tryby głównej pętli (korzystające z GetMessage() i PeekMessage()).
  • Wsparcie dla wielomonitorowych systemów.
  • Obsługa okna (zmiana rozmiaru, stylu, wykrywanie aktywności i przynależności do monitora, przycinanie, et cetera).
  • Dodatkowo implementacja prostego loggera na potrzeby obsługi błędów.

Zasada działania

Na cały framework składają się cztery klasy:
  • WinAPI_System - odpowiada głównie za pompowanie komunikatów. Oprócz tego w tej klasie znajduje się kod obsługujący systemy wielomonitorowe oraz podstawowe metody opakowujące wywołania czystych funkcji systemowych.
  • WinAPI_Window - zawiera właściwie wszystkie metody potrzebne do obsługi okna - od jego tworzenia, przez obsługę komunikatów, aż do pobierania i ustawiania jego tytułu. Jest klasą przeznaczoną do odziedziczenia we własnej aplikacji.
  • WinAPI_Exception - malutka klasa realizująca ideę obiektu wyjątku.
  • Logger - implementacja modułu logującego przebieg aplikacji (nie będzie omówiona w tym artykule).
Sposób w jaki korzysta się z G WinAPI Framework 2.0 wygląda następująco:
  • Odziedziczenie po klasie WinAPI_Window (przykładowo w klasie TestApp).
  • Ustawienie podstawowych parametrów okna potrzebnych do jego stworzenia.
  • Wywołanie metody WinAPI_System::Launch() z podanym wskaźnikiem do okna aplikacji (przykładowo TestApp::this).
Po wykonaniu tych kroków w stosownych momentach wywoływane są przez WinAPI_System metody klasy TestApp (OnMessage, OnActivate, OnDeactivate, OnIdle, OnMonitorChange, OnResize).

Mając już jako taki zarys sposobu działania szkieletu, czas przyjrzeć się dokładniej metodom, które realizują wspomniane wcześniej zadania.

Implementacja

Kodu źródłowego jest dość sporo, toteż nie będę dokładnie omawiał każdej funkcji. Skupię się na dokładniejszym opisie zasady działania frameworka i co ważniejszych jego elementach.

Na początek podsumujmy co jest potrzebne do stworzenia w pełni działającej aplikacji systemu Microsoft Windows.

Otóż potrzebujemy klasę okna, funkcję o sygnaturze (LRESULT CALLBACK)(HWND, UINT, WPARAM, LPARAM), czyli popularnie nazywaną "procedurę okienkową" oraz pętlę, która będzie przekazywać komunikaty do tejże procedury.

Zwykle poniższy kod załatwia sprawę:

LRESULT CALLBACK WindowProcedure(HWND _wnd, UINT _msg, WPARAM _wParam, LPARAM _lParam)
{…}

WNDCLASSEX wcx;
wcx.lpfnWndProc = WindowProcedure;
... // wypełnienie pól danymi
RegisterClassEx(&wcx);
CreateWindowEx(...);

while(1)
  if(PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
  {
  if(msg.message == WM_QUIT)
    break;

  else
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  }
  else
  {...}

Ten kod nie jest zły, ale ma jedną poważną wadę jeśli rozpatrywać go w świetle ustalonych wcześniej kryteriów. Mianowicie - jest kompletnie nieobiektowy, a próba dodania kolejnego okna do aplikacji będzie się wiązała z pisaniem osobnej jego obsługi. Dlatego wygodnie jest napisać klasę, która będzie reprezentowała okno. Dzięki temu wystarczy, że stworzymy obiekt tej klasy i nasza aplikacja wzbogaci się o kolejne okno. Tak, w teorii wygląda to bardzo wygodnie. W praktyce okazuje się, że niekoniecznie musi być tak idyllicznie. Schody zaczynają się zwykle przy nieszczęsnym polu WNDCLASSEX::lpfnWndProc, czyli wskaźniku na funkcję, która nie może być niestatyczną metodą klasy. Zwykle najprostszym rozwiązaniem problemu jest zadeklarowanie procedury okienkowej jako statycznej. Ale wyobraźmy sobie, że chcemy mieć dwa okna w naszej aplikacji. Wtedy dwie instancje obiektu reprezentującego okno będą dzieliły tę samą funkcję obsługi komunikatów (jeśli nie wiesz dlaczego - odsyłam do dokumentacji po informacje na temat słowa kluczowego static). Ten sposób nam się nie podoba - nie spełnia założeń postawionych na początku artykułu. Chcemy by każde okno miało własny kod interpretujący komunikaty systemowe. Ja zrealizowałem to następująco:

LRESULT CALLBACK WindowProcedure(HWND _wnd, UINT _msg, WPARAM _wParam, LPARAM _lParam)
{
  WinAPI_Window* wnd = 0;

  if(_msg == WM_CREATE)
  {
    wnd =  reinterpret_cast<WinAPI_Window*>
      (reinterpret_cast<LPCREATESTRUCT>(_lParam)->lpCreateParams);

    SetWindowLong(_wnd, GWL_USERDATA, reinterpret_cast<LONG>(wnd));
  }

  wnd = reinterpret_cast<WinAPI_Window*>(GetWindowLong(_wnd, GWL_USERDATA));

  if(wnd)
    return wnd->WindowProcedure(_wnd, _msg, _wParam, _lParam);
  else
    return DefWindowProc(_wnd, _msg, _wParam, _lParam);
}

class WinAPI_Window
{
  friend LRESULT CALLBACK WindowProcedure(HWND _wnd, UINT _msg, WPARAM _wParam, LPARAM _lParam);

  private:

  LRESULT CALLBACK  WindowProcedure(HWND _wnd, UINT _msg, WPARAM _wParam, LPARAM _lParam);
... // reszta deklaracji
};

Zadeklarowałem globalną, uniwersalną procedurę komunikatów. Jej zadanie to wywołanie metody WinAPI_Window::WindowProcedure() odpowiedniego okna. Pytanie tylko skąd ta funkcja wie, na rzecz którego obiektu mamy ją wywołać? Otóż wystarczy, że przy wywołaniu funkcji CreateWindow() jako ostatni parametr podamy wskaźnik do naszego okna. Dzięki temu system wyśle komunikat WM_CREATE z lParam przechowującym wartość tego parametru. Następnie po skopiowaniu go do specjalnego kawałka pamięci przypisanego każdemu oknu, który mieści zmienną wskaźnikową, możemy zacząć działać. Za każdym razem, gdy system wyśle jakiś komunikat, w WindowProcedure() odczytujemy wartość z pola GWL_USERDATA, czyli wskaźnik na obiekt typu WinAPI_Window, a następnie wywołujemy docelową metodę WinAPI_Window::WindowProcedure().

Mamy już działający model hermetycznych procedur okienkowych. Teraz czas zadbać, by miały co robić - zajmiemy się pompą komunikatów.

Stanowcza większość aplikacji w systemie Windows korzysta z GetMessage(). Jest to dość oczywiste, gdyż są sterowane zdarzeniami. Oznacza to tyle, że jeśli nie zachodzi jakaś sytuacja, na którą aplikacja powinna zareagować, jej proces oczekuje na nadejście kolejnego komunikatu. W efekcie taka aplikacja zajmuje czas procesora tylko wtedy, gdy jest to konieczne.

Całkiem inaczej jest w przypadku gier. Jako że potrzebują one jak najwięcej mocy obliczeniowej, czyli praktycznie w całości jest im przydzielany możliwy czas procesora, korzystają z PeekMessage(). Skutkuje to tym, że w głównej pętli sprawdzane jest czy nie nadszedł nowy komunikat. W przypadku, gdy kolejka jest pusta gra kontynuuje swoją symulację. Jest to dobre rozwiązanie. Jednak wyobraź sobie taką sytuację, w której użytkownik zapragnie przełączyć się na chwilę na inna aplikację. Ot, choćby na komunikator albo przeglądarkę. Efekt - gra nadal będzie obciążać procesor, a aktywna aplikacja mulić (opisałem tu przypadek, gdy w systemie mamy tylko jeden procesor/jeden rdzeń).

Rozwiązaniem tego problemu jest zakodowanie odpowiedniej pętli komunikatów, która będzie sprawdzać, czy do jej procesu należy jakieś okno wymagające większej uwagi procesora. Jeśli takiego nie ma, uruchamiany jest tryb z GetMessage(). W przeciwnym wypadku ten z PeekMessage().

void WinAPI_System::EnterMainLoop()
{
  MSG  msg;
  BOOL  ret;

  msg.message = WM_NULL;

  loop = true;
  running = true;
  
  while(loop)
  {
    if(Idle())
    {
      if(PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
      {
        if(msg.message == WM_QUIT)
        {
          loop = false;
      }
      else
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }
    }
    else
    {
      DoIdle();
    }
  }
  else
  {
    if((ret = GetMessage(&msg, 0, 0, 0)) == -1)
    {
      throw SystemException("Main loop error");
    }
    else
    {
      if(!ret)
      {
        loop = false;
      }
      else
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }
    }
  }
  }
}

Jak można zauważyć, moja pętla komunikatów jest połączeniem dwu klasycznych modeli. W jedną całość spaja je metoda WinAPI_System::Idle() sprawdzająca czy na liście zarejestrowanych okien znajduje się choć jedno, które deklaruje, że chce wykorzystać procesor, gdy w kolejce nie ma żadnych komunikatów. Jeśli takie istnieje, odpalana jest funkcja PeekMessage() i jeśli nie ma żadnego komunikatu, zostaje wywołana metoda WinAPI_Window::DoIdle() dla odpowiednich okien. Natomiast jeśli wszystkie okna deklarują, że są bardzo leniwe i nie chce im się pracować - uruchamiana jest funkcja GetMessage(), która usypia proces, aż do nadejścia jakiegoś komunikatu.

Niejasne zostaje jeszcze miejsce rzucenia wyjątku. SystemException() to makro tworzące obiekt wyjątku:

#define SystemException(_desc) WinAPI_Exception(_desc, __FILE__, __FUNCTION__, __LINE__);

class WinAPI_Exception
{
  public:

  WinAPI_Exception(const std::string& _description,
    const std::string& _file, const std::string& _function, int _line);

  std::string  Description() const;
  std::string  File() const;
  std::string  Function() const;
  int  Line() const;

  private:

  std::string  description;
  std::string  file;
  std::string  function;
  int  line;
};

Na tym etapie mamy już kompletny projekt modelu obsługi komunikatów. Czas zająć się ich właściwą interpretacją.

#define PASS_MESSAGE_ON def = false; break

LRESULT CALLBACK WinAPI_Window::WindowProcedure(HWND _wnd, UINT _msg, WPARAM _wParam, LPARAM _lParam)
{
  bool def = true;

  switch(_msg)
  {
  case WM_*:
  // jakiś kod...
  PASS_MESSAGE_ON;

  default:
    def = true;
  }

  if(OnMessage(_wnd, _msg, _wParam, _lParam) && def)
  return DefWindowProc(_wnd, _msg, _wParam, _lParam);

  return 0;
}

Tak wygląda schemat procedury okienkowej. Na początku standardowo w instrukcji switch podejmujemy stosowne działania w stosunku do konkretnego komunikatu, a potem ustawiamy flagę def na false. Czemu? Ano, bierz pod uwagę, że WinAPI_Window to klasa abstrakcyjna - będziemy po niej dziedziczyć w każdej, wyspecjalizowanej klasie okna. W jej implementacji będzie zawarta tylko standardowa obsługa, zarządzanie najbardziej potrzebnymi informacjami. Całą resztę roboty przekażemy klasie pochodnej, czyli w tym wypadku będzie to wywołanie czysto wirtualnej metody WinAPI_Window::OnMessage(), która zwraca typ bool. Jeśli komunikat został zinterpretowany, ma obowiązek zwrócić false. Jeśli zwróci true i jednocześnie WinAPI_System::WindowProcedure() nie obsłużyła danego komunikatu, to zostanie wywołana domyślna, systemowa procedura DefWindowProc().

WinAPI_Window::WindowProcedure() interpretuje następujące sześć komunikatów: WM_ACTIVATE, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_MOVE, WM_SIZE, WM_CREATE.

case WM_ACTIVATE:
{
  switch(LOWORD(_wParam))
  {
  case WA_ACTIVE:
  case WA_CLICKACTIVE:
    if((BOOL)(HIWORD(_wParam)))
    Deactivate();
    else
    Activate();
  break;
    case WA_INACTIVE:
    Deactivate();
  break;
  }
}
PASS_MESSAGE_ON;

Dzięki WM_ACTIVATE będziemy wiedzieli czy dane okno jest aktywne.

case WM_ENTERSIZEMOVE:
  moving = true;
  moved = false;
PASS_MESSAGE_ON;

Ten komunikat jest wysyłany, gdy okno zaczyna być przesuwane lub zaczyna zmieniać rozmiar. Ustawiane są odpowiednie flagi, które pomogą później w interpretacji, jakie dokładnie zdarzenie miało miejsce.

case WM_EXITSIZEMOVE:
  moving = false;
  if(moved)
  {
  UpdateSizePos();

  HandlePossibleMonitorChange();

  if(clip)
    ClipWindow();

  moved = false;

  OnResize(clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);
  }
PASS_MESSAGE_ON;

System wysyła WM_EXITSIZEMOVE, gdy okno zmieniło położenie lub rozmiar. Jeśli flaga moved jest ustawiona na true, oznacza to, że położenie okna naprawdę się zmieniło i należy uaktualnić stosowne zmienne przez WinAPI_Window::UpdateSizePos() oraz ewentualnie dokonać przycięcia do monitora.

case WM_MOVE:
  moved = true;
PASS_MESSAGE_ON;

Komunikat wysyłany za każdym razem, gdy okno poruszy się choćby o jeden piksel. Flaga moved mówi o tym, czy będzie konieczna aktualizacja zmiennych przechowujących pozycję okna.

case WM_SIZE:
  switch(_wParam)
  {
  case SIZE_MAXIMIZED:
  case SIZE_MINIMIZED:
    UpdateSizePos();

    HandlePossibleMonitorChange();

    if(clip)
    ClipWindow();

    OnResize(clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);
  break;

  case SIZE_RESTORED:
    if(moving)
    moved = true;
    else
    {
    UpdateSizePos();

    HandlePossibleMonitorChange();

    if(clip)
      ClipWindow();

    OnResize(clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);
    }
  break;
  }
PASS_MESSAGE_ON;

Tutaj decydujemy jakie akcje podjąć, gdy okno jest maksymalizowane, minimalizowane i przywracane do poprzedniego rozmiaru. Jeśli to konieczne, zaktualizowana zostaje pozycja okna oraz dokonywane jest ewentualne przycinanie.

case WM_CREATE:
  wnd = _wnd;
  if(OnMessage(_wnd, _msg, _wParam, _lParam))
  {
  LERROR("Failed to create window");

  return -1;
  }
return 0;

Troszkę nietypowo wygląda obsługa WM_CREATE. Otóż jeśli WinAPI_Window::OnMessage() zwróci true, co będzie oznaczało błąd, zostanie zwrócona do systemu wartość -1. Skutkiem tego będzie zerowy uchwyt z funkcji CreateWindow().

Wszędzie tam, gdzie okno mogło zmienić aktualny monitor wywoływana jest metoda WinAPI_Window::HandlePossibleMonitorChange(). Oto jej implementacja:

void WinAPI_Window::HandlePossibleMonitorChange()
{
  if(!monitorChange)
  return;
  
  HMONITOR hm = MonitorFromWindow(wnd,MONITOR_DEFAULTTONEAREST);

  if(hm == monitor->monitor)
  return;  // zmiana monitora nie miala miejsca

  const SystemMonitor* mon = winAPI_System.GetSystemMonitor(hm);

  if(!mon)
  {
  LERROR("GetSystemMonitor returned 0");
  return;
  }

  monitor = mon;

  OnMonitorChange(mon);
}

Działanie opiera się głównie na systemowej funkcji MonitorFromWindow(). Z parametrem MONITOR_DEFAULTTONEAREST zwraca uchwyt monitora, na którym znajduje się największy kawałek rozpatrywanego okna (lub jest jego najbliżej). Chwilę należałoby przyjrzeć się jak zrealizowana jest idea wielomonitorowości. Dla frameworka monitor jest następującą strukturą:

struct WinAPI_System_Monitor
{
  std::string  name;  // nazwa monitora
  std::string  adapterName;  // nazwa karty graficznej
  std::string  device;  // nazwa systemowa [ \\.\DISPLAY... ]

  bool  primary;  // true oznacza, ze jest monitorem domyslnym

  int  id;  // numer identyfikujacy monitor w systemie

  HMONITOR  monitor;  // uchwyt monitora

  LONG  x;  // pozycja x
  LONG  y;  // pozycja y
  DWORD  width;  // szerokosc w pikselach
  DWORD  height;  // wysokosc w pikselach
  DWORD  depth;  // glebia kolorow
  DWORD  frequency;  // odswiezanie

  RECT  worksapce;  // obszar roboczy
};
typedef WinAPI_System_Monitor SystemMonitor;

Lista systemowych monitorów przechowywana jest w WinAPI_System::systemMonitors, a tworzona za pomocą dość rozbudowanej metody WinAPI_System::GetMonitorsInfo(). Ciekawych implementacji odsyłam do źródeł. Efektem jej działania jest dodatkowo ustawienie WinAPI_System::primarySystemMonitor - wskaźnika na domyślny monitor (czyli ten, na którym mamy pasek zadań).

Dopełnieniem modelu wielomonitorowości są metody GetSystemMonitor(unsigned), GetSystemMonitor(HMonitor) i GetPrimarySystemMonitor(). Każda z nich zwraca wskaźnik na systemowy monitor na podstawie przekazanych danych.

Takim sposobem mamy zaimplementowaną większość założeń postawionych na samym początku. Została nam tylko obsługa wielu okien (która tak naprawdę już sama się zrobiła przy okazji obiektowości kodu) i zarządzanie ich właściwościami, czyli:

void WinAPI_System::AddWindow(WinAPI_Window* _window)
{
  // TODO: sprawdzac czy takie okno nie zostalo juz dodane
  // chociaz.. sam nie wiem po co ;)

  _window->PrepareWindow();

  windows.push_back(_window);
}

void WinAPI_System::RemoveWindow(WinAPI_Window* _window)
{
  using std::list;

  list<WinAPI_Window*&rt;::iterator it = windows.begin();

  for(; it != windows.end(); ++it)
  {
  if((*it) == _window)
  {
    _window->DestroyWindow();
    windows.erase(it);

    return;
  }
  }
}

I wsparcie dla wielu okien gotowe! Banalnie proste, nieprawdaż? Co do właściwości samych okien: tutaj nieunikniony będzie większy wkład pracy. Konieczne są metody zmieniające rozmiar, style (te podstawowe jak i rozszerzone), odpowiedzialne za przycinanie okna, pokazywanie i ukrywanie, et cetera, et cetera. Czyli dużo nudnego kodu wywołującego jeszcze bardziej nudne funkcje Windows API. Nie będę ich tutaj omawiał. Zainteresowanych i ciekawych jak to działa, odsyłam do źródeł.

Poniżej przykładowe deklaracje klasy WinAPI_Window oraz WinAPI_System:

class WinAPI_Window
{
  friend LRESULT CALLBACK WindowProcedure(HWND _wnd, UINT _msg, WPARAM _wParam, LPARAM _lParam);

  friend class WinAPI_System;

  public:

  WinAPI_Window();
  virtual ~WinAPI_Window();

  // ogolne/informacyjne metody
  void    SetCaption(const std::string& _caption);
  std::string  GetCaption() const;
  std::string  GetClassName() const;
  CreateWindowInfo*  GetCreateInfo();
  void    MsgBox(const std::string& _text, const std::string& _caption = "") const;
  const HWND  GetHWND() const;
  bool    Active() const;
  bool    Idle() const;
  bool    IsMinimized() const;
  bool    IsMaximized() const;


  // metody dotyczace rozmiaru
  void    SetWidth(LONG _width, bool _client);
  void    SetHeight(LONG _height, bool _client);
  void    SetSize(LONG _width, LONG _height, bool _client);
  int    GetWidth(bool _client) const;
  int    GetHeight(bool _client) const;
  void    SetRect(const RECT* _rect, bool _client);
  const RECT*  GetRect(bool _client) const;
  

  // metody dotyczace stylow
  int    SetBasicStyles(DWORD _styles, bool _preserveClient);
  int    SetExtendedStyles(DWORD _styles, bool _preserveClient);
  DWORD    GetBasicStyles() const;
  DWORD    GetExtendedStyles() const;

  // meotdy dotyczace zachowania sie okna
  void    EnableIdle();
  void    DisableIdle();
  void    EnableClipping();
  void    DisableClipping();
  bool    Clipping() const;
  void    EnableMonitorChanging();
  void    DisableMonitorChanging();
  bool    MonitorChanging() const;
  void    Show();
  void    Hide();

  protected:

  virtual bool  OnMessage(HWND _wnd, UINT _msg, WPARAM _wParam, LPARAM _lParam) = 0;
  virtual void  OnActivate() = 0;
  virtual void  OnDeactivate() = 0;
  virtual void  OnIdle() = 0;
  virtual void  OnMonitorChange(const SystemMonitor* const _monitor) = 0;
  virtual void  OnResize(unsigned _width, unsigned _height) = 0;

  private:

  LRESULT CALLBACK  WindowProcedure(HWND _wnd, UINT _msg, WPARAM _wParam, LPARAM _lParam);

  void    PrepareWindow() throw(WinAPI_Exception);
  void    DestroyWindow();

  void    UpdateSizePos();
  void    ClipWindow();
  void    CenterWindow();
  void    HandlePossibleMonitorChange();

  void    Activate();
  void    Deactivate();
  void    DoIdle();

  HWND    wnd;
  std::string  className;
  std::string  caption;
  DWORD    basicStyles;
  DWORD    extendedStyles;

  CreateWindowInfo  createInfo;

  const
  SystemMonitor*  monitor;  // monitor, do ktorego aktualnie jest przypisane okno

  RECT    windowRect;
  RECT    clientRect;
  bool    active;
  bool    moving;  // nazwa troche mylaca; true oznacza przeciaganie krawedzi okna
  bool    moved;
  bool    visible;  // determinuje czy uzyte zostalo ShowWindow() z SW_SHOW czy SW_HIDE
  bool    monitorChange;
  bool    clip;    // true oznacza przycinanie okna do monitora
  bool    idle;    // true oznacza, ze WinAPI_System bedzie wywolywal metode Idle()
  bool    destroyed;
};

class WinAPI_System
{
  public:

  WinAPI_System();
  ~WinAPI_System();

  void    Launch(WinAPI_Window* _window) throw(WinAPI_Exception);

  void    AddWindow(WinAPI_Window* _window);
  void    RemoveWindow(WinAPI_Window* _window);

  const
  HINSTANCE  GetInstance() const;
  
  const
  SystemMonitor*  GetSystemMonitor(unsigned _id) const;
  
  const
  SystemMonitor*  GetSystemMonitor(HMONITOR _monitor) const;
  
  const
  SystemMonitor*  GetPrimarySystemMonitor() const;

  HICON    LoadIcon_(int _id);
  HBITMAP  LoadBitmap_(int _id);
  HCURSOR  LoadCursor_(int _id);
  HBRUSH    LoadBrush(DWORD _color);

  private:
  
  void    EnterMainLoop()  throw(WinAPI_Exception);
  void    DoIdle();
  bool    Idle();
  void    GetSystemMonitorsInfo();

  HMODULE  richEditModule;
  bool    loop;
  bool    running;

  const
  SystemMonitor*  primarySystemMonitor;

  std::vector<SystemMonitor&rt;  systemMonitors;

  std::list<WinAPI_Window*&rt; windows;
};

Przykład użycia

class TestApp
  :  public WinAPI_Window
{
  public:

  int  Start();

  private:

  bool  OnMessage(HWND _wnd, UINT _msg, WPARAM _wParam, LPARAM _lParam);
  void  OnActivate(){}
  void  OnDeactivate(){}
  void  OnIdle(){}
  void  OnMonitorChange(const SystemMonitor* const _monitor){}
  void  OnResize(unsigned _width, unsigned _height){}
};

int TestApp::Start()
{
  CreateWindowInfo* cwi = GetCreateInfo();

  cwi->caption  = "TestApp";
  cwi->className  = "Application";
  cwi->basicStyles  = WS_OVERLAPPEDWINDOW | WS_VISIBLE;
  cwi->extendedStyles  = 0;
  cwi->classStyles  = 0;
  cwi->width  = 640;
  cwi->height  = 480;
  cwi->x    = 0;
  cwi->y    = 0;
  cwi->brush  = winAPI_System.LoadBrush(12345608);
  cwi->cursor  = winAPI_System.LoadCursor_(0);
  cwi->iconBig  = winAPI_System.LoadIcon_(101);
  cwi->iconSmall  = winAPI_System.LoadIcon_(101);
  cwi->clientRect  = true;

  try
  {
  winAPI_System.Launch(this);
  }
  catch(WinAPI_Exception& _e)
  {
  logger.WriteError(_e.File(), _e.Function(), _e.Line(), _e.Description());

  return -1;
  }

  return 0;
}

bool TestApp::OnMessage(HWND _wnd, UINT _msg, WPARAM _wParam, LPARAM _lParam)
{
  switch(_msg)
  {
  case WM_CREATE:
    MsgBox("Hello world!");
  return false;

  case WM_CLOSE:
    PostQuitMessage(0);
  return false;
  };

  return true;
}

Po wypełnieniu odpowiednich pól WinAPI_Window::createInfo i wywołaniu metody WinAPI_System::Launch() będzie można obsługiwać zdarzenia, dzięki sześciu metodom odziedziczonym po WinAPI_Window. Jak widać dostajemy praktycznie wszystkie informacje jakie są potrzebne o oknie, w którym wyświetlana będzie gra.

Przykładowo jeśli ktoś zapragnie napisać jakieś demo w DirectX wystarczy, że odziedziczy po klasie WinAPI_Window i wszystko ma podane jak na tacy. Zmiana rozmiaru okna? Nie ma sprawy, wystarczy dodać obsługę zmiany backbuffera w TestApp::OnResize(). Podobnie rzecz ma się, gdy nastąpi zmiana monitora. Wystarczy zmienić ustawienia urządzenia Direct3D i je ponownie utworzyć. Nie ma również problemu, aby przejść w tryb pełnoekranowy. Trzeba tylko wywołać WinAPI_Window::SetBasicStyles() z odpowiednimi parametrami, zrobić co potrzebne z Direct3D i gotowe. Również w bardzo łatwy sposób można pozbyć się opisanego wcześniej problemu z muleniem aplikacji. W metodach TestApp::OnActivate() i TestApp::OnDeactivate() wystarczy wywołać WinAPI_Window::Enable/DisableIdle().

Epilog

Myślę, że te kilkadziesiąt kilobajtów kodu ułatwi niektórym osobom pisanie własnego szkieletu aplikacji windowsowej. Dodając własne, wymyślne funkcje i rozszerzając funkcjonalność można zbudować bardzo poręczny framework. Niewątpliwie taki gotowy do użycia kod przyspieszy prace nad nowymi produkcjami.

Kompletny kod wraz z wielookienkowym przykładem użycia można pobrać ze strony projektu G WinAPI Framework 2.0.

Kontakt (mail i MSN Messenger): [email protected]

Życzę miłego kodowania!

Tekst dodał:
Adam Sawicki
17.12.2007 21:13

Ostatnia edycja:
Adam Sawicki
17.12.2007 21:13

Kategorie:

Aby edytować tekst, musisz się zalogować.

# Edytuj Porównaj Czas Autor Rozmiar
#1 edytuj 17.12.2007 21:13 Adam Sawicki 26.96 KB
Zwykły
Do sprawdzenia
Do akceptacji
  • RL89 (@RL89) 17 grudnia 2007 21:48
    Super artykuł. Na pewno się przyda ;)
  • ~siódmy 18 grudnia 2007 21:57
    winapi, fuuuuuuuuj :P sam teraz piszę program w winapi żeby nie było nigdy że znam go tylko z teorii i nienawidzę go za to że ma tyle możliwości, nadaje się TYLKO do robienia frameworków, a program lepiej robić już na gotowym frameworku

    chwała za to że to napisałeś
  • bs.mechanik (@bsmechanik) 13 lutego 2008 14:13
    bardzo ciekawy artykuł który pojawił sie w idealnym dla mine momencie :D
  • Michał Gruszczyński (@Lulu93) 16 maja 2008 23:02
    ode mnie wielki PLUS =D, n takich ludziach powinien opierac sie iinternet.... :P
  • ~poopa 20 czerwca 2008 01:05
    A ja mam pętle statyczną i nie widzę żadnych, ale to żadnych przeciwwskazań dla takiego rozwiązania. Na koniec zrobiłeś coś gorszego, a robiącego dokładnie to samo... stworzyłeś globalną funkcję którą po prostu podpiąłeś za pomocą szamba (moim zdaniem robi sieczkę w zależnościach) zwanego friend. Ja tam wolę kilka f unkcji statycznych w klasie bazowej o nazwie Program... No baaa, przecież program jest statyczny...czemu jego funkcje nie miały by takie być. Nasza prywatna pętla komunikatów tylko identyfikuje nadawcę i uruchamia zdarzenia. Nie ma w tym nic niezgodnego z pochodzeniem i przeznaczeniem - jest logiczne.
  • Zgred (@Zgred) 20 czerwca 2008 02:02
    Po pierwsze to nie jest pętla, tylko >procedura okienkowa<. Po drugie właściwie tyle ile jest okien, o samodzielnie zdefiniowanych klasach przez RegisterClass*() w aplikacji, tyle procedur okienkowych jest potrzebnych. Po trzecie tego frienda można się łatwo pozbyć - jednak tego nie zrobiłem, bo jest >wygodny<. Poza tym nie bierzesz pod uwagę najważniejszej sprawy: okno jest reprezentowane przez klasę. W niej jest zaimplementowana cała jego rozbudowana obsługa. Jest to korzystne, ot choćby dlatego, że mozna sobie odziedziczyć po tejże klasie i wyspecjalizować okno. Używając statycznych procedur okienkowych albo powielasz kod dla każdej specjalizacji klasy, albo skazany jesteś na przekazywanie do nich wskaźnika na obiekt okna - strasznie niewygodne. W mojej implementacji istnieje WindowProcedure(), która jest Routerem dla komunikatów. Nie ma w tym niczego złego. A napewno niczego nielogicznego i niewygodnego. Wręcz przeciwnie. Tworzysz nowe okno i nie martwisz się o komunikaty - one już same wiedzą, gdzie popłynąć.
  • ~poopa 20 czerwca 2008 17:04
    Do globalnej funkcji...

    Nie poprawiaj mnie. ;) Już zamiast procedura, bardziej procesor komunikatów (przypuszczam że oryginalna nazwa WNDPROC to właśnie od procesora okna, a nie procedury...) okna by pasował, ale co tam.

    Mocno bronisz tego nie używania statycznych... Mam jedną pętle komunikatów w programie która zgłasza od razu zdarzenia swoim dzieciom - formom(które należą do programu), te zgłaszają swoim dzieciom - kontrolkom... itd. , itp. To nie... twoje rozwiązanie z globalnym wyłapywaczem i procedurami dla każdego okna lepsze.

    BTW. Jakoś nie dostrzegłem u ciebie sposobu na hierarchię okien. Wywołasz w procesorze okna inny procesor? Dopiszesz komunikat? heh... nie wiem, pewnie coś wymyślisz.

    Ale co ja tam się będę kłócił - rób jak chcesz. Byleś nie był tak mocno przekonany o "idealności" tego, a nie innego rozwiązania.
  • Zgred (@Zgred) 20 czerwca 2008 17:49
    Nie ma idealnego rozwiązania i nie będzie, póki WNDCLASS* przyjmuje wskaźnik na funkcję. To jak sobie kto będzie radził przy pisaniu obiektowej struktury programu, już od jego upodobań zależy. Tobie akurat nie przypadła ta konstrukcja do gustu, ale innym owszem. A co do procedury okienkowej - nawet w dokumentacji tak na te funkcje mówią: http://msdn.microsoft.com/en-us/library/ms632593(VS.85).aspx
  • ~poopa 20 czerwca 2008 18:02
    oops... no to przepraszam za te procedury. ;)
  • ~Someone 23 listopada 2008 12:45
    Witam

    Wiem coś o pisaniu wrapperów na jakie kolwiek API, nawet w minimalnym zakresie - np. tworzenia nakładki na okienka tak by były wielkim wodotryskiem w Pure WinAPI. Jednak nie wydaje mi się aby było to optymalne rozwiązanie jakie podałeś, podałeś to darmowo więc ok każdy może z tym zrobić co chce. Jednak nadużywanie wyjątków tam gdzie nie ma sytuacji wyjątkowych to lekka przesada. Jestem przeciwinikiem stosowania wyjątków, są sytuacje w których ich zastosowanie jest konieczne, ale jak pisałem tam gdzie mamy sytuacje wyjątkowe/kryzysowe. Jeśli można obsłużyć zdarzenie - jakiekolwiek - bez ich pomocy należy to zrobić.

    Pozdrawiam
  • Napisz komentarz:
    Aby dodać swój komentarz, musisz się zalogować.
Licencja Creative Commons

Warsztat używa plików cookies. | Copyright © 2006-2017 Warsztat · Kontakt · Regulamin i polityka prywatności
build #ff080b4740 (Tue Mar 25 11:39:28 CET 2014)