🐍 第34课:GUI开发

—— tkinter:Python 内置 GUI 工具包

🏆 窗口+组件+事件+实用工具
✅ Python验证通过

📌 本课目标

1️⃣ tkinter 基础

import tkinter as tk
from tkinter import ttk, messagebox, filedialog

# 创建主窗口
root = tk.Tk()
root.title("我的第一个 GUI")
root.geometry("600x400")

# 标签
label = tk.Label(root, text="Hello, tkinter!", font=("Arial", 16))
label.pack(pady=20)

# 按钮
def on_click():
    messagebox.showinfo("提示", "按钮被点击了!")

button = tk.Button(root, text="点我", command=on_click, bg="#34d399", fg="white")
button.pack(pady=10)

# 输入框
entry = tk.Entry(root, width=30)
entry.pack(pady=5)
entry.insert(0, "请输入...")

# 文本框
text = tk.Text(root, width=50, height=10)
text.pack(pady=5)

# 运行主循环
# root.mainloop()  # 取消注释即可运行

2️⃣ 布局管理

import tkinter as tk
from tkinter import ttk

root = tk.Tk()
root.title("布局示例")

# ===== pack 布局 =====
frame_pack = tk.LabelFrame(root, text="Pack布局")
frame_pack.pack(fill="x", padx=10, pady=5)

tk.Label(frame_pack, text="顶部", bg="#34d399").pack(fill="x")
tk.Label(frame_pack, text="中间", bg="#7dd3fc").pack(fill="x", expand=True)
tk.Label(frame_pack, text="底部", bg="#fbbf24").pack(fill="x")

# ===== grid 布局 =====
frame_grid = tk.LabelFrame(root, text="Grid布局")
frame_grid.pack(fill="x", padx=10, pady=5)

tk.Label(frame_grid, text="用户名:").grid(row=0, column=0, sticky="e", padx=5, pady=5)
tk.Entry(frame_grid, width=20).grid(row=0, column=1, padx=5, pady=5)

tk.Label(frame_grid, text="密码:").grid(row=1, column=0, sticky="e", padx=5, pady=5)
tk.Entry(frame_grid, width=20, show="*").grid(row=1, column=1, padx=5, pady=5)

tk.Button(frame_grid, text="登录").grid(row=2, column=0, columnspan=2, pady=10)

# grid 配置列权重
frame_grid.columnconfigure(1, weight=1)

# root.mainloop()

3️⃣ ttk 现代组件

import tkinter as tk
from tkinter import ttk

root = tk.Tk()
root.title("ttk 组件")

# Notebook(标签页)
notebook = ttk.Notebook(root)
notebook.pack(fill="both", expand=True, padx=10, pady=10)

# 标签页1:表格
tab1 = ttk.Frame(notebook)
notebook.add(tab1, text="数据表")

tree = ttk.Treeview(tab1, columns=("name", "age", "city"), show="headings")
tree.heading("name", text="姓名")
tree.heading("age", text="年龄")
tree.heading("city", text="城市")
tree.column("name", width=100)
tree.column("age", width=80)
tree.column("city", width=100)

data = [("张三", 25, "北京"), ("李四", 30, "上海"), ("王五", 28, "广州")]
for item in data:
    tree.insert("", "end", values=item)
tree.pack(fill="both", expand=True)

# 标签页2:进度条
tab2 = ttk.Frame(notebook)
notebook.add(tab2, text="进度")

progress = ttk.Progressbar(tab2, mode="determinate", length=300)
progress.pack(pady=20)
progress["value"] = 65

# 标签页3:组合框
tab3 = ttk.Frame(notebook)
notebook.add(tab3, text="选择")

combo = ttk.Combobox(tab3, values=["Python", "Java", "Go", "Rust"])
combo.set("Python")
combo.pack(pady=20)

# Scale 滑块
scale = ttk.Scale(tab3, from_=0, to=100, orient="horizontal")
scale.pack(pady=10, fill="x", padx=20)

# root.mainloop()

4️⃣ 实战:剪贴板管理器

"""注意:以下代码需要在有显示器的环境中运行。
在无头服务器上无法运行 GUI,但逻辑代码可以验证。
"""

import tkinter as tk
from tkinter import ttk
import json
from pathlib import Path

class ClipboardManager:
    """剪贴板历史管理器"""
    
    HISTORY_FILE = Path.home() / ".clipboard_history.json"
    
    def __init__(self):
        self.history = self._load_history()
    
    def _load_history(self):
        if self.HISTORY_FILE.exists():
            with open(self.HISTORY_FILE) as f:
                return json.load(f)
        return []
    
    def _save_history(self):
        with open(self.HISTORY_FILE, "w") as f:
            json.dump(self.history, f, ensure_ascii=False, indent=2)
    
    def add(self, text):
        if text and text not in self.history:
            self.history.insert(0, text)
            self.history = self.history[:100]  # 最多100条
            self._save_history()
    
    def search(self, keyword):
        return [h for h in self.history if keyword.lower() in h.lower()]
    
    def clear(self):
        self.history = []
        self._save_history()

