Qt/C++入門基礎學習001-繪圖基礎

這一節介紹 Qt 的繪圖基礎知識,我們都知道,Qt 裏繪圖使用的是 QPainter,但是首先需要弄明白:在什麼上繪圖和在哪裏繪圖,然後纔是怎麼繪圖,我們就圍繞這幾個問題來展開。

在什麼上繪圖

The QPaintDevice class is the base class of objects that can be painted on with QPainter.

A paint device is an abstraction of a two-dimensional space that can be drawn on using a QPainter. Its default coordinate system has its origin located at the top-left position. X increases to the right and Y increases downwards. The unit is one pixel.

The drawing capabilities of QPaintDevice are currently implemented by the QWidget, QImage, QPixmap, QGLPixelBuffer, QPicture, and QPrinter subclasses.

上面的內容來自於 Qt 的幫助文檔,在 QPaintDevice 的子類裏用 QPainter 繪圖,最常見的就是在 QWidget, QPixmap, QPixture, QPrinter 上面繪圖。

在哪裏繪圖

知道了可以在哪些類中繪圖了,總不能在這些類的子類中隨便寫個函數就可以繪圖了吧!這需要分情況,例如在 MainWidget 的構造函數裏創建一個 QPixmap,並在它上面畫圖,然後設置爲 QLabel 的 pixmap:

MainWidget::MainWidget(QWidget *parent)
    : QWidget(parent), ui(new Ui::MainWidget) {
    ui->setupUi(this);

    // 創建 pixmap
    pixmap = QPixmap(100, 100);
    pixmap.fill(Qt::gray);
    QPainter painter(&pixmap);
    painter.drawRect(10, 10, 80, 80);
    painter.drawText(20, 30, "Hello World");

    // 使用 pixmap
    ui->label->setPixmap(pixmap);
}

在非 widget 上繪圖,如上面的 QPixmap,在什麼地方都可以,但是在 QWidget 及其子類裏繪圖卻沒有這麼自由,通常都是要在哪個 widget 上繪圖,就需要在它的 paintEvent() 函數裏繪圖,即重寫 paintEvent() 函數。

例如類 PandoraWidget 是 QWidget 的子類,要在它上面畫一個矩形,它的 paintEvent() 函數如下:

void PandoraWidget::paintEvent(QPaintEvent *) {
    QPainter painter(this); // this 是 PandoraWidget 的指針
    painter.setPen(Qt::gray);
    painter.setBrush(Qt::green);
    painter.drawRect(10, 10, 50, 50);
}

考慮一個問題,PandoraWidget 上有一個叫 magicLabel 的 QLabel,打算在 magicLabel 上畫一個矩形,在 PandoraWidget 的 paintEvent() 函數像下面這樣用 magicLabel 構造 QPainter,然後繪圖可以嗎?

void PandoraWidget::paintEvent(QPaintEvent *) {
    QPainter painter(ui->magicLabel); // 注意這裏
    painter.setPen(Qt::gray);
    painter.setBrush(Qt::green);
    painter.drawRect(10, 10, 50, 50);
}

運行程序,結果並沒有在 magicLabel 上繪製出矩形,而且還輸出了下面的錯誤:

QWidget::paintEngine: Should no longer be called QPainter::begin: Paint device returned engine == 0, type: 1 QPainter::setPen: Painter not active QPainter::setBrush: Painter not active QPainter::drawRects: Painter not active

總是提示 Painter not active,上面提到過:想要在哪個 widget 上繪圖,就需要在它的 paintEvent() 函數裏繪圖,這裏的 paintEvent() 函數是 PandoraWidget 的,所以繪圖到 PandoraWidget 上成功了,但 paintEvent() 函數不是 QLabel 的,所以企圖繪圖到 magicLabel 上沒成功。

那是不是就是說,如果想在 magicLabel 上繪圖,就必須新創建一個類例如叫 MagicLabel,並且繼承自 QLabel,然後在它的 paintEvent() 裏繪圖?如果有 10 個子 widget,都想在上面畫點啥,是不是每個 widget 都要對應創建一個類來實現繪圖?我就是想畫個圈而已,要創建這麼多類也太麻煩了,真的想畫個圈圈詛咒 Qt 啊。

