wxpython中的图像处理

载入图像

在wxpython中,与平台无关的图像处理由类wx.Image管理,而与平台有关的图像处理由类wx.Bitmap管理。要从一个文件载入一个图像,使用wx.Image的构造函数:

wx.Image(name, type=wx.BITMAP_TYPE_ANY)

 参数name是图像文件的名字,参数type是处理器类型,表示要读取图像的类型。如果type是wx.BITMAP_TYPE_ANY,那么wxPython将试图自动检测该文件的类型。使用这个类还可以生成指定大小的空图像:

wx.Image(width, height, clear=True)

 clear=True代表图像背景为黑色。生成空图像也可以使用wx.EmptyImage(width,height)。

Image类还提供了一些对图像进行基本操作的方法,如改变大小、裁剪、旋转等。

创建位图

我们可以直接使用wx.Bitmap来创建一个位图,用法和wx.Image差不多,如wx.Bitmap(name, type=wx.BITMAP_TYPE_ANY)、wx.Bitmap(width, height, clear=True)和wx.BitmapImage(width,height)。同样他也有一组对位图进行基本操作的方法。

wx.StaticBitmap

这是一个控件,继承自wx.Control,可以用来显示一个静态的位图,就像wx.StaticText用来显示静态文本一样。使用wx.StaticBitmap我们可以实现一个以某张图像为背景的窗口。

构造函数:

StaticBitmap(parent, id=ID_ANY, bitmap=NullBitmap, pos=DefaultPosition,
             size=DefaultSize, style=0, name=StaticBitmapNameStr)

 光标

可以使用wx.Cursor来创建一个光标对象,构造函数是:

Cursor(cursorName, type=BITMAP_TYPE_ANY, hotSpotX=0, hotSpotY=0)  #从一个图像文件构建

Cursor(cursorId) # 从标准ID构建

Cursor(image) #从一个Image对象构建

 关于标准ID对应的光标样式,可以看这儿:传送门

创建之后,可以使用wx.Window的SetCursor(cursor)方法设置可视物的光标。

设备上下文

设备上下文可以让我们进行绘画,wx.DC是设备上下文的基类,它定义了一个公共的API,比如话各种图形、绘制图片、绘制文本、设置画笔和笔刷等。画笔和笔刷表示线条和填充的样式。

基于屏幕的设备上下文

  • wx.ClientDC
  • wx.PaintDC
  • wx.WindowDC
  • wx.ScreenDC
屏幕设备上下文是指临时创建的。这意味你无论你何时需要它们时只应该局部地创建它们,并且使它们能够被正常垃圾回收。你不应该试图以一个实例变量的形式占有一个设备上下文——这是不安全的,并可能导致程序的不稳定。

通常,你会使用wx.ClientDC或wx.PaintDC来绘制到屏幕上的一个窗口。如果你在一个EVT_PAINT事件处理期间绘制到屏幕,那么你必须使用wx.PaintDC。在其它的时间,你必须使用wx.ClientDC。实际上,无论你何时绑定一个处理器到EVT_PAINT事件,你都必须在该处理器方法中创建一个wx.PaintDC对象,即使你不使用它(不创建一个wx.PaintDC会导致平台认为该事件并未完全处理,并因此会发送另一个事件)。描绘(paint)事件要求不同的设备上下文的原因是,这个wx.PaintDC实例是被优化来只在重绘事件期间的窗口刷新区域中进行绘制的,以便重绘更快。两者的构造函数分别是wx.ClientDC(window)和wx.PaintDC(window)。当你使用这两个上下文时,你将只能在该窗口部件的客户区中进行绘制。这意味在一个框架中,你不能在边框、标题栏或其它装饰物上进行绘制。

如果你需要在框架的整个区域上进行绘制,包括边框和装饰物,那么你应该使用wx.WindowDC。如果你想让整个屏幕作为你的画布。在这种情况下,你可以使用wx.ScreenDC。

非屏幕设备上下文

  • wx.MemoryDC
  • wx.MetafifileDC
  • wx.PostScriptDC
  • wx.PrinterDC
第一个是wx.MemoryDC,它使你能够绘制到一个储存在内存中位图而非正显示的。你可以使用一个不带参数的构造函数wx.MemoryDC()来创建一个wx.MemoryDC,但在使用它之前,你必须将它与一个位图相关联。这通过调用一个带有一个wx.Bitmap类型的参数的方法SelectObject(bitmap)来实现。一旦你这样做了,你就可以绘制到这个内存设备上下文,并且内部的位图被改变。当你完成绘制时,你可以使用Blit()方法将位图绘制到一个窗口。

其他三个不常用,暂时不讨论。

缓冲设备上下文

  • wx.BufferedDC
  • wx.BufferedPaintDC
缓冲使你能够发送单独的绘制命令到缓冲区,然后一次性地把它们绘制到屏幕。当你一下做几个重绘时,这防止了屏幕的闪烁。因此,当做动画或其它屏幕密集绘制时,缓冲是一个常见的技术。

