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

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