莫急莫急,這裏傳大家本人祕藏多年的一絕技,就能在 PandoraWidget 的函數裏給 magicLabel 繪圖了:在事件過濾器 eventFilter() 中攔截 magicLabel 的 QEvent::Paint 事件,用 magicLabel 創建 QPainter,就可以在 magicLabel 上繪圖了,上代碼,否則估計有人要把我畫在圈圈裏了:

PandoraWidget::PandoraWidget(QWidget *parent)
    : QWidget(parent), ui(new Ui::PandoraWidget) {
    ui->setupUi(this);
    ui->magicLabel->installEventFilter(this);
}

bool PandoraWidget::eventFilter(QObject *watched, QEvent *event) {
    if (watched == ui->magicLabel && event->type() == QEvent::Paint) {
        magicTime();
    }

    return QWidget::eventFilter(watched, event);
}

void PandoraWidget::magicTime() {
    QPainter painter(ui->magicLabel);
    painter.setPen(Qt::gray);
    painter.setBrush(Qt::green);
    painter.drawRect(10, 10, 50, 50);
}

怎麼繪圖

下圖來自《C++ GUI Programming with Qt 4》,列出了 QPainter 常用的畫圖方法,都是以 draw 開頭,非常直觀的列出了繪圖函數和繪製出來的圖形:

img

下面具體的介紹這些函數的使用,它們中很多都有重載的函數,這裏只使用其中的一種,其它的用法都差不多,就不一一介紹,需要時查看幫助文檔就可以了。

座標系

數學中使用的座標系是笛卡爾座標系,X 軸正向向右,Y 軸正向向上。但是,QPainter 也有自己的座標系,和笛卡爾座標系有點不一樣,原點在 widget 的左上角而不是正中心,X 軸正向向右,Y 軸正向向下。注意: 每個 widget 都有自己獨立的座標系。

img

畫線 - drawLine()

給定 2 個點,使用 drawLine() 畫一條線。

void MainWidget::paintEvent(QPaintEvent *) {
    QPainter painter(this);
    painter.drawLine(30, 30, 150, 150);
}

drawLine() 有什麼用?例如可以用來畫網格線: img

void GridWidget::paintEvent(QPaintEvent *) {
    QPainter painter(this);
    painter.translate(30, 30);

    int w   = 300;
    int h   = 210;
    int gap = 30;

    // 畫水平線
    for (int y = 0; y <= h; y += gap) {
        painter.drawLine(0, y, w, y);
    }

    // 畫垂直線
    for (int x = 0; x <= w; x += gap) {
        painter.drawLine(x, 0, x, h);
    }
}

畫多線段 - drawLines()

給定 N 個點,第 1 和第 2 個點連成線,第 3 和第 4 個點連成線,……,N 個點練成 (N+1)/2 條線,如果 N 是奇數,第 N 個點和 (0,0) 連成線。

void MultipleLinesWidget::paintEvent(QPaintEvent *) {
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    painter.translate(20, 20);

    static const QPointF points[4] = {
        QPointF(0.0, 100.0),
        QPointF(20.0, 0.0),
        QPointF(100.0, 0.0),
        QPointF(120.0, 100.0)
    };

    painter.drawLines(points, 2); // 4 個點連成 2 條線
}

畫折線- drawPolyline()

給定 N 個點,第 1 和第 2 個點連成線,第 2 和第 3 個點連成線,……,第 N-1 和第 N 個點連成線,N 個點共連成 N-1 條線。

void PolylineWidget::paintEvent(QPaintEvent *) {
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    painter.translate(20, 20);

    static const QPointF points[4] = {
        QPointF(0.0, 100.0),
        QPointF(20.0, 0.0),
        QPointF(100.0, 0.0),
        QPointF(120.0, 100.0)
    };

    painter.drawPolyline(points, 4);
}

畫多邊形 - drawPolygon()

給定 N 個點,第 1 和第 2 個點連成線,第 2 和第 3 個點連成線,……,第 N-1 和第 N 個點連成線,第 N 個點和第 1 個點連接成線形成一個封閉的多邊形。

