Files
fluent_widgets_pyside6/app/view/cood_forms_interface.py

1842 lines
73 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
row[7], # 9. name # 以表格中的 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, list)
# 机器停止信号,机器臂停止移动
machine_stop_signal = Signal()
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.status_label.setWordWrap(True) # 启用自动换行
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.machineStopBtn = PushButton("机器停止")
self.machineStopBtn.setIcon(FIF.PAUSE.icon())
self.machineStopBtn.clicked.connect(self.machine_stop_signal)
btnLayout.addWidget(self.machineStopBtn)
# ==============第二行按钮布局==================
bottomBtnLayout = QHBoxLayout()
bottomBtnLayout.setSpacing(15)
# 1. 上移 + 下移
moveComboLayout = QHBoxLayout()
moveComboLayout.setSpacing(15) # 组合内按钮间距
self.moveUpBtn = PushButton("上移")
self.moveUpBtn.clicked.connect(self.moveRowUp)
moveComboLayout.addWidget(self.moveUpBtn)
self.moveDownBtn = PushButton("下移")
self.moveDownBtn.clicked.connect(self.moveRowDown)
moveComboLayout.addWidget(self.moveDownBtn)
bottomBtnLayout.addLayout(moveComboLayout)
# 2. 状态编辑 + 默认状态
stateComboLayout = QHBoxLayout()
stateComboLayout.setSpacing(15)
self.stateEditBtn = PushButton("状态编辑")
self.stateEditBtn.clicked.connect(self.onStateEdit)
stateComboLayout.addWidget(self.stateEditBtn)
self.defaultStateBtn = PushButton("默认状态")
# self.defaultStateBtn.clicked.connect(self.onDefaultState) # 需实现默认状态逻辑
stateComboLayout.addWidget(self.defaultStateBtn)
bottomBtnLayout.addLayout(stateComboLayout)
# 3. 保存数据 按钮
self.saveDataBtn = PushButton("保存数据")
self.saveDataBtn.setIcon(FIF.SAVE.icon())
self.saveDataBtn.clicked.connect(self.saveToDatabase)
bottomBtnLayout.addWidget(self.saveDataBtn)
self.mainLayout.addLayout(btnLayout) # 添加 按钮布局一
self.mainLayout.addLayout(bottomBtnLayout) # 添加 按钮布局二
# 数据保存到数据库,获取数据时调用
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=TrueNo=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() # 多行的坐标
name_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)
# ========== 9/17 新增: 点位的名字 移动时携带点位名=====
name_col_idx = 6 # 名字的列索引为6
name_item = self.table.item(row_idx, name_col_idx)
cood_name = name_item.text().strip() # 坐标/点位 的名字
name_rows.append(cood_name)
# 发送移动到这些 坐标 和 名字 的信号
self.move_to_coodinate_signal.emit(coordinate_rows, name_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. 取出两行当前列的itemtakeItem会移除原位置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, list)
# 表单中 机器臂停止移动信号
form_machine_stop_signal = Signal()
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 = self.createNewFormInstance(firstFormName)
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 = self.createNewFormInstance(formName)
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 createNewFormInstance(self, formName:str, hideVHeader=False, initRowCount=True):
newForm = CoordinateTableWidget(form_name=formName, parent=self, hideVHeader=hideVHeader, initRowCount=initRowCount)
newForm.update_table_signal.connect(self.handleFormUpdate)
newForm.move_to_coodinate_signal.connect(self.form_move_signal)
newForm.machine_stop_signal.connect(self.form_machine_stop_signal)
return newForm
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 = self.createNewFormInstance(formName=form_name, initRowCount=False)
# 填充数据到新表单(包含点位名)
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())