Създаване на OpenGL прозорец с Win32 API
Преди да започна, искам да знаете че изграждането на OpenGL прозорец с Win32 API може да ви дойде в повече, особено ако никога не сте се сблъсквали с подобно нещо. За разлика от библиотеката GLUT където ви представих и цяла тема, обяснявайки подробно всяка функция, тук нещата ще бъдат по "нестандартни". Какво значи това? Ами погрижил съм се, да разберете задачата на всяка част от кода, но нищо повече. Няма да разглеждам подробно всичко, защото ако направя това рискувам да напиша книга :) Материала е просто огромен. Все пак хората които са правили прозорци с Win32 API ще се чувстват в свои води, а останалите...ами следващото обяснение е за тях. Всяко windows базирано приложение установява цикъл на съобщенията във своята WinMain( ) функция. Когато Windows изпрати съобщение ( то може да бъде най-различно - от съобщение за натискане на даден клавиш до съобщение за активиране на screensaver ) вашата програма прочита това съобщение ( ако има други след него - чакат на опашка ) и го изпраща отново на Windows. Особенното тук, е че вашия код трябва да съдържа една специална CALLBACK функция ( ще видите след малко ), която се извиква многократно от Windows. Тя приема като аргумент последното съобщение, което е било прочетено от вашата програма и върнато обратно на Windows. Може би се чудите какъв е смисъла вашата програма и Windows да си подхвърлят по два пъти едно и също съобщение. Една от причините е че процесорното време трябва да бъде правилно разделено между всички изпълняващи се приложения. Общо взето това е основното, което един начинаещ Win32 API програмист трябва да знае. Сега идва интересната част, а това е самия код. При създаването на проекта с Visual C++ изберете задължително Win32 Application, а не Win32 Console Application както при GLUT. И един последен съвет. Включете хелпа на вашия компилатор. Там са изнесени подробно всички функции и структури от Win32 API, които съм използвал в настоящия код. Най-вероятно ще ви помогне да разберете по-лесно нещата.

#include < windows.h > // Хедърният файл на Windows
#include < gl/glu.h > // Хедърният файл на GLU
#include < gl/gl.h > // Хедърният файл на OpenGL

void Render( ); // прототипът на рендериращата ни функция
bool Init( ); // прототипът на инициализиращата ни функция

// дефинираме променлива от тип HGLRC, в която ще се съдържа манипулатор за OpenGL контекст на рендериране

HGLRC hRC;

// дефинираме променлива от тип HDC, в която ще се съдържа манипулатор за контекст на устройство
HDC hDC;

Не гледайте отчаяно. Контекста на устройството представлява просто структура, която съдържа в себе си някои други обекти като четка, писалка, шрифт и др. Можете да ги използвате, когато изчертавате нещо върху клиентската област от прозореца или върху т.н. битмап. Битмапът е най-важния елемент, понеже той играе ролята на платно, върху което рисуваме. За да получим достъп до тези ресурси, трябва да изискаме от Windows манипулатор за контекст на устройство на нашия прозорец ( ще видите как става това по-нататък ). Важно е да знаете, че всеки прозорец трябва да има свой собствен манипулатор за контекст на устройство. А сега да обясня за другия манипулатор. След като получите ( ще видите как ) манипулатор за OpenGL контекст на рендериране, свързан към контекста на устройството на съответния прозорец, е нужно да направите OpenGL контекста на рендериране текущ. Какво означава това? Ами много просто. Представете си че имате 10 прозореца, всеки със свой манипулатор за контекст на устройство и по още един OpenGL манипулатор за контекст на рендериране. Правейки някой от тези OpenGL контексти на рендериране текущ ( чрез съответния манипулатор ), вие избирате върху кой от прозорците да се рендерира вашата 3D графика. Може би обяснението ми не е върха, но е по-добре отколкото нищо... За да не се объркате тотално, е желателно да продължите с WinMain( ) функцията, която се намира някъде по-надолу!

