674 lines
25 KiB
Python
674 lines
25 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
SQLite数据库查看器 - 基于PySide6
|
||
功能:打开product目录下的product.db的sqlite文件,显示表和数据的界面
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
import sqlite3
|
||
from loguru import logger
|
||
|
||
from PySide6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout,
|
||
QWidget, QPushButton, QTableWidget, QTableWidgetItem,
|
||
QListWidget, QListWidgetItem, QSplitter, QFileDialog,
|
||
QLabel, QStatusBar, QMessageBox, QHeaderView, QComboBox,
|
||
QLineEdit, QGroupBox, QTextEdit, QStyledItemDelegate, QMenu)
|
||
from PySide6.QtCore import Qt, QSize
|
||
from PySide6.QtGui import QAction, QFontMetrics
|
||
|
||
|
||
class MultiLineDelegate(QStyledItemDelegate):
|
||
"""多行文本委托,支持自动调整行高"""
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.min_height = 30 # 最小行高
|
||
self.max_height = 200 # 最大行高
|
||
|
||
def paint(self, painter, option, index):
|
||
"""自定义绘制,支持多行文本"""
|
||
# 保存原始选项
|
||
opt = option
|
||
|
||
# 获取文本内容
|
||
text = index.data(Qt.DisplayRole)
|
||
if text is None:
|
||
text = ""
|
||
|
||
# 设置文本换行
|
||
text = str(text)
|
||
|
||
# 计算文本高度
|
||
metrics = QFontMetrics(option.font)
|
||
rect = option.rect
|
||
|
||
# 计算需要的行数
|
||
lines = text.count('\n') + 1
|
||
line_height = metrics.lineSpacing()
|
||
text_height = lines * line_height + 10 # 添加一些边距
|
||
|
||
# 限制高度在最小和最大值之间
|
||
if text_height < self.min_height:
|
||
text_height = self.min_height
|
||
elif text_height > self.max_height:
|
||
text_height = self.max_height
|
||
|
||
# 调整绘制区域高度
|
||
opt.rect.setHeight(text_height)
|
||
|
||
# 调用父类绘制方法
|
||
super().paint(painter, opt, index)
|
||
|
||
def sizeHint(self, option, index):
|
||
"""返回建议的单元格大小"""
|
||
# 获取文本内容
|
||
text = index.data(Qt.DisplayRole)
|
||
if text is None:
|
||
text = ""
|
||
|
||
text = str(text)
|
||
|
||
# 计算文本尺寸
|
||
metrics = QFontMetrics(option.font)
|
||
|
||
# 计算行数
|
||
lines = text.count('\n') + 1
|
||
line_height = metrics.lineSpacing()
|
||
text_height = lines * line_height + 10 # 添加边距
|
||
|
||
# 计算文本宽度(考虑换行)
|
||
if '\n' in text:
|
||
# 多行文本,计算最长行的宽度
|
||
max_width = 0
|
||
for line in text.split('\n'):
|
||
line_width = metrics.horizontalAdvance(line) + 20
|
||
max_width = max(max_width, line_width)
|
||
else:
|
||
# 单行文本
|
||
max_width = metrics.horizontalAdvance(text) + 20
|
||
|
||
# 限制高度
|
||
if text_height < self.min_height:
|
||
text_height = self.min_height
|
||
elif text_height > self.max_height:
|
||
text_height = self.max_height
|
||
|
||
# 最小宽度设置为100像素
|
||
max_width = max(max_width, 100)
|
||
|
||
return QSize(max_width, text_height)
|
||
|
||
|
||
class SQLiteViewer(QMainWindow):
|
||
"""SQLite数据库查看器主窗口"""
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
logger.info("初始化SQLite数据库查看器")
|
||
self.db_connection = None
|
||
self.current_table = None
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
"""初始化用户界面"""
|
||
logger.info("设置主窗口界面")
|
||
self.setWindowTitle("SQLite数据库查看器")
|
||
self.setGeometry(100, 100, 1200, 800)
|
||
|
||
# 创建中央部件
|
||
central_widget = QWidget()
|
||
self.setCentralWidget(central_widget)
|
||
|
||
# 创建主布局
|
||
main_layout = QVBoxLayout(central_widget)
|
||
|
||
# 创建顶部按钮布局
|
||
self.create_top_buttons(main_layout)
|
||
|
||
# 创建筛选控件区域
|
||
self.create_filter_section(main_layout)
|
||
|
||
# 创建分割器(左侧表列表,右侧数据表格)
|
||
self.create_splitter(main_layout)
|
||
|
||
# 创建状态栏
|
||
self.create_status_bar()
|
||
|
||
# 创建菜单栏
|
||
self.create_menubar()
|
||
|
||
logger.info("界面初始化完成")
|
||
|
||
def create_top_buttons(self, layout):
|
||
"""创建顶部按钮布局"""
|
||
logger.info("创建顶部按钮")
|
||
button_layout = QHBoxLayout()
|
||
|
||
# 打开数据库按钮
|
||
self.open_button = QPushButton("打开SQLite数据库")
|
||
self.open_button.clicked.connect(self.open_database)
|
||
button_layout.addWidget(self.open_button)
|
||
|
||
# 刷新按钮
|
||
self.refresh_button = QPushButton("刷新")
|
||
self.refresh_button.clicked.connect(self.refresh_data)
|
||
self.refresh_button.setEnabled(False)
|
||
button_layout.addWidget(self.refresh_button)
|
||
|
||
# 数据库路径显示
|
||
self.db_path_label = QLabel("未打开数据库")
|
||
button_layout.addWidget(self.db_path_label)
|
||
|
||
button_layout.addStretch()
|
||
layout.addLayout(button_layout)
|
||
|
||
def create_filter_section(self, layout):
|
||
"""创建筛选控件区域"""
|
||
logger.info("创建筛选控件区域")
|
||
|
||
# 创建筛选分组框
|
||
filter_group = QGroupBox("数据筛选")
|
||
filter_layout = QHBoxLayout(filter_group)
|
||
|
||
# 字段选择标签
|
||
filter_layout.addWidget(QLabel("筛选字段:"))
|
||
|
||
# 字段选择下拉框
|
||
self.field_combo = QComboBox()
|
||
self.field_combo.setMinimumWidth(150)
|
||
filter_layout.addWidget(self.field_combo)
|
||
|
||
# 筛选条件标签
|
||
filter_layout.addWidget(QLabel("筛选条件:"))
|
||
|
||
# 筛选条件输入框
|
||
self.filter_input = QLineEdit()
|
||
self.filter_input.setPlaceholderText("输入筛选条件,如:<75 或 name='test' 或 created_at>'2024-01-01'")
|
||
self.filter_input.setMinimumWidth(300)
|
||
filter_layout.addWidget(self.filter_input)
|
||
|
||
# 筛选按钮
|
||
self.filter_button = QPushButton("筛选")
|
||
self.filter_button.clicked.connect(self.apply_filter)
|
||
filter_layout.addWidget(self.filter_button)
|
||
|
||
# 清除筛选按钮
|
||
self.clear_filter_button = QPushButton("清除筛选")
|
||
self.clear_filter_button.clicked.connect(self.clear_filter)
|
||
self.clear_filter_button.setEnabled(False)
|
||
filter_layout.addWidget(self.clear_filter_button)
|
||
|
||
filter_layout.addStretch()
|
||
|
||
# 初始状态下禁用筛选控件
|
||
self.field_combo.setEnabled(False)
|
||
self.filter_input.setEnabled(False)
|
||
self.filter_button.setEnabled(False)
|
||
|
||
layout.addWidget(filter_group)
|
||
|
||
def create_splitter(self, layout):
|
||
"""创建分割器界面"""
|
||
logger.info("创建分割器界面")
|
||
splitter = QSplitter(Qt.Horizontal)
|
||
|
||
# 左侧:表列表
|
||
left_widget = QWidget()
|
||
left_layout = QVBoxLayout(left_widget)
|
||
|
||
left_layout.addWidget(QLabel("数据库表列表:"))
|
||
self.table_list = QListWidget()
|
||
self.table_list.itemClicked.connect(self.on_table_selected)
|
||
left_layout.addWidget(self.table_list)
|
||
|
||
# 右侧:数据表格
|
||
right_widget = QWidget()
|
||
right_layout = QVBoxLayout(right_widget)
|
||
|
||
right_layout.addWidget(QLabel("表数据:"))
|
||
self.data_table = QTableWidget()
|
||
self.data_table.setAlternatingRowColors(True)
|
||
|
||
# 设置表格支持多行内容和可调整列宽
|
||
self.data_table.setItemDelegate(MultiLineDelegate(self.data_table))
|
||
self.data_table.setWordWrap(True) # 启用自动换行
|
||
self.data_table.setTextElideMode(Qt.ElideNone) # 不省略文本
|
||
|
||
# 设置列头支持拖拽调整大小
|
||
header = self.data_table.horizontalHeader()
|
||
header.setSectionsMovable(True) # 允许移动列
|
||
header.setStretchLastSection(False) # 不自动拉伸最后一列
|
||
|
||
# 设置行头自动调整高度
|
||
self.data_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
|
||
|
||
# 添加右键菜单支持
|
||
self.data_table.setContextMenuPolicy(Qt.CustomContextMenu)
|
||
self.data_table.customContextMenuRequested.connect(self.show_table_context_menu)
|
||
|
||
right_layout.addWidget(self.data_table)
|
||
|
||
splitter.addWidget(left_widget)
|
||
splitter.addWidget(right_widget)
|
||
splitter.setSizes([300, 900])
|
||
|
||
layout.addWidget(splitter)
|
||
|
||
def create_status_bar(self):
|
||
"""创建状态栏"""
|
||
logger.info("创建状态栏")
|
||
self.status_bar = QStatusBar()
|
||
self.setStatusBar(self.status_bar)
|
||
self.status_bar.showMessage("就绪")
|
||
|
||
def create_menubar(self):
|
||
"""创建菜单栏"""
|
||
logger.info("创建菜单栏")
|
||
menubar = self.menuBar()
|
||
|
||
# 文件菜单
|
||
file_menu = menubar.addMenu("文件")
|
||
|
||
open_action = QAction("打开数据库", self)
|
||
open_action.triggered.connect(self.open_database)
|
||
file_menu.addAction(open_action)
|
||
|
||
exit_action = QAction("退出", self)
|
||
exit_action.triggered.connect(self.close)
|
||
file_menu.addAction(exit_action)
|
||
|
||
def open_database(self):
|
||
"""打开SQLite数据库文件"""
|
||
logger.info("打开数据库文件对话框")
|
||
|
||
# 默认打开product目录下的product.db
|
||
default_path = os.path.join('product', 'product.db')
|
||
if os.path.exists(default_path):
|
||
file_path, _ = QFileDialog.getOpenFileName(
|
||
self, "打开SQLite数据库", default_path, "SQLite数据库文件 (*.db *.sqlite *.sqlite3)"
|
||
)
|
||
else:
|
||
file_path, _ = QFileDialog.getOpenFileName(
|
||
self, "打开SQLite数据库", "", "SQLite数据库文件 (*.db *.sqlite *.sqlite3)"
|
||
)
|
||
|
||
if file_path:
|
||
logger.info(f"打开数据库文件: {file_path}")
|
||
self.connect_to_database(file_path)
|
||
|
||
def connect_to_database(self, file_path):
|
||
"""连接到指定的SQLite数据库"""
|
||
try:
|
||
if self.db_connection:
|
||
self.db_connection.close()
|
||
|
||
self.db_connection = sqlite3.connect(file_path)
|
||
logger.info("数据库连接成功")
|
||
|
||
self.db_path_label.setText(f"数据库: {os.path.basename(file_path)}")
|
||
self.status_bar.showMessage(f"已连接到数据库: {os.path.basename(file_path)}")
|
||
self.refresh_button.setEnabled(True)
|
||
|
||
# 加载表列表
|
||
self.load_table_list()
|
||
|
||
except sqlite3.Error as e:
|
||
logger.error(f"数据库连接失败: {e}")
|
||
QMessageBox.critical(self, "错误", f"无法打开数据库: {e}")
|
||
|
||
def load_table_list(self):
|
||
"""加载数据库表列表"""
|
||
if not self.db_connection:
|
||
return
|
||
|
||
try:
|
||
cursor = self.db_connection.cursor()
|
||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||
tables = cursor.fetchall()
|
||
|
||
self.table_list.clear()
|
||
for table in tables:
|
||
item = QListWidgetItem(table[0])
|
||
self.table_list.addItem(item)
|
||
|
||
logger.info(f"加载了 {len(tables)} 个表")
|
||
self.status_bar.showMessage(f"已加载 {len(tables)} 个表")
|
||
|
||
except sqlite3.Error as e:
|
||
logger.error(f"加载表列表失败: {e}")
|
||
QMessageBox.critical(self, "错误", f"加载表列表失败: {e}")
|
||
|
||
def on_table_selected(self, item):
|
||
"""当表被选中时加载表数据和字段列表"""
|
||
table_name = item.text()
|
||
logger.info(f"选中表: {table_name}")
|
||
self.current_table = table_name
|
||
self.load_table_data(table_name)
|
||
self.update_field_combo(table_name)
|
||
|
||
def load_table_data(self, table_name):
|
||
"""加载指定表的数据"""
|
||
if not self.db_connection:
|
||
return
|
||
|
||
try:
|
||
cursor = self.db_connection.cursor()
|
||
|
||
# 获取表结构
|
||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||
columns = cursor.fetchall()
|
||
column_names = [col[1] for col in columns]
|
||
|
||
# 获取数据
|
||
cursor.execute(f"SELECT * FROM {table_name}")
|
||
data = cursor.fetchall()
|
||
|
||
# 设置表格
|
||
self.data_table.setRowCount(len(data))
|
||
self.data_table.setColumnCount(len(column_names))
|
||
self.data_table.setHorizontalHeaderLabels(column_names)
|
||
|
||
# 填充数据
|
||
for row_idx, row_data in enumerate(data):
|
||
for col_idx, cell_data in enumerate(row_data):
|
||
# 处理None值和格式化数据
|
||
if cell_data is None:
|
||
display_text = ""
|
||
elif isinstance(cell_data, (int, float)):
|
||
# 数字类型保持原样,但转换为字符串
|
||
display_text = str(cell_data)
|
||
else:
|
||
# 文本类型,保留原始格式,包括换行符
|
||
display_text = str(cell_data)
|
||
|
||
item = QTableWidgetItem(display_text)
|
||
item.setToolTip(display_text) # 添加悬停提示
|
||
self.data_table.setItem(row_idx, col_idx, item)
|
||
|
||
# 调整列宽 - 使用Interactive模式让用户可以手动调整
|
||
header = self.data_table.horizontalHeader()
|
||
header.setSectionResizeMode(QHeaderView.Interactive)
|
||
|
||
# 设置初始列宽为内容宽度,但有最大宽度限制
|
||
for col in range(len(column_names)):
|
||
# 计算该列内容的最大宽度
|
||
max_width = 0
|
||
for row in range(min(100, len(data))): # 只检查前100行,避免性能问题
|
||
item = self.data_table.item(row, col)
|
||
if item and item.text():
|
||
text_width = self.data_table.fontMetrics().horizontalAdvance(item.text()) + 20
|
||
max_width = max(max_width, text_width)
|
||
|
||
# 设置列宽,最小100像素,最大400像素
|
||
column_width = min(max(max_width, 100), 400)
|
||
self.data_table.setColumnWidth(col, column_width)
|
||
|
||
logger.info(f"加载表 {table_name} 数据完成,共 {len(data)} 行")
|
||
self.status_bar.showMessage(f"表 {table_name}: {len(data)} 行数据")
|
||
|
||
except sqlite3.Error as e:
|
||
logger.error(f"加载表数据失败: {e}")
|
||
QMessageBox.critical(self, "错误", f"加载表数据失败: {e}")
|
||
|
||
def update_field_combo(self, table_name):
|
||
"""更新字段选择下拉框"""
|
||
if not self.db_connection:
|
||
return
|
||
|
||
try:
|
||
cursor = self.db_connection.cursor()
|
||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||
columns = cursor.fetchall()
|
||
|
||
# 清空当前字段列表
|
||
self.field_combo.clear()
|
||
|
||
# 添加所有字段到下拉框
|
||
for column in columns:
|
||
field_name = column[1] # 字段名在第二个位置
|
||
self.field_combo.addItem(field_name)
|
||
|
||
# 启用筛选控件
|
||
self.field_combo.setEnabled(True)
|
||
self.filter_input.setEnabled(True)
|
||
self.filter_button.setEnabled(True)
|
||
|
||
logger.info(f"更新字段下拉框: {table_name}, 共 {len(columns)} 个字段")
|
||
|
||
except sqlite3.Error as e:
|
||
logger.error(f"获取表字段信息失败: {e}")
|
||
QMessageBox.warning(self, "错误", f"获取表字段信息失败: {e}")
|
||
|
||
def apply_filter(self):
|
||
"""应用筛选条件"""
|
||
if not self.db_connection or not self.current_table:
|
||
return
|
||
|
||
selected_field = self.field_combo.currentText()
|
||
filter_condition = self.filter_input.text().strip()
|
||
|
||
if not selected_field or not filter_condition:
|
||
QMessageBox.warning(self, "警告", "请选择筛选字段并输入筛选条件")
|
||
return
|
||
|
||
try:
|
||
cursor = self.db_connection.cursor()
|
||
|
||
# 检查是否为数值比较(支持 <, >, <=, >=, =, != 操作符)
|
||
import re
|
||
numeric_pattern = r'^\s*([><]=?|!=|=)\s*([\d.]+)\s*$'
|
||
match = re.match(numeric_pattern, filter_condition)
|
||
|
||
if match:
|
||
# 数值比较
|
||
operator = match.group(1)
|
||
value = match.group(2)
|
||
query = f"SELECT * FROM {self.current_table} WHERE {selected_field} {operator} ?"
|
||
filter_value = float(value)
|
||
else:
|
||
# 文本模糊匹配
|
||
query = f"SELECT * FROM {self.current_table} WHERE {selected_field} LIKE ?"
|
||
filter_value = f"%{filter_condition}%"
|
||
|
||
# 执行查询
|
||
cursor.execute(query, (filter_value,))
|
||
data = cursor.fetchall()
|
||
|
||
# 获取表结构
|
||
cursor.execute(f"PRAGMA table_info({self.current_table})")
|
||
columns = cursor.fetchall()
|
||
column_names = [col[1] for col in columns]
|
||
|
||
# 更新表格显示
|
||
self.data_table.setRowCount(len(data))
|
||
self.data_table.setColumnCount(len(column_names))
|
||
self.data_table.setHorizontalHeaderLabels(column_names)
|
||
|
||
# 填充筛选后的数据
|
||
for row_idx, row_data in enumerate(data):
|
||
for col_idx, cell_data in enumerate(row_data):
|
||
# 处理None值和格式化数据
|
||
if cell_data is None:
|
||
display_text = ""
|
||
elif isinstance(cell_data, (int, float)):
|
||
# 数字类型保持原样,但转换为字符串
|
||
display_text = str(cell_data)
|
||
else:
|
||
# 文本类型,保留原始格式,包括换行符
|
||
display_text = str(cell_data)
|
||
|
||
item = QTableWidgetItem(display_text)
|
||
item.setToolTip(display_text) # 添加悬停提示
|
||
self.data_table.setItem(row_idx, col_idx, item)
|
||
|
||
# 调整列宽 - 使用Interactive模式让用户可以手动调整
|
||
header = self.data_table.horizontalHeader()
|
||
header.setSectionResizeMode(QHeaderView.Interactive)
|
||
|
||
# 设置初始列宽为内容宽度,但有最大宽度限制
|
||
for col in range(len(column_names)):
|
||
# 计算该列内容的最大宽度
|
||
max_width = 0
|
||
for row in range(min(100, len(data))): # 只检查前100行,避免性能问题
|
||
item = self.data_table.item(row, col)
|
||
if item and item.text():
|
||
text_width = self.data_table.fontMetrics().horizontalAdvance(item.text()) + 20
|
||
max_width = max(max_width, text_width)
|
||
|
||
# 设置列宽,最小100像素,最大400像素
|
||
column_width = min(max(max_width, 100), 400)
|
||
self.data_table.setColumnWidth(col, column_width)
|
||
|
||
# 启用清除筛选按钮
|
||
self.clear_filter_button.setEnabled(True)
|
||
|
||
if match:
|
||
logger.info(f"应用数值筛选条件: {selected_field} {operator} {value}, 匹配到 {len(data)} 行数据")
|
||
self.status_bar.showMessage(f"筛选结果: {len(data)} 行数据 (条件: {selected_field} {operator} {value})")
|
||
else:
|
||
logger.info(f"应用文本筛选条件: {selected_field} LIKE '%{filter_condition}%', 匹配到 {len(data)} 行数据")
|
||
self.status_bar.showMessage(f"筛选结果: {len(data)} 行数据 (条件: {selected_field} 包含 '{filter_condition}')")
|
||
|
||
except sqlite3.Error as e:
|
||
logger.error(f"筛选数据失败: {e}")
|
||
QMessageBox.critical(self, "错误", f"筛选数据失败: {e}")
|
||
|
||
def clear_filter(self):
|
||
"""清除筛选条件,显示所有数据"""
|
||
if not self.current_table:
|
||
return
|
||
|
||
try:
|
||
# 重新加载完整数据
|
||
self.load_table_data(self.current_table)
|
||
|
||
# 清空筛选条件
|
||
self.filter_input.clear()
|
||
|
||
# 禁用清除筛选按钮
|
||
self.clear_filter_button.setEnabled(False)
|
||
|
||
logger.info("清除筛选条件,显示所有数据")
|
||
self.status_bar.showMessage("已清除筛选条件,显示所有数据")
|
||
|
||
except Exception as e:
|
||
logger.error(f"清除筛选失败: {e}")
|
||
QMessageBox.critical(self, "错误", f"清除筛选失败: {e}")
|
||
|
||
def refresh_data(self):
|
||
"""刷新当前数据"""
|
||
logger.info("刷新数据")
|
||
if self.current_table:
|
||
self.load_table_data(self.current_table)
|
||
else:
|
||
self.load_table_list()
|
||
|
||
def show_table_context_menu(self, position):
|
||
"""显示表格右键菜单"""
|
||
menu = QMenu()
|
||
|
||
# 添加菜单项
|
||
auto_resize_action = menu.addAction("自动调整列宽")
|
||
auto_resize_rows_action = menu.addAction("自动调整行高")
|
||
copy_action = menu.addAction("复制选中内容")
|
||
|
||
# 显示菜单
|
||
action = menu.exec(self.data_table.mapToGlobal(position))
|
||
|
||
if action == auto_resize_action:
|
||
self.auto_resize_columns()
|
||
elif action == auto_resize_rows_action:
|
||
self.auto_resize_rows()
|
||
elif action == copy_action:
|
||
self.copy_selected_content()
|
||
|
||
def auto_resize_columns(self):
|
||
"""自动调整所有列宽"""
|
||
logger.info("自动调整列宽")
|
||
|
||
# 遍历所有列
|
||
for col in range(self.data_table.columnCount()):
|
||
# 计算该列内容的最大宽度
|
||
max_width = 0
|
||
for row in range(min(100, self.data_table.rowCount())): # 只检查前100行
|
||
item = self.data_table.item(row, col)
|
||
if item and item.text():
|
||
text_width = self.data_table.fontMetrics().horizontalAdvance(item.text()) + 20
|
||
max_width = max(max_width, text_width)
|
||
|
||
# 设置列宽,最小100像素,最大500像素
|
||
column_width = min(max(max_width, 100), 500)
|
||
self.data_table.setColumnWidth(col, column_width)
|
||
|
||
self.status_bar.showMessage("已自动调整列宽")
|
||
|
||
def auto_resize_rows(self):
|
||
"""自动调整所有行高"""
|
||
logger.info("自动调整行高")
|
||
|
||
# 触发重新计算行高
|
||
self.data_table.resizeRowsToContents()
|
||
|
||
self.status_bar.showMessage("已自动调整行高")
|
||
|
||
def copy_selected_content(self):
|
||
"""复制选中的内容"""
|
||
selected_items = self.data_table.selectedItems()
|
||
if not selected_items:
|
||
return
|
||
|
||
# 按行列组织数据
|
||
rows = {}
|
||
for item in selected_items:
|
||
row = item.row()
|
||
col = item.column()
|
||
if row not in rows:
|
||
rows[row] = {}
|
||
rows[row][col] = item.text()
|
||
|
||
# 构建复制的文本
|
||
text_lines = []
|
||
for row in sorted(rows.keys()):
|
||
row_data = []
|
||
for col in sorted(rows[row].keys()):
|
||
row_data.append(rows[row][col])
|
||
text_lines.append('\t'.join(row_data))
|
||
|
||
# 复制到剪贴板
|
||
clipboard = QApplication.clipboard()
|
||
clipboard.setText('\n'.join(text_lines))
|
||
|
||
self.status_bar.showMessage(f"已复制 {len(selected_items)} 个单元格的内容")
|
||
|
||
def closeEvent(self, event):
|
||
"""关闭事件处理"""
|
||
logger.info("关闭应用程序")
|
||
if self.db_connection:
|
||
self.db_connection.close()
|
||
event.accept()
|
||
|
||
|
||
def main():
|
||
"""主函数"""
|
||
logger.info("启动SQLite数据库查看器")
|
||
|
||
# 配置日志
|
||
logger.add("sqlite_viewer.log", rotation="10 MB", level="INFO")
|
||
|
||
app = QApplication(sys.argv)
|
||
|
||
# 设置应用程序信息
|
||
app.setApplicationName("SQLite数据库查看器")
|
||
app.setApplicationVersion("1.0.0")
|
||
|
||
viewer = SQLiteViewer()
|
||
viewer.show()
|
||
|
||
logger.info("应用程序启动完成")
|
||
sys.exit(app.exec())
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |