原本我不打算放一個過於複雜的範例,畢竟講解起來可能需要好幾篇幅。但既然繪圖相關的基礎已經介紹完畢,還是決定實作一個簡單的畫板程式來串連前面的知識點。這個畫板支援繪製直線與矩形,我會分兩種方式實現:第一種使用普通的 QWidget 作為畫布,第二種則採用 Graphics View Framework。之所以安排這兩種實作,是因為讀者反映對 Graphics View 的相關概念不太理解,藉此機會加以說明。
我們的專案架構仍使用 Qt Creator 建立的 Gui Application 專案。main() 函式就不再贅述,直接來看 MainWindow 的實作。順帶一提,我習慣不使用 .ui 檔案,所有介面程式碼皆為手寫。先看最終執行結果:
雖然功能簡單,但足以將前面所學的繪圖、事件處理、信號槽等知識串聯起來。
MainWindow 負責建立工具列、動作群組,並透過信號通知畫布切換繪圖模式:
// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QtGui>
#include "shape.h"
#include "paintwidget.h"
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
signals:
void changeCurrentShape(Shape::Code newShape);
private slots:
void drawLineActionTriggered();
void drawRectActionTriggered();
};
#endif // MAINWINDOW_H
// mainwindow.cpp
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
QToolBar *bar = this->addToolBar("Tools");
QActionGroup *group = new QActionGroup(bar);
// 直線動作
QAction *drawLineAction = new QAction("Line", bar);
drawLineAction->setIcon(QIcon(":/line.png"));
drawLineAction->setToolTip(tr("Draw a line."));
drawLineAction->setStatusTip(tr("Draw a line."));
drawLineAction->setCheckable(true);
drawLineAction->setChecked(true);
group->addAction(drawLineAction);
bar->addAction(drawLineAction);
// 矩形動作
QAction *drawRectAction = new QAction("Rectangle", bar);
drawRectAction->setIcon(QIcon(":/rect.png"));
drawRectAction->setToolTip(tr("Draw a rectangle."));
drawRectAction->setStatusTip(tr("Draw a rectangle."));
drawRectAction->setCheckable(true);
group->addAction(drawRectAction);
bar->addAction(drawRectAction);
// 狀態列訊息標籤
QLabel *statusMsg = new QLabel;
statusBar()->addWidget(statusMsg);
// 設定中心元件為繪圖元件
PaintWidget *paintWidget = new PaintWidget(this);
setCentralWidget(paintWidget);
// 信號槽連接
connect(drawLineAction, SIGNAL(triggered()),
this, SLOT(drawLineActionTriggered()));
connect(drawRectAction, SIGNAL(triggered()),
this, SLOT(drawRectActionTriggered()));
connect(this, SIGNAL(changeCurrentShape(Shape::Code)),
paintWidget, SLOT(setCurrentShape(Shape::Code)));
}
void MainWindow::drawLineActionTriggered()
{
emit changeCurrentShape(Shape::Line);
}
void MainWindow::drawRectActionTriggered()
{
emit changeCurrentShape(Shape::Rect);
}
程式碼邏輯清晰:QActionGroup 確保同一時間只能選擇一種繪圖模式;triggered() 信號無參數,因此在槽函式中重新發射帶參數的 changeCurrentShape 信號;中心元件設為自訂的 PaintWidget。
PaintWidget 繼承自 QWidget,處理滑鼠事件並管理圖形列表:
// paintwidget.h
#ifndef PAINTWIDGET_H
#define PAINTWIDGET_H
#include <QtGui>
#include <QDebug>
#include "shape.h"
#include "line.h"
#include "rect.h"
class PaintWidget : public QWidget
{
Q_OBJECT
public:
PaintWidget(QWidget *parent = 0);
public slots:
void setCurrentShape(Shape::Code s)
{
if(s != currShapeCode) {
currShapeCode = s;
}
}
protected:
void paintEvent(QPaintEvent *event);
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
private:
Shape::Code currShapeCode;
Shape *shape;
bool perm;
QList<Shape*> shapeList;
};
#endif // PAINTWIDGET_H
// paintwidget.cpp
#include "paintwidget.h"
PaintWidget::PaintWidget(QWidget *parent)
: QWidget(parent), currShapeCode(Shape::Line), shape(NULL), perm(false)
{
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
}
void PaintWidget::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.setBrush(Qt::white);
painter.drawRect(0, 0, size().width(), size().height());
foreach(Shape * shape, shapeList) {
shape->paint(painter);
}
if(shape) {
shape->paint(painter);
}
}
void PaintWidget::mousePressEvent(QMouseEvent *event)
{
switch(currShapeCode)
{
case Shape::Line:
shape = new Line;
break;
case Shape::Rect:
shape = new Rect;
break;
}
if(shape != NULL) {
perm = false;
shapeList << shape;
shape->setStart(event->pos());
shape->setEnd(event->pos());
}
}
void PaintWidget::mouseMoveEvent(QMouseEvent *event)
{
if(shape && !perm) {
shape->setEnd(event->pos());
update();
}
}
void PaintWidget::mouseReleaseEvent(QMouseEvent *event)
{
perm = true;
}
核心邏輯在於三個滑鼠事件的配合:
shapeList 保存歷史圖形。update() 觸發重繪,形成「橡皮筋」效果。perm = true 標記繪製完成,後續移動不再更新該圖形。paintEvent 中先繪製白色背景,再遍歷 shapeList 繪製所有歷史圖形,最後繪製正在編輯的圖形(若存在)。
採用繼承架構,Shape 為抽象基類,定義通用介面與資料成員:
// shape.h
#ifndef SHAPE_H
#define SHAPE_H
#include <QtGui>
class Shape
{
public:
enum Code { Line, Rect };
Shape();
void setStart(QPoint s) { start = s; }
void setEnd(QPoint e) { end = e; }
QPoint startPoint() { return start; }
QPoint endPoint() { return end; }
virtual void paint(QPainter & painter) = 0;
protected:
QPoint start;
QPoint end;
};
#endif // SHAPE_H
兩個具體子類別分別實作 paint():
// line.h / line.cpp
class Line : public Shape {
public:
Line();
void paint(QPainter &painter) { painter.drawLine(start, end); }
};
// rect.h / rect.cpp
class Rect : public Shape {
public:
Rect();
void paint(QPainter &painter) {
painter.drawRect(start.x(), start.y(),
end.x() - start.x(), end.y() - start.y());
}
};
設計亮點:直線由兩點決定,矩形以兩點為對角頂點亦可確定,故基類僅需儲存 start 與 end 兩點。paint() 為純虛函式,強制子類實作繪製邏輯。若未來需擴充顏色、線寬等屬性,只需在 Shape 中新增即可,體現物件導向設計的優勢。
完整專案原始碼可參考原文附件。這個範例將繪圖、事件、信號槽、類別繼承等核心概念完整串聯,是 Qt 入門的絕佳練習。