void SetupPixelFormat( HDC hDC )
{
  int nPixelFormat;
// тук ще съхраним индентификатора на формата на пикселите

  // Създаваме структурната променлива pfd и попълваме нейните елементи. Не обръщайте голямо
  // внимание на игнорираните елементи на структурата. Някои от тях по стандарт дори вече не се
  // използват.

  static PIXELFORMATDESCRIPTOR pfd = {

      sizeof( PIXELFORMATDESCRIPTOR ), // големина на структурата
      1, // версия на структурата, в случая 1.0
      PFD_DRAW_TO_WINDOW | // поддържа се рисуване в/у прозореца
      PFD_SUPPORT_OPENGL | // поддържа се OpenGl
      PFD_DOUBLEBUFFER, // поддържа се двоен буфер
      PFD_TYPE_RGBA, // RGBA режим за цветовете
      32, // дълбочина на цвета
      0, 0, 0, 0, 0, 0, // игнорираме цветовите битове
      0, 0, // няма alpha буфер
      0, // няма accumulation буфер
      0, 0, 0, 0, // игнорираме accumulation битовете
      16, // дълбочина на z-буфера
      0, // няма stencil буфер
      0, // няма auxiliary буфери
      0, // игнорираме тази променлива
      0, // резервираме определен брой слоеве за изрисуване
      0, 0, 0 }; // игнорираме маските на слоевете за изрисуване

  // Функцията ChoosePixelFormat( ) избира формат за пикселите, който съответства на избраните от нас
  // в по-горната структура характеристики и се поддържа от контекста на устройството. Функцията
  // връща индетификатор на този формат.

  nPixelFormat = ChoosePixelFormat( hDC, &pfd );

  // Установяваме избрания формат на пикселите.
  SetPixelFormat( hDC, nPixelFormat, &pfd );

  // Ако вече нищо не разбирате, значи всичко е наред. Това е най-малкото което може да ви се случи
  // при Windows програмирането :)) Та общо взето това е при пикселния формат - избирате режим на
  // рендериране, само дето тука за разлика от GLUT не става с една функция...
}


// Това е CALLBACK функцията ( или функцията на прозореца ), която се извиква от Windows. Тя приема
// от Windows първите четири аргумента от структурата MSG, която видяхте във функцията WinMain( ).
// Ako сe чудите за какво говоря, значи нищо не сте гледали, веднага отидете на WinMain( ), за да не идвам аз! :))

LRESULT CALLBACK WindowFunc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
{
  int width, height;
// Ширина и дължина на прозореца, нужни за негово правилно оразмеряване.

  switch( message )
  {


  // WM_CREATE е първото съобщение, което нашия прозорец получава веднага след неговото създаване
  case WM_CREATE:

      // Изискваме контекст на устройство за нашия прозорец. Ако всичко премине успешно
      // получаваме манипулатор към този ресурс.

      hDC = GetDC( hwnd );
      // Определяме формат на пикселите. Разгледайте функцията SetupPixelFormat( ) по-нагоре и ще
      // видите за какво става въпрос...

      SetupPixelFormat( hDC );
      // Изискваме OpenGL контекст на рендериране към контекста на устройството. Ако всичко
      // премине успешно получаваме манипулатор към този ресурс.

      hRC = wglCreateContext( hDC );
      // Правим OpenGL контекста на рендериране на нашия прозорец текущ.
      wglMakeCurrent( hDC, hRC );
      return 0;
  break;


  // Получаваме съобщението WM_SIZE при промяна на размерите на прозореца.
  case WM_SIZE:
      // получаваме новите ширина и дължина на прозореца
      height = HIWORD( lParam );
      width = LOWORD( lParam );

      if( height==0 ) height = 1;

      glViewport( 0, 0, width, height );
      glMatrixMode( GL_PROJECTION );
      glLoadIdentity( );

      // определяме новата 3D перспектива на виждане
      gluPerspective( 45.0f, (GLfloat)width/(GLfloat)height, 0.1, 200.0 );

      glMatrixMode( GL_MODELVIEW );
      glLoadIdentity( );

      return 0;
  break;

  // При затваряне на прозореца...
  case WM_CLOSE:

  case WM_DESTROY:

      wglMakeCurrent( hDC, NULL ); // изключваме текущия OpenGL контекст за рендериране
      wglDeleteContext( hRC ); // унищожаваме OpenGL контекста за рендериране

      // изискваме терминиране на приложението, чрез изпращане на WM_QUIT съобщение от страна на Windows
      PostQuitMessage( 0 );
      return 0;
  break;


  // При натискане на бутон от клавиатурата...
  case WM_KEYDOWN:

      switch(wParam)
      {


          // ако натиснатият бутон е Escape, изискваме терминиране на приложението,
          // чрез изпращане на WM_QUIT съобщение от страна на Windows

          case VK_ESCAPE:
              PostQuitMessage( 0 );
          break;
      }
  break;
  }

  // Всички необработени съобщения от нашата CALLBACK функция се изпращат отново на Windows с
  // помоща на функцията DefWindowProc( ) за обработка по подразбиране.
  return (DefWindowProc( hwnd, message, wParam, lParam ));
}


