之前一直以为 Qt Quick 里 Canvas 才干够自绘。后来发觉不是,原来还有好几种方式都能够画图!

能够使用原始的 OpenGL(Qt Quick 使用 OpenGL 渲染)。能够构造QSGNode 来画图,还能够使用 QPainter !哇。 QPainter 我非常熟悉啊。于是,我用 QPainter 结合 QML 实现了一个简单的涂鸦程序: PaintedItem 。它有下列功能:

  • 设置线条宽度
  • 设置线条颜色
  • 设置背景颜色
  • 清除涂鸦
  • 无限级undo

程序非常简陋。效果例如以下:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZm9ydW9r/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

图1 PaintedItem效果图

程序尽管简单。但也还是有一些新内容之前没有提到:

  • QQuickPaintedItem
  • C++实现QML可视图元(Item)
  • 自己定义图元怎样处理鼠标事件

以下咱们一个一个来说一下。

版权全部 foruok ,转载请注明出处:http://blog.csdn.net/foruok

QQuickPaintedItem

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 的使用方法,你能够參考。

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_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

说下 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中的“清除”button时,会调用 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 实现了颜色选择button。

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.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;
}
}

不必多说了……

颜色选择button的实现

也比較直观,直接上代码了:

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;
}
}

ColorPicker 内部调用 ColorDialog 来选择颜色。

ColorDialog 是使用 Qt.createQmlObject() 动态创建的,具体使用方法请參考《Qt Quick 组件与对象动态创建具体解释》。

用户选择了一个颜色后。button上半部分的矩形的填充颜色会变化,同一时候也会发出 colorPicked() 信号。假设用户取消选择,则使用默认的颜色。

OK ,就介绍到这里了。

版权全部 foruok ,转载请注明出处:http://blog.csdn.net/foruok 。

源代码下载点我点我

回想一下我的Qt Quick系列文章:

Qt Quick实现的涂鸦程序的更多相关文章

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

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

  2. Qt Quick程序的发布

    要将程序发布出去,首先需要使用release方式编译程序,然后将生成的.exe可执行文件和需要的库文件发在一起打包进行发布. 要确定需要哪些动态库文件,可以直接双击.exe文件,提示缺少那个dll文件 ...

  3. 发布Qt Quick桌面应用程序的方法(使得planets在XP上运行)

    发布Qt Quick桌面应用程序的方法 Qt是一款优秀的跨平台开发框架,它可以在桌面.移动平台以及嵌入式平台上运行.目前Qt 5介绍程序发布的文章帖子比较少.大家又非常想要知道如何发布Qt应用程序,于 ...

  4. 打包发布 Qt Quick/Widgets 程序

    使用的QT自带的部署工具(windeployqt.exe,路径QT安装路径),版本替换debug/release Qt Quick "C:\Qt\Qt5.8.0\5.8\mingw53_32 ...

  5. 从头学Qt Quick(1) --体验快速构建动态效果界面

    自2005年Qt4发布以来,Qt已经为成千上万的应用程序提供了框架服务,现在Qt已经基本上支持所有的开发平台了,这里面既包含了桌面.嵌入式领域,也包括了Android.IOS.WP等移动操作平台,甚至 ...

  6. Qt on Android: Qt Quick 之 Hello World 图文具体解释

    在上一篇文章,<Qt on Android:QML 语言基础>中,我们介绍了 QML 语言的语法,在最后我们遗留了一些问题没有展开,这篇呢,我们就正式開始撰写 Qt Quick 程序,而那 ...

  7. Qt 学习之路 :Qt Quick Controls

    自 QML 第一次发布已经过去一年多的时间,但在企业应用领域,QML 一直没有能够占据一定地位.很大一部分原因是,QML 缺少一些在企业应用中亟需的组件,比如按钮.菜单等.虽然移动领域,这些组件已经变 ...

  8. Qt Quick 与 QML语言(初学笔记1)

    Qt Quick Qt Quick是一些新的UI技术的集合,用来帮助开发者创建一种现在越来越多用于手机.多媒体播放器.机顶盒以及其他便携式设备上的直观的.现代的.流畅的用户界面.简单来说,Qt Qui ...

  9. 对Qt for Android的评价(很全面,基本已经没有问题了,网易战网客户端就是Qt quick写的),可以重用QT积累20年的RTL是好事,QML效率是HTML5的5倍

    现在Qt不要光看跨平台了,Qt也有能力和原生应用进行较量的.可以直接去Qt官网查看他和那些厂商合作.关于和Java的比较,框架和Java进行比较似乎不且实际.如果是C++和Java比较,网上有很多文章 ...

随机推荐

  1. 爬虫技术之——bloom filter(含java代码)

    在爬虫系统中,在内存中维护着两个关于URL的队列,ToDo队列和Visited队列,ToDo队列存放的是爬虫从已经爬取的网页中解析出来的即将爬取的URL,但是网页是互联的,很可能解析出来的URL是已经 ...

  2. Request、Request.Form、Request.QueryString 用法的区别

    Request.Form:获取以POST方式提交的数据. Request.QueryString:获取地址栏参数(以GET方式提交的数据). Request:包含以上两种方式(优先获取GET方式提交的 ...

  3. MATLAB Coder从MATLAB生成C/C++代码步骤

    MATLAB Coder可以从MATLAB代码生成独立的.可读性强.可移植的C/C++代码. 使用MATLAB Coder产生代码的3个步骤: 准备用于产生代码的MATLAB算法: 检查MATLAB代 ...

  4. Delphi下使用OpenOffice+JodConverter+SWFtools进行文件转换

    目的:office文件转换为PDF或SWF,最终可使用Flexpaper调用adobe flash player进行浏览 放弃两个文件转换工具: 1.FlashPaper,转换出的文件由于自带工具栏, ...

  5. 【脚本语言对比】BASH,PERL以及PYTHON

    据说: BASH能调用linux的应用程序,这是其最大的优点,也是其最大的缺点. PERL那复杂的语法确实看得让人想吐. python很优美,但是据说对正则的支持不够,没有perl强大. 总结一下学习 ...

  6. STL源码剖析读书笔记--第6章&第7章--算法与仿函数

    老实说,这两章内容还蛮多的,但是其实在应用中一点点了解比较好.所以我决定这两张在以后使用过程中零零散散地总结,这个时候就说些基本概念好了.实际上,这两个STL组件都及其重要,我不详述一方面是自己偷懒, ...

  7. hadoop-1.2.0 eclipse插件编译

    linux.windows下通用,亲测. 下面以window为例,假设:hadoop工程目录位于D:\work\eclipse64\hadoop-1.2.0.1.3.0.0,eclipse安装目录为E ...

  8. 根据headerView位置改变headerView颜色(collectionView/tableview)

    滑动时,tableview中的headerView 的frame不断改变,collectionView的headerView的center不断改变.sotableview: -(void)setFra ...

  9. 数据结构 -- 简单图的实现与遍历 (Java)

    ---恢复内容开始--- 作者版权所有,转载请注明出处,多谢. http://www.cnblogs.com/Henvealf/p/5534071.html 前些天因为某些原因,就试着回想一下图(gr ...

  10. <一道题>abc+cba=1333,求满足条件的abc的值,隐含条件a!=0,c!=0

    这类东西,无非就是穷举法.见下面代码: #include <stdio.h> #include <stdlib.h> /* *abc + cba = 1333 * *a = ? ...