适用人群:零基础到进阶开发者
目标:系统掌握 wxPython 的核心概念、布局、事件、绘图、多线程、数据绑定、打包发布等实战能力。
# 目录
- 目录
- 简介与环境准备
- 第一个 wxPython 程序
- 应用结构与主循环
- 常用控件速览
- 布局管理(Sizer)
- 事件机制与事件绑定
- 菜单、工具栏与状态栏
- 对话框与文件操作
- 绘图基础与自定义控件
- 数据模型与列表 / 树控件
- 线程与异步任务
- 定时器与后台任务
- 国际化与字体
- 主题与样式定制
- 应用配置与持久化
- 日志与错误处理
- 打包发布
- 项目结构与实战建议
- 综合型小项目例子(完整代码)
- 中项目例子(完整代码)
- 常见坑与排错技巧
- 延伸阅读
- 结语
# 简介与环境准备
wxPython 是基于 C++ 的 wxWidgets 的 Python 绑定,跨平台(Windows/macOS/Linux),原生控件风格,适合构建桌面应用。
# 安装
pip install wxPython |
Windows 上安装可能需要较长时间或使用预编译版本。
# 版本选择建议
- 新项目优先使用 wxPython 4.x(Phoenix 分支)。
- Python 版本建议 3.8+,避免过旧版本导致兼容问题。
# 虚拟环境(推荐)
python -m venv .venv | |
source .venv/bin/activate # Windows: .venv\Scripts\activate | |
pip install wxPython |
# 验证安装
python -c "import wx; print(wx.version())" |
# 基本概念
- wx.App:应用对象,管理主循环
- wx.Frame:主窗口
- wx.Panel:容器控件,常作为布局根
- wx.Sizer:布局管理器
- 事件机制:用户操作和系统消息响应
# 常用方法速查(入门必会)
Show():显示窗口Close():关闭窗口SetTitle():设置窗口标题SetSize()/SetMinSize():设置窗口大小 / 最小尺寸Centre():窗口居中Layout():触发布局刷新Refresh():触发重绘
# 第一个 wxPython 程序
import wx | |
class MainFrame(wx.Frame): | |
def __init__(self): | |
super().__init__(None, title="Hello wxPython", size=(400, 300)) | |
panel = wx.Panel(self) | |
label = wx.StaticText(panel, label="你好,wxPython!", pos=(20, 20)) | |
self.Centre() | |
if __name__ == "__main__": | |
app = wx.App() | |
frame = MainFrame() | |
frame.Show() | |
app.MainLoop() |
# 带布局与事件的完整最小例子
import wx | |
class MainFrame(wx.Frame): | |
def __init__(self): | |
super().__init__(None, title="计数器", size=(320, 200)) | |
panel = wx.Panel(self) | |
self.count = 0 | |
self.label = wx.StaticText(panel, label="计数:0") | |
btn_inc = wx.Button(panel, label="+1") | |
btn_reset = wx.Button(panel, label="重置") | |
sizer = wx.BoxSizer(wx.VERTICAL) | |
sizer.Add(self.label, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 10) | |
sizer.Add(btn_inc, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 5) | |
sizer.Add(btn_reset, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 5) | |
panel.SetSizer(sizer) | |
btn_inc.Bind(wx.EVT_BUTTON, self.on_inc) | |
btn_reset.Bind(wx.EVT_BUTTON, self.on_reset) | |
self.Centre() | |
def on_inc(self, event): | |
self.count += 1 | |
self.label.SetLabel(f"计数:{self.count}") | |
def on_reset(self, event): | |
self.count = 0 | |
self.label.SetLabel("计数:0") | |
if __name__ == "__main__": | |
app = wx.App() | |
frame = MainFrame() | |
frame.Show() | |
app.MainLoop() |
上面的小示例,用一个 wx.Frame 类定义好 UI 界面。在这个继承了 wx.Frame 的类中,用 BoxSizer 进行布局,加入 StaticText、Button 等界面控件,然后用 Bind 将控件的响应与计数事件 on_inc、onreset 联系起来。
# 应用结构与主循环
典型结构:
- 创建
wx.App - 创建
wx.Frame - 绑定事件
- 进入
MainLoop()
注意:
- UI 只能在主线程更新
MainLoop()只能调用一次
# 生命周期要点
wx.App创建后,MainLoop()驱动消息循环wx.Frame关闭时触发wx.EVT_CLOSE- 应用退出前可做清理:停止线程、保存配置等
什么是生命周期?
可以把它理解为 “应用从启动到退出的全过程”。在 wxPython 中,生命周期指:创建应用对象与窗口 → 进入
MainLoop()处理事件 → 窗口关闭触发清理逻辑 → 应用退出。了解生命周期能帮助你把初始化、事件处理、资源释放放在正确的位置。
# 最佳实践
- 在
wx.App子类中集中初始化资源 - 主窗口负责 UI,业务逻辑放到独立模块
- 用
wx.CallAfter或wx.PostEvent安全更新 UI
# App/Frame 常用方法
wx.App.OnInit():应用初始化入口wx.App.MainLoop():进入消息循环wx.Frame.CreateStatusBar():创建状态栏wx.Frame.SetMenuBar():设置菜单栏wx.Frame.CreateToolBar():创建工具栏wx.Frame.SetIcon():设置窗口图标
# 常用控件速览
| 控件 | 说明 |
|---|---|
| wx.Button | 按钮 |
| wx.StaticText | 文本标签 |
| wx.TextCtrl | 输入框 |
| wx.CheckBox | 复选框 |
| wx.RadioButton | 单选按钮 |
| wx.ComboBox | 下拉选择 |
| wx.ListCtrl | 列表 |
| wx.TreeCtrl | 树形 |
| wx.Gauge | 进度条 |
| wx.Slider | 滑块 |
| wx.DatePickerCtrl | 日期选择 |
示例:
btn = wx.Button(panel, label="确定") | |
text = wx.TextCtrl(panel, value="默认文本") | |
check = wx.CheckBox(panel, label="启用") |
# 控件常用方法(通用)
GetValue()/SetValue():读取 / 设置值GetLabel()/SetLabel():读取 / 设置标签Enable()/Disable():启用 / 禁用控件Show()/Hide():显示 / 隐藏控件SetToolTip():设置提示文本
# StaticText 使用指南
wx.StaticText 用于显示不可编辑的文本标签,常见场景包括标题、说明、表单字段名。
创建方式:
label = wx.StaticText(panel, label="用户名") |
常用样式:
wx.ALIGN_LEFT/wx.ALIGN_CENTER/wx.ALIGN_RIGHT:文本对齐wx.ST_NO_AUTORESIZE:禁用自动调整尺寸wx.ST_ELLIPSIZE_START/wx.ST_ELLIPSIZE_MIDDLE/wx.ST_ELLIPSIZE_END:超长文本省略
常用方法:
SetLabel()/GetLabel():设置 / 获取文本Wrap(width):按宽度自动换行SetFont():设置字体SetForegroundColour():设置文字颜色SetToolTip():设置悬浮提示
示例:标题样式 + 自动换行
title = wx.StaticText(panel, label="这是一个较长的标题文本,会自动换行") | |
title.SetFont(wx.Font(14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)) | |
title.SetForegroundColour(wx.Colour(30, 30, 30)) | |
title.Wrap(300) |
示例:表单字段 + 省略显示
name_label = wx.StaticText(panel, label="这是一个很长很长的字段名", style=wx.ST_ELLIPSIZE_END) | |
name_label.SetToolTip("完整字段名:这是一个很长很长的字段名") |
使用建议:
- 使用
Wrap()时应在布局完成后调用,必要时配合Layout() - 要固定宽度可先
SetMinSize((宽度, -1))再Wrap() - 若需可交互文本,考虑使用
wx.HyperlinkCtrl或wx.TextCtrl
# TextCtrl 常用样式
pwd = wx.TextCtrl(panel, style=wx.TE_PASSWORD) | |
multi = wx.TextCtrl(panel, style=wx.TE_MULTILINE | wx.TE_RICH2) | |
readonly = wx.TextCtrl(panel, value="只读", style=wx.TE_READONLY) |
# TextCtrl 常用方法
GetValue()/SetValue():读写文本AppendText():追加文本Clear():清空文本SetInsertionPointEnd():光标移到末尾
# 常用辅助控件
wx.StaticLine:分隔线wx.BitmapButton:图标按钮wx.HyperlinkCtrl:超链接wx.Notebook:选项卡wx.SplitterWindow:分割面板
# 布局管理(Sizer)
推荐使用 Sizer 控制布局,避免固定坐标。
# 常见 Sizer
wx.BoxSizer:线性布局(横 / 竖)wx.GridSizer:等分网格wx.FlexGridSizer:可伸缩网格wx.StaticBoxSizer:带标题的容器
# BoxSizer 示例
sizer = wx.BoxSizer(wx.VERTICAL) | |
sizer.Add(wx.StaticText(panel, label="用户名"), 0, wx.ALL, 5) | |
sizer.Add(wx.TextCtrl(panel), 0, wx.EXPAND | wx.ALL, 5) | |
sizer.Add(wx.Button(panel, label="登录"), 0, wx.ALL, 5) | |
panel.SetSizer(sizer) |
# Sizer 关键参数
proportion:伸缩比例flag:对齐、边距、填充border:边距大小
# 常用 flag 速记
wx.EXPAND:填充可用空间wx.ALL / wx.LEFT / wx.RIGHT / wx.TOP / wx.BOTTOM:边距wx.ALIGN_CENTER / wx.ALIGN_RIGHT / wx.ALIGN_LEFT:对齐
# Spacer 与嵌套 Sizer
main = wx.BoxSizer(wx.VERTICAL) | |
row = wx.BoxSizer(wx.HORIZONTAL) | |
row.Add(wx.StaticText(panel, label="关键词"), 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) | |
row.Add(wx.TextCtrl(panel), 1, wx.ALL | wx.EXPAND, 5) | |
row.Add(wx.Button(panel, label="搜索"), 0, wx.ALL, 5) | |
main.Add(row, 0, wx.EXPAND) | |
main.AddSpacer(10) | |
main.Add(wx.StaticLine(panel), 0, wx.EXPAND | wx.ALL, 5) | |
panel.SetSizer(main) |
# 布局细节建议
- 使用
SetSizerAndFit()自动调整窗口最小尺寸 - 用
SetMinSize()控制控件最小宽高 - 布局完成后必要时调用
Layout()
# Sizer 常用方法
Add():添加控件 / 子布局AddSpacer()/AddStretchSpacer():空白 / 弹性空白Insert():插入控件Clear()/Detach():清空 / 移除子项Fit():根据子控件调整父窗口大小
# 事件机制与事件绑定
事件绑定方式:
btn.Bind(wx.EVT_BUTTON, self.on_click) |
事件处理函数示例:
def on_click(self, event): | |
wx.MessageBox("按钮被点击了") |
# 事件对象常用方法
def on_text(self, event): | |
value = event.GetString() | |
source = event.GetEventObject() | |
print("输入:", value, "控件:", source) |
# 事件传播
def on_any(self, event): | |
# 处理完后继续向下传递 | |
event.Skip() |
# 自定义事件(简化写法)
import wx.lib.newevent | |
MyEvent, EVT_MY_EVENT = wx.lib.newevent.NewEvent() | |
def trigger_custom_event(window): | |
evt = MyEvent(message="hello") | |
wx.PostEvent(window, evt) |
常用事件:
wx.EVT_BUTTONwx.EVT_TEXTwx.EVT_CHECKBOXwx.EVT_CLOSEwx.EVT_SIZE
# 事件常用方法
event.GetEventObject():事件源控件event.GetId():事件源 IDevent.Skip():继续传递事件event.GetString():文本相关事件内容
# 菜单、工具栏与状态栏
# 菜单
menu_bar = wx.MenuBar() | |
file_menu = wx.Menu() | |
item_exit = file_menu.Append(wx.ID_EXIT, "退出") | |
menu_bar.Append(file_menu, "文件") | |
self.SetMenuBar(menu_bar) | |
self.Bind(wx.EVT_MENU, self.on_exit, item_exit) |
# 工具栏
toolbar = self.CreateToolBar() | |
tool = toolbar.AddTool(wx.ID_ANY, "打开", wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN)) | |
toolbar.Realize() |
# 状态栏
self.CreateStatusBar() | |
self.SetStatusText("就绪") |
# 快捷键(Accelerator Table)
accels = [ | |
(wx.ACCEL_CTRL, ord('O'), wx.ID_OPEN), | |
(wx.ACCEL_CTRL, ord('Q'), wx.ID_EXIT), | |
] | |
self.SetAcceleratorTable(wx.AcceleratorTable(accels)) |
# 菜单 / 工具栏常用方法
wx.Menu.Append():添加菜单项wx.Menu.AppendSeparator():分隔线wx.ToolBar.AddTool():添加工具按钮wx.ToolBar.Realize():渲染工具栏SetStatusText():设置状态栏文本
# 对话框与文件操作
# 文件选择对话框
with wx.FileDialog(self, "选择文件", wildcard="*.txt", style=wx.FD_OPEN) as dlg: | |
if dlg.ShowModal() == wx.ID_OK: | |
path = dlg.GetPath() |
# 消息框
wx.MessageBox("操作成功", "提示", wx.OK | wx.ICON_INFORMATION) |
# 目录选择与输入对话框
with wx.DirDialog(self, "选择目录") as dlg: | |
if dlg.ShowModal() == wx.ID_OK: | |
folder = dlg.GetPath() | |
with wx.TextEntryDialog(self, "请输入名称", "输入") as dlg: | |
if dlg.ShowModal() == wx.ID_OK: | |
name = dlg.GetValue() |
# 颜色与字体对话框
with wx.ColourDialog(self) as dlg: | |
if dlg.ShowModal() == wx.ID_OK: | |
colour = dlg.GetColourData().GetColour() | |
with wx.FontDialog(self) as dlg: | |
if dlg.ShowModal() == wx.ID_OK: | |
font = dlg.GetFontData().GetChosenFont() |
# 对话框常用方法
ShowModal():模态显示GetPath()/GetPaths():获取文件路径GetValue():获取输入对话框文本GetColourData()/GetFontData():获取颜色 / 字体数据
# 绘图基础与自定义控件
绘图使用 wx.PaintDC 或 wx.GraphicsContext 。
class DrawPanel(wx.Panel): | |
def __init__(self, parent): | |
super().__init__(parent) | |
self.Bind(wx.EVT_PAINT, self.on_paint) | |
def on_paint(self, event): | |
dc = wx.PaintDC(self) | |
dc.SetPen(wx.Pen(wx.BLUE, 2)) | |
dc.DrawLine(10, 10, 200, 10) | |
dc.DrawRectangle(10, 30, 100, 60) | |
dc.DrawText("Hello", 10, 100) |
## 绘图常用方法
- `SetPen()` / `SetBrush()`:设置线条/填充
- `DrawLine()` / `DrawRectangle()` / `DrawCircle()`:基础图形
- `DrawText()`:绘制文本
- `Clear()`:清空画布
# 双缓冲避免闪烁
class BufferedPanel(wx.Panel): | |
def __init__(self, parent): | |
super().__init__(parent) | |
self.SetBackgroundStyle(wx.BG_STYLE_PAINT) | |
self.Bind(wx.EVT_PAINT, self.on_paint) | |
def on_paint(self, event): | |
dc = wx.AutoBufferedPaintDC(self) | |
dc.Clear() | |
dc.SetBrush(wx.Brush(wx.Colour(240, 240, 240))) | |
dc.DrawRectangle(0, 0, *self.GetClientSize()) |
# 触发重绘
self.Refresh() # 标记重绘 | |
self.Update() # 立即重绘 |
# 数据模型与列表 / 树控件
# ListCtrl 示例
list_ctrl = wx.ListCtrl(panel, style=wx.LC_REPORT) | |
list_ctrl.InsertColumn(0, "姓名", width=120) | |
list_ctrl.InsertColumn(1, "年龄", width=60) | |
idx = list_ctrl.InsertItem(0, "张三") | |
list_ctrl.SetItem(idx, 1, "18") |
# TreeCtrl 示例
tree = wx.TreeCtrl(panel) | |
root = tree.AddRoot("根节点") | |
tree.AppendItem(root, "子节点") |
# DataViewListCtrl(更强的表格)
dv = wx.dataview.DataViewListCtrl(panel) | |
dv.AppendTextColumn("姓名") | |
dv.AppendTextColumn("年龄") | |
dv.AppendItem(["李四", "20"]) |
# 虚拟列表(大数据量)
class MyVirtualList(wx.ListCtrl): | |
def __init__(self, parent, data): | |
super().__init__(parent, style=wx.LC_REPORT | wx.LC_VIRTUAL) | |
self.data = data | |
self.InsertColumn(0, "内容") | |
self.SetItemCount(len(data)) | |
def OnGetItemText(self, item, col): | |
return self.data[item] |
# 列表 / 树控件常用方法
InsertColumn():添加列InsertItem()/SetItem():新增 / 设置单元格DeleteItem()/DeleteAllItems():删除行GetItemCount():行数AddRoot()/AppendItem():树节点管理
# 线程与异步任务
主线程更新 UI,耗时任务放到后台线程。
import threading | |
def run_task(): | |
# 耗时任务 | |
wx.CallAfter(update_ui) | |
threading.Thread(target=run_task, daemon=True).start() |
wx.CallAfter 确保 UI 更新在主线程执行。
# 线程 + 队列安全更新
import queue | |
q = queue.Queue() | |
def run_task(): | |
for i in range(10): | |
q.put(i) | |
wx.CallAfter(update_ui_from_queue) | |
def update_ui_from_queue(): | |
while not q.empty(): | |
value = q.get() | |
print("收到:", value) |
# 轻量消息发布(可选)
from wx.lib.pubsub import pub | |
def worker(): | |
pub.sendMessage("task.done", result=123) | |
pub.subscribe(lambda result: print(result), "task.done") |
# 线程与 UI 更新常用方法
wx.CallAfter():线程安全 UI 更新wx.PostEvent():派发自定义事件wx.Timer:周期性任务
# 定时器与后台任务
self.timer = wx.Timer(self) | |
self.Bind(wx.EVT_TIMER, self.on_timer, self.timer) | |
self.timer.Start(1000) |
# CallLater(一键延迟执行)
wx.CallLater(500, lambda: self.SetStatusText("延迟完成")) |
# 定时器常用方法
Start(ms):启动定时器Stop():停止定时器IsRunning():是否运行
# 国际化与字体
- 使用 UTF-8
- 在 Windows 上指定中文字体时注意字体名称
font = wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, faceName="Microsoft YaHei") | |
text.SetFont(font) |
# 简单 i18n(gettext)示例
import gettext | |
gettext.bindtextdomain('myapp', 'locale') | |
gettext.textdomain('myapp') | |
_ = gettext.gettext | |
label.SetLabel(_("你好")) |
# 字体相关常用方法
SetFont()/GetFont():设置 / 获取字体SetForegroundColour()/SetBackgroundColour():文字 / 背景色
# 主题与样式定制
常用设置:
SetBackgroundColourSetForegroundColour- 自定义图标与主题资源
panel.SetBackgroundColour(wx.Colour(245, 245, 245)) |
# 系统主题与颜色
bg = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) | |
fg = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) | |
panel.SetBackgroundColour(bg) | |
panel.SetForegroundColour(fg) |
# 样式常用方法
SetBackgroundColour():设置背景色SetForegroundColour():设置前景色SetCursor():设置鼠标指针
# 应用配置与持久化
使用 wx.Config 保存配置:
config = wx.Config("MyApp") | |
config.Write("username", "admin") | |
config.Flush() | |
value = config.Read("username", "") |
# 推荐配置路径
paths = wx.StandardPaths.Get() | |
config_dir = paths.GetUserConfigDir() |
# Config 常用方法
Write()/Read():写入 / 读取配置DeleteEntry():删除配置项Flush():落盘保存
# 日志与错误处理
import logging | |
logging.basicConfig(level=logging.INFO) | |
logging.info("启动") |
# wx 原生日志
wx.LogMessage("应用启动") | |
wx.LogWarning("这是警告") |
可在异常处弹出提示:
try: | |
... | |
except Exception as e: | |
wx.MessageBox(str(e), "错误", wx.OK | wx.ICON_ERROR) |
# 日志常用方法
wx.LogMessage():普通日志wx.LogWarning():警告日志wx.LogError():错误日志
# 打包发布
常用:PyInstaller
pip install pyinstaller | |
pyinstaller -w -F main.py |
注意事项:
- 加载资源使用相对路径处理
- 合理配置图标与数据文件
# 资源路径兼容(源码 / 打包)
import sys | |
import os | |
def resource_path(relative_path): | |
if getattr(sys, 'frozen', False): | |
base = sys._MEIPASS | |
else: | |
base = os.path.abspath(".") | |
return os.path.join(base, relative_path) |
# 打包包含数据文件
pyinstaller -w -F main.py --add-data "assets;assets" |
# 打包与资源常用方法
resource_path():解决打包后资源路径wx.Icon()/SetIcon():设置应用图标wx.Bitmap():加载位图资源
# 项目结构与实战建议
推荐结构:
myapp/
app.py
ui/
main_frame.py
dialogs.py
services/
data_service.py
resources/
icons/
建议:
- 窗口与业务逻辑分离
- 使用面向对象组织 UI
- 统一资源管理
# 入口与应用类
class MyApp(wx.App): | |
def OnInit(self): | |
self.frame = MainFrame() | |
self.frame.Show() | |
return True | |
if __name__ == "__main__": | |
app = MyApp(False) | |
app.MainLoop() |
# 综合型小项目例子(完整代码)
下面给出几个 “可直接运行” 的小项目示例,覆盖布局、事件、对话框、列表、绘图、线程与配置等主题。
# 小项目 1:简易待办清单(列表 + 事件 + 持久化)
功能:新增 / 删除 / 勾选完成,自动保存到本地 JSON。
import wx | |
import json | |
import os | |
DATA_FILE = "todo_data.json" | |
class TodoFrame(wx.Frame): | |
def __init__(self): | |
super().__init__(None, title="待办清单", size=(500, 400)) | |
panel = wx.Panel(self) | |
self.listbox = wx.CheckListBox(panel) | |
self.input = wx.TextCtrl(panel) | |
btn_add = wx.Button(panel, label="添加") | |
btn_remove = wx.Button(panel, label="删除选中") | |
btn_add.Bind(wx.EVT_BUTTON, self.on_add) | |
btn_remove.Bind(wx.EVT_BUTTON, self.on_remove) | |
self.Bind(wx.EVT_CLOSE, self.on_close) | |
row = wx.BoxSizer(wx.HORIZONTAL) | |
row.Add(self.input, 1, wx.ALL | wx.EXPAND, 5) | |
row.Add(btn_add, 0, wx.ALL, 5) | |
row.Add(btn_remove, 0, wx.ALL, 5) | |
main = wx.BoxSizer(wx.VERTICAL) | |
main.Add(self.listbox, 1, wx.ALL | wx.EXPAND, 5) | |
main.Add(row, 0, wx.EXPAND) | |
panel.SetSizer(main) | |
self.load_data() | |
self.Centre() | |
def on_add(self, event): | |
text = self.input.GetValue().strip() | |
if not text: | |
return | |
self.listbox.Append(text) | |
self.input.SetValue("") | |
def on_remove(self, event): | |
selected = list(self.listbox.GetSelections()) | |
for idx in reversed(selected): | |
self.listbox.Delete(idx) | |
def load_data(self): | |
if not os.path.exists(DATA_FILE): | |
return | |
with open(DATA_FILE, "r", encoding="utf-8") as f: | |
items = json.load(f) | |
for item in items: | |
self.listbox.Append(item["text"]) | |
self.listbox.Check(self.listbox.Count - 1, item["done"]) | |
def save_data(self): | |
items = [] | |
for i in range(self.listbox.Count): | |
items.append({ | |
"text": self.listbox.GetString(i), | |
"done": self.listbox.IsChecked(i) | |
}) | |
with open(DATA_FILE, "w", encoding="utf-8") as f: | |
json.dump(items, f, ensure_ascii=False, indent=2) | |
def on_close(self, event): | |
self.save_data() | |
event.Skip() | |
if __name__ == "__main__": | |
app = wx.App() | |
frame = TodoFrame() | |
frame.Show() | |
app.MainLoop() |
# 小项目 2:图片浏览器(文件对话框 + 缩放 + 状态栏)
import wx | |
class ImageViewer(wx.Frame): | |
def __init__(self): | |
super().__init__(None, title="图片浏览器", size=(800, 600)) | |
panel = wx.Panel(self) | |
self.bitmap = wx.StaticBitmap(panel) | |
btn_open = wx.Button(panel, label="打开图片") | |
slider = wx.Slider(panel, minValue=10, maxValue=200, value=100) | |
btn_open.Bind(wx.EVT_BUTTON, self.on_open) | |
slider.Bind(wx.EVT_SLIDER, self.on_zoom) | |
row = wx.BoxSizer(wx.HORIZONTAL) | |
row.Add(btn_open, 0, wx.ALL, 5) | |
row.Add(wx.StaticText(panel, label="缩放"), 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) | |
row.Add(slider, 1, wx.ALL | wx.EXPAND, 5) | |
main = wx.BoxSizer(wx.VERTICAL) | |
main.Add(row, 0, wx.EXPAND) | |
main.Add(self.bitmap, 1, wx.ALL | wx.EXPAND, 5) | |
panel.SetSizer(main) | |
self.scale = 1.0 | |
self.image = None | |
self.CreateStatusBar() | |
self.Centre() | |
def on_open(self, event): | |
with wx.FileDialog(self, "选择图片", wildcard="*.png;*.jpg;*.jpeg;*.bmp", style=wx.FD_OPEN) as dlg: | |
if dlg.ShowModal() == wx.ID_OK: | |
path = dlg.GetPath() | |
self.image = wx.Image(path) | |
self.scale = 1.0 | |
self.update_bitmap() | |
self.SetStatusText(path) | |
def on_zoom(self, event): | |
self.scale = event.GetInt() / 100.0 | |
self.update_bitmap() | |
def update_bitmap(self): | |
if not self.image: | |
return | |
w = int(self.image.GetWidth() * self.scale) | |
h = int(self.image.GetHeight() * self.scale) | |
bmp = self.image.Scale(w, h, wx.IMAGE_QUALITY_HIGH).ConvertToBitmap() | |
self.bitmap.SetBitmap(bmp) | |
self.Layout() | |
if __name__ == "__main__": | |
app = wx.App() | |
frame = ImageViewer() | |
frame.Show() | |
app.MainLoop() |
# 小项目 3:简易记事本(菜单 + 快捷键 + 文件操作)
import wx | |
class Notepad(wx.Frame): | |
def __init__(self): | |
super().__init__(None, title="简易记事本", size=(700, 500)) | |
self.text = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_RICH2) | |
self.file_path = None | |
self.create_menu() | |
self.CreateStatusBar() | |
self.Centre() | |
def create_menu(self): | |
menu_bar = wx.MenuBar() | |
menu_file = wx.Menu() | |
menu_file.Append(wx.ID_NEW, "新建\tCtrl+N") | |
menu_file.Append(wx.ID_OPEN, "打开\tCtrl+O") | |
menu_file.Append(wx.ID_SAVE, "保存\tCtrl+S") | |
menu_file.AppendSeparator() | |
menu_file.Append(wx.ID_EXIT, "退出\tCtrl+Q") | |
menu_bar.Append(menu_file, "文件") | |
self.SetMenuBar(menu_bar) | |
self.Bind(wx.EVT_MENU, self.on_new, id=wx.ID_NEW) | |
self.Bind(wx.EVT_MENU, self.on_open, id=wx.ID_OPEN) | |
self.Bind(wx.EVT_MENU, self.on_save, id=wx.ID_SAVE) | |
self.Bind(wx.EVT_MENU, self.on_exit, id=wx.ID_EXIT) | |
def on_new(self, event): | |
self.text.SetValue("") | |
self.file_path = None | |
self.SetStatusText("新建文件") | |
def on_open(self, event): | |
with wx.FileDialog(self, "打开文件", wildcard="*.txt", style=wx.FD_OPEN) as dlg: | |
if dlg.ShowModal() == wx.ID_OK: | |
self.file_path = dlg.GetPath() | |
with open(self.file_path, "r", encoding="utf-8") as f: | |
self.text.SetValue(f.read()) | |
self.SetStatusText(self.file_path) | |
def on_save(self, event): | |
if not self.file_path: | |
with wx.FileDialog(self, "保存文件", wildcard="*.txt", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as dlg: | |
if dlg.ShowModal() == wx.ID_OK: | |
self.file_path = dlg.GetPath() | |
if self.file_path: | |
with open(self.file_path, "w", encoding="utf-8") as f: | |
f.write(self.text.GetValue()) | |
self.SetStatusText(self.file_path) | |
def on_exit(self, event): | |
self.Close() | |
if __name__ == "__main__": | |
app = wx.App() | |
frame = Notepad() | |
frame.Show() | |
app.MainLoop() |
# 中项目例子(完整代码)
中项目示例提供更完整的应用结构:多面板、配置、日志、异步任务等,适合作为学习串联多个专题的项目模板。
# 中项目 1:任务管理器(多页 Notebook + 线程任务 + 配置 + 日志)
import wx | |
import wx.lib.newevent | |
import threading | |
import time | |
import json | |
import os | |
CONFIG_FILE = "task_app_config.json" | |
TaskEvent, EVT_TASK_EVENT = wx.lib.newevent.NewEvent() | |
class TaskRunner(threading.Thread): | |
def __init__(self, window, name, duration): | |
super().__init__(daemon=True) | |
self.window = window | |
self.name = name | |
self.duration = duration | |
def run(self): | |
for i in range(self.duration): | |
time.sleep(1) | |
wx.PostEvent(self.window, TaskEvent(name=self.name, progress=i + 1, total=self.duration)) | |
class TaskManagerFrame(wx.Frame): | |
def __init__(self): | |
super().__init__(None, title="任务管理器", size=(800, 600)) | |
panel = wx.Panel(self) | |
self.notebook = wx.Notebook(panel) | |
self.page_tasks = wx.Panel(self.notebook) | |
self.page_logs = wx.Panel(self.notebook) | |
self.notebook.AddPage(self.page_tasks, "任务") | |
self.notebook.AddPage(self.page_logs, "日志") | |
# 任务页 | |
self.task_list = wx.ListCtrl(self.page_tasks, style=wx.LC_REPORT) | |
self.task_list.InsertColumn(0, "任务", width=200) | |
self.task_list.InsertColumn(1, "进度", width=200) | |
self.task_list.InsertColumn(2, "状态", width=150) | |
self.input_name = wx.TextCtrl(self.page_tasks) | |
self.input_duration = wx.SpinCtrl(self.page_tasks, min=1, max=60, initial=5) | |
btn_add = wx.Button(self.page_tasks, label="添加任务") | |
btn_add.Bind(wx.EVT_BUTTON, self.on_add_task) | |
self.Bind(EVT_TASK_EVENT, self.on_task_event) | |
form = wx.BoxSizer(wx.HORIZONTAL) | |
form.Add(wx.StaticText(self.page_tasks, label="任务名"), 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) | |
form.Add(self.input_name, 1, wx.ALL | wx.EXPAND, 5) | |
form.Add(wx.StaticText(self.page_tasks, label="时长(秒)"), 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) | |
form.Add(self.input_duration, 0, wx.ALL, 5) | |
form.Add(btn_add, 0, wx.ALL, 5) | |
task_sizer = wx.BoxSizer(wx.VERTICAL) | |
task_sizer.Add(form, 0, wx.EXPAND) | |
task_sizer.Add(self.task_list, 1, wx.ALL | wx.EXPAND, 5) | |
self.page_tasks.SetSizer(task_sizer) | |
# 日志页 | |
self.log_text = wx.TextCtrl(self.page_logs, style=wx.TE_MULTILINE | wx.TE_READONLY) | |
log_sizer = wx.BoxSizer(wx.VERTICAL) | |
log_sizer.Add(self.log_text, 1, wx.ALL | wx.EXPAND, 5) | |
self.page_logs.SetSizer(log_sizer) | |
# 主布局 | |
main = wx.BoxSizer(wx.VERTICAL) | |
main.Add(self.notebook, 1, wx.EXPAND) | |
panel.SetSizer(main) | |
self.load_config() | |
self.Centre() | |
def log(self, msg): | |
self.log_text.AppendText(msg + "\n") | |
def on_add_task(self, event): | |
name = self.input_name.GetValue().strip() or "未命名任务" | |
duration = self.input_duration.GetValue() | |
idx = self.task_list.InsertItem(self.task_list.GetItemCount(), name) | |
self.task_list.SetItem(idx, 1, f"0/{duration}") | |
self.task_list.SetItem(idx, 2, "运行中") | |
self.log(f"开始任务:{name},时长:{duration}s") | |
TaskRunner(self, name, duration).start() | |
self.save_config() | |
def on_task_event(self, event): | |
# 通过任务名找到对应行(简单示例) | |
for i in range(self.task_list.GetItemCount()): | |
if self.task_list.GetItemText(i) == event.name: | |
self.task_list.SetItem(i, 1, f"{event.progress}/{event.total}") | |
if event.progress == event.total: | |
self.task_list.SetItem(i, 2, "完成") | |
self.log(f"任务完成:{event.name}") | |
break | |
def load_config(self): | |
if not os.path.exists(CONFIG_FILE): | |
return | |
with open(CONFIG_FILE, "r", encoding="utf-8") as f: | |
data = json.load(f) | |
self.input_name.SetValue(data.get("last_name", "")) | |
self.input_duration.SetValue(data.get("last_duration", 5)) | |
def save_config(self): | |
data = { | |
"last_name": self.input_name.GetValue(), | |
"last_duration": self.input_duration.GetValue() | |
} | |
with open(CONFIG_FILE, "w", encoding="utf-8") as f: | |
json.dump(data, f, ensure_ascii=False, indent=2) | |
if __name__ == "__main__": | |
app = wx.App() | |
frame = TaskManagerFrame() | |
frame.Show() | |
app.MainLoop() |
# 中项目 2:数据查看器(左树右表 + 搜索 + 导入 CSV)
import wx | |
import csv | |
class DataViewer(wx.Frame): | |
def __init__(self): | |
super().__init__(None, title="数据查看器", size=(900, 600)) | |
splitter = wx.SplitterWindow(self) | |
panel_left = wx.Panel(splitter) | |
panel_right = wx.Panel(splitter) | |
# 左侧树 | |
self.tree = wx.TreeCtrl(panel_left) | |
root = self.tree.AddRoot("数据集") | |
self.tree.AppendItem(root, "CSV 导入") | |
self.tree.ExpandAll() | |
left_sizer = wx.BoxSizer(wx.VERTICAL) | |
left_sizer.Add(self.tree, 1, wx.ALL | wx.EXPAND, 5) | |
panel_left.SetSizer(left_sizer) | |
# 右侧表格 | |
self.search = wx.TextCtrl(panel_right) | |
btn_load = wx.Button(panel_right, label="导入 CSV") | |
self.list = wx.ListCtrl(panel_right, style=wx.LC_REPORT) | |
btn_load.Bind(wx.EVT_BUTTON, self.on_load_csv) | |
self.search.Bind(wx.EVT_TEXT, self.on_search) | |
top = wx.BoxSizer(wx.HORIZONTAL) | |
top.Add(wx.StaticText(panel_right, label="搜索"), 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) | |
top.Add(self.search, 1, wx.ALL | wx.EXPAND, 5) | |
top.Add(btn_load, 0, wx.ALL, 5) | |
right_sizer = wx.BoxSizer(wx.VERTICAL) | |
right_sizer.Add(top, 0, wx.EXPAND) | |
right_sizer.Add(self.list, 1, wx.ALL | wx.EXPAND, 5) | |
panel_right.SetSizer(right_sizer) | |
splitter.SplitVertically(panel_left, panel_right, 200) | |
self.data = [] | |
self.Centre() | |
def on_load_csv(self, event): | |
with wx.FileDialog(self, "导入 CSV", wildcard="*.csv", style=wx.FD_OPEN) as dlg: | |
if dlg.ShowModal() == wx.ID_OK: | |
path = dlg.GetPath() | |
with open(path, "r", encoding="utf-8") as f: | |
reader = csv.reader(f) | |
rows = list(reader) | |
if not rows: | |
return | |
self.data = rows | |
self.render_table(rows) | |
def render_table(self, rows): | |
self.list.ClearAll() | |
headers = rows[0] | |
for i, h in enumerate(headers): | |
self.list.InsertColumn(i, h, width=150) | |
for row in rows[1:]: | |
idx = self.list.InsertItem(self.list.GetItemCount(), row[0]) | |
for c in range(1, len(row)): | |
self.list.SetItem(idx, c, row[c]) | |
def on_search(self, event): | |
keyword = self.search.GetValue().strip() | |
if not self.data: | |
return | |
if not keyword: | |
self.render_table(self.data) | |
return | |
headers = self.data[0] | |
filtered = [headers] | |
for row in self.data[1:]: | |
if any(keyword in cell for cell in row): | |
filtered.append(row) | |
self.render_table(filtered) | |
if __name__ == "__main__": | |
app = wx.App() | |
frame = DataViewer() | |
frame.Show() | |
app.MainLoop() |
# 常见坑与排错技巧
- UI 卡顿:耗时任务放线程
- 布局错乱:确保调用
SetSizer和Layout() - 事件不触发:检查 Bind 是否正确
- 高 DPI 问题:开启缩放并测试
- 资源路径错误:打包后请使用
resource_path - 线程崩溃:后台线程不要直接更新 UI
- 窗口尺寸异常:使用
Fit()或SetMinSize()控制
# 延伸阅读
- wxPython 官方文档
- wxWidgets 文档(概念通用)
- 经典开源项目示例
# 结语
本指南覆盖了 wxPython 的入门、进阶与实战关键点。建议在实际项目中不断迭代,逐步形成自己的 UI 组件库与工程化习惯。
