20_使用SDL显示BMP图片

文本的主要内容是:使用SDL显示一张BMP图片,算是为后面的《显示YUV图片》做准备。

为什么是显示BMP图片?而不是显示JPG或PNG图片?

  • 因为SDL内置了加载BMP的API,使用起来会更加简单,便于初学者学习使用SDL
  • 如果想要轻松加载JPG、PNG等其他格式的图片,可以使用第三方库:SDL_image

png转bmp

将之前的png图片转成bmp图片
先通过ffprobe in.png命令查看png图片的一些格式

Input #0, png_pipe, from 'in.png':
  Duration: N/A, bitrate: N/A
    Stream #0:0: Video: png, rgb24(pc), 512x512, 25 tbr, 25 tbn, 25 tbc

然后通过ffmpeg命令将png图片转成bmp

ffmpeg -i in.png -s 512x512 -pix_fmt rgb24 in.bmp

宏定义

#include 
#include 

// 出错了就执行goto end
#define END(judge, func) \
    if (judge) { \
        qDebug() << #func << "Error" << SDL_GetError(); \
        goto end; \
    }

SDL2 是一个纯 C 语音的库,我们在 C++ 代码中调用不需要加extern "C",是因为 SDL 内部做了判断,如果是 C++ 环境自动帮我们添加 extern "C"

变量定义

// 窗口
SDL_Window *window = nullptr;
// 渲染上下文
SDL_Renderer *renderer = nullptr;
// 像素数据
SDL_Surface *surface = nullptr;
// 纹理(直接跟特定驱动程序相关的像素数据)
SDL_Texture *texture = nullptr;

初始化子系统

// 初始化Video子系统
END(SDL_Init(SDL_INIT_VIDEO), SDL_Init);

加载BMP

#ifdef Q_OS_WIN
    #define FILENAME "../test/in.bmp"
#else
    #define FILENAME "/Users/zuojie/QtProjects/audio-video-dev/test/in.bmp"
#endif

// 加载BMP
surface = SDL_LoadBMP(FILENAME);
END(!surface, SDL_LoadBMP);

创建窗口

// 创建窗口
window = SDL_CreateWindow(
             // 窗口标题
             "SDL显示BMP图片",
             // 窗口的 x 坐标(SDL_WINDOWPOS_UNDEFINED:不指定 SDL_WINDOWPOS_CENTERED:中间)
             SDL_WINDOWPOS_UNDEFINED,
             //  窗口的 y 坐标
             SDL_WINDOWPOS_UNDEFINED,
             // 窗口宽度(跟图片宽度一样)
             surface->w,
             // 窗口高度(跟图片高度一样)
             surface->h,
             // 显示窗口,SDL_WindowFlags枚举值
             SDL_WINDOW_SHOWN
         );
END(!window, SDL_CreateWindow);

SDL_WindowFlags 枚举:

typedef enum
{
    SDL_WINDOW_FULLSCREEN = 0x00000001,         /**< 全屏窗口 */
    SDL_WINDOW_OPENGL = 0x00000002,             /**< 窗口可用于 OpenGL 上下文 */
    SDL_WINDOW_SHOWN = 0x00000004,              /**< 显示窗口 */
    SDL_WINDOW_HIDDEN = 0x00000008,             /**< 隐藏窗口 */
    SDL_WINDOW_BORDERLESS = 0x00000010,         /**< 不显示窗口装饰 */
    SDL_WINDOW_RESIZABLE = 0x00000020,          /**< 窗口可调整大小 */
    SDL_WINDOW_MINIMIZED = 0x00000040,          /**< 窗口最小化 */
    SDL_WINDOW_MAXIMIZED = 0x00000080,          /**< 窗口最大化 */
    SDL_WINDOW_INPUT_GRABBED = 0x00000100,      /**< 窗口可捕获键盘输入焦点 */
    SDL_WINDOW_INPUT_FOCUS = 0x00000200,        /**< 窗口拥有输入焦点 */
    SDL_WINDOW_MOUSE_FOCUS = 0x00000400,        /**< 窗口拥有光标 */
    SDL_WINDOW_FULLSCREEN_DESKTOP = ( SDL_WINDOW_FULLSCREEN | 0x00001000 ),
    SDL_WINDOW_FOREIGN = 0x00000800,            /**< window not created by SDL */
    SDL_WINDOW_ALLOW_HIGHDPI = 0x00002000,      /**< window should be created in high-DPI mode if supported.
                                                     On macOS NSHighResolutionCapable must be set true in the
                                                     application's Info.plist for this to have any effect. */
    SDL_WINDOW_MOUSE_CAPTURE = 0x00004000,      /**< window has mouse captured (unrelated to INPUT_GRABBED) */
    SDL_WINDOW_ALWAYS_ON_TOP = 0x00008000,      /**< 窗口永远置于最前 */
    SDL_WINDOW_SKIP_TASKBAR  = 0x00010000,      /**< window should not be added to the taskbar */
    SDL_WINDOW_UTILITY       = 0x00020000,      /**< window should be treated as a utility window */
    SDL_WINDOW_TOOLTIP       = 0x00040000,      /**< window should be treated as a tooltip */
    SDL_WINDOW_POPUP_MENU    = 0x00080000,      /**< window should be treated as a popup menu */
    SDL_WINDOW_VULKAN        = 0x10000000,      /**< window usable for Vulkan surface */
    SDL_WINDOW_METAL         = 0x20000000       /**< window usable for Metal view */
} SDL_WindowFlags;