# GUI 界面(需要显示器)
class ClipboardApp:
    def __init__(self):
        self.manager = ClipboardManager()
        self.root = tk.Tk()
        self.root.title("剪贴板管理器")
        self.root.geometry("400x500")
        
        # 搜索框
        search_frame = ttk.Frame(self.root)
        search_frame.pack(fill="x", padx=10, pady=5)
        ttk.Label(search_frame, text="搜索:").pack(side="left")
        self.search_var = tk.StringVar()
        self.search_var.trace_add("write", lambda *_: self.filter_list())
        search_entry = ttk.Entry(search_frame, textvariable=self.search_var)
        search_entry.pack(side="left", fill="x", expand=True, padx=5)
        
        # 列表
        self.listbox = tk.Listbox(self.root, font=("Courier", 10))
        self.listbox.pack(fill="both", expand=True, padx=10, pady=5)
        self.listbox.bind("<<ListboxSelect>>", self.on_select)
        
        # 按钮
        btn_frame = ttk.Frame(self.root)
        btn_frame.pack(fill="x", padx=10, pady=5)
        ttk.Button(btn_frame, text="清除历史", command=self.clear).pack(side="right")
        
        self.refresh_list()
    
    def refresh_list(self):
        self.listbox.delete(0, "end")
        for item in self.manager.history:
            self.listbox.insert("end", item[:50])
    
    def filter_list(self):
        keyword = self.search_var.get()
        items = self.manager.search(keyword) if keyword else self.manager.history
        self.listbox.delete(0, "end")
        for item in items:
            self.listbox.insert("end", item[:50])
    
    def on_select(self, event):
        selection = self.listbox.curselection()
        if selection:
            idx = selection[0]
            text = self.manager.history[idx]
            self.root.clipboard_clear()
            self.root.clipboard_append(text)
    
    def clear(self):
        self.manager.clear()
        self.refresh_list()
    
    def run(self):
        self.root.mainloop()

5️⃣ 验证脚本

#!/usr/bin/env python3
"""第34课 GUI开发验证"""
import tkinter as tk
from tkinter import ttk

def test_tkinter_import():
    """tkinter 导入测试"""
    assert hasattr(tk, 'Tk')
    assert hasattr(tk, 'Label')
    assert hasattr(tk, 'Button')
    assert hasattr(tk, 'Entry')
    assert hasattr(tk, 'Text')
    print("✅ tkinter导入测试通过")

def test_ttk_components():
    """ttk 组件测试"""
    assert hasattr(ttk, 'Button')
    assert hasattr(ttk, 'Entry')
    assert hasattr(ttk, 'Label')
    assert hasattr(ttk, 'Treeview')
    assert hasattr(ttk, 'Notebook')
    assert hasattr(ttk, 'Combobox')
    assert hasattr(ttk, 'Progressbar')
    print("✅ ttk组件测试通过")

def test_clipboard_manager():
    """剪贴板管理器逻辑测试"""
    import json
    import tempfile
    import os
    
    class TestManager:
        def __init__(self):
            self.history = []
        
        def add(self, text):
            if text and text not in self.history:
                self.history.insert(0, text)
                self.history = self.history[:100]
        
        def search(self, keyword):
            return [h for h in self.history if keyword.lower() in h.lower()]
        
        def clear(self):
            self.history = []
    
    mgr = TestManager()
    mgr.add("Hello World")
    mgr.add("Python GUI")
    mgr.add("Hello Python")
    
    assert len(mgr.history) == 3
    assert mgr.search("Hello") == ["Hello Python", "Hello World"]
    
    mgr.add("Hello World")  # 重复不添加
    assert len(mgr.history) == 3
    
    mgr.clear()
    assert len(mgr.history) == 0
    print("✅ 剪贴板管理器测试通过")

def test_widget_creation():
    """控件创建测试(无需显示)"""
    root = tk.Tk()
    root.withdraw()  # 不显示窗口
    
    label = tk.Label(root, text="Test")
    button = tk.Button(root, text="Click")
    entry = tk.Entry(root)
    
    assert label.cget("text") == "Test"
    assert button.cget("text") == "Click"
    
    root.destroy()
    print("✅ 控件创建测试通过")

if __name__ == "__main__":
    test_tkinter_import()
    test_ttk_components()
    test_clipboard_manager()
    test_widget_creation()
    print("\n🎉 第34课全部验证通过!")

6️⃣ 实战:文件搜索工具

import tkinter as tk
from tkinter import ttk, filedialog
from pathlib import Path
import threading

