标签:
之前一直以为 Qt Quick 里 Canvas 才干够自绘。后来发觉不是,原来还有好几种方式都能够画图!
能够使用原始的 OpenGL(Qt Quick 使用 OpenGL 渲染)。能够构造QSGNode 来画图,还能够使用 QPainter !哇。 QPainter 我非常熟悉啊。于是,我用 QPainter 结合 QML 实现了一个简单的涂鸦程序: PaintedItem 。它有下列功能:
程序非常简陋。效果例如以下:
图1 PaintedItem效果图
程序尽管简单。但也还是有一些新内容之前没有提到:
以下咱们一个一个来说一下。
版权全部 foruok ,转载请注明出处:http://blog.csdn.net/foruok 。
Qt Quick 的核心是 Scene Graph ,能够在 Qt 帮助的索引模式下以 “Scene Graph” 为keyword来检索学习。 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 的使用方法,你能够參考。
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.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_OBJECT Q_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 ElementGroup bool m_bEnabled; bool m_bPressed; bool m_bMoved; QPen m_pen; // the Current Pen }; #endif // PAINTEDITEM_H
在 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; }
undo() 方法相应界面上的“撤销”功能,它删除近期的一个画图序列,然后触发绘制。
如今我们说一下画图序列的生成逻辑。
在 mousePressEvent() 中生成一个新的画图序列,在 mouseMoveEvent() 中讲当前点和上一个点组合为一条线,增加当前画图序列( m_element ),当 mouseReleaseEvent() 被调用时,把鼠标左键抬起时的指针位置的坐标也处理了,这样一个完整的画图序列就生成了。
直接看代码(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 文档, main.qml 负责主界面, ColorPicker.qml 实现了颜色选择button。
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.2 Window { 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.0 Rectangle { 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: parent onClicked: 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; } }
ColorDialog 是使用 Qt.createQmlObject() 动态创建的,具体使用方法请參考《Qt Quick 组件与对象动态创建具体解释》。
用户选择了一个颜色后。button上半部分的矩形的填充颜色会变化,同一时候也会发出 colorPicked() 信号。假设用户取消选择,则使用默认的颜色。
OK ,就介绍到这里了。
版权全部 foruok ,转载请注明出处:http://blog.csdn.net/foruok 。
回想一下我的Qt Quick系列文章:
标签:
原文地址:http://www.cnblogs.com/yxwkf/p/5374619.html