我们也可以从一个已经存在的本地窗口创建 window,传入的参数是指向本地窗口的指针。我创建了一个 QLabel,此处我们传入 QLabel 的 winId() 就可以:

创建渲染上下文

// 创建渲染上下文(默认的渲染目标是window)
renderer = SDL_CreateRenderer(window, 
                              // 要初始化的渲染设备的索引,设置 -1 则初始化第一个支持 flags 的设备
                              -1,
                              SDL_RENDERER_ACCELERATED |
                              SDL_RENDERER_PRESENTVSYNC);
if (!renderer) { // 说明开启硬件加速失败
    renderer = SDL_CreateRenderer(window, -1, 0);
}
END(!renderer, SDL_CreateRenderer);

renderer 创建时传入的 window 是默认的渲染目标。SDL 文档SDL_GetRenderTarget 中也有说明。

SDL_RendererFlags:

/**
*  \brief Flags used when creating a rendering context
*/
typedef enum
{
    SDL_RENDERER_SOFTWARE = 0x00000001,         /**< 使用软件加速 */
    SDL_RENDERER_ACCELERATED = 0x00000002,      /**< 使用硬件加速 */
    SDL_RENDERER_PRESENTVSYNC = 0x00000004,     /**< 和显示器刷新率同步 */
    SDL_RENDERER_TARGETTEXTURE = 0x00000008     /**< 渲染器支持渲染到纹理 */
} SDL_RendererFlags;

这里我们是参考ffplay源码的写法:


ffplay 源码

创建纹理

// 创建纹理
texture = SDL_CreateTextureFromSurface(
              renderer,
              surface);
END(!texture, SDL_CreateTextureFromSurface);

渲染

// 设置绘制颜色(这里随便设置了一个颜色:黄色)
END(SDL_SetRenderDrawColor(renderer,
                             255, 255, 0,
                             SDL_ALPHA_OPAQUE),
      SDL_SetRenderDrawColor);

// 用DrawColor清除渲染目标
END(SDL_RenderClear(renderer),
      SDL_RenderClear);

// 复制纹理到渲染目标上(默认是window)可以使用SDL_SetRenderTarget()修改渲染目标
// srcrect源矩形框,dstrect目标矩形框,两者都传nullptr表示整个纹理渲染到整个目标上去
END(SDL_RenderCopy(renderer, texture, nullptr, nullptr),
      SDL_RenderCopy);

// 将此前的所有需要渲染的内容更新到屏幕上
SDL_RenderPresent(renderer);
  1. srcrect:源矩形框,代表截取纹理哪一部分出来;dstrect:目标矩形框,代表纹理渲染到 Rendering Target 哪一个部分;如下图:


    复制纹理到渲染目标
  2. srcrect 和 dstrect 全都传 nullptr,将整一个纹理渲染到整个渲染目标上去:


    复制纹理到渲染目标

延迟退出

// 延迟3秒退出
SDL_Delay(3000);

如果我想让显示的bmp图片窗口一直显示呢?

while (!isInterruptionRequested()) {
    SDL_Event event;
    SDL_WaitEvent(&event);
    switch (event.type) {
        case SDL_QUIT:
            goto end;
    }
}

释放资源

end:
    // 释放资源
    SDL_FreeSurface(surface);
    SDL_DestroyTexture(texture);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

代码链接

SDL的扩展用法

如果我们没有拷贝纹理数据到渲染目标的话,窗口会是一片漆黑。我们是可以设置渲染目标背景色的:

