Qt嵌入UI界面实战:.ui、QML与外部进程窗口整合

发布时间:2026/10/26 12:48:25
Qt嵌入UI界面实战:.ui、QML与外部进程窗口整合 简介面向 C/Qt 初学者以及需要在 Visual Studio 2022 中集成图形界面的开发者这份 Qt 嵌入 UI 界面 Demo 以 Qt 15.2 为环境围绕 Qt Designer 可视化布局、.ui 文件在 C 工程中的加载与控制、QMainWindow 窗口组织以及信号与槽事件处理展开既可作课程实验示例也可作为桌面工具界面的起步模板。压缩包共 87 个文件约 108.06MB包含 10 个 cpp 源文件、8 个 h 头文件、4 个 ui 界面文件以及 vcxproj、sln、qrc、exe 等工程配置与运行文件便于对照阅读源码、检查项目配置、了解 qrc 资源组织与多界面协作并直接启动验证。已有 464 人学习下载。读者可从中理解多窗口界面的组织方式、界面控件与业务代码的衔接方法掌握 VS2022 下 Qt 工程的构建与调试要点并借鉴按钮点击等交互的信号槽写法为后续跨平台图形界面开发打基础。1. 一个窗口里塞进三套界面这个 Qt 嵌入 ui 界面 Demo 到底在演示什么接手一个老工具时最容易碰到的局面是主界面是几年前写死的新页面在另一个 exe 里第三套图表页又是别人用 QML 做的。把整个工程合并成本太高几乎不可行但用户只想要“一个窗口搞定”。这份 Qt 嵌入 ui 界面 Demo 把“嵌入”拆成可运行的样例集合运行时加载 .ui、把一个 QWidget 挂进另一个布局、把外部进程窗口装进容器、把 QML 页面塞进 Widgets 界面。适合正在做老工具集成、多团队页面合并或者想拆掉巨型 MainWindow 的开发者。2. 技术选型Widgets 打底QML 与外部进程当租客“嵌入 UI”这个词在 Qt 圈里指向四种完全不同的东西先分清层级比先看代码重要。我在项目里见过最常见的翻车是把“加载一个 .ui 文件”和“嵌入一个外部程序窗口”当成同一件事结果代码写到一半发现方向根本不对。2.1 四种嵌入方式的边界与代价先做一次判断被嵌的那个东西和宿主在不在同一个进程里是不是 QWidget是不是 Qt 写的这三个问题问完方案基本就唯一了。方式核心 API适合的场景主要代价运行时加载 .uiQUiLoader::load界面要热更新、外发给客户后对方能改布局启动多一次磁盘 IO自定义控件需注册到 loader控件级嵌入layout-addWidget同进程内的页面合并、把大窗口拆小基本无代价也是最稳的一条路外部进程窗口QWidget::createWindowContainerQWindow::fromWinId两套技术栈或两个 exe 要合并成一个界面焦点、缩放、输入法、层级都要单独调QML 页面QQuickWidget新页面要动画、图表、触摸手感多一层渲染上下文和 OpenGL 类控件容易冲突选型的顺序我一般是这样推的如果被嵌的界面本来就是 QWidget直接addWidget不要绕远路如果它是 QML用QQuickWidget而不是QQuickView 容器前者是 QWidget 子类能进布局、能被父窗口裁剪只有当对方是一个独立的 exe、你拿不到源码或不想合并构建时才动createWindowContainer。最后一类最容易被低估。它本质上不是“嵌入控件”而是“把一个别的进程的顶层窗口重新设父窗口”跟跨平台窗口系统的实现强相关。同一个 API 在三个平台上的表现差异能让你在联调时怀疑人生——这块我在第 5 章单独讲。2.2 宿主为什么用 Widgets 而不是纯 QML这个 Demo 的宿主也就是装下所有页面的那一层用的是 Qt Widgets不是纯 QML。理由很实际宿主这一层要做的是菜单栏、工具栏、可拖拽停靠面板、多文档标签页这些在 Widgets 里是现成的在 QML 里要自己搭一遍还得处理键盘焦点链和快捷键冲突。QML 更适合做“视觉重”的页面仪表盘、折线图、带缓动动画的状态切换。所以项目里的分工是——宿主和页面框架用 Widgets具体某几页用 QML两者用QQuickWidget桥接。这种混搭在 Qt 里是官方支持的但有一个前提QQuickWidget走的是离屏渲染和QOpenGLWidget、QVideoWidget这类直接操作原生上下文的东西放同一窗口时容易互相抢资源。真正要做视频页我宁愿用QQuickView加容器也不要硬塞进QQuickWidget。2.3 版本与模块依赖Qt5 和 Qt6 的差异点这套代码我按 Qt6 写同时也保留 Qt5 能编的写法因为实际项目里客户环境什么版本都有血泪经验是——别在 CMake 里写死版本号用find_package(QT NAMES Qt6 Qt5 ...)那种兼容写法能省掉一次返工。几个必须记住的差异QUiLoader属于UiTools模块Qt5 是Qt5::UiToolsQt6 是Qt6::UiTools不在Widgets里CMake 少写一行就链接失败。Qt::AA_EnableHighDpiScaling在 Qt6 已经移除默认开启Qt5 里不设这个属性嵌入外部窗口时容器和内容的尺寸会对不上表现为“嵌进来的窗口只显示左上角一小块”。QQuickWidget在 Qt6 里对图形后端更敏感Windows 上如果程序跑在远程桌面里setGraphicsApi(QSGRendererInterface::Software)往往是唯一的后悔药。3. 目录结构与构建链路从 .ui 文件到可执行程序工程结构直接决定了后面改起来顺不顺。我见过不少 Demo 把所有东西堆在根目录.ui、.qml、.cpp 混在一起第二天自己都找不到哪个 .ui 是编译期的、哪个是运行期加载的。3.1 目录树与文件归位QtEmbedUiDemo/ ├── CMakeLists.txt ├── src/ │ ├── main.cpp │ ├── MainWindow.h / MainWindow.cpp # 宿主左侧导航 右侧 QStackedWidget │ ├── EmbedHost.h / EmbedHost.cpp # 四种嵌入方式的实现都收在这里 │ ├── PageFactory.h # 页面注册表id - 懒加载函数 │ └── ui/ │ └── MainWindow.ui # 编译期 uic 用AUTOUIC ├── resources/ │ ├── resources.qrc │ ├── ui/ │ │ ├── DevicePage.ui # 运行时 QUiLoader 用 │ │ └── SettingPage.ui │ └── qml/ │ ├── ChartPage.qml │ └── components/Gauge.qml ├── thirdparty/ │ └── EmbeddedApp/ # 被嵌入的独立进程示例 │ ├── CMakeLists.txt │ └── main.cpp └── tools/ └── deploy.sh关键在于把src/ui/和resources/ui/分开。前者是编译期由uic转成ui_MainWindow.h的改完要重新编译后者是运行时用QUiLoader读的改完直接替换文件就生效。这两类 .ui 混在一个目录里迟早有人改了“热更新那份”却发现界面没变——因为程序加载的是另一份。thirdparty/EmbeddedApp是一个极简的独立 Qt 程序只干一件事启动后把自己窗口的winId()打印到 stdout。它是被嵌对象单独构建宿主用QProcess拉起来。这样整个 Demo 不依赖任何外部二进制。3.2 CMakeLists 关键行逐条解释cmake_minimum_required(VERSION 3.16) project(QtEmbedUiDemo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # AUTOMOC: 处理 Q_OBJECTAUTOUIC: 编译期把 .ui 转成 ui_xxx.hAUTORCC: 编译 .qrc set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) # 兼容 Qt5 / Qt6先找不带版本的 QT再按结果找具体模块 find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Gui Widgets Quick QuickWidgets) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets Quick QuickWidgets) # UiTools 单独判断有些精简版 SDK 没带这个模块缺了它 QUiLoader 直接编不过 find_package(Qt${QT_VERSION_MAJOR} QUIET COMPONENTS UiTools) add_executable(QtEmbedUiDemo src/main.cpp src/MainWindow.cpp src/MainWindow.h src/EmbedHost.cpp src/EmbedHost.h resources/resources.qrc ) target_include_directories(QtEmbedUiDemo PRIVATE src) target_link_libraries(QtEmbedUiDemo PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Quick Qt${QT_VERSION_MAJOR}::QuickWidgets) if(TARGET Qt${QT_VERSION_MAJOR}::UiTools) target_link_libraries(QtEmbedUiDemo PRIVATE Qt${QT_VERSION_MAJOR}::UiTools) target_compile_definitions(QtEmbedUiDemo PRIVATE HAVE_UITOOLS1) endif()AUTOUIC和QUiLoader是两条互斥的路线这是最容易搞混的一点AUTOUIC在编译期把 .ui 变成 C 头文件界面完全写死在二进制里QUiLoader在运行期解析 XML界面可以被替换。同一个 .ui 文件不要既丢给 AUTOUIC 又被 loader 读否则你改文件时会陷入“到底哪份生效”的玄学。HAVE_UITOOLS这个宏是给EmbedHost.cpp用的没装 UiTools 时整个“运行时加载”页会被条件编译掉程序依然能跑只是少一个功能。这种降级写法在老项目里很常见比直接编译失败友好得多。3.3 资源归位与运行时路径.qrc里我把运行时加载的资源全部登记这样发布时只有一个 exe不用带一堆散文件RCC qresource prefix/embed file aliasui/DevicePage.uiui/DevicePage.ui/file file aliasui/SettingPage.uiui/SettingPage.ui/file file aliasqml/ChartPage.qmlqml/ChartPage.qml/file /qresource /RCCalias决定了程序里访问的路径写成:/embed/ui/DevicePage.ui。注意QQuickWidget::setSource收的是QUrl从 qrc 加载要写成QUrl(qrc:/embed/qml/ChartPage.qml)前缀是qrc:而不是:这两个写混了不会报错只会给你一个空白窗口调试时能耗掉半个下午。如果哪天需要“不重新编译就能换界面”把 qrc 里的 .ui 改成磁盘路径即可QUiLoader对两者一视同仁。我一般会在配置里留一个uiOverrideDir非空时优先从磁盘读方便现场改。4. 核心代码拆解三种嵌入方式的完整链路四种方式在EmbedHost里各有一个入口函数宿主窗口只负责调不关心细节。这种收口在后期换成别的方式时只改一个文件就能解决。4.1 运行时加载 .uiQUiLoader findChild#include QUiLoader #include QFile #include QPushButton #include QCoreApplication // uiPath 支持 :/embed/ui/xxx.uiqrc和 /opt/app/ui/xxx.ui磁盘两种 QWidget *EmbedHost::loadUiFile(const QString uiPath, QWidget *host) { QFile file(uiPath); if (!file.open(QIODevice::ReadOnly)) { qWarning(open ui failed: %s, qPrintable(uiPath)); return nullptr; } QUiLoader loader; // 自定义控件比如自绘仪表要先注册工厂否则会被降级成 QWidget loader.setWorkingDirectory(QFileInfo(uiPath).absolutePath()); // 第二个参数是父对象决定归属关系和析构时机 QWidget *page loader.load(file, host); file.close(); if (!page) { qWarning(load ui failed: %s, qPrintable(loader.errorString())); return nullptr; } // 关键运行时加载拿不到 ui_xxx.h 里的成员指针只能按 objectName 找 if (auto *btn page-findChildQPushButton *(okButton)) { QObject::connect(btn, QPushButton::clicked, host, [this] { emit pageActionTriggered(ok); }); } else { // 找不到控件通常是 .ui 里忘了填 objectName而不是代码错 qWarning(widget okButton not found in %s, qPrintable(uiPath)); } return page; }参数上要留意两点。loader.load()的第二个参数传host之后返回的 widget 会作为host的子对象被管理宿主析构时自动回收不用手动 delete传nullptr就得自己管生命周期。setWorkingDirectory影响的是 .ui 里引用的相对路径资源图标之类不设的话图标全丢而且不报错。findChild用objectName匹配这要求设计师在 Designer 里给每个需要交互的控件填名字。这是运行时加载方案唯一强制约束我在项目里会在 CI 里加一个脚本扫描 .ui 里被代码引用的 objectName 是否都存在缺一个就报错——比等到运行时界面点不动再去查划算得多。4.2 嵌入外部进程窗口QWindow::fromWinId createWindowContainer#include QWindow #include QWidget #include QProcess #include QLayout bool EmbedHost::embedExternalWindow(WId foreignWinId, QWidget *container) { QWindow *foreign QWindow::fromWinId(foreignWinId); if (!foreign) { qWarning(fromWinId failed: %llu, static_castunsigned long long(foreignWinId)); return false; } // 容器接管这个 QWindow 的生命周期foreign 不用手动 delete QWidget *wrapper QWidget::createWindowContainer(foreign, container); // 不设焦点策略嵌进来的子窗口收不到键盘事件表现为能看不能点 wrapper-setFocusPolicy(Qt::StrongFocus); wrapper-setMinimumSize(320, 240); if (QLayout *lay container-layout()) { lay-addWidget(wrapper); } else { // 容器没有布局时只能手动跟随 resize否则嵌进来的窗口不会跟着变大 wrapper-setGeometry(container-rect()); } return true; } // 启动被嵌进程并从 stdout 拿到它的 winId实测比按窗口标题查找可靠得多 WId EmbedHost::launchAndGrabWid(const QString exePath) { m_proc new QProcess(this); m_proc-setProcessChannelMode(QProcess::SeparateChannels); m_proc-start(exePath, {--print-wid}); if (!m_proc-waitForReadyRead(5000)) { qWarning(embedded app did not report wid); return 0; } // 被嵌程序第一行输出就是 winId十进制 return static_castWId(m_proc-readLine().trimmed().toULongLong()); }WId是平台原生窗口句柄Windows 上是HWNDLinux 上是xcb_window_t。这个 Demo 用的取法是我最推荐的一种被嵌程序启动时加个--print-wid参数构造完主窗口后把winId()打到 stdout宿主用QProcess读第一行。相比按窗口标题或进程名去枚举系统窗口这种方式不怕重名、不怕多实例联调时也方便——被嵌程序单独跑一次就知道它会吐出什么。有一个前置条件被嵌程序的窗口必须已经show()过winId()才有值而且必须是顶层窗口。如果对方程序是纯 QML 的QQuickView一样能拿到winId但窗口标志里带Qt::Tool之类的属性时嵌入后可能出现无边框、无法移动的问题。4.3 嵌入 QML 页面QQuickWidget 上下文注入#include QQuickWidget #include QQmlContext #include QQmlEngine #include QLayout void EmbedHost::embedQmlPage(const QUrl qmlUrl, QWidget *container) { auto *quick new QQuickWidget(container); // 根对象跟随容器尺寸不设的话 QML 页面始终是初始大小 quick-setResizeMode(QQuickWidget::SizeRootObjectToView); // 把 C 对象注入 QMLQML 里用 hostBridge.xxx() 调用 quick-rootContext()-setContextProperty(hostBridge, this); // 提前注册类型QML 里才能直接用 qmlRegisterUncreatableTypeEmbedHost(Embed.Host, 1, 0, EmbedHost, C only); quick-setSource(qmlUrl); if (quick-status() QQuickWidget::Error) { for (const QQmlError err : quick-errors()) qWarning(qml error: %s, qPrintable(err.toString())); } if (QLayout *lay container-layout()) lay-addWidget(quick); }setContextProperty注入的对象在 QML 侧是全局可见的方便但也容易污染命名空间项目大了我一般改用qmlRegisterSingletonInstance。SizeRootObjectToView这个模式必须设否则 QML 根元素的高度写死容器拉大之后下面留一大片空白。status()和errors()这两句是排错的关键。QML 加载失败时QQuickWidget不会抛异常只是安静地给你一个透明窗口不看errors()根本不知道哪一行写错了。4.4 页面注册与懒加载把四种方式统一调度#include QStackedWidget #include functional // id - 构造函数的注册表真正构造发生在第一次切换时 void EmbedHost::registerPage(const QString id, std::functionQWidget *(QWidget *) factory) { m_factories.insert(id, std::move(factory)); } void EmbedHost::switchTo(const QString id) { QWidget *page m_cache.value(id, nullptr); if (!page) { auto it m_factories.find(id); if (it m_factories.end()) { qWarning(page %s not registered, qPrintable(id)); return; } page it.value()(m_stack); // 传 m_stack 当父对象交给 Qt 管生命周期 m_cache.insert(id, page); m_stack-addWidget(page); } m_stack-setCurrentWidget(page); }懒加载在这里不是为了炫技是因为嵌入外部窗口那一页如果启动就构造会连带把被嵌进程一起拉起来启动时间凭空多两秒。改成第一次点进去才构造主界面秒开。m_cache保证页面切回来时不重建避免 QML 页面反复初始化导致的闪烁。5. 避坑与排查黑匣子里最容易翻车的五个点嵌入类问题的麻烦之处在于报错信息极少多数时候只是一个空白区域。下面五条都是我在实际联调里踩过的按“现象 → 原因 → 解决”记下来。5.1 现象加载完 .ui 界面是空的没有任何报错原因QUiLoader::load返回了非空指针但 .ui 文件本身是一个没有布局的裸QWidget里面所有子控件都是绝对坐标。父容器 resize 之后控件全被挤到尺寸为 0 的区域外。另一种可能是 rcc 里alias写错实际读到了一个不存在的路径——但QFile::open又成功了因为 qrc 的路径匹配比想象中宽松。解决先在 Designer 里给根 widget 加一层布局选中根对象点“垂直布局”即可。代码侧加一句qDebug() page-children().size();如果子对象数量为 0说明文件路径对了但内容不对如果数量正常那就是布局问题。5.2 现象外部窗口嵌进来之后黑屏、白屏或者只显示左上角一块原因三种可能混在一起。最常见的是被嵌程序还没完成首帧绘制宿主已经把它塞进容器了其次是高 DPI 缩放策略不一致宿主按 150% 缩放、被嵌程序按 100% 渲染容器尺寸对不上第三种是窗口标志不兼容被嵌窗口带Qt::FramelessWindowHint时某些平台上会失去客户区。解决拿到winId后不要立刻createWindowContainer用QTimer::singleShot(300, ...)延后一拍或者在宿主main()里统一设QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough)让两边用同一套取整规则。5.3 现象QML 页面嵌进去之后同窗口里的视频控件或 OpenGL 控件变成黑块原因QQuickWidget用离屏渲染会创建自己的渲染目标QOpenGLWidget、QVideoWidget直接操作原生上下文。两者在同一个顶层窗口里OpenGL 上下文被来回切换谁先谁后取决于创建顺序表现出来就是“有时候好的有时候黑的”非常玄学。解决不要试图靠调整创建顺序解决那是碰运气。要么把 QML 页改成QQuickViewcreateWindowContainer走原生窗口路径要么在初始化时加QQuickWindow::setGraphicsApi(QSGRendererInterface::Software)用软件渲染换稳定。前者性能好后者兼容性强看场景选。5.4 现象加载 .ui 之后样式表不生效按钮还是系统默认的灰原因QUiLoader加载出来的 widget 虽然挂在宿主下面但.qss是在宿主MainWindow上setStyleSheet的。样式表的作用域按对象树向下传递如果这个 widget 是在setStyleSheet之后动态创建并addWidget的多数情况下会继承——但如果页面在构造时被设了独立的styleSheet属性哪怕是空串继承链就断了。解决改用qApp-setStyleSheet(...)全局设置或在switchTo里对新页面显式重新应用一次样式。项目里我倾向全局 qss 少量局部覆盖减少这种依赖顺序的问题。5.5 现象本机跑得好好的拷到客户机器上提示找不到 .ui / .qml原因开发时用的是磁盘相对路径./resources/ui/xxx.ui工作目录是构建目录发布后工作目录变成了别的地方路径全废。另一个常见原因是 Qt5 发布时漏了platforms/qwindows.dll程序直接起不来用户以为是“找不到资源”。解决所有运行时资源一律打进 qrc路径写:/embed/...彻底摆脱工作目录。部署脚本用windeployqt或macdeployqt跑一遍别手工拷 dll。发布前在干净虚拟机里跑一次这一步没有捷径。6. 二次开发把 Demo 改成能用的东西真正要把它改成产品第一件事是把EmbedHost里的四个函数收成一套统一接口而不是让调用方分别判断用哪种方式。// 统一入口宿主只描述我要一页不关心这页从哪来 enum class EmbedKind { UiFile, Widget, ExternalWindow, Qml }; struct PageSpec { EmbedKind kind; QString source; // .ui 路径 / qml url / 被嵌程序路径 }; QWidget *EmbedHost::createPage(const PageSpec spec, QWidget *parent);改造顺序我一般这么排先让createPage支持UiFile和Qml这两类纯进程内好测再补ExternalWindow因为涉及进程管理和窗口句柄出问题最难查最后把Widget加进去当兜底。验证方法也很朴素——每加一种方式就在宿主里开一个隐藏的调试页把四种方式各放一个实例跑一遍 resize、最小化恢复、切换页面、切输入法。这四步能覆盖我遇到过八成以上的嵌入问题。还有一点是提前留好回滚点ExternalWindow模式在部分系统环境下就是嵌不稳所以接口要允许“降级成独立窗口弹出”。我在PageSpec里加一个bool allowDetach嵌不进去时自动改成QDialog承载用户至少能用。从那以后我每次做嵌入相关的改动都会强制走一遍“干净机器 三种缩放比例 输入法切换”的验证流程哪怕只改了一行布局代码。嵌入这类问题本机永远是对的问题都在别人机器上。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询