1806 lines
71 KiB
Python
1806 lines
71 KiB
Python
# 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())
|