wxPython自定义控件
虽然wxPython提供了丰富的内置控件,但在某些情况下,您可能需要创建自定义控件来满足特定需求。本章将介绍如何创建自定义控件、处理绘图操作以及实现自定义行为。
自定义控件基础
创建自定义控件通常涉及继承现有的wxPython控件类,并重写相关方法来实现自定义行为。最常见的做法是继承wx.Window或wx.Panel类。
创建简单的自定义控件
让我们从一个简单的自定义按钮开始:
Python
import wx
class CustomButton(wx.Panel):
def __init__(self, parent, label="自定义按钮"):
super().__init__(parent)
self.label = label
self.pressed = False
self.hovered = False
# 设置初始大小
self.SetMinSize((120, 40))
# 绑定事件
self.Bind(wx.EVT_PAINT, self.on_paint)
self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
self.Bind(wx.EVT_LEFT_UP, self.on_left_up)
self.Bind(wx.EVT_ENTER_WINDOW, self.on_enter)
self.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave)
self.Bind(wx.EVT_SIZE, self.on_size)
def on_paint(self, event):
dc = wx.PaintDC(self)
gc = wx.GraphicsContext.Create(dc)
# 获取控件大小
width, height = self.GetSize()
# 设置颜色
if self.pressed:
bg_color = wx.Colour(100, 150, 255)
elif self.hovered:
bg_color = wx.Colour(150, 180, 255)
else:
bg_color = wx.Colour(120, 170, 255)
# 绘制背景
gc.SetBrush(wx.Brush(bg_color))
gc.SetPen(wx.Pen(wx.Colour(80, 120, 220), 2))
gc.DrawRoundedRectangle(0, 0, width, height, 10)
# 绘制文本
font = self.GetFont()
gc.SetFont(font, wx.WHITE)
text_width, text_height = gc.GetTextExtent(self.label)
x = (width - text_width) / 2
y = (height - text_height) / 2
if self.pressed:
# 按下时文本稍微偏移
x += 1
y += 1
gc.DrawText(self.label, x, y)
def on_left_down(self, event):
self.pressed = True
self.Refresh()
self.CaptureMouse()
def on_left_up(self, event):
if self.pressed:
self.pressed = False
self.Refresh()
if self.HasCapture():
self.ReleaseMouse()
# 触发按钮点击事件
evt = wx.CommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, self.GetId())
evt.SetEventObject(self)
self.GetEventHandler().ProcessEvent(evt)
def on_enter(self, event):
self.hovered = True
self.Refresh()
def on_leave(self, event):
self.hovered = False
self.pressed = False
self.Refresh()
def on_size(self, event):
self.Refresh()
event.Skip()
使用设备上下文绘图
wxPython提供了多种绘图方式,最基础的是使用设备上下文(DC):
Python
import wx
import math
class DrawingPanel(wx.Panel):
def __init__(self, parent):
super().__init__(parent)
self.Bind(wx.EVT_PAINT, self.on_paint)
self.Bind(wx.EVT_SIZE, self.on_size)
def on_paint(self, event):
dc = wx.PaintDC(self)
self.draw_shapes(dc)
def on_size(self, event):
self.Refresh()
event.Skip()
def draw_shapes(self, dc):
# 获取面板大小
width, height = self.GetSize()
# 设置背景色
dc.SetBackground(wx.Brush(wx.WHITE))
dc.Clear()
# 设置画笔和画刷
dc.SetPen(wx.Pen(wx.BLUE, 3))
dc.SetBrush(wx.Brush(wx.GREEN))
# 绘制矩形
dc.DrawRectangle(10, 10, 100, 80)
# 绘制圆形
dc.SetBrush(wx.Brush(wx.RED))
dc.DrawCircle(200, 50, 40)
# 绘制线条
dc.SetPen(wx.Pen(wx.BLACK, 2))
dc.DrawLine(10, 100, 200, 100)
# 绘制文本
dc.SetFont(wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD))
dc.SetTextForeground(wx.BLUE)
dc.DrawText("Hello wxPython!", 10, 120)
# 绘制多边形
points = [(300, 20), (350, 70), (250, 70)]
dc.SetPen(wx.Pen(wx.PURPLE, 2))
dc.SetBrush(wx.Brush(wx.YELLOW))
dc.DrawPolygon(points)
使用GraphicsContext进行高级绘图
GraphicsContext提供了更高级的绘图功能,支持抗锯齿、透明度等:
Python
import wx
import math
class AdvancedDrawingPanel(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)
gc = wx.GraphicsContext.Create(dc)
self.draw_advanced_shapes(gc)
def draw_advanced_shapes(self, gc):
# 获取面板大小
width, height = self.GetSize()
# 设置背景
gc.SetBrush(wx.Brush(wx.WHITE))
gc.DrawRectangle(0, 0, width, height)
# 绘制渐变矩形
gradient = gc.CreateLinearGradientBrush(0, 0, 200, 100,
wx.Colour(255, 0, 0),
wx.Colour(0, 0, 255))
gc.SetBrush(gradient)
gc.SetPen(wx.Pen(wx.BLACK, 2))
gc.DrawRoundedRectangle(20, 20, 200, 100, 15)
# 绘制透明圆形
gc.SetBrush(wx.Brush(wx.Colour(0, 255, 0, 128))) # 50%透明度
gc.SetPen(wx.Pen(wx.BLACK, 2))
gc.DrawEllipse(250, 30, 80, 80)
# 绘制旋转文本
font = wx.Font(16, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
gc.SetFont(font, wx.BLACK)
gc.PushState()
gc.Translate(150, 200)
gc.Rotate(math.pi / 4) # 旋转45度
gc.DrawText("旋转文本", 0, 0)
gc.PopState()
# 绘制贝塞尔曲线
gc.SetPen(wx.Pen(wx.RED, 3))
path = gc.CreatePath()
path.MoveToPoint(50, 250)
path.AddCurveToPoint(100, 200, 150, 300, 200, 250)
gc.StrokePath(path)
创建自定义图表控件
让我们创建一个简单的柱状图控件:
Python
import wx
class BarChart(wx.Panel):
def __init__(self, parent, data=None):
super().__init__(parent)
self.data = data or []
self.Bind(wx.EVT_PAINT, self.on_paint)
self.Bind(wx.EVT_SIZE, self.on_size)
def set_data(self, data):
self.data = data
self.Refresh()
def on_paint(self, event):
dc = wx.PaintDC(self)
gc = wx.GraphicsContext.Create(dc)
self.draw_chart(gc)
def on_size(self, event):
self.Refresh()
event.Skip()
def draw_chart(self, gc):
if not self.data:
return
width, height = self.GetSize()
# 设置边距
margin = 40
chart_width = width - 2 * margin
chart_height = height - 2 * margin
# 设置背景
gc.SetBrush(wx.Brush(wx.WHITE))
gc.DrawRectangle(0, 0, width, height)
# 找到最大值用于缩放
max_value = max(self.data) if self.data else 1
# 绘制坐标轴
gc.SetPen(wx.Pen(wx.BLACK, 2))
gc.StrokeLine(margin, margin, margin, height - margin) # Y轴
gc.StrokeLine(margin, height - margin, width - margin, height - margin) # X轴
# 绘制柱状图
bar_width = chart_width / len(self.data) * 0.8
spacing = chart_width / len(self.data) * 0.2
colors = [wx.Colour(255, 99, 132), wx.Colour(54, 162, 235),
wx.Colour(255, 205, 86), wx.Colour(75, 192, 192),
wx.Colour(153, 102, 255), wx.Colour(255, 159, 64)]
for i, value in enumerate(self.data):
x = margin + i * (bar_width + spacing)
bar_height = (value / max_value) * chart_height
y = height - margin - bar_height
color = colors[i % len(colors)]
gc.SetBrush(wx.Brush(color))
gc.SetPen(wx.Pen(color.darker(), 1))
gc.DrawRectangle(x, y, bar_width, bar_height)
# 绘制数值标签
gc.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL), wx.BLACK)
text = str(value)
text_width, text_height = gc.GetTextExtent(text)
gc.DrawText(text, x + (bar_width - text_width) / 2, y - text_height - 2)
自定义控件的事件处理
自定义控件通常需要定义自己的事件:
Python
import wx
# 定义自定义事件类型
wx.CustomControlEvent = wx.NewEventType()
EVT_CUSTOM_CONTROL = wx.PyEventBinder(wx.CustomControlEvent, 1)
class CustomControlEvent(wx.PyCommandEvent):
def __init__(self, event_type, id):
wx.PyCommandEvent.__init__(self, event_type, id)
self.value = None
def SetValue(self, value):
self.value = value
def GetValue(self):
return self.value
class CustomSlider(wx.Panel):
def __init__(self, parent, min_value=0, max_value=100, initial_value=50):
super().__init__(parent)
self.min_value = min_value
self.max_value = max_value
self.value = initial_value
self.dragging = False
# 设置大小
self.SetMinSize((200, 30))
# 绑定事件
self.Bind(wx.EVT_PAINT, self.on_paint)
self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
self.Bind(wx.EVT_LEFT_UP, self.on_left_up)
self.Bind(wx.EVT_MOTION, self.on_motion)
self.Bind(wx.EVT_SIZE, self.on_size)
def on_paint(self, event):
dc = wx.PaintDC(self)
gc = wx.GraphicsContext.Create(dc)
self.draw_slider(gc)
def draw_slider(self, gc):
width, height = self.GetSize()
# 绘制背景
gc.SetBrush(wx.Brush(wx.WHITE))
gc.DrawRectangle(0, 0, width, height)
# 绘制轨道
track_height = 4
track_y = (height - track_height) / 2
gc.SetBrush(wx.Brush(wx.Colour(200, 200, 200)))
gc.SetPen(wx.Pen(wx.Colour(150, 150, 150), 1))
gc.DrawRectangle(10, track_y, width - 20, track_height)
# 绘制已填充部分
fill_width = (self.value - self.min_value) / (self.max_value - self.min_value) * (width - 20)
gc.SetBrush(wx.Brush(wx.Colour(66, 133, 244)))
gc.DrawRectangle(10, track_y, fill_width, track_height)
# 绘制滑块
slider_x = 10 + fill_width - 10
slider_y = height / 2 - 10
gc.SetBrush(wx.Brush(wx.Colour(66, 133, 244)))
gc.SetPen(wx.Pen(wx.Colour(33, 100, 200), 1))
gc.DrawEllipse(slider_x, slider_y, 20, 20)
def on_left_down(self, event):
self.dragging = True
self.update_value(event.GetPosition())
self.CaptureMouse()
def on_left_up(self, event):
if self.dragging:
self.dragging = False
if self.HasCapture():
self.ReleaseMouse()
def on_motion(self, event):
if self.dragging and event.Dragging():
self.update_value(event.GetPosition())
def update_value(self, pos):
width, height = self.GetSize()
x = max(10, min(pos.x, width - 10))
ratio = (x - 10) / (width - 20)
self.value = self.min_value + ratio * (self.max_value - self.min_value)
self.value = max(self.min_value, min(self.max_value, self.value))
self.Refresh()
# 发送自定义事件
evt = CustomControlEvent(wx.CustomControlEvent, self.GetId())
evt.SetValue(self.value)
evt.SetEventObject(self)
self.GetEventHandler().ProcessEvent(evt)
def on_size(self, event):
self.Refresh()
event.Skip()
def GetValue(self):
return self.value
def SetValue(self, value):
self.value = max(self.min_value, min(self.max_value, value))
self.Refresh()
完整示例:自定义仪表盘控件
下面是一个更复杂的自定义控件示例——仪表盘:
Python
import wx
import math
class Dashboard(wx.Panel):
def __init__(self, parent, min_value=0, max_value=100, units=""):
super().__init__(parent)
self.min_value = min_value
self.max_value = max_value
self.value = min_value
self.units = units
self.SetMinSize((200, 200))
self.Bind(wx.EVT_PAINT, self.on_paint)
self.Bind(wx.EVT_SIZE, self.on_size)
def SetValue(self, value):
self.value = max(self.min_value, min(self.max_value, value))
self.Refresh()
def on_paint(self, event):
dc = wx.PaintDC(self)
gc = wx.GraphicsContext.Create(dc)
self.draw_dashboard(gc)
def draw_dashboard(self, gc):
width, height = self.GetSize()
center_x, center_y = width / 2, height / 2
radius = min(width, height) / 2 - 10
# 绘制背景
gc.SetBrush(wx.Brush(wx.WHITE))
gc.DrawRectangle(0, 0, width, height)
# 绘制外圆
gc.SetBrush(wx.Brush(wx.Colour(240, 240, 240)))
gc.SetPen(wx.Pen(wx.Colour(200, 200, 200), 2))
gc.DrawEllipse(center_x - radius, center_y - radius, radius * 2, radius * 2)
# 绘制刻度
gc.SetPen(wx.Pen(wx.BLACK, 1))
for i in range(0, 101, 10):
angle = math.pi * (i / 100)
start_x = center_x + (radius - 10) * math.cos(angle)
start_y = center_y + (radius - 10) * math.sin(angle)
end_x = center_x + radius * math.cos(angle)
end_y = center_y + radius * math.sin(angle)
gc.StrokeLine(start_x, start_y, end_x, end_y)
# 绘制刻度标签
if i % 20 == 0:
label = str(int(self.min_value + (self.max_value - self.min_value) * i / 100))
text_x = center_x + (radius - 25) * math.cos(angle)
text_y = center_y + (radius - 25) * math.sin(angle)
gc.SetFont(wx.Font(8, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL), wx.BLACK)
text_width, text_height = gc.GetTextExtent(label)
gc.DrawText(label, text_x - text_width / 2, text_y - text_height / 2)
# 绘制指针
ratio = (self.value - self.min_value) / (self.max_value - self.min_value)
angle = math.pi * ratio
pointer_x = center_x + (radius - 20) * math.cos(angle)
pointer_y = center_y + (radius - 20) * math.sin(angle)
gc.SetPen(wx.Pen(wx.RED, 3))
gc.StrokeLine(center_x, center_y, pointer_x, pointer_y)
# 绘制中心圆点
gc.SetBrush(wx.Brush(wx.RED))
gc.DrawEllipse(center_x - 5, center_y - 5, 10, 10)
# 绘制数值和单位
gc.SetFont(wx.Font(16, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD), wx.BLUE)
value_text = f"{self.value:.1f} {self.units}"
text_width, text_height = gc.GetTextExtent(value_text)
gc.DrawText(value_text, center_x - text_width / 2, center_y + 20)
def on_size(self, event):
self.Refresh()
event.Skip()