2010年2月5日 星期五

如何使用 PyQwt 畫圖 (四)

在前三篇關於 PyQwt 的文章中,基於介紹方便我們將圖形外觀的設定、繪圖資料的產生、以及繪圖程序全都放在自訂類別 Ex01、Ex02、Ex03、以及 Ex03_2 的 __init__ 函式中,每個自訂類別都做差不多的事但卻只能產生畫出特定方程式的物件。在本篇文章中我們示範架構一個較通用的 PyQwt 繪圖類別,設計一些有用的物件方法讓使用者可以透過這些介面來完成像是圖形外觀設定、或繪圖形式選擇等工作。

在建構一個類別時我們必須考慮這個類別的本質與提供的功能,以我們想要建立的二維繪圖類別為例,其本質是一個可以顯示二維圖形的 GUI 元件,因此我們可以使用 Qwt 的 QwtPlot 為基礎類別再擴充我們想要的功能以成為新的自訂類別。另外也需要提供各種類別方法當作與外界溝通的介面,讓使用者可以透過它們調整圖形的外觀,或者傳遞數據資料到特定的介面函式讓它把圖畫出來,至於類別方法的種類與數目就視使用者的需求來建立。在這個範例中我們將建立一些模仿 matlab 或  matplotlib 繪圖指令的類別方法,使用相似的指令名稱以及引數內容來完成各項繪圖功能。以下列出我們將在自訂類別中所設計的仿 matlab/matplotlib  型式之繪圖指令與說明:
  • plot(x, y, lc, ls, lw):plot 物件方法用來繪製二維的曲線 y = f(x) 到圖形物件上,引數 x 與 y 的型態為數列,lc、ls、與 lw 分別設定曲線的顏色 (line color) 、曲線的樣式 (line style)、以及曲線的線寬。
  • xlim(xmin, xmax);ylim(ymin, ymax):xlim 與 ylim 物件方法讓使用者可以控制圖形物件 x 軸與 y 軸的顯示範圍。 
  • xlabel(text, color);ylabel(text, color);title(text, color):xlabel、ylabel、以及 title 物件方法分別設定 x 軸、y 軸、以及畫布標題所要顯示的文字內容與顏色。
  • hold(tf):hold 物件方法設定清除畫面的旗標值為 True 或 False,以決定在每次畫新曲線時是否清除或保留上一次畫的圖。
  • grid(tf):grid 物件方法決定是否在圖形物件上顯示格線。
在這個範例中我們就只建構這些基本的類別方法,如果使用者需要更多的功能,像是畫布的背景顏色設定、多座標軸的設定、座標格線的設定、在特定值標上記號等需求,可以以這個範例為基礎再去擴充。

在實作這個二維繪圖類別前我們先定義兩個位於模組階層的辭典物件:colors 與 linestyles,這兩個辭典物件分別儲存 Qt 有關顏色與線條樣式的設定值,以方便我們需要設定圖形物件的顏色或線條樣式時,可以使用較簡化與直覺的 key 值來選取需要的設定值。類別的名稱取名為 Plot2DCurve,繼承自 QwtPlot  類別,在產生一個 Plot2DCurve 的物件時,我們給定以下的預設值:
  • 圖形物件的畫布背景顏色為白色。
  • 圖形物件的邊寬設定為 10 pixel。
  • 設定座標軸尺規對齊畫布。
  • 設定畫布框架的寬度與顯示樣式。
  • 設定座標軸尺規與畫布框架的距離為零,且不顯示座標軸尺規的龍骨 (backbone)。
  • 設定更新畫布的顯示條件為清除舊的曲線。
這些預設值全部設定在類別的 __init__ 物件方法中。另外,我們也在 __init__ 中加入 一個 QwtPlotGrid 物件 self.gd,以控制格線在圖形物件上的行為。到目前敘述為止的程式碼如下:

colors = {"black": Qt.black, "k": Qt.black,
          "white": Qt.white, "w": Qt.white,
          "red": Qt.red, "r": Qt.red,
          "green": Qt.green, "g": Qt.green,
          "blue": Qt.blue, "b": Qt.blue,
          "yellow": Qt.yellow, "y": Qt.yellow}

