Qt Quick实现的涂鸦程序
? ? 之前一直以為 Qt Quick 里 Canvas 才可以自繪,后來發覺不是,原來還有好幾種方式都可以繪圖!可以使用原始的 OpenGL(Qt Quick 使用 OpenGL 渲染),可以構造QSGNode 來繪圖,還可以使用 QPainter !哇, QPainter 我很熟悉啊。于是,我用 QPainter 結合 QML 實現了一個簡單的涂鴉程序: PaintedItem 。它有下列功能:
- 設置線條寬度
- 設置線條顏色
- 設置背景顏色
- 清除涂鴉
- 無限級undo
? ? 程序很簡陋,效果如下:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖1 PaintedItem效果圖
? ? 程序雖然簡單,但也還是有一些新內容之前沒有提到:
- QQuickPaintedItem
- C++實現QML可視圖元(Item)
- 自定義圖元如何處理鼠標事件
? ? 下面咱們一個一個來說一下。
? ? 版權所有 foruok ,轉載請注明出處:http://blog.csdn.net/foruok 。
QQuickPaintedItem
? ? Qt Quick 的核心是 Scene Graph ,可以在 Qt 幫助的索引模式下以 “Scene Graph” 為關鍵字來檢索學習。 Scene Graph 的設計思想和 QGraphicsView/QGraphicsScene 框架類似,一個場景,很多圖元往場景里放。不同之處是 Item 的繪制, QGraphicsView 框架里是通過 View 的繪圖事件來驅動 Item 的繪制,QGraphicsItem 有一個 paint() 虛函數,只要你從 QGraphicsItem 繼承來的 Item 實現這個 paint() 函數,就可以往 QPaintDevice 上繪制了,邏輯直接;而 Qt Quick 的繪制,其實另有一個渲染線程, Scene 里的 Item 沒有 paint() 這種直觀的繪圖函數,只有一個 updatePaintNode() 方法讓你來構造你的 Item 的幾何表示,當程序輪轉到渲染循環時,渲染循環把所有 Item 的 QSGNode 樹取出來繪制。
? ? updatePaintNode() 這種繪制的方式很不直觀,它來自 OpenGL 或者 Direct 3D 的繪圖模式:你構造圖元的幾何表示,別人會在某一個時刻根據你提供的材料幫你繪制,就像你扔一袋垃圾到門口,過一陣子有人會來幫你收走這種感覺。用慣 Qt Widgets 和 QPainter 的開發者可能會不適應這種方式,所以 Qt Quick 提供了一種兼容老習慣的方式:引入 QQuickPaintedItem ,使用 QPainter 繪制。
? ? 一般地,你可以這樣理解: QQuickPaintedItem 使用 Qt Widgets 里慣常的 2D 繪圖方式,將你想要的線條、圖片、文字等繪制到一個內存中的 QImage 上,然后把這個 QImage 作為一個 QSGNode 放在那里等著 Qt Quick 的渲染線程來取走它,把它繪制到實際的場景中。按照這種理解, QQuickPaintedItem 會多個繪圖步驟,有性能上的損失!不過為了開發方便,有時候這一點點性能損失是可以承受的——只要你的應用仍然可以流暢運行。
? ? QQuickPaintedItem 是一切想使用 QPainter 來繪圖的 Qt Quick Item 的基類,它有一個純虛函數——?paint(QPainter * painter) ?,你自定義的 Item 只要實現 paint() 虛函數就可以了。
? ? QQuickPaintedItem 是 QQuickItem 的派生類, QQuickItem 的 boundingRect() 方法返回一個 Item 的矩形,你可以根據它來繪制你的 Item 。fillColor() 返回 Item 的填充顏色(默認是透明的), Qt Quick 會使用這個顏色在 paint() 方法調用前繪制你的 Item 的背景。 setFillColor() ?可以改變填充顏色。
? ? Qt Quick 提供了一個“Scene Graph - Painted Item”示例來演示 QQuickPaintedItem 的用法,你可以參考。
C++實現QML可視圖元
? ? Qt Quick 提供的相當一部分圖形元素都是在 C++ 中實現后導出到 QML 環境中的,比如 Text 。那我們也可以這么做,只要你從 QQuickItem(對應 QML 中的 Item 元素) 繼承來實現你的 C++ 類即可。
? ? 我們的示例要使用 QPainter 繪圖,所以從 QQuickPaintedItem 繼承,重寫 paint() 方法。
? ? 完成了 C++ 類,導出到 QML 環境中,就可以像使用 QML 內建元素一樣來使用我們導出的類。如何導出又如何在 QML 中使用,請參看《Qt Quick 之 QML 與 C++ 混合編程詳解》。
自定義圖元如何處理鼠標事件
? ? 在 QML 中我們一直使用 MouseArea 來處理鼠標事件。 MouseArea 對應 C++ 中的 QQuickMouseArea 類,其實也是 QQuickItem 的派生類。 其實 QQuickItem 定義了一系列處理鼠標事件的虛函數,比如 mousePressEvent 、 mouseMoveEvent 、 mouseMoveEvent 等,它本身就可以處理鼠標事件,只不過 QQuickItem 沒有導出這些函數,我們在 QML 中無法使用。而之所以引入 QQuickMouseArea (QML 中的 MouseArea ),是為了方便鼠標事件的處理,你不需要為每個 Item 像 QWidget 那樣來重寫很多方法,那樣真的很煩的, QML 的這種方式雖然多用了一個對象,但是更方便一些??墒俏覀兊?PaintedItem 類,如果繞回到 QML 中使用 MouseArea 來處理鼠標事件,那我們跟蹤鼠標軌跡來繪制線條時,就需要不斷地將鼠標事件中攜帶的像素點信息再回傳到 C++ 中來,非常麻煩,性能也不好,所以我們直接重寫 QQuickItem 的相關虛函數來處理鼠標事件。
? ? 我們知道 MouseArea 有一個 acceptedButtons 屬性,可以設置 Item 處理哪個鼠標按鍵,而實際上,“要處理的鼠標按鍵”這個信息,是保存在 QQuickItem 中的,通過?setAcceptedMouseButtons() 方法來設置。默認情況下, QQuickItem 不處理任何鼠標按鍵,所以我們要處理鼠標按鍵,必須在我們的 PaintedItem 中來設置一下,就像 MouseArea 那樣。我們的示例中,在 PaintedItem 的構造函數中做了這件事:
PaintedItem::PaintedItem(QQuickItem *parent): QQuickPaintedItem(parent), m_element(0), m_bEnabled(true), m_bPressed(false), m_bMoved(false), m_pen(Qt::black) {setAcceptedMouseButtons(Qt::LeftButton); }? ? 如代碼所示,我們只處理鼠標左鍵。如果你不設置這個,你收不到任何鼠標事件。
PaintedItem 源碼分析
? ? 因為我們實現的功能簡單,源碼也不復雜。
自定義 Item
? ? 先看 PaintedItem.h :
#ifndef PAINTEDITEM_H #define PAINTEDITEM_H #include <QQuickPaintedItem> #include <QVector> #include <QPointF> #include <QLineF> #include <QPen>class ElementGroup { public:ElementGroup(){}ElementGroup(const QPen &pen): m_pen(pen){}ElementGroup(const ElementGroup &e){m_lines = e.m_lines;m_pen = e.m_pen;}ElementGroup & operator=(const ElementGroup &e){if(this != &e){m_lines = e.m_lines;m_pen = e.m_pen;}return *this;}~ElementGroup(){}QVector<QLineF> m_lines;QPen m_pen; };class PaintedItem : public QQuickPaintedItem {Q_OBJECTQ_PROPERTY(bool enabled READ isEnabled WRITE setEnabled)Q_PROPERTY(int penWidth READ penWidth WRITE setPenWidth)Q_PROPERTY(QColor penColor READ penColor WRITE setPenColor)public:PaintedItem(QQuickItem *parent = 0);~PaintedItem();bool isEnabled() const{ return m_bEnabled; }void setEnabled(bool enabled){ m_bEnabled = enabled; }int penWidth() const { return m_pen.width(); }void setPenWidth(int width) { m_pen.setWidth(width); }QColor penColor() const { return m_pen.color(); }void setPenColor(QColor color) { m_pen.setColor(color); }Q_INVOKABLE void clear();Q_INVOKABLE void undo();void paint(QPainter *painter);protected:void mousePressEvent(QMouseEvent *event);void mouseMoveEvent(QMouseEvent *event);void mouseReleaseEvent(QMouseEvent *event);void purgePaintElements();protected:QPointF m_lastPoint;QVector<ElementGroup*> m_elements;ElementGroup * m_element; // the Current ElementGroupbool m_bEnabled;bool m_bPressed;bool m_bMoved;QPen m_pen; // the Current Pen };#endif // PAINTEDITEM_H? ? 說下 ElementGroup 這個類,它保存了鼠標左鍵按下、移動、直到左鍵釋放這一個動作序列產生的需要繪制的線條,保存在成員變量 m_lines 中,而繪制這些線條所用的畫筆則由 m_pen 表示。
? ? 在 PaintedItem 中,成員變量 m_elements 表示繪圖過程中的所有動作序列。 m_element 則指向當前的動作序列, m_pen 代表用戶所配置的畫筆。
? ? 其它的方法都比較直觀,不再贅述。
? ? 下面是 PaintedItem.cpp :
#include "PaintedItem.h" #include <QPainter> #include <QPen> #include <QBrush> #include <QColor> #include <QDebug>PaintedItem::PaintedItem(QQuickItem *parent): QQuickPaintedItem(parent), m_element(0), m_bEnabled(true), m_bPressed(false), m_bMoved(false), m_pen(Qt::black) {setAcceptedMouseButtons(Qt::LeftButton); }PaintedItem::~PaintedItem() {purgePaintElements(); }void PaintedItem::clear() {purgePaintElements();update(); }void PaintedItem::undo() {if(m_elements.size()){delete m_elements.takeLast();update();} }void PaintedItem::paint(QPainter *painter) {painter->setRenderHint(QPainter::Antialiasing);int size = m_elements.size();ElementGroup *element;for(int i = 0; i < size; ++i){element = m_elements.at(i);painter->setPen(element->m_pen);painter->drawLines(element->m_lines);} }void PaintedItem::mousePressEvent(QMouseEvent *event) {m_bMoved = false;if(!m_bEnabled || !(event->button() & acceptedMouseButtons())){QQuickPaintedItem::mousePressEvent(event);}else{//qDebug() << "mouse pressed";m_bPressed = true;m_element = new ElementGroup(m_pen);m_elements.append(m_element);m_lastPoint = event->localPos();event->setAccepted(true);} }void PaintedItem::mouseMoveEvent(QMouseEvent *event) {if(!m_bEnabled || !m_bPressed || !m_element){QQuickPaintedItem::mousePressEvent(event);}else{//qDebug() << "mouse move";m_element->m_lines.append(QLineF(m_lastPoint, event->localPos()));m_lastPoint = event->localPos();update();} }void PaintedItem::mouseReleaseEvent(QMouseEvent *event) {if(!m_element || !m_bEnabled || !(event->button() & acceptedMouseButtons())){QQuickPaintedItem::mousePressEvent(event);}else{//qDebug() << "mouse released";m_bPressed = false;m_bMoved = false;m_element->m_lines.append(QLineF(m_lastPoint, event->localPos()));update();} }void PaintedItem::purgePaintElements() {int size = m_elements.size();if(size > 0){for(int i = 0; i < size; ++i){delete m_elements.at(i);}m_elements.clear();}m_element = 0; }? ? 說一下“清除”功能的實現,當你點擊圖1中的“清除”按鈕時,會調用 PaintedItem 的 clear() 方法, clear() 內部調用 purgePaintElements() ,把 m_elements 內保存的所有繪圖序列都刪除,再調用 update() 方法觸發重新繪制。
? ? undo() 方法對應界面上的“撤銷”功能,它刪除最近的一個繪圖序列,然后觸發繪制。
? ? 現在我們說一下繪圖序列的生成邏輯。
? ? 在 mousePressEvent() 中生成一個新的繪圖序列,在 mouseMoveEvent() 中講當前點和上一個點組合為一條線,加入當前繪圖序列( m_element ),當 mouseReleaseEvent() 被調用時,把鼠標左鍵抬起時的指針位置的坐標也處理了,這樣一個完整的繪圖序列就生成了。
導出自定義Item
? ? 直接看代碼(main.cpp ):
int main(int argc, char *argv[]) {QGuiApplication app(argc, argv);qmlRegisterType<PaintedItem>("an.qml.Controls", 1, 0, "APaintedItem");QQmlApplicationEngine engine;engine.load(QUrl(QStringLiteral("qrc:///main.qml")));return app.exec(); }QML文檔
? ? 有兩個 QML 文檔, main.qml 負責主界面, ColorPicker.qml 實現了顏色選擇按鈕。
main.qml
? ? main.qml 文檔沒什么好說的了,PaintedItem 導出為 APaintedItem ,它的使用與一般的 QML 元素一致。下面是完整的 main.qml :
import QtQuick 2.2 import QtQuick.Window 2.1 import an.qml.Controls 1.0 import QtQuick.Controls 1.2 import QtQuick.Layouts 1.1 import QtQuick.Controls.Styles 1.2Window {visible: true;minimumWidth: 600;minimumHeight: 480;Rectangle {id: options;anchors.left: parent.left;anchors.right: parent.right;anchors.top: parent.top;implicitHeight: 70;color: "lightgray";Component{id: btnStyle;ButtonStyle {background: Rectangle {implicitWidth: 70;implicitHeight: 28;border.width: control.hovered ? 2 : 1;border.color: "#888";radius: 4;gradient: Gradient {GradientStop { position: 0 ; color: control.pressed ? "#ccc" : "#eee" }GradientStop { position: 1 ; color: control.pressed ? "#aaa" : "#ccc" }}}label: Text {text: control.text;font.pointSize: 12;color: "blue";horizontalAlignment: Text.AlignHCenter;verticalAlignment: Text.AlignVCenter;}}}ColorPicker {id: background;anchors.left: parent.left;anchors.leftMargin: 4;anchors.verticalCenter: parent.verticalCenter;text: "背景";selectedColor: "white";onColorPicked: painter.fillColor = clr;}ColorPicker {id: foreground;anchors.left: background.right;anchors.top: background.top;anchors.leftMargin: 4;text: "前景";selectedColor: "black";onColorPicked: painter.penColor = clr;}Rectangle {id: splitter;border.width: 1;border.color: "gray";anchors.left: foreground.right;anchors.leftMargin: 4;anchors.top: foreground.top;width: 3;height: foreground.height;}Slider {id: thickness;anchors.left: splitter.right;anchors.leftMargin: 4;anchors.bottom: splitter.bottom;minimumValue: 1;maximumValue: 100;stepSize: 1.0;value: 1;width: 280;height: 24;onValueChanged: if(painter != null)painter.penWidth = value;}Text {id: penThickLabel;anchors.horizontalCenter: thickness.horizontalCenter;anchors.bottom: thickness.top;anchors.bottomMargin: 4;text: "畫筆:%1px".arg(thickness.value);font.pointSize: 16;color: "steelblue";}Text {id: minLabel;anchors.left: thickness.left;anchors.bottom: thickness.top;anchors.bottomMargin: 2;text: thickness.minimumValue;font.pointSize: 12;}Text {id: maxLabel;anchors.right: thickness.right;anchors.bottom: thickness.top;anchors.bottomMargin: 2;text: thickness.maximumValue;font.pointSize: 12;}Rectangle {id: splitter2;border.width: 1;border.color: "gray";anchors.left: thickness.right;anchors.leftMargin: 4;anchors.top: foreground.top;width: 3;height: foreground.height;}Button {id: clear;anchors.left: splitter2.right;anchors.leftMargin: 4;anchors.verticalCenter: splitter2.verticalCenter;width: 70;height: 28;text: "清除";style: btnStyle;onClicked: painter.clear();}Button {id: undo;anchors.left: clear.right;anchors.leftMargin: 4;anchors.top: clear.top;width: 70;height: 28;text: "撤銷";style: btnStyle;onClicked: painter.undo();}Rectangle {border.width: 1;border.color: "gray";width: parent.width;height: 2;anchors.bottom: parent.bottom;}}APaintedItem {id: painter;anchors.top: options.bottom;anchors.left: parent.left;anchors.right: parent.right;anchors.bottom: parent.bottom;} }? ? 不必多說了……
顏色選擇按鈕的實現
? ? 也比較直觀,直接上代碼了:
import QtQuick 2.2 import QtQuick.Dialogs 1.0Rectangle {id: colorPicker;width: 64;height: 60;color: "lightgray";border.width: 2;border.color: "darkgray";property alias text: label.text;property alias textColor: label.color;property alias font: label.font;property alias selectedColor: currentColor.color;property var colorDialog: null;signal colorPicked(color clr);Rectangle {id: currentColor;anchors.top: parent.top;anchors.topMargin: 4;anchors.horizontalCenter: parent.horizontalCenter;width: parent.width - 12;height: 30;}Text {id: label;anchors.bottom: parent.bottom;anchors.bottomMargin: 4;anchors.horizontalCenter: parent.horizontalCenter;font.pointSize: 14;color: "blue";}MouseArea {anchors.fill: parentonClicked: if(colorDialog == null){colorDialog = Qt.createQmlObject("import QtQuick 2.2;import QtQuick.Dialogs 1.0; ColorDialog{}",colorPicker, "dynamic_color_dialog");colorDialog.accepted.connect(colorPicker.onColorDialogAccepted);colorDialog.rejected.connect(colorPicker.onColorDialogRejected);colorDialog.open();}}function onColorDialogAccepted(){selectedColor = colorDialog.color;colorPicked(colorDialog.color);colorDialog.destroy();colorDialog = null;}function onColorDialogRejected(){colorPicked(color);colorDialog.destroy();colorDialog = null;} }? ? ColorPicker 內部調用 ColorDialog 來選擇顏色。 ColorDialog 是使用 Qt.createQmlObject() 動態創建的,具體用法請參考《 Qt Quick 組件與對象動態創建詳解》。
? ? 用戶選擇了一個顏色后,按鈕上半部分的矩形的填充顏色會變化,同時也會發出 colorPicked() 信號。如果用戶取消選擇,則使用默認的顏色。
? ? OK ,就介紹到這里了。
? ??版權所有 foruok ,轉載請注明出處:http://blog.csdn.net/foruok?。
? ? 源碼下載點我點我。
? ? 回顧一下我的Qt Quick系列文章:
- Qt Quick 簡介
- QML 語言基礎
- Qt Quick 之 Hello World 圖文詳解
- Qt Quick 簡單教程
- Qt Quick 事件處理之信號與槽
- Qt Quick事件處理之鼠標、鍵盤、定時器
- Qt Quick 事件處理之捏拉縮放與旋轉
- Qt Quick 組件與對象動態創建詳解
- Qt Quick 布局介紹
- Qt Quick 之 QML 與 C++ 混合編程詳解
- Qt Quick 圖像處理實例之美圖秀秀(附源碼下載)
- Qt Quick 之 PathView 詳解
- Qt Quick實例之挖頭像
- Qt Quick綜合實例之文件查看器
- Qt Quick調試之顯示代碼行號
總結
以上是生活随笔為你收集整理的Qt Quick实现的涂鸦程序的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: lin通讯从节点同步间隔场_LIN模块介
- 下一篇: 你的设计应该「所见即所得」