QML实现的支持动图的编辑器(比之前要好)

【写在前面】

        在我之前的博客中就做过一个支持动图的编辑器,但是效果很差,而且还会出现其他的问题。

        然而最近找到了更好的实现方法,已经基本可以用了。


【正文开始】 

        老规矩,先上效果图:

        看起来还不错,现在开始讲解实现,实际上很简单,不过,有一些地方要注意。

        首先,是 ImageHelper,这个类就是用来插入图片到 qml 中的 TextEdit / TextArea 等等的辅助类:

        imagehelper.h:

#ifndef IMAGEHELPER_H
#define IMAGEHELPER_H

#include <QMovie>
#include <QTextCursor>
#include <QQuickWindow>

class Api : public QObject
{
    Q_OBJECT

public:
    Api(QObject *parent = nullptr);

    Q_INVOKABLE bool exists(const QString &arg);
    Q_INVOKABLE QString baseName(const QString &arg);
};

class QQuickTextDocument;
class ImageHelper : public QObject
{
    Q_OBJECT

    Q_PROPERTY(QQuickTextDocument* document READ document WRITE setDocument NOTIFY documentChanged)
    Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
    Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged)
    Q_PROPERTY(int selectionEnd READ selectionEnd WRITE setSelectionEnd NOTIFY selectionEndChanged)
    //用于控制插入的图片的最大宽/高
    Q_PROPERTY(int maxWidth READ maxWidth WRITE setMaxWidth NOTIFY maxWidthChanged)
    Q_PROPERTY(int maxHeight READ maxHeight WRITE setMaxHeight NOTIFY maxHeightChanged)

public:
    ImageHelper(QObject *parent = nullptr);
    ~ImageHelper();

    Q_INVOKABLE void insertImage(const QUrl &url);
    Q_INVOKABLE void cleanup();

    QQuickTextDocument* document() const;
    void setDocument(QQuickTextDocument *document);

    int cursorPosition() const;
    void setCursorPosition(int position);

    int selectionStart() const;
    void setSelectionStart(int position);

    int selectionEnd() const;
    void setSelectionEnd(int position);

    int maxWidth() const;
    void setMaxWidth(int max);

    int maxHeight() const;
    void setMaxHeight(int max);

signals:
    void needUpdate();
    void documentChanged();
    void cursorPositionChanged();
    void selectionStartChanged();
    void selectionEndChanged();
    void maxWidthChanged();
    void maxHeightChanged();

private:
    QTextDocument *textDocument() const;
    QTextCursor textCursor() const;

private:
    QHash<QUrl, QMovie *> m_urls;
    QQuickTextDocument *m_document;
    int m_cursorPosition;
    int m_selectionStart;
    int m_selectionEnd;
    int m_maxWidth;
    int m_maxHeight;
};

#endif // IMAGEHELPER_H

        前四个属性是绑定到 qml TextEdit 的属性,其中,关键的 QQuickTextDocument TextEdit 提供的,在 C++ 中访问其文档的的属性。

        注意,这里有坑。

QQuickTextDocument的细节描述: 

The QQuickTextDocument class provides access to the QTextDocument of QQuickTextEdit.

This class provides access to the QTextDocument of QQuickTextEdit elements. This is provided to allow usage of the Rich Text Processing functionalities of Qt. You are not allowed to modify the document, but it can be used to output content, for example with QTextDocumentWriter), or provide additional formatting, for example with QSyntaxHighlighter.

The class has to be used from C++ directly, using the property of the TextEdit.

Warning: The QTextDocument provided is used internally by Qt Quick elements to provide text manipulation primitives. You are not allowed to perform any modification of the internal state of the QTextDocument. If you do, the element in question may stop functioning or crash.

翻译:

QQuickTextDocument类提供对QQuickTextEdit的QTextDocument的访问。

该类提供对QQuickTextEdit元素的QTextDocument的访问。 这是为了允许使用Qt的富文本处理功能。 您不能修改文档,但可以使用它来输出内容,例如使用QTextDocumentWriter),或者提供其他格式,例如使用QSyntaxHighlighter。

必须使用TextEdit的属性直接从C ++使用该类。

