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()