吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 4659|回复: 55
收起左侧

[Python 原创] Python Excel跨表查询工具-第二版增加保存方案功能

  [复制链接]
Doublevv 发表于 2024-12-2 11:31
本帖最后由 Doublevv 于 2024-12-14 11:43 编辑

用SQL语句跨表查询excel表格
========================================
第二版
相较第一版
1.增加了保存查询方案、载入查询方案、删除查询方案功能,使用更加方便。
2.修改界面为800*600;
3.修改表别名为tb1、tb2,编写SQL语句时请相应修改。
界面
2024-12-14_20.png

上源码:
[Python] 纯文本查看 复制代码
import tkinter as tkfrom tkinter import filedialog, messagebox, ttk, simpledialog
import os
import pandas as pd
import duckdb
import subprocess # 用于打开文件
import json


class CrossTableQueryTool:
def __init__(self, root):
self.root = root
self.root.title("Excel跨表格查询工具")
self.root.geometry("800x600") # 设置窗口大小

# 创建控件
self.label1 = tk.Label(root, text="1.添加要查询的Excel文件")
self.label1.place(x=10, y=10, width=200)

# 数据源文件操作按钮
self.btn_add = tk.Button(root, text="添加", command=self.add_table)
self.btn_add.place(x=380, y=10, width=120, height=30)

self.btn_delete = tk.Button(root, text="删除", command=self.delete_table)
self.btn_delete.place(x=520, y=10, width=120, height=30)

self.btn_load_solution = tk.Button(root, text="载入查询方案", command=self.load_solution_dialog)
self.btn_load_solution.place(x=660, y=10, width=120, height=30)

self.table_frame = tk.Frame(root)
self.table_frame.place(x=10, y=50, width=780, height=160)

self.table_tree = ttk.Treeview(self.table_frame, columns=("alias", "file_name", "file_path"), show="headings")
self.table_tree.heading("alias", text="别名")
self.table_tree.heading("file_name", text="文件名")
self.table_tree.heading("file_path", text="文件路径")
self.table_tree.column("alias", width=40)
self.table_tree.column("file_name", width=220)
self.table_tree.column("file_path", width=558)
self.table_tree.pack()

self.label2 = tk.Label(root, text="2.选择一个Excel文件保存查询结果")
self.label2.place(x=10, y=220, width=220)

self.btn_browse = tk.Button(root, text="浏览", command=self.browse_save_path)
self.btn_browse.place(x=520, y=220, width=120, height=30)

# 保存路径
self.label_save_path = tk.Label(root, text="保存路径:")
self.label_save_path.place(x=10, y=260, width=120)

self.entry_save_path = tk.Entry(root, width=640)
self.entry_save_path.place(x=132, y=260, width=644)

self.label3 = tk.Label(root, text="3.编写SQL语句")
self.label3.place(x=10, y=300, width=120)

self.txt_query = tk.Text(root, height=10, width=50)
self.txt_query.place(x=10, y=330, width=780, height=220)

# 执行查询、保存查询方案、删除查询方案按钮
self.btn_execute = tk.Button(root, text="执行查询", command=self.execute_query)
self.btn_execute.place(x=30, y=560, width=340, height=30)

self.btn_save_solution = tk.Button(root, text="保存查询方案", command=self.save_solution)
self.btn_save_solution.place(x=420, y=560, width=160, height=30)

self.btn_delete_solution = tk.Button(root, text="删除查询方案", command=self.delete_solution_dialog)
self.btn_delete_solution.place(x=600, y=560, width=160, height=30)

self.conn = duckdb.connect()
self.tables = {}
self.table_count = 0

def add_table(self):
if self.table_count >= 2:
messagebox.showerror("Error", "最多只能添加两个Excel文件。")
return
file_path = filedialog.askopenfilename(filetypes=[("Excel or csv files", "*.xlsx *.xls *.csv")])
if file_path:
alias = f"tb{self.table_count + 1}"
file_name = os.path.basename(file_path)
self.tables[alias] = file_path
self.table_tree.insert("", "end", values=(alias, file_name, file_path))
self.table_count += 1

def delete_table(self):
if not self.table_tree.selection():
messagebox.showerror("Error", "请先选择要删除的数据源文件。")
return
selected_item = self.table_tree.selection()[0]
alias = self.table_tree.item(selected_item)["values"][0]
del self.tables[alias]
self.table_tree.delete(selected_item)
self.table_count -= 1

def browse_save_path(self):
save_path = filedialog.asksaveasfilename(defaultextension=".xlsx",
filetypes=[("Excel files", "*.xlsx")]) or self.entry_save_path.get()
if save_path:
self.entry_save_path.delete(0, tk.END)
self.entry_save_path.insert(0, save_path)

def execute_query(self):
if not self.tables:
messagebox.showerror("Error", "请先添加Excel文件。")
return
if not self.txt_query.get("1.0", tk.END).strip():
messagebox.showerror("Error", "请输入SQL查询语句。")
return
if not self.entry_save_path.get():
messagebox.showerror("Error", "请输入保存路径。")
return
try:
# 读取Excel文件并加载到DuckDB中
for alias, file_path in self.tables.items():
if file_path.endswith(".csv"):
df = pd.read_csv(file_path, encoding='GBK') # 指定编码
elif file_path.endswith(".xls") or file_path.endswith(".xlsx"):
df = pd.read_excel(file_path)
else:
raise ValueError("不支持的文件类型")
self.conn.register(alias, df)
query = self.txt_query.get("1.0", tk.END).strip()
result = self.conn.execute(query).fetchdf()
# 保存结果到新的Excel文件
result.to_excel(self.entry_save_path.get(), index=False)
messagebox.showinfo("Success", "查询执行成功,结果已保存到新的Excel文件。")
# 自动打开生成的XLSX文件
subprocess.Popen([self.entry_save_path.get()], shell=True)
except Exception as e:
messagebox.showerror("Error", f"执行查询时发生错误: {str(e)}")

def save_solution(self):
solution_name = simpledialog.askstring("输入方案名称", "请输入查询方案名称:")
if solution_name:
solution_data = {
"name": solution_name,
"tables": [(alias, os.path.basename(path), path) for alias, path in self.tables.items()],
"save_path": self.entry_save_path.get(),
"sql_query": self.txt_query.get("1.0", tk.END).strip()
}
solutions = []
if os.path.exists("solutions.json"):
with open("solutions.json", "r", encoding="utf-8") as f:
solutions = json.load(f)
solutions.append(solution_data)
with open("solutions.json", "w", encoding="utf-8") as f:
json.dump(solutions, f, ensure_ascii=False, indent=4)
messagebox.showinfo("Success", f"查询方案 '{solution_name}' 已保存。")

def load_solution_dialog(self):
solutions = []
if os.path.exists("solutions.json"):
with open("solutions.json", "r", encoding="utf-8") as f:
solutions = json.load(f)

if not solutions:
messagebox.showerror("Error", "没有找到任何查询方案。")
return

solution_names = [sol["name"] for sol in solutions]

dialog = tk.Toplevel(self.root)
dialog.title("选择查询方案")
dialog.geometry("300x150")

label = tk.Label(dialog, text="请选择一个查询方案:")
label.pack(pady=10)

solution_var = tk.StringVar()
combobox_solutions = ttk.Combobox(dialog, textvariable=solution_var, values=solution_names)
combobox_solutions.pack(pady=5)

btn_select = tk.Button(dialog, text="选择",
command=lambda: self.load_selected_solution(solution_var.get(), dialog))
btn_select.pack(pady=10)

def load_selected_solution(self, selected_name, dialog):
if not selected_name:
messagebox.showerror("Error", "请选择一个查询方案。")
return

with open("solutions.json", "r", encoding="utf-8") as f:
solutions = json.load(f)

selected_solution = next((sol for sol in solutions if sol["name"] == selected_name), None)
if selected_solution:
self.tables.clear()
self.table_tree.delete(*self.table_tree.get_children())
self.table_count = 0
for alias, file_name, file_path in selected_solution["tables"]:
self.tables[alias] = file_path
self.table_tree.insert("", "end", values=(alias, file_name, file_path))
self.table_count += 1
self.entry_save_path.delete(0, tk.END)
self.entry_save_path.insert(0, selected_solution["save_path"])
self.txt_query.delete("1.0", tk.END)
self.txt_query.insert(tk.END, selected_solution["sql_query"])
messagebox.showinfo("Success", f"查询方案 '{selected_name}' 已加载。")
dialog.destroy()
else:
messagebox.showerror("Error", f"找不到名为 '{selected_name}' 的查询方案。")

def delete_solution_dialog(self):
solutions = []
if os.path.exists("solutions.json"):
with open("solutions.json", "r", encoding="utf-8") as f:
solutions = json.load(f)

if not solutions:
messagebox.showerror("Error", "没有找到任何查询方案。")
return

solution_names = [sol["name"] for sol in solutions]

dialog = tk.Toplevel(self.root)
dialog.title("删除方案")
dialog.geometry("300x150")

label = tk.Label(dialog, text="请选择要删除的方案:")
label.pack(pady=10)

solution_var = tk.StringVar()
combobox_solutions = ttk.Combobox(dialog, textvariable=solution_var, values=solution_names)
combobox_solutions.pack(pady=5)

btn_delete = tk.Button(dialog, text="删除",
command=lambda: self.delete_selected_solution(solution_var.get(), dialog))
btn_delete.pack(pady=10)

def delete_selected_solution(self, selected_name, dialog):
if not selected_name:
messagebox.showerror("Error", "请选择一个方案。")
return

with open("solutions.json", "r", encoding="utf-8") as f:
solutions = json.load(f)

updated_solutions = [sol for sol in solutions if sol["name"] != selected_name]

with open("solutions.json", "w", encoding="utf-8") as f:
json.dump(updated_solutions, f, ensure_ascii=False, indent=4)

messagebox.showinfo("Success", f"查询方案 '{selected_name}' 已删除。")
dialog.destroy()


if __name__ == "__main__":
root = tk.Tk()
app = CrossTableQueryTool(root)
root.mainloop()


========================================
第一版
界面展示:
2024-12-02_07.png


前一阶段,一直在做2个excel表格关联查询,有一些BI软件可以做这事,都是比较大型的,直到看到头条推送了一条个人创作的小工具,感觉挺好,联系了他,可惜只在某宝售卖。
于是就用python弄了这个工具。

成品链接就不放了,对真正学习python的人,这个问题很容易解决。

测试数据
https://wwic.lanzouo.com/ieAee2gwb6be    密码52pj

存在问题:使用csv文件编码只能是GBK,可以用wps另存一下,就是GBK编码的了。
功能扩展暂没有精力弄了,想修改的自己解决吧。

源码: (不保证代码质量,未经完全测试,使用自担风险
[Python] 纯文本查看 复制代码
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import os
import pandas as pd
import duckdb
import subprocess  # 用于打开文件

class CrossTableQueryTool:
    def __init__(self, root):
        self.root = root
        self.root.title("Excel跨表查询工具")
        self.root.geometry("500x460")  # 设置窗口大小

        # 创建控件
        self.label1 = tk.Label(root, text="1. 选择2个Excel文件比对查询")
        self.label1.place(x=4, y=10, width=200)

        # 数据源文件操作按钮
        self.btn_add = tk.Button(root, text="添加", command=self.add_table)
        self.btn_add.place(x=240, y=10, width=60, height=22)

        self.btn_delete = tk.Button(root, text="删除", command=self.delete_table)
        self.btn_delete.place(x=320, y=10, width=60, height=22)

        self.table_frame = tk.Frame(root)
        self.table_frame.place(x=10, y=45, width=480, height=75)

        self.table_tree = ttk.Treeview(self.table_frame, columns=("file_name", "alias", "file_path"), show="headings")
        self.table_tree.heading("file_name", text="文件名")
        self.table_tree.heading("alias", text="别名")
        self.table_tree.heading("file_path", text="文件路径")
        self.table_tree.column("file_name", width=120)
        self.table_tree.column("alias", width=45)
        self.table_tree.column("file_path", width=313)
        self.table_tree.pack()

        self.label2 = tk.Label(root, text="2.选择一个Excel文件保存查询结果")
        self.label2.place(x=4, y=125, width=220)

        # 保存路径和浏览按钮
        self.label_save_path = tk.Label(root, text="保存路径:")
        self.label_save_path.place(x=4, y=155, width=80)

        self.entry_save_path = tk.Entry(root, width=40)
        self.entry_save_path.place(x=80, y=155, width=340)

        self.btn_browse = tk.Button(root, text="浏览", command=self.browse_save_path)
        self.btn_browse.place(x=430, y=155, width=60, height=22)

        self.label3 = tk.Label(root, text="3.编写SQL查询语句")
        self.label3.place(x=15, y=185, width=120)

        self.txt_query = tk.Text(root, height=10, width=50)
        self.txt_query.place(x=10, y=210, width=480, height=200)

        self.btn_execute = tk.Button(root, text="执行查询", command=self.execute_query)
        self.btn_execute.place(x=100, y=420, width=300)

        self.conn = duckdb.connect()
        self.tables = {}
        self.table_count = 0

    def add_table(self):
        if self.table_count >= 2:
            messagebox.showerror("Error", "最多只能添加两个Excel文件。")
            return
        file_path = filedialog.askopenfilename(filetypes=[("Excel or csv files", "*.xlsx *.xls *.csv")])
        if file_path:
            file_name = os.path.basename(file_path)
            alias = f"table{self.table_count + 1}"
            self.tables[alias] = file_path
            self.table_tree.insert("", "end", values=(file_name, alias, file_path))
            self.table_count += 1

    def delete_table(self):
        if not self.table_tree.selection():
            messagebox.showerror("Error", "请先选择要删除的数据源文件。")
            return
        selected_item = self.table_tree.selection()[0]
        alias = self.table_tree.item(selected_item)["values"][1]
        del self.tables[alias]
        self.table_tree.delete(selected_item)
        self.table_count -= 1

    def browse_save_path(self):
        save_path = filedialog.asksaveasfilename(defaultextension=".xlsx",
                                                 filetypes=[("Excel files", "*.xlsx")]) or self.entry_save_path.get()
        if save_path:
            self.entry_save_path.delete(0, tk.END)
            self.entry_save_path.insert(0, save_path)

    def execute_query(self):
        if not self.tables:
            messagebox.showerror("Error", "请先添加Excel文件。")
            return
        if not self.txt_query.get("1.0", tk.END).strip():
            messagebox.showerror("Error", "请输入SQL查询语句。")
            return
        if not self.entry_save_path.get():
            messagebox.showerror("Error", "请输入保存路径。")
            return
        try:
            # 读取Excel文件并加载到DuckDB中
            for alias, file_path in self.tables.items():
                if file_path.endswith(".csv"):
                    df = pd.read_csv(file_path, encoding='GBK')  # 指定编码
                elif file_path.endswith(".xls") or file_path.endswith(".xlsx"):
                    df = pd.read_excel(file_path)
                else:
                    raise ValueError("不支持的文件类型")
                self.conn.register(alias, df)
            query = self.txt_query.get("1.0", tk.END).strip()
            result = self.conn.execute(query).fetchdf()
            # 保存结果到新的Excel文件
            result.to_excel(self.entry_save_path.get(), index=False)
            messagebox.showinfo("Success", "查询执行成功,结果已保存到新的Excel文件。")
            # 自动打开生成的XLSX文件
            subprocess.Popen([self.entry_save_path.get()], shell=True)
        except Exception as e:
            messagebox.showerror("Error", f"执行查询时发生错误: {str(e)}")


if __name__ == "__main__":
    root = tk.Tk()
    app = CrossTableQueryTool(root)
    root.mainloop()

免费评分

参与人数 9吾爱币 +15 热心值 +8 收起 理由
menoooooos + 1 + 1 我很赞同!
ccccccc444 + 1 + 1 谢谢@Thanks!
hshcompass + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
CSKSuper + 1 都是技术人,相互交流以此鼓励!
苏紫方璇 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
cccfind911 + 1 谢谢@Thanks!
elan + 1 + 1 谢谢@Thanks!
cn19491001 + 1 + 1 用心讨论,共获提升!
starf + 1 + 1 我很赞同!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

狐白本白 发表于 2024-12-24 14:21
根据楼主代码做了一些优化,ui采用pyqt6 ,
新增表结构字段,双击可以直接插入到sql编辑框

[Python] 纯文本查看 复制代码
# 导入必要的系统模块
import sys  # 提供Python运行时环境的变量和函数
import os   # 提供与操作系统交互的功能
import traceback  # 提供详细的异常跟踪功能
import json  # 提供JSON数据处理功能
import warnings  # 提供警告信息控制功能

# 忽略运行时警告信息在文件开头添加以下代码来处理警告
warnings.filterwarnings("ignore", category=RuntimeWarning)

# 检查并导入第三方依赖包
try:
    # pandas: 用于数据分析和处理的库
    import pandas as pd
    # duckdb: 提供SQL查询功能的嵌入式数据库
    import duckdb
    # sqlparse: SQL语句格式化工具
    import sqlparse
    # PyQt6: GUI界面开发框架
    from PyQt6.QtWidgets import (
        QApplication,  # 应用程序类
        QMainWindow,  # 主窗口类
        QWidget,      # 基础窗口部件
        QVBoxLayout,  # 垂直布局
        QHBoxLayout,  # 水平布局 
        QPushButton,  # 按钮
        QLabel,       # 标签
        QLineEdit,    # 单行文本框
        QTreeWidget,  # 树形视图
        QTreeWidgetItem,  # 树形视图项
        QTextEdit,    # 多行文本框
        QFileDialog,  # 文件选择对话框
        QMessageBox,  # 消息框
        QInputDialog, # 输入对话框
        QDialog,      # 对话框
        QComboBox     # 下拉框
    )
    from PyQt6.QtCore import Qt  # Qt核心功能
    from PyQt6.QtGui import QFont  # 字体相关功能
except ImportError as e:
    # 如果缺少必要的包,提示安装方法并退出程序
    print(f"缺少必要的包: {str(e)}")
    print("请使用pip安装缺失的包,例如:")
    print("pip install pandas duckdb sqlparse PyQt6")
    sys.exit(1)

class CrossTableQueryTool(QMainWindow):
    """Excel跨表格查询工具的主窗口类"""
    
    def __init__(self):
        """初始化主窗口和所有UI组件"""
        
        # 设置环境变量禁用IMK相关警告(主要针对macOS)
        os.environ['DISABLE_IMK'] = '1'
        
        # 设置应用程序字体Qt字体 - 使用系统支持的等宽字体
        font = QFont()
        # 按优先级尝试设置等宽字体,确保代码显示整齐
        for font_name in ["Menlo", "Monaco", "Courier New", "Courier"]:
            test_font = QFont(font_name)
            if test_font.exactMatch():
                font = test_font
                break
        # 如果没找到指定字体,则使用系统默认等宽字体
        if not font.exactMatch():
            font.setStyleHint(QFont.StyleHint.Monospace)
        
        # 设置应用程序默认字体
        QApplication.setFont(font)
        
        # 调用父类初始化方法
        super().__init__()
        
        # 设置窗口标题和最小尺寸
        self.setWindowTitle("Excel跨表格查询工具")
        self.setMinimumSize(800, 600)
        
        # 初始化成员变量
        self.tables = {}  # 存储表格信息的字典
        self.table_count = 0  # 已加载的表格数量
        self.conn = duckdb.connect()  # 创建DuckDB连接
        
        # 设置solutions.json的保存路径
        try:
            # 首选:获取当前脚本所在目录
            self.solutions_dir = os.path.dirname(os.path.abspath(__file__))
        except:
            try:
                # 备选1:获取当前工作目录
                self.solutions_dir = os.getcwd()
            except:
                # 备选2:使用用户主目录
                self.solutions_dir = os.path.expanduser('~')
        
        # 构建完整的方案文件路径
        self.solutions_file = os.path.join(self.solutions_dir, "solutions.json")
        print(f"方案文件将保存到: {self.solutions_file}")  # 输出调试信息

        # 创建主窗口部件和布局
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QVBoxLayout(main_widget)

        # 1. 创建文件选择区域
        file_section = QWidget()  # 创建文件选择区域容器
        file_layout = QVBoxLayout(file_section)  # 使用垂直布局
        
        # 创建顶部按钮区域
        button_layout = QHBoxLayout()  # 水平布局放置按钮
        self.label1 = QLabel("1.添加要查询的Excel文件")  # 标题标签
        
        # 添加主题切换下拉框
        self.theme_combo = QComboBox()
        self.theme_combo.addItems(["默认主题", "苹果蓝主题"])  # 添加主题选项
        self.theme_combo.setFixedWidth(120)  # 设置固定宽度
        self.theme_combo.setToolTip("切换界面主题")  # 添加悬停提示
        
        self.btn_add = QPushButton("添加")  # 添加文件按钮
        self.btn_delete = QPushButton("删除")  # 删除文件按钮
        self.btn_load_solution = QPushButton("载入查询方案")  # 加载已保存方案的按钮
        
        # 将按钮添加到布局中
        button_layout.addWidget(self.label1)
        button_layout.addStretch()  # 添加弹性空间
        button_layout.addWidget(self.theme_combo)  # 主题下拉框放在添加按钮左侧
        button_layout.addWidget(self.btn_add)
        button_layout.addWidget(self.btn_delete)
        button_layout.addWidget(self.btn_load_solution)
        
        # 创建文件列表树形视图
        self.table_tree = QTreeWidget()
        self.table_tree.setHeaderLabels(["别名", "文件名", "文件路径", "表结构"])  # 设置列表头
        # 设置各列的宽度
        self.table_tree.setColumnWidth(0, 60)   # 别名列
        self.table_tree.setColumnWidth(1, 150)  # 文件名列
        self.table_tree.setColumnWidth(2, 300)  # 文件路径列
        self.table_tree.setColumnWidth(3, 200)  # 表结构列
        
        # 为树形视图添加双击事件处理
        self.table_tree.itemDoubleClicked.connect(self.show_structure_detail)
        
        # 将按钮区域和树形视图添加到文件选择区域的布局中
        file_layout.addLayout(button_layout)
        file_layout.addWidget(self.table_tree)
        
        # 2. 创建保存路径区域
        save_section = QWidget()  # 创建保存路径区域容器
        save_layout = QVBoxLayout(save_section)  # 使用垂直布局
        
        # 创建保存路径区域的顶部布局
        save_header = QHBoxLayout()
        self.label2 = QLabel("2.选择一个Excel文件保存查询结果")  # 标题标签
        self.btn_browse = QPushButton("浏览")  # 浏览按钮
        save_header.addWidget(self.label2)
        save_header.addStretch()  # 添加弹性空间
        save_header.addWidget(self.btn_browse)
        
        # 创建保存路径输入区域
        save_path_layout = QHBoxLayout()
        self.label_save_path = QLabel("保存路径:")  # 路径标签
        self.entry_save_path = QLineEdit()  # 路径输入框
        save_path_layout.addWidget(self.label_save_path)
        save_path_layout.addWidget(self.entry_save_path)
        
        # 将各个部件添加到保存路径区域的布局中
        save_layout.addLayout(save_header)
        save_layout.addLayout(save_path_layout)
        
        # 3. 创建SQL查询区域
        query_section = QWidget()  # 创建SQL查询区域容器
        query_layout = QVBoxLayout(query_section)  # 使用垂直布局
        
        # 创建SQL查询区域的顶部布局
        query_header = QHBoxLayout()
        self.label3 = QLabel("3.编写SQL语句")  # 标题标签
        self.btn_sqlparse = QPushButton("美化SQL")  # SQL格式化按钮
        query_header.addWidget(self.label3)
        query_header.addStretch()  # 添加弹性空间
        query_header.addWidget(self.btn_sqlparse)
        
        # 创建SQL编辑框
        self.txt_query = QTextEdit()  # 多行文本编辑框用于输入SQL
        
        # 将各个部件添加到SQL查询区域的布局中
        query_layout.addLayout(query_header)
        query_layout.addWidget(self.txt_query)
        
        # 创建底部按钮区域
        bottom_layout = QHBoxLayout()
        self.btn_execute = QPushButton("执行查询")  # 执行查询按钮
        self.btn_save_solution = QPushButton("保存查询方案")  # 保存方案按钮
        self.btn_delete_solution = QPushButton("删除查询方案")  # 删除方案按钮
        
        # 将按钮添加到底部布局中
        bottom_layout.addWidget(self.btn_execute)
        bottom_layout.addWidget(self.btn_save_solution)
        bottom_layout.addWidget(self.btn_delete_solution)
        
        # 将所有主要区域添加到主布局中
        main_layout.addWidget(file_section)  # 文件选择区域
        main_layout.addWidget(save_section)  # 保存路径区域
        main_layout.addWidget(query_section)  # SQL查询区域
        main_layout.addLayout(bottom_layout)  # 底部按钮区域
        
        # 初始化表结构对话框属性
        self.structure_dialog = None
        
        # 默认使用系统主题
        # self.setMacStyle()  # 注释掉这行,让程序启动时使用默认主题
        
        # 连接所有信号和槽
        self.connectSignals()

    def setMacStyle(self):
        """设置MacOS风格的UI样式"""
        self.setStyleSheet("""
            QMainWindow {
                background-color: #f5f5f5; /* 设置主窗口背景色为浅灰色 */
            }
            QPushButton {
                background-color: #007AFF; /* 按钮背景色为苹果蓝 */
                color: white; /* 按钮文字为白色 */
                border: none; /* 无边框 */
                border-radius: 6px; /* 圆角边框 */
                padding: 5px 10px; /* 内边距 */
                font-size: 13px; /* 字体大小 */
            }
            QPushButton:hover {
                background-color: #0051FF; /* 鼠标悬停时的颜色 */
            }
            QLabel {
                font-size: 13px; /* 标签字体大小 */
                color: #333333; /* 标签文字颜色 */
            }
            QLineEdit {
                border: 1px solid #cccccc; /* 文本框边框 */
                border-radius: 4px; /* 圆角 */
                padding: 5px; /* 内边距 */
                background-color: white; /* 背景色 */
                color: #333333; /* 文字颜色 */
                selection-background-color: #007AFF; /* 选中文本的背景色 */
                selection-color: white; /* 选中文本的颜色 */
            }
            QTextEdit {
                border: 1px solid #cccccc; /* 多行文本框边框 */
                border-radius: 4px; /* 圆角 */
                padding: 5px; /* 内边距 */
                background-color: white; /* 背景色 */
                color: #333333; /* 文字颜色 */
                selection-background-color: #007AFF; /* 选中文本的背景色 */
                selection-color: white; /* 选中文本的颜色 */
                font-family: Menlo, Monaco, Consolas, monospace; /* 等宽字体 */
            }
            QTreeWidget {
                border: 1px solid #cccccc; /* 树形视图边框 */
                border-radius: 4px; /* 圆角 */
                background-color: white; /* 背景色 */
                color: #333333; /* 文字颜色 */
                selection-background-color: #007AFF; /* 选中项的背景色 */
                selection-color: white; /* 选中项的文字颜色 */
            }
            QTreeWidget::item {
                padding: 4px; /* 树形视图项的内边距 */
            }
            QTreeWidget::item:selected {
                background-color: #007AFF; /* 选中项的背景色 */
                color: white; /* 选中项的文字颜色 */
            }
            QTreeWidget::item:hover {
                background-color: #E5E5E5; /* 鼠标悬停时的背景色 */
            }
            QHeaderView::section {
                background-color: #f5f5f5; /* 表头背景色 */
                color: #333333; /* 表头文字颜色 */
                padding: 5px; /* 内边距 */
                border: none; /* 无边框 */
                border-right: 1px solid #cccccc; /* 右边框 */
                border-bottom: 1px solid #cccccc; /* 下边框 */
            }
            QComboBox {
                border: 1px solid #cccccc; /* 下拉框边框 */
                border-radius: 4px; /* 圆角 */
                padding: 5px; /* 内边距 */
                background-color: white; /* 背景色 */
                color: #333333; /* 文字颜色 */
                min-width: 100px; /* 最小宽度 */
            }
            QComboBox:hover {
                border-color: #007AFF; /* 鼠标悬停时的边框颜色 */
            }
            QComboBox:focus {
                border-color: #007AFF; /* 获得焦点时的边框颜色 */
            }
            QComboBox::drop-down {
                border: none; /* 下拉按钮无边框 */
                width: 20px; /* 下拉按钮宽度 */
            }
            QComboBox::down-arrow {
                image: url(down_arrow.png); /* 下拉箭头图标 */
                width: 12px; /* 箭头宽度 */
                height: 12px; /* 箭头高度 */
            }
            QComboBox QAbstractItemView {
                border: 1px solid #cccccc; /* 下拉列表边框 */
                border-radius: 4px; /* 圆角 */
                background-color: white; /* 背景色 */
                color: #333333; /* 文字颜色 */
                selection-background-color: #007AFF; /* 选中项背景色 */
                selection-color: white; /* 选中项文字颜色 */
                padding: 4px; /* 内边距 */
            }
            QComboBox QAbstractItemView::item {
                padding: 4px; /* 下拉列表项内边距 */
                min-height: 20px; /* 最小高度 */
            }
            QComboBox QAbstractItemView::item:hover {
                background-color: #E5E5E5; /* 鼠标悬停时的背景色 */
            }
            QComboBox QAbstractItemView::item:selected {
                background-color: #007AFF; /* 选中项的背景色 */
                color: white; /* 选中项的文字颜色 */
            }
        """)

    def connectSignals(self):
        """连接所有信号到对应的槽函数"""
        self.btn_add.clicked.connect(self.add_table)  # 添加表格按钮点击事件
        self.btn_delete.clicked.connect(self.delete_table)  # 删除表格按钮点击事件
        self.btn_load_solution.clicked.connect(self.load_solution_dialog)  # 加载方案按钮点击事件
        self.btn_browse.clicked.connect(self.browse_save_path)  # 浏览按钮点击事件
        self.btn_sqlparse.clicked.connect(self.format_sql)  # SQL格式化按钮点击事件
        self.btn_execute.clicked.connect(self.execute_query)  # 执行查询按钮点击事件
        self.btn_save_solution.clicked.connect(self.save_solution)  # 保存方案按钮点击事件
        self.btn_delete_solution.clicked.connect(self.delete_solution_dialog)  # 删除方案按钮点击事件
        
        # 添加主题切换的信号连接
        self.theme_combo.currentIndexChanged.connect(self.changeTheme)  # 主题切换事件

    def add_table(self):
        """添加Excel或CSV文件到查询工具"""
        # 检查是否已达到最大表格数量限制(2个)
        if self.table_count >= 2:
            QMessageBox.warning(None, "警告", "最多只能添加两个Excel文件。")
            return
        
        try:
            # 打开文件选择对话框
            file_path, _ = QFileDialog.getOpenFileName(
                None,
                "选择Excel或CSV文件",
                "",  # 默认目录
                "Excel Files (*.xlsx *.xls *.xlsm *.xltx *.xltm);;CSV Files (*.csv);;All Files (*.*)"  # 文件类型过滤
            )
            
            # 如果用户取消选择,直接返回
            if not file_path:
                return
            
            # 检查文件扩展名是否支持
            file_ext = os.path.splitext(file_path)[1].lower()
            if file_ext not in ['.xlsx', '.xls', '.csv', '.xlsm', '.xltx', '.xltm']:
                QMessageBox.warning(None, "警告", "不支持的文件类型,请选择Excel或CSV文件。")
                return
            
            # 检查文件是否存在且可访问
            if not os.path.exists(file_path):
                QMessageBox.critical(None, "错误", f"文件不存在:{file_path}")
                return
            
            # 测试文件是否可读
            try:
                with open(file_path, 'rb') as f:
                    pass
            except Exception as e:
                QMessageBox.critical(None, "错误", f"文件无法访问:{file_path}\n{str(e)}")
                return
            
            # 生成表格别名和获取文件名
            alias = f"tb{self.table_count + 1}"  # 表格别名格式: tb1, tb2
            file_name = os.path.basename(file_path)  # 获取文件名
            
            # 尝试预览文件结构
            try:
                if file_ext == '.csv':
                    # CSV文件需要尝试不同的编码方式
                    encodings = ['utf-8', 'gbk', 'gb2312', 'gb18030']  # 常用中文编码
                    df = None
                    error_msg = ""
                    
                    # 依次尝试不同编码
                    for encoding in encodings:
                        try:
                            df = pd.read_csv(file_path, encoding=encoding, nrows=1)
                            break  # 如果成功读取则跳出循环
                        except Exception as e:
                            error_msg = f"{error_msg}\n尝试 {encoding} 编码失败: {str(e)}"
                            continue
                    
                    # 如果所有编码都失败
                    if df is None:
                        raise Exception(f"无法读取CSV文件,所有编码尝试均失败:{error_msg}")
                else:
                    # Excel文件尝试使用不同的引擎读取
                    try:
                        # 首选openpyxl引擎(支持新版Excel)
                        df = pd.read_excel(file_path, engine='openpyxl')
                    except Exception as e1:
                        try:
                            # 备选xlrd引擎(支持老版Excel)
                            df = pd.read_excel(file_path, engine='xlrd')
                        except Exception as e2:
                            QMessageBox.critical(
                                None,
                                "错误",
                                f"无法读取Excel文件:\n\nopenpyxl错误: {str(e1)}\n\nxlrd错误: {str(e2)}"
                            )
                            return
                
                # 如果成功读取文件,添加到界面
                self.tables[alias] = file_path  # 保存表格信息
                
                # 生成表结构预览信息
                columns = df.columns.tolist()  # 获取所有列名
                if len(columns) > 3:
                    # 如果列数超过3个,只显示前3个列名
                    structure_preview = f"{', '.join(columns[:3])}... (共{len(columns)}列)"
                else:
                    # 否则显示所有列名
                    structure_preview = f"{', '.join(columns)}"
                
                # 创建并添加树形项
                item = QTreeWidgetItem([
                    alias,  # 别名
                    file_name,  # 文件名
                    file_path,  # 完整路径
                    structure_preview  # 结构预览
                ])
                self.table_tree.addTopLevelItem(item)
                self.table_count += 1  # 更新表格计数
                
                # 显示成功提示
                QMessageBox.information(None, "成功", f"成功导入文件:{file_name}\n包含 {len(columns)} 个列")
                
            except Exception as e:
                # 捕获并显示导入过程中的任何错误
                error_detail = traceback.format_exc()
                QMessageBox.critical(
                    None,
                    "错误",
                    f"导入文件失败:{file_name}\n\n错误信息:\n{str(e)}"
                )
                return
            
        except Exception as e:
            # 捕获文件选择过程中的任何错误
            error_detail = traceback.format_exc()
            QMessageBox.critical(
                None,
                "错误",
                f"选择文件时发生错误:\n\n{str(e)}"
            )

    def delete_table(self):
        """删除选中的表格"""
        # 获取当前选中的项
        selected_items = self.table_tree.selectedItems()
        if not selected_items:
            QMessageBox.warning(None, "警告", "请先选择要删除的数据源文件。")
            return
            
        # 删除选中的表格
        item = selected_items[0]
        alias = item.text(0)  # 获取表格别名
        del self.tables[alias]  # 从表格字典中删除
        self.table_tree.takeTopLevelItem(self.table_tree.indexOfTopLevelItem(item))  # 从树形视图中移除
        self.table_count -= 1  # 更新表格计数

    def browse_save_path(self):
        """选择查询结果的保存路径"""
        # 打开文件保存对话框
        file_path, _ = QFileDialog.getSaveFileName(
            None,
            "保存查询结果",
            "",
            "Excel Files (*.xlsx);;All Files (*.*)"
        )
        
        # 如果选择了路径且不以.xlsx结尾,则添加扩展名
        if file_path:
            if not file_path.lower().endswith('.xlsx'):
                file_path += '.xlsx'
            self.entry_save_path.setText(file_path)  # 更新保存路径显示

    def format_sql(self):
        """格式化SQL查询语句"""
        # 获取当前SQL文本
        sql = self.txt_query.toPlainText()
        if sql.strip():  # 如果SQL不为空
            # 使用sqlparse格式化SQL
            formatted_sql = sqlparse.format(
                sql,
                reindent=True,  # 重新缩进
                keyword_case='upper',  # 关键字大写
                indent_width=4  # 缩进宽度
            )
            self.txt_query.setPlainText(formatted_sql)  # 更新显示格式化后的SQL

    def execute_query(self):
        """执行SQL查询"""
        # 检查是否已添加数据源
        if not self.tables:
            QMessageBox.warning(None, "警告", "请先添加Excel文件。")
            return
            
        # 检查是否输入了SQL查询
        if not self.txt_query.toPlainText().strip():
            QMessageBox.warning(None, "警告", "请输入SQL查询语句。")
            return
            
        # 检查是否设置了保存路径
        if not self.entry_save_path.text():
            QMessageBox.warning(None, "警告", "请选择保存路径。")
            return
            
        try:
            # 注册所有表格到DuckDB
            for alias, file_path in self.tables.items():
                try:
                    # 根据文件类型选择不同的读取方式
                    if file_path.lower().endswith('.csv'):
                        # CSV文件尝试不同编码
                        encodings = ['utf-8', 'gbk', 'gb2312', 'gb18030']
                        df = None
                        error_msg = ""
                        
                        for encoding in encodings:
                            try:
                                df = pd.read_csv(file_path, encoding=encoding)
                                break
                            except Exception as e:
                                error_msg = str(e)
                                continue
                                
                        if df is None:
                            raise Exception(f"无法读取CSV文件 {file_path},尝试了以下编码{', '.join(encodings)}\n错误信息:{error_msg}")
                    else:
                        # 读取Excel文件
                        df = pd.read_excel(file_path)
                    
                    # 将数据注册到DuckDB
                    self.conn.register(alias, df)
                    
                except Exception as e:
                    raise Exception(f"读取文件 {file_path} 失败:\n{str(e)}")
            
            # 执行SQL查询
            query = self.txt_query.toPlainText().strip()
            try:
                result = self.conn.execute(query).fetchdf()  # 执行查询并获取结果
            except Exception as e:
                raise Exception(f"SQL执行错误:\n{str(e)}\n\nSQL语句:\n{query}")
            
            # 保存查询结果
            try:
                result.to_excel(self.entry_save_path.text(), index=False)
            except Exception as e:
                raise Exception(f"保存结果文件失败:\n{str(e)}\n\n保存路径:{self.entry_save_path.text()}")
            
            # 显示成功消息
            QMessageBox.information(
                None,
                "成功",
                f"查询执行成功!\n结果已保存到:{self.entry_save_path.text()}\n共 {len(result)} 条记录。"
            )
            
            # 自动打开结果文件
            self.open_result_file()
            
        except Exception as e:
            # 捕获并显示执行过程中的任何错误
            error_detail = traceback.format_exc()
            QMessageBox.critical(
                None,
                "错误",
                f"执行查询时发生错误:\n\n{str(e)}\n\n详细信息:\n{error_detail}"
            )

    def open_result_file(self):
        """打开查询结果文件"""
        try:
            if sys.platform == 'darwin':  # macOS系统
                os.system(f'open "{self.entry_save_path.text()}"')
            elif sys.platform == 'win32':  # Windows系统
                os.startfile(self.entry_save_path.text())
            else:  # Linux系统
                os.system(f'xdg-open "{self.entry_save_path.text()}"')
        except Exception as e:
            QMessageBox.warning(None, "警告", f"打开结果文件失败: {str(e)}")

    def save_solution(self):
        """保存当前的查询方案"""
        # 检查是否有可保存的内容
        if not self.tables or not self.txt_query.toPlainText().strip():
            QMessageBox.warning(None, "警告", "请先添加数据源并编写SQL查询。")
            return
        
        try:
            # 弹出输入对话框获取方案名称
            name, ok = QInputDialog.getText(None, "保存方案", "请输入查询方案名称:")
            if not ok or not name.strip():  # 如果用户取消或未输入名称
                return
            
            # 构建方案数据结构
            solution_data = {
                "name": name,  # 方案名称
                "tables": [(alias, os.path.basename(path), path)  # 表信息列表
                          for alias, path in self.tables.items()],
                "save_path": self.entry_save_path.text(),  # 保存路径
                "sql_query": self.txt_query.toPlainText()  # SQL查询语句
            }
            
            # 确保保存目录存在
            try:
                os.makedirs(os.path.dirname(self.solutions_file), exist_ok=True)
            except Exception as e:
                QMessageBox.critical(None, "错误", f"创建目录失败:\n{str(e)}")
                return
            
            # 读取现有的方案列表
            solutions = []
            if os.path.exists(self.solutions_file):
                try:
                    with open(self.solutions_file, "r", encoding="utf-8") as f:
                        content = f.read().strip()
                        if content:  # 如果文件不为空
                            solutions = json.loads(content)
                except Exception as e:
                    print(f"读取solutions.json失败: {str(e)}")
            
            # 检查是否存在同名方案
            for i, sol in enumerate(solutions):
                if sol["name"] == name:
                    # 询问用户是否覆盖已有方案
                    reply = QMessageBox.question(
                        None,
                        "确认覆盖",
                        f"已存在名为 '{name}' 的方案,是否覆盖?",
                        QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
                    )
                    if reply == QMessageBox.StandardButton.Yes:
                        solutions[i] = solution_data  # 覆盖已有方案
                        break
                    else:
                        return
            else:
                # 如果不存在同名方案,直接添加
                solutions.append(solution_data)
            
            # 保存方案到文件
            try:
                # 先写入临时文件
                temp_file = f"{self.solutions_file}.tmp"
                with open(temp_file, "w", encoding="utf-8") as f:
                    json.dump(solutions, f, ensure_ascii=False, indent=4)
                
                # 验证临时文件
                with open(temp_file, "r", encoding="utf-8") as f:
                    content = f.read().strip()
                    if not content:  # 检查文件是否为空
                        raise Exception("写入的文件内容为空")
                    # 验证JSON格式是否正确
                    json.loads(content)
                
                # 如果验证通过,替换原文件
                if os.path.exists(self.solutions_file):
                    os.remove(self.solutions_file)
                os.rename(temp_file, self.solutions_file)
                
                print(f"方案已保存到: {self.solutions_file}")  # 输出调试信息
                QMessageBox.information(
                    None, 
                    "成功", 
                    f"查询方案 '{name}' 已保存到:\n{self.solutions_file}"
                )
                
            except Exception as e:
                # 清理临时文件
                if os.path.exists(temp_file):
                    try:
                        os.remove(temp_file)
                    except:
                        pass
                QMessageBox.critical(
                    None, 
                    "错误", 
                    f"保存方案文件失败:\n{str(e)}\n\n"
                    f"尝试保存到: {self.solutions_file}\n"
                    f"请检查文件权限或磁盘空间。"
                )
                return
            
        except Exception as e:
            QMessageBox.critical(
                None, 
                "错误", 
                f"保存方案时发生错误:\n{str(e)}\n请重试或检查系统权限。"
            )

    def load_solution_dialog(self):
        """显示加载查询方案的对话框"""
        # 检查方案文件是否存在
        if not os.path.exists(self.solutions_file):
            QMessageBox.warning(None, "警告", f"没有找到查询方案文件:\n{self.solutions_file}")
            return
        
        try:
            # 读取所有已保存的方案
            with open(self.solutions_file, "r", encoding="utf-8") as f:  # 以UTF-8编码打开文件
                solutions = json.load(f)  # 解析JSON数据
        except Exception as e:
            QMessageBox.critical(None, "错误", f"读取方案文件时发生错误:\n{str(e)}")
            return
            
        # 检查是否有可用的方案
        if not solutions:
            QMessageBox.warning(None, "警告", "没有保存的查询方案。")
            return
            
        # 创建方案选择对话框
        dialog = QDialog(self)  # 创建模态对话框
        dialog.setWindowTitle("选择查询方案")  # 设置对话框标题
        dialog.setMinimumWidth(300)  # 设置最小宽度
        
        # 创建对话框布局
        layout = QVBoxLayout(dialog)  # 创建垂直布局
        
        # 创建下拉框用于选择方案
        combo = QComboBox()  # 创建下拉框
        combo.addItems([sol["name"] for sol in solutions])  # 添加所有方案名称到下拉框
        
        # 创建加载按钮
        btn_load = QPushButton("加载")  # 创建加载按钮
        btn_load.clicked.connect(lambda: self.load_solution(solutions[combo.currentIndex()], dialog))  # 连接点击事件
        
        # 添加控件到布局
        layout.addWidget(QLabel("选择要加载的方案:"))  # 添加提示标签
        layout.addWidget(combo)  # 添加下拉框
        layout.addWidget(btn_load)  # 添加加载按钮
        
        # 显示对话框
        dialog.exec()  # 以模态方式显示对话框

    def load_solution(self, solution, dialog):
        """加载选中的查询方案"""
        try:
            # 清空现有数据
            self.tables.clear()
            self.table_tree.clear()
            self.table_count = 0
            
            # 加载表格信息
            for alias, file_name, file_path in solution["tables"]:
                if os.path.exists(file_path):  # 检查文件是否存在
                    try:
                        # 读取文件获取结构信息
                        file_ext = os.path.splitext(file_path)[1].lower()
                        if file_ext == '.csv':
                            # CSV文件尝试不同编码
                            df = None
                            for encoding in ['utf-8', 'gbk', 'gb2312', 'gb18030']:
                                try:
                                    df = pd.read_csv(file_path, encoding=encoding, nrows=1)
                                    break
                                except Exception:
                                    continue
                            if df is None:
                                raise Exception("无法读取CSV文件")
                        else:
                            # Excel文件尝试不同引擎
                            try:
                                df = pd.read_excel(file_path, engine='openpyxl', nrows=1)
                            except Exception:
                                df = pd.read_excel(file_path, engine='xlrd', nrows=1)
                        
                        # 生成表结构预览
                        columns = df.columns.tolist()
                        if len(columns) > 3:
                            structure_preview = f"{', '.join(columns[:3])}... (共{len(columns)}列)"
                        else:
                            structure_preview = f"{', '.join(columns)}"
                        
                        # 添加到界面
                        self.tables[alias] = file_path
                        item = QTreeWidgetItem([alias, file_name, file_path, structure_preview])
                        self.table_tree.addTopLevelItem(item)
                        self.table_count += 1
                        
                    except Exception as e:
                        QMessageBox.warning(
                            None,
                            "警告",
                            f"加载文件 {file_path} 的表结构失败:\n{str(e)}"
                        )
                else:
                    QMessageBox.warning(
                        None,
                        "警告",
                        f"文件不存在:{file_path}\n请检查文件路径。"
                    )
            
            # 加载其他设置
            self.entry_save_path.setText(solution["save_path"])  # 设置保存路径
            self.txt_query.setPlainText(solution["sql_query"])  # 设置SQL查询
            
            # 关闭对话框并显示成功消息
            dialog.accept()
            QMessageBox.information(None, "成功", f"查询方案 '{solution['name']}' 已加载。")
            
        except Exception as e:
            QMessageBox.critical(None, "错误", f"加载方案时发生错误:\n{str(e)}")

    def delete_solution_dialog(self):
        """显示删除查询方案的对话框"""
        # 检查方案文件是否存在
        if not os.path.exists(self.solutions_file):
            QMessageBox.warning(None, "警告", f"没有找到查询方案文件:\n{self.solutions_file}")
            return
        
        try:
            # 读取所有已保存的方案
            with open(self.solutions_file, "r", encoding="utf-8") as f:  # 以UTF-8编码打开文件
                solutions = json.load(f)  # 解析JSON数据
        except Exception as e:
            QMessageBox.critical(None, "错误", f"读取方案文件时发生错误:\n{str(e)}")
            return
            
        # 检查是否有可用的方案
        if not solutions:
            QMessageBox.warning(None, "警告", "没有保存的查询方案。")
            return
            
        # 创建方案选择对话框
        dialog = QDialog(self)  # 创建模态对话框
        dialog.setWindowTitle("删除查询方案")  # 设置对话框标题
        dialog.setMinimumWidth(300)  # 设置最小宽度
        
        # 创建对话框布局
        layout = QVBoxLayout(dialog)  # 创建垂直布局
        
        # 创建下拉框用于选择方案
        combo = QComboBox()  # 创建下拉框
        combo.addItems([sol["name"] for sol in solutions])  # 添加所有方案名称到下拉框
        
        # 创建删除按钮
        btn_delete = QPushButton("删除")  # 创建删除按钮
        btn_delete.clicked.connect(lambda: self.delete_solution(combo.currentText(), solutions, dialog))  # 连接点击事件
        
        # 添加控件到布局
        layout.addWidget(QLabel("选择要删除的方案:"))  # 添加提示标签
        layout.addWidget(combo)  # 添加下拉框
        layout.addWidget(btn_delete)  # 添加删除按钮
        
        # 显示对话框
        dialog.exec()  # 以模态方式显示对话框

    def delete_solution(self, name, solutions, dialog):
        """删除选中的查询方案"""
        # 确认是否删除
        reply = QMessageBox.question(
            None,
            "确认删除",
            f"确定要删除方案 '{name}' 吗?",
            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
        )
        
        if reply == QMessageBox.StandardButton.Yes:
            try:
                # 移除选中的方案
                solutions = [sol for sol in solutions if sol["name"] != name]
                
                # 保存更新后的方案列表
                with open(self.solutions_file, "w", encoding="utf-8") as f:
                    json.dump(solutions, f, ensure_ascii=False, indent=4)
                
                # 关闭对话框并显示成功消息
                dialog.accept()
                QMessageBox.information(None, "成功", f"查询方案 '{name}' 已删除。")
                
            except Exception as e:
                QMessageBox.critical(None, "错误", f"删除方案时发生错误:\n{str(e)}")

    def show_structure_detail(self, item, column):
        """显示表格结构的详细信息"""
        if column == 3:  # 只有点击表结构列时才显示
            alias = item.text(0)  # 获取表格别名
            file_path = item.text(2)  # 获取文件路径
            
            try:
                # 读取文件获取完整结构
                file_ext = os.path.splitext(file_path)[1].lower()  # 获取文件扩展名并转为小写
                if file_ext == '.csv':
                    # CSV文件尝试不同编码
                    encodings = ['utf-8', 'gbk', 'gb2312', 'gb18030']  # 支持的编码列表
                    df = None  # 初始化数据框变量
                    for encoding in encodings:  # 遍历所有支持的编码
                        try:
                            df = pd.read_csv(file_path, encoding=encoding, nrows=1)  # 尝试读取文件
                            break  # 如果成功读取则跳出循环
                        except Exception:  # 如果当前编码读取失败
                            continue  # 继续尝试下一个编码
                else:
                    # Excel文件尝试不同引擎
                    try:
                        df = pd.read_excel(file_path, engine='openpyxl', nrows=1)  # 尝试使用openpyxl引擎
                    except Exception:
                        df = pd.read_excel(file_path, engine='xlrd', nrows=1)  # 如果失败则使用xlrd引擎
                
                if df is not None:  # 如果成功读取到数据
                    # 创建详情对话框
                    self.structure_dialog = QDialog(self)  # 创建对话框实例
                    self.structure_dialog.setWindowTitle(f"表结构详情 - {alias}")  # 设置对话框标题
                    self.structure_dialog.setMinimumSize(400, 500)  # 设置最小尺寸
                    
                    # 设置窗口标志
                    self.structure_dialog.setWindowFlags(
                        Qt.WindowType.Window |  # 独立窗口
                        Qt.WindowType.WindowStaysOnTopHint |  # 保持在最前
                        Qt.WindowType.Tool  # 工具窗口
                    )
                    
                    # 创建布局
                    layout = QVBoxLayout(self.structure_dialog)  # 创建垂直布局
                    
                    # 创建表格显示结构
                    table = QTreeWidget()  # 创建树形视图
                    table.setHeaderLabels(["列名", "数据类型"])  # 设置表头
                    table.setColumnWidth(0, 200)  # 设置列名列宽
                    table.setColumnWidth(1, 150)  # 设置数据类型列宽
                    
                    # 添加列信息
                    for col in df.columns:  # ���历所有列
                        item = QTreeWidgetItem([
                            str(col),  # 列名
                            str(df[col].dtype)  # 数据类型
                        ])
                        table.addTopLevelItem(item)  # 添加到树形视图
                    
                    # 添加双击事件处理
                    def on_item_double_clicked(item, column):
                        """处理双击列名的事件"""
                        if column == 0:  # 只响应列名列的双击
                            column_name = item.text(0)  # 获取列名
                            cursor = self.txt_query.textCursor()  # 获取文本光标
                            cursor.insertText(f"{alias}.{column_name}")  # 插入列名到SQL编辑框
                            self.txt_query.setFocus()  # 保持焦点在SQL编辑框
                    
                    table.itemDoubleClicked.connect(on_item_double_clicked)  # 连接双击事件
                    
                    # 添加提示标签
                    layout.addWidget(QLabel(f"表 {alias} 包含 {len(df.columns)} 个列:"))  # 添加列数提示
                    layout.addWidget(QLabel("双击列名可将其添加到SQL编辑框"))  # 添加操作提示
                    layout.addWidget(table)  # ��加表格到布局
                    
                    # 计算并设置对话框位置
                    main_geo = self.geometry()  # 获取主窗口位置
                    self.structure_dialog.move(
                        main_geo.x() + main_geo.width() + 10,  # 主窗口右侧10像素
                        main_geo.y()  # 与主窗口顶部对齐
                    )
                    
                    # 显示对话框
                    self.structure_dialog.show()  # 显示结构详情对话框
                    
                    # 保持焦点在主窗口
                    self.activateWindow()  # 激活主窗口
                    self.txt_query.setFocus()  # 设置焦点到SQL编辑框
                    
            except Exception as e:
                QMessageBox.critical(None, "错误", f"读取表结构失败:\n{str(e)}")  # 显示错误信息

    def changeTheme(self, index):
        """根据下拉框选择切换主题"""
        if index == 0:  # 默认主题
            self.setStyleSheet("")  # 清除所有自定义样式
        else:  # 苹果蓝主题
            self.setMacStyle()  # 应用苹果蓝主题

# 程序入口点
if __name__ == "__main__":
    try:
        # 禁用所有Qt警告
        os.environ['QT_LOGGING_RULES'] = '*.debug=false;qt.qpa.*=false'
        os.environ['DISABLE_IMK'] = '1'  # 禁用IMK(macOS)
        os.environ['OBJC_DISABLE_INITIALIZE_FORK_SAFETY'] = 'YES'  # macOS fork安全设置
        
        # 忽略特定警告
        warnings.filterwarnings("ignore", category=RuntimeWarning)
        
        # 创建应用程序实例
        app = QApplication(sys.argv)
        
        # 设置默认字体
        font = QFont()
        # 按优先级尝试设置等宽字体
        for font_name in ["Menlo", "Monaco", "Courier New", "Courier"]:
            test_font = QFont(font_name)
            if test_font.exactMatch():
                font = test_font
                break
        # 如果没找到指定字体,使用系统默认等宽字体
        if not font.exactMatch():
            font.setStyleHint(QFont.StyleHint.Monospace)
        
        # 设置应用程序字体
        QApplication.setFont(font)
        
        # 设置高DPI支持
        try:
            app.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps)
        except AttributeError:
            try:
                app.setAttribute(Qt.AA_UseHighDpiPixmaps)
            except AttributeError:
                print("警告: 无法设置高DPI支持")
        
        # 设置菜单栏样式
        try:
            app.setAttribute(Qt.ApplicationAttribute.AA_DontUseNativeMenuBar)
        except AttributeError:
            try:
                app.setAttribute(Qt.AA_DontUseNativeMenuBar)
            except AttributeError:
                print("警告: 无法设置菜单栏样式")
        
        # 设置应用程序样式
        app.setStyle('Fusion')
        
        # 创建并显示主窗口
        window = CrossTableQueryTool()
        window.show()
        
        # 启动应用程序主循环
        sys.exit(app.exec())
        
    except Exception as e:
        # 捕获并显示启动过程中的任何错误
        error_detail = traceback.format_exc()
        print(f"程序启动失败:\n{str(e)}\n\n详细信息:\n{error_detail}")
        
        # 如果应用程序实例已创建,显示错误对话框
        if 'app' in locals():
            QMessageBox.critical(
                None,
                "严重错误",
                f"程序启动失败:\n\n{str(e)}\n\n详细信息:\n{error_detail}"
            )
        sys.exit(1)
LiCan857 发表于 2024-12-2 16:58
新增了一个美化sql的功能

[Python] 纯文本查看 复制代码
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import os
import pandas as pd
import duckdb
import subprocess  # 用于打开文件
import sqlparse


class CrossTableQueryTool:
    def __init__(self, root):
        self.root = root
        self.root.title("Excel跨表查询工具")
        self.root.geometry("500x460")  # 设置窗口大小

        # 创建控件
        self.label1 = tk.Label(root, text="1. 选择2个Excel文件比对查询")
        self.label1.place(x=4, y=10, width=200)

        # 数据源文件操作按钮
        self.btn_add = tk.Button(root, text="添加", command=self.add_table)
        self.btn_add.place(x=240, y=10, width=60, height=22)

        self.btn_delete = tk.Button(root, text="删除", command=self.delete_table)
        self.btn_delete.place(x=320, y=10, width=60, height=22)

        self.table_frame = tk.Frame(root)
        self.table_frame.place(x=10, y=45, width=480, height=75)

        self.table_tree = ttk.Treeview(self.table_frame, columns=("file_name", "alias", "file_path"), show="headings")
        self.table_tree.heading("file_name", text="文件名")
        self.table_tree.heading("alias", text="别名")
        self.table_tree.heading("file_path", text="文件路径")
        self.table_tree.column("file_name", width=120)
        self.table_tree.column("alias", width=45)
        self.table_tree.column("file_path", width=313)
        self.table_tree.pack()

        self.label2 = tk.Label(root, text="2.选择一个Excel文件保存查询结果")
        self.label2.place(x=4, y=125, width=220)

        # 保存路径和浏览按钮
        self.label_save_path = tk.Label(root, text="保存路径:")
        self.label_save_path.place(x=4, y=155, width=80)

        self.entry_save_path = tk.Entry(root, width=40)
        self.entry_save_path.place(x=80, y=155, width=340)

        self.btn_browse = tk.Button(root, text="浏览", command=self.browse_save_path)
        self.btn_browse.place(x=430, y=155, width=60, height=22)

        self.label3 = tk.Label(root, text="3.编写SQL查询语句")
        self.label3.place(x=15, y=185, width=120)

        self.btn_sqlparse = tk.Button(root, text="美化SQL", command=lambda: self.txt_query.insert(tk.END, self.format_sql(self.txt_query.get("1.0", tk.END))))

        self.btn_sqlparse.place(x=430, y=185, width=60, height=22)

        self.txt_query = tk.Text(root, height=10, width=50)
        self.txt_query.place(x=10, y=210, width=480, height=200)

        self.btn_execute = tk.Button(root, text="执行查询", command=self.execute_query)
        self.btn_execute.place(x=100, y=420, width=300)

        self.conn = duckdb.connect()
        self.tables = {}
        self.table_count = 0

    def format_sql(self, sql, reindent=True, keyword_case='lower'):
        # 清空文本框
        self.txt_query.delete("1.0", tk.END)
        # 使用sqlparse.format方法格式化SQL
        formatted_sql = sqlparse.format(
            sql,
            reindent=reindent,
            keyword_case=keyword_case
        )
        return formatted_sql

    def add_table(self):
        # if self.table_count >= 2:
        #     messagebox.showerror("Error", "最多只能添加两个Excel文件。")
        #     return
        file_path = filedialog.askopenfilename(filetypes=[("Excel or csv files", "*.xlsx *.xls *.csv")])
        if file_path:
            file_name = os.path.basename(file_path)
            alias = f"t{self.table_count + 1}"
            self.tables[alias] = file_path
            self.table_tree.insert("", "end", values=(file_name, alias, file_path))
            self.table_count += 1

    def delete_table(self):
        if not self.table_tree.selection():
            messagebox.showerror("Error", "请先选择要删除的数据源文件。")
            return
        selected_item = self.table_tree.selection()[0]
        alias = self.table_tree.item(selected_item)["values"][1]
        del self.tables[alias]
        self.table_tree.delete(selected_item)
        self.table_count -= 1

    def browse_save_path(self):
        save_path = filedialog.asksaveasfilename(defaultextension=".xlsx",
                                                 filetypes=[("Excel files", "*.xlsx")]) or self.entry_save_path.get()
        if save_path:
            self.entry_save_path.delete(0, tk.END)
            self.entry_save_path.insert(0, save_path)

    def execute_query(self):
        if not self.tables:
            messagebox.showerror("Error", "请先添加Excel文件。")
            return
        if not self.txt_query.get("1.0", tk.END).strip():
            messagebox.showerror("Error", "请输入SQL查询语句。")
            return
        if not self.entry_save_path.get():
            messagebox.showerror("Error", "请输入保存路径。")
            return
        try:
            # 读取Excel文件并加载到DuckDB中
            for alias, file_path in self.tables.items():
                if file_path.endswith(".csv"):
                    df = pd.read_csv(file_path, encoding='GBK')  # 指定编码
                elif file_path.endswith(".xls") or file_path.endswith(".xlsx"):
                    df = pd.read_excel(file_path)
                else:
                    raise ValueError("不支持的文件类型")
                self.conn.register(alias, df)
            query = self.txt_query.get("1.0", tk.END).strip()
            result = self.conn.execute(query).fetchdf()
            # 保存结果到新的Excel文件
            result.to_excel(self.entry_save_path.get(), index=False)
            messagebox.showinfo("Success", "查询执行成功,结果已保存到新的Excel文件。")
            # 自动打开生成的XLSX文件
            subprocess.Popen([self.entry_save_path.get()], shell=True)
        except Exception as e:
            messagebox.showerror("Error", f"执行查询时发生错误: {str(e)}")


if __name__ == "__main__":
    root = tk.Tk()
    app = CrossTableQueryTool(root)
    root.mainloop()

点评

欢迎交流  发表于 2024-12-2 21:34
daian 发表于 2024-12-2 11:44
z222 发表于 2024-12-2 12:24
感谢大佬的分享
yzpping 发表于 2024-12-2 12:56
   不能给打包好的工具吗?
醉酒听风 发表于 2024-12-2 13:03
不错,感谢分享,学习一下
jun269 发表于 2024-12-2 14:24
楼主,,谢谢您,我才开始学py,试了下打包,出现:
Traceback (most recent call last):
  File "C:\Users\lijunli\Desktop\excel.py", line 4, in <module>
    import pandas as pd
ModuleNotFoundError: No module named 'pandas'
咋个解决,谢谢
baomh 发表于 2024-12-2 14:29
jun269 发表于 2024-12-2 14:24
楼主,,谢谢您,我才开始学py,试了下打包,出现:
Traceback (most recent call last):
  File "C:%user ...

看起来没有安装pandas这个Python模块,或许可以试试
[Python] 纯文本查看 复制代码
pip install pandas
林释 发表于 2024-12-2 14:32
jun269 发表于 2024-12-2 14:24
楼主,,谢谢您,我才开始学py,试了下打包,出现:
Traceback (most recent call last):
  File "C:%user ...

这报错也不难理解呀,不然百度不是更快吗 ,缺少pandas库,pip install pandas
Cident 发表于 2024-12-2 14:50
可以可以,感谢大佬分享
adolphchen 发表于 2024-12-2 15:33

感谢大佬的分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-6-11 14:42

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表