From b90395ea244442f60314e35d6537c8e54b95404d Mon Sep 17 00:00:00 2001 From: yanganjie Date: Wed, 10 Sep 2025 20:27:51 +0800 Subject: [PATCH] =?UTF-8?q?=E7=82=B9=E4=BD=8D=E8=AE=BE=E7=BD=AE=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E5=A2=9E=E5=8A=A0=E4=BA=86=E7=8A=B6=E6=80=81=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=EF=BC=8C=E4=BB=8E=E6=95=B0=E6=8D=AE=E5=BA=93=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/model/point_state.py | 128 +++++++++ app/view/cood_forms_interface.py | 322 +++++++++++++++++++--- app/view/mi_an/status_edit_dialog.py | 382 +++++++++++++++++++++++++++ 3 files changed, 789 insertions(+), 43 deletions(-) create mode 100644 app/model/point_state.py create mode 100644 app/view/mi_an/status_edit_dialog.py diff --git a/app/model/point_state.py b/app/model/point_state.py new file mode 100644 index 0000000..da6d06e --- /dev/null +++ b/app/model/point_state.py @@ -0,0 +1,128 @@ +""" +包括一个点位需要的状态: +1、点位名称 +2、速度 +3、工具id +4、工件id +5、关节坐标 J1-J6 +6、运动类型 +7、平滑时间 +""" +class PointState: + VALID_SPEED_RANGE = (0, 100) + VALID_TOOL_WORK_ID_RANGE = (0, 14) + VALID_JOINT_COUNT = 6 + VALID_JOINT_RANGE = (-180, 180) + VALID_MOTION_TYPES = ["直线", "曲线中间点", "曲线终点", "自由路径"] + VALID_BLEND_TIME_RANGE = (0, 500) + + def __init__(self, pos_name, speed, tool_id, work_id, joint_values, motion_type, blend_time): + # 数据合法性判断 + self.pos_name = self._validate_pos_name(pos_name) + self.speed = self._validate_speed(speed) + self.tool_id = self._validate_tool_work_id(tool_id, "工具ID") + self.work_id = self._validate_tool_work_id(work_id, "工件ID") + self.joint_values = self._validate_joint_values(joint_values) + self.motion_type = self._validate_motion_type(motion_type) + self.blend_time = self._validate_blend_time(blend_time) + + def _validate_pos_name(self, pos_name): + """校验点位名称(非空)""" + if not isinstance(pos_name, str): + raise TypeError("点位名称必须是字符串类型") + stripped_name = pos_name.strip() + if not stripped_name: + raise ValueError("点位名称不能为空或仅包含空格") + return stripped_name + + def _validate_speed(self, speed): + """校验速度(0-100范围)""" + if not isinstance(speed, (int, float)): + raise TypeError(f"速度必须是数字类型,当前类型:{type(speed).__name__}") + min_val, max_val = self.VALID_SPEED_RANGE + if not (min_val <= speed <= max_val): + raise ValueError(f"速度必须在 {min_val}-{max_val} 之间,当前值:{speed}") + return speed + + def _validate_tool_work_id(self, value, field_name): + """校验工具ID/工件ID(0-14范围,整数)""" + if not isinstance(value, int): + raise TypeError(f"{field_name}必须是整数类型,当前类型:{type(value).__name__}") + min_val, max_val = self.VALID_TOOL_WORK_ID_RANGE + if not (min_val <= value <= max_val): + raise ValueError(f"{field_name}必须在 {min_val}-{max_val} 之间,当前值:{value}") + return value + + def _validate_joint_values(self, joint_values): + """校验关节值(6个元素,每个在-180~180范围)""" + if not isinstance(joint_values, list): + raise TypeError(f"关节值必须是列表类型,当前类型:{type(joint_values).__name__}") + if len(joint_values) != self.VALID_JOINT_COUNT: + raise ValueError( + f"关节值必须包含 {self.VALID_JOINT_COUNT} 个元素(J1-J6)," + f"当前数量:{len(joint_values)}" + ) + + # 逐个校验关节值 + validated_joints = [] + for i, val in enumerate(joint_values, 1): + if not isinstance(val, (int, float)): + raise TypeError(f"J{i}关节值必须是数字类型,当前类型:{type(val).__name__}") + + min_val, max_val = self.VALID_JOINT_RANGE + if not (min_val <= val <= max_val): + raise ValueError( + f"J{i}关节值必须在 {min_val}~{max_val} 之间,当前值:{val}" + ) + validated_joints.append(val) + + return validated_joints + + def _validate_motion_type(self, motion_type): + """校验运动类型(必须是预定义的选项)""" + if not isinstance(motion_type, str): + raise TypeError(f"运动类型必须是字符串类型,当前类型:{type(motion_type).__name__}") + if motion_type not in self.VALID_MOTION_TYPES: + raise ValueError( + f"运动类型必须是以下之一:{self.VALID_MOTION_TYPES}," + f"当前值:{motion_type}" + ) + return motion_type + + def _validate_blend_time(self, blend_time): + """校验平滑时间(-1表示停止,否则0-500范围)""" + if not isinstance(blend_time, (int, float)): + raise TypeError(f"平滑时间必须是数字类型,当前类型:{type(blend_time).__name__}") + + # 停止模式(-1)或正常平滑时间(0-500) + if blend_time == -1: + return -1 + + min_val, max_val = self.VALID_BLEND_TIME_RANGE + if not (min_val <= blend_time <= max_val): + raise ValueError( + f"平滑时间必须是-1(停止)或 {min_val}-{max_val} 之间(毫秒)," + f"当前值:{blend_time}" + ) + return blend_time + + def to_dict(self): + """转换为字典,方便序列化或存储""" + return { + "pos_name": self.pos_name, + "speed": self.speed, + "tool_id": self.tool_id, + "work_id": self.work_id, + "joint_values": self.joint_values, + "motion_type": self.motion_type, + "blend_time": self.blend_time + } + + def __str__(self): + """打印调试""" + return ( + f"点位名:{self.pos_name},速度:{self.speed}%\n" + f"工具ID: {self.tool_id},工件ID: {self.work_id}\n" + f"关节值:{self.joint_values}\n" + f"运动类型:{self.motion_type},平滑时间:{self.blend_time}" + ) diff --git a/app/view/cood_forms_interface.py b/app/view/cood_forms_interface.py index 50a3129..1917629 100644 --- a/app/view/cood_forms_interface.py +++ b/app/view/cood_forms_interface.py @@ -1,6 +1,7 @@ # coding:utf-8 import sys import os +import time from PySide6.QtCore import ( Qt, QSize, @@ -55,6 +56,9 @@ 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""" @@ -155,6 +159,8 @@ class FormDatabase: 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 @@ -259,27 +265,139 @@ class FormDatabase: 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] - 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], - ), - ) + # 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 @@ -354,7 +472,7 @@ class DatabaseSaveThread(QThread): super().__init__() self.db_path = db_path self.form_name = form_name - self.data_rows = data_rows # 主线程传递过来的UI数据 + self.data_rows = data_rows # UI线程传递过来的UI数据 self.user_choice = None # 存储用户选择(True=覆盖,False=取消) self.mutex = QMutex() self.condition = QWaitCondition() @@ -383,12 +501,15 @@ class DatabaseSaveThread(QThread): # 用户选择覆盖:删除旧数据 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}」已保存") @@ -418,11 +539,11 @@ class DatabaseSaveThread(QThread): # 数据库读取操作线程 class DatabaseReadThread(QThread): - # 信号1: 从form_info 读取所有的 id-name 发送 + # 信号1: 从form_info表 读取所有的 id-name 发送 # [(form_id1, form_name1),...] form_id_name_signal = Signal(list) - # 信号2: 从 from_data 读取所有的 选择的表单的数据发送 + # 信号2: 从 from_data表 读取所有的 选择的表单的数据发送 # {form_name1: data_rows1, ...} form_data_signal = Signal(dict) @@ -458,7 +579,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_and_name(form_id) + data_rows = db.get_form_data(form_id) form_date_dict[form_name] = data_rows self.form_data_signal.emit(form_date_dict) @@ -499,6 +620,12 @@ class CoordinateTableWidget(QWidget): 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) @@ -536,9 +663,12 @@ class CoordinateTableWidget(QWidget): if self.initRowCount: self.table.setRowCount(5) # 表的行数 - self.table.setColumnCount(7) # 表的列数 + # 新增第八列保存 毫秒级时间戳 + self.table.setColumnCount(8) # 表的列数 - self.table.setHorizontalHeaderLabels(["x", "y", "z", "rx", "ry", "rz", "name"]) + self.table.setHorizontalHeaderLabels( + ["x", "y", "z", "rx", "ry", "rz", "name", "timestamp"] + ) self.table.horizontalHeader().setStyleSheet( "QHeaderView::section {font-size: 19px; font-weight: bold;}" ) @@ -546,6 +676,9 @@ class CoordinateTableWidget(QWidget): "QHeaderView::section {font-size: 14px; font-weight: bold;}" ) + # 第八列(索引7)不显示 (通常不需要显示时间戳[唯一标识]) + self.table.setColumnHidden(7, True) + if self.hideVHeader: self.table.verticalHeader().hide() # 隐藏行标题(行名) @@ -608,7 +741,7 @@ class CoordinateTableWidget(QWidget): # 状态编辑按钮 self.stateEditBtn = PushButton("状态编辑") - # self.stateEditBtn.clicked.connect(self.moveRowDown) + self.stateEditBtn.clicked.connect(self.onStateEdit) btnLayout2.addWidget(self.stateEditBtn) self.mainLayout.addLayout(btnLayout) # 添加 按钮布局一 @@ -616,13 +749,14 @@ class CoordinateTableWidget(QWidget): # 数据保存到数据库,获取数据时调用 def get_ui_data(self): - """从表格UI中获取数据 [[行索引row_idx, x, y, z, rx, ry, rz, name], ......]""" + """获取数据 [[行索引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_row = True # 标记当前行是否完全有效 + is_valid_cood = True # 标记当前行的坐标是否完全有效 # 这里只有前6列是 x, y, z, rx, ry, rz # 目前 从ui获取的 需要保存到数据库的 只有 x, y, z, rx, ry, rz @@ -634,7 +768,7 @@ class CoordinateTableWidget(QWidget): # 对于非法数据 和 不完整数据,不保存在数据库 # 1、判断填写的 x, y, z, rx, ry, rz中是否有为空的坐标 if not item or not item.text().strip(): - is_valid_row = False + is_valid_cood = False break # 跳过这一行数据,保存下一行数据 # 2、检查填写的坐标是否都为数字 @@ -642,24 +776,40 @@ class CoordinateTableWidget(QWidget): try: coord_num = float(coord_str) # 尝试转换为数字 except ValueError: - is_valid_row = False + is_valid_cood = False break # 跳过这一行数据,保存下一行数据 # 保存单个坐标,浮点类型 row_data.append(coord_num) - # 新增:点位名字 - # 增加点位名字的判断,当点位名 为空时,使用默认的 "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) + # 前面的坐标数据都有效,才保存到数据库 + 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) - # 放入这一行的数据 行索引row_idx, x, y, z, rx, ry, rz, name - if is_valid_row: # 有效,才保存到数据库 + # 新增:点位状态 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) @@ -723,12 +873,14 @@ class CoordinateTableWidget(QWidget): 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) @@ -745,6 +897,7 @@ class CoordinateTableWidget(QWidget): msg_box.setStyleSheet("QLabel{color: black;}") msg_box.exec() + # 外界用于设置坐标的接口 def update_table_data(self, positionList=None): selectedRows = self.getSelectedRows() for row in selectedRows: @@ -899,6 +1052,7 @@ class CoordinateTableWidget(QWidget): return # 获取UI数据 + # 目前,一行的点位数据包含了 点位状态 pos_state_dict data_rows = self.get_ui_data() if not data_rows: self.status_label.setText("没有数据可以保存, 请检查数据有效性!!!") @@ -1059,10 +1213,77 @@ class CoordinateTableWidget(QWidget): # 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() # 7列(x/y/z/rx/ry/rz/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,避免引用冲突) @@ -1153,6 +1374,7 @@ class CoordinateFormsWidget(QWidget): self.initFirstForm() # 初始化样式,应用主题样式 + # 后续使用 qss 文件 self.applyThemeStyle() def applyThemeStyle(self): @@ -1510,7 +1732,7 @@ class CoordinateFormsWidget(QWidget): # 由读取的数据生成新表单 def generateNewForms(self, form_data): """根据读取到的数据生成新表单(UI线程处理)""" - # form_data格式:{form_name: [(row_idx, x, y, z, rx, ry, rz, name), ...], ...} + # 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(): # 创建新表单 @@ -1522,12 +1744,18 @@ class CoordinateFormsWidget(QWidget): new_form.move_to_coodinate_signal.connect(self.form_move_signal) # 填充数据到新表单(包含点位名) - for row_idx, x, y, z, rx, ry, rz, name in data_rows: + 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))) @@ -1537,7 +1765,10 @@ class CoordinateFormsWidget(QWidget): new_form.table.setItem(row_idx, 5, QTableWidgetItem(str(rz))) new_form.table.setItem(row_idx, 6, QTableWidgetItem(str(name))) - # 将填充的数据设置居中显示,一共是 0 到 6(如上),共7列 + # 时间戳(唯一标识) + 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 @@ -1550,6 +1781,11 @@ class CoordinateFormsWidget(QWidget): ) 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): diff --git a/app/view/mi_an/status_edit_dialog.py b/app/view/mi_an/status_edit_dialog.py new file mode 100644 index 0000000..88c60cb --- /dev/null +++ b/app/view/mi_an/status_edit_dialog.py @@ -0,0 +1,382 @@ +from PySide6.QtCore import Qt, Signal +from PySide6.QtWidgets import ( + QApplication, + QWidget, + QVBoxLayout, + QHBoxLayout, + QFormLayout, + QGroupBox, + QDialog, +) +from qfluentwidgets import ( + PushButton, + ComboBox, + DoubleSpinBox, + SpinBox, + setTheme, + Theme, + StrongBodyLabel, + EditableComboBox, + RadioButton, + isDarkTheme, + MessageBox, +) +from ...model.point_state import PointState + + +class StatusEditDialog(QDialog): + point_state_applied = Signal(int, PointState) + + def __init__(self, pos_name, selected_row_idx, parent=None): + super().__init__(parent) + # 窗口基本设置 + self.setWindowTitle("状态编辑") # 设置窗口标题 + self.resize(600, 660) # 窗口大小 + self.setMinimumSize(550, 560) # 最小尺寸限制 + + # 点位名称 + self.pos_name = pos_name + + # 选中行的行索引 + self.selected_row_idx = selected_row_idx + + self.__initWidget() + + def __initWidget(self): + # 创建控件 + self.__createWidget() + + # 设置样式 + self.__initStyles() + + # 设置布局 + self.__initLayout() + + # 绑定 + self.__bind() + + # 创建相关控件 + def __createWidget(self): + # 1. 点位名称输入 + self.name_combo = EditableComboBox() + + self.name_combo.addItems( + ["抓取点", "破袋点", "震动点", "扔袋点", "相机/待抓点"] + ) + + # 检查点位名称在下拉框是否已经存在 + target_pos_name = self.pos_name + pos_name_index = self.name_combo.findText(target_pos_name) + # 若未找到(索引=-1),则添加 表单中 的点位名字 + if pos_name_index == -1: + self.name_combo.addItem(self.pos_name) + # 选中新添加的这项 + new_index = self.name_combo.count() - 1 + self.name_combo.setCurrentIndex(new_index) + else: + # 已经存在的话就直接选中 + self.name_combo.setCurrentIndex(pos_name_index) + + self.name_combo.setPlaceholderText("请设置点位名称") + + # 2. 工具坐标系id + self.tool_coord_spin = SpinBox() + self.tool_coord_spin.setRange(0, 99) # 0-99范围 + self.tool_coord_spin.setValue(0) # 默认值 + self.tool_coord_btn = PushButton("获取当前工具坐标id") + + # 3. 工件坐标系id + self.work_coord_spin = SpinBox() + self.work_coord_spin.setRange(0, 99) # 0-99范围 + self.work_coord_spin.setValue(0) # 默认值 + self.work_coord_btn = PushButton("获取当前工件坐标id") + + # 4-9. 关节坐标 J1 到 J6 + self.j_spins = [] + for _ in range(6): + spin = DoubleSpinBox() + spin.setRange(-180, 180) # 角度范围 (-180度到180度) + spin.setDecimals(3) # 保留3位小数 + spin.setSingleStep(0.001) # 默认步长 + self.j_spins.append(spin) + + # 关节坐标默认值 (默认为无效值) + self.j_spins[0].setValue(-9999) + self.j_spins[1].setValue(-9999) + self.j_spins[2].setValue(-9999) + self.j_spins[3].setValue(-9999) + self.j_spins[4].setValue(-9999) + self.j_spins[5].setValue(-9999) + + # 关节坐标设置 右侧的步长设置 和 获取关节坐标按钮 + self.step_group = QGroupBox("单击步长设置") + self.step_input = DoubleSpinBox() + self.step_input.setRange(0.001, 180.0) # 步长范围 + self.step_input.setDecimals(3) # 保留3位小数 + self.step_input.setValue(0.001) # 默认步长 + self.get_values_btn = PushButton("获取当前J1-J6值") + + # 10. 速度 (移动速度) + self.approach_speed_spin = DoubleSpinBox() + self.approach_speed_spin.setRange(0, 100) + self.approach_speed_spin.setDecimals(0) # 小数点 + self.approach_speed_spin.setValue(20) + self.approach_speed_spin.setSingleStep(10) + + # 11. 运动类型(下拉选择) + self.motion_type_combo = ComboBox() + self.motion_type_combo.addItems(["直线", "曲线中间点", "曲线终点", "自由路径"]) + + # 12. 平滑选择 + self.stop_radio = RadioButton("停止") + self.smooth_radio = RadioButton("平滑过渡") + self.smooth_ms_spin = DoubleSpinBox() # 平滑过渡的时间(毫秒) + self.smooth_ms_spin.setRange(0, 500) # 范围:0 - 500 ms + self.smooth_ms_spin.setDecimals(0) # 整数毫秒 + self.smooth_ms_spin.setValue(0) # 默认值0 + self.smooth_ms_spin.setSingleStep(10) # 步长:10毫秒 + self.smooth_ms_spin.setEnabled(False) # 初始禁用(仅“平滑过渡”选中时启用) + self.stop_radio.setChecked(True) # 默认选“停止” + + # 13. 应用按钮 + self.apply_btn = PushButton("应用") + self.apply_btn.setMinimumWidth(160) # 按钮最小宽度 + + def __initStyles(self): + # 根据主题设置样式表 + if isDarkTheme(): # 深色主题 + self.step_group.setStyleSheet( + """ + QGroupBox { + color: white; /* 标题文字颜色 */ + border: 1px solid white; /* 边框线条颜色和宽度 */ + border-radius: 6px; /* 边框圆角 */ + margin-top: 10px; /* 标题与边框的距离 */ + } + QGroupBox::title { + subcontrol-origin: margin; + subcontrol-position: top left; /* 标题位置 */ + left: 10px; /* 标题左边距 */ + padding: 0 3px 0 3px; /* 标题内边距 */ + } + """ + ) + self.setStyleSheet("background-color: rgb(32, 32, 32);") + else: # 浅色主题 + self.step_group.setStyleSheet( + """ + QGroupBox { + color: black; /* 标题文字颜色 */ + border: 1px solid black; /* 边框线条颜色和宽度 */ + border-radius: 6px; /* 边框圆角 */ + margin-top: 10px; /* 标题与边框的距离 */ + } + QGroupBox::title { + subcontrol-origin: margin; + subcontrol-position: top left; /* 标题位置 */ + left: 10px; /* 标题左边距 */ + padding: 0 3px 0 3px; /* 标题内边距 */ + } + """ + ) + self.setStyleSheet("background-color: rgb(243, 243, 243);") + + def __initLayout(self): + # 主布局(直接应用于当前Widget) + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(24, 24, 24, 24) # 边距 + main_layout.setSpacing(16) # 控件间距 + + # 表单布局(管理标签和输入框) + form_layout = QFormLayout() + form_layout.setRowWrapPolicy(QFormLayout.DontWrapRows) # 不自动换行 + # 标签右对齐+垂直居中 + form_layout.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter) + form_layout.setSpacing(12) # 表单行间距 + + # 1. 添加点位名称布局 + form_layout.addRow(StrongBodyLabel("点位名称"), self.name_combo) + + # 2. 添加工具坐标布局 + tool_coord_layout = QHBoxLayout() + tool_coord_layout.addWidget(self.tool_coord_spin) + tool_coord_layout.addWidget(self.tool_coord_btn) + form_layout.addRow(StrongBodyLabel("工具坐标id"), tool_coord_layout) + + # 3. 添加工件坐标布局 + work_coord_layout = QHBoxLayout() # 工件坐标水平布局 + work_coord_layout.addWidget(self.work_coord_spin) + work_coord_layout.addWidget(self.work_coord_btn) + form_layout.addRow(StrongBodyLabel("工件坐标id"), work_coord_layout) + + # 4-9 关节坐标布局 + joint_control_layout = QHBoxLayout() + + # 左侧:关节角输入(J1-J6) + joint_input_layout = QFormLayout() + joint_input_layout.setRowWrapPolicy(QFormLayout.DontWrapRows) + joint_input_layout.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter) + joint_input_layout.setSpacing(12) + + for index in range(6): + joint_input_layout.addRow( + StrongBodyLabel(f"J{index + 1} (°)"), self.j_spins[index] + ) + + # 将关节坐标输入布局 添加到 关节坐标布局 + joint_control_layout.addLayout(joint_input_layout) + + # 右侧:步长设置和获取按钮 + control_panel_layout = QVBoxLayout() + control_panel_layout.setSpacing(16) + step_layout = QVBoxLayout(self.step_group) + step_layout.setContentsMargins(10, 15, 10, 15) + step_layout.setSpacing(10) + step_layout.addWidget(self.step_input) + + # step_layout添加到控制面板布局 + control_panel_layout.addWidget(self.step_group) + control_panel_layout.addWidget(self.get_values_btn) + control_panel_layout.addStretch() # 拉伸项,使内容靠上 + + # 将 控制面板布局(右侧) 添加到 关节控制布局 + joint_control_layout.addLayout(control_panel_layout) + + # 将关节控制水平布局添加到表单布局 + form_layout.addRow(StrongBodyLabel("关节坐标"), joint_control_layout) + + # 10. 速度布局 + form_layout.addRow(StrongBodyLabel("速度 (%)"), self.approach_speed_spin) + + # 11. 运动类型(下拉选择)布局 + form_layout.addRow(StrongBodyLabel("运动类型"), self.motion_type_combo) + + # 12. "在此点" 平滑选择布局 + stop_layout = QHBoxLayout() + stop_layout.addWidget(self.stop_radio) + stop_layout.addWidget(self.smooth_radio) + stop_layout.addWidget(self.smooth_ms_spin) + stop_layout.addWidget(StrongBodyLabel("ms")) + stop_layout.setAlignment(Qt.AlignLeft) # 与标签左对齐 + form_layout.addRow(StrongBodyLabel("在此点"), stop_layout) + + # 将表单布局添加到主布局 + main_layout.addLayout(form_layout) + + # 13. 底部按钮布局(居中显示) + btn_layout = QHBoxLayout() + btn_layout.setAlignment(Qt.AlignHCenter) # 水平居中 + btn_layout.addWidget(self.apply_btn) + + # 将底部按钮布局添加到主布局 + main_layout.addLayout(btn_layout) + + # 让表单控件顶部对齐 + main_layout.addStretch(1) + + def __bind(self): + # 更新 J1 到 J6 的步长 + self.step_input.valueChanged.connect(self.onUpdateStepSize) + + # 获取 J1 到 J6 的值(外部相关) + self.get_values_btn.clicked.connect(self.onGetJointValues) + + # 调整平滑时间设置控件可不可用 + self.stop_radio.toggled.connect( + lambda checked: self.smooth_ms_spin.setEnabled(not checked) + ) + self.smooth_radio.toggled.connect( + lambda checked: self.smooth_ms_spin.setEnabled(checked) + ) + + # 应用按钮点击 (外部相关) + self.apply_btn.clicked.connect(self.onApplyBtnClicked) + + # 设置状态编辑框中的 点位的状态 + def setPointStateValue(self, pos_state_dict: dict): + # 设置除了点位名字之外的所有 点位状态的值 + self.approach_speed_spin.setValue(pos_state_dict["speed"]) + self.tool_coord_spin.setValue(pos_state_dict["tool_id"]) + self.work_coord_spin.setValue(pos_state_dict["work_id"]) + for index in range(6): + self.j_spins[index].setValue(pos_state_dict["joint_values"][index]) + + # 运动状态设置 + # 查找目标文本对应的索引 + target_motion_type = pos_state_dict["motion_type"] + # 1. 查找目标文本对应的索引 + motion_index = self.motion_type_combo.findText(target_motion_type) + # 2. 若未找到(索引=-1),默认选中第0项;否则选中对应索引 + if motion_index == -1: + self.motion_type_combo.setCurrentIndex(0) + else: + self.motion_type_combo.setCurrentIndex(motion_index) + + if pos_state_dict["blend_time"] == -1: # 此时为 停止 + self.stop_radio.setChecked(True) + else: + self.smooth_radio.setChecked(True) + self.smooth_ms_spin.setValue(pos_state_dict["blend_time"]) + + def onUpdateStepSize(self, value): + """更新所有关节角输入框的步长""" + for spin in self.j_spins: + spin.setSingleStep(value) + + def onGetJointValues(self): + """获取J1-J6的值(这里用示例值模拟)""" + # 实际应用中,这里应该从设备或其他数据源获取值 + # 这里用随机值模拟 + import random + + for i in range(6): + # 生成一个-180到180之间的随机数,保留3位小数 + value = round(random.uniform(-180, 180), 3) + self.j_spins[i].setValue(value) + print("已获取并更新J1-J6的值") + + def onApplyBtnClicked(self): + """应用按钮点击事件处理""" + # 1、获取点名称 + pos_name = self.name_combo.text() + # 2、速度 + speed = self.approach_speed_spin.value() + # 3、tool_id + tool_id = self.tool_coord_spin.value() + # 4、work_id + work_id = self.work_coord_spin.value() + # 5-10、所有关节坐标 J1 到 J6 + joint_values = [spin.value() for spin in self.j_spins] + # 11、运动类型 (直线、 曲线中间点、 曲线终点、 自由路径) + motion_type = self.motion_type_combo.currentText() + # 12、平滑时间(停止=-1,否则取输入值) + blend_time = -1 if self.stop_radio.isChecked() else self.smooth_ms_spin.value() + + try: + point_state = PointState( + pos_name=pos_name, + speed=speed, + tool_id=tool_id, + work_id=work_id, + joint_values=joint_values, + motion_type=motion_type, + blend_time=blend_time, + ) + # print("状态编辑结果:", point_state.__str__()) + + # 发送信号给 表单窗口 CoordinateTableWidget对象 + self.point_state_applied.emit(self.selected_row_idx, point_state) + + # 关闭状态编辑窗口 + self.close() + except ValueError as e: + # 捕获校验错误,弹窗提示用户 + MessageBox("状态错误", str(e), self).exec() + + +# if __name__ == "__main__": +# app = QApplication([]) +# setTheme(Theme.DARK) # 设置浅色主题(可选:Theme.DARK) +# widget = StatusEditWidget() +# widget.show() # 显示窗口 +# app.exec()