Qt入門(19)——自定義視窗部件

尹成發表於2014-09-30
我們介紹可以畫自己的第一個自定義視窗部件。我們也加入了一個有用的鍵盤介面。


我們新增了一個槽:setRange()。

        void setRange( int minVal, int maxVal );
現在我們新增了設定LCDRange範圍的可能性。直到現在,它就可以被設定為0~99。


在建構函式中有一個變化。

    void LCDRange::setRange( int minVal, int maxVal )
    {
        if ( minVal < 0 || maxVal > 99 || minVal > maxVal ) {
          qWarning( "LCDRange::setRange(%d,%d)\n"
                   "\tRange must be 0..99\n"
                   "\tand minVal must not be greater than maxVal",
                   minVal, maxVal );
          return;
        }
        slider->setRange( minVal, maxVal );
    }



setRange()設定了LCDRange中滑塊的範圍。因為我們已經把QLCDNumber設定為只顯示兩位數字了,我們想通過限制minVal和maxVal為0~99來避免QLCDNumber的溢位。(我們可以允許最小值為-9,但是我們沒有那樣做。)如果引數是非法的,我們使用Qt的qWarning()函式來向使用者發出警告並立即返回。qWarning()是一個像printf一樣的函式,預設情況下它的輸出傳送到stderr。如果你想改變的話,你可以使用::qInstallMsgHandler()函式安裝自己的處理函式。


