在建構一個類別時我們必須考慮這個類別的本質與提供的功能,以我們想要建立的二維繪圖類別為例,其本質是一個可以顯示二維圖形的 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)。
- 設定更新畫布的顯示條件為清除舊的曲線。
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 引數示範如何設定文字顯示的顏色。這些類別方法的撰碼步驟只有三道程序:- 建立一個 QwtText 物件:tx,其內容為接收到的字串值。
- 以 tx 的物件方法 .setColor() 設定文字顯示的顏色。之前建立的模組階層辭典 colors 在這裡派上了用場,我們建立的物件方法所接收到的 color 引數會成為 colors 的 key 值以取出相對應的 Qt 顏色設定值,再傳給 .setColor() 函式來設定文字要顯示的顏色。因為我們建立的辭典 key 值都是小寫字母,所以在索引 colors 的值前我們多做一道手續,以字串的 .lower() 函式將 color 引數強制轉換成小寫文字,這樣無論使用者是輸入大寫或小寫文字做顏色引數都可以找到與之對應的顏色辭典值。
- 使用 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() 類別方法的撰碼程序如下:
- 建立一個 QwtPlotCurve() 物件:curve。
- 檢查 self.ishold 的值,以決定是否保留之前畫的圖。如果 self.ishold 為 False,則呼叫 QwtPlot 的 .clear() 方法將舊的圖清除。
- 使用 curve.setRenderHint(QwtPlotItem.RenderAntialiased) 將曲線反鋸齒化。
- 以 curve.setData() 將 x 與 y 數列設定給 curve 物件。
- 以 curve.setPen() 設定曲線物件 curve 的顏色、樣式、以及線寬。
- 將曲線貼附在畫布上。
- 呼叫 QwtPlot 的 replot() 方法來更新畫布上的繪圖物件。
#!/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)
沒有留言:
張貼留言