From ba3d0069195bc02e8d6cc8644fdee9876e7078bd Mon Sep 17 00:00:00 2001 From: yanganjie Date: Fri, 15 Aug 2025 18:00:27 +0800 Subject: [PATCH] =?UTF-8?q?add:=20cood=5Fforms=5Finterface.py=20(=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=BA=86=E4=BD=8D=E7=BD=AE=E8=AE=BE=E5=AE=9A=E7=9A=84?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=E4=BB=A5=E5=8F=8A=E7=82=B9=E4=BD=8D=E7=A7=BB?= =?UTF-8?q?=E5=8A=A8=E5=8A=9F=E8=83=BD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/view/cood_forms_interface.py | 1197 ++++++++++++++++++++++++++++++ main.py | 149 ++-- 2 files changed, 1300 insertions(+), 46 deletions(-) create mode 100644 app/view/cood_forms_interface.py diff --git a/app/view/cood_forms_interface.py b/app/view/cood_forms_interface.py new file mode 100644 index 0000000..c172c27 --- /dev/null +++ b/app/view/cood_forms_interface.py @@ -0,0 +1,1197 @@ +# coding:utf-8 +import sys +import os +from PySide6.QtCore import ( + Qt, + QSize, + QPoint, + QEvent, + Slot, + Signal, + QThread, + QMutex, + QWaitCondition, +) + +from PySide6.QtGui import QIcon, QColor, QAction + +from PySide6.QtWidgets import ( + QApplication, + QWidget, + QVBoxLayout, + QHBoxLayout, + QStackedWidget, + QMessageBox, + QLabel, + QFrame, + QTreeWidgetItem, + QTreeWidgetItemIterator, + QTableWidget, + QTableWidgetItem, + QListWidget, + QListWidgetItem, + QInputDialog, + QMenu, + QSizePolicy, + QDialog, +) + +from qfluentwidgets import ( + TableWidget, + HorizontalFlipView, + TabBar, + TabCloseButtonDisplayMode, + setTheme, + isDarkTheme, + Theme, + PushButton, + FluentIcon as FIF, + Dialog, +) + +from .gallery_interface import GalleryInterface +from ..common.translator import Translator +from ..common.style_sheet import StyleSheet + +import sqlite3 + + +class CoodFormsInterface(GalleryInterface): + """Cood Forms interface""" + + def __init__(self, parent=None): + t = Translator() + super().__init__(title=t.view, subtitle="CoordinateFormsWidget", parent=parent) + self.setObjectName("coodFormsInterface") + + # CoordinateFormsWidget + self.formsWidget = CoordinateFormsWidget(self) + card = self.addExampleCard( + title=self.tr(""), + widget=self.formsWidget, + sourcePath=".", + stretch=1, + ) + card.topLayout.setContentsMargins(6, 0, 6, 16) + + # 动态设置高度 + self.__connectMainWindowSignal() + + def __connectMainWindowSignal(self): + main_window = self.window() + if main_window: + self.onWindowHeightChanged(main_window.height() - 160) # 设置窗口初始高度 + main_window.heightChanged.connect(self.onWindowHeightChanged) + + def onWindowHeightChanged(self, new_height): + self.formsWidget.setFixedHeight(new_height - 160) + + +class FormDatabase: + def __init__(self, db_path): + self.db_path = db_path + self.conn = None # 数据库连接对象 + self.cursor = None # 游标对象 + + def init_database(self): + """初始化SQLite数据库, 不存在则创建form_info表和form_data表""" + try: + # 数据库文件路径(db目录下的forms.db) + os.makedirs( + os.path.dirname(self.db_path), exist_ok=True + ) # 确保数据库目录存在 + cursor = self.__connect() + + # 创建表单信息表 form_info + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS form_info ( + form_id INTEGER PRIMARY KEY AUTOINCREMENT, + form_name TEXT NOT NULL UNIQUE, -- 表单名称唯一 + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """ + ) + + # 创建表单数据表form_data(存储每行数据) + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS form_data ( + data_id INTEGER PRIMARY KEY AUTOINCREMENT, + form_id INTEGER NOT NULL, -- 关联表单信息表的 form_id + row_index INTEGER NOT NULL, -- 行号(从0开始) + x TEXT, + y TEXT, + z TEXT, + rx TEXT, + ry TEXT, + rz TEXT, + FOREIGN KEY (form_id) REFERENCES form_info(form_id) ON DELETE CASCADE, + UNIQUE(form_id, row_index) -- 确保一行数据只存一次 + ) + """ + ) + + # 自动更新表单修改时间 + # 更新表单修改时间的触发器 + cursor.execute( + """ + CREATE TRIGGER IF NOT EXISTS update_form_time + AFTER UPDATE ON form_data + FOR EACH ROW + BEGIN + UPDATE form_info SET update_time = CURRENT_TIMESTAMP WHERE form_id = NEW.form_id; + END + """ + ) + + # 提交,初始化数据库成功 + self.commit_and_close() + + except Exception as exc: + self.rollback_and_close() + raise exc + + def __connect(self): + """连接数据库并获取游标""" + if not self.conn: + self.conn = sqlite3.connect(self.db_path) + self.cursor = self.conn.cursor() + return self.cursor + + def commit_and_close(self): + """提交事务并关闭连接""" + if self.conn: + self.conn.commit() # 提交事务 + self.conn.close() # 关闭连接 + self.conn = None + self.cursor = None + + def rollback_and_close(self): + """回滚事务并关闭连接""" + if self.conn: + self.conn.rollback() # 回滚事务 + self.conn.close() # 关闭连接 + self.conn = None + self.cursor = None + + def check_form_exists(self, form_name): + """检查表单是否已经存在 + 成功返回form_id + 失败返回None + """ + try: + cursor = self.__connect() + cursor.execute( + "SELECT form_id FROM form_info WHERE form_name = ?", (form_name,) + ) + result = cursor.fetchone() + return result[0] if result else None + except Exception as exc: + self.rollback_and_close() + raise exc + + def delete_form_data(self, form_id): + """删除指定表单的所有数据""" + try: + cursor = self.__connect() + cursor.execute("DELETE FROM form_data WHERE form_id = ?", (form_id,)) + except Exception as exc: + self.rollback_and_close() + raise exc + + def add_new_form(self, form_name): + """新增表单 (返回新表单的form_id)""" + try: + cursor = self.__connect() + cursor.execute("INSERT INTO form_info (form_name) VALUES (?)", (form_name,)) + return cursor.lastrowid # 返回新插入的ID + except Exception as exc: + self.rollback_and_close() + raise exc + + def insert_form_data(self, form_id, data_rows): + """批量插入form_data表""" + try: + cursor = self.__connect() + for row in data_rows: + # row格式: [row_idx, x, y, z, rx, ry, rz] + cursor.execute( + """INSERT INTO form_data + (form_id, row_index, x, y, z, rx, ry, rz) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", + (form_id, row[0], row[1], row[2], row[3], row[4], row[5], row[6]), + ) + except Exception as exc: + self.rollback_and_close() + raise exc + + def get_all_form_id_name(self): + """获取所有form_id 和 对应的 form_name""" + try: + cursor = self.__connect() + cursor.execute( + "SELECT form_id, form_name FROM form_info ORDER BY create_time" + ) + # [(form_id1, form_name1), (form_id2, form_name2), ...] + form_id_name = cursor.fetchall() + return form_id_name + except Exception as exc: + self.rollback_and_close() + raise exc + + def get_row_idx_coordinate_list(self, form_id): + """根据form_id查询对应的表单数据 data_rows (包含行索引row_idx, x, y, z, rx, ry, rz)""" + try: + cursor = self.__connect() + cursor.execute( + "SELECT row_index, x, y, z, rx, ry, rz FROM form_data " + "WHERE form_id = ?", + (form_id,), + ) + # [(row_idx, x, y, z, rx, ry, rz), ...] + data_rows = cursor.fetchall() + return data_rows + except Exception as exc: + self.rollback_and_close() + raise exc + + def get_sorted_coordinate_list(self, form_name): + """ + form_name 等同于线名 + form_name 存在, 返回 [(x1, y1, z1, rx1, ry1, rz1), ......], 按照row_idx排序 + 不存在, 返回 None + """ + pass + + # 包含 坐标 和 对应的状态(运动类型?点位类型?) + def get_coordinate_state_list(self, form_name): + pass + + +# 数据库保存线程 +class DatabaseSaveThread(QThread): + # 信号1:需要用户确认是否覆盖数据库中的数据 + need_confirm_signal = Signal() + # 信号2:数据库操作结果 + result_signal = Signal(str) + + def __init__(self, db_path, form_name, data_rows): + super().__init__() + self.db_path = db_path + self.form_name = form_name + self.data_rows = data_rows # 主线程传递过来的UI数据 + self.user_choice = None # 存储用户选择(True=覆盖,False=取消) + self.mutex = QMutex() + self.condition = QWaitCondition() + + def run(self): + try: + db = FormDatabase(self.db_path) + + # 初始化数据库,至少执行一次 + db.init_database() + + # 检查form_name名的表单是否已经存在 + exist_form_id = db.check_form_exists(self.form_name) + + if exist_form_id is not None: + # 存在表单,发送信号给主线程, 向用户请求确认 + self.need_confirm_signal.emit() + + # 等待UI线程传入用户选择 + self.wait_for_user_choice() + + if not self.user_choice: + # 用户选择不覆盖,结束操作 + self.result_signal.emit(f"已取消,表单「{self.form_name}」未修改") + return + + # 用户选择覆盖:删除旧数据 + db.delete_form_data(exist_form_id) + # 插入新数据(复用exist_form_id) + db.insert_form_data(exist_form_id, self.data_rows) + self.result_signal.emit(f"表单「{self.form_name}」已覆盖并保存") + + else: + # 表单不存在:直接新增 + new_form_id = db.add_new_form(self.form_name) + db.insert_form_data(new_form_id, self.data_rows) + self.result_signal.emit(f"表单「{self.form_name}」已保存") + + # 操作完成,提交并关闭连接 + db.commit_and_close() + # db.rollback_and_close() + + except Exception as e: + # 数据库操作异常 + self.result_signal.emit(f"保存失败:{str(e)}") + + def wait_for_user_choice(self): + """阻塞线程,等待用户选择(条件变量)""" + self.mutex.lock() + # 等待,直到set_user_choice调用wakeAll() + self.condition.wait(self.mutex) + self.mutex.unlock() + + def set_user_choice(self, choice): + """UI线程调用: 设置用户选择并唤醒数据库保存操作线程""" + self.mutex.lock() + self.user_choice = choice + self.condition.wakeAll() # 唤醒等待的数据库保存操作线程 + self.mutex.unlock() + + +# 数据库读取操作线程 +class DatabaseReadThread(QThread): + # 信号1: 从form_info 读取所有的 id-name 发送 + # [(form_id1, form_name1),...] + form_id_name_signal = Signal(list) + + # 信号2: 从 from_data 读取所有的 选择的表单的数据发送 + # {form_name1: data_rows1, ...} + form_data_signal = Signal(dict) + + # 信号3: 状态信息 + state_signal = Signal(str) + + def __init__(self, db_path): + super().__init__() + self.db_path = db_path + self.selected_form_id_name = None # 选择的 表单id-name + + # 初始化信号量 + self.mutex = QMutex() + self.condition = QWaitCondition() + + def run(self): + try: + db = FormDatabase(self.db_path) + + # 初始化数据库 + db.init_database() + + # 从form_info表 查询所有的 form_id 和 form_name + all_form_id_name = db.get_all_form_id_name() + self.form_id_name_signal.emit(all_form_id_name) + + # 等待用户选择 想要查看的 form + self.wait_for_selection() + + if not self.selected_form_id_name: + self.state_signal.emit("未选择任何表单") + return + + form_date_dict = dict() + for form_id, form_name in self.selected_form_id_name: + data_rows = db.get_row_idx_coordinate_list(form_id) + form_date_dict[form_name] = data_rows + + self.form_data_signal.emit(form_date_dict) + db.commit_and_close() + + except Exception as exc: + # 数据库异常 + self.state_signal.emit(f"读取失败:{str(exc)}") + + def wait_for_selection(self): + self.mutex.lock() + self.condition.wait(self.mutex) + self.mutex.unlock() + + def set_selected_forms(self, selected_form_id_name): + """UI线程调用, 设置选择的 表单的 id-name""" + self.mutex.lock() + self.selected_form_id_name = selected_form_id_name + self.condition.wakeAll() + self.mutex.unlock() + + +class CoordinateTableWidget(QWidget): + # 选中行更新信号 + update_table_signal = Signal() + + # 移动到选中行的 坐标的信号 + move_to_coodinate_signal = Signal(list) + + def __init__( + self, parent=None, form_name="default", hideVHeader=False, initRowCount=True + ): + super().__init__(parent=parent) + # 保存当前表单名,根据form_name设置行名 + self.form_name = form_name + self.hideVHeader = hideVHeader # 是否隐藏行名 + self.initRowCount = initRowCount # 是否需要初始的行数 + self.setObjectName("CoordinateTableWidget") + + # 主布局 + self.mainLayout = QVBoxLayout(self) + self.mainLayout.setContentsMargins(30, 30, 30, 30) + self.mainLayout.setSpacing(20) + + # 创建表格 + self.createTable() + + # 创建按钮区域 + self.createButtons() + + # 状态标签 + self.status_label = QLabel("未选中任何行") + self.mainLayout.addWidget(self.status_label) + + # 应用主题样式 + self.applyThemeStyle() + # StyleSheet.VIEW_INTERFACE.apply(self) + + # 初始化数据(行名会关联表单名) + self.initTableData() + + # 更新表单名(供外部调用,修改后同步更新行名) + def update_form_name(self, new_name): + self.form_name = new_name + self.updateRowHeaders() # 同步更新所有行名 + + def createTable(self): + self.table = TableWidget(self) + self.table.setBorderVisible(True) + self.table.setBorderRadius(8) + self.table.setWordWrap(False) + + # 表单的初始列数和行数 + if self.initRowCount: + self.table.setRowCount(5) # 表的行数 + + self.table.setColumnCount(6) # 表的列数 + + self.table.setHorizontalHeaderLabels(["x", "y", "z", "rx", "ry", "rz"]) + self.table.horizontalHeader().setStyleSheet( + "QHeaderView::section {font-size: 19px; font-weight: bold;}" + ) + self.table.verticalHeader().setStyleSheet( + "QHeaderView::section {font-size: 14px; font-weight: bold;}" + ) + + if self.hideVHeader: + self.table.verticalHeader().hide() # 隐藏行标题(行名) + + for i in range(6): + self.table.horizontalHeader().setSectionResizeMode( + i, self.table.horizontalHeader().ResizeMode.Stretch + ) + + self.table.setSelectionBehavior(self.table.SelectionBehavior.SelectRows) + self.table.setSelectionMode(self.table.SelectionMode.ExtendedSelection) + self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.table.customContextMenuRequested.connect(self.showContextMenu) + self.table.itemSelectionChanged.connect(self.onCellSelected) + self.mainLayout.addWidget(self.table, stretch=1) + + def createButtons(self): + btnLayout = QHBoxLayout() + btnLayout.setSpacing(15) + + self.addBtn = PushButton("添加行") + self.addBtn.clicked.connect(self.addNewRow) + btnLayout.addWidget(self.addBtn) + + self.deleteBtn = PushButton("删除行") + self.deleteBtn.clicked.connect(self.deleteSelectedRow) + btnLayout.addWidget(self.deleteBtn) + + self.setBtn = PushButton("设置点位") + self.setBtn.clicked.connect(self.setSelectedRow) + btnLayout.addWidget(self.setBtn) + + self.resetBtn = PushButton("重置点位") + self.resetBtn.clicked.connect(self.resetSelectedRow) + btnLayout.addWidget(self.resetBtn) + + # 移动按钮 + self.readBtn = PushButton("移动") + self.readBtn.setIcon(FIF.RIGHT_ARROW.icon()) # FOLDER + self.readBtn.clicked.connect(self.moveToSelectedCoodinate) + btnLayout.addWidget(self.readBtn) + + # 保存按钮 + self.saveBtn = PushButton("保存数据") + self.saveBtn.setIcon(FIF.SAVE.icon()) # 使用保存图标 + self.saveBtn.clicked.connect(self.saveToDatabase) # 关联保存函数 + btnLayout.addWidget(self.saveBtn) + + self.mainLayout.addLayout(btnLayout) + + def get_ui_data(self): + """从表格UI中获取数据 [[行索引row_idx, x, y, z, rx, ry, rz], ......]""" + row_count = self.table.rowCount() + column_count = self.table.columnCount() + data_rows = [] + for row_idx in range(row_count): + row_data = [row_idx] + for col_idx in range(column_count): + item = self.table.item(row_idx, col_idx) + + # 数据合法性和完整性判定: 保证获取到的是有效数据 + row_data.append( + item.text().strip() if item and item.text().strip() else "-9999" + ) # 该表格为空,填充 -9999 + + # 放入这一行的数据 行索引row_idx, x, y, z, rx, ry, rz + data_rows.append(row_data) + + return data_rows + + def showConfirmDialog(self): + """显示覆盖确认对话框""" + msg_box = QMessageBox(self) + msg_box.setWindowTitle("覆盖确认") + msg_box.setText(f"表单「{self.form_name}」已存在,是否覆盖?") + msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + msg_box.setDefaultButton(QMessageBox.No) + msg_box.setStyleSheet("QMessageBox QLabel { color: black !important; }") + reply = msg_box.exec() + + # 将用户选择(Yes=True,No=False)传给线程,唤醒数据库线程继续执行 + if ( + hasattr(self, "db_thread") + and self.db_thread is not None + and not self.db_thread.isFinished() + ): + self.db_thread.set_user_choice(reply == QMessageBox.Yes) + + def applyThemeStyle(self): + if isDarkTheme(): + self.setStyleSheet( + """ + #CoordinateTableWidget {background-color: rgb(30, 30, 30);} + #CoordinateTableWidget QLabel {font-size: 16px; color: rgb(240, 240, 240);} + #CoordinateTableWidget QTableWidget {color: rgb(240, 240, 240); gridline-color: rgb(60, 60, 60);} + #CoordinateTableWidget QHeaderView::section { + background-color: rgb(40, 40, 40); + color: rgb(200, 200, 200); + border: 1px solid rgb(60, 60, 60); + padding: 5px; + } + """ + ) + else: + self.setStyleSheet( + """ + #CoordinateTableWidget {background-color: rgb(245, 245, 245);} + #CoordinateTableWidget QLabel {font-size: 16px; color: rgb(60, 60, 60);} + #CoordinateTableWidget QTableWidget {color: rgb(60, 60, 60); gridline-color: rgb(200, 200, 200);} + #CoordinateTableWidget QHeaderView::section { + background-color: rgb(230, 230, 230); + color: rgb(60, 60, 60); + border: 1px solid rgb(200, 200, 200); + padding: 5px; + } + """ + ) + + # 初始化表 + def initTableData(self): + for row in range(self.table.rowCount()): + # 行名格式:表单名_行号 + header_item = QTableWidgetItem(f"{self.form_name}_{row+1}") + header_item.setTextAlignment(Qt.AlignCenter) + self.setThemeTextColor(header_item) + self.table.setVerticalHeaderItem(row, header_item) + + for col in range(self.table.columnCount()): + item = QTableWidgetItem("") + item.setTextAlignment(Qt.AlignCenter) + self.setThemeTextColor(item) + self.table.setItem(row, col, item) + + def setThemeTextColor(self, item): + item.setForeground(Qt.white if isDarkTheme() else Qt.black) + + def showNoSelectWarning(self, msg): + msg_box = QMessageBox(self) + msg_box.setIcon(QMessageBox.Warning) + msg_box.setWindowTitle("提示") + msg_box.setText(msg) + msg_box.setStandardButtons(QMessageBox.Ok) + msg_box.setStyleSheet("QLabel{color: black;}") + msg_box.exec() + + def update_table_data(self, positionList=None): + selectedRows = self.getSelectedRows() + for row in selectedRows: + for col in range(self.table.columnCount()): + item = QTableWidgetItem( + str(positionList[col]) + if (positionList and 0 <= col < len(positionList)) + else "-9999" + ) + item.setTextAlignment(Qt.AlignCenter) + self.setThemeTextColor(item) + self.table.setItem(row, col, item) + + if positionList: + self.status_label.setText(f"已将选中行设置为{positionList}") + else: + self.status_label.setText( + "已将选中行设置为[-9999, -9999, -9999, -9999, -9999, -9999]" + ) + + """ + ======================================槽函数================================= + """ + + # 添加新行时,行名同样关联当前表单名 + def addNewRow(self): + # 检查有没有选中行 + # 如果有选中行,在选中行的最下方新增一行 + # 没有选中行,直接在最下方新增一行 + selected_rows = self.getSelectedRows() + insert_pos = max(selected_rows) + 1 if selected_rows else self.table.rowCount() + + # 新增一行 + self.table.insertRow(insert_pos) + + # 更新行名 + self.updateRowHeaders() + + # 初始化单元格 + for col in range(self.table.columnCount()): + item = QTableWidgetItem("") + item.setTextAlignment(Qt.AlignCenter) + self.setThemeTextColor(item) + self.table.setItem(insert_pos, col, item) + + self.status_label.setText(f"已添加第 {insert_pos+1} 行") + + # 删除行 + def deleteSelectedRow(self): + selectedRows = self.getSelectedRows() + if not selectedRows: + self.showNoSelectWarning("请先选中再删除!") + return + + msg_box = QMessageBox(self) + msg_box.setWindowTitle("提示") + rowsStr = ", ".join(map(str, [row + 1 for row in selectedRows])) + msg_box.setText(f"确定要删除选中的第 {rowsStr} 行吗?") + msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + msg_box.setStyleSheet("QLabel{color: black;}") + if msg_box.exec() != QMessageBox.Yes: + return + + for row in sorted(selectedRows, reverse=True): + self.table.removeRow(row) + self.updateRowHeaders() # 删除后重新编号行名 + self.status_label.setText("已删除选中行") + + # 设置选中行 + def setSelectedRow(self): + selectedRows = self.getSelectedRows() + if not selectedRows: + self.showNoSelectWarning("请先选中再设置点位!") + return + + # 选中行默认设置 + self.update_table_data() + + # 发送选中行设置(更新)信号 + self.update_table_signal.emit() + + # 重置点位 + def resetSelectedRow(self): + selectedRows = self.getSelectedRows() + if not selectedRows: + self.showNoSelectWarning("请先选中再重置!") + return + + for row in selectedRows: + for col in range(self.table.columnCount()): + item = QTableWidgetItem("") + item.setTextAlignment(Qt.AlignCenter) + self.setThemeTextColor(item) + self.table.setItem(row, col, item) + + self.status_label.setText("已重置选中行") + + # 移动 + def moveToSelectedCoodinate(self): + selectedRows = self.getSelectedRows() + if not selectedRows: + self.showNoSelectWarning("请先选中再移动!") + return + + # 得到选中行的 点位(坐标) 列表,然后发送 + sortedRowIdList = sorted(selectedRows) # 将选中行的行索引进行排序 + coordinate_rows = list() + for row_idx in sortedRowIdList: + row_coordinate = list() # 保存这一行的坐标 + for col_idx in range(6): # 0-5 ,这些列为坐标 x, y, z, rx, ry, rz + item = self.table.item(row_idx, col_idx) + + # 坐标合法性监测 + + row_coordinate.append(item.text().strip()) + + # 放入这一行的坐标 + # 结果:[[x1, y1, z1, rx1, ry1, rz1], ......] + # 里面的 x1等都为字符串类型 + coordinate_rows.append(row_coordinate) + # 发送移动到这些坐标的信号 + self.move_to_coodinate_signal.emit(coordinate_rows) + + # 保存数据 + def saveToDatabase(self): + # 检查是否已有正在运行的数据库线程 + if hasattr(self, "db_thread") and self.db_thread and self.db_thread.isRunning(): + self.status_label.setText("有未完成的数据库保存操作, 请稍后再试......") + return + + # 获取UI数据 + data_rows = self.get_ui_data() + if not data_rows: + self.status_label.setText("没有数据需要保存") + return + + # 创建数据库操作线程并启动 + self.db_thread = DatabaseSaveThread( + db_path="db/forms.db", form_name=self.form_name, data_rows=data_rows + ) + + # 连接信号:线程需要确认时,UI线程弹对话框 + self.db_thread.need_confirm_signal.connect(self.showConfirmDialog) + # 连接信号:数据库操作线程返回最终结果,UI线程更新 状态标签 + self.db_thread.result_signal.connect(self.status_label.setText) + + # 启动线程 + self.db_thread.start() + + def showContextMenu(self, position): + """显示右键菜单(复制行数据)""" + if not self.getSelectedRows(): + return + + menu = QMenu() + copyAction = QAction("复制此行数据", self) + copyAction.triggered.connect(self.copyRowData) + menu.addAction(copyAction) + menu.exec(self.table.mapToGlobal(position)) + + def copyRowData(self): + """复制选中行数据到剪贴板""" + selectedRows = self.getSelectedRows() + if not selectedRows: + return + + # 目前只支持复制一行数据, + # 选中多行,默认复制选中的多行中行数最小的那行 + row = selectedRows[0] + rowData = [] + for col in range(self.table.columnCount()): + item = self.table.item(row, col) + rowData.append(item.text() if item else "") + + dataStr = ", ".join(rowData) + clipboard = QApplication.clipboard() + clipboard.setText(dataStr) + self.status_label.setText(f"已复制第 {row + 1} 行的数据: {dataStr}") + + def getSelectedRows(self): + return list(set(item.row() for item in self.table.selectedItems())) + + def onCellSelected(self): + selectedRows = self.getSelectedRows() + if selectedRows: + self.status_label.setText( + f"选中行:{', '.join(str(row + 1) for row in selectedRows)}" + ) + else: + self.status_label.setText("未选中任何行") + + # 更新行名 + def updateRowHeaders(self): + for row in range(self.table.rowCount()): + # 行名随表单名动态变化:表单名_行号 + header_item = QTableWidgetItem(f"{self.form_name}_{row+1}") + header_item.setTextAlignment(Qt.AlignCenter) + self.setThemeTextColor(header_item) + self.table.setVerticalHeaderItem(row, header_item) + + +class CoordinateFormsWidget(QWidget): + # 表单中 更新信号 (用于更新表单中的选中行数据) + form_update_signal = Signal(CoordinateTableWidget) + + # 表单中 移动到坐标的信号 (用于机器臂的移动) + form_move_signal = Signal(list) + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Coordinate Forms") + self.setObjectName("CoordinateFormsWidget") + self.resize(1100, 750) + self.formTotalCnt = 0 # 创建过的总表单数 + + # 主布局 + self.mainLayout = QVBoxLayout(self) + self.mainLayout.setContentsMargins(0, 0, 0, 0) + self.mainLayout.setSpacing(0) + + # 标签栏(支持双击修改名称) + self.tabBar = TabBar(self) + self.tabBar.setMovable(True) + self.tabBar.setTabMaximumWidth(200) + self.tabBar.setCloseButtonDisplayMode(TabCloseButtonDisplayMode.ON_HOVER) + + # 绑定槽函数 + self.tabBar.tabAddRequested.connect(self.addNewForm) + self.tabBar.tabCloseRequested.connect(self.closeForm) + self.tabBar.currentChanged.connect(self.switchForm) + + # 给tabBar安装事件过滤器,用于监听双击事件 + self.tabBar.installEventFilter(self) + self.mainLayout.addWidget(self.tabBar) + + # 表单内容区 + self.formStack = QStackedWidget(self) + self.formStack.setObjectName("CoordinateFormStack") + self.mainLayout.addWidget(self.formStack) + + # 读取按钮 + self.readBtn = PushButton("读取数据") + self.readBtn.setIcon(FIF.LIBRARY.icon()) + self.readBtn.clicked.connect(self.readFromDatabase) + self.mainLayout.addWidget(self.readBtn) + + # 初始化第一个表单 + self.initFirstForm() + + # 初始化样式,应用主题样式 + self.applyThemeStyle() + + def applyThemeStyle(self): + """根据当前主题应用样式""" + if isDarkTheme(): + # 深色主题样式 + self.setStyleSheet( + """ + #CoordinateFormStack { + background-color: rgb(30, 30, 30); /* 堆叠窗口背景设为深色 */ + } + #CoordinateFormsWidget { + background-color: rgb(30, 30, 30); /* 主窗口背景设为深色 */ + } + """ + ) + else: + # 浅色主题样式 + self.setStyleSheet( + """ + #CoordinateFormStack { + background-color: rgb(245, 245, 245); /* 堆叠窗口背景设为浅色 */ + } + #CoordinateFormsWidget { + background-color: rgb(245, 245, 245); /* 主窗口背景设为浅色 */ + } + """ + ) + + # 事件过滤器:监听TabBar的鼠标双击事件 + def eventFilter(self, obj, event): + # 只处理TabBar的双击事件 + if obj == self.tabBar and event.type() == QEvent.MouseButtonDblClick: + if event.button() == Qt.LeftButton: # 左键双击 + # 获取双击位置(相对于TabBar) + pos = event.position().toPoint() + # 判断双击位置是否在标签区域内 + tab_region = self.tabBar.tabRegion() + if tab_region.contains(pos): + # 计算双击的是哪个标签 + index = self.getTabIndexByPos(pos) + if index != -1: + self.renameForm(index) # 调用修改表单名的方法 + return True # 事件已处理 + return super().eventFilter(obj, event) + + # 根据点击位置计算对应的标签索引 + def getTabIndexByPos(self, pos): + """通过坐标判断双击的是哪个标签""" + x = 0 + for i in range(self.tabBar.count()): + # 获取第i个标签的宽度 + tab_rect = self.tabBar.tabRect(i) + if x <= pos.x() < x + tab_rect.width(): + return i # 返回标签索引 + x += tab_rect.width() + return -1 # 未命中任何标签 + + # 初始化第一个表单 + def initFirstForm(self): + self.formTotalCnt += 1 # 1 + firstFormName = f"default" + formKey = f"form_{self.formTotalCnt}" # 唯一标识, form_1 + + # 创建表单实例时传入表单名 + newForm = CoordinateTableWidget(form_name=firstFormName, parent=self) + newForm.update_table_signal.connect(self.handleFormUpdate) + newForm.move_to_coodinate_signal.connect(self.form_move_signal) + + self.tabBar.addTab(routeKey=formKey, text=firstFormName, icon=FIF.TILES.icon()) + self.formStack.addWidget(newForm) + + # 添加新表单 + def addNewForm(self): + """添加新表单(带自定义名称输入)""" + self.formTotalCnt += 1 + formCount = self.tabBar.count() + defaultName = f"default{formCount}" + + # 弹出输入框让用户输入表单名 + # formName, ok = QInputDialog.getText( + # self, "创建新表单名称", "请输入新表单的名称:", text=defaultName + # ) + + formName, ok = QInputDialog.getText( + self, "请输入新创建的表单名", "", text=defaultName + ) + + # 用户取消,不添加新表单 + if not ok: + return + + # 用户输入为空,使用默认名称 + if not formName.strip(): + formName = defaultName + + formKey = f"form_{self.formTotalCnt}" # 唯一标识 + + # 创建表单实例时传入表单名 + newForm = CoordinateTableWidget(form_name=formName, parent=self) + newForm.update_table_signal.connect(self.handleFormUpdate) + newForm.move_to_coodinate_signal.connect(self.form_move_signal) + + self.tabBar.addTab(routeKey=formKey, text=formName, icon=FIF.TILES.icon()) + self.formStack.addWidget(newForm) + + # 切换到新创建的表单, 并同步内容 + self.tabBar.setCurrentIndex(self.tabBar.count() - 1) + self.tabBar.currentChanged.emit(self.tabBar.currentIndex()) + + # 关闭表单 + def closeForm(self, index): + # if self.tabBar.count() <= 1: # 必须保留一个表单 + # msg_box = QMessageBox(self) + # msg_box.setIcon(QMessageBox.Warning) + # msg_box.setWindowTitle("提示") + # msg_box.setText("至少保留一个表单") + # msg_box.setStandardButtons(QMessageBox.Ok) + # msg_box.setStyleSheet("QMessageBox QLabel { color: black !important; }") + # msg_box.exec() + # return + + self.tabBar.removeTab(index) + formToRemove = self.formStack.widget(index) + self.formStack.removeWidget(formToRemove) + formToRemove.deleteLater() + + def switchForm(self, index): + if 0 <= index < self.formStack.count(): + self.formStack.setCurrentIndex(index) + + # 更新表单名 + def renameForm(self, index): + """双击标签修改表单名,并同步更新行名""" + if index < 0 or index >= self.tabBar.count(): + return + + # 获取当前表单名作为默认值 + currentName = self.tabBar.tabText(index) + + # 弹出输入框修改名称 + # newName, ok = QInputDialog.getText( + # self, "修改表单名称", "请输入新的表单名称:", text=currentName + # ) + newName, ok = QInputDialog.getText( + self, "请输入修改后的表单名", "", text=currentName + ) + if not ok or not newName.strip(): + return # 取消或空输入则不修改 + + # 更新标签栏显示的名称 + self.tabBar.setTabText(index, newName) + + # 获取对应的表单实例,更新表单并同步更新行名 + formWidget = self.formStack.widget(index) + if isinstance(formWidget, CoordinateTableWidget): + formWidget.update_form_name(newName) # 同步更新行名 + + # 处理表单选中行更新 + def handleFormUpdate(self): + sender_form = self.sender() + if isinstance(sender_form, CoordinateTableWidget): + self.form_update_signal.emit(sender_form) + + def readFromDatabase(self): + """启动读取线程,处理表单选择和数据生成""" + # 检查是否已有运行中的读取线程 + if ( + hasattr(self, "read_thread") + and self.read_thread + and self.read_thread.isRunning() + ): + # self.status_label.setText("有未完成的数据库读取操作,请稍后再试......") + return + + # 创建读取线程 + self.read_thread = DatabaseReadThread(db_path="db/forms.db") + + # bind + # 连接信号:接收所有表单名,显示选择页面 + self.read_thread.form_id_name_signal.connect(self.showFormSelection) + # 连接信号:接收表单数据,生成新表单 + self.read_thread.form_data_signal.connect(self.generateNewForms) + # 连接信号:状态提示 (数据库错误等) + self.read_thread.state_signal.connect(self.showDatabaseReadWarning) + + # 启动线程 + self.read_thread.start() + + def showDatabaseReadWarning(self, msg): + msg_box = QMessageBox(self) + msg_box.setIcon(QMessageBox.Warning) + msg_box.setWindowTitle("提示") + msg_box.setText(msg) + msg_box.setStandardButtons(QMessageBox.Ok) + msg_box.setStyleSheet("QLabel{color: black;}") + msg_box.exec() + + def showFormSelection(self, all_form_id_name): + """显示多选表单选择页面 (UI线程处理)""" + if not all_form_id_name: + # self.status_label.setText("数据库为空, 没有任何表单") + self.read_thread.set_selected_forms(list()) + return + + # 提取表单名(用于列表显示) + form_names = [name for (_, name) in all_form_id_name] + + # 创建多选列表对话框 + dialog = QDialog(self) + + # 设置对话框宽高 + dialog.resize(400, 300) # 可根据需要调整数值 + + dialog.setWindowTitle("选择表单") + layout = QVBoxLayout(dialog) + list_widget = QListWidget() + list_widget.setSelectionMode(QListWidget.ExtendedSelection) # 支持多选 + + # 设置列表样式 + list_widget.setStyleSheet( + """ + QListWidget { + font-size: 14px; /* 字体大小 */ + padding: 5px; /* 内边距 */ + } + QListWidget::item { + height: 30px; /* 行高 */ + } + """ + ) + + # 表单名添加到列表 + list_widget.addItems(form_names) + layout.addWidget(list_widget) + + # 确认/取消按钮 + btn_layout = QHBoxLayout() + confirm_btn = PushButton("确认") + confirm_btn.setStyleSheet("color: black;") + cancel_btn = PushButton("取消") + cancel_btn.setStyleSheet("color: black;") + btn_layout.addWidget(confirm_btn) + btn_layout.addWidget(cancel_btn) + layout.addLayout(btn_layout) + + # 确认按钮逻辑:获取选中的表单ID和名称 + def onDialogConfirm(): + selected_items = list_widget.selectedItems() + if not selected_items: # 没有选择,则退出读取 + # self.read_thread.set_selected_forms(list()) + # QMessageBox.warning(dialog, "提示", "请至少选择一个表单") + dialog.reject() # 按取消处理,在onDialogClosed中处理 + return + + # 映射选中的名称到 (form_id, form_name) + selected = [ + all_form_id_name[i] + for i in range(len(all_form_id_name)) + if list_widget.item(i).isSelected() + ] + self.read_thread.set_selected_forms(selected) # 通知线程用户选择 + dialog.accept() + + # 对话框关闭时的统一处理 + def onDialogClosed(result): + # 如果不是用户确认(result != QDialog.Accepted),则通知线程取消 + if result != QDialog.Accepted: + if ( + hasattr(self, "read_thread") + and self.read_thread + and not self.read_thread.isFinished() + ): + self.read_thread.set_selected_forms(list()) + + # bind + confirm_btn.clicked.connect(onDialogConfirm) + cancel_btn.clicked.connect(dialog.reject) + dialog.finished.connect(onDialogClosed) + + # 显示表单选择框 + dialog.exec() + + # 由读取的数据生成新表单 + def generateNewForms(self, form_data): + """根据读取到的数据生成新表单(UI线程处理)""" + # form_data格式:{form_name: [(row_idx, x, y, z, rx, ry, rz), ...], ...} + for form_name, data_rows in form_data.items(): + # 创建新表单 + self.formTotalCnt += 1 # 创建的总的表单数加一 + new_form = CoordinateTableWidget( + form_name=f"{form_name}", parent=self, initRowCount=False + ) + new_form.update_table_signal.connect(self.handleFormUpdate) + new_form.move_to_coodinate_signal.connect(self.form_move_signal) + + # 填充数据到新表单 + for row_idx, x, y, z, rx, ry, rz in data_rows: + # 如果数单行数超过表格行数,自动添加行 + while row_idx >= new_form.table.rowCount(): + new_form.addNewRow() + + # 填充单元格 + new_form.table.setItem(row_idx, 0, QTableWidgetItem(x)) + new_form.table.setItem(row_idx, 1, QTableWidgetItem(y)) + new_form.table.setItem(row_idx, 2, QTableWidgetItem(z)) + new_form.table.setItem(row_idx, 3, QTableWidgetItem(rx)) + new_form.table.setItem(row_idx, 4, QTableWidgetItem(ry)) + new_form.table.setItem(row_idx, 5, QTableWidgetItem(rz)) + # 将新表单添加到UI + form_key = f"form_{self.formTotalCnt}" # 唯一标识 + self.tabBar.addTab( + routeKey=form_key, text=f"{form_name}", icon=FIF.TILES.icon() + ) + self.formStack.addWidget(new_form) + + +# 更新表单中选中行数据 x,y,z,rx,ry,rz +# def test(form_obj: CoordinateTableWidget): +# form_obj.update_table_data([666, 666, 666, 666, 666, 666]) + + +# if __name__ == "__main__": +# app = QApplication(sys.argv) +# setTheme(Theme.DARK) +# window = CoordinateFormsWidget() +# window.form_update_signal.connect(test) +# window.show() +# sys.exit(app.exec()) diff --git a/main.py b/main.py index 57a74b6..db67132 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,39 @@ # coding:utf-8 import sys -from PySide6.QtCore import Qt, QRect, QUrl -from PySide6.QtGui import QIcon, QPainter, QImage, QBrush, QColor, QFont, QDesktopServices -from PySide6.QtWidgets import QApplication, QFrame, QStackedWidget, QHBoxLayout, QLabel +import os +import sqlite3 +from PySide6.QtCore import Qt, QRect, QUrl, Signal +from PySide6.QtGui import ( + QIcon, + QPainter, + QImage, + QBrush, + QColor, + QFont, + QDesktopServices, +) +from PySide6.QtWidgets import ( + QApplication, + QFrame, + QStackedWidget, + QHBoxLayout, + QLabel, + QWidget, +) -from qfluentwidgets import (NavigationInterface, NavigationItemPosition, NavigationWidget, MessageBox, - isDarkTheme, setTheme, Theme, setThemeColor, qrouter, FluentWindow, NavigationAvatarWidget) +from qfluentwidgets import ( + NavigationInterface, + NavigationItemPosition, + NavigationWidget, + MessageBox, + isDarkTheme, + setTheme, + Theme, + setThemeColor, + qrouter, + FluentWindow, + NavigationAvatarWidget, +) from qfluentwidgets import FluentIcon as FIF from qframelesswindow import FramelessWindow, StandardTitleBar @@ -14,6 +42,9 @@ from app.view.produce_interface import ProduceInterface from app.view.text_interface import TextInterface from app.view.data_interface import DataInterface +from app.view.cood_forms_interface import CoodFormsInterface + + class Widget(QFrame): def __init__(self, text: str, parent=None): @@ -22,11 +53,12 @@ class Widget(QFrame): self.label.setAlignment(Qt.AlignCenter) self.hBoxLayout = QHBoxLayout(self) self.hBoxLayout.addWidget(self.label, 1, Qt.AlignCenter) - self.setObjectName(text.replace(' ', '-')) - + self.setObjectName(text.replace(" ", "-")) class Window(FramelessWindow): + ## 定义信号:调整高度 + heightChanged = Signal(int) def __init__(self): super().__init__() @@ -43,15 +75,15 @@ class Window(FramelessWindow): self.stackWidget = QStackedWidget(self) # create sub interface - # self.system = SystemInterface(self) - # self.product = ProduceInterface(self) - # self.robot = Widget('机械臂基础设置', self) - # self.io = Widget('IO面板', self) - # self.position = Widget('位置设定', self) - # self.basic = Widget('基础设置', self) - # self.point = Widget('点位调试', self) - # self.other = Widget('其他设置', self) - self.data=DataInterface(self) + self.system = SystemInterface(self) + self.product = ProduceInterface(self) + self.robot = Widget("机械臂基础设置", self) # 暂时不用 + self.io = Widget("IO面板", self) # 需要完成 + self.position = CoodFormsInterface(self) # 位置设定 + self.basic = Widget("基础设置", self) # 需要完成 + self.point = Widget("点位调试", self) + self.other = Widget("其他设置", self) + self.data = DataInterface(self) # initialize layout self.initLayout() @@ -73,18 +105,30 @@ class Window(FramelessWindow): # self.navigationInterface.setAcrylicEnabled(True) # self.addSubInterface 加入导航栏页面 - # self.addSubInterface(self.system, FIF.SETTING, '系统设置', NavigationItemPosition.SCROLL) - # self.addSubInterface(self.product, FIF.COMPLETED, '生产界面', parent=self.system) - # self.addSubInterface(self.robot, FIF.ROBOT, '机械臂基础设置', parent=self.system) - # self.addSubInterface(self.io, FIF.GAME, 'IO面板', parent=self.system) - # self.addSubInterface(self.position, FIF.IOT, '位置设定', parent=self.system) - # self.addSubInterface(self.basic, FIF.DEVELOPER_TOOLS, '基础设置', parent=self.system) - # self.addSubInterface(self.point, FIF.MOVE, '点位调试', parent=self.system) - # - # self.navigationInterface.addSeparator() - #self.addSubInterface(self.other, FIF.APPLICATION, '其他设置', NavigationItemPosition.SCROLL) - - self.addSubInterface(self.data, FIF.PHOTO, '数据采集', NavigationItemPosition.SCROLL) + self.addSubInterface( + self.system, FIF.SETTING, "系统设置", NavigationItemPosition.SCROLL + ) + self.addSubInterface( + self.product, FIF.COMPLETED, "生产界面", parent=self.system + ) + self.addSubInterface( + self.robot, FIF.ROBOT, "机械臂基础设置", parent=self.system + ) + self.addSubInterface(self.io, FIF.GAME, "IO面板", parent=self.system) + self.addSubInterface(self.position, FIF.IOT, "位置设定", parent=self.system) + self.addSubInterface( + self.basic, FIF.DEVELOPER_TOOLS, "基础设置", parent=self.system + ) + self.addSubInterface(self.point, FIF.MOVE, "点位调试", parent=self.system) + + # self.navigationInterface.addSeparator() + self.addSubInterface( + self.other, FIF.APPLICATION, "其他设置", NavigationItemPosition.SCROLL + ) + + self.addSubInterface( + self.data, FIF.PHOTO, "数据采集", NavigationItemPosition.SCROLL + ) # add navigation items to scroll area # for i in range(1, 21): # self.navigationInterface.addItem( @@ -97,8 +141,8 @@ class Window(FramelessWindow): # add custom widget to bottom self.navigationInterface.addWidget( - routeKey='avatar', - widget=NavigationAvatarWidget('zhiyiYo', 'resource/shoko.png'), + routeKey="avatar", + widget=NavigationAvatarWidget("zhiyiYo", "resource/shoko.png"), onClick=self.showMessageBox, position=NavigationItemPosition.BOTTOM, ) @@ -117,13 +161,13 @@ class Window(FramelessWindow): def initWindow(self): self.resize(900, 700) - self.setWindowIcon(QIcon('resource/logo.png')) - self.setWindowTitle('密胺投料控制系统') + self.setWindowIcon(QIcon("resource/logo.png")) + self.setWindowTitle("密胺投料控制系统") self.titleBar.setAttribute(Qt.WA_StyledBackground) desktop = QApplication.screens()[0].availableGeometry() w, h = desktop.width(), desktop.height() - self.move(w//2 - self.width()//2, h//2 - self.height()//2) + self.move(w // 2 - self.width() // 2, h // 2 - self.height() // 2) # NOTE: set the minimum window width that allows the navigation panel to be expanded self.navigationInterface.setMinimumExpandWidth(900) @@ -131,8 +175,15 @@ class Window(FramelessWindow): self.setQss() - def addSubInterface(self, interface, icon, text: str, position=NavigationItemPosition.TOP, parent=None): - """ add sub interface """ + def addSubInterface( + self, + interface, + icon, + text: str, + position=NavigationItemPosition.TOP, + parent=None, + ): + """add sub interface""" self.stackWidget.addWidget(interface) self.navigationInterface.addItem( routeKey=interface.objectName(), @@ -141,12 +192,12 @@ class Window(FramelessWindow): onClick=lambda: self.switchTo(interface), position=position, tooltip=text, - parentRouteKey=parent.objectName() if parent else None + parentRouteKey=parent.objectName() if parent else None, ) def setQss(self): - color = 'dark' if isDarkTheme() else 'light' - with open(f'resource/{color}/demo.qss', encoding='utf-8') as f: + color = "dark" if isDarkTheme() else "light" + with open(f"resource/{color}/demo.qss", encoding="utf-8") as f: self.setStyleSheet(f.read()) def switchTo(self, widget): @@ -158,22 +209,28 @@ class Window(FramelessWindow): #!IMPORTANT: This line of code needs to be uncommented if the return button is enabled # qrouter.push(self.stackWidget, widget.objectName()) - + + def resizeEvent(self, event): + super().resizeEvent(event) + self.heightChanged.emit(self.height()) + def showMessageBox(self): w = MessageBox( - '支持作者🥰', - '个人开发不易,如果这个项目帮助到了您,可以考虑请作者喝一瓶快乐水🥤。您的支持就是作者开发和维护项目的动力🚀', - self + "支持作者🥰", + "个人开发不易,如果这个项目帮助到了您,可以考虑请作者喝一瓶快乐水🥤。您的支持就是作者开发和维护项目的动力🚀", + self, ) - w.yesButton.setText('来啦老弟') - w.cancelButton.setText('下次一定') + w.yesButton.setText("来啦老弟") + w.cancelButton.setText("下次一定") if w.exec(): QDesktopServices.openUrl(QUrl("https://afdian.net/a/zhiyiYo")) -if __name__ == '__main__': - app = QApplication(sys.argv) +if __name__ == "__main__": + # 初始化数据库 + # init_database() + app = QApplication([]) w = Window() w.show() app.exec()