在wxPython中有两个缓冲设备上下文——wx.BufferedDC,它可被用于缓冲任何的设备上下文(但是通常只用于wx.ClientDC);wx.BufferedPaintDC,它专门被设计用来缓冲一个wx.PaintDC。作为内存设备上下文的一个简单包装,这两个缓冲上下文的工作方式基本上是一样。wxBufferedDC的构造函数要求一个设备上下文和一个可选的位图作为参数——wx.BufferedDC(dc, buffer=None)。另一方面,wx.BufferedPaintDC要求一个窗口和一个可选的位图作为参数——wx.BufferedPaintDC(dc, buffer=None)。参数dc是最终你想要绘制到的设备上下文,对于wx.BufferedPaintDC,窗口参数被用于内在地创建一个wx.PaintDC,buffer参数是一个位图,它被作为临时的缓冲。如果buffer参数没有指定,那么该设备上下文内在地创建它自己的位图。一旦缓冲设备上下文被创建,你就可以像其它的设备上下文样使用它。在内部,缓冲上下文使用一个内存设备上下文和该位图来储存绘制。

关于DC的几个注意点

  • 调用Clear()方法会使用当前背景画笔清除设备上下文。如果未设置背景画笔,则使用纯白色画笔清除设备上下文。调用此方法会产生一个wx.EVT_PAINT事件。

实战:自制画板

转载于:https://qixinbo.info/2019/10/16/imagepy_11/

import wx


class SketchWindow(wx.Window):
    def __init__(self, parent, ID):
        wx.Window.__init__(self, parent, ID)
        self.SetBackgroundColour("White")
        self.color = "Black"
        self.thickness = 1

        #该行创建了一个wx.Pen实例,从中可以指定画线的颜色、宽度和线型等。
        self.pen = wx.Pen(self.color, self.thickness, wx.SOLID)
        self.lines = []
        self.curLine = []
        self.pos = (0, 0)
        self.InitBuffer()

        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_SIZE, self.OnSize)
        self.Bind(wx.EVT_IDLE, self.OnIdle) #空闲时处理
        self.Bind(wx.EVT_PAINT, self.OnPaint)

    def InitBuffer(self):
        size = self.GetClientSize()

        #  通过两步创建缓冲设备上下文:(1)创建一张空白位图作为缓冲区;
        # (2)使用上述缓冲区创建一个缓冲设备上下文。这个缓冲上下文是为了防止线条的重绘使得屏幕闪烁。
        self.buffer = wx.Bitmap(size.width, size.height)
        dc = wx.BufferedDC(None, self.buffer)
        #  创建一个wx.Brush画刷来设置设备上下文的背景,同时使用该背景画刷来清空上下文的内容。
        dc.SetBackground(wx.Brush(self.GetBackgroundColour()))
        dc.Clear()

        self.DrawLines(dc)
        self.reInitBuffer = False

    def GetLinesData(self):
        return self.lines[:]

    def SetLinesData(self, lines):
        self.lines = lines[:]
        self.InitBuffer()
        self.Refresh()

    def OnLeftDown(self, event):
        self.curLine = []
        #GetPosition()返回的是wx.Point类型的数据,需要调用它的Get()函数将坐标转化为元组格式。
        self.pos = event.GetPosition().Get()
        #CaptureMouse()方法使得所有的鼠标输入局限在该窗口中,
        # 即使有时候拖动鼠标超出该窗口的边界。该动作必须在后面通过调用ReleaseMouse()来取消。
        self.CaptureMouse()


    # 定义鼠标左键弹起时的动作,此时是将之前鼠标划过的线条存储到self.lines中,
    # 用于窗口重绘时的那个看不见的dc的绘图。同时如上所述,
    # ReleaseMouse()将系统返回到上一个CaptureMouse()之前的状态。wxPython使用一个堆栈来追踪捕获鼠标的窗口,
    # 因此ReleaseMouse()和CaptureMouse()的数目必须相等。
    def OnLeftUp(self, event):
        if self.HasCapture():
            self.lines.append((self.color,
                               self.thickness,
                               self.curLine))
            self.curLine = []
            self.ReleaseMouse()

    # 画线时,要首先判断鼠标拖动是不是画线的一部分,即既要鼠标左键按下,
    # 又要鼠标在拖动;如果这两个条件都满足,则进入画线状态。
    # 因为wx.BufferedDC是一种临时创建的设备上下文,因此在画线之前要重新创建一个wx.BufferedDC,
    # 这里创建了一个wx.ClientDC作为主上下文,然后重新使用了那张空白位图作为缓冲。
    def OnMotion(self, event):
        if event.Dragging() and event.LeftIsDown():
            dc = wx.BufferedDC(wx.ClientDC(self), self.buffer)
            self.drawMotion(dc, event)
        event.Skip()

    #这一步是在设备上下文上进行绘图,注意coords是新旧坐标的综合,两个tuple相加的结果是两者拼接起来,这个语法要注意。
    # 同时使用星号变量将coords这个tuple中的四个元素拆分为单个元素传入DrawLine()函数。
    def drawMotion(self, dc, event):
        dc.SetPen(self.pen)
        newPos = event.GetPosition().Get()
        coords = self.pos + newPos
        self.curLine.append(coords)
        dc.DrawLine(*coords)
        self.pos = newPos

    #如果窗口的尺寸被改变,则将self.reInitBuffer属性置为True,然后什么都不用做,直到调用下一个空闲事件。
    def OnSize(self, event):
        self.reInitBuffer = True

    #当出现空闲事件后,程序就会趁机响应改变尺寸的动作。这里选择将改变尺寸的动作放在空闲事件处理中,
    # 而不是放在它本来的尺寸变化事件处理中,是为了允许多个尺寸改变事件能够快速地连续执行,而不用等待每一个的重绘。
    def OnIdle(self, event):
        if self.reInitBuffer:
            self.InitBuffer()
            self.Refresh(False)

    #处理重绘要求是比较简单的,即只需要创建一个缓冲绘图设备上下文,注意因为这里是wx.PaintEvent中,
    # 因此,需要使用wx.PaintDC,而不是wx.ClientDC。
    def OnPaint(self, event):
        dc = wx.BufferedPaintDC(self, self.buffer)

    # 在窗口尺寸改变、需要重绘时,使用那个看不见的dc根据之前存储的线条进行绘制。
    def DrawLines(self, dc):
        for colour, thickness, line in self.lines:
            pen = wx.Pen(colour, thickness, wx.SOLID)
            dc.SetPen(pen)
            for coords in line:
                dc.DrawLine(*coords)

    def SetColor(self, color):
        self.color = color
        self.pen = wx.Pen(self.color, self.thickness, wx.SOLID)

    def SetThickness(self, num):
        self.thickness = num
        self.pen = wx.Pen(self.color, self.thickness, wx.SOLID)