class FileSearchApp:
    """文件搜索工具"""
    
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("文件搜索工具")
        self.root.geometry("700x500")
        self._build_ui()
    
    def _build_ui(self):
        # 搜索栏
        search_frame = ttk.Frame(self.root)
        search_frame.pack(fill="x", padx=10, pady=5)
        
        ttk.Label(search_frame, text="目录:").pack(side="left")
        self.dir_var = tk.StringVar(value=str(Path.home()))
        ttk.Entry(search_frame, textvariable=self.dir_var, width=40).pack(side="left", padx=5)
        ttk.Button(search_frame, text="浏览", command=self._browse).pack(side="left")
        
        ttk.Label(search_frame, text="关键词:").pack(side="left", padx=(10,0))
        self.keyword_var = tk.StringVar()
        ttk.Entry(search_frame, textvariable=self.keyword_var, width=20).pack(side="left", padx=5)
        ttk.Button(search_frame, text="搜索", command=self._search).pack(side="left")
        
        # 结果表格
        columns = ("path", "size", "modified")
        self.tree = ttk.Treeview(self.root, columns=columns, show="headings")
        self.tree.heading("path", text="路径")
        self.tree.heading("size", text="大小")
        self.tree.heading("modified", text="修改时间")
        self.tree.column("path", width=400)
        self.tree.pack(fill="both", expand=True, padx=10, pady=5)
        
        self.status_var = tk.StringVar(value="就绪")
        ttk.Label(self.root, textvariable=self.status_var).pack(fill="x", padx=10, pady=2)
    
    def _browse(self):
        d = filedialog.askdirectory()
        if d: self.dir_var.set(d)
    
    def _search(self):
        directory = self.dir_var.get()
        keyword = self.keyword_var.get()
        self.tree.delete(*self.tree.get_children())
        self.status_var.set("搜索中...")
        
        def do_search():
            count = 0
            for f in Path(directory).rglob("*"):
                if f.is_file() and (not keyword or keyword.lower() in f.name.lower()):
                    size = f.stat().st_size
                    from datetime import datetime
                    mtime = datetime.fromtimestamp(f.stat().st_mtime).strftime("%Y-%m-%d %H:%M")
                    self.tree.insert("", "end", values=(str(f), f"{size:,}", mtime))
                    count += 1
            self.status_var.set(f"找到 {count} 个文件")
        
        threading.Thread(target=do_search, daemon=True).start()
    
    def run(self):
        self.root.mainloop()

7️⃣ 实战:番茄钟计时器

import tkinter as tk
from tkinter import ttk, messagebox

class PomodoroTimer:
    """番茄钟计时器"""
    
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("番茄钟")
        self.root.geometry("300x250")
        self.running = False
        self.remaining = 25 * 60
        self.mode = "work"
        self._build_ui()
    
    def _build_ui(self):
        self.time_var = tk.StringVar(value="25:00")
        tk.Label(self.root, textvariable=self.time_var, 
                font=("Courier", 48, "bold"), fg="#34d399", bg="#0f172a").pack(pady=20)
        
        self.mode_var = tk.StringVar(value="工作时间")
        tk.Label(self.root, textvariable=self.mode_var, font=("Arial", 14)).pack()
        
        btn_frame = ttk.Frame(self.root)
        btn_frame.pack(pady=15)
        self.start_btn = ttk.Button(btn_frame, text="开始", command=self._toggle)
        self.start_btn.pack(side="left", padx=5)
        ttk.Button(btn_frame, text="重置", command=self._reset).pack(side="left", padx=5)
        
        setting_frame = ttk.Frame(self.root)
        setting_frame.pack(pady=5)
        ttk.Label(setting_frame, text="工作:").pack(side="left")
        self.work_min = tk.IntVar(value=25)
        ttk.Spinbox(setting_frame, from_=1, to=60, textvariable=self.work_min, width=3).pack(side="left")
        ttk.Label(setting_frame, text="分钟  休息:").pack(side="left")
        self.break_min = tk.IntVar(value=5)
        ttk.Spinbox(setting_frame, from_=1, to=30, textvariable=self.break_min, width=3).pack(side="left")
    
    def _toggle(self):
        if self.running:
            self.running = False
            self.start_btn.config(text="继续")
        else:
            self.running = True
            self.start_btn.config(text="暂停")
            self._tick()
    
    def _tick(self):
        if not self.running: return
        if self.remaining <= 0:
            self.running = False
            messagebox.showinfo("番茄钟", "时间到!")
            return
        mins, secs = divmod(self.remaining, 60)
        self.time_var.set(f"{mins:02d}:{secs:02d}")
        self.remaining -= 1
        self.root.after(1000, self._tick)
    
    def _reset(self):
        self.running = False
        self.remaining = self.work_min.get() * 60
        self.start_btn.config(text="开始")
        mins, secs = divmod(self.remaining, 60)
        self.time_var.set(f"{mins:02d}:{secs:02d}")
    
    def run(self):
        self.root.mainloop()

🔑 本课要点

  1. tkinter——Python 内置 GUI,无需安装额外依赖
  2. ttk 组件——现代化外观,Button/Entry/Treeview 等
  3. grid 布局——灵活的网格布局系统
  4. 事件绑定——bind() 绑定键盘鼠标事件
  5. 实用小工具——剪贴板管理器/计时器/文件搜索等