通过JSON文件类配置数据,生成CRUD的界面,支持列表和树结构,支持数据钻取。
目录
一、form_config.json文件
二、代码及使用示例
三、效果图
一、form_config.json文件
{
"id": 1,
"title": "用户管理",
"database": "sqlite:///CRUD_DEMO_DATABASE.db",
"table": "user",
"fields": [
{
"name": "id",
"label": "编号",
"type": "id",
"placeholder": "0",
"required": true,
"hidden": false,
"is_search": true
},
{
"name": "name",
"label": "名称",
"type": "text",
"placeholder": "张三丰",
"required": true,
"hidden": false,
"is_search": true
},
{
"name": "age",
"label": "年龄",
"type": "number",
"placeholder": 99,
"required": false,
"hidden": false,
"is_search": true
},
{
"name": "province",
"label": "省份",
"type": "select",
"options": [
{
"label": "请选择...",
"value": null
},
{
"label": "浙江省",
"value": "1"
},
{
"label": "安徽省",
"value": "2"
},
{
"label": "广东省",
"value": "3"
},
{
"label": "江苏省",
"value": "4"
},
{
"label": "四川省",
"value": "5"
}
],
"placeholder": "男",
"required": false,
"hidden": false,
"is_search": true
},
{
"name": "create_time",
"label": "创建时间",
"type": "datetime",
"placeholder": "2024-07-01 08:56:51",
"required": false,
"hidden": false,
"is_search": true
}
],
"drill": [
{
"button_name": "我的菜单",
"form_config_json": "./crud_form_config_tree.json",
"foreign_key": "user_id"
},
{
"button_name": "我的书籍",
"form_config_json": "./crud_form_config_drill.json",
"foreign_key": "user_id"
}
]
}
{
"id": 2,
"title": "书籍管理",
"database": "sqlite:///CRUD_DEMO_DATABASE.db",
"table": "book",
"fields": [
{
"name": "id",
"label": "编号",
"type": "id",
"placeholder": "0",
"required": true,
"hidden": false,
"is_search": true
},
{
"name": "user_id",
"label": "用户ID",
"type": "number",
"placeholder": "0",
"required": true,
"hidden": false,
"is_search": true
},
{
"name": "name",
"label": "书名",
"type": "text",
"placeholder": "万历十五年",
"required": true,
"hidden": false,
"is_search": true
},
{
"name": "author",
"label": "作者",
"type": "text",
"placeholder": "当年明月",
"required": false,
"hidden": false,
"is_search": true
}
],
"drill": [
{
"button_name": "用户列表",
"form_config_json": "./crud_form_config.json",
"foreign_key": "user_id"
}
]
}
{
"id": 3,
"title": "菜单管理",
"database": "sqlite:///TREE_DEMO_DATABASE.db",
"table": "menu",
"fields": [
{
"name": "id",
"label": "编号",
"type": "id",
"placeholder": "0",
"required": true,
"hidden": false,
"is_search": true
},
{
"name": "user_id",
"label": "用户ID",
"type": "number",
"placeholder": "0",
"required": true,
"hidden": false,
"is_search": true
},
{
"name": "name",
"label": "名称",
"type": "text",
"placeholder": "张三丰",
"required": true,
"hidden": false,
"is_search": true
},
{
"name": "parent_id",
"label": "父节点",
"type": "select",
"options": [],
"placeholder": "请选择父节点",
"required": false,
"hidden": false,
"is_search": true
}
],
"drill": [
{
"button_name": "我的书籍",
"form_config_json": "./crud_form_config_drill.json",
"foreign_key": "user_id"
}
]
}
二、代码及使用示例
import json
import uuid
import dataset
import pandas as pd
from PySide6.QtGui import QRegularExpressionValidator
from PySide6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QLineEdit, QPushButton,
QTableWidget, QTableWidgetItem, QHeaderView, QComboBox, QLabel, QSpinBox, QDialog, QDialogButtonBox, QDateTimeEdit,
QGroupBox, QMessageBox, QTreeWidget, QTreeWidgetItem
)
from PySide6.QtCore import QDateTime, QThread, Signal
from plugIn.title_plugin import QCustomTitleBar, WindowResizer
class ExportThread(QThread):
finished = Signal()
def __init__(self, table):
super().__init__()
self.table = table
def run(self):
data = list(self.table.all())
df = pd.DataFrame(data)
df.to_excel('exported_data.xlsx', index=False)
# self.finished.emit()
pass
class CrudWidget(QWidget):
def __init__(self, form_json: str = "./form_config.json", foreign_key=None, foreign_value=None):
"""
:param form_json 表单文件
:param foreign_key 外键名称
:param foreign_value 外键值
"""
super().__init__()
self.foreign_key = foreign_key
self.foreign_value = foreign_value
# 初始化解析表单配置
try:
with open(form_json, 'r', encoding='utf-8') as f:
self.config = json.load(f) # 加载表单配置
except FileNotFoundError:
raise ValueError(f"表单配置文件未找到:{self.config}")
if 'id' not in self.config:
raise ValueError("表单配置文件格式错误【无id参数】")
if 'fields' not in self.config:
raise ValueError("表单配置文件格式错误【无fields参数】")
if 'title' not in self.config:
raise ValueError("表单配置文件格式错误【无title参数】")
if 'database' not in self.config:
raise ValueError("表单配置文件格式错误【无database参数】")
if 'table' not in self.config:
raise ValueError("表单配置文件格式错误【无table参数】")
fields = self.config['fields']
if type(fields) is not list:
raise ValueError("表单配置文件格式错误【fields必须是list类型】")
for field in fields:
if 'name' not in field:
raise ValueError("表单配置文件格式错误【字段缺少name】")
if 'label' not in field:
raise ValueError("表单配置文件格式错误【字段缺少label】")
if 'type' not in field:
raise ValueError("表单配置文件格式错误【字段缺少type】")
self.id = self.config['id']
self.title = self.config['title']
self.database = self.config['database']
self.table = self.config['table']
self.fields = self.config['fields']
self.drill = self.config['drill']
self.setWindowTitle(self.title) # 设置窗口标题
self.setGeometry(100, 100, 800, 600) # 设置窗口位置和大小
# self.title_bar = QCustomTitleBar(self, windowTitle=self.title)
# self.resizer = WindowResizer(self)
self.setStyleSheet("""
QLineEdit, QSpinBox {
padding: 5px;
border: 1px solid #d0d0d0;
border-radius: 5px;
}
QLabel {
font-size: 14px;
color: #333333;
}
""")
self.db = dataset.connect(self.database) # 连接数据库
self.table = self.db[self.config['table']] # 获取数据表
self.init_ui() # 初始化UI
self.load_data() # 加载数据
def init_ui(self):
self.layout = QVBoxLayout(self) # 创建垂直布局
self.init_search_block() # 初始化搜索块
self.init_data_block() # 初始化数据块
def init_search_block(self):
"""初始化搜索块"""
if hasattr(self, 'search_group') and hasattr(self, 'search_group_layout'):
while self.search_group_layout.count():
item = self.search_group_layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.deleteLater()
else:
self.search_group = QGroupBox() # 创建搜索区组
self.layout.addWidget(self.search_group) # 将搜索布局添加到主布局
self.search_group_layout = QHBoxLayout() # 创建水平布局
self.search_group.setLayout(self.search_group_layout) # 设置组布局
self.search_widgets = {} # 搜索部件字典
for field in self.config['fields']:
if field['is_search']: # 是否为搜索字段进行判断
label = field['label'] # 获取字段标签
widget = None
if field['type'] == 'text':
widget = QLineEdit() # 创建输入框
widget.returnPressed.connect(self.search_data) # 监听回车键
elif field['type'] == 'number':
widget = QLineEdit() # 创建数字输入框
validator = QRegularExpressionValidator(r'^\d*$')
widget.setValidator(validator)
widget.returnPressed.connect(self.search_data) # 监听回车键
elif field['type'] == 'datetime':
widget = QDateTimeEdit()
widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss") # 设置显示格式
widget.setCalendarPopup(True) # 启用弹出式日历选择
elif field['type'] == 'select':
widget = QComboBox()
widget.currentIndexChanged.connect(self.search_data) # 监听切换selection事件
for option in field['options']:
widget.addItem(option['label'], option['value'])
elif field['type'] == 'switch':
# 使用QRadioButton
pass
else:
widget = QLineEdit() # 创建输入框
widget.returnPressed.connect(self.search_data) # 监听回车键
self.search_widgets[field['name']] = widget # 将输入框添加到字典
self.search_group_layout.addWidget(QLabel(label)) # 添加标签到布局
self.search_group_layout.addWidget(widget) # 添加输入框到布局
self.search_button = QPushButton("搜索") # 创建搜索按钮
self.search_button.clicked.connect(self.search_data) # 连接搜索按钮信号
self.reset_button = QPushButton("重置") # 创建重置按钮
self.reset_button.clicked.connect(self.reset_search) # 连接重置按钮信号
self.search_group_layout.addWidget(self.search_button) # 添加搜索按钮到布局
self.search_group_layout.addWidget(self.reset_button) # 添加重置按钮到布局
def init_data_block(self):
self.data_layout = QVBoxLayout() # 创建垂直布局
control_group = QGroupBox() # 创建控制区组
control_group_layout = QVBoxLayout() # 创建垂直布局
control_group.setLayout(control_group_layout) # 设置组布局
self.control_layout = QHBoxLayout() # 创建水平布局
self.add_button = QPushButton("新增") # 创建新增按钮
self.add_button.clicked.connect(self.add_data) # 连接新增按钮信号
self.delete_button = QPushButton("删除") # 创建删除按钮
self.delete_button.clicked.connect(self.delete_data) # 连接删除按钮信号
self.clear_button = QPushButton("清空") # 创建清空按钮
self.clear_button.clicked.connect(self.clear_data) # 连接清空按钮信号
self.export_button = QPushButton("导出") # 创建导出按钮
self.export_button.clicked.connect(self.export_data) # 连接导出按钮信号
self.hide_search_button = QPushButton("隐藏搜索块") # 创建隐藏搜索块按钮
self.hide_search_button.clicked.connect(self.toggle_search_block) # 连接隐藏搜索块按钮信号
self.refresh_button = QPushButton("刷新数据") # 创建刷新数据按钮
self.refresh_button.clicked.connect(self.load_data) # 连接刷新数据按钮信号
self.control_layout.addWidget(self.add_button) # 添加新增按钮到布局
self.control_layout.addWidget(self.delete_button) # 添加删除按钮到布局
self.control_layout.addWidget(self.clear_button) # 添加清空按钮到布局
self.control_layout.addWidget(self.export_button) # 添加导出按钮到布局
self.control_layout.addStretch() # 添加伸缩项
self.control_layout.addWidget(self.hide_search_button) # 添加隐藏搜索块按钮到布局
self.control_layout.addWidget(self.refresh_button) # 添加刷新数据按钮到布局
control_group_layout.addLayout(self.control_layout) # 将控制布局添加到数据布局
self.data_layout.addWidget(control_group) # 将控制布局添加到数据布局
self.table_widget = QTableWidget() # 创建表格部件
self.table_widget.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) # 设置表格不可编辑
# "详情", "编辑", "删除"为基础按钮,外加钻取按钮
self.table_widget.setColumnCount(len(self.config['fields']) + 3 + len(self.drill)) # 设置列数
headers = [field['label'] for field in self.config['fields']] + ["详情", "编辑", "删除"] + [d['button_name'] for
d in
self.drill] # 设置表头
self.table_widget.setHorizontalHeaderLabels(headers) # 设置表头标签
self.table_widget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) # 设置列宽自动调整
self.table_widget.setSortingEnabled(True) # 启用排序
self.data_layout.addWidget(self.table_widget) # 将表格部件添加到数据布局
pagination_group = QGroupBox() # 创建页码区组
pagination_group_layout = QVBoxLayout() # 创建垂直布局
pagination_group.setLayout(pagination_group_layout) # 设置组布局
self.pagination_layout = QHBoxLayout() # 创建水平布局
self.current_page_label = QLabel("当前页码: 1") # 创建当前页码标签
self.total_records_label = QLabel("总记录数: 0") # 创建总记录数标签
self.page_size_combo = QComboBox() # 创建每页显示条数下拉框
self.page_size_combo.addItems(["5", "10", "20", "50", "100"]) # 添加选项
self.page_size_combo.currentIndexChanged.connect(self.load_data) # 连接下拉框信号
self.prev_button = QPushButton("上一页") # 创建上一页按钮
self.prev_button.clicked.connect(self.prev_page) # 连接上一页按钮信号
self.next_button = QPushButton("下一页") # 创建下一页按钮
self.next_button.clicked.connect(self.next_page) # 连接下一页按钮信号
self.page_input = QLineEdit("1") # 创建页码输入框
self.page_input.setValidator(QRegularExpressionValidator(r'^(?:[0-9]|[1-9][0-9]|100)$')) # 限制数字范围为 0 到 100
self.page_input.textChanged.connect(
lambda text: self.page_input.setText("1") if text == "" else None) # 置空时自动设置为 1
self.page_input.setFixedWidth(30) # 设置输入框宽度
self.jump_button = QPushButton("跳转") # 创建跳转按钮
self.jump_button.clicked.connect(self.jump_to_page) # 连接跳转按钮信号
self.pagination_layout.addWidget(self.current_page_label) # 添加当前页码标签到布局
self.pagination_layout.addWidget(self.total_records_label) # 添加总记录数标签到布局
self.pagination_layout.addWidget(QLabel("每页显示:")) # 添加标签到布局
self.pagination_layout.addWidget(self.page_size_combo) # 添加下拉框到布局
self.pagination_layout.addStretch() # 添加伸缩项
self.pagination_layout.addWidget(self.prev_button) # 添加上一页按钮到布局
self.pagination_layout.addWidget(self.next_button) # 添加下一页按钮到布局
self.pagination_layout.addWidget(self.page_input) # 添加页码输入框到布局
self.pagination_layout.addWidget(self.jump_button) # 添加跳转按钮到布局
pagination_group_layout.addLayout(self.pagination_layout) # 将分页布局添加到数据布局
self.data_layout.addWidget(pagination_group) # 将分页布局添加到数据布局
self.layout.addLayout(self.data_layout) # 将数据布局添加到主布局
def load_data(self):
self.table_widget.setRowCount(0) # 清空表格
page_size = int(self.page_size_combo.currentText()) # 获取每页显示条数
page = int(self.page_input.text()) # 获取当前页码
offset = (page - 1) * page_size # 计算偏移量
conditions = {} # 搜索条件字典
if self.foreign_key and self.foreign_value:
conditions[self.foreign_key] = self.foreign_value
query = self.table.find(**conditions) # 查询数据
total_records = len(list(query)) # 获取总记录数
self.total_records_label.setText(f"总记录数: {total_records}") # 更新总记录数标签
data = self.table.find(_limit=page_size, _offset=offset, **conditions) # 分页查询数据
for row_data in data:
row = self.table_widget.rowCount() # 获取当前行数
self.table_widget.insertRow(row) # 插入新行
for i, field in enumerate(self.config['fields']):
if field['name'] in row_data:
if field['type'] == 'select':
mapping = {}
for options in field['options']:
mapping[options['value']] = options['label']
item = QTableWidgetItem(str(mapping[row_data[field['name']]])) # 创建表格项
self.table_widget.setItem(row, i, item) # 设置表格项到单元格
else:
item = QTableWidgetItem(str(row_data[field['name']])) # 创建表格项
self.table_widget.setItem(row, i, item) # 设置表格项到单元格
else:
item = QTableWidgetItem("") # 创建表格项
self.table_widget.setItem(row, i, item) # 设置表格项到单元格
detail_button = QPushButton("详情") # 创建详情按钮
detail_button.clicked.connect(lambda _, r=row: self.show_detail(r)) # 连接详情按钮信号
self.table_widget.setCellWidget(row, len(self.config['fields']), detail_button) # 设置详情按钮到单元格
edit_button = QPushButton("编辑") # 创建编辑按钮
edit_button.clicked.connect(lambda _, r=row: self.edit_data(r)) # 连接编辑按钮信号
self.table_widget.setCellWidget(row, len(self.config['fields']) + 1, edit_button) # 设置编辑按钮到单元格
delete_button = QPushButton("删除") # 创建删除按钮
delete_button.clicked.connect(lambda _, r=row: self.delete_data_row(r)) # 连接删除按钮信号
self.table_widget.setCellWidget(row, len(self.config['fields']) + 2, delete_button) # 设置删除按钮到单元格
# 数据钻取按钮
for index, d in enumerate(self.drill):
drill_button = QPushButton(d['button_name']) # 创建钻取按钮
drill_button.clicked.connect(lambda checked, r=row, d=d: self.drill_data_row(r, d)) # 连接删除按钮信号
self.table_widget.setCellWidget(row, len(self.config['fields']) + 2 + index + 1,
drill_button) # 设置删除按钮到单元格
self.current_page_label.setText(f"当前页码: {page}") # 更新当前页码标签
def search_data(self):
if not hasattr(self, 'table_widget'):
return # 如果没有表格,则不执行操作
conditions = {} # 搜索条件字典
if self.foreign_key and self.foreign_value:
conditions[self.foreign_key] = self.foreign_value
for name, widget in self.search_widgets.items():
if isinstance(widget, QComboBox):
# value = widget.currentText() # 获取下拉框选中项
value = widget.currentData()
elif isinstance(widget, QDateTimeEdit):
value = widget.text()
if widget.text() == '2000-01-01 00:00:00':
value = None
else:
value = widget.text() # 获取输入框文本
if value:
conditions[name] = value # 添加到搜索条件字典
print(conditions)
query = self.table.find(**conditions) # 根据条件查询数据
data = list(query) # 获取查询结果
self.table_widget.setRowCount(0) # 清空表格
for row_data in data:
row = self.table_widget.rowCount() # 获取当前行数
self.table_widget.insertRow(row) # 插入新行
for i, field in enumerate(self.config['fields']):
if field['type'] == 'select':
mapping = {}
for options in field['options']:
mapping[options['value']] = options['label']
item = QTableWidgetItem(str(mapping[row_data[field['name']]])) # 创建表格项
self.table_widget.setItem(row, i, item) # 设置表格项到单元格
else:
item = QTableWidgetItem(str(row_data[field['name']])) # 创建表格项
self.table_widget.setItem(row, i, item) # 设置表格项到单元格
detail_button = QPushButton("详情") # 创建详情按钮
detail_button.clicked.connect(lambda _, r=row: self.show_detail(r)) # 连接详情按钮信号
self.table_widget.setCellWidget(row, len(self.config['fields']), detail_button) # 设置详情按钮到单元格
edit_button = QPushButton("编辑") # 创建编辑按钮
edit_button.clicked.connect(lambda _, r=row: self.edit_data(r)) # 连接编辑按钮信号
self.table_widget.setCellWidget(row, len(self.config['fields']) + 1, edit_button) # 设置编辑按钮到单元格
delete_button = QPushButton("删除") # 创建删除按钮
delete_button.clicked.connect(lambda _, r=row: self.delete_data_row(r)) # 连接删除按钮信号
self.table_widget.setCellWidget(row, len(self.config['fields']) + 2, delete_button) # 设置删除按钮到单元格
# 数据钻取按钮
for index, d in enumerate(self.drill):
drill_button = QPushButton(d['button_name']) # 创建钻取按钮
drill_button.clicked.connect(lambda checked, r=row, d=d: self.drill_data_row(r, d)) # 连接删除按钮信号
self.table_widget.setCellWidget(row, len(self.config['fields']) + 2 + index + 1,
drill_button) # 设置删除按钮到单元格
def reset_search(self):
self.init_search_block()
self.load_data() # 重新加载数据
def add_data(self):
dialog = QDialog(self) # 创建对话框
dialog.setWindowTitle("新增数据") # 设置对话框标题
form_layout = QFormLayout(dialog) # 创建表单布局
dialog.setLayout(form_layout) # 设置对话框布局
form_widgets = {} # 表单部件字典
for field in self.config['fields']:
label = field['label'] # 获取字段标签
if field['type'] == 'id':
pass
elif field['type'] == 'text':
widget = QLineEdit() # 创建输入框
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setReadOnly(True)
widget.setText(str(self.foreign_value))
elif field['type'] == 'number':
widget = QLineEdit() # 创建数字输入框
validator = QRegularExpressionValidator(r'^\d*$')
widget.setValidator(validator)
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setReadOnly(True)
widget.setText(str(self.foreign_value))
elif field['type'] == 'datetime':
widget = QDateTimeEdit()
widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss") # 设置显示格式
widget.setCalendarPopup(True) # 启用弹出式日历选择
widget.setDateTime(QDateTime.currentDateTime())
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setReadOnly(True)
widget.setDateTime(self.foreign_value)
elif field['type'] == 'select':
widget = QComboBox()
for option in field['options']:
widget.addItem(option['label'], option['value'])
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setEditable(False)
widget.setCurrentIndex(self.foreign_value)
else:
widget = QLineEdit() # 创建输入框
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setReadOnly(True)
widget.setText(str(self.foreign_value))
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) # 创建按钮框
buttons.accepted.connect(dialog.accept) # 连接确认按钮信号
buttons.rejected.connect(dialog.reject) # 连接取消按钮信号
form_layout.addRow(buttons) # 添加按钮框到布局
if dialog.exec() == QDialog.DialogCode.Accepted:
data = {}
for name, widget in form_widgets.items():
if isinstance(widget, QLineEdit):
data[name] = widget.text()
validator = QRegularExpressionValidator(r'^\d*$')
widget.setValidator(validator)
elif isinstance(widget, QSpinBox):
data[name] = widget.value()
elif isinstance(widget, QDateTimeEdit):
data[name] = widget.dateTime().toString("yyyy-MM-dd HH:mm:ss")
elif isinstance(widget, QComboBox):
data[name] = widget.currentData()
self.table.insert(data) # 插入数据
self.load_data() # 重新加载数据
def delete_data(self):
# 删除数据逻辑
selected_rows = set()
for item in self.table_widget.selectedItems():
selected_rows.add(item.row())
if not selected_rows:
QMessageBox.warning(self, "警告", "请选择要删除的行")
return
confirm = QMessageBox.warning(self, "确认删除", "你确定要删除选中的行吗?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if confirm == QMessageBox.StandardButton.Yes:
for row in sorted(selected_rows, reverse=True):
# 获取要删除的记录的ID
id_item = self.table_widget.item(row, 0) # 假设ID在第一列
if id_item is not None:
record_id = int(id_item.text())
# 从数据库中删除记录
self.table.delete(id=record_id)
# 从表格中删除行
self.table_widget.removeRow(row)
def clear_data(self):
confirm = QMessageBox.warning(self, "确认清空", "你确定要清空所有数据吗?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if confirm == QMessageBox.StandardButton.Yes:
self.table.delete() # 清空数据表
self.load_data() # 重新加载数据
def export_data(self):
self.export_thread = ExportThread(self.table)
self.export_thread.finished.connect(self.export_finished)
self.export_thread.start()
def export_finished(self):
QMessageBox.information(self, "导出完成", "数据已成功导出到 exported_data.xlsx")
def toggle_search_block(self):
if self.search_group.isVisible():
self.search_group.setVisible(False)
self.hide_search_button.setText("显示搜索块")
else:
self.search_group.setVisible(True)
self.hide_search_button.setText("隐藏搜索块")
def prev_page(self):
current_page = int(self.page_input.text()) # 获取当前页码
if current_page > 1:
self.page_input.setText(str(current_page - 1)) # 更新页码
self.load_data() # 重新加载数据
def next_page(self):
current_page = int(self.page_input.text()) # 获取当前页码
page_size = int(self.page_size_combo.currentText()) # 获取每页显示条数
total_records = int(self.total_records_label.text().split(": ")[1]) # 获取总记录数
if (current_page * page_size) < total_records:
self.page_input.setText(str(current_page + 1)) # 更新页码
self.load_data() # 重新加载数据
def jump_to_page(self):
self.load_data() # 重新加载数据
def show_detail(self, row):
dialog = QDialog(self) # 创建对话框
dialog.setWindowTitle("数据详情") # 设置对话框标题
form_layout = QFormLayout(dialog) # 创建表单布局
dialog.setLayout(form_layout) # 设置对话框布局
for i, field in enumerate(self.config['fields']):
label = field['label'] # 获取字段标签
value = self.table_widget.item(row, i).text() # 获取表格项文本
form_layout.addRow(label, QLabel(value)) # 添加标签和值到布局
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) # 创建按钮框
buttons.accepted.connect(dialog.accept) # 连接确认按钮信号
form_layout.addRow(buttons) # 添加按钮框到布局
dialog.exec() # 显示对话框
def edit_data(self, row):
dialog = QDialog(self) # 创建对话框
dialog.setWindowTitle("编辑数据") # 设置对话框标题
form_layout = QFormLayout(dialog) # 创建表单布局
dialog.setLayout(form_layout) # 设置对话框布局
form_widgets = {} # 表单部件字典
for i, field in enumerate(self.config['fields']):
label = field['label'] # 获取字段标签
value = self.table_widget.item(row, i).text() # 获取表格项文本
if field['type'] == 'id':
widget = QLineEdit(value) # 创建标签框,ID不可编辑
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
widget.setReadOnly(True)
elif field['type'] == 'text':
widget = QLineEdit(value) # 创建输入框
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setReadOnly(True)
widget.setText(str(self.foreign_value))
elif field['type'] == 'number':
widget = QLineEdit(value) # 创建数字输入框
validator = QRegularExpressionValidator(r'^\d*$')
widget.setValidator(validator)
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setReadOnly(True)
widget.setText(str(self.foreign_value))
elif field['type'] == 'datetime':
widget = QDateTimeEdit()
widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss") # 设置显示格式
widget.setCalendarPopup(True) # 启用弹出式日历选择
widget.setDateTime(QDateTime.fromString(value, "yyyy-MM-dd hh:mm:ss"))
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setReadOnly(True)
widget.setDateTime(self.foreign_value)
elif field['type'] == 'select':
widget = QComboBox()
for option in field['options']:
widget.addItem(option['label'], option['value'])
index = widget.findText(value)
widget.setCurrentIndex(index)
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setEditable(False)
widget.setCurrentIndex(self.foreign_value)
else:
widget = QLineEdit(value) # 创建输入框
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setReadOnly(True)
widget.setText(str(self.foreign_value))
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) # 创建按钮框
buttons.accepted.connect(dialog.accept) # 连接确认按钮信号
buttons.rejected.connect(dialog.reject) # 连接取消按钮信号
form_layout.addRow(buttons) # 添加按钮框到布局
if dialog.exec() == QDialog.DialogCode.Accepted:
data = {}
for name, widget in form_widgets.items():
if isinstance(widget, QLineEdit) or isinstance(widget, QLabel):
data[name] = widget.text()
elif isinstance(widget, QSpinBox):
data[name] = widget.value()
elif isinstance(widget, QDateTimeEdit):
data[name] = widget.dateTime().toString("yyyy-MM-dd HH:mm:ss")
elif isinstance(widget, QComboBox):
data[name] = widget.currentData()
self.table.update(data, ['id']) # 更新数据
self.load_data() # 重新加载数据
def delete_data_row(self, row):
confirm = QMessageBox.warning(self, "确认删除", "你确定要删除选中的行吗?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if confirm == QMessageBox.StandardButton.Yes:
# 获取要删除的记录的ID
id_item = self.table_widget.item(row, 0) # 假设ID在第一列
if id_item is not None:
record_id = int(id_item.text())
# 从数据库中删除记录
self.table.delete(id=record_id)
# 从表格中删除行
self.table_widget.removeRow(row)
def drill_data_row(self, row: int, drill: dict):
# 获取记录的ID
id_item = self.table_widget.item(row, 0) # 假设ID在第一列
if id_item is not None:
record_id = int(id_item.text()) # 数据ID
button_name = drill['button_name']
drill_form_config_json_path = drill['form_config_json']
foreign_key = drill['foreign_key']
drill_form_config_json_data = json.load(open(drill_form_config_json_path, 'r', encoding='utf-8'))
window_id = f"crud_widget_{drill_form_config_json_data['id']}"
# 防止多次点击打开多个窗口
if not hasattr(self, window_id) or getattr(self, window_id) is None:
if 'parent_id' in [f['name'] for f in drill_form_config_json_data['fields']]:
widget: TreeWidget = TreeWidget(form_json=drill_form_config_json_path, foreign_key=foreign_key,
foreign_value=record_id)
else:
pass
widget: CrudWidget = CrudWidget(form_json=drill_form_config_json_path, foreign_key=foreign_key,
foreign_value=record_id)
widget.isVisible()
widget.isEnabled()
setattr(self, window_id, widget)
widget.show()
else:
# 每次重现的时候将参数强制赋值进去
widget: CrudWidget = getattr(self, window_id)
setattr(widget, "form_json", drill_form_config_json_path)
setattr(widget, "foreign_key", foreign_key)
setattr(widget, "foreign_value", record_id)
widget.show()
widget.activateWindow() # 激活窗口
widget.raise_() # 将窗口提升到最前面
def showEvent(self, event):
"""
这个函数必须重写一下。子窗口关闭后,引用还在上一层存着,并不会销毁,而是隐藏。再次显示并不会初始化。所以在显示的时候我们手动初始化一下。
"""
super().showEvent(event)
self.load_data() # 加载数据
class TreeWidget(QWidget):
def __init__(self, form_json: str | dict = "./tree_form_config.json", foreign_key=None, foreign_value=None):
"""
:param form_json 表单文件
:param foreign_key 外键名称
:param foreign_value 外键值
"""
super().__init__()
self.foreign_key = foreign_key
self.foreign_value = foreign_value
# 初始化解析表单配置
try:
with open(form_json, 'r', encoding='utf-8') as f:
self.config = json.load(f) # 加载表单配置
except FileNotFoundError:
raise ValueError(f"表单配置文件未找到:{self.config}")
if 'id' not in self.config:
raise ValueError("表单配置文件格式错误【无id参数】")
if 'fields' not in self.config:
raise ValueError("表单配置文件格式错误【无fields参数】")
if 'title' not in self.config:
raise ValueError("表单配置文件格式错误【无title参数】")
if 'database' not in self.config:
raise ValueError("表单配置文件格式错误【无database参数】")
if 'table' not in self.config:
raise ValueError("表单配置文件格式错误【无table参数】")
fields = self.config['fields']
if type(fields) is not list:
raise ValueError("表单配置文件格式错误【fields必须是list类型】")
for field in fields:
if 'name' not in field:
raise ValueError("表单配置文件格式错误【字段缺少name】")
if 'label' not in field:
raise ValueError("表单配置文件格式错误【字段缺少label】")
if 'type' not in field:
raise ValueError("表单配置文件格式错误【字段缺少type】")
if 'parent_id' not in [f['name'] for f in fields]:
raise ValueError("表单配置文件格式错误【fields缺少parent_id】")
self.id = self.config['id']
self.title = self.config['title']
self.database = self.config['database']
self.table = self.config['table']
self.fields = self.config['fields']
self.drill = self.config['drill']
self.setWindowTitle(self.title) # 设置窗口标题
self.setGeometry(100, 100, 800, 600) # 设置窗口位置和大小
# self.title_bar = QCustomTitleBar(self, windowTitle=self.title)
# self.resizer = WindowResizer(self)
self.setStyleSheet("""
QLineEdit, QSpinBox {
padding: 5px;
border: 1px solid #d0d0d0;
border-radius: 5px;
}
QLabel {
font-size: 14px;
color: #333333;
}
""")
self.db = dataset.connect(self.database) # 连接数据库
self.table = self.db[self.config['table']] # 获取数据表
self.init_ui() # 初始化UI
self.load_data() # 加载数据
def init_ui(self):
self.layout = QVBoxLayout(self) # 创建垂直布局
self.init_search_block() # 初始化搜索块
self.init_data_block() # 初始化数据块
def init_search_block(self):
"""初始化搜索块"""
if hasattr(self, 'search_group') and hasattr(self, 'search_group_layout'):
while self.search_group_layout.count():
item = self.search_group_layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.deleteLater()
else:
self.search_group = QGroupBox() # 创建搜索区组
self.layout.addWidget(self.search_group) # 将搜索布局添加到主布局
self.search_group_layout = QHBoxLayout() # 创建水平布局
self.search_group.setLayout(self.search_group_layout) # 设置组布局
self.search_widgets = {} # 搜索部件字典
for field in self.config['fields']:
if field['is_search']: # 是否为搜索字段进行判断
label = field['label'] # 获取字段标签
widget = None
if field['type'] == 'text':
widget = QLineEdit() # 创建输入框
widget.returnPressed.connect(self.search_data) # 监听回车键
elif field['type'] == 'number':
widget = QLineEdit() # 创建数字输入框
validator = QRegularExpressionValidator(r'^\d*$')
widget.setValidator(validator)
widget.returnPressed.connect(self.search_data) # 监听回车键
elif field['type'] == 'datetime':
widget = QDateTimeEdit()
widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss") # 设置显示格式
widget.setCalendarPopup(True) # 启用弹出式日历选择
elif field['type'] == 'select':
widget = QComboBox()
widget.currentIndexChanged.connect(self.search_data) # 监听切换selection事件
self.update_parent_options(widget)
else:
widget = QLineEdit() # 创建输入框
widget.returnPressed.connect(self.search_data) # 监听回车键
self.search_widgets[field['name']] = widget # 将输入框添加到字典
self.search_group_layout.addWidget(QLabel(label)) # 添加标签到布局
self.search_group_layout.addWidget(widget) # 添加输入框到布局
self.search_button = QPushButton("搜索") # 创建搜索按钮
self.search_button.clicked.connect(self.search_data) # 连接搜索按钮信号
self.reset_button = QPushButton("重置") # 创建重置按钮
self.reset_button.clicked.connect(self.reset_search) # 连接重置按钮信号
self.search_group_layout.addWidget(self.search_button) # 添加搜索按钮到布局
self.search_group_layout.addWidget(self.reset_button) # 添加重置按钮到布局
def init_data_block(self):
self.data_layout = QVBoxLayout() # 创建垂直布局
control_group = QGroupBox() # 创建控制区组
control_group_layout = QVBoxLayout() # 创建垂直布局
control_group.setLayout(control_group_layout) # 设置组布局
self.control_layout = QHBoxLayout() # 创建水平布局
self.add_button = QPushButton("新增") # 创建新增按钮
self.add_button.clicked.connect(self.add_data) # 连接新增按钮信号
self.delete_button = QPushButton("删除") # 创建删除按钮
self.delete_button.clicked.connect(self.delete_data) # 连接删除按钮信号
self.clear_button = QPushButton("清空") # 创建清空按钮
self.clear_button.clicked.connect(self.clear_data) # 连接清空按钮信号
self.refresh_button = QPushButton("刷新数据") # 创建刷新数据按钮
self.refresh_button.clicked.connect(self.load_data) # 连接刷新数据按钮信号
# 初始状态为收起
self.is_expanded = False
self.expand_button = QPushButton("一键展开") # 创建刷新数据按钮
self.expand_button.clicked.connect(self.toggle_expand_collapse) # 连接刷新数据按钮信号
self.control_layout.addWidget(self.add_button) # 添加新增按钮到布局
self.control_layout.addWidget(self.delete_button) # 添加删除按钮到布局
self.control_layout.addWidget(self.clear_button) # 添加清空按钮到布局
self.control_layout.addStretch() # 添加伸缩项
self.control_layout.addWidget(self.expand_button) # 添加展开数据按钮到布局
self.control_layout.addWidget(self.refresh_button) # 添加刷新数据按钮到布局
control_group_layout.addLayout(self.control_layout) # 将控制布局添加到数据布局
self.data_layout.addWidget(control_group) # 将控制布局添加到数据布局
self.tree_widget = QTreeWidget() # 创建树形部件
# "详情", "编辑", "删除"为基础按钮,外加钻取按钮
self.tree_widget.setColumnCount(len(self.config['fields']) + 3 + len(self.drill)) # 设置列数
headers = [field['label'] for field in self.config['fields']] + ["详情", "编辑", "删除"] + [d['button_name'] for
d in
self.drill] # 设置表头
self.tree_widget.setHeaderLabels(headers) # 设置表头
self.tree_widget.header().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) # 设置列宽自动调整
self.tree_widget.setSortingEnabled(True) # 启用排序
self.data_layout.addWidget(self.tree_widget) # 将树形部件添加到数据布局
self.layout.addLayout(self.data_layout) # 将数据布局添加到主布局
def load_data(self):
self.tree_widget.clear() # 清空树形部件
conditions = {} # 搜索条件字典
if self.foreign_key and self.foreign_value:
conditions[self.foreign_key] = self.foreign_value
data = self.table.all(**conditions) # 查询数据
self.build_tree(list(data)) # 构建树形结构
self.tree_widget.expandAll() # 展开树结构
def build_tree(self, data):
nodes = {}
for row_data in data:
item = []
for field in self.config['fields']:
if field['name'] in row_data:
item.append(str(row_data[field['name']]))
else:
item.append(None)
node = QTreeWidgetItem(item)
nodes[row_data['id']] = node
for row_data in data:
if row_data['parent_id'] is None:
self.tree_widget.addTopLevelItem(nodes[row_data['id']])
else:
parent_node = nodes.get(row_data['parent_id'])
if parent_node is None:
parent_node = nodes.get(int(row_data['parent_id']))
if parent_node is not None:
parent_node.addChild(nodes[row_data['id']])
else:
self.tree_widget.addTopLevelItem(nodes[row_data['id']])
self.add_buttons(nodes[row_data['id']])
def add_buttons(self, node):
detail_button = QPushButton("详情") # 创建详情按钮
detail_button.clicked.connect(lambda _, n=node: self.show_detail(n)) # 连接详情按钮信号
self.tree_widget.setItemWidget(node, len(self.config['fields']), detail_button) # 设置详情按钮到单元格
edit_button = QPushButton("编辑") # 创建编辑按钮
edit_button.clicked.connect(lambda _, n=node: self.edit_data(n)) # 连接编辑按钮信号
self.tree_widget.setItemWidget(node, len(self.config['fields']) + 1, edit_button) # 设置编辑按钮到单元格
delete_button = QPushButton("删除") # 创建删除按钮
delete_button.clicked.connect(lambda _, n=node: self.delete_data_node(n)) # 连接删除按钮信号
self.tree_widget.setItemWidget(node, len(self.config['fields']) + 2, delete_button) # 设置删除按钮到单元格
# 数据钻取按钮
for index, d in enumerate(self.drill):
drill_button = QPushButton(d['button_name']) # 创建钻取按钮
drill_button.clicked.connect(lambda _, n=node: self.drill_data_row(n, d)) # 连接删除按钮信号
self.tree_widget.setItemWidget(node, len(self.config['fields']) + 2 + index + 1, drill_button) # 设置删除按钮到单元格
def search_data(self):
conditions = {} # 搜索条件字典
if self.foreign_key and self.foreign_value:
conditions[self.foreign_key] = self.foreign_value
for name, widget in self.search_widgets.items():
if isinstance(widget, QComboBox):
value = widget.currentData()
else:
value = widget.text() # 获取输入框文本
if value:
conditions[name] = value # 添加到搜索条件字典
if 'parent_id' in conditions and conditions['parent_id'] is not None:
params = ""
for index, (k, v) in enumerate(conditions.items()):
params = params + f" {k}=:{k}"
if index != len(conditions.items()) - 1:
params = params + " and"
query = rf"""
WITH RECURSIVE descendants AS (
SELECT id, name, parent_id
FROM {self.config['table']}
WHERE {params}
UNION ALL
SELECT cdt.id, cdt.name, cdt.parent_id
FROM {self.config['table']} cdt
INNER JOIN descendants d ON cdt.parent_id = d.id
)
SELECT * FROM descendants
"""
data = list(self.db.query(query, **conditions))
else:
query = self.table.find(**conditions) # 根据条件查询数据
data = list(query) # 获取查询结果
if hasattr(self, 'tree_widget'):
self.tree_widget.clear() # 清空树形部件
else:
self.tree_widget = QTreeWidget() # 创建树形部件
self.build_tree(data) # 构建树形结构
self.tree_widget.expandAll() # 展开树结构
def reset_search(self):
self.init_search_block()
self.load_data() # 重新加载数据
def add_data(self):
dialog = QDialog(self) # 创建对话框
dialog.setWindowTitle("新增数据") # 设置对话框标题
form_layout = QFormLayout(dialog) # 创建表单布局
dialog.setLayout(form_layout) # 设置对话框布局
form_widgets = {} # 表单部件字典
for field in self.config['fields']:
label = field['label'] # 获取字段标签
if field['type'] == 'id':
pass
elif field['type'] == 'text':
widget = QLineEdit() # 创建输入框
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setReadOnly(True)
widget.setText(str(self.foreign_value))
elif field['type'] == 'number':
widget = QLineEdit() # 创建数字输入框
validator = QRegularExpressionValidator(r'^\d*$')
widget.setValidator(validator)
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setReadOnly(True)
widget.setText(str(self.foreign_value))
elif field['type'] == 'datetime':
widget = QDateTimeEdit()
widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss") # 设置显示格式
widget.setCalendarPopup(True) # 启用弹出式日历选择
widget.setDateTime(QDateTime.currentDateTime())
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setReadOnly(True)
widget.setDateTime(self.foreign_value)
elif field['type'] == 'select':
widget = QComboBox()
self.update_parent_options(widget)
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setEditable(False)
widget.setCurrentIndex(self.foreign_value)
else:
widget = QLineEdit() # 创建输入框
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setReadOnly(True)
widget.setText(str(self.foreign_value))
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) # 创建按钮框
buttons.accepted.connect(dialog.accept) # 连接确认按钮信号
buttons.rejected.connect(dialog.reject) # 连接取消按钮信号
form_layout.addRow(buttons) # 添加按钮框到布局
if dialog.exec() == QDialog.DialogCode.Accepted:
data = {}
for name, widget in form_widgets.items():
if isinstance(widget, QLineEdit):
data[name] = widget.text()
elif isinstance(widget, QSpinBox):
data[name] = widget.value()
elif isinstance(widget, QDateTimeEdit):
data[name] = widget.dateTime().toString("yyyy-MM-dd HH:mm:ss")
elif isinstance(widget, QComboBox):
data[name] = widget.currentData()
self.table.insert(data) # 插入数据
self.load_data() # 重新加载数据
def delete_data(self):
selected_items = self.tree_widget.selectedItems()
if not selected_items:
QMessageBox.warning(self, "警告", "请选择要删除的行")
return
confirm = QMessageBox.warning(self, "确认删除", "你确定要删除选中的行吗?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if confirm == QMessageBox.StandardButton.Yes:
for item in selected_items:
self.delete_item(item)
def delete_item(self, item):
id_item = item.text(0) # 假设ID在第一列
if id_item is not None:
record_id = int(id_item)
self.table.delete(id=record_id)
parent = item.parent()
if parent is None:
self.tree_widget.takeTopLevelItem(self.tree_widget.indexOfTopLevelItem(item))
else:
parent.removeChild(item)
def toggle_expand_collapse(self):
if self.is_expanded:
self.tree_widget.collapseAll()
self.expand_button.setText("一键展开")
else:
self.tree_widget.expandAll()
self.expand_button.setText("一键收起")
self.is_expanded = not self.is_expanded
def clear_data(self):
confirm = QMessageBox.warning(self, "确认清空", "你确定要清空所有数据吗?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if confirm == QMessageBox.StandardButton.Yes:
self.table.delete() # 清空数据表
self.load_data() # 重新加载数据
def update_parent_options(self, combo_box, self_id=None):
combo_box.clear()
combo_box.addItem("请选择父节点", None)
conditions = {} # 搜索条件字典
if self.foreign_key and self.foreign_value:
conditions[self.foreign_key] = self.foreign_value
data = self.table.find(**conditions)
for row_data in data:
# 有些时候父节点和子节点是同一个,所以需要排除这种情况
if self_id is not None and str(row_data['id']) == str(self_id):
continue
combo_box.addItem(row_data['name'], row_data['id'])
def show_detail(self, node):
dialog = QDialog(self) # 创建对话框
dialog.setWindowTitle("数据详情") # 设置对话框标题
form_layout = QFormLayout(dialog) # 创建表单布局
dialog.setLayout(form_layout) # 设置对话框布局
for i, field in enumerate(self.config['fields']):
label = field['label'] # 获取字段标签
value = node.text(i) # 获取表格项文本
form_layout.addRow(label, QLabel(value)) # 添加标签和值到布局
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) # 创建按钮框
buttons.accepted.connect(dialog.accept) # 连接确认按钮信号
form_layout.addRow(buttons) # 添加按钮框到布局
dialog.exec() # 显示对话框
def edit_data(self, node):
dialog = QDialog(self) # 创建对话框
dialog.setWindowTitle("编辑数据") # 设置对话框标题
form_layout = QFormLayout(dialog) # 创建表单布局
dialog.setLayout(form_layout) # 设置对话框布局
form_widgets = {} # 表单部件字典
for i, field in enumerate(self.config['fields']):
label = field['label'] # 获取字段标签
value = node.text(i) # 获取节点文本
if field['type'] == 'id':
widget = QLineEdit(value) # 创建标签框,ID不可编辑
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
widget.setReadOnly(True)
elif field['type'] == 'text':
widget = QLineEdit(value) # 创建输入框
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setReadOnly(True)
widget.setText(str(self.foreign_value))
elif field['type'] == 'number':
widget = QLineEdit(value) # 创建数字输入框
validator = QRegularExpressionValidator(r'^\d*$')
widget.setValidator(validator)
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setReadOnly(True)
widget.setText(str(self.foreign_value))
elif field['type'] == 'datetime':
widget = QDateTimeEdit()
widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss") # 设置显示格式
widget.setCalendarPopup(True) # 启用弹出式日历选择
widget.setDateTime(QDateTime.fromString(value, "yyyy-MM-dd hh:mm:ss"))
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setReadOnly(True)
widget.setDateTime(self.foreign_value)
elif field['type'] == 'select':
widget = QComboBox()
self.update_parent_options(widget, self_id=node.text(0))
if value and value != 'None':
index = widget.findData(int(value))
widget.setCurrentIndex(index)
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setEditable(False)
widget.setCurrentIndex(self.foreign_value)
else:
widget = QLineEdit(value) # 创建输入框
form_widgets[field['name']] = widget # 将部件添加到字典
form_layout.addRow(label, widget) # 添加标签和部件到布局
if field['name'] == self.foreign_key: # 如果是外键字段,则隐藏输入框
widget.setReadOnly(True)
widget.setText(str(self.foreign_value))
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) # 创建按钮框
buttons.accepted.connect(dialog.accept) # 连接确认按钮信号
buttons.rejected.connect(dialog.reject) # 连接取消按钮信号
form_layout.addRow(buttons) # 添加按钮框到布局
if dialog.exec() == QDialog.DialogCode.Accepted:
data = {}
for name, widget in form_widgets.items():
if isinstance(widget, QLineEdit) or isinstance(widget, QLabel):
data[name] = widget.text()
elif isinstance(widget, QSpinBox):
data[name] = widget.value()
elif isinstance(widget, QDateTimeEdit):
data[name] = widget.dateTime().toString("yyyy-MM-dd HH:mm:ss")
elif isinstance(widget, QComboBox):
data[name] = widget.currentData()
if str(data['parent_id']) == str(data['id']):
raise Exception("父节点ID不能等于自身ID")
# 还有一种情况要排除掉,那就是两个节点,互为其父节点。
one = self.table.find_one(id=data['parent_id'])
if one and one['parent_id'] == data['id']:
raise Exception("父节点不能为其子节点")
self.table.update(data, ['id']) # 更新数据
self.load_data() # 重新加载数据
def delete_data_node(self, node):
confirm = QMessageBox.warning(self, "确认删除", "你确定要删除选中的行吗?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if confirm == QMessageBox.StandardButton.Yes:
id_item = node.text(0) # 假设ID在第一列
if id_item is not None:
record_id = int(id_item)
self.table.delete(id=record_id)
parent = node.parent()
if parent is None:
self.tree_widget.takeTopLevelItem(self.tree_widget.indexOfTopLevelItem(node))
else:
parent.removeChild(node)
def drill_data_row(self, node, drill: dict):
# 获取记录的ID
record_id = node.text(0) # 假设ID在第一列
if record_id is not None:
button_name = drill['button_name']
drill_form_config_json_path = drill['form_config_json']
foreign_key = drill['foreign_key']
drill_form_config_json_data = json.load(open(drill_form_config_json_path, 'r', encoding='utf-8'))
window_id = f"crud_widget_{drill_form_config_json_data['id']}"
# 防止多次点击打开多个窗口
if not hasattr(self, window_id) or getattr(self, window_id) is None:
if 'parent_id' in [f['name'] for f in drill_form_config_json_data['fields']]:
widget: TreeWidget = TreeWidget(form_json=drill_form_config_json_path, foreign_key=foreign_key,
foreign_value=record_id)
else:
pass
widget: CrudWidget = CrudWidget(form_json=drill_form_config_json_path, foreign_key=foreign_key,
foreign_value=record_id)
widget.isVisible()
widget.isEnabled()
setattr(self, window_id, widget)
widget.show()
else:
# 每次重现的时候将参数强制赋值进去
widget = getattr(self, window_id)
setattr(widget, "form_json", drill_form_config_json_path)
setattr(widget, "foreign_key", foreign_key)
setattr(widget, "foreign_value", record_id)
widget.show()
widget.activateWindow() # 激活窗口
widget.raise_() # 将窗口提升到最前面
def showEvent(self, event):
"""
这个函数必须重写一下。子窗口关闭后,引用还在上一层存着,并不会销毁,而是隐藏。再次显示并不会初始化。所以在显示的时候我们手动初始化一下。
"""
super().showEvent(event)
self.init_search_block()
self.load_data() # 加载数据
class ExistingWindow(QWidget):
def __init__(self, parent=None):
super(ExistingWindow, self).__init__(parent)
self.setWindowTitle("Existing Window")
self.resize(600, 400)
self.title_bar = QCustomTitleBar(self, windowTitle="配置")
self.resizer = WindowResizer(self)
self.layout = QVBoxLayout()
self.setLayout(self.layout)
# 其他初始化代码
self.button = QPushButton("Test")
self.layout.addWidget(self.button)
self.crud_widget = None # 用于存储CrudWidget实例
def btn_event():
# 防止多次点击打开多个窗口
if not hasattr(self, "crud_widget") or self.crud_widget is None:
self.crud_widget = CrudWidget(form_json='crud_form_config.json')
self.crud_widget.show()
else:
self.crud_widget.show()
self.crud_widget.activateWindow() # 激活窗口
self.crud_widget.raise_() # 将窗口提升到最前面
self.button.clicked.connect(btn_event)
if __name__ == '__main__':
app = QApplication([])
main = ExistingWindow()
main.show()
app.exec()
三、效果图
略