2009年10月20日 星期二

如何使用 PyQwt 畫圖 (二)

讓我們來看底下的這兩張圖:
左邊的圖是我們在上一篇介紹 PyQwt 的文章中所完成的圖,右邊的則是將它整型後的圖,看起來是不是比較順眼點?接下來將介紹如何調整 PyQwt 圖形物件的外觀。

在正式調整圖形之前,我們先來認識圖形外觀的構成元素。底下是一張將各種外觀元素稍微誇大的示意圖:
一個 QwtPlot 圖形物件在預設上包含一個畫布(canvas)物件,兩個座標軸的尺規物件(畫布下方的 x 軸與左方的 y 軸)。Canvas 是讓我們放置 QwtPlotItem 物件的地方(例如圖中的實心曲線),利用 QwtPlot 的物件方法 .setCanvasBackground(QColor) 可以讓我們設定 canvas 的背景顏色,上圖即是使用 Qt 內定的Qt.white 參數讓 canvas 的背景色由預設的灰色改成白色。我們可以使用 QwtPlot 的物件方法 .canvas() 取得圖的 QwtPlotCanvas 物件,以進行其它更細部的設定。例如在預設上 canvas 只顯現下方與左方的框條,因為 QwtPlotCanvas 是一個有框架結構的 QWidget (亦即繼承自 QFrame 的類別),所以我們可以用 QFrame 的物件方法 .setFrameStyle() 來設定 QwtPlotCanvas 的框架樣式(像是加個 box 在上面),以及用 .setLineWidth() 來設定框架的寬度。以下為設定 canvas 背景色與加上 2 pixel 寬度 box 的程式碼:
fig.setCanvasBackground(Qt.white)
fig.canvas().setLineWidth(2)
fig.canvas().setFrameStyle(QFrame.Box|QFrame.Plain)
程式碼內的 fig 是一個 QwtPlot 物件,以下的內容有提到 fig 的部分也是一樣,總之在範例中看到 fig 時請記得它是一個 QwtPlot 物件。

以淺藍色標示的框框是 QwtPlot 物件的邊寛(margin),預設值是 0,這裡我用 20 個 pixel 的寛度來突顯它的存在,習慣上我會留 10 個 pixel 寛。在使用上是以 PyQwtPlot 的物件方法 .setMargin() 來做設定,想要有 10 個 pixel 寛就呼叫物件方法 .setMargin(10) 即可。如果想知道目前 QwtPlot 物件的邊寬有多少,呼叫其物件方法 .margin() 即會回傳邊寛的值。

左下方以橘色標示的矩形為 canvas margin,是尺規兩側邊界與 canvas 兩側邊界間的寬度,預設值是 0,上圖設定為 20 pixel 以突顯它。設定 canvas margin 的方式不是那麼直接,因為這項特徵是以 QwtPlotLayout 的物件方法來設定,因此我們必須先取得 QwtPlot 的 QwtPlotLayout 物件後再呼叫其 .setCanvasMargin() 方法,以下的程式碼示範 20 pixel  寬的 canvas margin 設定方式:
fig.plotLayout().setCanvasMargin(20)
關於 canvas margin 的設定有一點要說的是,即使我們設定它的寬為 0,尺規邊界與 canvas 間還是會有一點寬度存在。這有點糟糕,因為在一般使用的情況下我們會希望尺規能與 canvas 完全對齊,幸運的是 QwtPlotLayout 提供一個物件方法 .setAlignCanvasToScales(bool) 來達成我們的心願。使用上一樣透過 QwtPlot 的 .plotLayout() 方法取得圖形的 QwtPlotLayout 物件,再呼叫 .setAlignCanvasToScales(True) 來完成設定:
fig.plotLayout().setAlignCanvasToScales(True)
因為 .setAlignCanvasToScales() 方法會抑制.setCanvasMargin() 的效用,因此在實作上只要叫用前者即可。

QwPlot 有四個內定的列舉常數成員:.yLeft、.yRight、.xBottom、以及 .xTop,稱作 axisId 參數,用來代表位於 canvase 的左右下上四個方位的座標軸。預設上只出現下方 x 軸與左方 y 軸,我們可以使用物件方法 .enableAxis(axisId, bool) 來啟用或取消指定的座標軸,例如:
fig.enableAxis(fig.yLeft, False)
fig.enableAxis(fig.yRight, True)
分別代表取消 fig 的左方 y 軸,以及開啟右方的 y 軸。至於在 canvas 上的曲線是參照到那一個座標軸的問題將會在下一篇介紹 QwtPlotItem 物件的文章中說明。