Windows базираните програми притежават WinMain( ) функция, която не се различава единственно по името си с функцията main( ), която доскоро използвахте във вашите програми. Самата функция трябва да се компилира посредством конвенцията за извикване използвана от Windows - WINAPI или APIENTRY, като двете са напълно аналогични. Ако не разбирате това, не се затормозявайте, защо е така, а просто го използвайте. Запазете търпението си за нещо по-смислено :) На функцията WinMain( ) се предават четири аргумента. Първият се отнася за текущата инстанция на програмата. Вторият - за предишната инстанция на програмата и винаги е равен на NULL, понеже Windows e многозадачен и може да изпълнява повече от една инстанция едновременно ( с изключение на Windows 3.1 ). Третият аргумент представлява указател към низ, съдържащ аргументите от командния ред, зададени при стартирането на програмата. Последният аргумент определя начина по който ще се изобрази прозореца на екрана.

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR CommandLine, int nWinMode)
{

  // В самото начало на WinMain( ) функцията дефинираме някои променливи:
  // - структурна променлива от тип MSG, в която ще се съхраняват съобщенията към прозореца
  // Ето какво представлява структурата MSG:
  /*
  typedef struct tagMSG
  {

  HWND hwnd; // манипулатора на прозореца, за който е отправено съобщението
  UINT message; // самото съобщение
  WPARAM wParam; // информация, свързана със съобщението
  LPARAM lParam; // още информация...
  DWORD time; // времето, през което е изпратено съобщението
  POINT pt; // това е структурна променлива, която съдържа x и y местоположението на мишката
                  // в момента когато е изпратено съобщението
  } MSG;
  */

  MSG msg;

  // - структурна променлива от тип WNDCLASSEX, с която ще дефинираме класа на прозореца
  // Тази ще си я видите самички в хелп-а :)
  WNDCLASSEX wcl;

  // - променлива от тип HWND, която ще съдържа манипулатора на нашия прозорец
  HWND hwnd;

  // Може би се чудите какви са тези манипулатори и глупости, но след малко ще видите... Отсега ви
  // казвам, че можете да използвате Help - а на вашия компилатор, за да получите напълно
  // изчерпателна информация за типа на всички променливи, които виждате в този код.

  // - променлива от тип DWORD, която ще използваме впоследствие за определяне на типа на прозореца

  DWORD dwStyle = WS_OVERLAPPEDWINDOW;

  int WindowWidth = 640; // - ширина на прозореца
  int WindowHeight = 480; // - дължина на прозореца
  int BitDepth = 32; // - дълбочина на цвета
  int DisplayFrequency = 85; // - честота на опресняване на екрана

  bool Finished; // - в процес на рендериране ли сме?
  bool Fullscreen = false;// - рендериране на цял екран?

  // Следващото нещо, което трябва да направим е да попълним елементите на структурната променлива
  // wcl от тип WNDCLASSEX, която ще използваме при регистрирането на прозореца.

  // определяме големина на нашата WNDCLASSEX структура
  wcl.cbSize = sizeof( WNDCLASSEX );
  // Определяме стил на прозорецa. В случая използваме три аргумента, с които изискваме съответно
  // прерисуване на прозореца при промяна на неговата дължина, ширина и създаване на уникален
  // манипултор на устройство за всеки прозорец, регистриран с този клас. За повече стилове можете
  // да проверите в Help - a, за структурата WNDCLASSEX...

  wcl.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
  // определяме функцията на прозореца
  wcl.lpfnWndProc = WindowFunc;
  // няма нужда от допълнителна информация за класа на прозореца и за самия прозорец
  wcl.cbClsExtra = 0;
  wcl.cbWndExtra = 0;
  // посочваме манипулатора на текущата инстанция
  wcl.hInstance = hInstance;
  // определяме иконата на приложението
  wcl.hIcon = LoadIcon( NULL, IDI_APPLICATION );
  // определяме малката икона на изображението
  wcl.hIconSm = LoadIcon( NULL, IDI_APPLICATION );
  // определяме курсора на мишката
  wcl.hCursor = LoadCursor(NULL, IDC_ARROW);
  // цвят на фона ( в случая - никакъв )
  wcl.hbrBackground = NULL;
  // няма меню
  wcl.lpszMenuName = NULL;
  // име на класа на прозореца
  wcl.lpszClassName = "OpenGL Window Class";


  // Регистрираме класа на прозореца. Ако регистрацията не премине успешно, Известяваме за грешка
  // с диалогов прозорец. WinMain( ) функцията връща нула и приложението се затваря...
  if( !RegisterClassEx( &wcl ) )
  {
      MessageBox( NULL, "Window class registration unsuccessful!", "Error", MB_OK | MB_ICONWARNING );
      return 0;
  }


  // Преди да изобразим самия прозорец, питаме потребителя с диалогов прозорец, дали желае
  // приложението да се изпълнява на цял екран ( например всички професионално направени игри )
  if( IDYES == MessageBox( NULL, "Do you want to switch to fullscreen mode?", "Fullscreen",
                                            MB_YESNO | MB_ICONQUESTION | MB_DEFBUTTON1 ) ) Fullscreen = true;


  // следващите инструкции се узпълняват, ако е избрано рендериране на цял екран
  if( Fullscreen )
  {

      DEVMODE dmScreenSettings;
      // Със следващата функция зареждаме текущите настройки на дисплея( резолюция, дълбочина на
      // цвета и други ) в структурата dmScreenSettings. Вижте в хелп-а за структурата от
      // предифиниран тип DEVMODE, ако исктате да се информирате по-подробно.
      if( !EnumDisplaySettings( NULL, ENUM_CURRENT_SETTINGS, &dmScreenSettings ) )
      {
          MessageBox(NULL, "Could Not Enum Display Settings", "Error", MB_OK | MB_ICONWARNING);
      }


      // Задаваме стойностите на настройките, които ще променяме:
      dmScreenSettings.dmPelsWidth = WindowWidth; // ширина
      dmScreenSettings.dmPelsHeight = WindowHeight; // дължина
      dmScreenSettings.dmBitsPerPel = BitDepth; // дълбочина на цвета
      dmScreenSettings.dmDisplayFrequency = DisplayFrequency; // честота на опресняване на монитора

      // Заменяме настройките на дисплея с тези от структурата dmScreenSettings. Ако функцията
      // ChangeDisplaySettings( ) върне различна стойност от макроса DISP_CHANGE_SUCCESSFUL извеждаме
      // съобщение за грешка.
      if( DISP_CHANGE_SUCCESSFUL != ChangeDisplaySettings( &dmScreenSettings, CDS_FULLSCREEN ) )
      {
          MessageBox( NULL, "Change to fullscreen unsuccessful! Program will continue in normal mode.", "Error",
                                MB_OK | MB_ICONWARNING );
      }

      // Ако сме сменили успешно настройките, определяме стил на прозореца - без рамка( все пак при
      // режим на цял екран това е напълно ненужно ) и скриваме курсора на мишката.

      else {

          dwStyle = WS_POPUP;

          ShowCursor( false );
      }
  }

  // Следващата функция създава самия прозорец и връща манипулатор към този прозорец
  hwnd = CreateWindow(

      "OpenGL Window Class",
// името на регистрирания вече клас на прозореца
      "Created with Win32 API", // определяме име на прозореца
      dwStyle, // определяме стила на прозореца в зависимост от dwStyle
      0, 0, // x и y координати на прозореца
      WindowWidth, // определяме ширина на прозореца
      WindowHeight, // определяме дължина на прозореца
      NULL, // манипулатор към родителски прозорец ( няма такъв )
      NULL, // манипулатор към меню ( няма такова )
      hInstance, // манипулатор на текущата инстанция на програмата
      NULL ); // указател към допулнителна информация ( няма )

  // Ако сме получили NULL, вместо валиден манипулатор на прозорец, значи прозореца не може да
  // бъде създаден. Известяваме за грешка с диалогов прозорец. След това WinMain( ) функцията
  // връща нула и приложението се затваря.

  if( !hwnd )
  {
      MessageBox( NULL, "Creating window unsuccessful!", "Error", MB_OK | MB_ICONWARNING );
      return 0;
  }

  ShowWindow( hwnd, SW_SHOW ); // изобразяваме създадения от нас прозорец
  UpdateWindow( hwnd ); // указваме, че прозореца трябва да пъде прерисуван, за всеки случай

  // Разбрахте ли вече за какво са тези манипултори? Както виждате те са един вид имена. Например
  // ако имате повече от един прозорец и искате да скриете точно определен, няма как да направите
  // това, без да кажете за кого се отнася заповедта т.е. използвате съответния прозорец.


  Finished = false; // това гo разбирате нали :)

  // Извикваме инициализиращата ни функция и ако тя върне нула, значи нещо не е наред и няма
  // да рендерираме нищо.

  if( !Init() ) Finished = true;

  // Това е цикъла на съобщенията, за който стана на въпрос в самото начало. Той продължава докато
  // променливата Finished не получи стойност false.

  while( !Finished )
  {
      // Зареждаме следващото съобщения от опашката ( ако има такова ) в структурната променлива msg
      // от тип MSG. Ако няма съобщение на опашката функцията връща нула.

      if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) )
      {
          // В случай, че сме получили съобщения за затваряне на програмата, променяме стойността на
          // Finished, така че цикъла на съобщенията да завърши.
          if( msg.message == WM_QUIT ) Finished = true;

          // Преобразуваме суровия клавиатурен код в знакови съобщения
          TranslateMessage( &msg );
          // Изпращаме последното прочетено и преобразувано съобщение обратно на Windows
          DispatchMessage( &msg );
      }
      // Извикваме рендериращата ни функция, ако няма съобщения на опашката, чакащи да бъдат
      // обработени и върнати на Windows.
      else Render( );
  }

  // Финцкията WinMain( ) завършва, като изпраща стойността msg.wParam,
  // съдържаща кода на WM_QUIT съобщениято при затварянето на програмата.

  return msg.wParam;
}


bool Init( )
{
  glClearColor( 0.0, 0.0, 0.0, 0.0 );

  return true;
}


void Render( )
{
  glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
// изчистваме буферите
  glLoadIdentity( ); // зареждаме първоначалната матрица

  glTranslatef( 0.0, 0.0, -10.0 ); // преместваме сцената 10 единици напред

  // изрисуваме триъгълник
  glBegin(GL_TRIANGLES);

      glVertex3f( -2.0, -2.0, 0.0 );
      glVertex3f( 0.0, 2.0, 0.0 );
      glVertex3f( 2.0, -2.0, 0.0 );

  glEnd( );


  // Разменяме двата буфера за изрисуване. Забележете че за аргумент подаваме контекста
  // на устройството на нашия прозорец.

  SwapBuffers( hDC );
}



Свалете Visual C++ сорс кода на праграмата.

Автор: Иван Георгиев Иванов [ Nickname: tuschko ]
e-mail: tuschko@abv.bg


Този сайт е хостван от сървър на
Headoff Gaming Intranetwork