文本的主要内容是:使用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源码的写法:
创建纹理
// 创建纹理
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);
srcrect:源矩形框,代表截取纹理哪一部分出来;dstrect:目标矩形框,代表纹理渲染到 Rendering Target 哪一个部分;如下图:
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;
,就可以实现显示新的矩形框后清楚原有的矩形框
扩展的代码链接
注意:上面代码是在window环境下运行在子线程中的,如果是mac环境则需要放在主线程中