Files
fluent_widgets_pyside6/app/view/cood_forms_interface.py

1198 lines
43 KiB
Python
Raw Normal View History

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