座標軸是以一個尺規物件(QwtScaleDraw)來呈現,它的外觀由三個部分組成:分別是龍骨(backbone)、刻度(tick),以及刻度值(label)。其中刻度又分為主要刻度(major tick)與次要刻度(minor tick):
在 QwtAbstractScaleDraw 類別中分別以三個列舉常數:.Backbone、.Ticks、以及 .Labels 來表示,利用 fig.axisScaleDraw(axisId) 的指令可以取得指定軸的 QwtScaleDraw 物件,再利用其父類別 QwtAbstractScaleDraw 的 .enableComponent(component, bool) 物件方法來決定保留那些尺規外觀的組成部分。例如要去掉 fig 下方 x 軸的龍骨只需加入以下程式碼:
x_sd = fig.ScaleDraw(fig.xBottom)
backbone = QwtAbstractScaleDraw.Backbone
x_sd.enableComponent(backbone, False)
QwtPlot 的 .setAxisScale(axisId, min, max, step) 物件方法讓我們可以設定尺規顯示的範圍,引數 axisId 為被設定的坐標軸;min、max、與 step 引數的型態為浮點數,分別用來設定尺規刻度值的最小值、 最大值、以及主要刻度間的間距。預設上 step 的值為零,代表按內定的準則來決定主要刻度的間距,因此大多數情況下我們只要給定前面三項引數即可。

很多時候我們會需要使用到特別型態的尺規來彰顯不同數量級間的變化,例如上圖我們設定尺規為對數型態(即 log10),這在一些科學與工程的應用上經常用到。設定的方法為叫用 QwtPlot 的 .setAxisScaleEngine(axisId, QwtScaleEngine),目前 Qwt 只提供兩個刻度型態的尺規器械物件,分別是線性的 QwtLinearScaleEngine 與對數化的 QwtLog10ScaleEngine。此外,我們還可以利用 QwtPlot 的 .setAxisMaxMajor() 與 .setAxisMaxMinor() 兩個物件方法來分別設定主要刻度與次要刻度的最大間隔數量,不過我並不建議用這種方法去調整主要刻度,因為刻度值是標在主要刻度的下方,定死主要刻度的數量有時候會使刻度值疊在一起,造成難以識別的窘境。至於次要刻度的數量就有必要依實際的需求進行設定。

最後有關尺規設定部分要提及的是它的邊寬設定,尺規的邊寬就是本文第二張圖中粉紅色長條的寬度。設定上是用 fig.axisWidget(axisId) 取得指定軸的 QwtScaleWidget 物件,再用它的 .setMargin() 方法來設定尺規的邊寬,預設值是保留 2 pixel 寬度,所以在不作任何處理的情況下尺規與 canvas 間會有點空隙存在。以下用幾行程式碼總結有關尺規設定的說明:
fig.setAxisScale(fig.xBottom, 0.1, 10)
fig.setAxisScale(fig.yLeft, 0, 10, 2)
fig.setAxisScaleEngine(fig.xBottom, QwtLog10ScaleEngine())
fig.setAxisMaxMinor(fig.xBottom, 10)
fig.axisWidget(fig.yLeft).setMargin(20)

關於圖的外觀還有很多地方可以調整,不過我的介紹就到此為止,讀者應該能夠以這篇文章為基礎,到 Qwt 的官方網站查看其它的設定細節。另外有關設定 canvas 標題與 x、y 軸的標題已經在第一篇文章中提及,這裡就不再贅述。以下為畫出本文第一張右邊的圖的程式碼,依照上面的說明相信讀者已經可以理解每一行程式碼的意義,並且有能力去修改它以符合自己對圖形外觀的需求。
#!/usr/bin/env python

import sys
import numpy as np
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.Qwt5 import *


class Ex02(QWidget):

    def __init__(self):
        QWidget.__init__(self)
        fig = QwtPlot()
        fig.setParent(self)
        fig.setCanvasBackground(Qt.white)
        fig.setMargin(10)
        fig.plotLayout().setAlignCanvasToScales(True)
        fig.canvas().setLineWidth(1)
        fig.canvas().setFrameStyle(QFrame.Box|QFrame.Plain)

        fig.setAxisScale(fig.xBottom, 0, 10)
        fig.setAxisScale(fig.yLeft, 0, 10)

        part = QwtAbstractScaleDraw.Backbone
        for axis in [fig.xBottom, fig.yLeft]:
            fig.axisWidget(axis).setMargin(0)
            fig.axisScaleDraw(axis).enableComponent(part, False)

        fig.setTitle("f(x) = sin(x) + 0.1x<sup>2<\sup>")
        fig.setAxisTitle(fig.xBottom, "x")
        fig.setAxisTitle(fig.yLeft, "f(x)")
        
        x = np.arange(0, 10, 0.01)
        y = np.sin(x) + 0.1*x**2

        curve = QwtPlotCurve()
        curve.setData(x, y)
        curve.attach(fig)
        fig.replot()

        fig.resize(400, 300)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    frame = Ex02()
    frame.show()
    app.exec_()

(發佈日期:2009/10/20)

沒有留言:

張貼留言