linestyles = {"solid": Qt.SolidLine, "-": Qt.SolidLine,
              "dashed": Qt.DashLine, "--": Qt.DashLine,
              "dash-dotted": Qt.DashDotLine, "-.": Qt.DashDotLine,
              "dash-double-dotted": Qt.DashDotDotLine, "-:": Qt.DashDotDotLine,
              "dotted": Qt.DotLine, ".": Qt.DotLine}


class Plot2DCurve(QwtPlot):

    def __init__(self, parent = None):
        QwtPlot.__init__(self, parent)
        self.setCanvasBackground(Qt.white)
        self.setMargin(10)
        self.plotLayout().setAlignCanvasToScales(True)
        #-- set canvas frame
        self.canvas().setLineWidth(1)
        self.canvas().setFrameStyle(QFrame.Box| QFrame.Plain)
        #-- set axis scale
        backbone = QwtAbstractScaleDraw.Backbone
        for axis in [self.xBottom, self.yLeft]:
            self.axisWidget(axis).setMargin(0)
            self.axisScaleDraw(axis).enableComponent(backbone, False)
        
        self.ishold = False
        self.gd = QwtPlotGrid()
接下來介紹如何建立畫布以及 x/y 軸標題文字的設定介面。這三個類別方法的核心指令是以 QwtPlot 的物件方法 .setTitle() 與 .setAxisTitle() 將接收的文字內容顯示到畫布的上方或指定的座標軸尺規上。除了顯示文字內容外,我們也可以在這些類別方法中增加一些設定文字字型、大小、或顏色等特徵的功能。在此例,我們以一個 color 引數示範如何設定文字顯示的顏色。這些類別方法的撰碼步驟只有三道程序:
  1. 建立一個 QwtText 物件:tx,其內容為接收到的字串值。
  2. 以 tx 的物件方法 .setColor() 設定文字顯示的顏色。之前建立的模組階層辭典 colors 在這裡派上了用場,我們建立的物件方法所接收到的 color 引數會成為 colors 的 key 值以取出相對應的 Qt 顏色設定值,再傳給 .setColor() 函式來設定文字要顯示的顏色。因為我們建立的辭典 key 值都是小寫字母,所以在索引 colors 的值前我們多做一道手續,以字串的 .lower() 函式將 color 引數強制轉換成小寫文字,這樣無論使用者是輸入大寫或小寫文字做顏色引數都可以找到與之對應的顏色辭典值。
  3. 使用 self.setTitle() 或 self.setAxisTitle() 將文字貼附到圖形物件上。
這三個類別方法的程式碼如下:

    def title(self, text, color="black"):
        tx = QwtText(text)
        tx.setColor(colors[color.lower()])
        self.setTitle(tx)

    def xlabel(self, text, color="black"):
        tx = QwtText(text)
        tx.setColor(colors[color.lower()])
        self.setAxisTitle(self.xBottom, tx)

    def ylabel(self, text, color="black"):
        tx = QwtText(text)
        tx.setColor(colors[color.lower()])
        self.setAxisTitle(self.yLeft, tx) 
在建立每個類別方法時,我們會對使用上非必要傳遞的引數提供預設值,像是文字的顏色就預設為黑色。

座標軸尺規的顯示範圖設定是使用 QwtPlot 的 .setAxisScale() 類別方法:

    def xlim(self, xmin, xmax):
        self.setAxisScale(self.xBottom, xmin, xmax)

    def ylim(self, ymin, ymax):
        self.setAxisScale(self.yLeft, ymin, ymax)

在這個繪圖類別中,我們設定了一個旗標 self.ishold 來決定是否清除上一次畫圖結果。預設上它的值是 False,代表每次畫新圖時不保留之前畫的圖。在實際應用上,有時會需要保留之前的圖,因此我們在自訂的繪圖類別中加入 hold() 物件方法來設定 self.ishold 的值,在必要的時候將 self.ishold 設定為 True 以保留之前畫圖的結果:

    def hold(self, tf=False):
        self.ishold = tf

