0
点赞
收藏
分享

微信扫一扫

wxpython 实现简易画板(1)

勇敢乌龟 2022-01-04 阅读 71
前端python

文章目录


wxpython 实现简易画板(1)

任何用户界面工具的最基本的行为是在屏幕上绘制。

wxpython 用于绘制的主要概念是 device context(直译为设备上下文,简称 DC)。设备上下文使用一个标准的 API 来管理设备(屏幕或打印机)的绘制,如在屏幕上绘制一条直线、曲线或文本。也就是说,实现绘制功能基本过程就是:先获得待绘制设备的 DC,再通过调用 DC 的 API 实现绘制功能。

实现“画板”,需要使用的设备上下文有:

  • wx.PaintDC – drawing to the screen, during EVT_PAINT
  • wx.ClientDC – drawing to the screen, outside EVT_PAINT
  • wx.BufferedPaintDC – drawing to a buffer, then the screen, during EVT_PAINT
  • wx.BufferedDC – drawing to a buffer, then the screen, outside EVT_PAINT
  • wx.MemoryDC – drawing to a bitmap

1.1. 实现思路

绘制过程:

  1. 鼠标左键按下:开始绘制,记录笔画的起点;
  2. 拖动鼠标:记录移动过程中鼠标经过的每个点的坐标。这些数据点两两相连形成最终的笔迹;
  3. 鼠标左键松开:结束绘制,保存此笔迹。

实现画板,有两种实现方法:直接法和缓冲法。简单理解,直接法相当于每绘制一次就刷新一下屏幕,当绘图比较复杂时会产生屏闪现象;缓冲法是将所有的绘制先缓存到某个内存中,然后再一次性复制到屏幕,即只刷新一次,有效的避免屏幕闪烁。

直接法和缓冲法都有一一对应的设备上下文,不能混用。直接法对应的设备上下文是 wx.ClientDCwx.PaintDC,缓冲法对应的设备上下文是 wx.BufferedDCwx.BUfferedPaintDC。其中,wx.PaintDCwx.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. 定义画板缓冲类

  1. 直接法和缓冲法的实现都比较简单,以下都进行实现,通过 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)
    
  2. 缓冲法相比直接法,在获得待绘制设备的 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
    
  3. 绑定 EVT_SIZEEVT_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))。

  4. 笔画绘制的实现

    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
举报

相关推荐

0 条评论