天天看點

QML實作的支援動圖的編輯器(比之前要好)

【寫在前面】

在我之前的部落格中就做過一個支援動圖的編輯器,但是效果很差,而且還會出現其他的問題。

然而最近找到了更好的實作方法,已經基本可以用了。

【正文開始】 

老規矩,先上效果圖:

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

繼續閱讀