控制格線是否顯示的功能由 grid() 類別方法來實現。為了簡化起見,在範例中我們將一些格線的特徵,諸如格線的樣式、顏色、寬度等,固定下來,整個 grid() 方法只接受 True 或 False 引數值以決定是否顯示格線。顯示格線是以 QwtPlotGrid 的 .attach() 方法將格線貼附在畫布上;移除格線則是使用 QwtPlotGrid 的 .detach() 方法。最後叫用 QwtPlot 的 .replot() 方法更新畫布上的繪圖物件:

    def grid(self, tf=False):
        if tf == True:
            gline = QPen(Qt.DotLine)
            gline.setColor(Qt.black)
            gline.setWidth(0)
            self.gd.setPen(gline)
            self.gd.attach(self)
        else:
            self.gd.detach()

        self.replot()

最後要完成的是 Plot2DCurve 這個類別中最重要的介面函式:plot()。plot 類別方法接受兩個必要的數列:x 與 y,然後畫出 y = f(x) 的曲線。曲線的顏色、樣式、與線寬由引數 lc、ls、以及 lw 來決定,在預設上我們給定曲線的特徵為 2 pt 寬的藍色實線。plot() 類別方法的撰碼程序如下:
  1. 建立一個 QwtPlotCurve() 物件:curve。
  2. 檢查 self.ishold 的值,以決定是否保留之前畫的圖。如果 self.ishold 為 False,則呼叫 QwtPlot 的 .clear() 方法將舊的圖清除。
  3. 使用 curve.setRenderHint(QwtPlotItem.RenderAntialiased) 將曲線反鋸齒化。
  4. 以 curve.setData() 將 x 與 y 數列設定給 curve 物件。
  5. 以 curve.setPen() 設定曲線物件 curve 的顏色、樣式、以及線寬。
  6. 將曲線貼附在畫布上。
  7. 呼叫 QwtPlot 的 replot() 方法來更新畫布上的繪圖物件。
加入 plot() 類別方法後,就完成整個 Plot2DCurve 繪圖類別。接下來我們將 Plot2DCurve 類別、 colors、以及 linestyles 等內容存放到 MatStyleQwt.py 檔案中,成為一個自訂的 Python 模組,以後要建立 Plot2DCurve 的物件時只要匯入這個模組即可。MatStyleQwt 模組的完整內容如下:
#!/usr/bin/env python

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.Qwt5 import *

colors = {"black": Qt.black, "k": Qt.black,
          "white": Qt.white, "w": Qt.white,
          "red": Qt.red, "r": Qt.red,
          "green": Qt.green, "g": Qt.green,
          "blue": Qt.blue, "b": Qt.blue,
          "yellow": Qt.yellow, "y": Qt.yellow}

linestyles = {"solid": Qt.SolidLine, "-": Qt.SolidLine,
              "dashed": Qt.DashLine, "--": Qt.DashLine,
              "dash-dotted": Qt.DashDotLine, "-.": Qt.DashDotLine,
              "dash-double-dotted": Qt.DashDotDotLine, "-:": Qt.DashDotDotLine,
              "dotted": Qt.DotLine, ".": Qt.DotLine}


