diff --git a/app/controller/mi_an/main_controller.py b/app/controller/mi_an/main_controller.py new file mode 100644 index 0000000..6f455fc --- /dev/null +++ b/app/controller/mi_an/main_controller.py @@ -0,0 +1,53 @@ +from ...view.mi_an_main_window import Window as MainWindow +from ...view.cood_forms_interface import CoordinateFormsWidget + +from typing import TypedDict + + +# 子界面管理 +class SubViewsDict(TypedDict): + position: CoordinateFormsWidget + + +class MainController: + def __init__(self): + # 主界面 + self.main_window = MainWindow() + + # 初始化子界面 + self._initSubViews() + + # 初始化子控制器 + self._initSubControllers() + + self.__connectSignals() + + def _initSubViews(self): + self.sub_views: SubViewsDict = { + "system": self.main_window.system, # 系统设置 + "product": self.main_window.product, # 生产界面 + "robot": self.main_window.robot, # 机器臂基础设置 + "io": self.main_window.io, # io面板 + "position": self.main_window.position.formsWidget, # 点位设置 + "basic": self.main_window.basic, # 基础设置 + "point": self.main_window.point, # 点位调试 + "other": self.main_window.other, # 其他设置 + "data": self.main_window.data, # 数据采集 + } + + def _initSubControllers(self): + # self.sub_controllers = { + # "system": SystemController(self.sub_views["system"]), + # "produce": ProduceController(self.sub_views["produce"]), + # } + pass + + def showMainWindow(self): + self.main_window.show() + + def __connectSignals(self): + pass + self.sub_views["position"].form_move_signal.connect(self.handleMove) + + def handleMove(self, pos_list: list): + print("handleMove 移动:", pos_list) diff --git a/app/view/cood_forms_interface.py b/app/view/cood_forms_interface.py index 3caf21e..50a3129 100644 --- a/app/view/cood_forms_interface.py +++ b/app/view/cood_forms_interface.py @@ -115,21 +115,52 @@ class FormDatabase: ) # 创建表单数据表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, - 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, + 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(单位:°) + 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) -- 确保一行数据只存一次 - ) + UNIQUE(form_id, row_index) + ) """ ) @@ -157,11 +188,13 @@ class FormDatabase: """连接数据库并获取游标""" 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() # 关闭连接 @@ -192,8 +225,9 @@ class FormDatabase: 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,)) @@ -201,6 +235,19 @@ class FormDatabase: 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: @@ -219,9 +266,19 @@ class FormDatabase: # 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]), + (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], + ), ) except Exception as exc: self.rollback_and_close() @@ -257,6 +314,22 @@ class FormDatabase: 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 等同于线名 @@ -385,7 +458,7 @@ class DatabaseReadThread(QThread): 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) + data_rows = db.get_row_idx_coordinate_and_name(form_id) form_date_dict[form_name] = data_rows self.form_data_signal.emit(form_date_dict) @@ -423,6 +496,7 @@ class CoordinateTableWidget(QWidget): self.form_name = form_name self.hideVHeader = hideVHeader # 是否隐藏行名 self.initRowCount = initRowCount # 是否需要初始的行数 + self.has_valid_copy = False # 标志是否进行了有效的复制(复制了一行的数据) self.setObjectName("CoordinateTableWidget") # 主布局 @@ -462,9 +536,9 @@ class CoordinateTableWidget(QWidget): if self.initRowCount: self.table.setRowCount(5) # 表的行数 - self.table.setColumnCount(6) # 表的列数 + self.table.setColumnCount(7) # 表的列数 - self.table.setHorizontalHeaderLabels(["x", "y", "z", "rx", "ry", "rz"]) + self.table.setHorizontalHeaderLabels(["x", "y", "z", "rx", "ry", "rz", "name"]) self.table.horizontalHeader().setStyleSheet( "QHeaderView::section {font-size: 19px; font-weight: bold;}" ) @@ -475,7 +549,7 @@ class CoordinateTableWidget(QWidget): if self.hideVHeader: self.table.verticalHeader().hide() # 隐藏行标题(行名) - for i in range(6): + for i in range(7): self.table.horizontalHeader().setSectionResizeMode( i, self.table.horizontalHeader().ResizeMode.Stretch ) @@ -507,8 +581,8 @@ class CoordinateTableWidget(QWidget): self.resetBtn.clicked.connect(self.resetSelectedRow) btnLayout.addWidget(self.resetBtn) - # 移动按钮 - self.readBtn = PushButton("移动") + # 机器臂(人)移动按钮 + self.readBtn = PushButton("机器移动") self.readBtn.setIcon(FIF.RIGHT_ARROW.icon()) # FOLDER self.readBtn.clicked.connect(self.moveToSelectedCoodinate) btnLayout.addWidget(self.readBtn) @@ -519,11 +593,30 @@ class CoordinateTableWidget(QWidget): self.saveBtn.clicked.connect(self.saveToDatabase) # 关联保存函数 btnLayout.addWidget(self.saveBtn) - self.mainLayout.addLayout(btnLayout) + 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.moveRowDown) + btnLayout2.addWidget(self.stateEditBtn) + + self.mainLayout.addLayout(btnLayout) # 添加 按钮布局一 + self.mainLayout.addLayout(btnLayout2) # 添加 按钮布局二 # 数据保存到数据库,获取数据时调用 def get_ui_data(self): - """从表格UI中获取数据 [[行索引row_idx, x, y, z, rx, ry, rz], ......]""" + """从表格UI中获取数据 [[行索引row_idx, x, y, z, rx, ry, rz, name], ......]""" row_count = self.table.rowCount() # column_count = self.table.columnCount() data_rows = [] @@ -531,7 +624,7 @@ class CoordinateTableWidget(QWidget): row_data = [row_idx] # 先保存行索引 is_valid_row = True # 标记当前行是否完全有效 - # 这里只有前6列是 x, y, rx, ry, rz + # 这里只有前6列是 x, y, z, rx, ry, rz # 目前 从ui获取的 需要保存到数据库的 只有 x, y, z, rx, ry, rz # 故这里为 0到5 for col_idx in range(6): @@ -555,10 +648,21 @@ class CoordinateTableWidget(QWidget): # 保存单个坐标,浮点类型 row_data.append(coord_num) - # 放入这一行的数据 行索引row_idx, x, y, z, rx, ry, rz + # 新增:点位名字 + # 增加点位名字的判断,当点位名 为空时,使用默认的 "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) + + # 放入这一行的数据 行索引row_idx, x, y, z, rx, ry, rz, name if is_valid_row: # 有效,才保存到数据库 data_rows.append(row_data) + # print("data_rows", data_rows) return data_rows def showConfirmDialog(self): @@ -618,8 +722,13 @@ class CoordinateTableWidget(QWidget): self.setThemeTextColor(header_item) self.table.setVerticalHeaderItem(row, header_item) + name_idx = 6 # 点位名的列索引号 for col in range(self.table.columnCount()): - item = QTableWidgetItem("") + item = ( + QTableWidgetItem("") + if col != name_idx + else QTableWidgetItem("normal") + ) item.setTextAlignment(Qt.AlignCenter) self.setThemeTextColor(item) self.table.setItem(row, col, item) @@ -639,7 +748,8 @@ class CoordinateTableWidget(QWidget): def update_table_data(self, positionList=None): selectedRows = self.getSelectedRows() for row in selectedRows: - for col in range(self.table.columnCount()): + # 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)) @@ -650,10 +760,10 @@ class CoordinateTableWidget(QWidget): self.table.setItem(row, col, item) if positionList: - self.status_label.setText(f"已将选中行设置为{positionList}") + self.status_label.setText(f"已将选中行点位设置为{positionList}") else: self.status_label.setText( - "已将选中行设置为[-9999, -9999, -9999, -9999, -9999, -9999]" + "已将选中行点位设置为[-9999, -9999, -9999, -9999, -9999, -9999]" ) """ @@ -675,8 +785,11 @@ class CoordinateTableWidget(QWidget): self.updateRowHeaders() # 初始化单元格 + name_idx = 6 # 点位名的列索引号 for col in range(self.table.columnCount()): - item = QTableWidgetItem("") + item = ( + QTableWidgetItem("") if col != name_idx else QTableWidgetItem("normal") + ) item.setTextAlignment(Qt.AlignCenter) self.setThemeTextColor(item) self.table.setItem(insert_pos, col, item) @@ -711,7 +824,7 @@ class CoordinateTableWidget(QWidget): self.showNoSelectWarning("请先选中再设置点位!") return - # 选中行默认设置 + # 选中行默认设置,主要是对 x, y, z, rx, ry, rz, 进行默认设置 self.update_table_data() # 发送选中行设置(更新)信号 @@ -724,16 +837,19 @@ class CoordinateTableWidget(QWidget): 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(self.table.columnCount()): + 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("已重置选中行") + self.status_label.setText("已重置选中行坐标点位") - # 移动 + # 移动 (只进行移动操作) def moveToSelectedCoodinate(self): selectedRows = self.getSelectedRows() if not selectedRows: @@ -807,13 +923,21 @@ class CoordinateTableWidget(QWidget): return menu = QMenu() - copyAction = QAction("复制此行数据", self) - copyAction.triggered.connect(self.copyRowData) - menu.addAction(copyAction) + + 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 @@ -822,14 +946,141 @@ class CoordinateTableWidget(QWidget): # 选中多行,默认复制选中的多行中行数最小的那行 row = selectedRows[0] rowData = [] - for col in range(self.table.columnCount()): + 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.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 _swapTwoRows(self, row1, row2): + """交换表格中两行的所有列数据(包括坐标和name列)""" + total_cols = self.table.columnCount() # 7列(x/y/z/rx/ry/rz/name) + + 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())) @@ -1066,6 +1317,7 @@ class CoordinateFormsWidget(QWidget): if isinstance(sender_form, CoordinateTableWidget): self.form_update_signal.emit(sender_form) + # 读取数据 def readFromDatabase(self): """启动读取线程,处理表单选择和数据生成""" # 检查是否已有运行中的读取线程 @@ -1074,7 +1326,7 @@ class CoordinateFormsWidget(QWidget): and self.read_thread and self.read_thread.isRunning() ): - # self.status_label.setText("有未完成的数据库读取操作,请稍后再试......") + self.showStateWarning("有未完成的数据库读取操作,请稍后再试......") return # 创建读取线程 @@ -1086,12 +1338,12 @@ class CoordinateFormsWidget(QWidget): # 连接信号:接收表单数据,生成新表单 self.read_thread.form_data_signal.connect(self.generateNewForms) # 连接信号:状态提示 (数据库错误等) - self.read_thread.state_signal.connect(self.showDatabaseReadWarning) + self.read_thread.state_signal.connect(self.showStateWarning) # 启动线程 self.read_thread.start() - def showDatabaseReadWarning(self, msg): + def showStateWarning(self, msg): msg_box = QMessageBox(self) msg_box.setIcon(QMessageBox.Warning) msg_box.setWindowTitle("提示") @@ -1101,11 +1353,12 @@ class CoordinateFormsWidget(QWidget): msg_box.exec() def showFormSelection(self, all_form_id_name): - """显示多选表单选择页面 (UI线程处理)""" + """显示多选表单选择页面 (UI线程处理), 新增右键可删除表单""" if not all_form_id_name: # self.status_label.setText("数据库为空, 没有任何表单") - self.read_thread.set_selected_forms(list()) - return + # self.read_thread.set_selected_forms(list()) + # return + pass # 直接进行后续操作,显示一个空的列表,表示数据库为空 # 提取表单名(用于列表显示) form_names = [name for (_, name) in all_form_id_name] @@ -1138,6 +1391,14 @@ class CoordinateFormsWidget(QWidget): 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("确认") @@ -1185,10 +1446,72 @@ class CoordinateFormsWidget(QWidget): # 显示表单选择框 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), ...], ...} + # form_data格式:{form_name: [(row_idx, x, y, z, rx, ry, rz, name), ...], ...} + # print("form_data", form_data) for form_name, data_rows in form_data.items(): # 创建新表单 self.formTotalCnt += 1 # 创建的总的表单数加一 @@ -1198,19 +1521,28 @@ class CoordinateFormsWidget(QWidget): 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: + # 填充数据到新表单(包含点位名) + for row_idx, x, y, z, rx, ry, rz, name in data_rows: + # print(row_idx, x, y, z, rx, ry, rz) # 如果数单行数超过表格行数,自动添加行 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)) + 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))) + + # 将填充的数据设置居中显示,一共是 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( @@ -1224,12 +1556,14 @@ class CoordinateFormsWidget(QWidget): # 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()) diff --git a/app/view/mi_an_main_window.py b/app/view/mi_an_main_window.py new file mode 100644 index 0000000..1a2dc0f --- /dev/null +++ b/app/view/mi_an_main_window.py @@ -0,0 +1,238 @@ +# coding:utf-8 +import sys +import os +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 FluentIcon as FIF +from qframelesswindow import FramelessWindow, StandardTitleBar + +from app.view.system_interface import SystemInterface +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): + super().__init__(parent=parent) + self.label = QLabel(text, self) + self.label.setAlignment(Qt.AlignCenter) + self.hBoxLayout = QHBoxLayout(self) + self.hBoxLayout.addWidget(self.label, 1, Qt.AlignCenter) + self.setObjectName(text.replace(" ", "-")) + + +class Window(FramelessWindow): + ## 定义信号:调整高度 + heightChanged = Signal(int) + + def __init__(self): + super().__init__() + self.setTitleBar(StandardTitleBar(self)) + + # use dark theme mode + # setTheme(Theme.DARK) + + # change the theme color + # setThemeColor('#0078d4') + + self.hBoxLayout = QHBoxLayout(self) + self.navigationInterface = NavigationInterface(self, showMenuButton=True) + 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 = CoodFormsInterface(self) # 位置设定 + self.basic = Widget("基础设置", self) # 需要完成 + self.point = Widget("点位调试", self) + self.other = Widget("其他设置", self) + self.data = DataInterface(self) # 数据采集 + + # initialize layout + self.initLayout() + + # add items to navigation interface + self.initNavigation() + + self.initWindow() + + def initLayout(self): + self.hBoxLayout.setSpacing(0) + self.hBoxLayout.setContentsMargins(0, self.titleBar.height(), 0, 0) + self.hBoxLayout.addWidget(self.navigationInterface) + self.hBoxLayout.addWidget(self.stackWidget) + self.hBoxLayout.setStretchFactor(self.stackWidget, 1) + + def initNavigation(self): + # enable acrylic effect + # 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 + ) + # add navigation items to scroll area + # for i in range(1, 21): + # self.navigationInterface.addItem( + # f'folder{i}', + # FIF.FOLDER, + # f'Folder {i}', + # lambda: print('Folder clicked'), + # position=NavigationItemPosition.SCROLL + # ) + + # add custom widget to bottom + self.navigationInterface.addWidget( + routeKey="avatar", + widget=NavigationAvatarWidget("用户设置", "resource/shoko.png"), + onClick=self.showMessageBox, + position=NavigationItemPosition.BOTTOM, + ) + + #!IMPORTANT: don't forget to set the default route key if you enable the return button + # qrouter.setDefaultRouteKey(self.stackWidget, self.musicInterface.objectName()) + + # set the maximum width + self.navigationInterface.setExpandWidth(220) + + self.stackWidget.currentChanged.connect(self.onCurrentInterfaceChanged) + self.stackWidget.setCurrentIndex(1) + + # always expand + self.navigationInterface.setCollapsible(False) + + def initWindow(self): + self.resize(900, 700) + 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) + + # NOTE: set the minimum window width that allows the navigation panel to be expanded + self.navigationInterface.setMinimumExpandWidth(900) + self.navigationInterface.expand(useAni=True) + + self.setQss() + + 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(), + icon=icon, + text=text, + onClick=lambda: self.switchTo(interface), + position=position, + tooltip=text, + 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: + self.setStyleSheet(f.read()) + + def switchTo(self, widget): + self.stackWidget.setCurrentWidget(widget) + + def onCurrentInterfaceChanged(self, index): + widget = self.stackWidget.widget(index) + self.navigationInterface.setCurrentItem(widget.objectName()) + + #!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, + ) + w.yesButton.setText("来啦老弟") + w.cancelButton.setText("下次一定") + + if w.exec(): + QDesktopServices.openUrl(QUrl("https://afdian.net/a/zhiyiYo")) + + +def move_test(pos_list: list): + print("移动:", pos_list) + + +# if __name__ == "__main__": +# app = QApplication([]) +# w = Window() +# w.position.formsWidget.form_move_signal.connect(move_test) +# w.show() +# app.exec() diff --git a/mi_an_main.py b/mi_an_main.py new file mode 100644 index 0000000..cbbbde7 --- /dev/null +++ b/mi_an_main.py @@ -0,0 +1,8 @@ +from PySide6.QtWidgets import QApplication +from app.controller.mi_an.main_controller import MainController + +if __name__ == "__main__": + app = QApplication([]) + controller = MainController() + controller.showMainWindow() + app.exec()