警告:提供的QTextDocument由Qt Quick元素在内部使用,以提供文本操作原语。 您不能对QTextDocument的内部状态执行任何修改。 如果这样做,相关元素可能会停止运行或崩溃。

         实际上,QQuickTextDocument::textDocument() 和 widgets中 的 QTextEdit::document() 是一种东西,

        但是,访问的数据却是不一样的(即:不能使用QTextBlock等来遍历),坑爹Σ( ° △ °|||)︴。

        这个问题在这里没什么影响,但在我另一个项目中却有很大的影响,不过我也已经解决了。

        先上MyTextArea.qml:

import QtQuick 2.12
import QtQuick.Controls 2.12
import an.controls 1.0

TextArea
{
    id: editor
    smooth: true
    textFormat: Text.RichText
    selectionColor: "#3399FF"
    selectByMouse: true
    selectByKeyboard: true
    wrapMode: TextEdit.Wrap

    function insertImage(src)
    {
        imageHelper.insertImage(src);
    }

    function cleanup()
    {
        editor.remove(0, length)
        imageHelper.cleanup();
    }

    ImageHelper
    {
        id: imageHelper
        document: editor.textDocument
        cursorPosition: editor.cursorPosition
        selectionStart: editor.selectionStart
        selectionEnd: editor.selectionEnd

        onNeedUpdate:
        {
            //editor.update() 这句不起作用,编辑器未改变,就不会更新,用下面的方法
            let alpha = editor.color.a;
            editor.color.a = alpha - 0.01;
            editor.color.a = alpha;
        }
    }
}

        qml 中唯一的问题在于更新 TextArea,普通的 update() 在 qml 中已经没有用了(可能是因为场景图的状态未改变??),所以必须让 editor 发生改变,这里我的做法就是稍微变化一丁点字体颜色的透明度,肉眼无法看到,并且也成功让 editor 进行了刷新。

        接着来看 ImageHelper 的实现:

#include "imagehelper.h"

#include <QFile>
#include <QFileInfo>
#include <QQmlFile>
#include <QQuickTextDocument>
#include <QDebug>

Api::Api(QObject *parent)
    : QObject(parent)
{
}

bool Api::exists(const QString &arg)
{
    return QFile::exists(arg);
}

QString Api::baseName(const QString &arg)
{
    return QFileInfo(arg).baseName();
}

ImageHelper::ImageHelper(QObject *parent)
    : QObject(parent),
      m_maxWidth(120),
      m_maxHeight(120)
{

}

ImageHelper::~ImageHelper()
{
    cleanup();
}

void ImageHelper::insertImage(const QUrl &url)
{
    QImage image = QImage(QQmlFile::urlToLocalFileOrQrc(url));
    if (image.isNull())
    {
        qDebug() << "不支持的图像格式";
        return;
    }
    QString filename = url.toString();
    QString suffix = QFileInfo(filename).suffix();
    if (suffix == "GIF" || suffix == "gif") //如果是gif,则单独处理
    {
        QString gif = filename;
        if (gif.left(4) == "file")
            gif = gif.mid(8);
        else if (gif.left(3) == "qrc")
            gif = gif.mid(3);

        textCursor().insertHtml("<img src='" + url.toString() + "' width = " +
                                QString::number(qMin(m_maxWidth, image.width())) + " height = " +
                                QString::number(qMin(m_maxHeight, image.height()))+ "/>");
        textDocument()->addResource(QTextDocument::ImageResource, url, image);
        if (m_urls.contains(url))
            return;
        else
        {
            QMovie *movie = new QMovie(gif);
            movie->setCacheMode(QMovie::CacheNone);
            connect(movie, &QMovie::finished, movie, &QMovie::start);   //循环播放
            connect(movie, &QMovie::frameChanged, this, [url, this](int)
            {
                QMovie *movie = qobject_cast<QMovie *>(sender());
                textDocument()->addResource(QTextDocument::ImageResource, url, movie->currentPixmap());
                emit needUpdate();
            });
            m_urls[url] = movie;
            movie->start();
        }
    }
    else
    {        
        QTextImageFormat format;
        format.setName(filename);
        format.setWidth(qMin(m_maxWidth, image.width()));
        format.setHeight(qMin(m_maxHeight, image.height()));
        textCursor().insertImage(format, QTextFrameFormat::InFlow);
    }
}

