第180集:GUI项目实战

一、项目概述

在本集中,我们将结合之前学过的所有GUI编程知识,开发一个完整的任务管理器应用。这个应用将具备以下功能:

  • 任务的添加、编辑、删除和标记完成
  • 任务的分类管理
  • 任务的搜索和过滤
  • 任务数据的保存和加载
  • 直观的用户界面(包含菜单栏、工具栏、状态栏)
  • 美观的样式设计

通过这个项目,我们将学习如何将零散的GUI知识整合起来,开发一个功能完整、用户友好的应用程序。

二、项目设计

1. 功能模块设计

任务管理器应用主要包含以下功能模块:

模块名称 主要功能
界面布局 设计应用的整体布局,包括菜单、工具栏、主界面和状态栏
任务管理 实现任务的添加、编辑、删除、标记完成等核心功能
数据管理 负责任务数据的保存(JSON格式)和加载
搜索过滤 实现任务的搜索和按状态/分类过滤
界面交互 处理用户的各种交互操作,如点击、输入等

2. 界面设计

应用界面将采用经典的桌面应用布局:

  • 菜单栏:包含文件、编辑、查看、帮助等菜单
  • 工具栏:提供常用操作的快捷按钮
  • 主界面
    • 左侧:任务分类列表
    • 右侧:任务列表(显示任务标题、优先级、截止日期等)
    • 底部:任务详情编辑区域
  • 状态栏:显示当前任务数量和应用状态

三、项目实现

1. 项目结构

task_manager/
├── main.py          # 主程序入口
├── task_manager.py  # 任务管理器类
├── task.py          # 任务类定义
├── data/            # 数据文件目录
│   └── tasks.json   # 任务数据文件
└── README.md        # 项目说明

2. 任务类定义

首先,我们定义一个Task类来表示任务:

# task.py
import json
from datetime import datetime

class Task:
    def __init__(self, title, description="", priority="中", category="默认", due_date=None, completed=False):
        self.id = datetime.now().strftime("%Y%m%d%H%M%S%f")  # 生成唯一ID
        self.title = title
        self.description = description
        self.priority = priority  # 高、中、低
        self.category = category
        self.due_date = due_date
        self.completed = completed
        self.created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    
    def to_dict(self):
        """将任务转换为字典,用于保存到JSON文件"""
        return {
            "id": self.id,
            "title": self.title,
            "description": self.description,
            "priority": self.priority,
            "category": self.category,
            "due_date": self.due_date,
            "completed": self.completed,
            "created_at": self.created_at
        }
    
    @classmethod
    def from_dict(cls, data):
        """从字典创建任务对象"""
        task = cls(
            title=data["title"],
            description=data["description"],
            priority=data["priority"],
            category=data["category"],
            due_date=data["due_date"],
            completed=data["completed"]
        )
        task.id = data["id"]
        task.created_at = data["created_at"]
        return task

3. 任务管理器类

接下来,我们实现任务管理器类,负责任务的管理和数据的保存加载:

# task_manager.py
import json
import os
from task import Task

class TaskManager:
    def __init__(self, data_file="data/tasks.json"):
        self.data_file = data_file
        self.tasks = []
        self.load_tasks()
    
    def load_tasks(self):
        """从JSON文件加载任务数据"""
        if os.path.exists(self.data_file):
            try:
                with open(self.data_file, "r", encoding="utf-8") as f:
                    tasks_data = json.load(f)
                    self.tasks = [Task.from_dict(task_data) for task_data in tasks_data]
            except Exception as e:
                print(f"加载任务数据失败:{e}")
                self.tasks = []
        else:
            # 如果目录不存在,创建目录
            os.makedirs(os.path.dirname(self.data_file), exist_ok=True)
            self.tasks = []
    
    def save_tasks(self):
        """将任务数据保存到JSON文件"""
        try:
            with open(self.data_file, "w", encoding="utf-8") as f:
                tasks_data = [task.to_dict() for task in self.tasks]
                json.dump(tasks_data, f, ensure_ascii=False, indent=4)
            return True
        except Exception as e:
            print(f"保存任务数据失败:{e}")
            return False
    
    def add_task(self, task):
        """添加新任务"""
        self.tasks.append(task)
        return self.save_tasks()
    
    def update_task(self, task_id, **kwargs):
        """更新任务信息"""
        for task in self.tasks:
            if task.id == task_id:
                for key, value in kwargs.items():
                    if hasattr(task, key):
                        setattr(task, key, value)
                return self.save_tasks()
        return False
    
    def delete_task(self, task_id):
        """删除任务"""
        self.tasks = [task for task in self.tasks if task.id != task_id]
        return self.save_tasks()
    
    def get_tasks(self, category=None, completed=None, search_query=None):
        """获取任务列表,支持过滤"""
        filtered_tasks = self.tasks
        
        # 按分类过滤
        if category and category != "全部":
            filtered_tasks = [task for task in filtered_tasks if task.category == category]
        
        # 按完成状态过滤
        if completed is not None:
            filtered_tasks = [task for task in filtered_tasks if task.completed == completed]
        
        # 按搜索关键词过滤
        if search_query:
            search_query = search_query.lower()
            filtered_tasks = [task for task in filtered_tasks if 
                            search_query in task.title.lower() or 
                            search_query in task.description.lower()]
        
        return filtered_tasks
    
    def get_categories(self):
        """获取所有任务分类"""
        categories = set([task.category for task in self.tasks])
        return ["全部"] + list(categories)

