# pyqt_vue_native **Repository Path**: lou_code/pyqt_vue_native ## Basic Information - **Project Name**: pyqt_vue_native - **Description**: 用于pyqt和vue交互的基于chrome的native框架 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 8 - **Created**: 2021-05-29 - **Last Updated**: 2022-02-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # PYQT_VUE_NATIVE框架 ## 介绍 数据交互方式: QWebChannel,web框架: vue-element-admin,QT框架 QWebEngineView web框架 通过QWebChannel封装的组件 调用QT程序, QT 通过信号返回给前端。 通过此框架,前端与QT的交互会非常简单! ![GIF 2021-4-30 14-26-32](README.assets/GIF2021-4-3014-26-32.gif) ![image-20210430142730569](README.assets/image-20210430142730569.png) ![image-20210430142917914](README.assets/image-20210430142917914.png) 框架分为两个部分 frame和custom,以及入口main.py 1. frame 主要是框架代码 2. custom主要是业务代码 3. main.py用于将业务组件绑定到框架中 ## 使用: main.py ```python #!/usr/local/bin/python3 # -*- coding: utf-8 -*- """ @File : main.py @Author : Link @Time : 2021/4/17 9:49 """ import sys from PyQt5.QtWebChannel import QWebChannel from PyQt5.QtWidgets import QApplication from frame import Main # 需要传入的参数 一个QObject和一个QWebChannel from frame import MainInterFace # QObject给QWebChannel发送信号 # ----------------------------------------------- # 需要自己编写的区域 # 从custom引入业务组件 from custom.view.table import A # 测试 from custom.view.liv_view import TO_LIV_KEY # 业务组件注册到MainInterFace class MyMainInterFace(MainInterFace): def __init__(self): super(MyMainInterFace, self).__init__() TO_LIV_KEY(self) A(self) myObj = MyMainInterFace() channel = QWebChannel() channel.registerObject("bridge", myObj) # ----------------------------------------------- class Application(QApplication): def __init__(self, argv): QApplication.__init__(self, argv) QApplication.setStyle('Fusion') if __name__ == '__main__': app = Application(sys.argv) myWin = Main(myObj, channel) # QT Window 需要传入 MyMainInterFace(), QWebChannel() myWin.show() sys.exit(app.exec_()) ``` ## Custom View层 自己编写 1. 如下简易程序示例,对应前端的Table页面 qt端 ```python from custom.fake_data.table import generator_table from frame import VueElementObject from frame import returnSuccess class A: def __init__(self, vue_obj: VueElementObject): vue_obj.func_dict["getTableData"] = self.get_table_data def get_table_data(self, req: dict): if req["func"] != 'getTableData': raise Exception("func error") return returnSuccess("success", func=req["func"], data=generator_table(req["data"]["row"])) ``` web端 ```javascript created() { this.$QtCallBackDict.getTableData = this.fetchData }, mounted() { this.getTableData() }, methods: { fetchData(table_data) { this.list = table_data this.listLoading = false }, getTableData() { this.$request('getTableData', { row: this.row }) this.listLoading = true } } ``` 2. 封装了装饰器的完整程序如下,对应前端的ToLiv页面 ```python from custom.fake_data.charts import generator_echarts_data from custom.fake_data.table import generator_table from frame import ViewInterface from frame import request_vue_data, return_vue_data from PyQt5.QtCore import QTimer # 继承了ViewInterface class TO_LIV_KEY(ViewInterface): # 小写用于qt app = None liv_parameter = None # 获取的测试档信息 # 大写用于前端 APP_OPERATE_CONTROL = True # 前端的操作权限 SELECT_GROUP = { "APP_SELECT_UP_LIST": [], # 1-10 "APP_SELECT_DOWN_LIST": [] # 11-12 } APP_TEXTAREA = "" APP_NOW_STATION = 0 # 当前测试位置号 APP_RESULT_DATA = { "DATA": None, "HEAD": None, } # 返回给前端table的测试数据汇总 APP_FORM = { "APP_MANUFACTURE_ID": "", "APP_PARAMETER_ID": None, "APP_BOARD_NO": 0, "APP_JUDGING": 0, "APP_AUTO_BOARD": False, # 板号自动加一 } APP_CHART_DATA = None # ECharts def __init__(self, vue_obj): """ :param app: QMainWindow :param vue_obj: VueElementObject 携带 pyqtSignal """ super(TO_LIV_KEY, self).__init__(vue_obj=vue_obj) @request_vue_data def liv_start(self, APP_FORM: dict): """ @request_vue_data 装饰器 接收从前端传来的数据 前端用法 this.$request('get_app_status'); :return: """ self.__set_control_status(False) self.set_fail_message("测试 异常信息响应") self.set_other_message(str(APP_FORM), 201) QTimer.singleShot(2000, lambda: self.__set_control_status(True)) QTimer.singleShot(3000, lambda: self.__set_echarts_data(generator_echarts_data())) QTimer.singleShot(4000, lambda: self.__set_table_data({ "DATA": generator_table(10), "HEAD": ['id', 'title', 'author', 'display_time'] })) @request_vue_data def checkbox_change(self, SELECT_GROUP: dict): """ 前端用法 this.$request('checkbox_change', this.SELECT_GROUP); :param kwargs: this.SELECT_GROUP :return: """ self.vue_obj.append_status_message.emit("CHECKBOX 更新成功!") # QT self.__set_checkbox_status(SELECT_GROUP) @request_vue_data def get_app_status(self): """ 顺便把其他的状态也给前端 :return: """ self.__checkbox_emit() self.__control_emit() self.__message_emit() self.__table_data_emit() self.__echarts_data_emit() # 带下划线(单双均可 双下划线方法改名后也会带单下划线) __的函数 不会被注册 参考ViewInterface类 # 写的有点多了 反正下面的写在一起也无所谓 主要是@return_vue_data装饰的函数 要在前端进行绑定 # 带下划线无装饰器: 内部函数 def __set_control_status(self, APP_OPERATE_CONTROL: bool): self.APP_OPERATE_CONTROL = APP_OPERATE_CONTROL self.__control_emit() def __set_checkbox_status(self, SELECT_GROUP: dict): self.SELECT_GROUP = SELECT_GROUP self.__checkbox_emit() def __set_message_status(self, APP_TEXTAREA: str): self.APP_TEXTAREA = APP_TEXTAREA self.__message_emit() def __set_table_data(self, APP_RESULT_DATA: dict): self.APP_RESULT_DATA = APP_RESULT_DATA self.__table_data_emit() def __set_echarts_data(self, APP_CHART_DATA: dict): self.APP_CHART_DATA = APP_CHART_DATA self.__echarts_data_emit() # 带下划线有装饰器的代表是只从QT端往前端发送 """ this.$QtCallBackDict.__control_emit = this.SetAppOperateControl; this.$QtCallBackDict.__checkbox_emit = this.SetCheckBoxStates; this.$QtCallBackDict.__message_emit = this.SetMessageStates; """ @return_vue_data def __checkbox_emit(self): return self.SELECT_GROUP @return_vue_data def __control_emit(self): return self.APP_OPERATE_CONTROL @return_vue_data def __message_emit(self): return self.APP_TEXTAREA @return_vue_data def __table_data_emit(self): return self.APP_RESULT_DATA @return_vue_data def __echarts_data_emit(self): return self.APP_CHART_DATA ``` web端 ```javascript created() { this.$QtCallBackDict.__control_emit = this.SetAppOperateControl; this.$QtCallBackDict.__checkbox_emit = this.SetCheckBoxStates; this.$QtCallBackDict.__message_emit = this.SetMessageStates; this.$QtCallBackDict.__echarts_data_emit = this.SetEchartsData; this.$QtCallBackDict.__table_data_emit = this.SetTableData; }, mounted() { this.$request('get_app_status'); }, methods: { HandleCheckAllChange() { // 传入到qt中 有异常就会返回提示! // 这边的逻辑会先再前端勾选后 后端去判定后 又返回前端 相当于勾选了两次 如果用TEMP就比较麻烦 this.$request('checkbox_change', this.SELECT_GROUP); }, // ------------------------------------ QT控制 SetCheckBoxStates(SELECT_GROUP) { this.SELECT_GROUP = SELECT_GROUP }, SetAppOperateControl(APP_OPERATE_CONTROL) { this.APP_OPERATE_CONTROL = APP_OPERATE_CONTROL }, SetMessageStates(APP_TEXTAREA) { this.APP_TEXTAREA = APP_TEXTAREA }, SetEchartsData(APP_CHART_DATA) { this.APP_CHART_DATA = APP_CHART_DATA }, SetTableData(APP_RESULT_DATA) { this.APP_RESULT_DATA = APP_RESULT_DATA }, // ------------------------------------ // 向QT发送请求 LivStart() { this.$request('liv_start'); this.$request('liv_start', this.APP_FORM); }, } ``` ## frame 控制层 frame.interface.channel_interface ```python class VueElementObject(QObject): func_dict = {} connectSignal = pyqtSignal(str) # QWebChannel Signal @pyqtSlot(str) def request(self, req: str): logger.info("vue request: {}".format(req)) try: request_data = json.loads(req) func = request_data.get("func", None) if func is None: raise Exception("没有传入[func]函数名") qt_func = self.func_dict.get(func, None) if qt_func is None: raise Exception("QT中不存在此函数:{}用于调用".format(func)) data = qt_func(request_data) # 如果返回了数据, 就通过信号返回给前端 否则结束流程 if data is None: return if not isinstance(data, str): raise Exception("返回值必须是一个json字符串类型") logger.info("pyqt emit: {}".format(data)) self.connectSignal.emit(data) except Exception as err: logger.warning("pyqt error: {}".format(err)) self.connectSignal.emit(returnFailure(str(err))) ``` frame.utils.utils: 控制返回数据的格式 和装饰器 ```python from frame.logger_moudle import logger import json def returnSuccess(message, **kwargs): return json.dumps({"data": kwargs, "message": message, 'code': 200}) def returnFailure(message, **kwargs): return json.dumps({"data": kwargs, "message": message, 'code': 400}) def returnOtherMessage(message, code): if code == 200: raise Exception("other message cannot use 200 see in js qt-request ChannelCallBack") return json.dumps({"message": message, 'code': code}) # 通过VueElementObject 的request 返回, 形成前端的请求闭环 def request_vue_data(func): def wrapper(ctx, kwargs: dict): # ctx.app.statusbar.clearMessage() try: func_name = kwargs["func"] if func.__name__ != func_name: raise Exception(f"error func: {func.__name__}, vue_func: {func_name}") data = kwargs.get("data", None) if data is None: return_data = func(ctx) else: return_data = func(ctx, data) if return_data is None: logger.info("func: {} done. no data emit.".format(func.__name__)) return logger.info("func: {} emit: {}".format(func.__name__, return_data)) return returnSuccess("success", func=func.__name__, data=return_data) except Exception as err: ctx.vue_obj.append_status_message.emit(str(err)) # QT logger.error(err) return returnFailure(str(err)) return wrapper # 在QT通过信号直接控制前端 def return_vue_data(func): def wrapper(ctx, *args, **kwargs): # ctx.app.statusbar.clearMessage() try: return_data = func(ctx, *args, **kwargs) if return_data is None: logger.info("func: {} done. no data emit.".format(func.__name__)) return logger.info("func: {} emit: {}".format(func.__name__, return_data)) return ctx.vue_obj.connectSignal.emit(returnSuccess("success", func=func.__name__, data=return_data)) except Exception as err: ctx.vue_obj.append_status_message.emit(str(err)) # QT logger.error(err) return ctx.vue_obj.connectSignal.emit(returnFailure(str(err))) return wrapper ```