本文是对《wxPython in Action》一书的第6.1节和第12.2节的翻译理解。
ImagePy很多的组件都用到了wxPython的设备上下文进行绘制,比如histogram panel、curve panel、colormap panel和Canvas等。可以说,对于wxPython不提供的组件,ImagePy都通过DC绘制进行了自己开发,因此这一部分对于理解ImagePy的UI交互会很有帮助。
wxPython设备上下文
为了能在屏幕上绘图,需要使用wxPython的称为设备上下文Device Context的对象,它能对显示设备进行抽象,给予每个设备一套通用的绘图方法,这样一来,编写的绘图代码就对任意的设备都是相同的。wxPython使用wx.DC及其子类来描述设备上下文。因为wx.DC是个抽象类,因此在实际使用时必须使用它的一个子类。
wx.DC的子类:
wx.BufferedDC:用来缓冲一系列的绘图命令,直到这些命令都完成且准备在屏幕上绘图,这可以防止出现不想要的闪烁现象;
wx.BufferedPaintDC:与wx.BufferedDC相同,但仅能在绘图事件wx.PaintEvent的处理过程中使用,仅能临时地创建该类的实例;
wx.ClientDC:用来在窗口的工作区绘图,即不能在边界或其他装饰区域绘图;该类应该被临时创建,且不能在wx.PaintEvent事件处理中使用;
wx.MemoryDC:用来将图像写入内存中的位图,不用来显示;然后可以选择这张位图,使用wx.DC.Blit()方法将位图绘制在窗口上;
wx.MetafileDC:在Windows操作系统上,该设备上下文可以用来创建标准的Windows元文件数据;
wx.PaintDC:与wx.ClientDC相同,但它仅能用在wx.PaintEvent事件处理中;仅临时创建该类的实例;
wx.PostScriptDC:用来存成PostScript文件;
wx.PrinterDC:在Windows平台上用来写文件给打印机;
wx.ScreenDC:用来直接在屏幕上绘图,仅能临时创建;
wx.WindowDC:用来在整个窗口上绘图,包括边界及其他装饰组件上;非Windows操作系统可能不支持该类。
画图板
原始代码见:
wxPython-In-Action/Chapter-06/example1.py
上述代码在Phoenix版的wxPython下无法运行,因为部分API已经改变,以下是经过修改后能够顺利运行的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102import 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
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()
self.buffer = wx.Bitmap(size.width, size.height)
dc = wx.BufferedDC(None, self.buffer)
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 = []
self.pos = event.GetPosition().Get()
self.CaptureMouse()
def OnLeftUp(self, event):
if self.HasCapture():
self.lines.append((self.color,
self.thickness,
self.curLine))
self.curLine = []
self.ReleaseMouse()
def OnMotion(self, event):
if event.Dragging() and event.LeftIsDown():
dc = wx.BufferedDC(wx.ClientDC(self), self.buffer)
self.drawMotion(dc, event)
event.Skip()
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
def OnSize(self, event):
self.reInitBuffer = True
def OnIdle(self, event):
if self.reInitBuffer:
self.InitBuffer()
self.Refresh(False)
def OnPaint(self, event):
dc = wx.BufferedPaintDC(self, self.buffer)
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()
下面对一些必要的知识点说明一下:1
self.pen = wx.Pen(self.color, self.thickness, wx.SOLID)
该行创建了一个wx.Pen实例,从中可以指定画线的颜色、宽度和线型等。1
2
3
4
5
6self.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)
为这个画图板绑定各种鼠标处理事件,包括鼠标左键按下和弹起、鼠标运动、窗口尺寸变化、窗口重绘,以及空闲时候的处理工作。1
2self.buffer = wx.Bitmap(size.width, size.height)
dc = wx.BufferedDC(None, self.buffer)
通过两步创建缓冲设备上下文:(1)创建一张空白位图作为缓冲区;(2)使用上述缓冲区创建一个缓冲设备上下文。这个缓冲上下文是为了防止线条的重绘使得屏幕闪烁。
下面的代码还会创建一个dc,注意这两个dc的不同。这里的dc更像是一个看不见的绘图层,它主要是在窗口尺寸变化、需要重绘的时候调用,是通过读取鼠标事件存储下来的一系列的坐标,然后调用self.DrawLines(dc)一次性绘出很多的线条。而第二个dc是一个即时的绘图dc。1
2dc.SetBackground(wx.Brush(self.GetBackgroundColour()))
dc.Clear()
创建一个wx.Brush画刷来设置设备上下文的背景,同时使用该背景画刷来清空上下文的内容。1
self.pos = event.GetPosition().Get()
得到当前鼠标的精确坐标位置,注意这个地方有API变化,书中的是GetPositionTuple(),同时GetPosition()返回的是wx.Point类型的数据,需要调用它的Get()函数将坐标转化为元组格式。1
self.CaptureMouse()
CaptureMouse()方法使得所有的鼠标输入局限在该窗口中,即使有时候拖动鼠标超出该窗口的边界。该动作必须在后面通过调用ReleaseMouse()来取消。1
2
3
4
5
6
7def OnLeftUp(self, event):
if self.HasCapture():
self.lines.append((self.color,
self.thickness,
self.curLine))
self.curLine = []
self.ReleaseMouse()
定义鼠标左键弹起时的动作,此时是将之前鼠标划过的线条存储到self.lines中,用于窗口重绘时的那个看不见的dc的绘图。同时如上所述,ReleaseMouse()将系统返回到上一个CaptureMouse()之前的状态。wxPython使用一个堆栈来追踪捕获鼠标的窗口,因此ReleaseMouse()和CaptureMouse()的数目必须相等。1
2
3if event.Dragging() and event.LeftIsDown():
dc = wx.BufferedDC(wx.ClientDC(self), self.buffer)
self.drawMotion(dc, event)
画线时,要首先判断鼠标拖动是不是画线的一部分,即既要鼠标左键按下,又要鼠标在拖动;如果这两个条件都满足,则进入画线状态。因为wx.BufferedDC是一种临时创建的设备上下文,因此在画线之前要重新创建一个wx.BufferedDC,这里创建了一个wx.ClientDC作为主上下文,然后重新使用了那张空白位图作为缓冲。1
2
3
4
5
6
7def 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
这一步是在设备上下文上进行绘图,注意coords是新旧坐标的综合,两个tuple相加的结果是两者拼接起来,这个语法要注意。同时使用星号变量将coords这个tuple中的四个元素拆分为单个元素传入DrawLine()函数。1
2def OnSize(self, event):
self.reInitBuffer = True
如果窗口的尺寸被改变,则将self.reInitBuffer属性置为True,然后什么都不用做,直到调用下一个空闲事件。1
2
3
4def OnIdle(self, event):
if self.reInitBuffer:
self.InitBuffer()
self.Refresh(False)
当出现空闲事件后,程序就会趁机响应改变尺寸的动作。这里选择将改变尺寸的动作放在空闲事件处理中,而不是放在它本来的尺寸变化事件处理中,是为了允许多个尺寸改变事件能够快速地连续执行,而不用等待每一个的重绘。1
2def OnPaint(self, event):
dc = wx.BufferedPaintDC(self, self.buffer)
处理重绘要求是比较简单的,即只需要创建一个缓冲绘图设备上下文,注意因为这里是wx.PaintEvent中,因此,需要使用wx.PaintDC,而不是wx.ClientDC。1
2
3
4
5
6def 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)
在窗口尺寸改变、需要重绘时,使用那个看不见的dc根据之前存储的线条进行绘制。
下面详细分析这三个dc之间的关系,可以在代码中适时地插入一些print函数和SaveFile函数看一下,如下:
1 | def InitBuffer(self): |
详细机理为:当初始化时,首先创建了一个全黑色的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用来刷新窗口,让前后图像具有一致性。
绘制雷达图
原始代码见:
wxPython-In-Action/Chapter-12/radargraph.py
1 | import wx |
该程序与上面的画图板原理相同,需要注意的是它将InitBuffer()放进了EVT_SIZE事件处理函数中,这样每次窗口变化时就会得到一个新的缓冲,雷达图的绘制也会随着窗口的变化实时改变。
该例子用到了更多的dc的功能,如DrawCircle、DrawText、DrawPolygon等。
绘制位图
原始代码见:
wxPython-In-Action/Chapter-12/draw_image.py1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42# This one shows how to draw images on a DC.
import wx
import random
random.seed()
class RandomImagePlacementWindow(wx.Window):
def __init__(self, parent, image):
wx.Window.__init__(self, parent)
self.photo = image.ConvertToBitmap()
# choose some random positions to draw the image at:
self.positions = [(10,10)]
for x in range(50):
x = random.randint(0, 1000)
y = random.randint(0, 1000)
self.positions.append( (x,y) )
# Bind the Paint event
self.Bind(wx.EVT_PAINT, self.OnPaint)
def OnPaint(self, evt):
# create and clear the DC
dc = wx.PaintDC(self)
brush = wx.Brush("sky blue")
dc.SetBackground(brush)
dc.Clear()
# draw the image in random locations
for x,y in self.positions:
dc.DrawBitmap(self.photo, x, y, True)
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title="Loading Images",
size=(640,480))
img = wx.Image("masked-portrait.png")
win = RandomImagePlacementWindow(self, img)
app = wx.App()
frm = TestFrame()
frm.Show()
app.MainLoop()
主要函数就是DrawBitmap(),如果是绘制图标,则用DrawIcon()。