适用人群:零基础到进阶开发者

目标:系统掌握 wxPython 的核心概念、布局、事件、绘图、多线程、数据绑定、打包发布等实战能力。

# 目录


# 简介与环境准备

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.CallAfterwx.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.HyperlinkCtrlwx.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_BUTTON
  • wx.EVT_TEXT
  • wx.EVT_CHECKBOX
  • wx.EVT_CLOSE
  • wx.EVT_SIZE

# 事件常用方法

  • event.GetEventObject() :事件源控件
  • event.GetId() :事件源 ID
  • event.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.PaintDCwx.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() :文字 / 背景色

# 主题与样式定制

常用设置:

  • SetBackgroundColour
  • SetForegroundColour
  • 自定义图标与主题资源
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 卡顿:耗时任务放线程
  • 布局错乱:确保调用 SetSizerLayout()
  • 事件不触发:检查 Bind 是否正确
  • 高 DPI 问题:开启缩放并测试
  • 资源路径错误:打包后请使用 resource_path
  • 线程崩溃:后台线程不要直接更新 UI
  • 窗口尺寸异常:使用 Fit()SetMinSize() 控制

# 延伸阅读

  • wxPython 官方文档
  • wxWidgets 文档(概念通用)
  • 经典开源项目示例

# 结语

本指南覆盖了 wxPython 的入门、进阶与实战关键点。建议在实际项目中不断迭代,逐步形成自己的 UI 组件库与工程化习惯。

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

RunfarAI 微信支付

微信支付

RunfarAI alipay

alipay

RunfarAI paypal

paypal