Qml實現簡易版Qt Linguist(語言家) & QXmlStreamReader / QXmlStreamWriter 的使用方法

【寫在前面】

因爲之前寫了幾篇文章,是 Qt 翻譯的相關的,然後自己就去了解了下 TS 文件( Translate Source 翻譯資源文件 )。

發現只是比較簡單的 XML 文件,又因爲自己對 Qt 讀寫 XML 用的並不是很多,就想着學習一下在 Qt 中讀寫XML

結果。。順便做了個 Qml 版的 Qt 語言家  ─.─|||。。


【正文開始】

在Qt 中,有三種讀寫 XML 文檔的方法:

1、QXmlStreamReader / QXmlStreamWriter

QXmlStreamReader 類提供了一個快速解析器,用於通過簡單的流 API 讀取格式正確的 XML。

QXmlStreamReader 是 Qt 自己的 SAX 解析器的更快,更方便的替代方法( 請參閱 QXmlSimpleReader )。在某些情況下,對於在其他情況下使用 DOM樹的應用程序來說,這可能是一種更快,更方便的替代方法( 請參閱QDomDocument )。

QXmlStreamReader 從 QIODevice( 請參閱setDevice()) 或從原始 QByteArray ( 請參閱addData() )讀取數據。

Qt 提供 QXmlStreamWriter 來編寫 XML。

流讀取器的基本概念是將 XML 文檔作爲令牌流報告,類似於 SAX。

QXmlStreamReader 和 SAX之間的主要區別在於如何報告這些 XML 令牌。使用 SAX,應用程序必須提供處理程序(回調函數),以便在解析器方便時從解析器接收所謂的 XML 事件。

使用 QXmlStreamReader,應用程序代碼本身可以驅動循環並根據需要從讀取器中逐個提取令牌。這是通過調用readNext() 來完成的,在該方法中,讀取器從輸入流中進行讀取,直到完成下一個標記爲止,然後它返回 tokenType()。然後可以使用一組方便的函數,包括 isStartElement() 和 text() 來檢查令牌,以獲得有關已讀取內容的信息。這種拉取方法的最大優點是可以使用它構建遞歸下降解析器,這意味着您可以輕鬆地將 XML 解析代碼拆分爲不同的方法或類。這使得在解析 XML 時輕鬆跟蹤應用程序自身的狀態。

2、DOM ( Document Object Model 文檔對象模型 ):

QDomDocument 類代表整個 XML 文檔。從概念上講,它是文檔樹的根,並提供對文檔數據的主要訪問。

由於元素,文本節點,註釋,處理指令等不能在文檔上下文之外存在,因此文檔類還包含創建這些對象所需的工廠功能。創建的節點對象具有 ownerDocument() 函數,該函數將它們與在其上下文中創建的文檔相關聯。最常使用的 DOM 類是 QDomNode,QDomDocument,QDomElement 和 QDomText。

解析的 XML 在內部由對象樹表示,可以使用各種 QDom 類訪問這些對象。所有 QDom 類僅引用內部樹中的對象。一旦引用它們的最後一個 QDom 對象或 QDomDocument 本身被刪除,DOM 樹中的內部對象將被刪除。

元素,文本節點等的創建是使用此類中提供的各種工廠功能完成的。使用 QDom 類的默認構造函數只會導致無法操縱或將其插入 Document 的空對象。

QDomDocument 類具有用於創建文檔數據的多個函數,例如,createElement(),createTextNode(),createComment(),createCDATASection(),createProcessingInstruction(),createAttribute() 和 createEntityReference()。其中一些函數具有支持名稱空間的版本,即 createElementNS() 和 createAttributeNS()。 createDocumentFragment() 函數用於保存文檔的一部分;這對於處理複雜的文檔很有用。
使用 setContent() 設置文檔的全部內容。此函數解析作爲 XML 文檔傳遞的字符串,並創建表示該文檔的 DOM 樹。可以使用 documentElement() 獲得根元素。可以使用 toString() 獲得文檔的文本表示形式。

注意:如果 XML 文檔很大,則 DOM 樹最終可能會保留大量內存。對於此類文檔,QXmlStreamReader 或 QXmlQuery 類可能是更好的解決方案。可以使用 importNode() 將另一個文檔中的節點插入文檔中。 

3、SAX ( Simple API for XML XML的簡單API):

SAX 是 XML 解析器的基於事件的標準接口。Qt 接口遵循 SAX2 Java 實現的設計。它的命名方案經過修改以適合Qt命名約定。

有關 SAX2 的詳細信息,對 SAX2 過濾器和閱讀器工廠的支持正在開發中。Qt 實現不包括 Java 接口中存在的SAX1 兼容性類。

SAX2 接口是一種事件驅動的機制,可爲用戶提供文檔信息。在這種情況下,“事件”表示解析器報告的內容,例如,它遇到了開始標籤或結束標籤等。

