wxPython自製Control : 用Matplotlib畫圖表的FigurePanel

最近可能會接到一個需要GUI上畫不少圖表的案子,於是就來研究了一下wxPython + matplotlib,前陣子買了wxPython in action,雖然一本1500元大洋讓我心有點淌血,不過這本書真的寫得很棒,讓我對wxPython有更深入的瞭解,如果案子接下來買n本都可以,我個人認為買書來讀是一種投資,所以基本上看上眼的書買起來也不會手軟,接著因為除了畫圖形,也需要用到不少GUI,我把wxPython官方的demo抓下來,裡面每個範例都跑了一次,對於wxPython能做到什麼地步有了更深入的瞭解,越來越覺得wxPython真是酷斃了,很好玩的GUI framework

在這之後,雖然案子還在歸劃的初期,不過我不是很喜歡空等而沒有程式可以寫的感覺,其實在接洽的這段期間就能先完成一些將會用到的東西,像是widget之類的,或寫寫原形來驗證確實可行之類的都很好,所以我就寫了一個用Matplotlib畫圖表的Control : FigurePanel,因為往後在寫GUI都會用到,如果每次都要特地寫來處理這些我會發瘋,當然最好的方法就是包成Control,如此一來所有圖表都可以直接使用,加上可以有統一的行為,例如另存新檔、列印等等,相當方便

什麼是Matplotlib? Matplotlib是一套強大的Python畫圖表用的函式庫,如果不知道他到底可以畫到什麼地步,就可以看他們官網的畫廊,相信看完後就知道它有多強大,幾乎你想得到的圖、想不到的圖,沒看過的圖,他都可以畫,當然,如果裡面沒有你要的圖,或著你要自創圖,當然也可以自己擴增,有了這套函式庫,原本我可能得先花個一個星期還多少時間來建構一套畫圖表的基礎,如今這些時間完全省了下來,這就是使用Open source的好處,沒有這些東西真的不知道要寫到民國幾年

切入正題,我們來看一下FigurePanel的原始碼

import wx
from wx import xrc
import matplotlib
matplotlib.use('WXAgg')
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg
from matplotlib.backends.backend_wx import NavigationToolbar2Wx
from matplotlib.figure import Figure

__all__ = ["HAVE_TOOL_BAR", "FigurePanel", "FigurePanelXmlHandler"]

# this flag indicate that there is toolbar in figure panel
HAVE_TOOL_BAR = 1

class FigurePanel(wx.PyPanel):
    def __init__(self, figSize=None, figDpi=None, *args, **kwargs):
        wx.Panel.__init__(self, *args, **kwargs)
        self.figSize = figSize
        self.figDpi = figDpi
        self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
        self.createFigure(figSize, figDpi)

    def createFigure(self, size, dpi):
        """Create figure canvas

        """
        self.fig = Figure(size, dpi)
        self.canvas = FigureCanvasWxAgg(self, wx.ID_ANY, self.fig)
        self.toolbar = None
        # Now put all into a sizer
        sizer = wx.BoxSizer(wx.VERTICAL)
        # This way of adding to sizer allows resizing
        sizer.Add(self.canvas, 1, wx.LEFT|wx.TOP|wx.GROW)

        # add  tool bar
        if self.GetWindowStyle() & HAVE_TOOL_BAR:
            self.toolbar = self.createToolbar()
            # On Windows, default frame size behaviour is incorrect
            # you don't need this under Linux
            tw, th = self.toolbar.GetSizeTuple()
            fw, fh = self.canvas.GetSizeTuple()
            self.toolbar.SetSize(wx.Size(fw, th))
            # Best to allow the toolbar to resize!
            sizer.Add(self.toolbar, 0, wx.GROW)

        self.SetSizer(sizer)
        self.Fit()

    def createToolbar(self):
        """Create toolbar for controlling figure panel, override this method
        to provide custom toolbar

        """
        toolbar = NavigationToolbar2Wx(self.canvas)
        toolbar.Realize()
        return toolbar

    def GetToolBar(self):
        # You will need to override GetToolBar if you are using an
        # unmanaged toolbar in your frame
        return self.toolbar

    def onEraseBackground(self, evt):
        # this is supposed to prevent redraw flicker on some X servers...
        pass

    def getFigure(self):
        """Get figure of this panel

        """
        return self.fig

