简介:这是一份面向Visual C++开发者的全屏播放视频示例工程,基于MFC对话框框架实现,主要解决Windows下视频播放与窗口/全屏切换的核心问题,适合有一定C++基础、希望入门多媒体开发的读者。压缩包内共13个文件,包括MFC对话框对应的头文件与源文件、界面资源脚本与图标,以及dsp/dsw工程文件,方便在VC++环境中直接打开查看,整体仅11KB。目前已有250人学习下载,可作为轻量级参考样例研读。通过分析源码,可学习MFC程序框架、对话框消息映射、视频播放组件的集成方式(如DirectShow或Windows Media Player控件),并理解进入全屏时调整窗口尺寸、隐藏边框与标题栏的具体写法;对视频加载、播放控制与全屏切换等常见需求提供了可运行的最小实现。工程结构紧凑、代码量少,便于快速抓住核心要点并迁移到自己的项目中。
1. MFC 对话框里最常被低估的全屏播放门槛
从老工程师手里接过来的 visual c++ 全屏播放视频工程,往往不是打开就能全屏的。这个 zip 里的“全屏播放视频Dlg.cpp / .h”说明它至少是一个基于 MFC 对话框的程序:对话框本身负责 UI,播放能力来自 ActiveX 控件或 DirectShow 渲染区。真正要把视频铺满整个屏幕,至少要把窗口样式、显示器边界、控件重排和 ESC 退出这四件事一起处理干净。很多资料喜欢给一个 put_FullScreenMode(TRUE) 就当解决,但实际产品里那只是两步中的第一步。这篇文章就以这个典型工程为骨架,把对话框骨架、播放通道、全屏切换和发布依赖拆开讲,整理成可以直接落地到代码里的做法。适合正在维护 C++ 桌面播放器、或者接手老 MFC 工程的开发者。
2. 读工程文件:MFC 对话框骨架与播放通道的选择
2.1 从文件清单判断这是哪一类 MFC 工程
打开压缩包先看文件清单,能快速判断工程年代和程序形态。这里最核心的是“全屏播放视频Dlg.h / .cpp”,这是典型的 CDialog 派生类文件,说明程序主界面是对话框,而不是文档视图结构。
| 文件 | 工程中的角色 | 全屏播放改造时的关注点 |
|---|---|---|
| 全屏播放视频Dlg.cpp / .h | CDialog 派生类,负责 UI 与消息映射 | 播放器控件变量、全屏状态、键盘消息 |
| Resource.h | 资源 ID 定义 | 对话框 ID、播放控件 ID、按钮 ID |
| 全屏播放视频.rc / .rc2 | 对话框资源、图标、版本信息 | ActiveX 控件在对话框模板中的 Layout |
| 全屏播放视频.dsp / .dsw | VC6 工程文件与工作区 | VS2010+ 打开时的工程升级 |
| StdAfx.cpp / .h | 预编译头 | MFC 公共头文件集中管理 |
| 全屏播放视频.clw | ClassWizard 状态数据库 | 丢失后可删除重建,不影响编译 |
.dsp 和 .dsw 是 VC6 到 VC2005 时代的工作区格式。用 VS2010 打开时,向导会要求转换成 .sln/.vcxproj;用 VS2022 则需要先装“用于最新 v143 生成工具的 C++ MFC (x86 和 x64)”组件。转换完成后,ClassWizard 功能由“类向导”替代,.clw 文件可以直接删掉。
StdAfx.h 里的内容也暴露了工程底子。常见写法是:
// StdAfx.h #define VC_EXTRALEAN // 剪掉 Win32 中不常被用到的 API #include <afxwin.h> // MFC 核心窗口和控件 #include <afxext.h> // MFC 扩展控件,如 CStatusBar #include <afxcmn.h> // 公共控件,如 CListCtrl #include <afxdisp.h> // OLE Automation,ActiveX 控件需要VC_EXTRALEAN会裁掉一部分极少使用的 Windows 头文件,缩短编译时间,但不要为了省时间把afxdisp.h也剪掉。后面要用 WMP ActiveX 控件,CVariant、COleVariant等类型都依赖这个头文件。
2.2 播放通道选型:WMP ActiveX 控件与 DirectShow
MFC 对话框工程里做视频播放,历史上只有两条主流路线:Windows Media Player ActiveX 控件和 DirectShow。
WMP 控件的 CLSID 固定为{6BF52A52-394A-11D3-B153-00C04F79FAA6}。在 .rc 的对话框模板中直接写 CONTROL 语句,就可以把这个控件嵌进对话框:
CONTROL "{6BF52A52-394A-11D3-B153-00C04F79FAA6}", IDC_VIDEO_PLAYER, "WMPlayer", WS_CHILD | WS_VISIBLE, 10, 10, 640, 400CONTROL 语句的六个参数依次是:控件 CLSID、资源 ID、控件类名、窗口样式、控件在对话框坐标系里的位置。这里的类名必须写WMPlayer,Clsid 和类名同时存在时,Windows 优先按 CLSID 创建控件实例。
DirectShow 则是更底层的技术。直接用IVideoWindow::put_FullScreenMode(TRUE)可以把画面切到全屏,但渲染器在 Windows 7 之后变成了 EVR,消息循环、窗口关联、分辨率兼容性都要自己处理,工程量明显更大。
2.3 对话框工程承载 ActiveX 控件的 OLE 前置条件
无论用哪种控件,MFC 对话框工程要先初始化 OLE 和 COM。很多老工程只在InitInstance里写了CWinApp::InitInstance(),ActiveX 控件创建时会失败,表现为对话框弹不出或者播放区域一片空白。
// 全屏播放视频.cpp BOOL CPlayerApp::InitInstance() { // ActiveX 控件依赖 OLE,必须先于主对话框创建前初始化 if (!AfxOleInit()) { AfxMessageBox(_T("OLE 初始化失败")); return FALSE; } CWinApp::InitInstance(); CPlayerDlg dlg; m_pMainWnd = &dlg; dlg.DoModal(); return FALSE; }AfxOleInit()内部会调用OleInitialize,之后生成的 ActiveX 播放控件才能正常创建窗口和接收事件。如果只调用CoInitialize,部分 MFC 控件包装类会出现断言错误。这个函数调用失败时,优先检查系统是否缺少 OLE 运行权限,以及主进程是否已经初始化过不同线程的 COM。
选型上我一般优先用 WMP ActiveX 控件,因为对话框资源里可以直接放置,UI 控制条不需要自己画,事件模型也简单。DirectShow 保留给需要自绘渲染、低延迟或特殊格式的场景。
3. 改造 Dlg:把 WMP 播放控件挂进对话框并初始化
3.1 资源放置与 DDX_Control 绑定
拿到这个示例工程后,最直接有效的改造方式是在对话框资源上放置一个 Windows Media Player 控件。在 VS 的资源编辑器里打开“全屏播放视频Dlg”,右键对话框选择“插入 ActiveX 控件”,从注册表里选 Windows Media Player,系统会自动生成一个新的控件 ID,通常叫IDC_OCX1,在 Resource.h 里对应一个整数。把它重命名为IDC_VIDEO_PLAYER,方便后面维护。
在类向导中为这个控件添加变量,类型选择CWMPPlayer4,生成后DoDataExchange里会多出这一行:
void CPlayerDlg::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); // 把对话框模板里的 ActiveX 控件与成员变量绑定 DDX_Control(pDX, IDC_VIDEO_PLAYER, m_wndPlayer); }DDX_Control不是普通控件赋值,它会内部调用CWnd::Attach把 ActiveX 控件的窗口句柄关联到m_wndPlayer上。这里必须使用类向导生成的CWMPPlayer4类型,而不是裸CWnd,否则后续访问put_URL、GetControls会很痛苦。
3.2 OnInitDialog 中的播放参数初始化
播放控件挂上之后,OnInitDialog里就可以对播放器行为做初始化。以下是一组适合全屏场景的参数:
BOOL CPlayerDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 隐藏右键菜单 m_wndPlayer.put_enableContextMenu(FALSE); // 隐藏控制条,全屏下保留系统自带控制条会很突兀 m_wndPlayer.put_uiMode(_T("none")); // 不拉伸画面,保持视频原始纵横比 m_wndPlayer.put_stretchToFit(FALSE); CString szFile = _T("D:\\media\\demo.wmv"); // 做一次存在性检查,避免播放控件进入错误状态 if (::GetFileAttributes(szFile) == INVALID_FILE_ATTRIBUTES) { AfxMessageBox(_T("测试文件不存在")); return FALSE; } m_wndPlayer.put_URL(szFile); return TRUE; }put_uiMode(_T("none"))是关键参数,它告诉控件不显示播放、暂停、进度条等系统 UI。全屏播放产品中通常要自己处理这些操作;如果只是想快速验证效果,uiMode可以保留为"full"。put_stretchToFit(FALSE)会让视频保留黑边,不会把宽高比拉变形,代价是播放控件内部会有画面留白,后续要自己调整控件尺寸来消除黑边。
3.3 播放控制按钮与消息映射
对话框里加两个“播放”“全屏”按钮后,消息映射如下:
BEGIN_MESSAGE_MAP(CPlayerDlg, CDialogEx) ON_BN_CLICKED(IDC_BTN_PLAY, &CPlayerDlg::OnBtnPlay) ON_BN_CLICKED(IDC_BTN_STOP, &CPlayerDlg::OnBtnStop) ON_BN_CLICKED(IDC_BTN_FULLSCREEN, &CPlayerDlg::OnBtnFullscreen) END_MESSAGE_MAP()播放与停止的响应函数里,直接操作 WMP 控件的高级接口:
void CPlayerDlg::OnBtnPlay() { // GetControls 返回 IWMPControls 的 MFC 包装对象 if (m_wndPlayer.GetControls().GetIsAvailable(_T("play"))) { m_wndPlayer.GetControls().play(); } } void CPlayerDlg::OnBtnStop() { if (m_wndPlayer.GetControls().GetIsAvailable(_T("stop"))) { m_wndPlayer.GetControls().stop(); } }GetIsAvailable用来探测当前媒体文件中“play/stop”操作是否可用。部分音频流或未完全加载的 URL 上,play可能无效,调用前判断可以避免异常。这个动作放在按钮回调里,逻辑简单,不需要事件接收器。
4. 全屏切换:窗口样式、坐标和输入焦点
4.1 为什么不能只放大窗口
把视频放满屏幕,不等于把窗口的MoveWindow调到屏幕大小。标题栏、系统菜单、可调整边框这些非客户区如果还在,视频画面就不会真正铺满屏幕。MFC 对话框的默认样式通常是WS_POPUP | WS_CAPTION | WS_SYSMENU,全屏时要临时移除这些样式,恢复时再原样还原。
| 窗口样式 | 窗口模式 | 全屏模式 | 说明 |
|---|---|---|---|
| WS_CAPTION | 有 | 无 | 去掉标题栏 |
| WS_THICKFRAME | 有 | 无 | 防止全屏下被拖动改变大小 |
| WS_DLGFRAME | 有 | 无 | 去掉对话框边框残留 |
| WS_CLIPCHILDREN | 有 | 有 | 防止子控件刷新造成花屏 |
| WS_EX_TOPMOST | 视情况 | 有 | 全屏时尽量保持最顶 |
4.2 保存原始状态并切换
在全屏切换前,先把正常窗口位置、样式和扩展样式保存下来。恢复的时候才能完整回到原来的形态,而不是简单恢复成“有标题栏的矩形”。
void CPlayerDlg::OnBtnFullscreen() { if (!m_bFullScreen) { // 1. 保存当前窗口矩形与样式 GetWindowRect(&m_rcNormal); m_dwOldStyle = (DWORD)GetWindowLongPtr(m_hWnd, GWL_STYLE); m_dwOldExStyle = (DWORD)GetWindowLongPtr(m_hWnd, GWL_EXSTYLE); // 2. 获取当前窗口所在显示器的完整屏幕区域 HMONITOR hMon = MonitorFromWindow(m_hWnd, MONITOR_DEFAULTTONEAREST); MONITORINFO mi = { sizeof(mi) }; GetMonitorInfo(hMon, &mi); m_rcMonitor = mi.rcMonitor; // 3. 移除非客户区样式,加上 TOPMOST SetWindowLongPtr(m_hWnd, GWL_STYLE, (m_dwOldStyle | WS_POPUP) & ~(WS_CAPTION | WS_THICKFRAME | WS_DLGFRAME)); SetWindowLongPtr(m_hWnd, GWL_EXSTYLE, m_dwOldExStyle | WS_EX_TOPMOST); // 4. SWP_FRAMECHANGED 让新样式立即生效 SetWindowPos(m_hWnd, HWND_TOPMOST, m_rcMonitor.left, m_rcMonitor.top, m_rcMonitor.Width(), m_rcMonitor.Height(), SWP_FRAMECHANGED | SWP_SHOWWINDOW); // 5. 播放控件铺满客户区 CRect rcClient; GetClientRect(&rcClient); m_wndPlayer.SetWindowPos(nullptr, 0, 0, rcClient.Width(), rcClient.Height(), SWP_NOZORDER); } else { // 恢复窗口样式和原始尺寸 SetWindowLongPtr(m_hWnd, GWL_STYLE, m_dwOldStyle); SetWindowLongPtr(m_hWnd, GWL_EXSTYLE, m_dwOldExStyle); SetWindowPos(m_hWnd, nullptr, m_rcNormal.left, m_rcNormal.top, m_rcNormal.Width(), m_rcNormal.Height(), SWP_FRAMECHANGED | SWP_ZORDER); } m_bFullScreen = !m_bFullScreen; }GetMonitorInfo返回的rcMonitor是当前显示器完整区域,覆盖任务栏;rcWork则是去除任务栏后的工作区。视频全屏播放通常要盖住任务栏,所以这里用rcMonitor。用GetSystemMetrics(SM_CXSCREEN/SM_CYSCREEN)只能拿到主显示器尺寸,在副屏播放时会偏。
提示:修改
GWL_STYLE之后必须再调用一次SetWindowPos,并且带上SWP_FRAMECHANGED,否则 Windows 不会立刻根据新样式重建非客户区,切全屏后标题栏可能还在。
4.3 Esc 退出全屏的可靠处理
对话框默认按下 Esc 会触发IDCANCEL,直接退出整个对话框。全屏状态下,Esc 应该只退出全屏模式,而不是关闭程序。正确做法是在PreTranslateMessage里拦截键盘消息:
BOOL CPlayerDlg::PreTranslateMessage(MSG* pMsg) { if (pMsg->message == WM_KEYDOWN) { UINT nChar = (UINT)pMsg->wParam; // 全屏时 Esc 只退出全屏,不关闭窗口 if (nChar == VK_ESCAPE && m_bFullScreen) { OnBtnFullscreen(); return TRUE; } // Alt+Enter 也可以做全屏切换 if (nChar == VK_RETURN && (GetKeyState(VK_MENU) & 0x8000)) { OnBtnFullscreen(); return TRUE; } } return CDialogEx::PreTranslateMessage(pMsg); }PreTranslateMessage会在消息分发给窗口过程之前执行,所以即使焦点在 WMP ActiveX 控件上,键盘消息也能被拦截。这里不要调用CDialogEx::PreTranslateMessage,否则 Enter 会被当作默认按钮处理。
另一种常踩的坑是鼠标双击切换全屏:WMP 控件铺满客户区后会先于父对话框收到鼠标消息,所以双击很难传到OnLButtonDblClk。产品中我通常不做双击切换,用按钮和 Alt+Enter 更靠谱。
5. 发布期收尾技巧:运行库、控件释放与防闪烁
5.1 Redistributable 和“VC++ 14.0 报错”的辨析
在开发机上程序能全屏播放,搬到别的机器后最常见的失败就是“缺少 mfc140u.dll”或“找不到 MSVCP140.dll”。这类 MFC 工程在动态链接模式下,需要目标机器安装对应位数的 Microsoft Visual C++ Redistributable。如果是 VS2010 之前的工程,运行库版本可能是 9.0 或 10.0,网上常有人把所有版本的 Visual C++ Redistributable All-in-One 打包一起装,那是最快的排查手段,但正规产品发布时应该在安装工程里带上对应合并模块。
另一个经常乱入的提示是error: Microsoft Visual C++ 14.0 or greater is required。它跟这个全屏播放工程没有直接关系,通常是 Python pip 安装包时缺少 VC++ Build Tools 的报错。看到它时,先去确认当前是在编译 MFC,还是在装 Python 扩展,不要在播放器项目里盲目装整套 Build Tools。
5.2 ActiveX 控件释放顺序
对话框销毁时,WMP 控件成员变量一般会随父窗口自动销毁,但全屏模式下直接关闭程序,有时会出现 WMP 进程残留。稳妥做法是在OnDestroy里先退出全屏,再清空 URL,最后销毁控件窗口:
void CPlayerDlg::OnDestroy() { // 关闭前先退出全屏,恢复窗口样式 if (m_bFullScreen) { OnBtnFullscreen(); } // 清空播放源,释放设备资源 if (::IsWindow(m_wndPlayer.GetSafeHwnd())) { m_wndPlayer.put_URL(_T("")); } CDialogEx::OnDestroy(); }put_URL(_T(""))会停止当前播放并释放 WMP 内部的解码器引用。有时候卡在播放状态直接DestroyWindow,在部分显卡驱动上会出现黑屏闪烁。
5.3 全屏切换防闪烁
全屏切换过程中的闪烁,主要来自窗口样式变化后触发的多次重绘。一个低成本技巧是在切换前后用SetRedraw暂时关闭窗口绘制:
SetRedraw(FALSE); // 修改样式和位置,移动控件 OnBtnFullscreen(); SetRedraw(TRUE); // 强制重绘整个客户区 Invalidate(TRUE); UpdateWindow();注意SetRedraw(FALSE)期间不要让用户事件穿插执行,否则按钮点击状态会丢失。切换完成后的UpdateWindow替代Invalidate,可以让播放控件的画面更早恢复。配合WS_CLIPCHILDREN样式,基本能压住大部分老 MFC 工程的全屏闪烁问题。
本文还有配套的精品资源,点击获取