void ImageHelper::cleanup()
{
    for (auto it : m_urls)
        it->deleteLater();
    m_urls.clear();
}

QQuickTextDocument* ImageHelper::document() const
{
    return  m_document;
}

void ImageHelper::setDocument(QQuickTextDocument *document)
{
    if (document != m_document)
    {
        m_document = document;
        emit documentChanged();
    }
}

int ImageHelper::cursorPosition() const
{
    return m_cursorPosition;
}

void ImageHelper::setCursorPosition(int position)
{
    if (position != m_cursorPosition)
    {
        m_cursorPosition = position;
        emit cursorPositionChanged();
    }
}

int ImageHelper::selectionStart() const
{
    return m_selectionStart;
}

void ImageHelper::setSelectionStart(int position)
{
    if (position != m_selectionStart)
    {
        m_selectionStart = position;
        emit selectionStartChanged();
    }
}

int ImageHelper::selectionEnd() const
{
    return m_selectionEnd;
}

void ImageHelper::setSelectionEnd(int position)
{
    if (position != m_selectionEnd)
    {
        m_selectionEnd = position;
        emit selectionEndChanged();
    }
}

int ImageHelper::maxWidth() const
{
    return m_maxWidth;
}

void ImageHelper::setMaxWidth(int max)
{
    if (max != m_maxWidth)
    {
        m_maxWidth = max;
        emit maxWidthChanged();
    }
}

int ImageHelper::maxHeight() const
{
    return m_maxHeight;
}

void ImageHelper::setMaxHeight(int max)
{
    if (max != m_maxHeight)
    {
        m_maxHeight = max;
        emit maxHeightChanged();
    }
}

QTextDocument* ImageHelper::textDocument() const
{
    if (m_document)
        return m_document->textDocument();
    else return nullptr;
}

QTextCursor ImageHelper::textCursor() const
{
    QTextDocument *doc = textDocument();
    if (!doc)
        return QTextCursor();

    QTextCursor cursor = QTextCursor(doc);
    if (m_selectionStart != m_selectionEnd)
    {
        cursor.setPosition(m_selectionStart);
        cursor.setPosition(m_selectionEnd, QTextCursor::KeepAnchor);
    }
    else
    {
        cursor.setPosition(m_cursorPosition);
    }

    return cursor;
}

        这里关键的两个函数,一个是 textCursor(),它根据光标的位置返回一个 QTextCursor ( 这个类emmm自己看文档吧 )。

        另一个则是 insertImage():

        这个函数通过几个步骤来实现往文档中插入任意图片:

        1、解析传入的 url 并使用 QImage 来尝试打开,实际上 gif 格式是可以用 QImage 来打开的 ( 不过只有第一帧 )。

        2、判断传入的图片是动图还是非动图。

        --- 如果非动图:创建一个 QTextImageFormat (文本图片格式),setName() 设置图片路径。

        --- 如果是动图:使用 insertHtml() 来插入动图(一帧),并使用 addResource() 加入到文档的资源中。实际上,在 widgets 中可以使用非动图的插入方式,但是,quick版的不行,必须使用 html 方式插入。

        3、( 动图 ) 判断此图片是否已经插入 QHash<QUrl, QMovie *> 中,这样,同一张 gif 就可以只使用一个 QMovie 来控制,提高了性能和资源,并且可以保持一致。

        接着,连接 QMovie 的信号 finished ( 用于循环播放 ),frameChanged:

        使用 addResource() 改变对应url的图片,通过 QMovie::currentPixmap() 获取当前帧的pixmap。

        最后,发出 needUpdate() 信号,在 qml 中连接-刷新。

        至此,ImageHelper 讲解完毕。


【结语】 

        打字好累啊....这一次做的编辑器确实比上次的好了太多,至少能够正常使用了。

        当然我还是要吐槽一下 qt quick,一些东西用起来和 widgets 版不一样是咋肥事?

        最后,依旧是放到 QmlControls 中了: https://github.com/mengps/QmlControls

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦起丶

您的鼓励和支持是我创作最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值