CanonField是一個知道如何顯示自己的新的自定義視窗部件。


    class CannonField : public QWidget
    {
        Q_OBJECT
    public:
        CannonField( QWidget *parent=0, const char *name=0 );
CanonField繼承了QWidget,我們使用了LCDRange中同樣的方式。


        int angle() const { return ang; }
        QSizePolicy sizePolicy() const;


    public slots:
        void setAngle( int degrees );


    signals:
        void angleChanged( int );


目前,CanonField只包含一個角度值,我們使用了LCDRange中同樣的方式。
    protected:
        void paintEvent( QPaintEvent * );




這是我們在QWidget中遇到的許多事件處理器中的第二個。只要一個視窗部件需要重新整理它自己(比如,畫視窗部件表面),這個虛擬函式就會被Qt呼叫。

    CannonField::CannonField( QWidget *parent, const char *name )
            : QWidget( parent, name )
    {



我們又一次使用和前一章中的LCDRange同樣的方式。
        ang = 45;
        setPalette( QPalette( QColor( 250, 250, 200) ) );
    }




建構函式把角度值初始化為45度並且給這個視窗部件設定了一個自定義調色盤。


這個調色盤只是說明背景色,並選擇了其它合適的顏色。(對於這個視窗部件,只有背景色和文字顏色是要用到的。)

    void CannonField::setAngle( int degrees )
    {
        if ( degrees < 5 )
            degrees = 5;
        if ( degrees > 70 )
            degrees = 70;
        if ( ang == degrees )
            return;
        ang = degrees;
        repaint();
        emit angleChanged( ang );
    }



這個函式設定角度值。我們選擇了一個5~70的合法範圍,並根據這個範圍來調節給定的degrees的值。當新的角度值超過了範圍,我們選擇了不使用警告。


如果新的角度值和舊的一樣,我們立即返回。這隻對當角度值真的發生變化時,發射angleChanged()訊號有重要意義。


然後我們設定新的角度值並重新畫我們的視窗部件。QWidget::repaint()函式清空視窗部件(通常用背景色來充滿)並向視窗部件發出一個繪畫事件。這樣的結構就是呼叫視窗部件的繪畫事件函式一次。


最後,我們發射angleChanged()訊號來告訴外面的世界,角度值發生了變化。emit關鍵字只是Qt中的關鍵字,而不是標準C++的語法。實際上,它只是一個巨集。
    void CannonField::paintEvent( QPaintEvent * )
    {
        QString s = "Angle = " + QString::number( ang );
        QPainter p( this );
        p.drawText( 200, 200, s );
    }




這是我們第一次試圖寫一個繪畫事件處理程式。這個事件引數包含一個繪畫事件的描述。QPaintEvent包含一個必須被重新整理的視窗部件的區域。現在,我們比較懶惰,並且只是畫每一件事。


我們的程式碼在一個固定位置顯示視窗部件的角度值。首先我們建立一個含有一些文字和角度值的QString,然後我們建立一個操作這個視窗部件的QPainter並使用它來畫這個字串。我們一會兒會回到QPainter,它可以做很多事。


    #include "cannon.h"我們包含了我們的新類:
    class MyWidget: public QWidget
    {
    public:
        MyWidget( QWidget *parent=0, const char *name=0 );
    };




這一次我們在頂層視窗部件中只使用了一個LCDRange和一個CanonField。


        LCDRange *angle = new LCDRange( this, "angle" );
在建構函式中,我們建立並設定了我們的LCDRange。


        angle->setRange( 5, 70 );
我們設定LCDRange能夠接受的範圍是5~70度。

        CannonField *cannonField
            = new CannonField( this, "cannonField" );



我們建立了我們的CannonField。

        connect( angle, SIGNAL(valueChanged(int)),
                 cannonField, SLOT(setAngle(int)) );
        connect( cannonField, SIGNAL(angleChanged(int)),
                 angle, SLOT(setValue(int)) );



這裡我們把LCDRange的valueChanged()訊號和CannonField的setAngle()槽連線起來了。只要使用者操作LCDRange,就會重新整理CannonField的角度值。我們也把它反過來連線了,這樣CannonField中角度的變化就可以重新整理LCDRange的值。在我們的例子中,我們從來沒有直接改變CannonField的角度,但是通過我們的最後一個connect()我們就可以確保沒有任何變化可以改變這兩個值之間的同步關係。


這說明了元件程式設計和正確封裝的能力。


注意只有當角度確實發生變化時,才發射angleChanged()是多麼的重要。如果LCDRange和CanonField都省略了這個檢查,這個程式就會因為第一次數值變化而進入到一個無限迴圈當中。


        QGridLayout *grid = new QGridLayout( this, 2, 2, 10 );
        //2×2,10畫素的邊界
到現在為止,我們沒有因為幾何管理把QVBox和QGrid視窗部件整合到一起。現在,無論如何,我們需要對我們的佈局加一些控制,所以我們使用了更加強大的QGridLayout類。QGridLayout不是一個視窗部件,它是一個可以管理任何視窗部件作為子物件的不同的類。


就像註釋中所說的,我們建立了一個以10畫素為邊界的2*2的陣列。(QGridLayout的建構函式有一點神祕,所以最好在這裡加入一些註釋。)


        grid->addWidget( quit, 0, 0 );
我們在網格的左上的單元格中加入一個Quit按鈕:0,0。


        grid->addWidget( angle, 1, 0, Qt::AlignTop );
我們把angle這個LCDRange放到左下的單元格,在單元格內向上對齊。(這只是QGridLayout所允許的一種對齊方式,而QGrid不允許。)


        grid->addWidget( cannonField, 1, 1 );
我們把CannonField物件放到右下的單元格。(右上的單元格是空的。)


        grid->setColStretch( 1, 10 );
我們告訴QGridLayout右邊的列(列1)是可拉伸的。因為左邊的列不是(它的拉伸因數是0,這是預設值),QGridLayout就會在MyWidget被重新定義大小的時候試圖讓左面的視窗部件大小不變,而重新定義CannonField的大小。


        angle->setValue( 60 );
我們設定了一個初始角度值。注意這將會引發從LCDRange到CannonField的連線。


        angle->setFocus();
我們剛才做的是設定angle獲得鍵盤焦點,這樣預設情況下鍵盤輸入會到達LCDRange視窗部件。


LCDRange沒有包含任何keyPressEvent(),所以這看起來不太可能有用。無論如何,它的建構函式中有了新的一行:


        setFocusProxy( slider );
LCDRange設定滑塊作為它的焦點代理。這就是說當程式或者使用者想要給LCDRange一個鍵盤焦點,滑塊就會就會注意到它。QSlider有一個相當好的鍵盤介面,所以就會出現我們給LCDRange新增的這一行。


鍵盤現在可以做一些事了——方向鍵、Home、End、PageUp和PageDown都可以作一些事情。
當滑塊被操作,CannonFiled會顯示新的角度值。如果重新定義大小,CannonField會得到儘可能多的空間。

相關文章