SDL + wxWidgets 통합 강좌
wxWidgets는 크로스 플랫폼 라이브러리이므로 Windows뿐 아니라 여러 운영 체제에서 사용할 수 있는 "그래픽 사용자 인터페이스"(메뉴 등)를 제공하는 라이브러리입니다. 프로그래머가 Win32 API만을 고집하게 되면 다른 운영 체제로 기존의 소스 코드를 이식할 수가 없어서 코드 변경을 해 주어야 하는 불편함이 따르는 데 반해 wxWidgets는 통합 그래픽 사용자 인터페이스를 사용함으로써 코드를 한 번만 짜 주면 여러 플랫폼에서 사용할 수 있다는 장점이 있습니다.
SDL 또한 마찬가지로 크로스 플랫폼 라이브러리이므로 여러 곳에서 사용할 수 있습니다. 영상과 소리 출력 등을 다루는 데 도움을 줍니다.
아래는 SDL의 창 내용을 wxWidgets의 사용자 인터페이스(창)에 결합시키는 강좌입니다.
이 강좌는 SDL_Surface를 wxWidgets에 이용하여 SDL의 그래픽을 제공합니다. (OpenGL을 이용하는 것이 아닙니다.)
일단 wx_sdl.cc라는 새로운 파일을 만들어 봅시다. 이름은 아무 것이나 상관 없으나 컴파일을 할 때(Makefile 등을 사용시) 신경을 써 주셔야 할 것입니다.
#include <iostream>
#include <wx/wxprec.h>
#ifndef WX_PRECOMP
#include <wx/wx.h>
#endif
#include <wx/dcbuffer.h>
#include <wx/image.h>
#include "SDL.h"
class SDLApp : public wxApp {
DECLARE_CLASS(SDLApp)
private:
SDLFrame *frame;
public:
bool OnInit();
int OnRun();
int OnExit();
};
bool SDLApp::OnInit() {
// SDLFrame 만들기
frame = new SDLFrame;
frame->SetClientSize(640, 480);
frame->Centre();
frame->Show();
// SDLFrame을 최상위 창으로
SetTopWindow(frame);
// 초기화 성공
return true;
}
int SDLApp::OnRun() {
// SDL 초기화
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
std::cerr << "unable to init SDL: " << SDL_GetError() << '\n';
return -1;
}
// idle 이벤트 생성
wxIdleEvent event;
event.SetEventObject(&frame->getPanel());
frame->getPanel().AddPendingEvent(event);
// 루프 시작
return wxApp::OnRun();
}
int SDLApp::OnExit() {
// SDL 끝내기
SDL_Quit();
// 표준 exit 코드 반환
return wxApp::OnExit();
}
IMPLEMENT_CLASS(SDLApp, wxApp)
IMPLEMENT_APP(SDLApp)
가장 기본적인 설정입니다. 헤더 파일은 wxWidgets, SDL, STL iostream 라이브러리를 포함하고 있습니다. 나중에는 다른 클래스를 만들 때 wx/image.h와 wx/dcbuffer.h가 필요합니다. 일단 계속해 봅시다. (wxWidgets와 SDL는 최신 버전을 이용하고 있습니다.)
SDLApp 클래스에는 다음의 세 가지가 있습니다: OnInit, OnRun, OnExit
일단은 응용 프로그램을 초기화하고(OnInit), 실행할 준비를 하고(OnRun), 응용 프로그램을 끝냅니다(OnExit). OnInIt()을 먼저 봅시다.
// SDLFrame 만들기
frame = new SDLFrame;
frame->SetClientSize(640, 480);
frame->Centre();
frame->Show();
// SDLFrame을 최상위 창으로
SetTopWindow(frame);
여기에 SDLFrame을 만들고 이 SDLFrame을 최상위 창으로 만들 것입니다. SDLFrame이 뭔지에 대해서는 아직 생각할 필요가 없습니다. 아직 만들지도 않았으니까요. 이제 wxFrame을 확장할 준비가 되었습니다. 클라이언트 창 크기를 640x480으로 설정하고 화면 가운데에 보이게 할 것입니다.
OnInit()은 매우 단순하므로 이제 OnRun()으로 넘어갑시다.
// SDL 초기화
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
std::cerr << "unable to init SDL: " << SDL_GetError() << '\n';
return -1;
}
여기서 SDL 라이브러리를 초기화합니다. 여기에 사용한 것은 비디오 출력밖에 없으며 이것은 초기화하려는 SDL의 일부일 뿐입니다.
// idle 이벤트 생성
wxIdleEvent event;
event.SetEventObject(&frame->getPanel());
frame->getPanel().AddPendingEvent(event);
유휴(idle) 이벤트가 있는 동안에도 드로잉(그리기)을 계속 할 것이므로 루프를 걸어 줍시다.
// 루프 시작
return wxApp::OnRun();
끝으로 wxWidgets 메인 이벤트 루프 처리를 시작하는 부모 wxApp:OnRun()를 호출합니다. SDL을 완전히 사용하고 있지 않더라도 wxWidgets가 일반적으로 이벤트를 핸들할 수 있게 도와 줍니다. 그러나 이 기능은 SDL과 wxWidgets 이벤트를 모두 사용하고 싶을 때에는 동작하지 않습니다. 끝으로 OnExit()입니다.
// SDL 끝내기
SDL_Quit();
// 표준 exit 코드 반환
return wxApp::OnExit();
OnExit()는 OnInit()보다 더 단순합니다. 대부분의 SDL 프로그램들은 atexit() 함수를 사용하여 SDL_Quit() 콜백을 등록하지만 일단 이것을 여기에 넣을 것입니다.
이제 기본 응용 프로그램 클래스가 완성되었으므로 SDLFrame를 빌드해 봅시다.
아래의 줄들을 SDL 헤더 include 바로 뒤에 추가합시다. (wx_sdl.cc 파일)
enum {
ID_FRAME = 10000,
ID_PANEL,
IDM_FILE_EXIT,
IDM_HELP_ABOUT
};
class SDLFrame : public wxFrame {
DECLARE_CLASS(SDLFrame)
DECLARE_EVENT_TABLE()
private:
SDLPanel *panel;
void onFileExit(wxCommandEvent &event);
void on_HelpAbout(wxCommandEvent &event);
public:
SDLFrame();
SDLPanel &getPanel();
};
inline void SDLFrame::onFileExit(wxCommandEvent &) { Close(); }
inline SDLPanel &SDLFrame::getPanel() { return *panel; }
IMPLEMENT_CLASS(SDLFrame, wxFrame)
BEGIN_EVENT_TABLE(SDLFrame, wxFrame)
EVT_MENU(IDM_FILE_EXIT, SDLFrame::onFileExit)
EVT_MENU(IDM_HELP_ABOUT, SDLFrame::on_HelpAbout)
END_EVENT_TABLE()
SDLFrame::SDLFrame() {
// SDLFrame 만들기
Create(NULL, ID_FRAME, wxT("Frame Title"), wxDefaultPosition,
wxDefaultSize, wxCAPTION | wxSYSTEM_MENU |
wxMINIMIZE_BOX | wxCLOSE_BOX);
// 메뉴 모음 만들기
wxMenuBar *mb = new wxMenuBar;
// 파일 메뉴 만들기
wxMenu *fileMenu = new wxMenu;
fileMenu->Append(IDM_FILE_EXIT, wxT("끝내기(&X)"));
// 파일 메뉴를 메뉴 모음에 추가
mb->Append(fileMenu, wxT("파일(&F)"));
// 도움말 메뉴 만들기
wxMenu *helpMenu = new wxMenu;
helpMenu->Append(IDM_HELP_ABOUT, wxT("정보"));
// 도움말 메뉴를 메뉴 모음에 추가
mb->Append(helpMenu, wxT("도움말(&H)"));
// 메뉴 모음을 SDLFrame에 추가
SetMenuBar(mb);
// SDLPanel 만들기
panel = new SDLPanel(this);
}
void SDLFrame::on_HelpAbout(wxCommandEvent &) {
wxMessageBox(wxT("wx-sdl tutorial\nCopyright (C) 2005 John Ratliff"),
wxT("about wx-sdl tutorial"), wxOK | wxICON_INFORMATION);
}
SDLFrame은 응용 프로그램에 위젯을 추가합니다. 응용 프로그램과 메뉴가 클릭할 수 있는 Frame을 가집니다. SDLFrame은 세 개의 메쏘드와 생성자가 있습니다. 생성자부터 시작합시다.
// SDLFrame 만들기
Create(NULL, ID_FRAME, wxT("Frame Title"), wxDefaultPosition,
wxDefaultSize, wxCAPTION | wxSYSTEM_MENU |
wxMINIMIZE_BOX | wxCLOSE_BOX);
몇 가지 기본 옵션으로 wxFrame를 만듭니다. 최상위 창이므로 부모 창은 없습니다. 인식자로 ID_FRAME을 사용합니다. 창 제목은 Frame Title입니다. SDLApp의 위치와 크기를 설정하므로 이것을 기본값으로 둡니다. 크기를 조절할 수 있는 테두리와 최대화 단추가 제거된 wxFrame의 기본 스타일을 만들어 봅시다.
// 메뉴 모음 만들기
wxMenuBar *mb = new wxMenuBar;
// 파일 메뉴 만들기
wxMenu *fileMenu = new wxMenu;
fileMenu->Append(IDM_FILE_EXIT, wxT("끝내기(&X)"));
// 파일 메뉴를 메뉴 모음에 추가
mb->Append(fileMenu, wxT("파일(&F)"));
// 도움말 메뉴 만들기
wxMenu *helpMenu = new wxMenu;
helpMenu->Append(IDM_HELP_ABOUT, wxT("정보"));
// 도움말 메뉴를 메뉴 모음에 추가
mb->Append(helpMenu, wxT("도움말(&H)"));
// 메뉴 모음을 SDLFrame에 추가
SetMenuBar(mb);
이제 SDLFrame을 위한 메뉴 모음을 만듭니다. 파일과 도움말이라는 두 개의 메뉴가 있습니다. 파일 메뉴는 끝내기 항목이 있고 도움말 메뉴에는 정보 항목이 있습니다.
BEGIN_EVENT_TABLE(SDLFrame, wxFrame)
EVT_MENU(IDM_FILE_EXIT, SDLFrame::onFileExit)
EVT_MENU(IDM_HELP_ABOUT, SDLFrame::on_HelpAbout)
END_EVENT_TABLE()
메뉴 항목의 이벤트를 핸들하려면 이벤트 테이블을 설정하여야 합니다. 끝내기를 누르면 SDLFrame::onFileExit을 호출합니다. 정보를 누르면 SDLFrame::on_HelpAbout을 호출합니다.
// SDLPanel 만들기
panel = new SDLPanel(this);
끝으로 하나의 클라이언트 창을 만들어 봅시다. 이것은 SDLPanel입니다. wxPanel을 확장할 준비가 되었습니다.
이 세 개의 메쏘드는 매우 단순하므로 여기에 다시 서술하지는 않겠습니다. onFileExit()은 Close()를 호출하여 프레임을 닫습니다. on_HelpAbout()는 기본 정보가 있는 wxMessageBox를 보여 줍니다. getPanel()은 SDLPanel에 대한 레퍼런스를 반환합니다.
이제 SDLPanel을 만들 차례입니다. 이 줄을 enum 바로 아래에 삽입합니다.
class SDLPanel : public wxPanel {
DECLARE_CLASS(SDLPanel)
DECLARE_EVENT_TABLE()
private:
SDL_Surface *screen;
void onPaint(wxPaintEvent &event);
void onEraseBackground(wxEraseEvent &event);
void onIdle(wxIdleEvent &event);
void createScreen();
public:
SDLPanel(wxWindow *parent);
~SDLPanel();
};
inline void SDLPanel::onEraseBackground(wxEraseEvent &) {}
IMPLEMENT_CLASS(SDLPanel, wxPanel)
BEGIN_EVENT_TABLE(SDLPanel, wxPanel)
EVT_PAINT(SDLPanel::onPaint)
EVT_ERASE_BACKGROUND(SDLPanel::onEraseBackground)
EVT_IDLE(SDLPanel::onIdle)
END_EVENT_TABLE()
SDLPanel::SDLPanel(wxWindow *parent) : wxPanel(parent, ID_PANEL), screen(NULL) {
// wxPanel 크기 설정
wxSize size(640, 480);
SetMinSize(size);
SetMaxSize(size);
}
SDLPanel::~SDLPanel() {
if (screen != NULL) {
SDL_FreeSurface(screen);
}
}
void SDLPanel::onPaint(wxPaintEvent &) {
// 화면이 없으면 그릴 수 없음
if (screen == NULL) {
return;
}
// lock the surface if necessary
if (SDL_MUSTLOCK(screen)) {
if (SDL_LockSurface(screen) < 0) {
return;
}
}
// 픽셀 데이터에서 비트맵 만들기
wxBitmap bmp(wxImage(screen->w, screen->h,
static_cast<unsigned char *>(screen->pixels), true));
// 화면 잠금 해제
if (SDL_MUSTLOCK(screen)) {
SDL_UnlockSurface(screen);
}
// 화면 그리기
wxBufferedPaintDC dc(this, bmp);
}
void SDLPanel::onIdle(wxIdleEvent &) {
// create the SDL_Surface
createScreen();
// 필요하면 surface 잠금
if (SDL_MUSTLOCK(screen)) {
if (SDL_LockSurface(screen) < 0) {
return;
}
}
// SDL에 요청 (밀리초)
int tick = SDL_GetTicks();
for (int y = 0; y < 480; y++) {
for (int x = 0; x < 640; x++) {
wxUint32 color = (y * y) + (x * x) + tick;
wxUint8 *pixels = static_cast<wxUint8 *>(screen->pixels) +
(y * screen->pitch) +
(x * screen->format->BytesPerPixel);
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
pixels[0] = color & 0xFF;
pixels[1] = (color >> 8) & 0xFF;
pixels[2] = (color >> 16) & 0xFF;
#else
pixels[0] = (color >> 16) & 0xFF;
pixels[1] = (color >> 8) & 0xFF;
pixels[2] = color & 0xFF;
#endif
}
}
// 필요하면 잠금 해제
if (SDL_MUSTLOCK(screen)) {
SDL_UnlockSurface(screen);
}
// 패널 새로 고침
Refresh(false);
// Sleep
wxMilliSleep(33);
}
void SDLPanel::createScreen() {
if (screen == NULL) {
int width, height;
GetSize(&width, &height);
screen = SDL_CreateRGBSurface(SDL_SWSURFACE, width, height,
24, 0, 0, 0, 0);
}
}
이 클래스는 가장 크고 어려워 보이지만 하나씩 따라가다 보면 어렵지 않습니다. 생성자 하나, 소멸자 하나, 네 개의 메쏘드가 있습니다. 생성자를 시작해 봅시다.
SDLPanel::SDLPanel(wxWindow *parent) : wxPanel(parent, ID_PANEL), screen(NULL) {
// wxPanel 크기 설정
wxSize size(640, 480);
SetMinSize(size);
SetMaxSize(size);
}
끝으로 올바른 창 크기를 갖도록 하기 위해 SDLPanel의 최소 및 최대 크기를 설정합니다.
BEGIN_EVENT_TABLE(SDLPanel, wxPanel)
EVT_PAINT(SDLPanel::onPaint)
EVT_ERASE_BACKGROUND(SDLPanel::onEraseBackground)
EVT_IDLE(SDLPanel::onIdle)
END_EVENT_TABLE()
매우 직관적인 테이블입니다. Paint 이벤트는 onPaint()가 핸들합니다. 각 배경 이벤트는 onEraseBackground()가 핸들합니다. 끝으로 idle(유휴) 이벤트는 onIdle이 핸들합니다. onIdle()을 살펴봅시다.
// SDL_Surface 만들기
createScreen();
첫 번째 부분은 화면 생성에 관한 것입니다. createScreen()을 살펴 보면 화면이 어떻게 나타나는지 볼 수 있습니다.
if (screen == NULL) {
int width, height;
GetSize(&width, &height);
screen = SDL_CreateRGBSurface(SDL_SWSURFACE, width, height,
24, 0, 0, 0, 0);
}
화면이 NULL이면 SDL_CreateRGBSurface를 이용한 것을 하나 만듭니다. 중요한 변수가 정의됩니다. SDL_SWSURFACE는 메모리 버퍼를 유지하는 데 사용합니다. width와 height는 SDLPanel에서 나옵니다. 24는 24비트 RGB 색을 이야기합니다. 다른 변수는 기본값으로 0으로 되어 있지만 아예 사용하지 않을 것입니다.
화면을 이제 가지게 되었는데 이것이 무엇인지 알았습니다. 이제 onIdle()로 되돌아갑시다.
// 필요하면 surface 잠금
if (SDL_MUSTLOCK(screen)) {
if (SDL_LockSurface(screen) < 0) {
return;
}
}
필요하면 이용하기 전에 SDL_Surface를 잠글(lock) 수 있게 해 줍니다.
// SDL 요청 (밀리초)
int tick = SDL_GetTicks();
for (int y = 0; y < 480; y++) {
for (int x = 0; x < 640; x++) {
wxUint32 color = (y * y) + (x * x) + tick;
wxUint8 *pixels = static_cast<wxUint8 *>(screen->pixels) +
(y * screen->pitch) +
(x * screen->format->BytesPerPixel);
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
pixels[0] = color & 0xFF;
pixels[1] = (color >> 8) & 0xFF;
pixels[2] = (color >> 16) & 0xFF;
#else
pixels[0] = (color >> 16) & 0xFF;
pixels[1] = (color >> 8) & 0xFF;
pixels[2] = color & 0xFF;
#endif
}
}
그렇게 복잡한 것은 아닙니다. 각 화소에 도형이 있는 빛깔을 만들어 냅니다. 24비트 색을 사용하고 있으므로 3개의 8비트 값을 가지게 됩니다.
// 필요하면 잠금 해제
if (SDL_MUSTLOCK(screen)) {
SDL_UnlockSurface(screen);
}
// 패널 새로 고침
Refresh(false);
// Sleep
wxMilliSleep(33);
화소에 대한 작업을 끝냈으면 surface의 잠금을 해제(unlock)합니다. 그 뒤 paint 이벤트를 만드는 Refresh()를 호출합니다. 끝으로 유휴 시간이 그리 자주 있겠지만 잠시 동안은 sleep로 처리해 두었습니다.
onIdle()은 paint 이벤트를 만들어 냈으므로 이제 onPaint()가 호출될 것입니다. onPaint()를 살펴 봅시다.
// 화면이 없으면 그릴 수 없음
if (screen == NULL) {
return;
}
// 필요하면 surface 잠금
if (SDL_MUSTLOCK(screen)) {
if (SDL_LockSurface(screen) < 0) {
return;
}
}
// 픽셀 데이터에서 비트맵 만들기
wxBitmap bmp(wxImage(screen->w, screen->h,
static_cast<unsigned char *>(screen->pixels), true));
//화면 잠금 해제
if (SDL_MUSTLOCK(screen)) {
SDL_UnlockSurface(screen);
}
오류 검사에 대한 것입니다. 화면이 존재하지 않은 패널에는 그리기(paint)를 할 수 없으므로 화면이 있는지부터 검사해 주어야 합니다. 접근하기 전에 화면을 잠가야 합니다.
다음에는 SDL 픽셀 데이터에서 만든 wxImage에서 wxBitmap을 을 만듭니다. 이 wxBitmap을 만든 뒤 화면의 잠금을 해제할 수 있습니다.
// 화면 그리기
wxBufferedPaintDC dc(this, bmp);
끝으로 wxBufferedPaintDC을 이용하여 화면의 bmp를 블릿(blit)처리합니다. 이것은 깜빡임을 줄여 줍니다. wxPaintDC를 사용하고 비트맵을 블릿시킬 수도 있었지만 비트맵으로는 wxBufferedPaintDC를 사용하는 것이 코드를 짜기가 더 쉽습니다.
배경을 그리고 지우는 이벤트가 이벤트 발생에 독립적으로 일어나므로 이들을 핸들해 주어야 합니다. paint 이벤트 쪽은 이미 다루었으므로 배경을 지우는 이벤트를 그냥 놔둡니다. 이렇게 하면 깜빡임을 줄일 수 있고 핸들러는 비어 있게 됩니다.
끝으로 소멸자를 살펴 봅시다.
if (screen != NULL) {
SDL_FreeSurface(screen);
}
이 소멸자는 SDL_Surface의 할당을 제거합니다.
──────────────────────────