前面的章节介绍了一个简单窗口的开发,这节将介绍如何把前面用到的东西用C++封装起来。
为什么用C++封装起来:
1、C语言没有异常机制,每次调用一个函数都需要通过检查返回值来判断是否成功,比较麻烦。
2、对我个人而言,开发效率上C++要优于C语言,并且C++的代码要容易组织管理,写出来的代码也更容易理解。
封装的主要部分:
1、将所有的SDL函数都用类包装起来,对于需要做返回值判断的函数,在包装的地方进行判断,然后决定是否抛出异常。这样对于调用者来说就不需要再做返回值判断了。
2、新建一个SDL类。该类用到了单例模式和工厂模式, 提供了访问所有SDL函数的入口。
3、将创建窗口和消息循环这部分代码封装成一个框架类,以后写代码时只要继承这个框架就可以了。
4、包装SDL_Surface结构体,使它可以像普通对象一样使用。因为通过SDL API获取一个SDL_Surface后需要要手动释放,否则会造成内存泄漏。
封装之后的main函数:
#include <string> #include "lessons/Lesson01.h" int main( int argc, char* args[] ) { Lesson01 frame; frame.setSize(800, 600); //设置窗口大小 frame.setTitle("Lesson01"); //设置标题 frame.open(); //打开窗口并开始循环 return 0; }
封装成这样子之后,我们可以将不同课程中的例子代码写在不同的类中,完全隔离开,到时候想运行哪一课的例子修改一下main中的Lesson01就可以了。
下面根据这份代码逐步介绍封装过程。上面main函数中用到了Lesson01类,下面先看看Lesson01里面有些什么东西。
Lesson01.h
#ifndef LESSON01_H_ #define LESSON01_H_ #include "../SDLFrame.h" class Lesson01 : public SDLFrame { public: Lesson01(); virtual ~Lesson01(); protected: void onRender(); //渲染窗口 void onInit(); //初始化 public: SDLSurfacePtr message; //界面要显示的图片 }; #endif /* LESSON01_H_ */
Lesson01.cpp
#include "Lesson01.h" Lesson01::Lesson01() { // TODO Auto-generated constructor stub } Lesson01::~Lesson01() { // TODO Auto-generated destructor stub } void Lesson01::onRender() { //将图片填充到screen SDL::video()->BlitSurface(message, NULL, screen, NULL); } void Lesson01::onInit() { //加载图片 SDLSurfacePtr loadedImage = SDL::video()->LoadBMP("E:\\code_picture\\javaeye.bmp"); //将图片转换成适合程序的格式 message = SDL::video()->DisplayFormat(loadedImage); }
这里可以看出Lesson01是继承自SDLFrame,而它本身只有两个函数,OnInit负责一些初始化工作,OnRender负责将要显示的内容填充到screen中去。
Lesson01.cpp中用到了SDL::video(),这就是前面提到的SDL类,该类提供了所有SDL函数的入口,这里的SDL::video()->BlitSurface等于SDL_BlitSurface,只是包装了一下而以。
下面看SDL类
头文件
#ifndef SDLCORE_H_ #define SDLCORE_H_ #include "SDLException.h" #include "SDLVideo.h" #include "SDLWindow.h" #include "SDLEvent.h" class SDL { public: SDL(); virtual ~SDL(); public: static void Init(Uint32 flags); //初始化SDL环境,见SDL.h中以SDL_INIT_开头的宏定义 static void Quit(); //退出SDL环境 public: static SDLVideo * video(); //SDLVideo封装了video相关的函数 static SDLWindow * window(); //SDLWindow封装了窗口相关的函数 static SDLEvent * event(); //SDLEvent封装了event相关的函数 }; #endif /* SDLCORE_H_ */
CPP文件
#include "SDLCore.h" SDL::SDL() { // TODO Auto-generated constructor stub } SDL::~SDL() { // TODO Auto-generated destructor stub } void SDL::Init(Uint32 flags) { int ret = SDL_Init(flags); if(ret == -1) { throw SDLException(std::string("初始化SDL错误:") + SDL_GetError()); } } void SDL::Quit() { SDL_Quit(); } SDLVideo * SDL::video() { static SDLVideo video; return &video; } SDLWindow * SDL::window() { static SDLWindow window; return &window; } SDLEvent * SDL::event() { static SDLEvent event; return &event; }
从上面的代码可以看出,SDL类只负责初始化和退出SDL环境,同时创建SDL相关的封装类对象,这里用到了C++静态成员变量的特性:全局生命周期且只被初始化一次。从而保证SDLVideo、SDLWindow、SDLEvent的对象全局唯一。
SDLException是程序定义的一个异常类,由于很普通,所以在这里不再进行解释。
SDLVideo、SDLWindow、SDLEvent都是SDL API函数的封装类,原理几乎是一样的,这里取其中的一个进行分析。
SDLVideo.h
#ifndef SDLVIDEO_H_ #define SDLVIDEO_H_ #include "SDLException.h" #include "SDL/SDL.h" #include "SDLSurface.h" class SDLVideo { friend class SDL; private: SDLVideo(); public: virtual ~SDLVideo(); public: /** * 设置窗口模式 * width 宽 * height 高 * bpp 颜色位数 * flags SDL.h中以SDL_INIT_开头的宏定义 * return 窗口对应的内存块 */ SDLSurfacePtr SetVideoMode(int width, int height, int bpp, Uint32 flags); /* * 将内存中的内容显示到屏幕上 * screen 内存块 */ void Flip(SDLSurfacePtr screen); /** * 将图片转换成程序需要的格式(源图片和转换后的图片在不同的内存中) * surface 源图片 * return 转换后的图片 */ SDLSurfacePtr DisplayFormat(SDLSurfacePtr surface); /* * 将硬盘上的图片加载到内存中(只支持BMP格式) * file 图片文件路径 * return 加载后内存中的图片区域 */ SDLSurfacePtr LoadBMP(std::string file); /** * 将源图片覆盖到目的图片区域上 * src 源图片 * srcrect 将要覆盖过去的源图片区域,NULL表示全部 * dst 目的图片 * dstrect 源图片要覆盖到目的图片的哪个地方,NULL表示左上角 */ void BlitSurface(SDLSurfacePtr src, SDL_Rect *srcrect, SDLSurfacePtr dst, SDL_Rect *dstrect); }; #endif /* SDLVIDEO_H_ */
SDLVideo.cpp
#include "SDLVideo.h" SDLVideo::SDLVideo() { // TODO Auto-generated constructor stub } SDLVideo::~SDLVideo() { // TODO Auto-generated destructor stub } SDLSurfacePtr SDLVideo::SetVideoMode(int width, int height, int bpp, Uint32 flags) { SDL_Surface * surface = SDL_SetVideoMode(width, height, bpp, flags); if(NULL == surface) { throw SDLException(std::string("SDL_SetVideoMode初始化视频模式时发生错误:") + SDL_GetError()); } return SDLSurfacePtr(new SDLSurface(surface)); } void SDLVideo::Flip(SDLSurfacePtr screen) { int ret = SDL_Flip(screen->value()); if(ret == -1) { throw SDLException(std::string("SDL_Flip内存内容显示到屏幕时发生错误:") + SDL_GetError()); } } SDLSurfacePtr SDLVideo::DisplayFormat(SDLSurfacePtr surface) { SDL_Surface *newSurface = SDL_DisplayFormat(surface->value()); if(NULL == newSurface) { throw SDLException(std::string("SDL_DisplayFormat转换图片格式为程序格式时发生错误:") + SDL_GetError()); } return SDLSurfacePtr(new SDLSurface(newSurface)); } SDLSurfacePtr SDLVideo::LoadBMP(std::string file) { SDL_Surface *surface = SDL_LoadBMP(file.c_str()); if(NULL == surface) { throw SDLException(std::string("SDL_LoadBMP加载BMP图片时发生错误:") + SDL_GetError()); } return SDLSurfacePtr(new SDLSurface(surface)); } void SDLVideo::BlitSurface(SDLSurfacePtr src, SDL_Rect *srcrect, SDLSurfacePtr dst, SDL_Rect *dstrect) { int ret = SDL_BlitSurface(src->value(), srcrect, dst->value(), dstrect); if(ret == -1) { throw SDLException(std::string("SDL_BlitSurface重叠图片时发生错误:") + SDL_GetError()); } }
从上面的代码可以看出SDLVideo只是简单的将SDL中video相关的函数做一下包装,检查SDL函数的返回值,如果有错误就抛出异常。在头文件中,将SDL类声明成友元类并且将构造函数设置为private是为了避免在除SDL类以外的地方实例化该类的对象。
这里用到了SDLSurfacePtr和SDLSurface。SDLSurfacePtr定义:
typedef boost::shared_ptr<SDLSurface> SDLSurfacePtr;
构造SDLSurfacePtr的代码为:
SDLSurfacePtr(new SDLSurface(surface));
可以看出,SDLSurfacePtr中有SDLSurface,SDLSurface中有SDL_Surface*。
boost库的shared_ptr是一种带引用计数的智能指针,当shared_ptr对象的引用计数变成0的时候,会自动delete它里面保存的对象,所以当最后一个SDLSurfacePtr对象析构的时候,SDLSurfacePtr会调用delete SDLSurface。关于shared_ptr的详细介绍,可以通过GOOGLE搜到很多资料。
SDLSurface的析构函数如下:
SDLSurface::~SDLSurface() { if(surface != NULL)//surface是SDL_Surface *类型 { SDL_FreeSurface(surface); } }
由于SDLSurface的析构函数中会调用SDL_Surface*的释放操作。从而使得内存中的SDL_Surface*被自动释放。这样就省去了手动释放SDL_Surface的麻烦。
最后来看看SDLFrame类
头文件:
#ifndef SDLFRAME_H_ #define SDLFRAME_H_ #include "SDL/SDLCore.h" class SDLFrame { public: static const std::string DEFAULT_TITLE; //默认窗口标题 static const int DEFAULT_SCREEN_WIDTH = 800; //默认窗口宽 static const int DEFAULT_SCREEN_HEIGHT = 600; //默认窗口高 public: SDLFrame(); virtual ~SDLFrame(); public: /* * 打开窗口 * flags 窗口模式,见SDL_video.h中的宏定义 */ void open(Uint32 flags = SDL_HWSURFACE | SDL_DOUBLEBUF); void setTitle(std::string title); void setSize(int width, int heigth); protected: /** * 消息处理函数,当有用户输入的时候,框架会调用此函数 * event 待处理的消息 * return 如果为false,则程序退出 */ virtual bool onEvent(const SDL_Event *event); /** * 当需要绘制窗口时,框架会调用此函数 */ virtual void onRender(); /** * 显示窗口前,框架会调用此函数 */ virtual void onInit(); protected: SDLSurfacePtr screen; std::string title; int width; int height; }; #endif /* SDLFRAME_H_ */
源文件:
#include "SDLFrame.h" const std::string SDLFrame::DEFAULT_TITLE = "SDL Tutorial"; SDLFrame::SDLFrame() { title = DEFAULT_TITLE; width = DEFAULT_SCREEN_WIDTH; height = DEFAULT_SCREEN_HEIGHT; } SDLFrame::~SDLFrame() { // TODO Auto-generated destructor stub } void SDLFrame::open(Uint32 flags) { //初始化SDL环境 SDL::Init(SDL_INIT_EVERYTHING); //设置屏幕模式 screen = SDL::video()->SetVideoMode(width, height, 32, flags); //设置窗口标题 SDL::window()->SetCaption(title); //初始化 onInit(); //开始事务循环 SDL_Event event; bool bQuit = false; while(!bQuit) { while( SDL::event()->PollEvent( &event ) ) { if(!onEvent(&event)) { bQuit = true; } } //绘制 onRender(); //将在内存中的处理结果显示到屏幕上 SDL::video()->Flip(screen); } //退出SDL环境 SDL::Quit(); } void SDLFrame::setTitle(std::string title) { this->title = title; } void SDLFrame::setSize(int width, int heigth) { this->width = width; this->height = heigth; } bool SDLFrame::onEvent(const SDL_Event *event) { switch(event->type) { case SDL_KEYDOWN: if(event->key.keysym.sym == SDLK_ESCAPE) { return false; } break; case SDL_QUIT: return false; break; default: break; } return true; } void SDLFrame::onRender() { } void SDLFrame::onInit() { }
SDLFrame类封装了消息循环,通过在循环中调用成员函数的方式将消息循环中公共的部分与特殊的部分分离开,从而可以在基类中重载这些成员函数使不同的基类表现出不通的特性。其中onEvent负责处理用户输入,onInit负责窗口创建后的初始化,onRender负责窗口的绘制。
这里onEvent只处理了窗口关闭和ESC键按下两个消息,子类可以通过重载来覆盖默认实现。onInit和onRender都是空实现。需要在子类中去实现具体的操作。
结合消息循环,现在再回过头去看Lesson01的代码,就会发现只要程序一有空闲,就会调用onRender函数,而Lesson1的onRender函数中只有一行代码:SDL::video()->BlitSurface(message, NULL, screen, NULL);,并且这行代码中用到的message和screen永远不会变,你可能会想老这样调用同样的代码是不是很浪费资源,在这里确实是浪费资源,其实只要将这行代码放到onInit函数的末尾就可以了。这里可以这样做的原因是因为程序初始化好了之后内存中的内容不再会发生变化,所以每次调用SDL::video()->Flip(screen)都不会改变屏幕显示的内容。说的通俗点,就是这个程序太简单了,用不着定时去更新窗口。后面的章节中将会看到定时更新窗口的用处。
这节的内容就介绍到这里,在以后的章节中,都将采用同Lesson01一样的方式来编写代码。附件中是本节内容的完整源代码。