class FigurePanelXmlHandler(xrc.XmlResourceHandler):
    def __init__(self):
        xrc.XmlResourceHandler.__init__(self)
        # Specify the styles recognized by objects of this type
        self.AddStyle("wxHAVE_TOOL_BAR", HAVE_TOOL_BAR)
        self.AddWindowStyles()

    # This method and the next one are required for XmlResourceHandlers
    def CanHandle(self, node):
        return self.IsOfClass(node, "FigurePanel")

    def DoCreateResource(self):
        assert self.GetInstance() is None

        figSize = self.GetParamValue("figsize")
        if not len(figSize):
            figSize = None
        else:
            figSize = map(int, figSize.split(','))

        figDpi =  self.GetParamValue("figdpi")
        if not len(figDpi):
            figDpi = None
        else:
            figDpi = int(figDpi)

        # Now create the object
        panel = FigurePanel(
            figSize,
            figDpi,
            self.GetParentAsWindow(),
            self.GetID(),
            self.GetPosition(),
            self.GetSize(),
            self.GetStyle("style", 0),
            self.GetName()
        )
        return panel

if __name__ == '__main__':
    app = wx.PySimpleApp()

    class Frame(wx.Frame):
        def __init__(self, *args, **kwargs):
            wx.Frame.__init__(self, *args, **kwargs)

            self.figPanel = FigurePanel(parent=self, figSize=(5, 5), figDpi=75)
            self.draw()

            sizer = wx.BoxSizer()
            sizer.Add(self.figPanel, 1, wx.GROW)
            self.SetSizer(sizer)
            self.Fit()

        def draw(self):
            fig = self.figPanel.getFigure()
            plot = fig.add_subplot(111)
            plot.plot([1,2,3])
            plot.set_ylabel('some numbers')

    frame = Frame(parent=None, title='Figure panel demo')
    frame.Show()
    app.MainLoop()

其實程式不難,很簡單,就只有這樣而已,配合XmlResource的InsertHandler就可以寫在xrc檔裡

from figure_panel import *

def getHandlers():
    """Get xrc handlers

    """
    from wx import xrc
    for key, value in globals().iteritems():
        try:
            if issubclass(value, xrc.XmlResourceHandler):
                yield value
        except TypeError:
            pass

def installHandlers(res):
    """Insert xml handlers into xml resource object

    @param res: resources object to insert
    """
    for handler in getHandlers():
        res.InsertHandler(handler())

有了xml handler,wxPython在讀你的xrc檔時就可以認得並且正確地建構出FigurePanel,有其它自訂的Control其實也都可以用這樣的方式來擴增到xrc檔中,xrc檔就可以像這樣寫


	
	
		wxVERTICAL
		
			
			wxALL|wxEXPAND
			5
			
				
			
		
	


一個簡單的例子,跑起來像這樣

figure_panel

從此之後在wxPython中畫圖表就非常輕鬆了

This entry was posted in Python, 中文文章 and tagged , , , , . Bookmark the permalink.

6 Responses to wxPython自製Control : 用Matplotlib畫圖表的FigurePanel

  1. Hua says:

    matplotlib畫出來的圖(這裡我用的是”plot”來畫 (x,y)), 放在wxPython的frame與dialog, 在圖畫的”移動”與”放大”會有不太一樣的反應; 在dialog上的圖”移動”的反應比較滿一些.

  2. victor says:

    不太懂@@,不太一樣的反應? 比較滿? 我這裡的是用Panel做的,放在Frame裡,所以如果放在Dialog裡反應會不一樣?

  3. Hua says:

    例如”移動”, plot的圖會隨著滑鼠拖曳, 很平順的移動, 然而在dialog中, 圖與滑鼠的連動有些延遲.像”跳格”般移動.
    後來我做了一個上下圖的x軸連動, 當上圖x軸limit改變時, 下圖也會跟著變. 這種情況下, 即使放在frame中也會有些不平順的表現.
    可以這麼說, matplotlib在圖的”重繪”上有些效能的問題.

  4. victor says:

    這我倒是沒有很強烈的感覺,我的應用的圖幾乎都是靜態的,有也應該是有變時才重畫,matplotlib在重畫上的確不夠快,每次都是整張重畫,我不知道是不是Backend的問題,我在想如果把Backend改成GDI+之類原生的畫圖方式,或是關掉反鋸齒之類的話,速度會不會改善

  5. kevin.huang says:

    hello 小弟我最近也再這套軟體開發一些TOOL,遇到最大的問題感覺就是開圖不像matlab這麼快,感覺每開一次圖就會頓一下..因為我處理可能到上百張圖我現在作法是那它自動存檔plt.savefig plt.close() 不讓他秀出來 可是在開檔存檔感覺還是頗佔資源 在畫圖時CPU都幾乎是100% 到大概70張左右就會當機 出現記憶體錯誤.. 不知道版主有沒有好的解決方法….目前我適用批次畫不超過60為原則..不知有沒有語法可以清理記憶體的