set_hwnd 交出去的只是一个窗口句柄,画面就跑到你的窗口里了。
交出去之后,窗口的消息归你管、画面的内容归它管——这条分工必须分清。
上一节的控制台版开的是 VLC 自己的窗口,这一节把画面搬进 MFC 对话框。 这是「嵌入」的全部秘密:一个句柄。 后面的 Duilib 与 Qt 用的是同一招,只是句柄的来源不同。
一、交出去的到底是什么
libvlc_media_player_set_hwnd(mp, hwnd) 只有一个参数有用:窗口句柄。
VLC 拿到它之后,会在那个窗口的客户区里画画。
所以它不是「把窗口交给 VLC」,是「允许 VLC 在这个窗口上画」。 这个区别在出问题时很重要:窗口还是你的、消息还是你收、VLC 只是个「会往上面贴图的客人」。
由此推出三条规则:
窗口的销毁由你负责,销毁前必须 set_hwnd(mp, NULL) 或先释放播放器。
窗口的尺寸变化你要告诉它(其实不用,它自己会看客户区大小)。
不要在同一个窗口上再放别的控件——它每帧都会覆盖整个客户区。
第三条是最容易踩的:很多人想在画面上叠一个「暂停」按钮, 结果按钮一闪就没了——因为 VLC 每帧都在重绘整个客户区。 正确的做法是「另开一个透明子窗口放控件」,或者用 VLC 自己的 marquee 滤镜。
二、消息循环归谁
MFC 程序有一个消息泵,VLC 内部也有自己的线程。 这两者不冲突,因为分工不同:
| 谁 | 负责什么 | 在哪条线程 |
|---|---|---|
| MFC 消息泵 | 窗口消息、按钮、菜单 | 主线程(界面线程) |
| VLC 解码线程 | 读包、解码 | VLC 自己创建 |
| VLC 输出线程 | 往你的窗口贴图 | VLC 自己创建 |
| 事件回调 | 通知你的代码 | VLC 的线程 |
看第三行:贴图这件事发生在 VLC 的线程里,不是主线程。
这意味着它绕过了 MFC 的 OnPaint——你的对话框不会收到 WM_PAINT,画面是「直接画上去」的。
这也解释了为什么「在 OnPaint 里画东西会被盖掉」:你的绘制与它的绘制没有先后保证, 而且它每帧都重画一遍。要叠东西只能另开窗口。
三、嵌入的代码
在对话框的 OnInitDialog 里做四步:
BOOL CPlayerDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
m_inst = libvlc_new(0, NULL);
m_mp = libvlc_media_player_new(m_inst);
/* 关键一句:把画面交到这个对话框上 */
libvlc_media_player_set_hwnd(m_mp, GetSafeHwnd());
return TRUE;
}
void CPlayerDlg::OnBnClickedPlay()
{
libvlc_media_t* m = libvlc_media_new_path(m_inst, "test.mp4");
libvlc_media_player_set_media(m_mp, m);
libvlc_media_player_play(m_mp);
libvlc_media_release(m); /* 设完就可以放 */
}
注意 GetSafeHwnd() 拿的是对话框本身的句柄,所以画面会盖住整个对话框客户区,
包括你放的按钮。
正确的做法是放一个静态控件(Picture Control)当「画布」,把那个控件的句柄交给 VLC:
GetDlgItem(IDC_VIDEO)->GetSafeHwnd()。这样画面只占控件的位置,其他控件不受影响。
四、尺寸与黑边
把画面交给一个控件之后,一个必然的问题出现了: 视频的宽高比与控件的不一样,多出来的地方怎么办?
VLC 默认是「保持宽高比、留黑边」。所以你会看到上下或左右有黑条——这是正常行为,不是 bug。
demo 里把那笔账算了一遍:1920×1080 的视频放进 800×600 的窗口, 按宽度铺满是 800×450,上下各留 75 像素黑边。
三种处理方式,各有用处:
| 方式 | 效果 | 适合 |
|---|---|---|
| 保持宽高比 | 有黑边、不变形 | 看电影、监控 |
| 拉伸铺满 | 无黑边、会变形 | 基本不推荐 |
| 裁切铺满 | 无黑边、切边缘 | 全屏播放、UI 演示 |
要改这个行为,用 libvlc_video_set_aspect_ratio,
传 NULL 是「按原始比例」,传 "16:9" 是「强制按这个比例拉」。
注意它不是「缩放」,是「怎么填满」的策略。
五、全屏与退出
全屏不是 VLC 的事,是窗口的事。 你要做的是「把窗口改成无边框、铺满屏幕」,VLC 会跟着填满客户区。
三步:
记住原来的窗口样式与位置(GetWindowLong 与 GetWindowRect)。
改成 WS_POPUP 并 SetWindowPos 到屏幕大小。
退出全屏时还原这三样。
最容易漏的是「还原」:很多人做了全屏,按 Esc 之后窗口变成一个小方块、或者标题栏没了。 原因是只记住了位置、没记住样式。
退出时的释放顺序(这一条错了必崩):
先 libvlc_media_player_stop(等它真的停下)。
再 libvlc_media_player_release。
再 libvlc_release。
如果挂了事件回调,要在这之前 detach。
为什么 stop 不能省:不 stop 直接 release,VLC 可能还在往你的窗口贴图,
而窗口已经被销毁了——这就是「退出时偶尔崩」的来源。
六、动手 Demo:把窗口句柄交出去之后,黑边是算出来的
这段拿 1920×1080 的视频算六种窗口尺寸下的画面大小与黑边位置。 看第一列与最后一列:窗口形状决定黑边跑哪边。
vw, vh = 1920.0, 1080.0
for w, hh in ((1600, 600), (1000, 400), (800, 600), (1280, 720)):
ar_v, ar_w = vw / vh, w / float(hh)
if ar_w > ar_v: # 窗口比视频更宽
d_w = hh * ar_v # 按高度铺满,左右留黑边
else:
d_w = w # 按宽度铺满,上下留黑边
实跑输出:
--------------------------------------------------------------------
把窗口句柄交出去之后,黑边是算出来的
--------------------------------------------------------------------
**`libvlc_media_player_set_hwnd` 交出去的只是一个窗口句柄。**
**画面怎么摆进这个窗口,是 VLC 按「保持宽高比」算出来的。**
**算一遍:视频 1920×1080 放进 800×600 的窗口**
视频宽高比 1.778,窗口宽高比 1.333。
窗口比视频「更方」,所以按宽度铺满:800 ÷ 1.778 = 450 高。
上下各留黑边 (600 − 450) ÷ 2 = 75 像素。
窗口尺寸 画面尺寸 黑边 铺满方向
-----------------------------------------------------
1600×600 1067×600 左右各 267 按高度
1000×400 711×400 左右各 144 按高度
800×600 800×450 上下各 75 按宽度
1280×720 1280×720 上下各 0 按宽度
1024×768 1024×576 上下各 96 按宽度
640×480 640×360 上下各 60 按宽度
**这张表解释了一个常见现象**:**同一个视频,窗口拉成不同形状,黑边跑的位置会变。**
窗口比视频更宽 → 左右出现黑边(按高度铺满)。
窗口比视频更方 → 上下出现黑边(按宽度铺满)。
**工程上的两个选择**:
保持宽高比(默认):画面不变形,但有黑边。
拉伸铺满:没有黑边,但人脸会被压扁。
**还有第三个选择,很多播放器都有**:**裁切铺满(zoom)**——把超出窗口的部分切掉。
**它没有黑边也不变形,代价是画面边缘被吃掉。**
1600×600 与 1000×400 这两个窗口都比视频「更宽」,所以按高度铺满、左右留黑边。 而 800×600、1024×768 更「方」,按宽度铺满、上下留黑边。
1280×720 那一行黑边是 0——因为它的宽高比正好等于 16:9。 这就是为什么「窗口按视频比例裁剪」是很多播放器的默认行为:它一劳永逸地消掉了黑边。
工程上的建议: 如果做的是「看片」软件,默认按视频比例设窗口大小;如果做的是「监控」软件,控件尺寸固定、接受黑边——因为九宫格的格子必须一样大。
30 秒实验
- 在对话框里放一个 Picture Control,把它的句柄交给 VLC,看画面是否只占那块区域。
- 在同一个对话框上再放一个按钮,看它会不会被画面盖掉。
- 把
set_hwnd传成对话框本身的句柄,对比两种效果。
常见误区
- 以为
set_hwnd是把窗口交给 VLC 管。 它只是「允许在客户区画」,消息与销毁还是你的责任。 - 在画面上直接放按钮。 VLC 每帧重绘整个客户区,按钮会被盖掉。
- 退出时不 stop 直接 release。 可能还在贴图而窗口已销毁,这就是「退出崩溃」的常见来源。
自测三问(点开对答案)
1. `set_hwnd` 传对话框句柄与传 Picture Control 句柄有什么区别? —— 传对话框句柄,画面会盖住整个客户区(包括其他控件);传控件句柄,画面只占那块区域。 2. 1920×1080 放进 800×600 的窗口,黑边在哪边、多宽? —— 上下各 75 像素。按宽度铺满是 800×450,剩下 150 分上下。 3. 为什么退出前一定要先 stop? —— 否则 VLC 可能还在往已销毁的窗口贴图,导致崩溃。MFC 那套是「有边框窗口」的做法。下一节换成无边框的 Duilib——代价是「拖动窗口」这件小事要自己写。
- ☐ (本节完,下一节 02.03.07 使用Duilib美化基于LibVLC的播放器)