在前端开发领域,Vue3 凭借其高效的组件化开发模式和出色的响应式系统,深受开发者喜爱。而当我们想要将前端开发的触角延伸到桌面应用领域时,通常会想到 Electron 等框架。但今天,我要给大家介绍一种全新的组合:PyQt6+Vue3,通过这种跨界搭配,我们可以用熟悉的前端技术栈开发出高性能的桌面应用。本文将结合我的开源项目https://github.com/wkylin/pro-pyqt6-web,为大家详细讲解如何实现。
为什么选择 PyQt6+Vue3
PyQt6 的优势
Qt 是一套跨平台的 C++ 库,提供了丰富的 API 来访问现代桌面和移动系统的各个方面,包括多媒体、网络、图形界面等。PyQt6 则是 Qt v6 的 Python 绑定,它让我们可以使用 Python 语言来开发 Qt 应用。相比 C++,Python 的语法更加简洁易懂,开发效率更高。而且 PyQt6 体积小巧,能够深度集成到系统中,其原生性能表现出色,这是很多基于浏览器内核的桌面开发框架所无法比拟的。
Vue3 的舒适度
Vue3 已经成为前端开发的主流框架之一,其组件化的开发方式让代码的可维护性和复用性大大提高。响应式系统使得数据的管理和更新变得轻松简单。对于前端开发者来说,使用 Vue3 开发桌面应用的界面部分,就如同开发网页应用一样熟悉,无需重新学习一套全新的 UI 开发技术。
两者结合的魅力
将 PyQt6 和 Vue3 结合起来,我们可以充分发挥它们各自的优势。Vue3 负责构建精美的用户界面,进行高效的 UI 渲染。而 PyQt6 则作为 "壳子",实现窗口管理、系统对话框调用、本地文件操作以及网络请求拦截等前端难以实现的功能。通过 Qt 的 WebChannel,我们还能在 Vue 组件和 PyQt 逻辑之间搭建起一座通信桥梁,实现两者的无缝互调。
项目架构解析
从前端开发者的视角来看,这个项目的架构并不复杂。
js
pro-pyqt6-web/
├── vue/ // Vue
│ ├── vite.config.js
│ ├── src/
│ ├── public/
│ ├── package.json
│ ├── package-lock.json
│ ├── jsconfig.json
│ ├── index.html
│ ├── dist/
│ └── README.md
├── utils/
│ ├── resource_manager.py
│ ├── port_manager.py
│ ├── logger.py
│ └── __pycache__/
├── ui/
│ ├── splash_screen.py
│ ├── main_window.py
│ └── __pycache__/
├── main.spec // 构建
├── main.py // PyQt 入口
├── dist/
│ ├── main.exe
│ └── app_debug.log
├── core/
│ ├── server.py
│ ├── bridge.py
│ └── __pycache__/
├── config/
│ ├── settings.py
│ └── __pycache__/
├── build/
│ ├── web/
│ ├── spp/
│ ├── main/
│ └── app/
├── assets/
│ ├── qss/
│ └── icon/
├── app_debug.log
└── README.md
VUE
这部分就是我们熟悉的 Vue3 项目。我们可以使用 Vue CLI 轻松初始化项目,然后按照常规的前端开发流程进行界面搭建、组件开发和功能实现。最后,通过 npm run build 命令将 Vue 项目打包成静态文件,供 PyQt6 调用。
Main.py
它类似于前端项目中的 main.js,是整个应用的入口文件。在这里,我们会初始化 PyQt6 的应用实例,加载 Vue 打包后的静态页面,并启动应用的事件循环。
关键技术点详解
1. 窗口容器:QWebEngineView
在 PyQt6 中,QWebEngineView 就像是 "桌面版的 iframe"。它的作用是加载 Vue 打包生成的 index.html 文件,从而将 Vue 应用嵌入到桌面应用中。通过简单的几行代码,我们就能创建一个 QWebEngineView 实例,并设置它加载指定的页面。
py
# 创建应用实例
app = QApplication(sys.argv)
app.setApplicationName(AppConfig.APP_NAME)
app.setOrganizationName(AppConfig.ORGANIZATION_NAME)
app.setOrganizationDomain(AppConfig.ORGANIZATION_DOMAIN)
# 初始化WebView
self.web_view = QWebEngineView()
self.main_layout.addWidget(self.web_view)
QT 启动HTTP服务并加载html(Vue构建后的dist目录下的HTML)
py
def setup_server(self) -> None:
"""设置并启动HTTP服务器"""
# 更新启动画面状态
if self.splash:
self.splash.set_status("正在启动服务器...")
vue_dir = ResourceManager.get_path(AppConfig.VUE_DIST_PATH)
if not os.path.exists(vue_dir):
QMessageBox.critical(
self, "启动错误",
f"无法找到Vue项目目录:\n{vue_dir}"
)
self.close()
return
self.final_port = PortManager.find_available_port(self.original_port)
if not self.final_port:
QMessageBox.critical(self, "启动错误", "无法找到可用端口")
self.close()
return
# 初始化服务器管理器
self.server_manager = HTTPServerManager(self.final_port, vue_dir)
self.server_manager.signals.started.connect(self.on_server_started)
self.server_manager.signals.failed.connect(self.on_server_failed)
self.server_manager.start()
def load_html(self) -> None:
"""加载目标HTML页面"""
html_file = os.path.basename(self.html_path)
url = QUrl(f"http://localhost:{self.final_port}/{html_file}#/")
info(f"加载页面 | URL: {url.toString()}")
self.web_view.load(url)
self.web_view.page().loadFinished.connect(self.on_page_load_finished)
2. 跨端通信:WebChannel
WebChannel 是实现 Vue 组件和 PyQt 逻辑互调的关键。在前端,WebChannel 的 bridge 对象就相当于我们熟悉的 window.xxx。例如,在 Vue 组件中,我们可以通过调用 bridge.selectFile() 方法来触发 PyQt 的文件选择功能。
py
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtWebEngineCore import QWebEnginePage
from config.settings import AppConfig
from utils.logger import info, debug
class Bridge(QObject):
"""Qt与Web页面通信的桥接类"""
# 信号定义 - 发送消息到Web页面
messageFromQt = pyqtSignal(str)
jsonFromQt = pyqtSignal(dict)
def __init__(self, parent: QObject = None):
super().__init__(parent)
self.web_message_count = 0
self.channel = QWebChannel(self)
self.channel.registerObject("bridge", self)
def setup_channel(self, page: QWebEnginePage) -> None:
"""设置WebChannel到指定页面"""
page.setWebChannel(self.channel)
info("WebChannel已绑定到页面")
@pyqtSlot(str)
def processWebMessage(self, message: str) -> None:
"""处理来自Web页面的字符串消息"""
self.web_message_count += 1
info(f"收到Web消息 | 编号: {self.web_message_count}, 内容: {message}...")
self.messageFromQt.emit(f"已收到消息: {message}...")
@pyqtSlot(dict)
def processWebJson(self, data: dict) -> None:
"""处理来自Web页面的JSON数据"""
self.web_message_count += 1
info(f"收到Web JSON数据 | 编号: {self.web_message_count}")
debug(f"JSON内容: {data}")
# 处理后返回响应
response = {
"status": "success",
"received": True,
"message": "数据已收到",
"data": {
"original_size": len(str(data)),
"timestamp": self._get_timestamp()
}
}
self.jsonFromQt.emit(response)
@pyqtSlot(result=str)
def getQtVersion(self) -> str:
"""获取Qt版本信息"""
from PyQt6.QtCore import QT_VERSION_STR, PYQT_VERSION_STR
return f"PyQt6 版本: {PYQT_VERSION_STR}, Qt 版本: {QT_VERSION_STR}"
@pyqtSlot(int, int, result=int)
def calculateSum(self, a: int, b: int) -> int:
"""计算两个数的和"""
result = a + b
debug(f"计算 {a} + {b} = {result}")
return result
@pyqtSlot(int)
def receiveCalculationResult(self, result: int) -> None:
"""接收Web页面返回的计算结果"""
info(f"收到Web计算结果: {result}")
def send_message_to_web(self, message: str) -> None:
"""发送消息到Web页面"""
self.messageFromQt.emit(message)
def send_json_to_web(self, data: dict) -> None:
"""发送JSON数据到Web页面"""
self.jsonFromQt.emit(data)
def _get_timestamp(self) -> str:
"""获取当前时间戳"""
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
3. Vue 初始化WebChannel的函数
js
// 初始化WebChannel的函数
const initQWebChannel = () => {
// 检查Qt对象是否存在
if (window.qt && window.qt.webChannelTransport) {
try {
// 创建QWebChannel实例
const channel = new QWebChannel(window.qt.webChannelTransport, (channel) => {
// 获取桥接对象
qtObject.value = channel.objects.bridge;
window.bridge = channel.objects.bridge;
// 连接信号处理函数
if (qtObject.value) {
// 处理来自Qt的消息
qtObject.value.messageFromQt.connect((msg) => {
message.value = `收到Qt消息: ${msg}`;
try {
// 尝试解析JSON数据
qtInfo.value = JSON.parse(msg);
} catch (err) {
console.log('err', err)
}
});
// 发送测试消息到Qt
setTimeout(() => {
if (qtObject.value) {
qtObject.value.processWebMessage('Vue应用已连接');
}
}, 1000);
isConnected.value = true;
console.log('QWebChannel连接成功');
} else {
console.error('未找到桥接对象');
}
});
} catch (error) {
console.error('QWebChannel初始化错误:', error);
}
} else {
console.warn('Qt对象或webChannelTransport不可用');
}
};
// 页面加载完成后初始化WebChannel
onMounted(() => {
// 延迟初始化,确保DOM完全加载
setTimeout(() => {
console.log('尝试初始化QWebChannel...');
initQWebChannel();
// 设置重试机制
const maxAttempts = 5;
let attempts = 0;
const checkConnection = setInterval(() => {
if (isConnected.value || attempts >= maxAttempts) {
clearInterval(checkConnection);
if (!isConnected.value) {
console.error('QWebChannel连接失败,已达到最大尝试次数');
}
} else {
attempts++;
console.log(`尝试重新连接QWebChannel (${attempts}/${maxAttempts})`);
initQWebChannel();
}
}, 2000);
}, 500);
});
// 组件卸载时清理资源
onUnmounted(() => {
if (qtObject.value) {
try {
// 断开所有信号连接
qtObject.value.messageFromQt.disconnect();
} catch (e) {
console.warn('断开信号连接时出错:', e);
}
}
});
// 暴露webCalculator对象到全局
window.webCalculator = webCalculator;
实战环节:3 步实现 "Vue+Qt" 桌面应用
- 本地安装 Python 环境
- Vite 初始化vue3项目
- 安装 PyQt6 依赖
可以查看我开源项目:github.com/wkylin/pro-...
打包成桌面应用
- 前端应用:进入到VUE项目目录下:
js
npm run build
- 后端应用:dist/main.exe 可直接运行。
py
pyinstaller --icon="assets/icon/qt.ico" --add-data="assets/qss/qss.qss;assets/qss" --add-data="assets/icon/qt.ico;assets/icon" --add-data="vue/dist;vue/dist" -Fw main.py
软件界面
- 启动界面

- 主界面

- 通信示例界面


学习路径
对于前端开发者来说,可以先从简单地使用 Vue 调用 Qt 能力入手,逐渐熟悉 PyQt6 的各种功能。然后,可以尝试将 Qt 的一些强大组件,如图表组件、多媒体组件等,集成到 Vue 项目中,进一步扩展 Vue 的生态,打造出更加丰富、强大的桌面应用。
总结
通过本文,我们了解了如何使用 PyQt6 和 Vue3 进行跨界开发,打造高性能的桌面应用。这种开发方式为前端开发者提供了一个全新的思路,让我们能够将熟悉的前端技术应用到更广泛的领域。
我的开源项目github.com/wkylin/pro-...b为大家提供了一个完整的实践案例,包含了详细的代码和使用说明,大家可以前往下载学习。同时,我也想抛出一个问题:"你觉得 Vue+Qt 能替代 Electron 吗?" 欢迎大家在评论区留言讨论。
后续我还会推出更多关于 PyQt6+Vue3 开发的文章,比如 "用 Vue3+Qt 开发系统托盘应用",敬请关注!希望本文能帮助大家开启桌面应用开发的新旅程,探索更多技术融合的可能性。