点位设置界面增加了状态设置,从数据库加载状态

This commit is contained in:
2025-09-10 20:27:51 +08:00
parent 3c9784b362
commit b90395ea24
3 changed files with 789 additions and 43 deletions

128
app/model/point_state.py Normal file
View File

@ -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/工件ID0-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}"
)

View File

@ -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() # 7x/y/z/rx/ry/rz/name
total_cols = self.table.columnCount() # 8x/y/z/rx/ry/rz/name/timestamp
for col in range(total_cols):
# 1. 取出两行当前列的itemtakeItem会移除原位置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):

View File

@ -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()