在前两个案例中,我们还不具备处理用户任何输入的能力,都是在窗口出现 10 秒后自动退出。 本案例将会教大家如何让你的程序具备响应事件的能力。
本案例的重要概念
消息队列的概念
消息(事件)是可以不断产生的,而我们为了暂时保存未处理的消息,引入了一种数据数据结构——队列。

队列,是一种有顺序、有方向、有开头(front)和结尾(rear)的数据结构。在每一次操作中,我们只能在队列的尾部追加一项数据,也只能从队列的首部删除一项数据。
就好比我们在食堂排队一样,队首的人打饭,后来的人站在队伍的最后。
事件驱动的概念
“事件驱动”是一种用于接收并处理实时事件的编程方法。它的实现方法是:
- 使用一个事件循环(死循环),不断地接收外界传来的消息
- 迅速处理消息队列内的每一个消息,处理完毕的消息将退出这个队列,直到队列中没有任何消息。
- 不断重复上面这两个动作,直到程序退出为止。
新知识:在 SDL2 中采用事件驱动式编程
在 SDL2 中,我们看不到消息队列在哪里定义的,也无需了解这些知识,SDL 库的内部已经有复杂的实现。
定义一个 SDL_Events 结构体变量
SDL_Event events;
这个结构体存储了 SDL 中的消息及其属性值。 当某事件发生时,这个结构体中的对应的成员及其子成员将变成非空值。我们可以通过读取这些值是否非 0、具体是多少,来判断消息的类型与内容,方法请见下文。
创建一个主事件循环
我们定义一个主事件循环,再在它的后面定义一个 _quit 标签。此循环内部内部将会有 goto 语句负责终止它:
for(;;)
{
/*主事件循环的内容...*/
}
_quit:
/*后续的代码...*/
在以上的循环的内部,再创建一个循环,用于移除并处理消息队列中的每一个消息:
//当消息队列中有消息时会进行此循环,直到所有的消息都处理完毕
while (SDL_PollEvent(&events))
{
/*一些代码*/
}
/*本代码请在上文 for(;;) 的代码块内填写*/
在这个循环内部,我们做个判断,判断用户是否试图终止程序,方法就是上文提到的判断 SDL_Events 的成员值。 如果条件成立,那么就直接跳出所有层次的循环:
//如果触发了关闭窗口的事件
if (events.type == SDL_QUIT)
{
goto _quit;
}
降低 CPU 使用率
SDL 的事件循环非常占用 CPU 的时间,CPU 会马不停蹄地去进行无太大必要的循环,直接占满 CPU 的一个线程,非常不合适:

但是我们有相应的解决办法,就是在主事件循环里加入短暂的停顿,为 CPU 腾出空闲时间。
我们将事件循环的频率调整为只比屏幕刷新率略微高一点。这看似停顿的时间不是很长,但对于 CPU 来说,极大减轻了 CPU 的压力。 作者的电脑 CPU 是 Intel Core i5 - 11300H,减压后几乎占用率为 0:

可以通过以下方法实现:
获取延迟时间(毫秒)
这里作者使用了 SDL_DisplayMode 来实现,但不是本案例的重点,直接照着抄代码即可:
//这两行代码是什么意思不需要了解
SDL_DisplayMode displayMode;
SDL_GetWindowDisplayMode(window, &displayMode);
//获取屏幕刷新率,并求出延迟时间
uint16_t SCREEN_REFRESH_INTERVAL = (uint16_t)(1000.0 / displayMode.refresh_rate - 1);
在主循环内使用延迟
获取到延迟时间后,请在主事件循环for(;;){}里面追加一个延迟函数:
//延迟一会,降低 CPU 占用
SDL_Delay(SCREEN_REFRESH_INTERVAL);
至此,我们已经对 SDL 的事件驱动式编程有了初步了解。
案例完整代码
#include <SDL2/SDL.h>
#include <stdio.h>
#ifndef __cplusplus
typedef unsigned char bool;
## define true 1
## define false 0
#endif
#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();
void mySdlClose();
SDL_Window* window = NULL;
SDL_Surface* screenSurface = NULL;
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);
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()
{
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()
{
SDL_FreeSurface(pictureSurface);
pictureSurface = NULL;
SDL_DestroyWindow(window);
window = NULL;
SDL_Quit();
}
int main(int argc, char* argv[])
{
if (!mySdlInit())
{
printf("Failed to initialize!\n");
return -1;
}
if (!mySdlLoadMedia())
{
printf("Failed to load media!\n");
return -1;
}
SDL_BlitSurface(pictureSurface, NULL, screenSurface, NULL);
//这些代码是什么意思不需要了解
SDL_DisplayMode displayMode;
SDL_GetWindowDisplayMode(window, &displayMode);
//获取屏幕刷新率,并求出延迟时间
uint16_t SCREEN_REFRESH_INTERVAL = (uint16_t)(1000.0 / displayMode.refresh_rate - 1);
printf("Refresh interval = %d ms\n", SCREEN_REFRESH_INTERVAL);
//本案例可以只调用它一次,但是若想做动态画面,还是需要将它放在主循环内
SDL_UpdateWindowSurface(window);
//Event handler
SDL_Event events;
//当程序一直运行时
for (;;)
{
//处理队列中的消息
while (SDL_PollEvent(&events))
{
//如果触发了关闭窗口的事件
if (events.type == SDL_QUIT)
{
goto _quit;
}
}
//延迟一会,降低 CPU 占用
SDL_Delay(SCREEN_REFRESH_INTERVAL);
}
_quit:
puts("You have closed your SDL2 window");
mySdlClose();
return 0;
}