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.MemoryDC
- wx.MetafifileDC
- wx.PostScriptDC
- wx.PrinterDC
其他三个不常用,暂时不讨论。
缓冲设备上下文
- 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用来刷新窗口,让前后图像具有一致性。
(欢迎转载,转载请注明出处。文中如有错误,还请指出。)