# coding:utf-8 import sys import os import time 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 from .mi_an.status_edit_dialog import StatusEditDialog from ..model.point_state import PointState 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 TABLE IF NOT EXISTS form_data ( data_id INTEGER PRIMARY KEY AUTOINCREMENT, --ID form_id INTEGER NOT NULL, --关联form_info表的 form_id row_index INTEGER NOT NULL, --表单中的行号(从0开始) x DOUBLE NOT NULL, --X轴坐标(单位:mm) y DOUBLE NOT NULL, --Y轴坐标(单位:mm) z DOUBLE NOT NULL, --Z轴坐标(单位:mm) rx DOUBLE NOT NULL, --绕X轴旋转角度(单位:°) ry DOUBLE NOT NULL, --绕Y轴旋转角度(单位:°) rz DOUBLE NOT NULL, --绕Z轴旋转角度(单位:°) name VARCHAR(50) NOT NULL DEFAULT 'normal', --点位名(如“取料点”) speed DOUBLE NOT NULL DEFAULT 20.0, --移动速度 tool_id INTEGER NOT NULL DEFAULT 0, --工具ID(如夹爪、吸盘) workpiece_id INTEGER NOT NULL DEFAULT 0, --工件ID j1 DOUBLE NOT NULL DEFAULT -9999.0, --关节角度j1(单位:°) j2 DOUBLE NOT NULL DEFAULT -9999.0, --关节角度j2(单位:°) j3 DOUBLE NOT NULL DEFAULT -9999.0, --关节角度j3(单位:°) j4 DOUBLE NOT NULL DEFAULT -9999.0, --关节角度j4(单位:°) j5 DOUBLE NOT NULL DEFAULT -9999.0, --关节角度j5(单位:°) j6 DOUBLE NOT NULL DEFAULT -9999.0, --关节角度j6(单位:°) motion_type VARCHAR(20) NOT NULL DEFAULT '直线', -- 运动类型 blend_time INTEGER NOT NULL DEFAULT -1, -- 平滑时间,-1默认停止 ext1 DOUBLE, --扩展字段1 ext2 INTEGER, --扩展字段2 ext3 TEXT, --扩展字段3 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.conn.execute("PRAGMA foreign_keys = ON;") # 启用外键 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): """删除指定表单的所有数据(表单数据form_data表中的该表单的数据)""" 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 delete_form(self, form_id): """删除指定表单, 包括 表单信息表中表单信息 以及 数据表中的对应数据""" try: cursor = self.__connect() # 利用外键,只需要删除表单信息form_info表中的表单信息 # 数据表中的对应数据 会被自动删除 # 备注:SQLite的外键默认不开启,需要在 __connect中启用外键 cursor.execute("DELETE FROM form_info 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): # 备注: data_rows 中一行的数据 包含 row_index, x, y, z, rx, ry, rz, name, pos_state # pos_state (点位状态, 字典类型) 包含 pos_name,speed, tool_id, work_id, j1-j6, motion_type, blend_time (状态编辑界面设置完成后插入) """批量插入form_data表""" try: cursor = self.__connect() for row in data_rows: # row格式: [row_idx, x, y, z, rx, ry, rz, name, pos_state_dict] """ row索引为8的列为 点位状态的字典 (pos_state为字典类型) 格式如: {'pos_name': 'normal', 'speed': 20.0, 'tool_id': 0, 'work_id': 0, 'joint_values': [95.261, 82.247, -180.0, -75.121, -84.143, -15.421], 'motion_type': '直线', 'blend_time': -1} """ pos_state_dict = row[8] if not pos_state_dict: # 没有设置点位状态,点位状态使用默认值 cursor.execute( """INSERT INTO form_data (form_id, row_index, x, y, z, rx, ry, rz, name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( form_id, row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7], ), ) else: # 从状态字典pos_state_dict中提取数据 pos_name = pos_state_dict.get("pos_name", "normal") speed = pos_state_dict.get("speed", 20.0) tool_id = pos_state_dict.get("tool_id", 0) workpiece_id = pos_state_dict.get("work_id", 0) joint_values = pos_state_dict.get("joint_values", [-9999.0] * 6) j1, j2, j3, j4, j5, j6 = joint_values[:6] motion_type = pos_state_dict.get("motion_type", "直线") blend_time = pos_state_dict.get("blend_time", -1) cursor.execute( """INSERT INTO form_data (form_id, row_index, x, y, z, rx, ry, rz, name, speed, tool_id, workpiece_id, j1, j2, j3, j4, j5, j6, motion_type, blend_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( form_id, # 1. form_id row[0], # 2. row_index row[1], # 3. x row[2], # 4. y row[3], # 5. z row[4], # 6. rx row[5], # 7. ry row[6], # 8. rz pos_name, # 9. name # 以点位状态中的 pos_name 为准 speed, # 10. speed tool_id, # 11. tool_id workpiece_id, # 12. workpiece_id j1, # 13. j1 j2, # 14. j2 j3, # 15. j3 j4, # 16. j4 j5, # 17. j5 j6, # 18. j6 motion_type, # 19. motion_type blend_time, # 20. blend_time ), ) except Exception as exc: self.rollback_and_close() raise exc def get_form_data(self, form_id): """ 根据form_id查询表单数据, 返回所有的表单数据 返回格式为: data_rows = [ [row_idx, x, y, z, rx, ry, rz, name, pos_state_dict], ... ] """ try: cursor = self.__connect() # 查询指定form_id的所有数据行,并按row_index排序 cursor.execute( """SELECT row_index, x, y, z, rx, ry, rz, name, speed, tool_id, workpiece_id, j1, j2, j3, j4, j5, j6, motion_type, blend_time FROM form_data WHERE form_id = ? ORDER BY row_index""", (form_id,), ) data_rows = [] # 遍历查询结果 for row in cursor.fetchall(): # 解析基础坐标数据 row_idx = row[0] x, y, z, rx, ry, rz = row[1:7] name = row[7] # 解析 点位状态数据并构建字典 speed = row[8] tool_id = row[9] work_id = row[10] # j1, j2, j3, j4, j5, j6 = row[11:17] motion_type = row[17] blend_time = row[18] pos_state_dict = { "pos_name": name, # 保持与插入时一致,使用name作为pos_name "speed": speed, "tool_id": tool_id, "work_id": work_id, "joint_values": [j1, j2, j3, j4, j5, j6], "motion_type": motion_type, "blend_time": blend_time, } # 构建一行的数据 data_row = [row_idx, x, y, z, rx, ry, rz, name, pos_state_dict] data_rows.append(data_row) return data_rows 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_row_idx_coordinate_and_name(self, form_id): """根据form_id查询对应的表单数据 data_rows (包含行索引row_idx, x, y, z, rx, ry, rz, name)""" try: cursor = self.__connect() cursor.execute( "SELECT row_index, x, y, z, rx, ry, rz, name FROM form_data " "WHERE form_id = ?", (form_id,), ) # [(row_idx, x, y, z, rx, ry, rz, name), ...] 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线程传递过来的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_form_data(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.has_valid_copy = False # 标志是否进行了有效的复制(复制了一行的数据) self.setObjectName("CoordinateTableWidget") # 状态编辑界面的临时数据 # 状态编辑后点击应用保存在这里 # 临时编辑数据:{标识符: 编辑后的状态(name、j1...)} # name需要特殊处理 self.edited_state_dict = {} # 主布局 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(8) # 表的列数 self.table.setHorizontalHeaderLabels( ["x", "y", "z", "rx", "ry", "rz", "name", "timestamp"] ) self.table.horizontalHeader().setStyleSheet( "QHeaderView::section {font-size: 19px; font-weight: bold;}" ) self.table.verticalHeader().setStyleSheet( "QHeaderView::section {font-size: 14px; font-weight: bold;}" ) # 第八列(索引7)不显示 (通常不需要显示时间戳[唯一标识]) self.table.setColumnHidden(7, True) if self.hideVHeader: self.table.verticalHeader().hide() # 隐藏行标题(行名) for i in range(7): 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) btnLayout2 = QHBoxLayout() btnLayout2.setSpacing(15) # 上移按钮 self.moveUpBtn = PushButton("上移") self.moveUpBtn.clicked.connect(self.moveRowUp) btnLayout2.addWidget(self.moveUpBtn) # 下移按钮 self.moveDownBtn = PushButton("下移") self.moveDownBtn.clicked.connect(self.moveRowDown) btnLayout2.addWidget(self.moveDownBtn) # 状态编辑按钮 self.stateEditBtn = PushButton("状态编辑") self.stateEditBtn.clicked.connect(self.onStateEdit) btnLayout2.addWidget(self.stateEditBtn) self.mainLayout.addLayout(btnLayout) # 添加 按钮布局一 self.mainLayout.addLayout(btnLayout2) # 添加 按钮布局二 # 数据保存到数据库,获取数据时调用 def get_ui_data(self): """获取数据 [[行索引row_idx, x, y, z, rx, ry, rz, name, pos_state], ......]""" row_count = self.table.rowCount() # column_count = self.table.columnCount() # 这里的 data_rows 保存 [[行索引row_idx, x, y, z, rx, ry, rz, name, pos_state], ......] data_rows = [] for row_idx in range(row_count): row_data = [row_idx] # 先保存行索引 is_valid_cood = True # 标记当前行的坐标是否完全有效 # 这里只有前6列是 x, y, z, rx, ry, rz # 目前 从ui获取的 需要保存到数据库的 只有 x, y, z, rx, ry, rz # 故这里为 0到5 for col_idx in range(6): item = self.table.item(row_idx, col_idx) # 数据合法性和完整性判定(检查): 保证获取到的是有效数据 # 对于非法数据 和 不完整数据,不保存在数据库 # 1、判断填写的 x, y, z, rx, ry, rz中是否有为空的坐标 if not item or not item.text().strip(): is_valid_cood = False break # 跳过这一行数据,保存下一行数据 # 2、检查填写的坐标是否都为数字 coord_str = item.text().strip() try: coord_num = float(coord_str) # 尝试转换为数字 except ValueError: is_valid_cood = False break # 跳过这一行数据,保存下一行数据 # 保存单个坐标,浮点类型 row_data.append(coord_num) # 前面的坐标数据都有效,才保存到数据库 if is_valid_cood: # 新增:点位名字 # 增加点位名字的判断,当点位名 为空时,使用默认的 "normal" name_idx = 6 # 目前的点位名的列索引为6 (索引从0开始) name_item = self.table.item(row_idx, name_idx) if not name_item or not name_item.text().strip(): row_data.append("normal") else: pos_name = name_item.text().strip() row_data.append(pos_name) # 新增:点位状态 pos_state, pos_state的类型为字典 timestamp_idx = 7 # 目前的时间戳的列索引为7 timestamp_item = self.table.item(row_idx, timestamp_idx) timestamp = ( int(timestamp_item.text().strip()) if (timestamp_item and timestamp_item.text().strip()) else -1 # 表示没有进行状态编辑 ) # 从edited_state_dict取 点位状态 state_dict = ( self.edited_state_dict.get(timestamp, {}) if timestamp != -1 else {} ) # 点位状态的字典 row_data.append(state_dict) # 放入这一行的数据 行索引row_idx, x, y, z, rx, ry, rz, name, pos_state data_rows.append(row_data) # print("data_rows", data_rows) 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) name_idx = 6 # 点位名的列索引号 for col in range(self.table.columnCount()): item = ( QTableWidgetItem("") if col != name_idx else QTableWidgetItem("normal") ) 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: # x, y, z, rx, ry, rz 的索引为 0 到 5 for col in range(6): 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() # 初始化单元格 name_idx = 6 # 点位名的列索引号 for col in range(self.table.columnCount()): item = ( QTableWidgetItem("") if col != name_idx else QTableWidgetItem("normal") ) 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 # 选中行默认设置,主要是对 x, y, z, rx, ry, rz, 进行默认设置 self.update_table_data() # 发送选中行设置(更新)信号 self.update_table_signal.emit() # 重置点位 def resetSelectedRow(self): selectedRows = self.getSelectedRows() if not selectedRows: self.showNoSelectWarning("请先选中再重置!") return # x, y, z, rx, ry, rz 的索引为 0到5,故这里为6 # 只重置 x, y, z, rx, ry, rz cood_count = 6 for row in selectedRows: for col in range(cood_count): 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) # 坐标合法性检查 # 1、判断填写的 x, y, z, rx, ry, rz中是否为空 if not item or not item.text().strip(): self.status_label.setText( f"移动失败:第{row_idx+1}行有坐标为空,请检查!!!" ) return # 2、检查填写的坐标是否都为数字 coord_str = item.text().strip() try: coord_num = float(coord_str) # 尝试转换为数字 except ValueError: self.status_label.setText( f"移动失败:第{row_idx+1}行第{col_idx+1}列坐标不是有效数字,请检查!!!" ) return # 放入单个坐标(浮点类型),如 x row_coordinate.append(coord_num) # 放入这一行的坐标 # 最终结果:[[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数据 # 目前,一行的点位数据包含了 点位状态 pos_state_dict 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() if self.has_valid_copy: # 已经复制了有效的数据,可以进行粘贴操作 pasteAction = QAction("粘贴坐标到此行", self) pasteAction.triggered.connect(self.pasteRowData) menu.addAction(pasteAction) else: # 还没有进行复制,进行复制一行数据的操作 copyAction = QAction("复制此行坐标", self) copyAction.triggered.connect(self.copyRowData) menu.addAction(copyAction) menu.exec(self.table.mapToGlobal(position)) def copyRowData(self): """复制选中行数据到剪贴板 目前只复制选中行的 x, y, z, rx, ry, rz""" selectedRows = self.getSelectedRows() if not selectedRows: return # 目前只支持复制一行数据, # 选中多行,默认复制选中的多行中行数最小的那行 row = selectedRows[0] rowData = [] cood_count = 6 # # x, y, z, rx, ry, rz 的列索引为 0 到 5,故这里为6 for col in range(cood_count): 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}") # 更改复制状态 self.has_valid_copy = True def pasteRowData(self): """从剪贴板粘贴数据到选中行""" selectedRows = self.getSelectedRows() if not selectedRows: return # 支持粘贴到所有的选中行 for target_row in selectedRows: clipboard = QApplication.clipboard() data_str = clipboard.text().strip() if not data_str: self.status_label.setText("剪贴板为空,无法粘贴!!!") return pasted_data = [] for item in data_str.split(","): stripped_item = item.strip() try: pasted_data.append(float(stripped_item)) # 保存的格式为 float except ValueError: break # x, y, z, rx, ry, rz 的列索引为 0 到 5,故这里为6 # 此时一共需要粘贴六个坐标 cood_count = 6 # print(pasted_data) if len(pasted_data) != cood_count: self.status_label.setText( f"粘贴到第 {target_row + 1} 行失败, 复制的坐标{data_str}错误!!!" ) else: for col in range(cood_count): item = QTableWidgetItem(str(pasted_data[col])) item.setTextAlignment(Qt.AlignCenter) self.setThemeTextColor(item) self.table.setItem(target_row, col, item) # 提示粘贴结果(列出所有粘贴的行号) row_nums = [str(row + 1) for row in selectedRows] self.status_label.setText( f"已将坐标{pasted_data}粘贴到第{', '.join(row_nums)}行" ) # 粘贴后重置 复制状态,可以重新进行复制操作 self.has_valid_copy = False # 上移操作 def moveRowUp(self): """将选中行上移一行(仅支持单行选中)""" selectedRows = self.getSelectedRows() # 1. 校验:仅支持单行上移 if len(selectedRows) != 1: self.showNoSelectWarning("请仅选中一行进行上移操作!") return current_row = selectedRows[0] # total_rows = self.table.rowCount() # 2. 边界校验:第一行无法上移 if current_row == 0: self.status_label.setText("当前行为第一行,无法上移!!!") return # 3. 交换当前行与上一行的数据 target_row = current_row - 1 # 上移的目标行(当前行的上一行) self._swapTwoRows(current_row, target_row) # 4. 保持选中状态(选中移动后的行) self.table.selectRow(target_row) # 5. 更新状态提示 self.status_label.setText(f"第{current_row+1}行已上移至第{target_row+1}行") # 下移操作 def moveRowDown(self): """将选中行下移一行(仅支持单行选中)""" selectedRows = self.getSelectedRows() # 1. 校验:仅支持单行下移 if len(selectedRows) != 1: self.showNoSelectWarning("请仅选中一行进行下移操作!!") return current_row = selectedRows[0] total_rows = self.table.rowCount() # 2. 边界校验:最后一行无法下移 if current_row == total_rows - 1: self.status_label.setText("当前行为最后一行,无法下移!!!") return # 3. 交换当前行与下一行的数据 target_row = current_row + 1 # 下移的目标行(当前行的下一行) self._swapTwoRows(current_row, target_row) # 4. 保持选中状态(选中移动后的行) self.table.selectRow(target_row) # 5. 更新状态提示 self.status_label.setText(f"第{current_row+1}行已下移至第{target_row+1}行") # 状态编辑 def onStateEdit(self): selected_rows = self.getSelectedRows() # 目前只支持对一行进行状态编辑 if len(selected_rows) != 1: self.showNoSelectWarning("请仅选中一行进行状态编辑!!!") return # 需要进行状态编辑的行的 行索引 selected_row_idx = selected_rows[0] # 获取到该行的 点位名称,列索引为6 name_col_idx = 6 name_item = self.table.item(selected_row_idx, name_col_idx) # 此时的点位名 为空,则使用默认的 mormal,表示普通点 position_name = ( name_item.text().strip() if name_item and name_item.text().strip() else "normal" ) # print(position_name) # 状态编辑对话框 # 传入 点位名称, 选中行的行索引 (一行) status_diog = StatusEditDialog(position_name, selected_row_idx, self) # 获取该行的时间戳(唯一标识)[重要] # 时间戳的 列索引为7 timestamp_col_idx = 7 timestamp_item = self.table.item(selected_row_idx, timestamp_col_idx) # 如果存在时间戳,那么就需要读取相应的 点位状态,并设置到状态编辑界面 if timestamp_item and timestamp_item.text().strip(): timestamp = int(timestamp_item.text().strip()) status_diog.setPointStateValue(self.edited_state_dict[timestamp]) # 状态编辑界面点击应用按钮,保存状态编辑数据 status_diog.point_state_applied.connect(self.onSaveStateEdit) status_diog.exec() # 保存 编辑的 点位状态 def onSaveStateEdit(self, selected_row_idx: int, point_state: PointState): # 需要保存为 {唯一的值1 : 状态字典1, 唯一的值2 : 状态字典2, ...} # 唯一的值?时间戳作为唯一值 # 1、时间戳 unique_key = int(time.time() * 1000) # 毫秒级时间戳 self.edited_state_dict[unique_key] = point_state.to_dict() # 2. 设置当前行的隐藏列(列索引为7)为 当前时间戳 timestamp_idx = 7 # 当前,时间戳在表格中的索引为 7 (点位名称的后一个) timestamp_item = QTableWidgetItem(str(unique_key)) timestamp_item.setTextAlignment(Qt.AlignCenter) self.setThemeTextColor(timestamp_item) self.table.setItem(selected_row_idx, timestamp_idx, timestamp_item) # 3、同步点位的名称,显示到表格 name_idx = 6 # 当前,点位名称在表格中的索引为 6 name_item = QTableWidgetItem(str(point_state.pos_name)) name_item.setTextAlignment(Qt.AlignCenter) self.setThemeTextColor(name_item) self.table.setItem(selected_row_idx, name_idx, name_item) # print("onSaveStateEdit\n", self.edited_state_dict) # ================交换两行数据======================= def _swapTwoRows(self, row1, row2): """交换表格中两行的所有列数据(包括坐标和name列)""" total_cols = self.table.columnCount() # 8列(x/y/z/rx/ry/rz/name/timestamp) for col in range(total_cols): # 1. 取出两行当前列的item(takeItem会移除原位置item,避免引用冲突) # 行1的当前列item item1 = self.table.takeItem(row1, col) or QTableWidgetItem("") # 行2的当前列item item2 = self.table.takeItem(row2, col) or QTableWidgetItem("") # 2. 交换设置item(行1放行2的item,行2放行1的item) self.table.setItem(row1, col, item2) self.table.setItem(row2, col, item1) # 3. 确保交换后文本仍居中(保持样式一致性) self.table.item(row1, col).setTextAlignment(Qt.AlignCenter) self.table.item(row2, col).setTextAlignment(Qt.AlignCenter) # 4. 确保文本颜色符合主题(浅色/深色) self.setThemeTextColor(self.table.item(row1, col)) self.setThemeTextColor(self.table.item(row2, col)) 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() # 初始化样式,应用主题样式 # 后续使用 qss 文件 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.showStateWarning("有未完成的数据库读取操作,请稍后再试......") 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.showStateWarning) # 启动线程 self.read_thread.start() def showStateWarning(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 pass # 直接进行后续操作,显示一个空的列表,表示数据库为空 # 提取表单名(用于列表显示) 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) # ==========新增:右键可删除选中的表单 ====== list_widget.setContextMenuPolicy(Qt.CustomContextMenu) list_widget.customContextMenuRequested.connect( lambda position: self.showFormContextMenu( list_widget, position, all_form_id_name ) ) # 确认/取消按钮 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 showFormContextMenu(self, list_widget, position, all_form_id_name): """右键菜单:删除选中表单""" menu = QMenu() delete_action = QAction("删除选中表单", self) # 传递当前对话框实例,用于显示提示 delete_action.triggered.connect( lambda: self.deleteSelectedForm(list_widget, all_form_id_name) ) menu.addAction(delete_action) menu.exec(list_widget.mapToGlobal(position)) def deleteSelectedForm(self, list_widget, all_form_id_name): """删除选中的一个或多个表单(支持批量删除)""" selected_items = list_widget.selectedItems() if not selected_items: self.showStateWarning("请先选中要删除的表单!!") return # 1. 获取所有选中表单的 ID 和名称(支持批量) selected_forms = [ (all_form_id_name[i][0], all_form_id_name[i][1]) # (form_id, form_name) for i in range(len(all_form_id_name)) if list_widget.item(i).isSelected() ] if not selected_forms: return # 2. 显示确认对话框(列出所有要删除的表单名) form_names = ", ".join([name for (_, name) in selected_forms]) msg_box = QMessageBox() msg_box.setWindowTitle("确认删除") msg_box.setText(f"确定要删除以下表单吗?\n{form_names}") msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msg_box.setStyleSheet("QLabel{color: black;}") reply = msg_box.exec() if reply != QMessageBox.Yes: return try: # 3. 执行数据库删除(批量删除) db = FormDatabase("db/forms.db") db.init_database() for form_id, _ in selected_forms: db.delete_form(form_id) # 删除表单 db.commit_and_close() # 提交并关闭数据库连接 # 4. 刷新列表(重新读取数据库中的表单) db_reload = FormDatabase("db/forms.db") db_reload.init_database() new_all_form_id_name = db_reload.get_all_form_id_name() db_reload.commit_and_close() # 更新列表显示 list_widget.clear() list_widget.addItems([name for (_, name) in new_all_form_id_name]) # 更新状态提示 self.showStateWarning(f"已删除表单:{form_names}") except Exception as e: self.showStateWarning(f"删除失败:{str(e)}") # 由读取的数据生成新表单 def generateNewForms(self, form_data): """根据读取到的数据生成新表单(UI线程处理)""" # form_data格式:{form_name: [(row_idx, x, y, z, rx, ry, rz, name, pos_state_dict), ...], ...} # print("form_data", form_data) 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, name, pos_state_dict in data_rows: # print(row_idx, x, y, z, rx, ry, rz) # 如果数单行数超过表格行数,自动添加行 while row_idx >= new_form.table.rowCount(): new_form.addNewRow() # 这里获取一个唯一值作为 点位状态的标识 (就是前面的时间戳) unique_key = self.getPositionStateUniqueKey(row_idx) # 将获取到的点位状态 保存到 创建的新表单的edited_state_dict中 new_form.edited_state_dict[unique_key] = pos_state_dict # 填充单元格 new_form.table.setItem(row_idx, 0, QTableWidgetItem(str(x))) new_form.table.setItem(row_idx, 1, QTableWidgetItem(str(y))) new_form.table.setItem(row_idx, 2, QTableWidgetItem(str(z))) new_form.table.setItem(row_idx, 3, QTableWidgetItem(str(rx))) new_form.table.setItem(row_idx, 4, QTableWidgetItem(str(ry))) new_form.table.setItem(row_idx, 5, QTableWidgetItem(str(rz))) new_form.table.setItem(row_idx, 6, QTableWidgetItem(str(name))) # 时间戳(唯一标识) new_form.table.setItem(row_idx, 7, QTableWidgetItem(str(unique_key))) # 将填充的数据设置居中显示,一共是 0 到 6(如上),共7列 (不包括时间戳,时间戳不显示) for col_idx in range(7): new_form.table.item(row_idx, col_idx).setTextAlignment( Qt.AlignCenter ) # 将新表单添加到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) # 点状态唯一标识 def getPositionStateUniqueKey(self, row_idx): unique_key = int(time.time() * 1000) + row_idx return unique_key # 更新表单中选中行数据 x,y,z,rx,ry,rz # def test(form_obj: CoordinateTableWidget): # form_obj.update_table_data([666, 666, 666, 666, 666, 666]) # 移动 # def move_test(pos_list: list): # print("移动:", pos_list) # if __name__ == "__main__": # app = QApplication(sys.argv) # setTheme(Theme.DARK) # window = CoordinateFormsWidget() # window.form_update_signal.connect(test) # window.form_move_signal.connect(move_test) # window.show() # sys.exit(app.exec())