4. 主程序实现

最后,我们实现主程序,创建GUI界面并将所有功能整合起来:

# main.py
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
from task_manager import TaskManager
from task import Task
from datetime import datetime

class TaskManagerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("任务管理器")
        self.root.geometry("1000x700")
        
        # 初始化任务管理器
        self.task_manager = TaskManager()
        
        # 设置主题样式
        self.style = ttk.Style()
        self.style.theme_use("clam")
        
        # 创建界面组件
        self.create_menu()
        self.create_toolbar()
        self.create_main_frame()
        self.create_statusbar()
        
        # 刷新界面
        self.refresh_categories()
        self.refresh_tasks()
    
    def create_menu(self):
        """创建菜单栏"""
        menubar = tk.Menu(self.root)
        
        # 文件菜单
        file_menu = tk.Menu(menubar, tearoff=0)
        file_menu.add_command(label="新建任务", accelerator="Ctrl+N", command=self.add_task)
        file_menu.add_command(label="保存", accelerator="Ctrl+S", command=self.save_tasks)
        file_menu.add_separator()
        file_menu.add_command(label="退出", command=self.root.quit)
        menubar.add_cascade(label="文件", menu=file_menu)
        
        # 编辑菜单
        edit_menu = tk.Menu(menubar, tearoff=0)
        edit_menu.add_command(label="编辑任务", accelerator="Ctrl+E", command=self.edit_task)
        edit_menu.add_command(label="删除任务", accelerator="Delete", command=self.delete_task)
        menubar.add_cascade(label="编辑", menu=edit_menu)
        
        # 查看菜单
        view_menu = tk.Menu(menubar, tearoff=0)
        self.show_completed_var = tk.BooleanVar(value=True)
        view_menu.add_checkbutton(label="显示已完成任务", variable=self.show_completed_var, command=self.refresh_tasks)
        menubar.add_cascade(label="查看", menu=view_menu)
        
        # 帮助菜单
        help_menu = tk.Menu(menubar, tearoff=0)
        help_menu.add_command(label="关于", command=self.show_about)
        menubar.add_cascade(label="帮助", menu=help_menu)
        
        # 绑定快捷键
        self.root.bind("<Control-n>", lambda event: self.add_task())
        self.root.bind("<Control-s>", lambda event: self.save_tasks())
        self.root.bind("<Control-e>", lambda event: self.edit_task())
        self.root.bind("<Delete>", lambda event: self.delete_task())
        
        self.root.config(menu=menubar)
    
    def create_toolbar(self):
        """创建工具栏"""
        toolbar = ttk.Frame(self.root)
        toolbar.pack(fill=tk.X, padx=5, pady=5)
        
        # 添加按钮
        self.add_btn = ttk.Button(toolbar, text="新建任务", command=self.add_task)
        self.add_btn.pack(side=tk.LEFT, padx=2)
        
        self.edit_btn = ttk.Button(toolbar, text="编辑任务", command=self.edit_task)
        self.edit_btn.pack(side=tk.LEFT, padx=2)
        
        self.delete_btn = ttk.Button(toolbar, text="删除任务", command=self.delete_task)
        self.delete_btn.pack(side=tk.LEFT, padx=2)
        
        self.complete_btn = ttk.Button(toolbar, text="标记完成", command=self.toggle_complete)
        self.complete_btn.pack(side=tk.LEFT, padx=2)
        
        # 分隔线
        ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=5)
        
        # 搜索框
        ttk.Label(toolbar, text="搜索:").pack(side=tk.LEFT, padx=2)
        self.search_var = tk.StringVar()
        self.search_entry = ttk.Entry(toolbar, textvariable=self.search_var, width=30)
        self.search_entry.pack(side=tk.LEFT, padx=2)
        self.search_entry.bind("<KeyRelease>", lambda event: self.refresh_tasks())
    
    def create_main_frame(self):
        """创建主界面框架"""
        main_frame = ttk.Frame(self.root)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        # 左侧:分类列表
        left_frame = ttk.LabelFrame(main_frame, text="任务分类")
        left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5)
        
        self.category_var = tk.StringVar(value="全部")
        self.category_listbox = tk.Listbox(left_frame, width=25, height=20)
        self.category_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        # 分类列表滚动条
        category_scrollbar = ttk.Scrollbar(left_frame, orient=tk.VERTICAL, command=self.category_listbox.yview)
        category_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.category_listbox.config(yscrollcommand=category_scrollbar.set)
        
        # 分类列表点击事件
        self.category_listbox.bind("<<ListboxSelect>>", self.on_category_select)
        
        # 右侧:任务列表和详情
        right_frame = ttk.Frame(main_frame)
        right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        # 任务列表
        task_list_frame = ttk.LabelFrame(right_frame, text="任务列表")
        task_list_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        # 任务列表树视图
        columns = ("id", "title", "priority", "due_date", "completed")
        self.task_tree = ttk.Treeview(task_list_frame, columns=columns, show="headings", height=15)
        
        # 设置列标题
        self.task_tree.heading("id", text="ID", width=50)
        self.task_tree.heading("title", text="标题", width=200)
        self.task_tree.heading("priority", text="优先级", width=80)
        self.task_tree.heading("due_date", text="截止日期", width=100)
        self.task_tree.heading("completed", text="状态", width=80)
        
        # 设置列对齐
        for col in columns:
            self.task_tree.column(col, anchor=tk.CENTER)
        
        # 添加滚动条
        tree_scrollbar = ttk.Scrollbar(task_list_frame, orient=tk.VERTICAL, command=self.task_tree.yview)
        self.task_tree.configure(yscrollcommand=tree_scrollbar.set)
        
        self.task_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
        tree_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        # 任务列表点击事件
        self.task_tree.bind("<<TreeviewSelect>>", self.on_task_select)
        
        # 任务详情
        detail_frame = ttk.LabelFrame(right_frame, text="任务详情")
        detail_frame.pack(fill=tk.BOTH, padx=5, pady=5)
        
        # 标题
        ttk.Label(detail_frame, text="标题:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=3)
        self.title_var = tk.StringVar()
        ttk.Entry(detail_frame, textvariable=self.title_var, width=50).grid(row=0, column=1, padx=5, pady=3)
        
        # 优先级
        ttk.Label(detail_frame, text="优先级:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=3)
        self.priority_var = tk.StringVar(value="中")
        priority_combo = ttk.Combobox(detail_frame, textvariable=self.priority_var, values=["高", "中", "低"], state="readonly")
        priority_combo.grid(row=1, column=1, padx=5, pady=3, sticky=tk.W)
        
        # 截止日期
        ttk.Label(detail_frame, text="截止日期:").grid(row=2, column=0, sticky=tk.W, padx=5, pady=3)
        self.due_date_var = tk.StringVar()
        ttk.Entry(detail_frame, textvariable=self.due_date_var, width=20).grid(row=2, column=1, padx=5, pady=3, sticky=tk.W)
        
        # 分类
        ttk.Label(detail_frame, text="分类:").grid(row=3, column=0, sticky=tk.W, padx=5, pady=3)
        self.category_entry_var = tk.StringVar()
        ttk.Entry(detail_frame, textvariable=self.category_entry_var, width=20).grid(row=3, column=1, padx=5, pady=3, sticky=tk.W)
        
        # 描述
        ttk.Label(detail_frame, text="描述:").grid(row=4, column=0, sticky=tk.NW, padx=5, pady=3)
        self.description_var = tk.StringVar()
        description_text = tk.Text(detail_frame, height=5, width=50)
        description_text.grid(row=4, column=1, padx=5, pady=3, sticky=tk.W)
        self.description_text = description_text
        
        # 保存按钮
        ttk.Button(detail_frame, text="保存修改", command=self.save_task_changes).grid(row=5, column=1, padx=5, pady=10, sticky=tk.E)
    
    def create_statusbar(self):
        """创建状态栏"""
        self.status_var = tk.StringVar(value="就绪")
        statusbar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
        statusbar.pack(side=tk.BOTTOM, fill=tk.X)
    
    def refresh_categories(self):
        """刷新分类列表"""
        categories = self.task_manager.get_categories()
        self.category_listbox.delete(0, tk.END)
        
        for category in categories:
            self.category_listbox.insert(tk.END, category)
        
        # 默认选择"全部"
        if "全部" in categories:
            self.category_listbox.selection_set(categories.index("全部"))
    
    def refresh_tasks(self):
        """刷新任务列表"""
        # 清空任务列表
        for item in self.task_tree.get_children():
            self.task_tree.delete(item)
        
        # 获取当前选择的分类
        selected_category = "全部"
        if self.category_listbox.curselection():
            selected_index = self.category_listbox.curselection()[0]
            selected_category = self.category_listbox.get(selected_index)
        
        # 获取搜索关键词
        search_query = self.search_var.get()
        
        # 获取任务列表
        completed_filter = None if self.show_completed_var.get() else False
        tasks = self.task_manager.get_tasks(
            category=selected_category,
            completed=completed_filter,
            search_query=search_query
        )
        
        # 添加任务到列表
        for task in tasks:
            status = "已完成" if task.completed else "未完成"
            self.task_tree.insert("", tk.END, values=(
                task.id,
                task.title,
                task.priority,
                task.due_date or "",
                status
            ))
        
        # 更新状态栏
        self.status_var.set(f"共 {len(tasks)} 个任务")
    
    def on_category_select(self, event):
        """分类列表选择事件"""
        self.refresh_tasks()
    
    def on_task_select(self, event):
        """任务列表选择事件"""
        selected_items = self.task_tree.selection()
        if not selected_items:
            return
        
        # 获取选中的任务ID
        selected_item = selected_items[0]
        task_id = self.task_tree.item(selected_item)["values"][0]
        
        # 查找对应的任务
        for task in self.task_manager.tasks:
            if task.id == task_id:
                # 显示任务详情
                self.title_var.set(task.title)
                self.priority_var.set(task.priority)
                self.due_date_var.set(task.due_date or "")
                self.category_entry_var.set(task.category)
                self.description_text.delete(1.0, tk.END)
                self.description_text.insert(tk.END, task.description)
                break
    
    def add_task(self):
        """添加新任务"""
        # 创建一个默认任务
        task = Task(
            title="新任务",
            description="",
            priority="中",
            category="默认",
            due_date=None,
            completed=False
        )
        
        # 添加到任务管理器
        if self.task_manager.add_task(task):
            # 刷新界面
            self.refresh_categories()
            self.refresh_tasks()
            self.status_var.set("任务添加成功")
    
    def edit_task(self):
        """编辑选中的任务"""
        selected_items = self.task_tree.selection()
        if not selected_items:
            messagebox.showwarning("警告", "请先选择一个任务")
            return
        
        # 这里可以打开一个编辑对话框,现在我们直接使用详情区域进行编辑
        messagebox.showinfo("提示", "请在下方任务详情区域修改任务信息,然后点击保存修改按钮")
    
    def delete_task(self):
        """删除选中的任务"""
        selected_items = self.task_tree.selection()
        if not selected_items:
            messagebox.showwarning("警告", "请先选择一个任务")
            return
        
        # 确认删除
        if messagebox.askyesno("确认", "确定要删除选中的任务吗?"):
            # 获取选中的任务ID
            selected_item = selected_items[0]
            task_id = self.task_tree.item(selected_item)["values"][0]
            
            # 删除任务
            if self.task_manager.delete_task(task_id):
                # 刷新界面
                self.refresh_categories()
                self.refresh_tasks()
                # 清空详情区域
                self.clear_task_details()
                self.status_var.set("任务删除成功")
    
    def toggle_complete(self):
        """切换任务完成状态"""
        selected_items = self.task_tree.selection()
        if not selected_items:
            messagebox.showwarning("警告", "请先选择一个任务")
            return
        
        # 获取选中的任务ID
        selected_item = selected_items[0]
        task_id = self.task_tree.item(selected_item)["values"][0]
        
        # 查找对应的任务
        for task in self.task_manager.tasks:
            if task.id == task_id:
                # 切换完成状态
                new_status = not task.completed
                if self.task_manager.update_task(task_id, completed=new_status):
                    # 刷新界面
                    self.refresh_tasks()
                    self.status_var.set(f"任务已{"标记为完成" if new_status else "标记为未完成"}")
                break
    
    def save_task_changes(self):
        """保存任务修改"""
        selected_items = self.task_tree.selection()
        if not selected_items:
            messagebox.showwarning("警告", "请先选择一个任务")
            return
        
        # 获取选中的任务ID
        selected_item = selected_items[0]
        task_id = self.task_tree.item(selected_item)["values"][0]
        
        # 获取修改后的任务信息
        title = self.title_var.get()
        priority = self.priority_var.get()
        due_date = self.due_date_var.get() or None
        category = self.category_entry_var.get() or "默认"
        description = self.description_text.get(1.0, tk.END).strip()
        
        # 验证标题
        if not title:
            messagebox.showwarning("警告", "任务标题不能为空")
            return
        
        # 更新任务
        if self.task_manager.update_task(task_id, 
                                       title=title, 
                                       priority=priority, 
                                       due_date=due_date, 
                                       category=category, 
                                       description=description):
            # 刷新界面
            self.refresh_categories()
            self.refresh_tasks()
            self.status_var.set("任务修改保存成功")
    
    def save_tasks(self):
        """手动保存任务数据"""
        if self.task_manager.save_tasks():
            self.status_var.set("任务数据保存成功")
        else:
            self.status_var.set("任务数据保存失败")
    
    def clear_task_details(self):
        """清空任务详情区域"""
        self.title_var.set("")
        self.priority_var.set("中")
        self.due_date_var.set("")
        self.category_entry_var.set("")
        self.description_text.delete(1.0, tk.END)
    
    def show_about(self):
        """显示关于对话框"""
        about_text = "任务管理器 v1.0\n\n一个基于Python Tkinter开发的简单任务管理应用\n\n用于管理日常任务,支持添加、编辑、删除任务,\n以及按分类、状态进行过滤和搜索。"
        messagebox.showinfo("关于任务管理器", about_text)

# 运行应用
if __name__ == "__main__":
    root = tk.Tk()
    app = TaskManagerApp(root)
    root.mainloop()

四、项目测试和运行

1. 创建项目文件

将上面的代码分别保存为三个文件:

  • task.py:任务类定义
  • task_manager.py:任务管理器类
  • main.py:主程序入口

2. 运行应用

在命令行中执行以下命令运行应用:

python main.py

3. 测试功能

运行应用后,测试以下功能:

  1. 添加任务:点击"新建任务"按钮或按Ctrl+N
  2. 编辑任务:选择一个任务,在详情区域修改后点击"保存修改"
  3. 删除任务:选择一个任务,点击"删除任务"按钮或按Delete键
  4. 标记完成:选择一个任务,点击"标记完成"按钮
  5. 分类管理:在任务详情中修改分类,分类列表会自动更新
  6. 搜索过滤:在搜索框中输入关键词,任务列表会实时过滤
  7. 显示/隐藏已完成任务:在"查看"菜单中切换
  8. 保存数据:任务数据会自动保存到data/tasks.json文件

五、项目打包

我们可以使用PyInstaller将任务管理器应用打包成可执行文件:

pyinstaller -F -w -i "icon.ico" main.py

其中,icon.ico是应用的图标文件(可选)。

六、项目扩展

这个任务管理器应用可以进一步扩展以下功能:

  1. 任务提醒:添加任务截止日期提醒功能
  2. 数据备份:支持任务数据的导入和导出
  3. 主题切换:允许用户切换应用的主题样式
  4. 任务统计:添加任务完成情况的统计图表
  5. 多人协作:支持任务的共享和协作编辑
  6. 云同步:将任务数据同步到云端

七、总结

通过本集的学习,我们开发了一个功能完整的任务管理器应用,整合了之前学过的所有GUI编程知识,包括:

  • 窗口和界面布局设计
  • 菜单和工具栏的创建
  • 事件处理和快捷键绑定
  • 数据的保存和加载(JSON格式)
  • 列表和树视图的使用
  • 对话框和消息提示
  • 界面样式和用户体验优化

这个项目展示了如何将零散的GUI知识整合起来,开发一个实用的桌面应用程序。通过不断练习和扩展,你可以开发出更加复杂和功能丰富的Python GUI应用。

至此,我们的GUI编程部分已经全部完成。接下来,我们将进入Python测试与调试的学习,学习如何编写和运行测试,以及如何调试Python程序。

« 上一篇 打包发布 下一篇 » 测试概念