Files
fluent_widgets_pyside6/app/view/cood_forms_interface.py

1198 lines
43 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
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
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 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.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):
"""删除指定表单的所有数据"""
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 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):
"""批量插入form_data表"""
try:
cursor = self.__connect()
for row in data_rows:
# row格式: [row_idx, x, y, z, rx, ry, rz]
cursor.execute(
"""INSERT INTO form_data
(form_id, row_index, x, y, z, rx, ry, rz)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(form_id, row[0], row[1], row[2], row[3], row[4], row[5], row[6]),
)
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_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数据
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_row_idx_coordinate_list(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.setObjectName("CoordinateTableWidget")
# 主布局
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(6) # 表的列数
self.table.setHorizontalHeaderLabels(["x", "y", "z", "rx", "ry", "rz"])
self.table.horizontalHeader().setStyleSheet(
"QHeaderView::section {font-size: 19px; font-weight: bold;}"
)
self.table.verticalHeader().setStyleSheet(
"QHeaderView::section {font-size: 14px; font-weight: bold;}"
)
if self.hideVHeader:
self.table.verticalHeader().hide() # 隐藏行标题(行名)
for i in range(6):
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)
self.mainLayout.addLayout(btnLayout)
def get_ui_data(self):
"""从表格UI中获取数据 [[行索引row_idx, x, y, z, rx, ry, rz], ......]"""
row_count = self.table.rowCount()
column_count = self.table.columnCount()
data_rows = []
for row_idx in range(row_count):
row_data = [row_idx]
for col_idx in range(column_count):
item = self.table.item(row_idx, col_idx)
# 数据合法性和完整性判定: 保证获取到的是有效数据
row_data.append(
item.text().strip() if item and item.text().strip() else "-9999"
) # 该表格为空,填充 -9999
# 放入这一行的数据 行索引row_idx, x, y, z, rx, ry, rz
data_rows.append(row_data)
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)
for col in range(self.table.columnCount()):
item = QTableWidgetItem("")
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:
for col in range(self.table.columnCount()):
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()
# 初始化单元格
for col in range(self.table.columnCount()):
item = QTableWidgetItem("")
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
# 选中行默认设置
self.update_table_data()
# 发送选中行设置(更新)信号
self.update_table_signal.emit()
# 重置点位
def resetSelectedRow(self):
selectedRows = self.getSelectedRows()
if not selectedRows:
self.showNoSelectWarning("请先选中再重置!")
return
for row in selectedRows:
for col in range(self.table.columnCount()):
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)
# 坐标合法性监测
row_coordinate.append(item.text().strip())
# 放入这一行的坐标
# 结果:[[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数据
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()
copyAction = QAction("复制此行数据", self)
copyAction.triggered.connect(self.copyRowData)
menu.addAction(copyAction)
menu.exec(self.table.mapToGlobal(position))
def copyRowData(self):
"""复制选中行数据到剪贴板"""
selectedRows = self.getSelectedRows()
if not selectedRows:
return
# 目前只支持复制一行数据,
# 选中多行,默认复制选中的多行中行数最小的那行
row = selectedRows[0]
rowData = []
for col in range(self.table.columnCount()):
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}")
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()
# 初始化样式,应用主题样式
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.status_label.setText("有未完成的数据库读取操作,请稍后再试......")
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.showDatabaseReadWarning)
# 启动线程
self.read_thread.start()
def showDatabaseReadWarning(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
# 提取表单名(用于列表显示)
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)
# 确认/取消按钮
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 generateNewForms(self, form_data):
"""根据读取到的数据生成新表单(UI线程处理)"""
# form_data格式{form_name: [(row_idx, x, y, z, rx, ry, rz), ...], ...}
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 in data_rows:
# 如果数单行数超过表格行数,自动添加行
while row_idx >= new_form.table.rowCount():
new_form.addNewRow()
# 填充单元格
new_form.table.setItem(row_idx, 0, QTableWidgetItem(x))
new_form.table.setItem(row_idx, 1, QTableWidgetItem(y))
new_form.table.setItem(row_idx, 2, QTableWidgetItem(z))
new_form.table.setItem(row_idx, 3, QTableWidgetItem(rx))
new_form.table.setItem(row_idx, 4, QTableWidgetItem(ry))
new_form.table.setItem(row_idx, 5, QTableWidgetItem(rz))
# 将新表单添加到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)
# 更新表单中选中行数据 x,y,z,rx,ry,rz
# def test(form_obj: CoordinateTableWidget):
# form_obj.update_table_data([666, 666, 666, 666, 666, 666])
# if __name__ == "__main__":
# app = QApplication(sys.argv)
# setTheme(Theme.DARK)
# window = CoordinateFormsWidget()
# window.form_update_signal.connect(test)
# window.show()
# sys.exit(app.exec())