實際上,Qt5 以後,對於第二、第三種 ( 位於 QtXml 模塊中),Qt 已經不再維護,而第一種則被移動到 QtCore 中( 所以該用那個就不用我多說了吧 ):

更重要的是,我也不會用這兩種 : )。

因此,本篇文章只使用 QXmlStreamReader / QXmlStreamWriter 來解析 XML。

對於一個 TS 文件:它的格式相當簡單,例如:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN">
<context>
    <name>QObject</name>
    <message>
        <location filename="../src/main.cpp" line="22"/>
        <source>Can&apos;t create TsModel Object!</source>
        <translatorcomment>不能創建 TsModel 對象!</translatorcomment>
        <translation>不能創建 TsModel 對象!</translation>
    </message>
    <message>
        <location filename="../src/main.cpp" line="23"/>
        <source>Can&apos;t create Translator Object!</source>
        <translatorcomment>不能創建 Translator 對象!</translatorcomment>
        <translation>不能創建 Translator 對象!</translation>
    </message>
</context>
<context>
    <name>AutoDiscoveryHostsDialog</name>
    <message>
        <location filename="src/bench/autodiscoveryhostsdialog.ui" line="14"/>
        <source>Dialog</source>
        <translation type="unfinished"></translation>
    </message>
    <message>
        <location filename="src/bench/autodiscoveryhostsdialog.ui" line="20"/>
        <source>Select the Hosts you want to add:</source>
        <translation type="unfinished"></translation>
    </message>
</context>
</TS>

首先,它以一對 <TS></TS> 標籤爲根,包含多個 <context>,每個 <context> 只有一個 <name> 和多個 <message>,其中,<message> 爲真正的文本源,它包含 :

<location>:代碼位置信息 ( 相對 ts 文件的位置 )。

<source>:待翻譯的文本(資)源。

<translation type=>:翻譯後的文本,其中 type 爲文本的翻譯狀態

<translatorcomment>:可選,翻譯的註釋文本。

現在我們使用 QXmlStreamReader 進行解析,關鍵代碼如下:

bool TsParser::load(const QString &filename)
{
    cleanup();

    QFile tsFile(filename);
    if (!tsFile.open(QIODevice::ReadOnly)) {
        m_lastError = tsFile.errorString();
        return false;
    }

    m_tsReader->clear();
    m_tsReader->addData(tsFile.readAll());
    tsFile.close();

    if (m_tsReader->error() != QXmlStreamReader::NoError) {
        m_lastError = m_tsReader->errorString();
        return false;
    }

    return true;
}

void TsParser::parser()
{
    if (m_tsReader->readNextStartElement()) {
        if (m_tsReader->name() == QLatin1String("TS")) {
            m_tsVersion = m_tsReader->attributes().value("version").toString();
            m_tsLanguage = m_tsReader->attributes().value("language").toString();
            readTS();
        } else {
            m_lastError = tr("This 'ts' file not have <TS> element!");
            emit error();
        }
    } else {
        m_lastError = tr("Parser start error!");
        emit error();
    }
}

void TsParser::readTS()
{
    Q_ASSERT(m_tsReader->isStartElement() && m_tsReader->name() == QLatin1String("TS"));

    while (m_tsReader->readNextStartElement()) {
        if (m_tsReader->name() == QLatin1String("context")) {
            readContext();
        } else m_tsReader->skipCurrentElement();
    }
}

void TsParser::readContext()
{
    Q_ASSERT(m_tsReader->isStartElement() && m_tsReader->name() == QLatin1String("context"));

    QString name;
    while (m_tsReader->readNextStartElement()) {
        if (m_tsReader->name() == QLatin1String("name")) {
            name = m_tsReader->readElementText();
        } else if (m_tsReader->name() == QLatin1String("message")) {
            TsData *data = new TsData;
            data->setName(name);
            readMessage(data);
            m_result.append(data);
        } else m_tsReader->skipCurrentElement();
    }
}

void TsParser::readMessage(TsData *data)
{
    QList<QString> filenames;
    QList<int> lines;
    while (m_tsReader->readNextStartElement()) {
        if (m_tsReader->name() == QLatin1String("location")) {
            do {
                if (m_tsReader->name() == QLatin1String("location")) {
                    filenames.append(m_tsReader->attributes().value("filename").toString());
                    lines.append(m_tsReader->attributes().value("line").toInt());
                }
            } while (m_tsReader->readNextStartElement());
        } else if (m_tsReader->name() == QLatin1String("source")) {
            readSourceText(data);
        } else if (m_tsReader->name() == QLatin1String("translation")) {
            readTranslateText(data);
        } else if (m_tsReader->name() == QLatin1String("translatorcomment")) {
            readCommentsText(data);
        } else m_tsReader->skipCurrentElement();
    }

    data->setFileNames(filenames);
    data->setLines(lines);
}