drawPolygon() 和 drawPolyline() 很像,但是 drawPolygon() 畫的是一個封閉的區域,可以填充顏色,而 drawPolyline() 畫的是一些線段,即使它們連成一個封閉的區域也不能填充顏色。

void PolygonWidget::paintEvent(QPaintEvent *) {
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    painter.translate(20, 20);

    static const QPointF points[4] = {
        QPointF(0.0, 100.0),
        QPointF(20.0, 0.0),
        QPointF(100.0, 0.0),
        QPointF(120.0, 100.0)
    };

    painter.setBrush(Qt::yellow);
    painter.drawPolygon(points, 4);
}

可以用 drawPolygon() 來畫圓,其實本沒有圓,正多邊形的邊多了,便成了圓,這正是計算機裏繪製曲線的原理,插值逼近,在曲線上取 N 個點,點之間用線段連接起來,當 N 越大時,連接出來的圖形就越平滑,越接近曲線。

img

void PolygonCircleWidget::paintEvent(QPaintEvent *) {
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing); // 啓用抗鋸齒效果
    painter.translate(width() / 2, height() / 2); // 把座標原點移動到 widget 的中心
    painter.setBrush(Qt::lightGray);

    const int COUNT  = 10;  // 邊數,越大就越像圓
    const int RADIUS = 100; // 圓的半徑
    QPointF points[COUNT];  // 頂點數組

    for (int i = 0; i < COUNT; ++i) {
        float radians = 2 * M_PI / COUNT * i; // M_PI 是 QtMath 裏定義的,就是 PI
        float x = RADIUS * qCos(radians);
        float y = RADIUS * qSin(radians);
        points[i] = QPointF(x, y);
    }

    painter.drawPolygon(points, COUNT);
}

爲了介紹方便,數組 points 是在 paintEvent() 裏創建的,每次調用 paintEvent() 時都會重新生成一次 points,實際項目裏可不能這麼做,因爲 paintEvent() 會被多次的調用,每次調用都會生成 points。數組 points 只有在必要的時候才重新生成,否則就是浪費計算資源,所以可以放到構造函數裏,或者點擊按鈕改變 COUNT 的值後在對應的槽函數裏重新生成 points,然後調用 update() 函數刷新界面。

畫矩形 - drawRect()

給定矩形左上角的座標和矩形的長、寬就可以繪製矩形了。

void MainWidget::paintEvent(QPaintEvent *) {
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    painter.translate(30, 30);

    int x = 0;
    int y = 0;
    int w = 100;
    int h = 100;

    painter.drawRect(x, y, w, h);
}

畫圓角矩形- drawRoundRect() & drawRoundedRect()

繪製圓角矩形有 2 個方法:drawRoundRect()drawRoundedRect(),需要給定圓角矩形左上角的座標、長、寬、圓角的半徑。

當 drawRoundedRect() 中第 7 個參數 Qt::SizeMode 爲 Qt::RelativeSize 時,表示圓角半徑的單位是百分比,取值範圍是 [0, 100],此時 drawRoundedRect() 等價於 drawRoundRect(),其實底層是用這個百分比和對應邊長的一半相乘得到圓角的半徑(單位是像素)。Qt::SizeMode 爲 Qt::AbsoluteSize 時,表示圓角半徑的單位是像素。

有意思的是,在 QSS 中圓角半徑大於對應邊長的一半,圓角效果就沒了,但是使用 drawRoundedRect() 時,圓角的半徑大於對應邊長的一半時,圓角效果仍然有效,個人認爲這個是 QSS 的 bug,但是已經存在很久了。

下面使用不同的參數繪製了 3 個圓角矩形,便於比較他們之間的異同: img

void RoundRectWidget::paintEvent(QPaintEvent *) {
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    painter.setBrush(Qt::lightGray);
    painter.translate(30, 30);

    painter.drawRoundRect(0, 0, 100, 100, 50, 50); // 50%, 50%
    painter.drawRoundedRect(130, 0, 100, 100, 50, 50, Qt::AbsoluteSize); // 50px, 50px
    painter.drawRoundedRect(260, 0, 100, 100, 100, 100, Qt::RelativeSize); // 100%, 100%
}

