上节课说过了,surface 是用来绘制 2D 图形的场所,而图片也是二维图形,所以可以直接把图片张贴在它上面。
本节课,我们探讨如何把一张图片贴在 surface 上。

程序结构
将一些代码封装成函数
我们把所有的代码都放在 main 函数里是十分不明智的做法,应当适当地把一些功能封装成单独的函数,能够让代码可读性更强。
我们把负责进行 SDL 初始化的一些操作步骤封装在以下函数体内:
bool mySdlInit();
再把加载媒体的操作步骤封装在:
bool mySdlLoadMedia();
最后再把一些析构函数封装在:
void mySdlClose();
前两个函数的具体实现请在文章尾部找到。
新知识:创建一个用于显示图片的 surface
我们不能直接在之前已经创建好了的 surface 上面显示图片,而必须另外再为图片创建一个 surface:
SDL_Surface* pictureSurface = NULL;
变量作用域
这次我们把一些变量放在全局范围中。但是在大型程序开发过程中,这并不是很好的习惯。 我们给出的建议是:尽量降低函数之间的耦合度。一旦我们赋予多个函数访问同一个变量的权力,那么就会非常容易让程序出现 bug。 但是由于本案例并不是很大的项目,定义且使用全局变量也不会出现任何 bug。
本案例中,我们把上面的 pictureSurface 变量就放在了全局范围。然后再把以下变量也定义在全局范围:
//想要创建的窗口
SDL_Window* window = NULL;
//窗口surface
SDL_Surface* screenSurface = NULL;
SDL_Surface* 的析构函数
当我们不再用到某个 surface 后,请记得及时释放掉它之前申请的内存。
SDL_Surface* 对象的析构函数是: void SDL_FreeSurface(SDL_Surface* surface); 它需要将 surface 对象作为参数传入,用于释放指定对象的内存。
前文说到,动态分配的内存要及时析构掉,这样可以最大化节约内存的占用,使程序运行更稳定。
关于野指针的问题
指向无效地址的指针就叫做“野指针”。析构完毕后,原来位置的内存将会被操作系统回收,且不可重新使用。
原来指向对象的指针将会变成野指针,而访问野指针是一件非常危险的事情,它可能会破坏掉其他进程的内存,引发灾难性后果。 为了安全起见,我们建议您将析构完毕的对象指针指向 零(NULL) 地址。
SDL_FreeSurface(pictureSurface);
新知识:加载 BMP 图像
SDL 原生只支持 BMP 图像的加载。 若要加载其他格式的图片,您需要另外使用一些函数库,例如 FreeImage 等,但这不在教程讨论的范围内。
首先准备好 BMP 格式的图像。要求窗口大小必须与 BMP 图像的尺寸一致,因此可能需要修改我们之前自己定义的 SCREEN_WIDTH 和 SCREEN_HEIGHT 宏值。
我这里有 960x540 尺寸的 BMP 图片:

注:可以采用一些绘图软件调整图片尺寸,并导出 BMP 格式。
SDL2 加载 BMP 的函数
SDL_Surface* SDL_LoadBMP(const char* fileName);
该函数要求传入一个文件名称(相对或绝对路径)。 加载成功则返回 SDL_Surface 的实例,失败则返回空白值。
我们首先定义一个 BMP_FILE_NAME 宏,表示 BMP 文件名称:
#define BMP_FILE_NAME "bmp/hello.bmp"
我们可以轻松地写出 bool mySdlLoadMedia(); 的实现:
bool mySdlLoadMedia()
{
//加载 BMP 文件
pictureSurface = SDL_LoadBMP(BMP_FILE_NAME);
if (!pictureSurface)
{
printf("Unable to load image: " BMP_FILE_NAME "SDL_LoadBMP Error: %s\n", SDL_GetError());
return false;
}
return true;
}
新知识:将 BMP 图像贴到屏幕 surface 上
实现这个功能的函数是 SDL_BlitSurface,它有四个参数: 1: 图片 surface 对象 3: 屏幕 surface 对象 2、4: 这两个参数暂时用不到,将在以后讨论。
在本案例的 main 函数中,请写出以下语句:
SDL_BlitSurface(pictureSurface, NULL, screenSurface, NULL);
总结
本案例接触了三个新函数:SDL_FreeSurface、SDL_LoadBMP、SDL_BlitSurface,具备了基本的显示图像的能力。
本案例的完整代码
//以下预处理指令块用于模拟bool型变量
#ifndef __cplusplus//C++语言不需要此操作
typedef unsigned char bool;
## define true 1
## define false 0
#endif
#include <SDL2/SDL.h>
#include <stdio.h>
#define printf(...) fprintf(stderr,__VA_ARGS__)
#define puts(anything) fputs(anything,stderr)
//屏幕分辨率
#define SCREEN_WIDTH 960
#define SCREEN_HEIGHT 540
#define BMP_FILE_NAME "bmp/hello.bmp"
//准备工作
bool mySdlInit();
//加载媒体
bool mySdlLoadMedia();
//释放SDL的堆内存
void mySdlClose();
//想要创建的窗口
SDL_Window* window = NULL;
//窗口surface
SDL_Surface* screenSurface = NULL;
//图片surface(另创建的)
SDL_Surface* pictureSurface = NULL;
bool mySdlInit()
{
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
printf("SDL_Init Error: %s\n", SDL_GetError());
return false;
}
window = SDL_CreateWindow("SDL教程 - 显示一张图片", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_VULKAN | SDL_WINDOW_ALWAYS_ON_TOP);
if (!window)
{
printf("SDL_CreateWindow Error: %s\n", SDL_GetError());
return false;
}
screenSurface = SDL_GetWindowSurface(window);
if (!screenSurface)
{
printf("SDL_GetWindowSurface Error: %s\n", SDL_GetError());
return false;
}
return true;
}
bool mySdlLoadMedia()
{
//加载 BMP 文件
pictureSurface = SDL_LoadBMP(BMP_FILE_NAME);
if (!pictureSurface)
{
printf("Unable to load image: " BMP_FILE_NAME "SDL_LoadBMP Error: %s\n", SDL_GetError());
return false;
}
return true;
}
void mySdlClose()
{
//析构 pictureSurface 对象
SDL_FreeSurface(pictureSurface);
//将已经释放完毕的指针指向空白
pictureSurface = NULL;
//析构 window
SDL_DestroyWindow(window);
window = NULL;
//退出 SDL
SDL_Quit();
}
int main(int argc, char* args[])
{
//启动SDL并创建一个窗口
if (!mySdlInit())
{
printf("Failed to initialize!\n");
return -1;
}
//加载媒体文件
if (!mySdlLoadMedia())
{
printf("Failed to load media!\n");
return -1;
}
//将图片 surface 粘贴到原来的 surface 上
SDL_BlitSurface(pictureSurface, NULL, screenSurface, NULL);
SDL_UpdateWindowSurface(window);
SDL_Delay(10000);
//释放堆内存并关闭 SDL
mySdlClose();
return 0;
}