1、創建一個 QXmlStreamReader 流讀取器,可以來自 QIODevice 或者來自一串數據( 使用 addData() 添加) ,注意,如果要重複使用該 Reader,那麼在每次 addData() 之前請 clear() 一下。

2、使用 readNextStartElement() 讀取下一個開始元素 ( 注意如果遇到結束元素</> 或者發生錯誤,該函數返回 false ),另一種是使用 readNext() 讀取下一個元素 ( 注意每開始一個新的元素時會讀取一個空字符串 ),當然,也可以混合使用。

3、使用 name() / attributes()readElementText() 等等來獲取需要的數據。注意readElementText() 會一直讀到當前元素的結束元素。

4、使用 skipCurrentElement() 跳過當前元素( 以及它的節點 ),直到讀取到當前元素的結束元素,此函數主要用於跳過未知元素( 或者說不需要的元素 )。

一般的,我們會將讀取到的數據進行結構化,然後在上面進行更改即可,而無需更改 XML 本身。

接着,我們只需重新導出 XML 並交給 lrelease.exe 處理生成最終的 [.qm] 文件。

這裏使用 QXmlStreamWriter 來導出XML,其結構即爲 TS 文件的結構。:

bool TsParser::save(const QString &filename)
{
    QFile tsFile(filename);
    if (!tsFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
        m_lastError = tsFile.errorString();
        return false;
    }

    m_tsWriter->setDevice(&tsFile);
    m_tsWriter->writeStartDocument();
    m_tsWriter->writeDTD(QStringLiteral("<!DOCTYPE TS>"));
    m_tsWriter->writeStartElement("TS");
    m_tsWriter->writeAttribute("version", m_tsVersion);
    m_tsWriter->writeAttribute("language", m_tsLanguage);

    if (m_result.count() > 0) {
        QString currentName = m_result[0]->name();
        m_tsWriter->writeStartElement("context");
        m_tsWriter->writeTextElement("name", m_result[0]->name());
        for (int i = 0; i < m_result.count(); ++i) {
            if (currentName != m_result[i]->name()) {
                //</context>
                m_tsWriter->writeEndElement();
                m_tsWriter->writeStartElement("context");
                m_tsWriter->writeTextElement("name", m_result[i]->name());
                currentName = m_result[i]->name();
            }

            m_tsWriter->writeStartElement("message");
            QList<QString> fileNames = m_result[i]->fileNames();
            QList<int> lines = m_result[i]->lines();
            if (fileNames.count() != lines.count())
                return false;

            for (int j = 0; j < fileNames.count(); j++) {
                m_tsWriter->writeStartElement("location");
                m_tsWriter->writeAttribute("filename", fileNames[j]);
                m_tsWriter->writeAttribute("line", QString::number(lines[j]));
                m_tsWriter->writeEndElement();
            }
            m_tsWriter->writeTextElement("source", m_result[i]->sourceText());

            if (!m_result[i]->commentsText().isEmpty())
                m_tsWriter->writeTextElement("translatorcomment", m_result[i]->commentsText());

            QString translateText = m_result[i]->translateText();
            if (!translateText.isEmpty())
                m_tsWriter->writeTextElement("translation", translateText);
            else {
                m_tsWriter->writeStartElement("translation");
                m_tsWriter->writeAttribute("type", "unfinished");
                m_tsWriter->writeEndElement();
            }

            //</message>
            m_tsWriter->writeEndElement();
        }
    }

    //</TS>
    m_tsWriter->writeEndElement();
    m_tsWriter->writeEndDocument();
    tsFile.close();

    return true;
}

1、使用 writeStartDocument() 寫入XML 聲明,類似於這種:<?xml version="1.0" encoding="UTF-8"?>

2、使用 writeDTD() 寫入文檔類型定義( DTD )。

3、使用 writeStartElement() 寫入一個開始元素。

4、使用 writeTextElement() 寫入一個文本元素,包含開始和終止元素。

5、使用 writeAttribute() 寫入一條屬性。

6、使用 writeEndElement() 寫入一個結束元素。

7、最後使用 writeEndDocument() 關閉所有剩餘的打開開始元素並編寫換行符。


【結語】

說實話,QXmlStreamReader 和 QXmlStreamWriter 使用起來其實非常簡單。

然而,也因爲它適合構建遞歸下降解析器,所以會導致解析器可讀性很好,但實現起來較爲困難。

最後,如果有其他問題 & 錯誤,歡迎留言 / 評論 / 私信。

附上項目鏈接:最後,附上項目鏈接(多多star呀..⭐_⭐):

CSDN的:https://download.csdn.net/download/u011283226/12565089

Github的:https://github.com/mengps/TranslateEditor

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章