畫圓、橢圓 - drawEllipse()

給定橢圓的包圍矩形(bounding rectangle),使用 drawEllipse() 繪製橢圓。圓是特殊的橢圓,橢圓有兩個焦點,這兩個焦點合爲一個的時候就是一個正圓了,當包圍矩形是正方形時,drawEllipse() 繪製的就是圓。

當然,畫圓的方法很多,上面我們就使用了 drawPolygon(),drawRounedRect() 的方法畫圓,不過從語義上來說,用 drawEllipse() 來畫圓顯得更適合一些。

void EllipseWidget::paintEvent(QPaintEvent *) {
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    painter.translate(30, 30);

    painter.drawRect(0, 0, 200, 100);    // 橢圓的包圍矩形
    painter.setBrush(Qt::lightGray);
    painter.drawEllipse(0, 0, 200, 100); // 橢圓

    painter.drawEllipse(230, 0, 100, 100); // 圓
}

畫弧、弦、餅圖 - drawArc()、drawChord()、drawPie()

畫弧使用 drawArc() 畫弦使用 drawChord() 畫餅圖用 drawPie()

把這三個函數放在一起介紹,因爲它們的參數都一樣,而且 arc, chord, pie 外形也有很多相似之處:

void QPainter::drawArc(const QRectF & rectangle, int startAngle, int spanAngle)
void QPainter::drawPie(const QRectF & rectangle, int startAngle, int spanAngle)
void QPainter::drawChord(const QRectF & rectangle, int startAngle, int spanAngle)
  • rectangle: 包圍矩形
  • startAngle: 開始的角度,單位是十六分之一度,如果要從 45 度開始畫,則 startAngle 爲 45 * 16
  • spanAngle: 覆蓋的角度,單位是十六分之一度
  • 繪製圓心爲包圍矩形的正中心,0 度在圓心的 X 軸正方向上
  • 角度的正方向爲逆時針方向

下面程序的結果如圖: img

void ArcChordPieWidget::paintEvent(QPaintEvent *) {
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    static int startAngle = 45 * 16; // 開始角度是 45 度
    static int spanAngle = 130 * 16; // 覆蓋角度爲 130 度
    static QRectF boundingRect(0, 0, 150, 150); // 包圍矩形

    painter.translate(30, 30);
    painter.setBrush(Qt::NoBrush);
    painter.drawRect(boundingRect); // 繪製包圍矩形
    painter.setBrush(Qt::lightGray);
    painter.drawArc(boundingRect, startAngle, spanAngle); // 畫弧

    painter.translate(180, 0);
    painter.setBrush(Qt::NoBrush);
    painter.drawRect(boundingRect); // 繪製包圍矩形
    painter.setBrush(Qt::lightGray);
    painter.drawChord(boundingRect, startAngle, spanAngle); // 畫弦

    painter.translate(180, 0);
    painter.setBrush(Qt::NoBrush);
    painter.drawRect(boundingRect); // 繪製包圍矩形
    painter.setBrush(Qt::lightGray);
    painter.drawPie(boundingRect, startAngle, spanAngle); // 畫餅圖
}

修改 startAngle 和 spanAngle 爲負值看看是什麼效果。

繪製 QPixmap - drawPixmap()

void MainWidget::paintEvent(QPaintEvent *) {
    QPainter painter(this);
    QPixmap pixmap(":/img/Butterfly.png"); // 從資源文件讀取 pixmap

    painter.drawPixmap(0, 0, pixmap); // 繪製 pixmap
}

繪製 QImage - drawImage()

void MainWidget::paintEvent(QPaintEvent *) {
    QPainter painter(this);
    QImage image(":/img/Butterfly.png"); // 從資源文件讀取 image

    painter.drawImage(0, 0, image); // 繪製 image
}
  1. 國內開源:https://gitee.com/feiyangqingyun
  2. 國際開源:https://github.com/feiyangqingyun
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章