wxPython 画图板
终于开始Python学习之旅了,姑且以一个“画图板”小项目开始吧。放慢脚步,一点一点地学习。
1月28日更新
第一次遇到的麻烦便是“重绘”,查了好多资料,终于重绘成功了。
#-*- encoding: gbk -* import wx """ this version use class 'PaintDC' and 'ClientDC' """ class MainFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, -1, "画图板 - 芒果布丁", (0, 0), (800, 500)) self.Center(wx.BOTH) # 窗口居中显示 self.x1, self.x2, self.y1, self.y2 = 0, 0, 0, 0 self.st = 'line' self.pos = (0,0) self.pen = wx.Pen("green", 1, wx.SOLID) self.brush = wx.Brush('', wx.TRANSPARENT) #透明填充 self.shapes = [] self.SetBackgroundColour("black") self.b1 = wx.Button(self, -1, label="矩形", pos=(10, 10), size=(50, 30)) self.b2 = wx.Button(self, -1, label="圆形", pos=(10, 50), size=(50, 30)) self.b3 = wx.Button(self, -1, label="直线", pos=(10, 90), size=(50, 30)) self.b1.SetDefault() self.InitBuffer() self.Bind(wx.EVT_BUTTON, self.ToRect, self.b1) self.Bind(wx.EVT_BUTTON, self.ToOval, self.b2) self.Bind(wx.EVT_BUTTON, self.ToLine, self.b3) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_PAINT, self.OnPaint) def InitBuffer(self): size = self.GetClientSize() self.buffer = wx.EmptyBitmap(size.width, size.height) dc = wx.BufferedDC(None, self.buffer) dc.SetPen(self.pen) dc.SetBackground(wx.Brush(self.GetBackgroundColour())) dc.SetBrush(self.brush) dc.Clear() self.Draw(dc) def ToRect(self, event): self.st = 'rect' def ToOval(self, event): self.st = 'oval' def ToLine(self, event): self.st = 'line' def OnLeftDown(self, event): self.p1 = event.GetPositionTuple() self.x1, self.y1 = self.p1 def OnLeftUp(self, event): self.p2 = event.GetPositionTuple() self.shapes.append((self.st, self.p1 + self.p2)) dc = wx.ClientDC(self) self.Draw(dc) def OnPaint(self, event): dc = wx.PaintDC(self) # 处理一个paint(描绘)请求 self.Draw(dc) def Draw(self, dc): dc.SetPen(self.pen) dc.SetBackground(wx.Brush(self.GetBackgroundColour())) dc.SetBrush(self.brush) dc.Clear() for st,(x1,y1,x2,y2) in self.shapes: if st == 'line': dc.DrawLine(x1, y1, x2, y2) elif st == 'oval': dc.DrawEllipse(x1, y1, x2-x1, y2-y1) elif st == 'rect': dc.DrawRectangle(x1, y1, x2-x1, y2-y1) if __name__ =='__main__': app = wx.PySimpleApp() frame = MainFrame() frame.Show() app.MainLoop()
#-*- encoding: gbk -* import wx """ this version use class 'BufferedPaintDC' and 'BufferedDC' """ class MainFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, -1, "画图板 - 芒果布丁", (0, 0), (800, 500)) self.Center(wx.BOTH) # 窗口居中显示 self.x1, self.x2, self.y1, self.y2 = 0, 0, 0, 0 self.st = 'line' self.pos = (0,0) self.pen = wx.Pen("green", 1, wx.SOLID) self.brush = wx.Brush('', wx.TRANSPARENT) #透明填充 self.shapes = [] self.InitBuffer() self.SetBackgroundColour("black") self.b1 = wx.Button(self, -1, label="矩形", pos=(10, 10), size=(50, 30)) self.b2 = wx.Button(self, -1, label="圆形", pos=(10, 50), size=(50, 30)) self.b3 = wx.Button(self, -1, label="直线", pos=(10, 90), size=(50, 30)) self.Bind(wx.EVT_BUTTON, self.ToRect, self.b1) self.Bind(wx.EVT_BUTTON, self.ToOval, self.b2) self.Bind(wx.EVT_BUTTON, self.ToLine, self.b3) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_PAINT, self.OnPaint) def InitBuffer(self): size = self.GetClientSize() self.buffer = wx.EmptyBitmap(size.width, size.height) def ToRect(self, event): self.st = 'rect' def ToOval(self, event): self.st = 'oval' def ToLine(self, event): self.st = 'line' def OnLeftDown(self, event): self.p1 = event.GetPositionTuple() self.x1, self.y1 = self.p1 def OnLeftUp(self, event): self.p2 = event.GetPositionTuple() self.shapes.append((self.st, self.p1 + self.p2)) self.Draw() def OnPaint(self, event): dc = wx.BufferedPaintDC(self, self.buffer) # 处理一个paint(描绘)请求 def Draw(self): dc = wx.BufferedDC(wx.ClientDC(self), self.buffer) dc.SetPen(self.pen) dc.SetBackground(wx.Brush(self.GetBackgroundColour())) dc.SetBrush(self.brush) dc.Clear() for st,(x1,y1,x2,y2) in self.shapes: if st == 'line': dc.DrawLine(x1, y1, x2, y2) elif st == 'oval': dc.DrawEllipse(x1, y1, x2-x1, y2-y1) elif st == 'rect': dc.DrawRectangle(x1, y1, x2-x1, y2-y1) if __name__ =='__main__': app = wx.PySimpleApp() frame = MainFrame() frame.Show() app.MainLoop()
以上两种方法差别不大,第二种方法用buffer缓冲,据说可以消除画布闪烁,但第一种方法在没有缓冲的情况下也没有出现闪烁的情况。但是有必要对几个DC进行详细说明一下:
如何在屏幕上绘画
要在屏幕上绘画,我们要用到一个名为device context(设备上下文)的wxPython对象。设备上下文代表抽象的设备,它对于所有的设备有一套公用的绘画方法,所以对于不同的设备,你的代码是相同的,而不用考虑你所在的具体设备。设备上下文使用抽象的wxPython的类wx.DC和其子类来代表。由于wx.DC是抽象的,所以对于你的应用程序,你需要使用它的子类。
使用设备上下文
表6.1显示了wx.DC的子类及其用法。设备上下文用来在wxPython窗口部件上绘画,它应该是局部的,临时性的,不应该以实例变量、全局变量或其它形式在方法调用之间保留。在某些平台上,设备上下文是有限的资源,长期持有wx.DC可能导致你的程序不稳定。由于wxPython内部使用设备上下文的方式,对于在窗口部件中绘画,就存在几个有着细微差别的wx.DC的子类。第十二章将更详细地说明这些差别。
表6.1
wx.BufferedDC:用于缓存一套绘画命令,直到命令完整并准备在屏幕上绘画。这防止了显示中不必要的闪烁。
wx.BufferedPaintDC:和wx.BufferedDC一样,但是只能用在一个wx.PaintEvent的处理中。仅临时创建该类的实例。
wx.ClientDC:用于在一个窗口对象上绘画。当你想在窗口部件的主区域上(不包括边框或别的装饰)绘画时使用它。主区域有时也称为客户区。wx.ClientDC类也应临时创建。该类仅适用于wx.PaintEvent的处理之外。
wx.MemoryDC:用于绘制图形到内存中的一个位图中,此时不被显示。然后你可以选择该位图,并使用wx.DC.Blit()方法来把这个位图绘画到一个窗口中。
wx.MetafileDC:在Windows操作系统上,wx.MetafileDC使你能够去创建标准窗口图元文件数据。
wx.PaintDC:等同于wx.ClientDC,除了它仅用于一个wx.PaintEvent的处理中。仅临时创建该类的实例。
wx.PostScriptDC:用于写压缩的PostScript文件。
wx.PrinterDC:用于Windows操作系统上,写到打印机。
wx.ScreenDC:用于直接在屏幕上绘画,在任何被显示的窗口的顶部或外部。该类只应该被临时创建。
wx.WindowDC:用于在一个窗口对象的整个区域上绘画,包括边框以及那些没有被包括在客户区域中的装饰。非Windows系统可能不支持该类。
——引自这个网站
屏幕设备上下文是指临时创建的。这意味你无论你何时需要它们时只应该局部地创建它们,并且使它们能够被正常垃圾回收。你不应该试图以一个实例变量的形式占有一个设备上下文——这是不安全的,并可能导致程序的不稳定。
通常,你会使用wx.ClientDC或wx.PaintDC来绘制到屏幕上的一个窗口。使用哪个取决于你执行绘制的时机。如果你在一个EVT_PAINT事件处理期间绘制到屏幕,那么你必须使用wx.PaintDC。在其它的时间,你必须使用wx.ClientDC。实际上,无论你何时绑定一个处理器到EVT_PAINT事件,你都必须在该处理器方法中创建一个wx.PaintDC对象,即使你不使用它(不创建一个wx.PaintDC会导致平台认为该事件并未完全处理,并因此会发送另一个事件)。描绘(paint)事件要求不同的设备上下文的原因是,这个wx.PaintDC实例是被优化来只在重绘事件期间的窗口刷新区域中进行绘制的,以便重绘更快。
屏幕设备上下文是指临时创建的。这意味你无论你何时需要它们时只应该局部地创建它们,并且使它们能够被正常垃圾回收。你不应该试图以一个实例变量的形式占有一个设备上下文——这是不安全的,并可能导致程序的不稳定。
通常,你会使用wx.ClientDC或wx.PaintDC来绘制到屏幕上的一个窗口。使用哪个取决于你执行绘制的时机。如果你在一个EVT_PAINT事件处理期间绘制到屏幕,那么你必须使用wx.PaintDC。在其它的时间,你必须使用wx.ClientDC。实际上,无论你何时绑定一个处理器到EVT_PAINT事件,你都必须在该处理器方法中创建一个wx.PaintDC对象,即使你不使用它(不创建一个wx.PaintDC会导致平台认为该事件并未完全处理,并因此会发送另一个事件)。描绘(paint)事件要求不同的设备上下文的原因是,这个wx.PaintDC实例是被优化来只在重绘事件期间的窗口刷新区域中进行绘制的,以便重绘更快。
你可以经由一个简单的构造函数来创建一个客户区(client)或描绘(paint)上下文,构造函数的一个参数是你希望在其上进行绘制的wxPython窗口部件,两者的构造函数分别是wx.ClientDC(window)和wx.PaintDC(window)。当你使用这两个上下文时,你将只能在该窗口部件的客户区中进行绘制。这意味在一个框架中,你不能在边框、标题栏或其它装饰物上进行绘制。
如果你需要在框架的整个区域上进行绘制,包括边框和装饰物,那么你应该使用wx.WindowDC。创建一个wx.WindowDC的方法和wx.ClientDC相似,相应的构造函数是wx.WindowDC(window)。和wx.ClientDC一样,你不应该在一个描绘(paint)事件期间创建一个wx.WindowDC——边框绘制行为与描绘(paint)设备上下文的局部优化是不兼容的。
有时,你不愿被限制于只绘制到一个单一的窗口,你想让整个屏幕作为你的画布。在这种情况下,你可以使用wx.ScreenDC。同样地,你不能在一描绘(paint)事件期间创建它。这个构造函数没有参数(因为你不需要再指定绘制到的对象)——wx.ScreenDC()。创建了一个wx.ScreenDC之后,你就可以像使用其它的设备上下文一样使用它了。你绘制的图像将显示在你的显示器中的所有的窗口的上层。
缓冲使你能够发送单独的绘制命令到缓冲区,然后一次性地把它们绘制到屏幕。当你一下做几个重绘时,这防止了屏幕的闪烁。因此,当做动画或其它屏幕密集绘制时,缓冲是一个常见的技术。
在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参数没有指定,那么该设备上下文内在地创建它自己的位图。一旦缓冲设备上下文被创建,你就可以像其它的设备上下文样使用它。在内部,缓冲上下文使用一个内存设备上下文和该位图来储存绘制。这样的捷径就是你不需要做任何特别的事来得到缓冲以绘制到实际的设备上下文。当缓冲设备上下文被垃圾回收时(通常当该方法结束且它已经退出作用域时),C++销毁函数触发Blit(),这将绘制缓冲区的内容到实际的设备上下文,而没有更多的工作需要你做
——引自这个网址
用缓冲的方式的重绘部分为什么OnPaint方法只是定义一个BufferedPaintDC即可?
对于所有的显示要求,都将产生wx.EVT_PAINT事件(描绘事件),并调用我们这里的方法OnPaint进行屏幕刷新(重绘),你可以看到这是出乎意料的简单:创建一个缓存的画图设备上下文。实际上wx.PaintDC被创建(因为我们处在一个Paint请求里,所以我们需要wx.PaintDC而非一个wx.ClientDC实例),然后在dc实例被删除后(函数返回时被销毁),位图被一块块地传送(blit)给屏幕并最终显示。关于缓存的更详细的信息将在随后的段落中提供。
1月31日大年初一更新
这个版本主要更新了鼠标随动绘画并修正了一些bug。
#-*- encoding: gbk -* import wx """ this version use class 'BufferedPaintDC' and 'BufferedDC' """ class MainFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, -1, "画图板 - 芒果布丁", (0, 0), (800, 500)) self.Center(wx.BOTH) # 窗口居中显示 self.x1, self.x2, self.y1, self.y2 = 0, 0, 0, 0 self.st = 'line' self.pos = (0,0) self.pen = wx.Pen("green", 1, wx.SOLID) self.brush = wx.Brush('', wx.TRANSPARENT) #透明填充 self.shapes = [] self.SetBackgroundColour("black") self.b1 = wx.Button(self, -1, label="矩形", pos=(10, 10), size=(50, 30)) self.b2 = wx.Button(self, -1, label="圆形", pos=(10, 50), size=(50, 30)) self.b3 = wx.Button(self, -1, label="直线", pos=(10, 90), size=(50, 30)) self.b1.SetDefault() self.InitBuffer() self.Bind(wx.EVT_BUTTON, self.ToRect, self.b1) self.Bind(wx.EVT_BUTTON, self.ToOval, self.b2) self.Bind(wx.EVT_BUTTON, self.ToLine, self.b3) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_MOTION, self.OnMotion) def InitBuffer(self): size = self.GetClientSize() self.buffer = wx.EmptyBitmap(size.width, size.height) dc = wx.BufferedDC(None, self.buffer) dc.SetPen(self.pen) dc.SetBackground(wx.Brush(self.GetBackgroundColour())) dc.SetBrush(self.brush) dc.Clear() self.Draw(dc) def ToRect(self, event): self.st = 'rect' def ToOval(self, event): self.st = 'oval' def ToLine(self, event): self.st = 'line' def OnLeftDown(self, event): self.p1 = event.GetPositionTuple() self.x1, self.y1 = self.p1 self.CaptureMouse()#6 捕获鼠标 def OnMotion(self, event): if event.Dragging() and event.LeftIsDown():#8 确定是否在拖动 dc = wx.BufferedDC(wx.ClientDC(self), self.buffer)#9 创建另一个缓存的上下文 self.drawMotion(dc, event) event.Skip() def drawMotion(self, dc, event): self.p2 = event.GetPositionTuple() self.shapes.append((self.st, self.p1 + self.p2)) self.InitBuffer() self.shapes.pop(len(self.shapes)-1) def OnLeftUp(self, event): self.p2 = event.GetPositionTuple() self.shapes.append((self.st, self.p1 + self.p2)) self.InitBuffer() self.ReleaseMouse()#7 释放鼠标 def OnPaint(self, event): dc = wx.BufferedPaintDC(self, self.buffer) # 处理一个paint(描绘)请求 def Draw(self, dc): for st,(x1,y1,x2,y2) in self.shapes: if st == 'line': dc.DrawLine(x1, y1, x2, y2) elif st == 'oval': dc.DrawEllipse(x1, y1, x2-x1, y2-y1) elif st == 'rect': dc.DrawRectangle(x1, y1, x2-x1, y2-y1) #self.Refresh() # 不能手动刷新 会闪烁 if __name__ =='__main__': app = wx.PySimpleApp() frame = MainFrame() frame.Show() app.MainLoop()
#6:CaptureMouse()方法控制了鼠标并在窗口的内部捕获鼠标,即使是你拖动鼠标到窗口边框的外面,它仍然只响应窗口内的鼠标动作,也就是说当画一个图形并将鼠标移到了窗口外,当前图形也有效并被画在屏幕上。在程序的后面必须调用ReleaseMouse()来取消其对鼠标的控制。否则该窗口将无法通过鼠标关闭等,试将#7注释掉。
2月11日 更新
这个版本添加了菜单功能、菜单事件、图片打开功能。
#-*- encoding: gbk -* import wx import os """ this version use class 'BufferedPaintDC' and 'BufferedDC' """ class MainFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, -1, "画图板 - 芒果布丁", (0, 0), (800, 500)) self.Center(wx.BOTH) # 窗口居中显示 self.x1, self.x2, self.y1, self.y2 = 0, 0, 0, 0 self.iscaptured = -1 self.p1 = (0, 0) self.p2 = (0, 0) self.st = 'line' self.pos = (0,0) self.pen = wx.Pen("green", 1, wx.SOLID) self.img = None self.brush = wx.Brush('', wx.TRANSPARENT) #透明填充 self.shapes = [] self.SetBackgroundColour("black") self.b1 = wx.Button(self, -1, label="矩形", pos=(10, 10), size=(50, 30)) self.b2 = wx.Button(self, -1, label="圆形", pos=(10, 50), size=(50, 30)) self.b3 = wx.Button(self, -1, label="直线", pos=(10, 90), size=(50, 30)) self.b1.SetDefault() self.InitBuffer() self.Bind(wx.EVT_BUTTON, self.ToRect, self.b1) self.Bind(wx.EVT_BUTTON, self.ToOval, self.b2) self.Bind(wx.EVT_BUTTON, self.ToLine, self.b3) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_MOTION, self.OnMotion) self.SetMenuBar(self.getMenu()) def InitBuffer(self): size = self.GetClientSize() mm = wx.DisplaySize() # 获取屏幕大小 self.buffer = wx.EmptyBitmap(mm[0], mm[1]) dc = wx.BufferedDC(None, self.buffer) dc.SetPen(self.pen) dc.SetBackground(wx.Brush(self.GetBackgroundColour())) dc.SetBrush(self.brush) dc.Clear() self.Draw(dc) def ToRect(self, event): self.st = 'rect' def ToOval(self, event): self.st = 'oval' def ToLine(self, event): self.st = 'line' def OnLeftDown(self, event): self.p1 = event.GetPositionTuple() self.x1, self.y1 = self.p1 self.CaptureMouse()#6 捕获鼠标 self.iscaptured = 1 def OnLeftUp(self, event): if self.iscaptured == 1: self.ReleaseMouse()#7 释放鼠标 self.p2 = event.GetPositionTuple() self.shapes.append((self.st, self.p1 + self.p2)) self.InitBuffer() self.iscaptured = 0 def OnMotion(self, event): if event.Dragging() and event.LeftIsDown():#8 确定是否在拖动 dc = wx.BufferedDC(wx.ClientDC(self), self.buffer)#9 创建另一个缓存的上下文 self.drawMotion(dc, event) event.Skip() def drawMotion(self, dc, event): if self.iscaptured == 1: self.p2 = event.GetPositionTuple() self.shapes.append((self.st, self.p1 + self.p2)) self.InitBuffer() self.shapes.pop(len(self.shapes)-1) def OnPaint(self, event): wx.BufferedPaintDC(self, self.buffer) # 处理一个paint(描绘)请求 def Draw(self, dc): if self.img != None: dc.DrawBitmap(self.img, 0, 0, False) print len(self.shapes) for st,(x1,y1,x2,y2) in self.shapes: print x1, y1, x2, y2 if st == 'line': dc.DrawLine(x1, y1, x2, y2) elif st == 'oval': dc.DrawEllipse(x1, y1, x2-x1, y2-y1) elif st == 'rect': dc.DrawRectangle(x1, y1, x2-x1, y2-y1) def getMenu(self): menuBar = wx.MenuBar() menu = wx.Menu() m11 = menu.Append(-1, "新建") self.Bind(wx.EVT_MENU, self.newFile, m11) m12 = menu.Append(-1, "打开") self.Bind(wx.EVT_MENU, self.openFile, m12) m13 = menu.Append(-1, "保存") menu.AppendSeparator() exit = menu.Append(-1, "退出") menuBar.Append(menu, "文件") menu2 = wx.Menu() menu2.Append(-1, "撤销") menu2.Append(-1, "清除") menuBar.Append(menu2, "编辑") menu3 = wx.Menu() menuBar.Append(menu3, "图像") menu4 = wx.Menu() menuBar.Append(menu4, "帮助") return menuBar def newFile(self, event): pass def openFile(self, event): wildcard = "JPEG 图片 (*.jpg)|*.jpg|" \ "PNG 图片 (*.png)|*.png|" \ "BMP 图片 (*.bmp)|*.bmp|" \ "All files (*.*)|*.*" dialog = wx.FileDialog(None, "选择一个文件", os.getcwd(), "", wildcard, wx.OPEN) if dialog.ShowModal() == wx.ID_OK: name = dialog.GetPath() print dialog.GetPath() jpg = wx.Image(name, wx.BITMAP_TYPE_ANY).ConvertToBitmap() self.img = jpg dc = wx.BufferedDC(wx.ClientDC(self), self.buffer) dc.DrawBitmap(jpg, 0, 0, False) if __name__ =='__main__': app = wx.PySimpleApp() frame = MainFrame() frame.Show() app.MainLoop()
3月19日 更新
这个版本添加了图片保存功能。这个功能的实现是直接使用PIL的截图功能ImageGrab,代码异常简单。
import ImageGrab rectangle = (x, y, x+w, y+h) img = ImageGrab.grab(rectangle) img.save("1.jpg", 'JPEG')
经过测试发现,虽然这个方法成功截图,但是图片质量会下降,变得稍微模糊。如果画的是纯图形(直线、矩形、圆等),可以直接保存端点坐标到文件,打开的时候读取端点重新画图形就能完美还原。