🎬 第二章 VLC播放器及二次开发应用
📄 一句话概括

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 秒实验

  1. 在对话框里放一个 Picture Control,把它的句柄交给 VLC,看画面是否只占那块区域。
  2. 在同一个对话框上再放一个按钮,看它会不会被画面盖掉。
  3. 把 set_hwnd 传成对话框本身的句柄,对比两种效果。

常见误区

  1. 以为 set_hwnd 是把窗口交给 VLC 管。 它只是「允许在客户区画」,消息与销毁还是你的责任。
  2. 在画面上直接放按钮。 VLC 每帧重绘整个客户区,按钮会被盖掉。
  3. 退出时不 stop 直接 release。 可能还在贴图而窗口已销毁,这就是「退出崩溃」的常见来源。
自测三问(点开对答案) 1. `set_hwnd` 传对话框句柄与传 Picture Control 句柄有什么区别? —— 传对话框句柄,画面会盖住整个客户区(包括其他控件);传控件句柄,画面只占那块区域。 2. 1920×1080 放进 800×600 的窗口,黑边在哪边、多宽? —— 上下各 75 像素。按宽度铺满是 800×450,剩下 150 分上下。 3. 为什么退出前一定要先 stop? —— 否则 VLC 可能还在往已销毁的窗口贴图,导致崩溃。

MFC 那套是「有边框窗口」的做法。下一节换成无边框的 Duilib——代价是「拖动窗口」这件小事要自己写。

相关笔记