文章目录
wxpython 实现简易画板(1)
任何用户界面工具的最基本的行为是在屏幕上绘制。
wxpython 用于绘制的主要概念是 device context(直译为设备上下文,简称 DC)。设备上下文使用一个标准的 API 来管理设备(屏幕或打印机)的绘制,如在屏幕上绘制一条直线、曲线或文本。也就是说,实现绘制功能基本过程就是:先获得待绘制设备的 DC,再通过调用 DC 的 API 实现绘制功能。
实现“画板”,需要使用的设备上下文有:
wx.PaintDC
– drawing to the screen, during EVT_PAINTwx.ClientDC
– drawing to the screen, outside EVT_PAINTwx.BufferedPaintDC
– drawing to a buffer, then the screen, during EVT_PAINTwx.BufferedDC
– drawing to a buffer, then the screen, outside EVT_PAINTwx.MemoryDC
– drawing to a bitmap
1.1. 实现思路
绘制过程:
- 鼠标左键按下:开始绘制,记录笔画的起点;
- 拖动鼠标:记录移动过程中鼠标经过的每个点的坐标。这些数据点两两相连形成最终的笔迹;
- 鼠标左键松开:结束绘制,保存此笔迹。
实现画板,有两种实现方法:直接法和缓冲法。简单理解,直接法相当于每绘制一次就刷新一下屏幕,当绘图比较复杂时会产生屏闪现象;缓冲法是将所有的绘制先缓存到某个内存中,然后再一次性复制到屏幕,即只刷新一次,有效的避免屏幕闪烁。
直接法和缓冲法都有一一对应的设备上下文,不能混用。直接法对应的设备上下文是 wx.ClientDC
和 wx.PaintDC
,缓冲法对应的设备上下文是 wx.BufferedDC
和 wx.BUfferedPaintDC
。其中,wx.PaintDC
和 wx.BufferedPaintDC
都只能在 EVT_PAINT
中使用(没有为什么,就是这样规定限制的)。
1.2. 编程实现
1.2.1. 定义笔画类
笔画的信息包括:笔的颜色、粗细、样式和笔画上的每个点的坐标。必须保留笔画的信息,才能在重绘窗口的时候绘制之前绘制的直线。
class Myline():
"""笔画类,包含笔迹的颜色、粗细、样式、数据点"""
def __init__(self, color, thick, style, datas):
self.pen_msg = (color, thick, style)
self.datas = datas
1.2.2. 定义画板缓冲类
-
直接法和缓冲法的实现都比较简单,以下都进行实现,通过
BUFFERED
进行设置。import wx BUFFERED = True # 使用缓冲法,即 double buffered # BUFFERED = False # 使用直接法 class SimpleSketchWindow(wx.Window): """画板缓冲窗口""" def __init__(self, *args, **kw): super().__init__(*args, **kw) self.cur_pos = (0,0) # 当前鼠标位置 self.cur_line = [] # 当前笔画 [(x1, y1), (x2, y2), ... ,(xn,yn)] self.lines = [] # 所有笔画 [line1, line2, ..., line m] self.pen_color = 'RED' # 笔迹颜色 self.pen_thick = 4 # 笔迹粗细 self.pen_style = wx.PENSTYLE_SOLID # 笔类型 # 设置缓存区 if BUFFERED: self.buffer = None self.InitBuffer() # 设置背景颜色 self.SetBackgroundColour('white') # 设置鼠标图标为“铅笔” self.SetCursor(wx.Cursor(wx.CURSOR_PENCIL)) # 绑定事件 self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_MOTION, self.OnMotion) self.Bind(wx.EVT_PAINT, self.OnPaint) # 触发时机:窗口大小变换 self.Bind(wx.EVT_SIZE, self.OnSize)
-
缓冲法相比直接法,在获得待绘制设备的 DC 前,多了一步“设置缓冲区”,以确保缓冲区的大小与窗口是一致的。
代码:
def InitBuffer(self): """初始化缓冲区""" if BUFFERED: # 设置缓冲区与窗口的大小一致 size = self.GetClientSize() self.buffer = wx.Bitmap(*size) # 第一个参数为None,相当于初始化 buffer dc = wx.BufferedDC(None, self.buffer) else: # 直接获得当前窗口的设别上下文 dc = wx.ClientDC(self) # 默认的绘画:绘制已存在的笔迹 self.DefaultDrawing(dc) # 添加你的绘画 self.DoMyDrawing(dc) def DefaultDrawing(self, dc:wx.DC): """默认绘画""" # 设置背景颜色 dc.SetBackground(wx.Brush(self.GetBackgroundColour())) dc.Clear() # 使用当前背景刷来清除设备上下文。 # 绘制所有的笔画 self.DrawAllLines(dc) def DrawAllLines(self, dc:wx.DC): """绘制所有的笔画""" for line in self.lines: # 设置笔画 pen = wx.Pen(*line.pen_msg) dc.SetPen(pen) # 绘制直线 for i in range(1, len(line.datas)): coord = (line.datas[i-1].x, line.datas[i-1].y, line.datas[i].x, line.datas[i].y) dc.DrawLine(*coord) def DoMyDrawing(self, dc:wx.DC): """需要继承此类,然后重写此函数""" pass
-
绑定
EVT_SIZE
和EVT_PAINT
事件,缺一不可。EVT_SIZE
事件的触发时机为:程序运行时创建窗口,窗口被拉伸而改变大小;
EVT_PAINT
事件的触发时机为:程序运行时创建窗口,窗口需要重新绘制时(如窗口被拉伸后,窗口从最小化恢复到正常大小等);需要说明的是,绘制的过程(不拉伸窗口)貌似不会触发这两个事件,但绑定少其中一个事件都会产生一些奇怪的 BUG。建议一定要绑定这两个事件,同时注意一些细节:
def OnSize(self, event): """响应窗口大小改变""" # 缓冲模式:每次窗口大小变换,都需要重新设置缓冲区大小 # 然后重画窗口 self.InitBuffer() print("OnSize") event.Skip() def OnPaint(self, event): """响应Paint Event""" if BUFFERED: wx.BufferedPaintDC(self, self.buffer) else: dc = wx.PaintDC(self) # 重新绘制 self.DefaultDrawing(dc) self.DoMyDrawing(dc) print("OnPaint") event.Skip()
-
OnSize()
执行的效果:重新绘制窗口。这里是直接调用
InitBuffer()
,对于缓冲模式,需要在绘制窗口前,重新设置缓冲区的大小。 -
OnPaint()
分别调用直接法和缓冲法的“Paint Event 专用”的设备上下文。直接法是
wx.PaintDC
,缓冲法是wx.BufferedPaintDC
。这两个 API 都只能在EVT_PAINT
中使用(即在这个OnPaint
函数内)。另外,对于直接法还需要“重新绘制”(这里调用self.DefaultDrawing(dc)
和self.DoMyDrawing(dc)
)。
-
-
笔画绘制的实现
def OnLeftDown(self, event:wx.MouseEvent): """鼠标左键按下,记录起始坐标""" # 获得当前鼠标位置 self.cur_pos = event.GetPosition() # 新笔画的起点 self.cur_line = [] self.cur_line.append(self.cur_pos) print("Left Down: (%d, %d)" % (self.cur_pos.x, self.cur_pos.y)) event.Skip() def OnLeftUp(self, event:wx.MouseEvent): """鼠标左键松开,记录当前笔画""" if len(self.cur_line) > 1: self.lines.append(Myline( self.pen_color, self.pen_thick, self.pen_style, self.cur_line)) print("Left Up: (%d, %d)" % (self.cur_pos.x, self.cur_pos.y)) event.Skip() def OnMotion(self, event:wx.MouseEvent): """鼠标移动(左键拖动)""" if event.Dragging() and event.LeftIsDown(): # 更新鼠标的坐标 pre_pos = self.cur_pos self.cur_pos = event.GetPosition() self.cur_line.append(self.cur_pos) # 设置缓冲区 if BUFFERED: # 设置缓冲区,当dc销毁时,将 buffer 复制到当前窗口上 dc = wx.BufferedDC(wx.ClientDC(self), self.buffer) else: # 直接获得当前窗口的设别上下文 dc = wx.ClientDC(self) # 绘制直线 pen = wx.Pen(self.pen_color, self.pen_thick, self.pen_style) dc.SetPen(pen) coord = (pre_pos.x, pre_pos.y, self.cur_pos.x, self.cur_pos.y) dc.DrawLine(*coord) print("Drawing:", coord) event.Skip()
1.2.3. 继承画板缓冲类
继承的好处是,提高代码的利用率。继承后的子类可以增加新的自定义的特性,也可以重写父类的方法。
class SketchWindow(SimpleSketchWindow):
def __init__(self, *args, **kw):
super().__init__(*args, **kw)
def DoMyDrawing(self, dc: wx.DC):
"""绘制自定义内容"""
self.DrawLogo(dc)
def DrawLogo(self, dc:wx.DC):
"""绘制logo"""
dc.SetPen(wx.Pen('RED'))
dc.DrawRectangle(5, 5, 50, 50)
dc.SetBrush(wx.Brush("MEDIUM SEA GREEN"))
dc.SetPen(wx.Pen('BLUE', 4))
dc.DrawRectangle(15, 15, 50, 50)
1.2.4. 定义测试类
class SketchFrame(wx.Frame):
def __init__(self):
super().__init__(parent=None, id=-1,
title="简易的画板",
size=(800,600)
)
self.sketch = SketchWindow(parent=self, id=-1)
# 窗口居中
self.Center()
if __name__ == '__main__':
app = wx.App()
frm = SketchFrame()
frm.Show()
app.MainLoop()
1.3. 运行结果
运行程序之后,可以拉伸放大缩小窗口,笔迹是不会消失的。
1.4. 完整代码
使用版本为:Python 3.9.6 64bit,wxPython 4.1.1
# -*- encoding: utf-8 -*-
# Python 3.9.6 64bit
'''
@File : MySktech.py
@Time : 2022/01/03 21:51
@Author : Wreng
@Description : wxpython 实现简易的画板
@Other : version - Python 3.9.6 64bit, wxPython 4.1.1
'''
import wx
"""
实现画板,有两种方式:直接法和缓冲法。
缓冲法,可以有效的避免屏闪;当绘图不是很复杂的时候,直接法的屏闪也不是很明显。
两种的实现也比较简单,分别由专门的dc一一对应的。
"""
BUFFERED = True # 使用缓冲法,即 double buffered
# BUFFERED = False # 使用直接法
class Myline():
"""笔画类,包含笔迹的颜色、粗细、样式、数据点"""
def __init__(self, color, thick, style, datas):
self.pen_msg = (color, thick, style)
self.datas = datas
class SimpleSketchWindow(wx.Window):
"""画板缓冲窗口"""
def __init__(self, *args, **kw):
super().__init__(*args, **kw)
self.cur_pos = (0,0) # 当前鼠标位置
self.cur_line = [] # 当前笔画 [(x1, y1), (x2, y2), ... ,(xn,yn)]
self.lines = [] # 所有笔画 [line1, line2, ..., line m]
self.pen_color = 'RED' # 笔迹颜色
self.pen_thick = 4 # 笔迹粗细
self.pen_style = wx.PENSTYLE_SOLID # 笔类型
# 设置缓存区
if BUFFERED:
self.buffer = None
self.InitBuffer()
# 设置背景颜色
self.SetBackgroundColour('white')
# 设置鼠标图标为“铅笔”
self.SetCursor(wx.Cursor(wx.CURSOR_PENCIL))
# 绑定事件
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
self.Bind(wx.EVT_MOTION, self.OnMotion)
self.Bind(wx.EVT_PAINT, self.OnPaint) # 触发时机:窗口大小变换
self.Bind(wx.EVT_SIZE, self.OnSize)
def InitBuffer(self):
"""初始化缓冲区"""
if BUFFERED:
# 设置缓冲区与窗口的大小一致
size = self.GetClientSize()
self.buffer = wx.Bitmap(*size)
# 第一个参数为None,相当于初始化 buffer
dc = wx.BufferedDC(None, self.buffer)
else:
# 直接获得当前窗口的设别上下文
dc = wx.ClientDC(self)
# 默认的绘画:绘制已存在的笔迹
self.DefaultDrawing(dc)
# 添加你的绘画
self.DoMyDrawing(dc)
def DefaultDrawing(self, dc:wx.DC):
"""默认绘画"""
# 设置背景颜色
dc.SetBackground(wx.Brush(self.GetBackgroundColour()))
dc.Clear() # 使用当前背景刷来清除设备上下文。
# 绘制所有的笔画
self.DrawAllLines(dc)
def DrawAllLines(self, dc:wx.DC):
"""绘制所有的直线"""
for line in self.lines:
# 设置笔画
pen = wx.Pen(*line.pen_msg)
dc.SetPen(pen)
# 绘制直线
for i in range(1, len(line.datas)):
coord = (line.datas[i-1].x, line.datas[i-1].y,
line.datas[i].x, line.datas[i].y)
dc.DrawLine(*coord)
def DoMyDrawing(self, dc:wx.DC):
"""需要继承此类,然后重构此函数"""
pass
# ====================================================================
# 事件响应函数
# ====================================================================
def OnSize(self, event):
"""响应窗口大小改变"""
# 每次窗口大小变换,都需要重新设置缓冲区大小
self.InitBuffer()
print("OnSize")
event.Skip()
def OnPaint(self, event):
"""响应Paint Event"""
if BUFFERED:
wx.BufferedPaintDC(self, self.buffer)
else:
dc = wx.PaintDC(self)
# 重新绘制
self.DefaultDrawing(dc)
self.DoMyDrawing(dc)
print("OnPaint")
event.Skip()
def OnLeftDown(self, event:wx.MouseEvent):
"""鼠标左键按下,记录起始坐标"""
# 获得当前鼠标位置
self.cur_pos = event.GetPosition()
# 新笔画的起点
self.cur_line = []
self.cur_line.append(self.cur_pos)
print("Left Down: (%d, %d)" % (self.cur_pos.x, self.cur_pos.y))
event.Skip()
def OnLeftUp(self, event:wx.MouseEvent):
"""鼠标左键松开,记录当前笔画"""
if len(self.cur_line) > 1:
self.lines.append(Myline(
self.pen_color, self.pen_thick, self.pen_style, self.cur_line))
print("Left Up: (%d, %d)" % (self.cur_pos.x, self.cur_pos.y))
event.Skip()
def OnMotion(self, event:wx.MouseEvent):
"""鼠标移动(左键拖动)"""
if event.Dragging() and event.LeftIsDown():
# 更新鼠标的坐标
pre_pos = self.cur_pos
self.cur_pos = event.GetPosition()
self.cur_line.append(self.cur_pos)
# 设置缓冲区
if BUFFERED:
# 设置缓冲区,当dc销毁时,将 buffer 复制到当前窗口上
dc = wx.BufferedDC(wx.ClientDC(self), self.buffer)
else:
# 直接获得当前窗口的设别上下文
dc = wx.ClientDC(self)
# 绘制直线
pen = wx.Pen(self.pen_color, self.pen_thick, self.pen_style)
dc.SetPen(pen)
coord = (pre_pos.x, pre_pos.y, self.cur_pos.x, self.cur_pos.y)
dc.DrawLine(*coord)
print("Drawing:", coord)
event.Skip()
class SketchWindow(SimpleSketchWindow):
def __init__(self, *args, **kw):
super().__init__(*args, **kw)
def DoMyDrawing(self, dc: wx.DC):
"""绘制自定义内容"""
self.DrawLogo(dc)
def DrawLogo(self, dc:wx.DC):
"""绘制logo"""
dc.SetPen(wx.Pen('RED'))
dc.DrawRectangle(5, 5, 50, 50)
dc.SetBrush(wx.Brush("MEDIUM SEA GREEN"))
dc.SetPen(wx.Pen('BLUE', 4))
dc.DrawRectangle(15, 15, 50, 50)
class SketchFrame(wx.Frame):
def __init__(self):
super().__init__(parent=None, id=-1,
title="简易的画板",
size=(800,600)
)
self.sketch = SketchWindow(parent=self, id=-1)
# 窗口居中
self.Center()
if __name__ == '__main__':
app = wx.App()
frm = SketchFrame()
frm.Show()
app.MainLoop()
1.5. 相关参考
- https://docs.wxpython.org/
- https://wiki.wxpython.org/CustomisedDrawing
- https://wiki.wxpython.org/DoubleBufferedDrawing
- wxPython in Action by Noel Rappin, Robin Dunn, Chapter 6 and Chapter 12
- 蓝奏云下载:https://wwe.lanzoup.com/b01oz824f,密码:g7np
- 书中的代码可以在这找到:https://github.com/freephys/wxPython-In-Action