class Plot2DCurve(QwtPlot):

    def __init__(self, parent = None):
        QwtPlot.__init__(self, parent)
        self.setCanvasBackground(Qt.white)
        self.setMargin(10)
        self.plotLayout().setAlignCanvasToScales(True)
        #-- set canvas frame
        self.canvas().setLineWidth(1)
        self.canvas().setFrameStyle(QFrame.Box| QFrame.Plain)
        #-- set axis scale
        backbone = QwtAbstractScaleDraw.Backbone
        for axis in [self.xBottom, self.yLeft]:
            self.axisWidget(axis).setMargin(0)
            self.axisScaleDraw(axis).enableComponent(backbone, False)
        
        self.ishold = False
        self.gd = QwtPlotGrid()
        
    def title(self, text, color="black"):
        tx = QwtText(text)
        tx.setColor(colors[color.lower()])
        self.setTitle(tx)

    def xlabel(self, text, color="black"):
        tx = QwtText(text)
        tx.setColor(colors[color.lower()])
        self.setAxisTitle(self.xBottom, tx)

    def ylabel(self, text, color="black"):
        tx = QwtText(text)
        tx.setColor(colors[color.lower()])
        self.setAxisTitle(self.yLeft, tx)

    def xlim(self, xmin, xmax):
        self.setAxisScale(self.xBottom, xmin, xmax)

    def ylim(self, ymin, ymax):
        self.setAxisScale(self.yLeft, ymin, ymax)

    def hold(self, tf=False):
        self.ishold = tf

    def grid(self, tf=False):
        if tf == True:
            gline = QPen(Qt.DotLine)
            gline.setColor(Qt.black)
            gline.setWidth(0)
            self.gd.setPen(gline)
            self.gd.attach(self)
        else:
            self.gd.detach()

        self.replot()

    def plot(self, x, y, lc="blue", ls="-", lw=2):
        curve = QwtPlotCurve()
        if self.ishold == False:
            self.clear()

        curve.setRenderHint(QwtPlotItem.RenderAntialiased)
        curve.setData(x, y)
        curve.setPen(QPen(colors[lc.lower()], lw, linestyles[ls.lower()]))
        curve.attach(self)
        self.replot()

我們以一個實際例子來看如何使用 MatStyleQwt 模組。在此例中我們使用 PyQt 建立一個簡單的 GUI 程式,它的圖形介面包含四個 UI 元件(widget),第一個 UI 元件使用我們建立的 Plot2DCurve 類別產生一個繪圖視窗;另外三個 UI 為按扭元件,按下後會在繪圖視窗上畫出 cos(x)、sin(x)的圖形以及顯示格線,下圖為執行程式的結果:


完整程式碼如下:
#!/usr/bin/env python

import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import numpy as np
#-- import user-defined module
from MatStyleQwt import *


class FIGURE(QWidget):

    def __init__(self):
        QWidget.__init__(self)
        self.x = np.arange(-np.pi, np.pi, 0.01)
        #-- Create a Plot2DCurve object for drawing curves
        self.fig = fig = Plot2DCurve()
        fig.xlim(-np.pi, np.pi)
        fig.ylim(-5, 5)
        fig.xlabel("x")
        fig.ylabel("y")
        fig.title("Plot sin(x) and cos(x)", "g")
        fig.hold(True)
        self.grid_on = False
        fig.grid(self.grid_on)

        #-- Create push buttons
        self.btn_cosx = QPushButton("Plot cos(x)")
        self.connect(self.btn_cosx, SIGNAL("clicked()"), self.plot_cosx)
        self.btn_sinx = QPushButton("Plot sin(x)")
        self.connect(self.btn_sinx, SIGNAL("clicked()"), self.plot_sinx)
        self.btn_grid = QPushButton("GRID ON")
        self.connect(self.btn_grid, SIGNAL("clicked()"), self.show_grid)

        #-- Layout GUI
        btns_box = QHBoxLayout()
        btns_box.addWidget(self.btn_cosx)
        btns_box.addWidget(self.btn_sinx)
        btns_box.addWidget(self.btn_grid)
        main_box = QVBoxLayout()
        main_box.addWidget(self.fig)
        main_box.addLayout(btns_box)
        self.setLayout(main_box)
        self.resize(400, 300)
        
    def plot_cosx(self):
        x = self.x
        cosx = 2.0*np.cos(x)
        self.fig.plot(x, cosx)
        
    def plot_sinx(self):
        x = self.x
        sinx = 4.5*np.sin(x)
        self.fig.plot(x, sinx, "r", "--", 1)

    def show_grid(self):
        self.grid_on = not self.grid_on
        self.fig.grid(self.grid_on)
    
    
if __name__ == "__main__":
    app = QApplication(sys.argv)
    frame = FIGURE()
    frame.show()
    app.exec_()

有關 PyQwt 的介紹就到這裡結束,希望這系列共四篇介紹 PyQwt 的文章能夠成為對 PyQwt 有興趣的人一個入門的起點。想要知道更多、更詳細的 PyQwt 資訊與使用方法請到它的官方網站查看。

(發佈日期:2010/02/05)

沒有留言:

張貼留言