// 设置绘制颜色(画笔颜色) SDL_ALPHA_OPAQUE = 255
END(SDL_SetRenderDrawColor(renderer, 255, 255, 0, SDL_ALPHA_OPAQUE),SDL_SetRenderDrawColor);
// 设置绘制颜色(画笔颜色)清除渲染目标
END(SDL_RenderClear(renderer),SDL_RenderClear);

也可以在窗口绘制矩形框:

// 画一个黄色矩形框
SDL_SetRenderDrawColor(renderer,255,255,0,SDL_ALPHA_OPAQUE);
rect = {0, 0, 50, 50};
// SDL_RenderDrawRect(renderer, &rect);//空心矩形框
SDL_RenderFillRect(renderer, &rect);//实心矩形框

还可以修改渲染目标。有时候需要把同一个图形在窗口绘制多次,那么我们就可以新建一个 texture,然后修改渲染目标为 texture,在当前 texture 绘制完成后,修改渲染目标为 window,再复制 texture 到 window:

// 创建Texture
SDL_Texture *ShowBmpThread::createTexture(SDL_Renderer *renderer){
    // 创建纹理
    SDL_Texture *texture = SDL_CreateTexture(renderer, // 渲染上下文
                                             SDL_PIXELFORMAT_RGB24, // SDL_PixelFormatEnum,参考文档:https://wiki.libsdl.org/SDL_PixelFormatEnum
                                             SDL_TEXTUREACCESS_TARGET, // SDL_TextureAccess,此处我们要把纹理作为渲染目标,选择:SDL_TEXTUREACCESS_TARGET,参考文档:https://wiki.libsdl.org/SDL_TextureAccess
                                             50, // 纹理的宽
                                             50); // 纹理的高
    if (!texture) return nullptr;

    // 设置纹理为渲染目标
    if (SDL_SetRenderTarget(renderer, texture)) return nullptr;
    // 设置 texture 背景色
//    if (SDL_SetRenderDrawColor(renderer, 0, 155, 0, SDL_ALPHA_OPAQUE)) return nullptr;
//    if (SDL_RenderClear(renderer)) return nullptr;
    // 设置绘制颜色
    if (SDL_SetRenderDrawColor(renderer, 255, 255, 0, SDL_ALPHA_OPAQUE)) return nullptr;
    // 绘制矩形框
    SDL_Rect rect = {0, 0, 50, 50};
    if (SDL_RenderDrawRect(renderer, &rect)) return nullptr;
    // 绘制线条
    if (SDL_RenderDrawLine(renderer, 0, 0, 50, 50)) return nullptr;
    if (SDL_RenderDrawLine(renderer, 50, 0, 0, 50)) return nullptr;
    return texture;
}

监听鼠标点击事件,重新绘制矩形框到渲染目标window上:

while (!isInterruptionRequested()) {
    SDL_Event event;
    SDL_WaitEvent(&event);
    switch (event.type) {
        case SDL_QUIT:
            goto end;
    case SDL_MOUSEBUTTONUP:
            showClick(event, renderer, texture);
            break;
    }
}
void ShowBmpThread::showClick(SDL_Event &event,
                           SDL_Renderer *renderer,
                           SDL_Texture *texture) {
    SDL_MouseButtonEvent btn = event.button;
    int w = 0;
    int h = 0;
    // 查询纹理宽高
    if (SDL_QueryTexture(texture, nullptr, nullptr, &w, &h)) return;
    int x = btn.x - (w >> 1);
    int y = btn.y - (h >> 1);
    SDL_Rect dstRect = {x, y, w, h};

    // 清除①
    // if (SDL_RenderClear(renderer)) return;

    // 复制纹理到渲染目标
    if (SDL_RenderCopy(renderer, texture, nullptr, &dstRect)) return;

//    SDL_SetRenderDrawColor(renderer, 0, 0, 255, 255);
//    SDL_RenderDrawRect(renderer, &dstRect);

    // 更新渲染操作到屏幕上
    SDL_RenderPresent(renderer);
}
窗口绘制多次矩形框

在showClick方法中打开①处代码的注释if (SDL_RenderClear(renderer)) return;,就可以实现显示新的矩形框后清楚原有的矩形框

SDL显示矩形框

扩展的代码链接

注意:上面代码是在window环境下运行在子线程中的,如果是mac环境则需要放在主线程中

你可能感兴趣的:(20_使用SDL显示BMP图片)