class SketchFrame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, -1, "Sketch Frame",
                          size=(800, 600))
        self.sketch = SketchWindow(self, -1)


if __name__ == '__main__':
    app = wx.App()
    frame = SketchFrame(None)
    frame.Show(True)
    app.MainLoop()

 

 

详细机理为:当初始化时,首先创建了一个全黑色的wx.Bitmap位图self.buffer,然后将它传给了第一个dc,因为这个dc会设置背景为白色,所以其实这时将self.buffer存成图像,即buffer_before_DrawLines.jpg是一张白色图像,即非常重要的知识点就是dc会改变self.buffer。
在初始化时,InitBuffer()和OnPaint()函数都会调用,那么此时buffer_before_DrawLines.jpg、buffer_after_DrawLines.jpg和buffer_in_OnPaint.jpg都是纯白色图像,而因为鼠标还没有开始绘图,则OnMotion()不会被调用,那么buffer_in_OnMotion.jpg也不会调用。
当鼠标开始绘图时,buffer_in_OnMotion.jpg就会生成,且会将当前有线条的图像存储下来,即self.buffer也会被改变。
如果此时拖动窗口边界,但注意不要松开鼠标,则会发现OnPaint()函数一直执行,但InitBuffer()却没有执行,这就是因为之前将InitBuffer()放在了空闲事件中的结果,如果将它放在wx.EVT_SIZE中,那么也可以,但明显在拖动边界时你会感觉到很卡的感觉。
如果拖动了窗口边界,且松开鼠标后,InitBuffer()就会执行,此时可以发现,buffer_before_DrawLines.jpg因为存储的是初始化后的self.buffer,所以它仍然是白色的,而buffer_after_DrawLines.jpg则会存储有线条且当前窗口形状的图像,注意它是当前窗口形状的图像,而buffer_in_OnMotion.jpg是之前窗口的图像,除非再次用鼠标绘图。
如果不关联wx.EVT_PAINT事件,此时拖动窗口边界,则会发现此时会出现“擦掉”图像的现象,但下一次鼠标再次绘制时,之前的线条又会重现,这会非常让人困扰,所以该事件是非常必要的。

那么总结一下:
self.buffer是穿插在这几个dc间的纽带,每个dc都可以对它进行修改。第一个BufferedDC是为了在窗口尺寸变化时存储之前绘制的线条,第二个BufferedDC是实际在鼠标交互时的绘图层,它让用户能实时看到绘制了什么,然后在窗口重绘时它就销毁,将坐标信息传给第一个DC,第三个BufferedPaintDC是为了取出缓冲self.buffer用来刷新窗口,让前后图像具有一致性。

 

posted @ 2022-05-25 18:33  GXX探索者  阅读(1018)  评论(0编辑  收藏  举报