codecamp
案例三、事件驱动式编程

在前两个案例中,我们还不具备处理用户任何输入的能力,都是在窗口出现 10 秒后自动退出。 本案例将会教大家如何让你的程序具备响应事件的能力。

本案例的重要概念

消息队列的概念

消息(事件)是可以不断产生的,而我们为了暂时保存未处理的消息,引入了一种数据数据结构——队列

队列示意图

队列,是一种有顺序、有方向、有开头(front)和结尾(rear)的数据结构在每一次操作中,我们只能在队列的尾部追加一项数据,也只能从队列的首部删除一项数据。

就好比我们在食堂排队一样,队首的人打饭,后来的人站在队伍的最后。

事件驱动的概念

“事件驱动”是一种用于接收并处理实时事件的编程方法。它的实现方法是:

  1. 使用一个事件循环(死循环),不断地接收外界传来的消息
  2. 迅速处理消息队列内的每一个消息,处理完毕的消息将退出这个队列,直到队列中没有任何消息。
  3. 不断重复上面这两个动作,直到程序退出为止。

新知识:在 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 的压力。 作者的电脑 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;
}
案例二、在 surface 上面贴一张图片
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }