基于Tkinter的CSV文件读写与可视化工具开发实战

在现代办公和数据处理中,CSV文件因其简洁的格式和广泛的应用性,成为了数据交换和存储的常用方式。然而,传统的CSV文件处理工具往往功能单一,界面复杂,对于非专业人士来说存在一定的学习曲线。为了解决这个问题,我们开发了一款基于Tkinter的CSV文件读写与可视化工具。该工具提供了一个简洁直观的图形用户界面(GUI),使得用户无需深入了解CSV文件处理原理即可轻松完成CSV文件的读写、编辑、查询和可视化等操作。本文将详细介绍这款工具的开发过程及其主要功能。

一、项目背景与需求分析

1.1 项目背景

在数据分析和处理的日常工作中,CSV文件因其易于生成和解析的特性,被广泛应用于数据交换和存储。然而,随着数据量的增加和复杂度的提高,传统的CSV文件处理工具已经无法满足用户的需求。因此,开发一款功能全面、易于使用的CSV文件处理工具显得尤为重要。

1.2 需求分析

根据用户的实际需求,我们确定了以下主要功能:

  • CSV文件读写:支持CSV文件的打开、保存和另存为操作,同时能够处理不同编码格式的CSV文件。
  • 数据编辑:支持数据的增删改查操作,包括添加行、删除行、添加列和修改列数据类型等。
  • 数据查询:支持基于列的数据筛选和排序操作,方便用户快速定位所需数据。
  • 数据可视化:支持折线图、柱状图和散点图等多种图表类型的生成,帮助用户更直观地理解数据。

二、技术选型与架构设计

2.1 技术选型

为了实现上述功能,我们选择了以下技术栈:

  • Tkinter:作为Python的标准GUI库,Tkinter提供了丰富的控件和布局管理器,使得构建复杂的GUI界面变得简单直观。
  • Pandas:作为Python的数据分析库,Pandas提供了强大的数据处理和分析功能,能够轻松处理CSV文件中的数据。
  • Matplotlib:作为Python的绘图库,Matplotlib提供了丰富的绘图函数和接口,使得绘制各种静态、动态和交互式的图表变得简单快捷。

2.2 架构设计

整个工具采用MVC(Model-View-Controller)架构进行设计,以提高代码的可维护性和可扩展性。具体架构如下:

  • Model:负责数据的存储和处理,包括CSV文件的读写、数据的增删改查等操作。
  • View:负责用户界面的展示,包括各种控件的布局和样式设置。
  • Controller:负责处理用户的输入和事件,调用Model和View进行相应的操作。

三、主要功能实现

3.1 CSV文件读写

CSV文件读写是工具的基础功能之一。通过Tkinter的文件对话框,用户可以轻松选择并打开CSV文件。同时,工具支持多种编码格式的CSV文件处理,包括UTF-8、GBK、GB2312、GB18030和Big5等。在保存CSV文件时,用户可以选择不同的编码格式进行保存。

3.2 数据编辑

数据编辑功能包括添加行、删除行、添加列和修改列数据类型等操作。通过Tkinter的Treeview控件,用户可以直观地查看和编辑CSV文件中的数据。同时,工具提供了自动更正数据类型的功能,能够根据用户输入的内容自动将单元格内容转换为最适合的数据类型。

3.3 数据查询

数据查询功能允许用户基于列进行数据筛选和排序操作。通过Tkinter的Combobox控件,用户可以选择要筛选或排序的列,并输入相应的条件或顺序。工具会根据用户的输入实时更新显示的数据。

3.4 数据可视化

数据可视化功能支持折线图、柱状图和散点图等多种图表类型的生成。用户可以通过选择X轴和Y轴的数据列来生成相应的图表。工具还提供了丰富的图表配置选项,如标题、轴标签、刻度标签等,以满足用户的不同需求。

四、界面设计与用户体验

4.1 界面设计

工具的界面设计简洁直观,符合现代审美标准。通过Ttk(Themed Tkinter)库,我们美化了界面控件的样式,使其更加美观大方。同时,我们根据用户的使用习惯和需求,合理布局了各个控件,使得用户能够轻松找到所需的功能。

4.2 用户体验

为了提高用户体验,我们采取了以下措施:

  • 响应式设计:根据用户的屏幕大小和分辨率自动调整界面大小,确保在不同设备上都能获得良好的视觉效果。
  • 提示信息:在用户进行关键操作时提供必要的提示信息,帮助用户正确完成操作。
  • 错误处理:对用户输入的错误数据进行处理,并给出相应的错误提示,避免程序崩溃或数据丢失。

五、性能优化与扩展性

5.1 性能优化

为了提高工具的性能,我们采取了以下措施:

  • 异步处理:对于耗时的操作(如打开大文件、生成复杂图表等),我们采用异步处理的方式,避免界面卡顿或假死现象。
  • 缓存机制:对于频繁访问的数据(如CSV文件中的数据),我们采用缓存机制来减少磁盘I/O操作,提高数据访问速度。

5.2 扩展性

为了增加工具的扩展性,我们采取了以下措施:

  • 模块化设计:将工具的功能模块进行拆分,每个模块独立实现一个特定的功能,方便后续的功能扩展和维护。
  • 插件机制:提供插件接口,允许用户根据需要添加新的功能模块或扩展现有功能。

六、总结与展望

本文介绍了一款基于Tkinter的CSV文件读写与可视化工具的开发过程及其主要功能。通过集成Tkinter、Pandas和Matplotlib等库,工具实现了CSV文件的读写、编辑、查询和可视化等操作。同时,工具采用了响应式设计、提示信息和错误处理等措施来提高用户体验。未来,我们将继续优化和完善工具的功能和性能,增加更多的数据处理和分析功能,以满足用户的不同需求。希望本文能够为读者提供有益的参考和借鉴,推动CSV文件处理技术的发展和应用。

完整代码:

import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import ctypes
from matplotlib.font_manager import FontProperties
import matplotlib
matplotlib.rcParams['font.sans-serif'] = ['SimSun']  # 设置全局字体
matplotlib.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

class CSVVisualizerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("CSV文件读写和可视化工具 v1.0, 作者QQ1248693038")

        # 获取屏幕宽度和高度
        screen_width = root.winfo_screenwidth()
        screen_height = root.winfo_screenheight()

        # 设置窗口大小为屏幕的95%
        window_width = int(2560)
        window_height = int(1536)
        self.root.geometry(f"{window_width}x{window_height}")

        # 设置DPI感知
        ctypes.windll.shcore.SetProcessDpiAwareness(1)

        # 获取Windows缩放设置
        self.scale_factor = ctypes.windll.shcore.GetScaleFactorForDevice(0) / 100

        # 设置统一字体大小
        self.font_size = int(12 * self.scale_factor)
        self.default_font = ("SimSun", self.font_size)

        # 更新中文字体设置
        self.chinese_font = 'SimSun'
        plt.rcParams['font.sans-serif'] = [self.chinese_font]
        plt.rcParams['axes.unicode_minus'] = False

        # 初始化样式
        self.style = ttk.Style(self.root)
        self.style.configure('.', font=self.default_font)

        # 设置Combobox样式
        self.style.configure('TCombobox', padding=(0, int(5 * self.scale_factor)))
        self.root.option_add('*TCombobox*Listbox.font', self.default_font)

        # 创建菜单栏
        self.create_menu()

        # 创建主框架
        self.main_frame = ttk.Frame(self.root, padding=int(20 * self.scale_factor))
        self.main_frame.pack(fill=tk.BOTH, expand=True)

        # 创建表格
        self.create_table()

        # 数据
        self.df = None
        self.current_file = None
        self.current_encoding = None

    def create_menu(self):
        menubar = tk.Menu(self.root)
        self.root.config(menu=menubar)

        file_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="文件", menu=file_menu, font=self.default_font)
        file_menu.add_command(label="新建CSV", command=self.new_csv, font=self.default_font)
        file_menu.add_command(label="打开CSV", command=self.open_csv, font=self.default_font)
        file_menu.add_command(label="保存", command=self.save_csv, font=self.default_font)
        file_menu.add_command(label="另存为", command=self.save_as_csv, font=self.default_font)
        file_menu.add_separator()
        file_menu.add_command(label="退出", command=self.root.quit, font=self.default_font)

        edit_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="编辑", menu=edit_menu, font=self.default_font)
        edit_menu.add_command(label="添加行", command=self.add_row, font=self.default_font)
        edit_menu.add_command(label="删除行", command=self.delete_row, font=self.default_font)
        edit_menu.add_command(label="添加列", command=self.add_column, font=self.default_font)
        edit_menu.add_command(label="修改列数据类型", command=self.change_column_type, font=self.default_font)

        chart_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="图表", menu=chart_menu, font=self.default_font)
        chart_menu.add_command(label="折线图", command=lambda: self.select_axes("line"), font=self.default_font)
        chart_menu.add_command(label="柱状图", command=lambda: self.select_axes("bar"), font=self.default_font)
        chart_menu.add_command(label="散点图", command=lambda: self.select_axes("scatter"), font=self.default_font)

    def create_table(self):
        # 创建表格框架
        self.table_frame = ttk.Frame(self.main_frame)
        self.table_frame.pack(fill=tk.BOTH, expand=True)

        # 创建Treeview
        self.tree = ttk.Treeview(self.table_frame, style="Treeview")
        self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        # 设置Treeview样式
        self.style.configure("Treeview", rowheight=int(30 * self.scale_factor), font=self.default_font)
        self.style.configure("Treeview.Heading", font=self.default_font)

        # 添加垂直滚动条
        vsb = ttk.Scrollbar(self.table_frame, orient="vertical", command=self.tree.yview)
        vsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.configure(yscrollcommand=vsb.set)

        # 绑定双击事件
        self.tree.bind("<Double-1>", self.on_double_click)

    def new_csv(self):
        # 创建新的DataFrame
        self.df = pd.DataFrame(columns=['Column1', 'Column2', 'Column3'])
        self.update_table()
        messagebox.showinfo("成功", "已创建新的CSV文件")

    def open_csv(self):
        file_path = filedialog.askopenfilename(filetypes=[("CSV files", "*.csv")])
        if file_path:
            encodings = ['utf-8', 'gbk', 'gb2312', 'gb18030', 'big5']
            for encoding in encodings:
                try:
                    self.df = pd.read_csv(file_path, encoding=encoding)
                    self.update_table()
                    self.current_file = file_path
                    self.current_encoding = encoding
                    messagebox.showinfo("成功", f"CSV文件已成功加载 (编码: {encoding})")
                    return
                except UnicodeDecodeError:
                    continue
                except Exception as e:
                    messagebox.showerror("错误", f"无法加载CSV文件: {str(e)}")
                    return

            # 如果所有编码都失败,让用户选择编码
            encoding = self.ask_encoding()
            if encoding:
                try:
                    self.df = pd.read_csv(file_path, encoding=encoding)
                    self.update_table()
                    self.current_file = file_path
                    self.current_encoding = encoding
                    messagebox.showinfo("成功", f"CSV文件已成功加载 (编码: {encoding})")
                except Exception as e:
                    messagebox.showerror("错误", f"无法加载CSV文件: {str(e)}")

    def save_csv(self):
        if self.df is None:
            messagebox.showerror("错误", "没有数据可保存")
            return

        if self.current_file is None:
            self.save_as_csv()  # 如果是新文件,则调用另存为
        else:
            try:
                self.df.to_csv(self.current_file, index=False, encoding=self.current_encoding)
                messagebox.showinfo("成功", f"CSV文件已成功保存到原位置")
            except Exception as e:
                messagebox.showerror("错误", f"无法保存CSV文件: {str(e)}")

    def save_as_csv(self):
        if self.df is None:
            messagebox.showerror("错误", "没有数据可保存")
            return

        file_path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV files", "*.csv")])
        if file_path:
            encoding = self.ask_encoding()
            if encoding:
                try:
                    self.df.to_csv(file_path, index=False, encoding=encoding)
                    self.current_file = file_path
                    self.current_encoding = encoding
                    messagebox.showinfo("成功", f"CSV文件已成功保存 (编码: {encoding})")
                except Exception as e:
                    messagebox.showerror("错误", f"无法保存CSV文件: {str(e)}")

    def ask_encoding(self):
        encodings = ['utf-8', 'gbk', 'gb2312', 'gb18030', 'big5', 'ascii', 'latin1']
        encoding = simpledialog.askstring("选择编码", "请选择文件编码:", initialvalue="utf-8")
        return encoding if encoding in encodings else None

    def update_table(self):
        # 清除现有数据
        for i in self.tree.get_children():
            self.tree.delete(i)

        # 设置列
        self.tree["columns"] = list(self.df.columns)
        self.tree["show"] = "headings"

        for column in self.df.columns:
            self.tree.heading(column, text=column)
            self.tree.column(column, width=int(200 * self.scale_factor))

        # 插入数据
        for i, row in self.df.iterrows():
            self.tree.insert("", "end", values=list(row))

    def on_double_click(self, event):
        item = self.tree.selection()[0]
        column = self.tree.identify_column(event.x)
        col_num = int(column.replace("#", "")) - 1

        value = self.tree.item(item, "values")[col_num]

        # 创建编辑窗口
        edit_window = tk.Toplevel(self.root)
        edit_window.title("编辑单元格")
        edit_window.geometry(f"{int(400 * self.scale_factor)}x{int(150 * self.scale_factor)}")

        entry = ttk.Entry(edit_window, font=self.default_font)
        entry.insert(0, value)
        entry.pack(padx=int(20 * self.scale_factor), pady=int(20 * self.scale_factor), fill=tk.X)
        entry.focus_set()

        def save_edit():
            new_value = entry.get()
            # 自动更正数据类型
            new_value = self.auto_correct_type(new_value)
            self.tree.item(item, values=self.tree.item(item, "values")[:col_num] + (new_value,) + self.tree.item(item, "values")[col_num + 1:])
            row_num = self.tree.index(item)
            self.df.iloc[row_num, col_num] = new_value
            self.update_column_type(col_num)
            edit_window.destroy()

        save_button = ttk.Button(edit_window, text="保存", command=save_edit, style='TButton')
        save_button.pack(pady=int(10 * self.scale_factor))

        edit_window.bind("<Return>", lambda e: save_edit())

    def auto_correct_type(self, value):
        try:
            # 尝试转换为整数
            return int(value)
        except ValueError:
            try:
                # 尝试转换为浮点数
                return float(value)
            except ValueError:
                # 如果都失败,保持为字符串
                return value

    def add_row(self):
        if self.df is None:
            messagebox.showerror("错误", "没有加载数据")
            return

        new_row = pd.Series(['' for _ in range(len(self.df.columns))], index=self.df.columns)
        self.df = pd.concat([self.df, pd.DataFrame([new_row])], ignore_index=True)
        self.update_table()

    def update_column_type(self, col_num):
        column = self.df.columns[col_num]
        # 尝试将列转换为最适合的数据类型
        self.df[column] = pd.to_numeric(self.df[column], errors='ignore')

    def add_column(self):
        if self.df is None:
            messagebox.showerror("错误", "没有加载数据")
            return

        column_name = simpledialog.askstring("新列名", "请输入新列名:")
        if column_name:
            self.df[column_name] = ''
            self.update_table()

    def select_axes(self, chart_type):
        if self.df is None:
            messagebox.showerror("错误", "没有数据可绘制")
            return

        # 创建选择轴的窗口
        select_window = tk.Toplevel(self.root)
        select_window.title("选择轴")
        select_window.geometry(f"{int(400 * self.scale_factor)}x{int(250 * self.scale_factor)}")

        ttk.Label(select_window, text="选择X轴:", font=self.default_font).pack(pady=5)
        x_axis = ttk.Combobox(select_window, values=list(self.df.columns), font=self.default_font, state="readonly")
        x_axis.pack(pady=5)

        ttk.Label(select_window, text="选择Y轴:", font=self.default_font).pack(pady=5)
        y_axis = ttk.Combobox(select_window, values=list(self.df.columns), font=self.default_font, state="readonly")
        y_axis.pack(pady=5)

        def confirm():
            x = x_axis.get()
            y = y_axis.get()
            if x and y:
                self.plot_chart(chart_type, x, y)
                select_window.destroy()
            else:
                messagebox.showerror("错误", "请选择X轴和Y轴")

        ttk.Button(select_window, text="确认", command=confirm).pack(pady=20)

    def change_column_type(self):
        if self.df is None:
            messagebox.showerror("错误", "没有加载数据")
            return

        # 创建选择列的窗口
        select_window = tk.Toplevel(self.root)
        select_window.title("修改列数据类型")
        select_window.geometry(f"{int(400 * self.scale_factor)}x{int(300 * self.scale_factor)}")

        ttk.Label(select_window, text="选择列:", font=self.default_font).pack(pady=5)
        column = ttk.Combobox(select_window, values=list(self.df.columns), font=self.default_font, state="readonly")
        column.pack(pady=5)

        ttk.Label(select_window, text="选择新数据类型:", font=self.default_font).pack(pady=5)
        data_types = ['int', 'float', 'str', 'bool', 'datetime']
        new_type = ttk.Combobox(select_window, values=data_types, font=self.default_font, state="readonly")
        new_type.pack(pady=5)

        def confirm():
            col = column.get()
            dtype = new_type.get()
            if col and dtype:
                try:
                    if dtype == 'datetime':
                        self.df[col] = pd.to_datetime(self.df[col], errors='coerce')
                    elif dtype == 'bool':
                        self.df[col] = self.df[col].astype(str).map({'True': True, 'False': False})
                    else:
                        self.df[col] = self.df[col].astype(dtype)
                    self.update_table()
                    messagebox.showinfo("成功", f"列 '{col}' 的数据类型已更改为 {dtype}")
                    select_window.destroy()
                except Exception as e:
                    messagebox.showerror("错误", f"无法更改数据类型: {str(e)}")
            else:
                messagebox.showerror("错误", "请选择列和新数据类型")

        ttk.Button(select_window, text="确认", command=confirm).pack(pady=20)

    def delete_row(self):
        if self.df is None:
            messagebox.showerror("错误", "没有加载数据")
            return

        selected_items = self.tree.selection()
        if not selected_items:
            messagebox.showerror("错误", "请选择要删除的行")
            return

        if messagebox.askyesno("确认", "是否确定要删除选中的行?"):
            for item in selected_items:
                index = self.tree.index(item)
                self.df = self.df.drop(self.df.index[index])
                self.df = self.df.reset_index(drop=True)
            self.update_table()
            messagebox.showinfo("成功", "已删除选中的行")

    def plot_chart(self, chart_type, x_axis, y_axis):
        if not pd.api.types.is_numeric_dtype(self.df[y_axis]):
            messagebox.showerror("错误", "Y轴必须是数值类型")
            return

        chart_window = tk.Toplevel(self.root)
        chart_window.title(f"{chart_type.capitalize()} Chart")
        chart_window.geometry(f"{int(1000 * self.scale_factor)}x{int(800 * self.scale_factor)}")

        # 调整图表大小,减小高度
        fig, ax = plt.subplots(figsize=(12, 5), dpi=100)
        canvas = FigureCanvasTkAgg(fig, master=chart_window)

        if chart_type == "line":
            self.df.plot(kind='line', x=x_axis, y=y_axis, ax=ax)
        elif chart_type == "bar":
            self.df.plot(kind='bar', x=x_axis, y=y_axis, ax=ax)
        elif chart_type == "scatter":
            self.df.plot(kind='scatter', x=x_axis, y=y_axis, ax=ax)

        # 设置标题和轴标签,使用中文字体
        ax.set_title(f"{chart_type.capitalize()} Chart", fontproperties=self.chinese_font)
        ax.set_xlabel(x_axis, fontproperties=self.chinese_font)
        ax.set_ylabel(y_axis, fontproperties=self.chinese_font)

        # 设置刻度标签字体
        ax.tick_params(axis='both', which='major', labelsize=10)
        for label in ax.get_xticklabels() + ax.get_yticklabels():
            label.set_fontproperties(self.chinese_font)

        # 调整图表位置,增加底部边距
        plt.subplots_adjust(top=0.9, bottom=0.3)

        # 自动调整布局
        plt.tight_layout()

        canvas.draw()
        canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

        # 添加工具栏
        from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk
        toolbar = NavigationToolbar2Tk(canvas, chart_window)
        toolbar.update()
        canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)


if __name__ == "__main__":
    # 设置 Matplotlib 的全局字体
    matplotlib.rcParams['font.sans-serif'] = ['SimSun']
    matplotlib.rcParams['axes.unicode_minus'] = False
    matplotlib.rcParams['font.size'] = 24

    root = tk.Tk()
    app = CSVVisualizerApp(root)
    root.mainloop()

使用展示:

该展示显示了一个假设的洗浴中心各项服务的水深柱状图,其他功能大家复制代码运行即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值