之前一直以为 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实现的涂鸦程序相关推荐

  1. Qt Quick实现的疯狂算数游戏

    使用 Qt Quick 写了个小游戏:疯狂算数.支持 Windows 和 Android 两个平台. 游戏简单,但牵涉到下面你的 Qt Quick 主题: 自己实现一个按钮 自适应分辨率 国际化 QM ...

  2. Qt Quick里的图形效果——颜色(Color)

    Qt Quick提供了通过改变一个 Item 的颜色来产生各种各样效果的元素.有下面几种: BrightnessContrast,调整亮度和对比度 ColorOverlay,在源 Item 上覆盖一层 ...

  3. Qt Quick里的图形效果——渐变(Gradient)

    Qt Quick提供了三种渐变图形效果: ConicalGradient,锥形渐变 LinearGradient,线性渐变 RadialGradient,径向渐变 效果 下图是我设计的示例效果: 图 ...

  4. 如何创建Qt quick应用程序

    Qt(发音为" cute",而不是" cu-tee")是一个跨平台框架,通常用作图形工具包,它不仅创建CLI应用程序中也非常有用.而且它也可以在三种主要的台式机 ...

  5. Qt Quick中的The Visual Canvas

    The Visual Canvas The Visual Canvas 坐标系 Visual Parent Scene Graph The Visual Canvas Qt Quick提供的可视画布是 ...

  6. Qt Creator创建Qt Quick项目

    Qt Creator创建Qt Quick项目 Qt Creator创建Qt Quick项目 创建Qt Quick应用程序 创建基于Qt Quick的Python应用程序 创建Qt Quick UI项目 ...

  7. qml 时间控件_Qt6官方最新消息:桌面样式Qt quick Controls 2支持大多控件,委托更少,运行更流畅...

    Qt是一个跨平台框架,通常用作图形工具包,它不仅创建CLI应用程序中非常有用.而且它也可以在三种主要的台式机操作系统以及移动操作系统(如Symbian,Nokia Belle,Meego Harmat ...

  8. QT Quick 简介

    Qt Quick是一个用于帮助开发者设计直观,现代,流畅的用户界面的技术集,近年来被广泛应用于手机,媒体播放器,机顶盒和其他手提设备.Qt Quick中包含了大量的用户界面元素,及描述这些用户界面的陈 ...

  9. Qt Quick QML

    Qt Quick简介 Qt Quick是一个用于帮助开发者设计直观,现代,流畅的用户界面的技术集,近年来被广泛应用于手机,媒体播放器,机顶盒和其他手提设备.Qt Quick中包含了大量的用户界面元素, ...

最新文章

  1. 【每日一题】502. IPO
  2. 【MATLAB】矩阵操作 ( 矩阵下标 | 矩阵下标排列规则 )
  3. web前端知识体系大全
  4. 阿里P8架构师谈:从单体架构、到SOA、再到微服务的架构设计详解
  5. 【mongodb系统学习之四】查看mongodb进程
  6. php大量数据库抽象,PHP系列(十二)数据库抽象层pdo
  7. MFC开发IM-第四篇、mfc 对话框dialog的属性意思
  8. 【289天】跃迁之路——程序员高效学习方法论探索系列(实验阶段47-2017.11.21)...
  9. JS:关于JS字面量及其容易忽略的12个小问题
  10. oseasy还原卡_学校机房管理批量更新软件的问题还原卡、网刻、噢易OSS对比区别...
  11. python 时间格式处理
  12. 结合 服务器+后端+前端,完成 vue项目 后台管理系统
  13. 谈谈面试题之为什么用线程池?解释下线程池参数?
  14. java后台如何将rgb与16进制颜色进行转换
  15. 测试人员必备数据库技术之数据库约束(五)
  16. 人要感知到自己的渺小,行为才开始伟大
  17. 枪战英雄啥时候服务器维护结束,【公告】《枪战英雄》7月13日停服更新公告
  18. 不要陷入「完美主义者」的陷阱,你必须填的坑
  19. pip 查看和配置源
  20. linux中合并多个文件内容到一个文件的例子

热门文章

  1. 知乎1w视频播放能赚多钱?今天来大揭秘
  2. 深入理解编译注解(三)依赖关系 apt/annotationProcessor与Provided的区别
  3. 关于yo3 所遇到的问题
  4. 如果你决定离职,请把这五样东西留下,要不然你必身败名裂
  5. 信息系统项目管理师---第十一章项目风险管理历年考题
  6. 观复嘟嘟:职场是个技术活-马未都
  7. PAT 乙级 1046(划拳)
  8. web 文字描边动画
  9. spring框架三种类型项目实现--基础maven类型、纯注解开发型、aop结合注解开发型
  10. 视频水印怎么去掉?有什么好的去水印工具吗?