🎬 第六章 SDL 2开发库及高级应用
📄 一句话概括

嵌入只有一个函数:SDL_CreateWindowFrom(hwnd)。 代价是嵌入之后 SDL 不再泵消息,SDL_WaitEvent 不能用了。

📝 本节和前面几节的关系

第二章讲过把 LibVLC 的画面嵌进 MFC 与 Qt,用的是 set_hwnd。这一节是同一件事,换成了 SDL。 思路完全一致:交出一个窗口句柄,让库在那块画布上画。 区别在「谁泵消息」这条线画在哪。

一、一个函数,一个句柄

三种宿主的句柄来源:

宿主 句柄从哪来 消息循环归谁
MFC GetSafeHwnd() / m_hWnd MFC
Qt widget->winId() Qt
Win32 CreateWindow 的返回值 你自己的 GetMessage

第三列是全部要点:嵌入之后,SDL 只负责「画」,不再泵消息。 所以 SDL_WaitEvent 不能用了——它会跟宿主的消息循环抢消息,两边都卡。

正确的做法是「自己处理宿主的消息,再转给 SDL」:在 MFC 里重写 PreTranslateMessage, 在 Qt 里装事件过滤器,把键盘与鼠标事件翻译成 SDL_PushEvent 塞进去。

这跟第二章 LibVLC 那个坑是同一个: 嵌入之后「谁管消息」必须指定清楚,否则两边都在等,界面就死了。 区别是 VLC 有自己的线程、不太依赖宿主消息;SDL 的渲染与事件是跟窗口绑在一起的,所以更敏感。

二、尺寸和重绘要自己管

嵌入之后,窗口的尺寸由宿主决定,SDL 不会自动跟着变。 宿主一缩放,你得做三件事: 收到 WM_SIZE(或者 Qt 的 resizeEvent)。 SDL_SetWindowSize,或者干脆重建纹理。 重新算 RenderCopy 的目标矩形,否则画面还是旧尺寸。

不处理的表现很具体:画面还是原来的大小,右下角露出一块底色。 而且它看起来像「渲染没刷新」,其实是「尺寸没告诉它」。

「重建纹理」还是「只改目标矩形」,取舍很清楚: 只改目标矩形:纹理保持原分辨率,缩放由 RenderCopy 做。省一次分配,但放大后会糊。 重建纹理:按新尺寸重新分配。清晰,但每次缩放都要分配一次,拖动窗口时可能卡。

折中做法是「拖动过程中只改矩形,松手后再重建」——跟 04.03 那个「拖动时不 seek、松手才 seek」是同一个思路:把贵的操作推到「停下来」之后。

三、出错了怎么问它

Demo 里传了一个空句柄,SDL 返回空指针,错误串是「That operation is not supported」。 这句话信息量很低——它没说「句柄无效」,只说「不支持」。

原因在驱动上:dummy 驱动根本没有「从已有句柄创建窗口」这个能力, 所以报的是「这个操作不支持」而不是「参数错误」。

这暴露了 SDL 错误处理的一个普遍特点:返回空指针表示失败,原因放在 SDL_GetError() 里, 那个字符串经常只有一句话、指向不精确。

所以调试 SDL 的正确姿势是「把每个返回值和错误串都打出来」:不要写 if (!ptr) return;。 要写成「先打印 SDL_GetError() 再返回」。 这一行 printf 的投入产出比,比任何调试器都高。

最后补一个取舍:如果项目本身就是 MFC 或 Qt 的,值不值得为了显示视频引入 SDL? 如果只是「放一段视频」——不值得,QMediaPlayer 或 LibVLC 更省事。 如果要「自己控制每一帧怎么画」——值得, 因为 SDL 的纹理接口足够薄,而且第八章的 FFplay 就是这么做的。

四、动手 Demo:把 SDL 窗口塞进别人的窗口里

这段列出三种宿主的句柄来源与消息循环归属,再真跑一次「句柄不对」的情况, 看 SDL 到底给出什么样的错误信息。 结果那句话很值得看:它说的是「不支持」,不是「句柄无效」。

# 三种宿主的句柄来源
hwnd_mfc = "GetSafeHwnd()"      # MFC
hwnd_qt  = "widget->winId()"     # Qt

# 嵌入:一个函数
win = sdl2.SDL_CreateWindowFrom(handle)
if not win:
    print(sdl2.SDL_GetError())    # 一定要打出来

# 嵌入之后不能再调:会跟宿主抢消息
# sdl2.SDL_WaitEvent(...)

实跑输出:

