从原理到实现:如何在 Windows 上“嵌入外部程序窗口”到 QtQML 应用中
目标
- 从原理到实现,完整讲解:如何在 Windows 上“嵌入外部程序窗口”到我们的 Qt/QML 应用中。
- 解释我们当前实现的关键组件:
WindowEmbedder(C++,启动/查找/封装外部窗口)与测试页ImprovedWindowEmbedTestPage.qml(QML,触发和承载)。 - 深入分析 QML 如何混合
QWidget承载一个外部程序窗口。
一、外部窗口嵌入的核心原理(Windows)
本质是把“另一个进程的窗口”作为我们应用的一个“子窗口”来显示。关键步骤:
- 启动外部进程(记事本/画图等),等待主窗口创建
- 找到该窗口的原生句柄 HWND(通过类名/标题或更稳健的策略)
- 把这个 HWND 封装为 Qt 的
QWindow对象:QWindow::fromWinId(wid) - 再把
QWindow封装到一个QWidget容器里:QWidget::createWindowContainer(window) - 把这个
QWidget交给 QML 层的承载器组件去摆放与同步几何
我们当前实现的关键路径如下:
// 31:52:TopMatStudio/src/component/WindowEmbedder.cpp
void WindowEmbedder::embedWindow(const QString &processName,
const QString &windowClass,
const QString &windowTitle)
{
// 先清理当前窗口
cleanupCurrentWindow();
setStatusMessage(QString("正在启动: %1").arg(processName));
m_currentProcess = new QProcess(this);
m_currentProcess->start(processName);
if (!m_currentProcess->waitForStarted(3000)) {
setStatusMessage(QString("启动失败: %1").arg(processName));
emit errorOccurred(QString("Failed to start process: %1").arg(processName));
m_currentProcess->deleteLater();
m_currentProcess = nullptr;
return;
}
// 等待窗口创建 - 使用更短的延迟
m_currentProcess->waitForFinished(400);
- 先启动外部进程并等待启动成功,然后短暂延迟等待其窗口出现。
- 更稳健的做法是使用 WaitForInputIdle 或基于 PID 的窗口枚举(文末“改进建议”展开)。
// 54:77:TopMatStudio/src/component/WindowEmbedder.cpp
#ifdef Q_OS_WIN
// 尝试多次查找窗口,提高成功率
WId wid = 0;
int retryCount = 0;
const int maxRetries = 10;
while (retryCount < maxRetries && wid == 0) {
// 尝试使用不同的窗口标题组合
if (!windowTitle.isEmpty()) {
wid = (WId)FindWindowA(windowClass.isEmpty() ? nullptr : windowClass.toLocal8Bit().constData(),
windowTitle.toLocal8Bit().constData());
}
// 如果找不到,尝试只用类名
if (wid == 0 && !windowClass.isEmpty()) {
wid = (WId)FindWindowA(windowClass.toLocal8Bit().constData(), nullptr);
}
// 如果还是找不到,等待一下再试
if (wid == 0) {
QThread::msleep(200);
retryCount++;
}
}
- 我们实现了“多次重试”的查找策略,避免单次查找失败。
- 当前策略依赖窗口类名/标题;对于多语言标题或 UWP 外壳窗口不稳定时可改进为“按进程 PID 枚举窗口”(见文末)。
89:103:TopMatStudio/src/component/WindowEmbedder.cpp
// 创建Qt窗口容器
QWindow *window = QWindow::fromWinId(wid);
if (!window) { ... }
// 创建嵌入widget
m_embeddedWidget = QWidget::createWindowContainer(window);
if (!m_embeddedWidget) { ... }
fromWinId将 HWND 包装为QWindowcreateWindowContainer将QWindow放进一个QWidget,这样我们有了一个常规的 QWidget,可以交给 QML 的承载器去显示和布局
生命周期、安全与清理也覆盖到:
// 165:180:TopMatStudio/src/component/WindowEmbedder.cpp
void WindowEmbedder::cleanupCurrentWindow()
{
if (m_currentProcess) {
m_currentProcess->kill();
m_currentProcess->waitForFinished(3000);
m_currentProcess->deleteLater();
m_currentProcess = nullptr;
}
if (m_embeddedWidget) {
m_embeddedWidget->deleteLater();
m_embeddedWidget = nullptr;
}
m_currentWindowTitle.clear();
}
二、类型注册与 QML 接口
在程序入口把 C++ 类型注册到 QML,供测试页直接使用:
// 68:75:TopMatStudio/src/main.cpp
// 注册 MyWidget 类型到 QML
qmlRegisterType<MyWidget>("MyWidgets", 1, 0, "MyWidget");
// 注册 WindowEmbedder 类型到 QML
qmlRegisterType<WindowEmbedder>("WindowEmbedder", 1, 0, "WindowEmbedder");
WindowEmbedder 的 QML API 面向信号/属性设计:
- 方法:
embedWindow(processName, windowClass, windowTitle)、clearEmbeddedWindow()、getEmbeddedWidget() - 属性:
statusMessage、hasEmbeddedWindow - 信号:
windowEmbedded(windowTitle)、windowCleared()、errorOccurred(error)
三、QML 测试页如何组织(触发 + 承载)
测试页 ImprovedWindowEmbedTestPage.qml 负责 UI 与用户交互,调用 WindowEmbedder 完成嵌入,并把返回的 QWidget 交给一个“QML 承载器”显示。
- 在 QML 中实例化
WindowEmbedder并监听其信号:
//47:63:TopMatStudio/res/qml/editions/shared/page/ImprovedWindowEmbedTestPage.qml
// 窗口嵌入器实例
WindowEmbedder {
id: windowEmbedder
onWindowEmbedded: {
console.log("窗口已成功嵌入:", windowTitle)
}
onWindowCleared: {
console.log("窗口已清空")
currentProgramIndex = -1
}
onErrorOccurred: {
showError(error)
}
}
- 通过按钮触发嵌入(预设程序或自定义路径):
// 150:171:TopMatStudio/res/qml/editions/shared/page/ImprovedWindowEmbedTestPage.qml
Button {
text: modelData.name
...
onClicked: {
if (currentProgramIndex === index) {
windowEmbedder.clearEmbeddedWindow()
currentProgramIndex = -1
} else {
windowEmbedder.embedWindow(
modelData.processName,
modelData.windowClass,
modelData.windowTitle
)
currentProgramIndex = index
}
}
}
- 关键:承载返回的
QWidget
我们在 QML 中用一个“承载器”ExternalWindowContainer(自定义 QQuickItem)来摆放QWidget,响应窗口大小与 DPI 缩放变化。这一段把WindowEmbedder.getEmbeddedWidget()返回的QWidget*赋值给承载器:
// 272:293:TopMatStudio/res/qml/editions/shared/page/ImprovedWindowEmbedTestPage.qml
// 嵌入的窗口容器
ExternalWindowContainer {
id: embeddedWidget
anchors.fill: parent
anchors.margins: 5
visible: windowEmbedder.hasEmbeddedWindow
// 当窗口嵌入器有新的widget时,设置它
Connections {
target: windowEmbedder
function onWindowEmbedded(windowTitle) {
var w = windowEmbedder.getEmbeddedWidget()
if (w) {
embeddedWidget.widget = w
}
}
function onWindowCleared() {
embeddedWidget.clearWidget()
}
}
}
四、QML 混合 QWidget 的关键机制
Qt 提供两种“跨栈”互嵌的桥接方式:
- 把
QQuickWindow放进QWidget:QQuickWidget - 把
QWindow放进QWidget:QWidget::createWindowContainer
我们这里是第二种:目标是一个“外部窗口的 HWND → QWindow → QWidget”,所以使用了 createWindowContainer。
为了把这个 QWidget 出现在 QML 场景里,我们写了一个承载器 ExternalWindowContainer(QQuickItem 的子类,暴露 QWidget* widget 属性)。其核心职责:
- 在 QML 场景几何变化时,更新
QWidget的几何(考虑devicePixelRatio) - 确保
QWidget的原生窗口句柄和QQuickWindow的父子关系正确,避免漂移/遮挡 - 生命周期:清理/解绑,避免悬空句柄与崩溃
核心几何同步逻辑示意(实际实现位于 ExternalWindowContainer.cpp):
- 通过
mapToScene+window()->contentItem()->mapFromScene获取 QML 项目在窗口坐标中的位置 - 乘以
effectiveDevicePixelRatio()计算实际像素区域 - 调用
QWidget::setGeometry(x, y, w, h)与QWindow::setGeometry保持一致
与 WindowEmbedder 的分工关系:
WindowEmbedder负责“把外部程序窗口转成一个QWidget”ExternalWindowContainer负责“把这个QWidget放进 QML 场景且跟随布局变化”
这种分层让“进程控制/句柄封装”和“场景布局/绘制”解耦,便于测试与维护。
五、常见难点与改进建议
- 窗口查找的健壮性
- 目前通过
FindWindowA(class, title)多次重试;若目标应用是多语言标题或 UWP 外壳窗口,容易失败 - 更稳健方案:
- 使用
QProcess::processId()获取 PID,EnumWindows遍历顶层窗口配合GetWindowThreadProcessId按 PID 匹配 WaitForInputIdle(processHandle, timeout)等待目标进程就绪,用轮询退避查找
- 使用
- 目前通过
- 父子关系与样式
- 某些场景可能需要 Win32 原生
SetParent(hwndChild, hwndParent)、SetWindowLongPtr设置WS_CHILD、WS_CLIPSIBLINGS等样式,再交给 Qt 包装,提升裁剪与输入焦点一致性
- 某些场景可能需要 Win32 原生
- 几何与 DPI
- 我们在承载器中按
effectiveDevicePixelRatio()缩放,减少高分屏错位;复杂布局建议关注窗口缩放/移动时的抖动
- 我们在承载器中按
- 焦点/输入法
- 外部窗口有自己的输入循环,嵌入后焦点切换与快捷键需实测校正(必要时在承载器内转发部分事件)
- 进程生命周期
- 当前
clearEmbeddedWindow()会直接杀进程;如果希望“仅解绑显示,不杀进程”,可再加接口参数或新增“解绑”方法
- 当前
- 安全与隔离
- 嵌入任意第三方进程有稳定性/安全隐患(崩溃/卡顿/权限差异),建议限制白名单或增加提示
六、如何在你的项目中使用
- 打开 Playground → “窗口嵌入测试”
- 选择预置程序或输入自定义可执行文件路径(类名/标题可留空)
- 点击嵌入后,会显示在右侧“承载区域”
- 支持刷新几何与清空嵌入
七、结语
综上,我们采用“外部 HWND → QWindow → QWidget → QML 承载器”的链路,将外部程序稳定地嵌入到 QML 场景中。WindowEmbedder 专注进程与窗口句柄获取,ExternalWindowContainer 负责 QML 场景内的几何与显示管理,二者配合使得“QML 界面”与“外部原生窗口”顺畅融合。
- 如需把查找逻辑进一步稳定(按 PID/等待就绪/兼容 UWP),可在
WindowEmbedder中增补对应实现。 - 如需更强的父子裁剪/输入一致性,可在 Win32 层增加
SetParent/样式的控制逻辑,再交给 Qt 封装。
这套机制既适用于简单工具(记事本/画图),也能扩展到更复杂的工程工具,只需针对目标窗口的产生方式与句柄特征调整查找策略即可。