Qt 版的 set_hwnd 与 MFC 版一模一样,只是句柄从 winId() 来。
真正的差别在「跨线程回界面」:Qt 用信号槽排队,不能直接碰控件。
前两节是 Windows 原生路线,这一节换跨平台的 Qt。 这一节也是本章的收尾。三种宿主(MFC、Duilib、Qt)里,Qt 的写法最接近「现代客户端」, 也最能说明「跨线程」这件事。 Qt 自己的多媒体模块对比在第四章,那时会看到「不用 LibVLC 也能播」。
一、winId 与 hwnd 是同一件事
Qt 的每个 QWidget 在 Windows 上都有一个原生窗口句柄,用 winId() 取。
所以 set_hwnd 那一句与 MFC 版完全一样。
| 对比项 | MFC | Qt |
|---|---|---|
| 取句柄 | GetSafeHwnd() |
widget->winId() |
| 交给谁 | set_hwnd(mp, hwnd) |
set_hwnd(mp, (void*)winId) |
| 消息泵 | CWnd 的消息循环 |
QApplication 的事件循环 |
| 跨线程回界面 | PostMessage |
信号槽排队投递 |
winId() 是 WId 类型,在 Windows 上就是 HWND,但在别的平台上是别的东西(X11 的 Window、
macOS 的 NSView 指针)。
所以这里要显式转成 void*——LibVLC 的接口签名就是这么定的:它只认一个「平台相关的句柄」。
二、嵌入的代码
注意 winId() 会在第一次调用时「真的创建原生窗口」,
所以它必须在窗口已经 show() 之后调,否则可能拿到 0。
void Player::play(const QString& path)
{
m_inst = libvlc_new(0, NULL);
m_mp = libvlc_media_player_new(m_inst);
/* 关键:把 QWidget 的原生句柄交给 VLC */
libvlc_media_player_set_hwnd(m_mp,
(void*)ui->videoWidget->winId());
libvlc_media_t* m =
libvlc_media_new_path(m_inst, path.toUtf8().constData());
libvlc_media_player_set_media(m_mp, m);
libvlc_media_player_play(m_mp);
libvlc_media_release(m);
}
path.toUtf8().constData() 这一句容易出问题:toUtf8() 返回一个临时对象,
constData() 拿到的是它的内部指针,如果写成两行、临时对象先析构,指针就悬空了。
一行写完才安全。
这一点在 Qt 里是通用规则:临时对象的 constData() 不能存下来。
三、跨线程:Qt 的强项
LibVLC 的事件回调在它自己的线程里跑,而 Qt 的控件只能在界面线程里碰。 所以必须跨线程传递。
Qt 的解法是信号槽的「排队连接」:信号发出时把参数拷贝一份、放进事件队列, 等界面线程回到事件循环时再执行槽函数。
这带来一个重要结论:跨线程的信号参数必须「可拷贝」。 传指针不行(对面可能已经释放)、传引用不行(跨线程无意义), 只能传值或 Qt 的隐式共享类(QString、QImage 这些)。
demo 里算过延迟:60 Hz 下一轮事件循环是 16.7 毫秒, 所以一个投递过去的通知最快也要等这么久才被执行。 如果每帧都投递一次、而槽函数里又做了重活,队列会越积越长。
四、只传「有新数据了」
这是 Qt 播放器里最重要的一条工程经验:
错误做法:解码线程每解出一帧,就发一个信号把整帧图像传过去。 问题:界面处理不过来,队列积压,延迟越来越大。
正确做法:解码线程把最新一帧放进一个「只保留最新」的变量, 发一个「有新帧了」的信号(不带数据),界面收到后自己去取最新那帧。 这样队列永远不积压——旧帧直接被覆盖。
这个模式有个名字:单槽缓冲(latest-only buffer)。 它在「数据产生快、消费慢」的所有场景里都适用:摄像头预览、传感器读数、实时曲线。
这条规则与 LibVLC 的事件回调、MFC 的 PostMessage 完全一致: 跨线程只传「有新数据了」,不传数据本身。 三种框架,同一句话。
五、要不要用 Qt Multimedia
Qt 自带 QMediaPlayer,那为什么还要嵌 LibVLC?
| 对比项 | QMediaPlayer | LibVLC |
|---|---|---|
| 格式支持 | 依赖系统后端 | 自带几百种 |
| 部署体积 | 几乎为零 | 两三百 MB |
| 低延迟流 | 较弱 | 强 |
| 自定义渲染 | 受限 | 能拿帧自己画 |
| 跨平台一致性 | 各平台后端不同 | 一套行为 |
判断标准很简单:放普通 MP4/MP3,用 QMediaPlayer——体积小、跟系统集成好; 要放监控流、要低延迟、要格式全覆盖,用 LibVLC。
第四章会专门讲 QMediaPlayer,那时候你会看到它有多简单(三行代码就能播), 也会看到它的边界在哪儿。
六、动手 Demo:Qt 的 winId 与 MFC 的 hwnd 是同一件事
这段把两种框架的五项差异列出来,再算「排队投递」的延迟。 最后那张表的「最多等几轮」全是 1——这就是排队的下限。
TH([u"对比项", u"MFC", u"Qt"], [0, 0, 0])
T([u"句柄从哪来", u"`GetSafeHwnd()`", u"`widget->winId()`"], [0, 0, 0])
T([u"交给谁", u"`set_hwnd(hwnd)`", u"`set_hwnd((void*)winId)`"],
[0, 0, 0])
T([u"跨线程怎么回界面", u"`PostMessage`", u"信号槽排队投递"],
[0, 0, 0])
实跑输出:
--------------------------------------------------------------------
Qt 的 winId 与 MFC 的 hwnd 是同一件事
--------------------------------------------------------------------
**两个框架看起来完全不同,交出去的东西却一模一样:一个窗口句柄。**
**差别在「谁负责消息循环」和「跨线程怎么传」。**
对比项 MFC Qt
-----------------------------------------------------------------------
句柄从哪来 `GetSafeHwnd()` `widget->winId()`
交给谁 `set_hwnd(hwnd)` `set_hwnd((void*)winId)`
消息谁泵 `CWnd` 的消息泵 `QApplication` 的事件循环
跨线程怎么回界面 `PostMessage` 信号槽排队投递
重绘触发 `Invalidate` `update()`
**「跨线程回界面」这一行是两者最像的地方**:
**都不能直接碰控件,都得「投递一个消息,让主线程去执行」。**
**排队投递的延迟可以算**:事件循环每轮处理完所有待办,所以一个投递的事件最快在「下一轮」执行。
屏幕刷新 每帧毫秒 最多等几轮
----------------------------------------
30 Hz 33.3 1
60 Hz 16.7 1
144 Hz 6.9 1
**60 Hz 下一轮是 16.7 毫秒**,听起来很短。
**但每帧都投递一次、而槽函数里又做了重活,队列就会越积越长。**
表现是「画面延迟越来越大,最后像卡住」。
**所以 Qt 版的播放器有一个固定套路**:
**解码线程只负责「把帧放进一个只保留最新一帧的变量」,再发一个信号通知界面来取。**
**这样队列永远不会积压——旧帧直接被丢弃。**
这条规则与 VLC 的事件回调、MFC 的 PostMessage 完全一致:
**跨线程只传「有新数据了」,不传数据本身。**
「句柄从哪来」这一行是形式差异,「跨线程怎么回界面」是本质相同。 两者都是「投递一个消息,让主线程去执行」。
延迟那张表里,30/60/144 Hz 的「最多等几轮」都是 1,意思是「最快下一轮就执行」。 但这是「最快」,不是「平均」—— 如果队列里已经有十个待办,就要等十轮。
所以真正的瓶颈不是「一轮多长」,是「队列有多长」。 这就是为什么「只传有新数据了」这个模式这么重要:它让队列长度恒为 1。
30 秒实验
- 想一想:
winId()为什么必须在show()之后调用? - 如果把一帧 1080p 的图像(约 3 MB)每帧都通过信号传一次,60 fps 下带宽是多少?
- 判断一下:你的项目该用 QMediaPlayer 还是 LibVLC?
常见误区
- 在构造函数里调
winId()。 那时原生窗口可能还没创建,拿到的可能是 0。要等 show 之后。 - 把
toUtf8().constData()存下来。 临时对象析构后指针悬空。 - 跨线程信号传整帧图像。 队列会积压,延迟越来越大。应该传「有新帧了」。
自测三问(点开对答案)
1. Qt 里 `set_hwnd` 的第二个参数怎么写? —— `(void*)ui->videoWidget->winId()`——把 WId 转成 void 指针。 2. 跨线程的信号参数为什么必须可拷贝? —— 因为排队连接会把参数拷贝一份放进事件队列,等界面线程有空再执行。 3. 什么情况下该用 QMediaPlayer 而不是 LibVLC? —— 只放普通格式、不想带几百 MB 依赖、要跟系统媒体栈集成时。第二章到此为止。 VLC 这条路的特点是「全包了」——从解封装到显示都不用你管。 第三章开始换一条路:Qt 自己的多媒体栈,代码少得多,边界也明显得多。
- ☐ (本章完,下一节 03.01 Qt信号槽机制及应用)