--------------------------------------------------------------------
把 SDL 窗口塞进别人的窗口里
--------------------------------------------------------------------
**嵌入只有一个函数**:`SDL_CreateWindowFrom(hwnd)`。
**传进去一个宿主窗口的句柄,SDL 就在那块画布上画。**

  宿主      句柄从哪来                     消息循环归谁
  -----------------------------------------------------------------------
  MFC       `GetSafeHwnd()` / `m_hWnd`     MFC
  Qt        `widget->winId()`              Qt
  Win32     `CreateWindow` 的返回值        你自己的 `GetMessage` 循环

**第三列是全部要点**:**嵌入之后 SDL 只负责「画」,不再泵消息。**
**所以 `SDL_WaitEvent` 不能用了**——**它会跟宿主的消息循环抢消息,两边都卡。**

**真跑一次「句柄不对」的情况**,看它怎么说:

  `SDL_CreateWindowFrom(NULL)` = **失败**
  错误信息 = **That operation is not supported**

**「That operation is not supported」这句话信息量很低**,**它没说「句柄无效」,只说「不支持」。**
**因为在 dummy 驱动下根本没有「创建窗口自某句柄」这个能力。**

**这条正好演示了 SDL 的错误风格**:
**返回空指针表示失败,原因放在 `SDL_GetError()` 里,而那个字符串经常只有一句话、指向并不精确。**
**所以调试 SDL 的正确姿势是「先把每个返回值和错误串都打出来」**,**而不是盯着代码猜。**

**最后一个实践细节**:**嵌入之后尺寸由宿主决定**,
**宿主窗口一缩放,你要自己通知 SDL**——`SDL_SetWindowSize` 或者干脆重建纹理。
**不处理的表现是「画面还是原来的大小,右下角露底」**,**而 SDL 不会主动跟着宿主窗口变。**

「失败」加「That operation is not supported」这两行,是本节最好的教材: 它没有说「句柄是空的」,也没有说「参数无效」。

原因刚才讲过:dummy 驱动没这个能力。 但真机上也可能出这句话——比如你把一个「不是窗口」的句柄传进去, 或者那个窗口属于另一个进程。 这两种情况的错误串是一样的,所以只能靠自己排除。

这也解释了为什么 SDL 项目里到处都是 printf(SDL_GetError()): 不是程序员懒,是这个库的错误信息真的不够用。 反过来,这也是好习惯—— 它至少告诉你「哪一步失败了」。

最后那句「嵌入之后不能 SDL_WaitEvent」是本章的收尾提醒: 嵌入不是「装进去就完了」,是「分工要重新划」。 宿主管消息、SDL 管画,这条线划不清,两边都会卡。

30 秒实验

  1. 算一下:宿主窗口从 640×480 拉到 1280×960,RenderCopy 的目标矩形该变成什么?
  2. 判断一下:嵌入之后为什么不能用 SDL_WaitEvent?换成什么?
  3. 判断一下:你的项目是 MFC 的,只需要放一段视频,该引入 SDL 吗?

常见误区

  1. 嵌入后继续用 SDL_WaitEvent。 它会跟宿主消息循环抢消息,两边都卡。 要自己处理宿主消息再转成 SDL_PushEvent。
  2. 不处理宿主窗口缩放。 画面还是原来的大小,右下角露底。 要接 WM_SIZE 去 SDL_SetWindowSize 或者改目标矩形。
  3. 写 if (!ptr) return; 不打印错误。 SDL 的错误信息本来就少,再不打出来就只能靠猜。
  4. 以为嵌入就是「把窗口装进去」。 装进去之后消息循环、尺寸、重绘三件事都要重新分工。
自测三问(点开对答案) 1. 嵌入 SDL 窗口用的函数是什么? —— `SDL_CreateWindowFrom(hwnd)`,传宿主的窗口句柄。Qt 用 `winId()`、MFC 用 `GetSafeHwnd()`。 2. 为什么嵌入后不能用 `SDL_WaitEvent`? —— 因为消息循环归宿主了。`SDL_WaitEvent` 会跟宿主抢消息,两边互相等,界面就死了。 3. 宿主窗口缩放时要做哪三件事? —— 接 `WM_SIZE`、调 `SDL_SetWindowSize`(或重建纹理)、重算 `RenderCopy` 的目标矩形。

第六章到此为止。这一章把「画」这一层讲完了: 对象、扩展库、YUV 上传、编译、嵌入。第七章回到 FFmpeg——这次不只是调用它,而是看清它的框架长什么样。

相关笔记