Qt Quick实现的涂鸦程序

Qt Quick实现的涂鸦程序

大家好,又见面了,我是全栈君。

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

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

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

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

Qt Quick实现的涂鸦程序

                               图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系列文章:

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/115393.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • wxPython的基础教程

    wxPython的基础教程写在前面的话:上个假期学习了Python,发现它真的是一门很有趣的语言,所以这学期想学一些python的可视化编程,于是选择了wxPython。但是我在网上找中文教程找了好久都没有找到中文的教程(额,也许是我方法不对),无奈只好看英文的啦。于是在这个网站上看完了wxPython的基础教程,但是为了方便广大网友所以决定将这个网页中的内容翻译过来。花费了3个晚上的时间,终于把它翻译完了。但是我只是一个

  • SQL like子查询

    SQL like子查询like子查询字符匹配:%(百分号)任意字长度(可以为0)的字符_(下横线)代表任意单个字符eg:a%b表示以a开头,以b结尾的任意长度的字符。三个常用的字符串截取函数:substr(column,1,n)left(column,n)right(column,n)str=’abcdefg’substr(str,1,3):’bcd’left(s…

  • wps怎么做时间线_wps中的word文档如何制作时间轴「建议收藏」

    wps中的word文档如何制作时间轴很多小伙伴不知道wps中的word文档还可以制作时间轴哦,下面小编介绍一下具体办法。具体如下:1.打开一个空白的word文档,点击菜单栏的【插入】,单击选择一个带箭头的线条,在文档上面留一点空白写标题,然后插入线条拉到底部,还可以设置线条颜色,虚实等样式。2.点击工具栏中的【文本框】旁边的倒三角,再点击【多行文字】,插入一个大的文本框3.可以根据需要进行文…

  • 一文读懂BERT(原理篇)

    一文读懂BERT(原理篇)一文读懂BERT(从原理到实践)2018年的10月11日,Google发布的论文《Pre-trainingofDeepBidirectionalTransformersforLanguageUnderstanding》,成功在11项NLP任务中取得stateoftheart的结果,赢得自然语言处理学界的一片赞誉之声。本文是对近期关于BERT论文、相关文章、代码进…

  • mysql—总体备份和增量备份

    mysql—总体备份和增量备份

  • linux怎样配置yum源_yum配置源

    linux怎样配置yum源_yum配置源目录一:配置本地yum源二:配置网络yum源更新源可以获取最新的软件信息,以更新您的系统Redhat7配置源YUM(YellowdogUpdaterModified):yum是Redhat系列系统基于RPM包构建的软件更新机制,可以自动解决rpm包之间的依赖关系,所有软件包由集中的yum软件仓库提供。其理念是使用一个中心仓库(repository)管理一部分甚至一…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号