八、文件、流和 XML
对于跨平台的应用程序来说,处理文件是一个复杂的问题,因为即使是最基本的功能也会因平台而异。例如,Unix 系统使用斜杠(/
)作为路径中的分隔符,而 Windows 平台使用反斜杠(\
)。而这仅仅是开始;您还会遇到一系列令人不安的基本差异,比如不同的行尾和编码,当您试图让您的应用程序在多个平台上运行时,每种差异都会导致各种奇怪的问题。
为了克服这个问题,Qt 提供了一系列的类来处理路径、文件和流。Qt 还处理 XML 文件——一种以可移植方式组织内容的格式。
使用路径
QDir
类是 Qt 应用程序中处理路径和驱动器的关键。当指定一个QDir
对象的路径时,斜线(/
)被用作分隔符,并自动转换为当前平台上使用的任何分隔符。允许使用驱动器号,以冒号(:
)开头的路径被解释为对嵌入到应用程序中的资源的引用。
静态方法使得轻松导航文件系统成为可能。首先,QDir::current()
返回一个引用应用程序工作目录的QDir
。QDir::home()
返回用户主目录的QDir
。QDir::root()
返回根目录,QDir::temp()
返回临时文件的目录。QDir::drives()
返回一个QFileInfo
对象的QList
,代表所有可用驱动器的根。
注意 Unix 系统被认为只有一个驱动器/
,而 Windows 机器的驱动器空间可以配置成有几个驱动器。
对象用来保存文件和目录的信息。它有许多有用的方法,下面列出了其中一些:
isDir()
、isFile()
、isSymLink()
:如果文件信息对象表示目录、文件或符号链接(或 Windows 上的快捷方式),则返回true
。dir()
和absoluteDir()
:返回一个由文件信息对象表示的QDir
对象。dir
方法可以返回一个相对于当前目录的目录,而absoluteDir
返回一个以驱动器根目录开始的目录路径。exists()
:如果对象存在,返回true
。isHidden()
、isReadable()
、isWritable()
、isExecutable()
:返回文件状态信息。fileName()
:返回不带路径的文件名作为QString
。filePath()
:以QString
的形式返回包含路径的文件名。该路径可以相对于当前目录。absoluteFilePath()
:以QString
的形式返回包含路径的文件名。路径从驱动器根目录开始。completeBaseName()
和completeSuffix()
:返回保存文件名和文件名后缀(扩展名)的QString
对象。
让我们使用这些方法创建一个应用程序,列出所有驱动器和每个驱动器根目录下的文件夹。诀窍是使用QDir::drives
找到驱动器,然后找到每个驱动器的根目录(见清单 8-1 )。
清单 8-1。 用根目录列出驱动器
#include <QDir>
#include <QFileInfo>
#include <QtDebug>
int main( int argc, char **argv )
{
foreach( QFileInfo drive, QDir::drives() )
{
qDebug() << "Drive: " << drive.absolutePath();
QDir dir = drive.dir();
dir.setFilter( QDir::Dirs );
foreach( QFileInfo rootDirs, dir.entryInfoList() )
qDebug() << " " << rootDirs.fileName();
}
return 0;
}
QDir::drives
方法返回使用foreach
迭代的QFileInfo
对象的列表。通过qDebug
打印出驱动器的根路径后,使用dir
方法检索每个根的QDir
对象。
注意要在 Windows 环境中使用qDebug
,您必须将行CONFIG += console
添加到您的项目文件中。
QDir
对象的一个优点是它们可以用来获取目录列表。通过使用filter()
方法,您可以配置对象只返回目录。然后这些目录作为来自entryInfoList
方法的QFileInfo
对象的QList
返回。这些QFileInfo
对象代表目录,但是fileName
方法仍然返回目录名。isDir
和isFile
方法可以确认文件名是目录名还是文件名。如果您认为目录是包含对其内容的引用的文件,这就更容易理解了。
setFilter( Filters )
方法可用于根据许多不同的标准过滤出目录条目。您还可以组合过滤器标准来获得您想要的条目列表。支持以下值:
QDir::Dirs
:列出名称过滤器匹配的目录。
QDir::AllDirs
:列出所有目录(不应用名称过滤器)。
QDir::Files
:列出文件。
QDir::Drives
:列出驱动器。在 Unix 系统上它被忽略。
QDir::NoSymLinks
:不列出符号链接。在不支持符号链接的平台上,它被忽略。
QDir::NoDotAndDotDot
:不列出特殊条目.
和.
…
QDir::AllEntries
:列出目录、文件、驱动器和符号链接。
QDir::Readable
:列出可读文件。必须与Files
或Dirs
结合使用。
QDir::Writeable
:列出可写文件。必须与Files
或Dirs
结合使用。
QDir::Executable
:列出可执行文件。必须与Files
或Dirs
结合使用。
QDir::Modified
:列出已修改的文件。在 Unix 系统上它被忽略。
QDir::Hidden
:列出隐藏的文件。在 Unix 系统上,它列出了以.
开头的文件。
QDir::System
:列出系统文件。
QDir::CaseSensitive
:如果文件系统区分大小写,名称过滤器应该区分大小写。
filter
方法与setNameFilters()
方法相结合,后者采用文件名匹配模式的QStringList
,比如*.cpp
。请注意,名称过滤器是一个模式列表,因此可以用一个名称过滤器过滤*.cpp
、*.h
、*.qrc
、*.ui
和*.pro
文件。
使用文件
您可以使用QDir
查找文件,使用QFileInfo
查找关于文件的更多信息。为了更进一步,实际打开、读取、修改和创建文件,你必须使用QFile
类。
让我们通过查看清单 8-2 中的来开始查看QFile
。应用程序检查文件testfile.txt
是否存在。如果是,应用程序会尝试打开它进行写入。如果允许的话,它会再次关闭文件。在这个过程中,它使用qDebug
打印状态信息。
清单中突出显示的行显示了有趣的QFile
操作。首先,在构造器中设置文件名。可以使用setFileName(const QString&)
方法设置文件名,这使得重用QFile
对象成为可能。接下来,应用程序使用exists
方法来查看文件是否存在。
最后突出显示的一行试图打开文件进行写入,因为在 Qt 支持的所有平台上对文件进行写保护很容易。如果文件成功打开,open 方法返回true
。
清单的其余部分由输出调试消息和退出主函数的代码组成(使用return
)。如果文件打开成功,请确保在退出前关闭文件。
清单 8-2。 基本 QFile
操作
#include <QFile>
#include <QtDebug>
int main( int argc, char **argv )
{
QFile file( "testfile.txt" );
if( !file.exists() )
{
qDebug() << "The file" << file.fileName() << "does not exist.";
return −1;
}
if( !file.open( QIODevice::WriteOnly ) )
{
qDebug() << "Could not open" << file.fileName() << "for writing.";
return −1;
}
qDebug() << "The file opened.";
file.close();
return 0;
}
前面的清单打开文件进行写入。打开文件时,您可以使用其他标志来控制文件的读取和修改方式:
QIODevice::WriteOnly
:打开文件进行写入。QIODevice::ReadWrite
:打开文件进行读写。QIODevice::ReadOnly
:打开文件进行读取。
上述三个标志可以与以下标志结合使用,以详细控制文件访问模式:
QIODevice::Append
:将所有写入的数据追加到文件末尾。QIODevice::Truncate
:打开文件时清空文件。QIODevice::Text
:将文件作为文本文件打开。从文件中读取时,所有行尾都被翻译成\n
。当写入文件时,行尾被转换成适合目标平台的格式(例如,Windows 上的\r\n
和 Unix 上的\n
)。QIODevice::Unbuffered
:打开文件,没有任何缓冲。
您总是可以通过调用openMode()
方法来判断给定的QFile
对象使用的是哪种模式。它返回当前模式。对于关闭的文件,它返回QIODevice::NotOpen
。
使用流
打开文件后,使用 stream 类访问它会更方便。Qt 附带了两个流类:一个用于文本文件,一个用于二进制文件。通过打开一个流来访问一个文件,您可以使用重定向操作符(<<
和>>
)在文件中写入和读取数据。使用 streams,您还可以避开平台差异,比如字节顺序和不同的行尾策略。
文本流
使用文本流,您可以像在 C++ 标准库中一样连接一个文件——但是有所不同。奇怪的是,文件是以跨平台的方式处理的,这样当您在不同的计算机之间移动应用程序和文件时,行尾和其他类似的细节就不会弄乱结果。
要为文件创建文本流,创建一个QFile
对象,然后像往常一样打开它。建议您通过读写策略传递QIODevice::Text
标志。打开文件后,将指向文件对象的指针传递给一个QTextStream
对象的构造器。QTextStream
对象现在是进出文件的流,这取决于文件是如何打开的。
清单 8-3 显示了一个main
函数,它打开一个名为main.cpp
的文件作为文本阅读。如果文件打开成功,将创建一个文本流。在函数结束时,文件被关闭。
清单 8-3。 打开文本流阅读
int main( int argc, char **argv )
{
QFile file( "main.cpp" );
if( !file.open( QIODevice::ReadOnly | QIODevice::Text ) )
qFatal( "Could not open the file" );
QTextStream stream( &file );
...
file.close();
return 0;
}
清单 8-4 显示了一个简单的循环,用于前面清单中的main
函数。循环使用atEnd
来查看是否到达了文件的末尾。如果没有,使用>>
操作符从流中读取一个QString
,然后打印到调试控制台。
执行所示循环的结果看起来不像main.cpp
文件的内容。操作符>>
一直读到遇到第一个空格。所以线路#include <QFile>
会被分成#include
和<QFile>
。因为qDebug
在每次调用后添加了一个换行符,所以示例行将在调试控制台上打印两行。
清单 8-4。 从文本流中逐字阅读
while( !stream.atEnd() )
{
QString text;
stream >> text;
qDebug() << text;
}
解决方案是要么使用 stream 对象上的readAll()
方法读取整个文件,包括文本和换行符,要么逐行读取。使用readAll()
进行读取在大多数情况下是可行的,但是因为整个文件是一次性加载到内存中的,所以它很容易耗尽整个内存。
要逐行读取文件,使用readLine()
方法,一次读取一整行。清单 8-5 显示了先前清单中的循环,但是用readLine
代替。执行这个循环会在调试控制台上给出一个结果,显示出main.cpp
文件的内容。
清单 8-5。 从文本流中逐行读取
while( !stream.atEnd() )
{
QString text;
text = stream.readLine();
qDebug() << text;
}
数据流
有时你不能依赖于使用文本文件来存储数据。例如,您可能希望支持现有的非基于文本的文件格式,或者您可能希望生成较小的文件。通过以机器可读的二进制格式存储实际数据,而不是将其转换为人类可读的文本,您可以在保存和加载方法中节省文件大小和复杂性。
当需要读写二进制数据时,可以使用QDataStream
类。然而,在使用数据流时,有两件重要的事情需要记住:数据类型和版本控制。
对于数据类型,您必须确保对>>
操作符和<<
操作符使用完全相同的数据类型。在处理整数值时,最好使用qint8
、qint16
、qint32
或qint64
,而不是可以在平台间改变大小的short
、int
和long
数据类型。
第二个问题,版本控制,涉及到确保使用相同版本的 Qt 来读写数据,因为不同版本的 Qt 之间二进制数据的编码已经改变。为了避免这个问题,可以用setVersion(int)
方法设置QDataStream
的版本。如果你想使用 Qt 1.0 的数据流格式,将版本设置为QDataStream::Qt_1_0
。创建新格式时,建议使用尽可能高的版本(对于 Qt 4.2 应用,使用QDataStream::Qt_4_2
)。
所有基本的 C++ 类型和大多数 Qt 类型——比如QColor
、QList
、QString
、QRect
和QPixmap
——都可以通过数据流序列化。为了能够序列化你自己的类型,比如自定义的struct
,你需要为你的类型提供<<
和>>
操作符。清单 8-6 显示了ColorText
结构和它的重定向操作符。该结构用于保存字符串和颜色。
提示当一个对象或数据被序列化时,意味着该对象被转换成一系列适合流的数据。有时候这种转换是很自然的(比如一个字符串已经是一系列字符);在其他情况下,它需要一个转换操作(例如,一个树结构不能以自然的方式映射到一系列数据)。当需要转换时,必须设计一个序列化方案,定义如何序列化一个结构,以及如何从序列化的数据中恢复该结构。
在这个上下文中, type 表示任何类型——类、结构或联合。通过为这种类型提供<<
和>>
操作符,您可以在不需要任何特殊处理的情况下将该类型用于数据流。如果您查看清单中的流操作符,您会看到它们操作对一个QDataStream
对象和一个ColorText
对象的引用,并返回对一个QDataStream
对象的引用。这是您必须为希望能够序列化的所有自定义类型提供的接口。该实现基于使用现有的<<
和>>
操作符来序列化所讨论的类型。还要记住将数据放在流中的顺序与您计划读回数据的顺序相同。
如果您想为一种可变大小的类型编写流操作符——例如,一个类似字符串的类——您必须首先在您的<<
操作符中将字符串的长度发送给流,以了解您需要使用>>
操作符读回多少信息。
清单 8-6。ColorText
结构及其<<
和>>
运算符
struct ColorText
{
QString text;
QColor color;
};
QDataStream &operator<<( QDataStream &stream, const ColorText &data )
{
stream << data.text << data.color;
return stream;
}
QDataStream &operator>>( QDataStream &stream, ColorText &data )
{
stream >> data.text;
stream >> data.color;
return stream;
}
既然自定义类型ColorText
已经创建,让我们尝试序列化一系列ColorText
对象:一个QList<ColorText>
。清单 8-7 向你展示了如何做到这一点。首先,创建并填充一个列表对象。然后,在以与文本流相同的方式创建数据流之前,打开文件进行写入。最后一步是使用setVersion
来确保版本设置正确。当一切都设置好后,只需使用<<
操作符将列表发送到流中并关闭文件。所有的细节都是通过直接和间接调用QList
、ColorText
、QString
、QColor
的不同层次的<<
操作符整理出来的。
清单 8-7。 保存列表中的 ColorText
项
QList<ColorText> list;
ColorText data;
data.text = "Red";
data.color = Qt::red;
list << data;
...
QFile file( "test.dat" );
if( !file.open( QIODevice::WriteOnly ) )
return;
QDataStream stream( &file );
stream.setVersion( QDataStream::Qt_4_2 );
stream << list;
file.close();
将序列化的数据加载回来就像序列化它一样简单。只需创建正确类型的目标对象;在这种情况下,使用QList<ColorText>
。打开文件进行读取,然后创建数据流。确保数据流使用正确的版本,并使用>>
操作符从数据流中读取数据。
在清单 8-8 中,您可以看到数据是从一个文件中加载的,并且新加载的列表的内容是使用foreach
循环中的qDebug
转储到调试控制台的。
清单 8-8。 加载列表中的 ColorText
项
QList<ColorText> list;
QFile file( "test.dat" );
if( !file.open( QIODevice::ReadOnly ) )
return;
QDataStream stream( &file );
stream.setVersion( QDataStream::Qt_4_2 );
stream >> list;
file.close();
foreach( ColorText data, list )
qDebug() << data.text << "("
<< data.color.red() << ","
<< data.color.green() << ","
<< data.color.blue() << ")";
XML
XML 是一种元语言,使您能够在字符串或文本文件中存储结构化数据(XML 标准的细节超出了本书的范围)。XML 文件的基本构造块是标签、属性和文本。以清单 8-9 为例。document
标签包含author
标签和显示Some text
的文本。document
标签以开始标签<document>
开始,以结束标签</document>
结束。
清单 8-9。 一个非常简单的 XML 文件
<document name="DocName">
<author name="AuthorName" />
Some text
</document>
这两个标签都有一个名为name
的属性,值为DocName
和AuthorName
。一个标签可以有任意数量的属性,从无到无限。
author
标签没有内容,一次打开和关闭。写<author />
相当于写<author></author>
。
注意关于 XML,这些信息是你最起码需要知道的。这里展示的 XML 文件甚至不是一个真正的 XML 文件——它缺少文档类型定义。您甚至还没有开始学习 XML 的名称空间和其他有趣的细节。但是现在您已经知道了足够多的知识,可以开始使用 Qt 读写 XML 文件了。
Qt 支持两种处理 XML 文件的方式:DOM 和 SAX(在下面的小节中描述)。在开始之前,您需要知道 XML 支持是 Qt 模块QtXml
的一部分,这意味着您需要在项目文件中添加一行内容QT += xml
来包含它。
家
文档对象模型(DOM)的工作原理是将整个 XML 文档表示为内存中的节点对象树。尽管解析和修改文档很容易,但整个文件是一次性加载到内存中的。
创建 XML 文件
让我们从使用 DOM 类创建一个 XML 文件开始。为了使事情变得简单,我们的目标是创建清单 8-9 中的文档。该过程分为三个部分:创建节点、将节点放在一起,以及将文档写入文件。
第一步——创建节点——如清单 8-10 所示。XML 文件的不同构建块包括代表文档的QDomDocument
对象、代表标签的QDomElement
对象和代表document
标签中文本数据的QDomText
对象。
元素和文本对象不是使用构造器创建的。相反,你必须使用QDomDocument
对象的createElement( const QString&)
和createTextNode( const QString &)
方法。
清单 8-10。 为一个简单的 XML 文档创建节点
QDomDocument document;
QDomElement d = document.createElement( "document" );
d.setAttribute( "name", "DocName" );
QDomElement a = document.createElement( "author" );
a.setAttribute( "name", "AuthorName" );
QDomText text = document.createTextNode( "Some text" );
在清单 8-10 中创建的节点不以任何方式排序。它们可以被认为是独立的对象,即使它们都是用相同的文档对象创建的。
为了创建清单 8-9 中的所示的结构,必须使用appendChild( const QDomNode&)
方法将author
元素和文本放入文档元素中,如清单 8-11 中的所示。在清单中,您还可以看到,document
标签以同样的方式附加到文档中。它构建了相同的树结构,正如在您试图创建的文件中可以看到的那样。
清单 8-11。 将 DOM 树中的节点放在一起
document.appendChild( d );
d.appendChild( a );
d.appendChild( text );
最后一步是打开一个文件,打开一个流,输出 DOM 树,这就是清单 8-12 中发生的事情。DOM 树表示的 XML 字符串是通过调用相关的QDomDocument
对象上的toString(int)
来检索的。
清单 8-12。 将一个 DOM 文档写入一个文件
QFile file( "simple.xml" );
if( !file.open( QIODevice::WriteOnly | QIODevice::Text ) )
{
qDebug( "Failed to open file for writing." );
return −1;
}
QTextStream stream( &file );
stream << document.toString();
file.close();
加载 XML 文件
知道如何创建 DOM 树只是通过 DOM 树使用 XML 所需知识的一半。您还需要知道如何将 XML 文件读入到QDomDocument
中,以及如何找到文档中包含的元素和文本。
这比你想象的要容易得多。清单 8-13 显示了从文件中获取一个QDomDocument
对象的所有代码。只需打开文件进行读取,并尝试在调用合适的文档对象的setContent
成员时使用该文件。如果它返回true
,那么可以从 DOM 树中获得 XML 数据。如果不是,则 XML 文件无效。
清单 8-13。 从文件中获取 DOM 树
QFile file( "simple.xml" );
if( !file.open( QIODevice::ReadOnly | QIODevice::Text ) )
{
qDebug( "Failed to open file for reading." );
return −1;
}
QDomDocument document;
if( !document.setContent( &file ) )
{
qDebug( "Failed to parse the file into a DOM tree." );
file.close();
return −1;
}
file.close();
可以使用documentElement()
方法从文档对象中检索 DOM 树的根元素。给定该元素,很容易找到子节点。清单 8-14 展示了如何使用firstChild()
和nextSibling()
来遍历文档元素的子元素。
子对象作为QDomNode
对象返回,即QDomElement
和QDomText
的基类。您可以通过使用isElement()
和isText()
方法来判断您正在处理什么类型的节点。还有更多类型的节点,但文本和元素节点是最常用的。
您可以使用toElement()
方法将QDomNode
转换成QDomElement
。toText()
方法做同样的事情,但是返回一个QDomText
。然后使用从QDomCharacterData
继承的data()
方法获得实际的文本。
对于 element 对象,可以从tagName()
方法中获得标签的名称。可以使用attribute(const QString &, const QString &)
方法查询属性。它接受属性的名称和一个默认值。在清单 8-14 中,默认值为“未设置”
清单 8-14。 从 DOM 树中查找数据
QDomElement documentElement = document.documentElement();
QDomNode node = documentElement.firstChild();
while( !node.isNull() )
{
if( node.isElement() )
{
QDomElement element = node.toElement();
qDebug() << "ELEMENT" << element.tagName();
qDebug() << "ELEMENT ATTRIBUTE NAME"
<< element.attribute( "name", "not set" );
}
if( node.isText() )
{
QDomText text = node.toText();
qDebug() << text.data();
}
node = node.nextSibling();
}
清单 8-14 简单地列出了根节点的子节点。如果希望能够遍历更多层次的 DOM 树,就必须使用递归函数来查找遇到的所有元素节点的子节点。
修改 XML 文件
在许多应用程序中,能够读写 DOM 树是您需要知道的全部内容。将应用程序的数据保存在自定义结构中,在保存之前将数据转换成 DOM 树,然后在加载时从 DOM 树中提取数据,这通常就足够了。当 DOM 树结构足够接近你的应用程序的内部结构时,能够动态地修改 DOM 树是很好的,这就是清单 8-15 中发生的事情。
要将清单中的代码放到一个上下文中,您需要知道在运行这段代码之前,已经从一个文件中加载了文档。执行完代码后,文档被写回同一个文件。
您使用documentElement
找到根节点,这给了您一个起点。然后使用elementsByTagName(const QString &)
方法向根节点请求所有author
标签的列表(所有tagName
属性设置为author
的元素)。
如果列表为空,则向根节点添加一个 author 元素。使用insertBefore(const QDomNode &, const QDomNode &)
将新创建的元素添加到根节点。因为您给了一个无效的QDomNode
对象作为该方法的第二个参数,所以该元素作为第一个子节点被插入。
如果列表包含作者元素,您可以向其中添加修订元素。revision 元素有一个名为count
的属性,它的值是根据 author 元素中已经存在的 revision 元素的数量计算出来的。
这就够了。因为节点已经被添加到 DOM 树中,所以只需要再次保存它就可以获得更新的 XML 文件。
清单 8-15。 修改现有的 DOM 树
QDomNodeList elements = documentElement.elementsByTagName( "author" );
if( elements.isEmpty() )
{
QDomElement a = document.createElement( "author" );
documentElement.insertBefore( a, QDomNode() );
}
else if( elements.size() == 1 )
{
QDomElement a = elements.at(0).toElement();
QDomElement r = document.createElement( "revision" );
r.setAttribute( "count",
QString::number(
a.elementsByTagName( "revision" ).size() + 1 ) );
a.appendChild( r );
}
用 SAX 读取 XML 文件
XML 的简单 API(SAX)只能用于读取 XML 文件。它通过读取文件并定位开始标签、结束标签、属性和文本来工作;和调用处理程序对象中的函数来处理 XML 文档的不同部分。与使用 DOM 文档相比,这种方法的好处是不必一次将整个文件加载到内存中。
要使用 SAX,需要使用三个类:QXmlInputSource
、QXmlSimpleReader
和一个处理程序。清单 8-16 展示了一个应用程序的main
函数,它使用 SAX 来解析一个文件。QXmlInputSource
用于在QFile
和QXmlSimpleReader
对象之间提供一个预定义的接口。
QXmlSimpleReader
是QXmlReader
类的特殊版本。简单的阅读器足够强大,可以在几乎所有情况下使用。阅读器有一个使用setContentHandler
方法分配的内容处理程序。内容处理器必须继承QXmlContentHandler
,这正是MyHandler
类所做的。设置好一切之后,只需要调用parse(const QXmlInputSource *, bool)
方法,将 XML 输入源对象作为参数传递,然后等待阅读器向处理程序报告所有值得知道的事情。
清单 8-16。 用自定义处理程序类设置 SAX 阅读器
int main( int argc, char **argv )
{
QFile file( "simple.xml" );
if( !file.open( QIODevice::ReadOnly | QIODevice::Text ) )
{
qDebug( "Failed to open file for reading." );
return −1;
}
QXmlInputSource source( &file );
MyHandler handler;
QXmlSimpleReader reader;
reader.setContentHandler( &handler );
reader.parse( source );
file.close();
return 0;
}
处理程序类MyHandler
的声明可以在清单 8-17 中看到。该类从QXmlDefaultHandler
继承而来,后者从QXmlContentHandler
派生而来。继承QXmlDefaultHandler
的好处是默认的处理程序类提供了所有方法的虚拟实现,否则你将不得不实现为存根。
当遇到问题时,读取器会调用 handler 类中的方法。您希望处理文本和标记,并知道解析过程何时开始和结束,因此已经实现了类声明中显示的方法。所有方法都返回一个bool
值,用于在遇到错误时停止解析。所有方法都必须返回true
以便读者继续阅读。
**清单 8-17。**T3T0SAX 处理程序类
class MyHandler : public QXmlDefaultHandler
{
public:
bool startDocument();
bool endDocument();
bool startElement( const QString &namespaceURI,
const QString &localName,
const QString &qName,
const QXmlAttributes &atts );
bool endElement( const QString &namespaceURI,
const QString &localName,
const QString &qName );
bool characters( const QString &ch );
};
除了startElement
之外的所有方法看起来或多或少都像清单 8-18 中的所示的方法。一个简单的文本被打印到调试控制台,然后返回true
。在endElement
(如清单所示)的情况下,也会打印一个参数。
清单 8-18。 一个简单的处理类方法
bool MyHandler::endElement( const QString &namespaceURI, const QString &localName,
const QString &qName )
{
qDebug() << "End of element" << qName;
return true;
}
清单 8-19 中的startElement
方法稍微复杂一些。首先,打印元素的名称;然后打印通过一个QXmlAttributes
对象传递的属性列表。QXmlAttributes
不是一个标准容器,所以您必须使用一个索引变量来遍历它,而不仅仅是使用foreach
宏。在方法结束之前,您返回true
来告诉读者一切都在按预期运行。
清单 8-19。startElement
方法列出了元素的属性。
bool MyHandler::startElement( const QString &namespaceURI, const QString &localName,
const QString &qName, const QXmlAttributes &atts )
{
qDebug() << "Start of element" << qName;
for( int i=0; i<atts.length(); ++i )
qDebug() << " " << atts.qName(i) << "=" << atts.value(i);
return true;
}
打印qName
而不是namespaceURI
或localName
的原因是qName
是您期望的标记名。名称空间和本地名称超出了本书的范围。
通过实现 SAX 处理程序来构建 XML 解析器并不复杂。一旦想要将 XML 数据转换成应用程序的定制数据,就应该考虑使用 SAX。因为不会一次加载整个文档,所以降低了应用程序的内存需求,这可能意味着您的应用程序运行得更快。
文件和主窗口
你在第四章中已经了解到,使用isSafeToClose
和closeEvent
方法的设置是一个很好的起点,可以让用户在关闭修改过文档的窗口时选择保存文件。现在是时候为 SDI 应用程序添加对该功能的支持了(同样的概念也适用于 MDI 应用程序)。
从清单 8-20 的开始,您可以看到对SdiWindow
类声明的更改。添加突出显示的行是为了处理加载和保存功能。
所做的更改是将菜单项“打开”、“保存”和“另存为”添加到“文件”菜单中。对类声明的修改包括四个部分:处理菜单项的动作、动作的槽、加载和保存文档到实际文件的函数loadFile
和saveFile
,以及保存当前文件名的私有变量currentFilename
。所有与保存文档有关的方法都返回一个bool
值,告诉调用者文档是否被保存。
清单 8-20。 对 SdiWindow
类进行了修改,使其能够加载和保存文件
class SdiWindow : public QMainWindow
{
Q_OBJECT
public:
SdiWindow( QWidget *parent = 0 );
protected:
void closeEvent( QCloseEvent *event );
private slots:
void fileNew();
void helpAbout();
void fileOpen();
bool fileSave();
bool fileSaveAs();
private:
void createActions();
void createMenus();
void createToolbars();
bool isSafeToClose();
bool saveFile( const QString &filename );
void loadFile( const QString &filename );
QString currentFilename;
QTextEdit *docWidget;
QAction *newAction;
QAction *openAction;
QAction *saveAction;
QAction *saveAsAction;
QAction *closeAction;
QAction *exitAction;
QAction *cutAction;
QAction *copyAction;
QAction *pasteAction;
QAction *aboutAction;
QAction *aboutQtAction;
};
创建操作,然后将它们添加到适当的菜单中,其方式与现有操作完全相同。与打开动作相关的fileOpen
方法如清单 8-21 中的所示。它使用来自QFileDialog
类的静态getOpenFileName
方法来获取文件名。如果用户没有选择文件就关闭了对话框,结果字符串的isNull
方法返回true
。在这种情况下,您不打开文件就从插槽返回。
如果检索到实际的文件名,您可以尝试使用loadFile
加载文件。但是,如果当前文档未被赋予文件名且未被更改,则该文件将被加载到当前文档中。如果当前文档有文件名或已被修改,则创建一个新的SdiWindow
实例,然后将文件加载到其中。
所有SdiWindows
在保存或加载时都有文件名,因此只有新文件没有有效的文件名。
清单 8-21。 实现插槽连接打开动作
void SdiWindow::fileOpen()
{
QString filename = QFileDialog::getOpenFileName( this );
if( filename.isEmpty() )
return;
if( currentFilename.isEmpty() && !docWidget->document()->isModified() )
loadFile( filename );
else
{
SdiWindow *window = new SdiWindow();
window->loadFile( filename );
window->show();
}
}
loadFile(const QString&)
方法用于将给定文件的内容加载到当前窗口的文档中。该方法的源代码如清单 8-22 所示。该函数试图打开文件。如果文件无法打开,将向用户显示一个消息框。如果文件被打开,会创建一个QTextStream
,并使用readAll
加载整个文件内容。然后用setPlainText
方法给文档分配新的文本。当文档被更新后,currentFilename
变量被更新,修改标志被设置为false
,窗口标题也被更新。
清单 8-22。 源代码实际加载文件内容到文档中
void SdiWindow::loadFile( const QString &filename )
{
QFile file( filename );
if( !file.open( QIODevice::ReadOnly | QIODevice::Text ) )
{
QMessageBox::warning( this, tr("SDI"), tr("Failed to open file.") );
return;
}
QTextStream stream( &file );
docWidget->setPlainText( stream.readAll() );
currentFilename = filename;
docWidget->document()->setModified( false );
setWindowTitle( tr("%1[*] - %2" ).arg(filename).arg(tr("SDI")) );
}
loadFile
相反的方法是saveFile(const QString &)
。(你可以在清单 8-23 中看到它的实现。)尽管任务不同,但这两个函数的实现看起来非常相似。概念是相同的:尝试打开文件,以纯文本形式将文档发送到流并更新currentFilename
,重置修改位,并更新窗口标题。当一个文件被实际保存时,saveFile
函数返回true
;如果文件没有保存,函数返回false
。
清单 8-23。 将文档保存到文件中的源代码
bool SdiWindow::saveFile( const QString &filename )
{
QFile file( filename );
if( !file.open( QIODevice::WriteOnly | QIODevice::Text ) )
{
QMessageBox::warning( this, tr("SDI"), tr("Failed to save file.") );
return false;
}
QTextStream stream( &file );
stream << docWidget->toPlainText();
currentFilename = filename;
docWidget->document()->setModified( false );
setWindowTitle( tr("%1[*] - %2" ).arg(filename).arg(tr("SDI")) );
return true;
}
在清单 8-24 所示的fileSaveAs
方法的实现中使用了saveFile
方法的返回值。“另存为”插槽看起来非常像开放插槽。它使用getSaveFileName
方法要求用户输入新的文件名。如果选择了一个文件名,则调用saveFile
方法尝试保存文档。
请注意,如果取消文件对话框,将返回false
,当试图保存文档时,将返回来自saveFile
方法的返回值。只有当文档实际上已经被写入文件时,saveFile
才会返回true
。
清单 8-24。 另存为动作的源代码
bool SdiWindow::fileSaveAs()
{
QString filename =
QFileDialog::getSaveFileName( this, tr("Save As"), currentFilename );
if( filename.isEmpty() )
return false;
return saveFile( filename );
}
fileSave
方法试图将文档保存到与之前相同的文件——保存在currentFilename
中的名称。如果当前文件名为空,则该文件还没有被赋予文件名。在这种情况下,调用fileSaveAs
方法,向用户显示一个文件对话框来选择文件名。它在清单 8-25 的中显示为源代码。
fileSave
方法从saveFile
或fileSaveAs
返回返回值,这取决于使用哪种方法保存文件。
清单 8-25。 保存动作的源代码
bool SdiWindow::fileSave()
{
if( currentFilename.isEmpty() )
return fileSaveAs();
else
return saveFile( currentFilename );
}
使对话框按预期运行所需的最后一个选项是,当关闭修改过的文档时,让用户从显示的警告对话框中保存文件。清单 8-26 中的显示了isSafeToClose
方法的新实现,其中突出显示了包含实际变更的行。
第一个变化是使用QMessageBox::Save
枚举值在警告对话框中添加了保存选项。另一个变化是处理 Save 按钮的情况。如果按下按钮,则呼叫fileSave
。如果文件没有保存(即返回false
,close 事件中止。这使得用户不可能在没有实际选择的情况下丢失文档(或者经历某种电源故障)。
清单 8-26。 检查是否关闭文档的源代码
bool SdiWindow::isSafeToClose()
{
if( isWindowModified() )
{
switch( QMessageBox::warning( this, tr("SDI"),
tr("The document has unsaved changes.\n"
"Do you want to save it before it is closed?"),
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel ) )
{
case QMessageBox::Cancel:
return false;
case QMessageBox::Save:
return fileSave();
default:
return true;
}
}
return true;
}
添加这些保存和加载功能非常适合前面介绍的 SDI 结构。通过确认文档确实已经保存(通过使用所有相关方法的返回值),您可以建立一个防水保护,使得在没有确认的情况下无法关闭未保存的文档。
总结
在不同平台上使用文件通常意味着麻烦。不兼容性存在于所有级别:文件名、目录路径、换行符、字节序等等。通过使用QDir
和QFileInfo
类,可以避免路径、驱动器和文件名的问题。
找到文件后,您可以使用QFile
打开它。Qt 有流来读写数据。如果使用QTextStream
类,可以轻松处理文本文件;如果你使用QDataStream
类,从二进制文件中序列化和读回你的数据是很容易的。想想潜在的流版本控制问题。即使您对所有的应用程序部署使用相同的 Qt 版本,您将来也会得到更多的版本。一个简单的setVersion
电话可以挽救几天的沮丧。
将数据存储为文本或自定义二进制格式的另一种方法是使用 XML。Qt 使您能够使用 DOM,它允许您将整个 XML 文档读入内存,修改它,然后将其写回文件。如果您想读取一个 XML 文件,而不必一次全部加载,可以使用 Qt 的 SAX 类。
当您使用 XML 时,您需要将行QT += xml
添加到项目文件中,因为 XML 支持是在一个单独的模块中实现的。这个模块并不包含在 Qt 的所有版本中,所以在尝试使用它之前,请确认您可以访问它。
最后,您看到了 SDI 应用程序缺失的部分。添加本章最后一节中介绍的方法,可以轻松构建支持文件加载和保存的应用程序。**
九、提供帮助
有时用户需要帮助。有了 Qt,您可以通过各种方式给他们提供他们正在寻找的指令:向导、工具提示、状态栏消息和指向产品文档的指针等等。
当考虑如何在应用程序中添加与帮助相关的功能时,请记住,这不仅仅是简单地响应 F1 键(显示应用程序帮助窗口的实际机制)。当辅助成为你整个应用程序不可或缺的一部分时,它是最有效的。
通过使用一个好的设计,清楚地反映用户当前正在做什么以及他们在这个过程中的位置,你可以极大地减少对帮助的需求。一些工具和原则包括为复杂的设置提供向导,避免或清楚地指示不同的工作模式,如插入和覆盖,以及在用户将要做一些可能破坏大量信息的事情时提醒用户。
提供大量的帮助并不能让应用程序变得容易使用;过多的帮助会使用户很难找到他们想要的信息。你需要实现的是一个易于使用的整体:相关帮助和清晰设计的结合。这就是为什么使用您的应用程序是一种乐趣。
创建工具提示
向用户添加一些额外指导的最常见的方法之一是提供工具提示,这是包含信息的小标志(见图 9-1 )。当您将鼠标指针悬停在控件上一小段时间时,它们就会出现。
图 9-1。 分组框的对话框和工具提示
可以使用setTooltip(const QString&)
方法为所有小部件分配一个工具提示,该方法接受一个字符串,该字符串可以是纯文本,也可以是使用 HTML 格式化的。为了演示工具提示,我用一些小部件组合了一个QDialog
类。清单 9-1 展示了用于设置窗口小部件和布局的构造器(参见图 9-1 查看结果)。
清单 9-1。 对话框构造器
ToolTipDialog::ToolTipDialog() : QDialog()
{
QGroupBox *groupBox = new QGroupBox( tr("Group") );
QGridLayout *gbLayout = new QGridLayout( groupBox );
QCheckBox *checkBox = new QCheckBox( tr("Check!") );
QLabel *label = new QLabel( tr("label") );
QPushButton *pushButton = new QPushButton( tr("Push me!") );
gbLayout->addWidget( checkBox, 0, 0 );
gbLayout->addWidget( label, 0, 1 );
gbLayout->addWidget( pushButton, 1, 0, 1, 2 );
QGridLayout *dlgLayout = new QGridLayout( this );
dlgLayout->addWidget( groupBox, 0, 0 );
...
}
在清单 9-2 中,设置了复选框和分组框的工具提示。复选框只有一行,而分组框文本使用标准换行符\n
分成三行。当鼠标指针悬停在分组框中包含的小部件周围和之间时,分组框工具提示会显示出来。如果您将鼠标悬停在标签、复选框或按钮上,则会显示它们各自的工具提示。
清单 9-2。 设置简单的工具提示文本
checkBox->setToolTip( tr("This is a simple tool tip for the check box.") ); groupBox->setToolTip( tr("This is a group box tool tip.\n" "Notice that it appears between " "and around the contained widgets.\n" "It is also spanning several lines.") );
提示将一个字符串断开多行不影响结果。从 C++ 编译器的角度来看,字符串"foo"
—换行符— "bar"
与字符串"foobar"
是相同的。有时,能够分解一行是很方便的,因为它可以用来增加可读性,或者只是在打印时将代码放在纸上。
创建 HTML 格式的工具提示
虽然可以用<br />
HTML 标签表示新行,但是 Qt 实际上支持许多 HTML 标签,这些标签可以使格式化工具提示更加容易。清单 9-3 展示了一些可能的格式。产生的工具提示如图 9-2 中的所示。
清单 9-3。 一个 HTML 格式的工具提示
label->setToolTip( tr("<p> It is possible to do lists.</p>" "<ul>" "<li>You can <i>format</i> text.</li>" "<li><b>Bold</b> is possible too.</li>" "<li>And the <font color='#22aaff'>color</font> and " "<font size='+2'>size</font>.</li>" "</ul>" "<p>You can do ordered lists as well.</p>" "<ol>" "<li>First.</li>" "<li>Second.</li>" "<li>Third.</li>" "</ol>") );
图 9-2。 带有列表和格式的工具提示
以下列表解释了可用于设置工具提示格式的最常见标签:
<p>
…</p>
:这个标签用来括起一个段落。段落上下有一定间距,将它们与文本的其他部分分开。<br />
:这个标签代表一个换行符。如果你决定使用 HTML 标签,<br />
可以,但是\n
不行。\n
系统只适用于没有标签的文本。<i>
…</i>
:内附文字显示为斜体。<b>
…</b>
:内附文字显示为粗体。<font color='``nnn
…</font>
:内附文字以指定颜色nnn
显示。颜色可以表示为颜色名称(如红色、绿色、黑色或白色)或前缀为#
的十六进制值。格式为#rrggbb
,其中rr
为红色值,gg
为绿色值,bb
为蓝色值。<font size=
’nnn``'>
…</font>
:内附文字以备选尺寸显示。nnn
部分可以是以+
或−
为前缀的相对大小,也可以是固定大小(整数值)。<ul>
…</ul>
:包含以项目符号为前缀的列表项。<ol>
…</ol>
:包含以数字为前缀的列表项。<li>
…</li>
:包含的文本被视为列表项。
将图像插入工具提示
另一个非常有用的标签是img
标签,用于将文件或资源中的图像插入到文本中。图 9-3 显示了一个工具提示的例子。标签的语法看起来像<img src='
nnn
'>
,其中nnn
是文件名。如果文件名以:
开头,则是指嵌入到可执行文件中的资源。清单 9-4 展示了创建图 9-3 中的示例工具提示的源代码。
图 9-3。 带有文本和图像的工具提示
清单 9-4。 包含图像的工具提示
pushButton-> setToolTip( tr("<img src=':/img/qt.png'>"
"You can also insert images into your tool tips.") );
很容易为所有的小部件提供工具提示,从而给用户提供他们需要的支持。工具提示通常用于回答诸如“这个按钮是做什么的?”以及“隐藏标尺按钮去了哪里?”当你设计一个工具提示时,尽量保持文本最少,因为这些提示经常被用来快速理解各种界面小部件。
对小工具应用多个工具提示
有时候,您会希望为一个小部件分配几个工具提示——通常是在处理模型视图和其他显示复杂文档的小部件时。在这些情况下,单个小部件用于显示几个不同的项目,其中每个项目可能需要自己的工具提示。例如,假设您有一个绘图应用程序,其中您想要使用工具提示来显示圆的直径以及矩形的宽度和高度。因为整个绘图是使用单个查看小部件显示的,所以该小部件需要根据鼠标指针的位置提供不同的工具提示。
这样做有助于理解工具提示是如何显示的。工具提示的实际出现是通过ToolTip
事件触发的。通过拦截event(QEvent*)
方法中的事件,您可以根据鼠标指针的位置来更改工具提示。
图 9-4 显示了想要的效果:四个方块都是一个小部件的一部分,但是每个方块显示不同的工具提示文本。
注意当与一个QGraphicsView
和朋友一起工作时,你可以为每个QGraphicsItem
设置工具提示——避免需要为视图小部件或场景拦截ToolTip
事件。当使用项目视图时,您可以使用模型-视图架构,通过将数据分配给Qt::ToolTipRole
来为每个项目设置工具提示。如果你想为视图提供自定义工具提示,重新实现viewportEvent(QEvent*)
方法而不是event()
。
图 9-4。 同一个小工具对不同的部分显示不同的工具提示。
让我们从截取正确的事件开始,并为四个方块中的每一个设置工具提示文本。所有事件都通过event
方法,然后其中一些被分配给不同的处理程序,比如paintEvent
、mouseMoveEvent
和keyPressEvent
方法。因为没有toolTipEvent
方法,你必须在event
方法中拦截事件。
清单 9-5 中显示了拦截的源代码。因为event
方法接收一个QEvent
对象,所以您必须使用 type 属性来确定是否接收到了一个ToolTip
事件。QEvent
类是所有专用事件类的基类,所以一旦你知道你正在处理一个工具提示,你就可以将QEvent
对象转换成QHelpEvent
对象。
注意你怎么知道ToolTip
事件是作为QHelpEvent
对象发送的?看看enum QEvent::Type
的文档;您将看到所有事件类型的列表,以及沿着这样一个事件传递的对象类型。
在事件对象被转换成一个QHelpEvent
对象后,四个区域的矩形被设置。然后根据哪个矩形包含由QHelpEvent
对象的pos()
方法返回的点来设置工具提示。
设置工具提示文本后,不要将事件标记为已接受。相反,通过调用父处理程序QWidget::event
,调用默认处理程序(因为它知道如何显示实际的工具提示)。这也是所有非ToolTip
事件发生的地方——确保一切按预期运行。
清单 9-5。 拦截所有 ToolTip
事件,并更新工具提示文本,然后将其传递给默认处理程序
bool TipZones::event( QEvent *event )
{
if( event->type() == QEvent::ToolTip )
{
QHelpEvent *helpEvent = static_cast<QHelpEvent*>( event );
QRect redRect, greenRect, blueRect, yellowRect;
redRect = QRect( 0, 0, width()/2, height()/2 );
greenRect = QRect( width()/2, 0, width()/2, height()/2 );
blueRect = QRect( 0, height()/2, width()/2, height()/2 );
yellowRect = QRect( width()/2, height()/2, width()/2, height()/2 );
if( redRect.contains( helpEvent->pos() ) )
setToolTip( tr("Red") );
else if( greenRect.contains( helpEvent->pos() ) )
setToolTip( tr("Green") );
else if( blueRect.contains( helpEvent->pos() ) )
setToolTip( tr("Blue") );
else
setToolTip( tr("Yellow") );
}
return QWidget::event( event );
}
提供这有什么帮助提示
这是什么帮助看起来很像工具提示,只是用户调用了这是什么模式,然后单击了感兴趣的小部件。如果任何小部件有“这是什么”帮助,则通过单击对话框标题栏上出现的问号按钮可以进入“这是什么”模式。在图 9-5 中可以看到问号按钮。
图 9-5。 一个标题栏带有问号按钮的对话框
“这是什么”帮助文本往往比工具提示文本稍长一些,也更详细一些,因为用户通常想知道更多关于小部件的信息。
使用setWhatsThis(const QString&)
方法设置这是什么文本,并且可以为所有小部件设置。尽管作为参数传递的字符串与作为工具提示传递的字符串非常相似,但还是有一些区别。
最重要的区别是换行符。当指定这是什么文本时,重要的是使用<br />
标签,而不是\n
字符来换行。此外,“这是什么”文本总是自动换行的,除非您明确指定不换行的段落。图 9-6 显示了带和不带自动换行的文本是什么。
为了避免换行,你必须将文本放在一个带有属性style='white-space:pre'
的段落标签中。例如,下面一行显示了图中自动换行的文本:
checkBox-> setWhatsThis( tr("This is a simple <i>What's This help</i> "
"for the check box.") );
这段源代码显示了没有换行的相同文本:
checkBox->setWhatsThis( tr("<p style='white-space:pre'>This is a simple "
"<i>What's This help</i> for the check box.</p>") );
有时防止换行是有用的,但是尽可能让 Qt 处理它。通过让 Qt 换行,文本更有可能在屏幕上正确显示。以具有非常大的字体大小设置的低分辨率屏幕为例(见图 9-6 )。您的非换行文本可能不适合屏幕。
**图 9-6。** *同样的有无换行的这是什么文字*
说到格式,这个帮助文本可以处理工具提示文本可以处理的所有标签。图 9-7 显示了演示格式和内嵌图像的“这是什么”帮助框。虽然自动换行略有不同,但结果与工具提示框相同。
**图 9-7。** *这是什么帮助项处理与工具提示文本相同的格式。*
将链接嵌入这是什么帮助提示
尽管这是什么文本通常比工具提示文本更详细,有时甚至扩展的文本余量也不够。在这些情况下,能够在文本中放置一个超链接是很有用的。该链接可以指向您喜欢的任何内容,例如,一个对话框、联机帮助中的一个部分或 Web 上的一个页面。
当点击“这是什么”文本中的链接时,一个`WhatsThisClicked`事件被发送到与“这是什么”帮助提示相关的小部件。这个事件可以在`event`方法中被拦截,就像在为小部件的不同部分提供不同提示时拦截`ToolTip`事件一样。然而,因为可能会有许多包含链接的“这是什么帮助”对话框,所以一个好的解决方案是在一个地方拦截所有的`WhatsThisClicked`事件。这个过程使您能够使用相同的机制以相同的方式处理所有链接。可以使用事件过滤器来执行事件接收。
这个想法是有一个事件过滤器,可以安装在所有提供这是什么帮助的对话框上。然后,每当单击一个链接时,filter 对象就会发出一个信号。该信号可以连接到执行适当动作(例如打开帮助页面)的中心点。
清单 9-6 显示了`LinkFilter`过滤器类的类声明。它提供了一个单击链接时发出的信号、一个构造器和`eventFilter`方法。构造器简单地将`parent`指针传递给`QObject`构造器来让 Qt 满意。
**清单 9-6。** *事件过滤类的声明*
#ifndef LINKFILTER_H
#define LINKFILTER_H
#include <QObject>
class LinkFilter : public QObject
{
Q_OBJECT
public:
LinkFilter( QObject *parent=0 );
signals:
void linkClicked( const QString &);
protected:
bool eventFilter( QObject*, QEvent* );
};
#endif // LINKFILTER_H
实际的过滤发生在清单 9-7 的中。处理所有类型为`WhatsThisClicked`的事件。`QEvent`对象被转换成一个`QWhatsThisClickedEvent`对象,通过`linkClicked`信号从该对象发出`href`属性。确保在发出信号和采取任何行动之前调用隐藏“这是什么”框的`QWhatsThis::hideText`方法。
最后,已处理的事件返回`true`,阻止任何进一步的事件处理。所有其他事件返回`false`——通知 Qt 该事件被忽略。
**清单 9-7。** *过滤事件为* `WhatsThisClicked` *事件*
bool LinkFilter::eventFilter( QObject *object, QEvent *event )
{
if( event->type() == QEvent::WhatsThisClicked )
{
QWhatsThisClickedEvent wtcEvent = static_cast<QWhatsThisClickedEvent>(event);
QWhatsThis::hideText();
emit linkClicked( wtcEvent->href() );
return true;
}
return false;
}
为了测试`LinkFilter`类,创建了一个简单的对话框类`LinkDialog`,该对话框有一个构造器和一个槽:`showLink(const QString&)`。(清单 9-8 显示了对话框的构造器。)
首先创建并安装一个`LinkFilter`作为对话框的事件过滤器。`linkClicked`信号连接到对话框的`showLink`插槽。请注意,`WhatsThisClicked`事件是通过对话框传递的,因此您可以在这里拦截对话框中所有小部件的点击链接。因为过滤器安装在对话框上,所以可以在显示对话框之前从主窗口安装过滤器。
安装过滤器后,会创建一个`QPushButton`小部件,并设置这是什么文本。要创建一个链接,`<a href='*nnn*'>`...`</a>`使用标记。`nnn`部分是作为`QWhatsThisClickedEvent`的`href`属性传递的字符串,然后通过`linkClicked`信号传递。`<a href=...>`和`</a>`部分之间的文本将显示为链接。
在构造器结束之前,按钮被放置在布局中。
**清单 9-8。** *用* `LinkFilter` *事件过滤器*设置一个对话框
LinkDialog::LinkDialog() : QDialog()
{
LinkFilter *filter = new LinkFilter( this );
this->installEventFilter( filter );
connect( filter, SIGNAL(linkClicked(const QString&)),
this, SLOT(showLink(const QString&)) );
QPushButton *button = new QPushButton( “What is this?” );
button->setWhatsThis( “This is a test link.” );
QGridLayout *layout = new QGridLayout( this );
layout->addWidget( button, 0, 0 );
}
图 9-8 显示了*这是什么*文本和正在显示的链接。当用户点击链接时,触发`QWhatsThisClickedEvent`,发出`linkClicked`信号,触发`showLink`槽。该插槽的源代码如清单 9-9 所示。
**图 9-8。** *这个带链接的文字是什么*
**清单 9-9。** *使用消息框显示点击的链接*
void LinkDialog::showLink( const QString &link )
{
QMessageBox::information( this, tr(“Link Clicked”), tr(“Link: %1”).arg( link ) );
}
插槽所做的只是显示一个带有链接字符串的消息框(见图 9-9 )。在这里,您可以添加代码来解释给定的字符串,然后采取适当的行动,而不只是显示一个消息框。
**图 9-9。** *显示链接文本的对话框*
利用状态栏
状态栏通常位于应用程序窗口的底部,通常用于显示临时消息以及关于工作模式、在当前文档中的位置、当前文件的大小等信息。显示的信息非常依赖于应用程序类型,但它是对用户有用的信息。
状态栏由一个`QStatusBar`小部件表示。当你在主窗口中使用状态栏时,你可以通过`statusBar()`方法获得对状态栏对象的引用。第一次调用该方法时会创建一个状态栏,而连续调用只会返回一个指向该状态栏的指针。
状态栏最常见的用途是显示诸如`"Loading"`、`"Saving"`、`"Ready"`、`"Done"`等消息。这些信息使用`showMessage(const QString&, int)`方法显示。例如,下面一行显示两秒钟的消息文本`"Ready"`(见图 9-10 ):
statusBar->showMessage( tr(“Ready”), 2000 );
图 9-10。 一个状态栏显示一条临时消息
给showMessage
的时间是以毫秒为单位指定的(以秒为单位的时间乘以 1000 得到以毫秒为单位的时间)。如果您调用showMessage
而没有指定时间或指定零毫秒的时间,该消息将一直显示,直到您调用showMessage
替换该消息,或者直到您调用clearMessage()
删除该消息。
当不用于状态消息时,状态栏可以包含一组小部件。这些小部件的通常用途是为用户提供有用的信息,以便随时可以使用。
小部件可以正常或永久地添加到状态栏中。不同的是,普通的窗口小部件被消息覆盖,而永久的窗口小部件总是显示。小部件是从左到右添加的,但是永久小部件总是出现在普通小部件的右侧。
图 9-11 所示的状态栏显示了一个带有进度条和三个标签的状态栏。标签“N”表示当前文档没有被修改。这显示了状态栏的局限性之一:可用空间是有限的,所以信息必须以非常紧凑的格式呈现。可以为标签设置一个工具提示来解释显示的内容,但这不是一个非常直观的解决方案。
图 9-11。 一个带有进度条和三个标签的状态栏
状态栏和窗口小部件的创建如清单 9-10 所示。代码取自基于QMainWindow
的类的构造器。突出显示的行是影响状态栏的行。首先获取一个指向状态栏的指针,然后使用addPermanentWidget(QWidget*, int)
添加永久小部件,最后使用addWidget(QWidget*, int)
添加三个普通小部件。
清单 9-10。 状态栏及其小部件设置在主窗口的构造器中。
MainWindow::MainWindow() : QMainWindow()
{
…
QStatusBar *statusBar = this->statusBar();
QProgressBar *progressBar = new QProgressBar;
QLabel *mode = new QLabel( tr(" EDIT ") );
QLabel *modified = new QLabel( tr(" Y ") );
QLabel *size = new QLabel( tr(" 999999kB ") );
mode->setMinimumSize( mode->sizeHint() );
mode->setAlignment( Qt::AlignCenter );
mode->setText( tr(“EDIT”) );
mode->setToolTip( tr(“The current working mode.”) );
statusBar->addPermanentWidget( mode );
modified->setMinimumSize( modified->sizeHint() );
modified->setAlignment( Qt::AlignCenter );
modified->setText( tr(“N”) );
modified->setToolTip( tr("Indicates if the current document "
“has been modified or not.”) );
size->setMinimumSize( size->sizeHint() );
size->setAlignment( Qt::AlignRight | Qt::AlignVCenter );
size->setText( tr("%1kB ").arg(0) );
size->setToolTip( tr(“The memory used for the current document.”) );
progressBar->setTextVisible( false );
progressBar->setRange( 0, 0 );
statusBar->addWidget( progressBar, 1 );
statusBar->addWidget( modified );
statusBar->addWidget( size );
…
}
请注意,小部件是以大尺寸创建的,并且设置了针对sizeHint
的minimumSize
策略。这意味着小部件不会缩小到比这个更小的尺寸。通过在添加进度条时将第二个参数设置为1
,可以让它占用剩余的可用空间。第二个参数是拉伸因子,默认为零。通过使用它,您可以确保当主窗口调整大小时,小部件保持它们的相对大小。
然后,标签在添加到状态栏之前会获得正确的文本和工具提示。请注意,永久小部件出现在右侧,即使它是在普通小部件之前添加的。这是为了让消息可以覆盖正常的小部件,同时保持永久的小部件可见。在图 9-12 中可以看到一个例子。
图 9-12。 显示消息的状态栏和永久小工具
状态栏更常见的用途之一是显示不同的工作模式。(别忘了状态栏相当小。)也尝试用其他方式显示不同的工作模式:更改鼠标指针,更改正在处理的对象的手柄外观,或者简单地更改背景颜色。仅仅在状态栏上显示一个小小的三个字母的代码是迷惑任何用户的好方法。
创建向导
当用户面对大量选项时,向导可以通过按逻辑顺序显示选项来提供帮助,并以解释文本的形式为每个选项提供额外的支持。
根据 Qt,向导是包含所有页面的QWidgetStack
;QPushButton
下一个、上一个和取消按钮的小部件;和一个保存所有组件的QDialog
。每个页面本身就是一个QWidget
,可以包含其他用于设置的小部件。
一个QWidgetStack
是一个可以容纳其他部件的特殊部件。这些小部件保存在一个堆栈中(就像在一堆卡片中一样),其中只有当前的小部件是可见的。这使得通过简单地改变堆栈的当前小部件就可以在页面中向前和向后移动。
设计向导的最好工具是 Qt Designer,但是为了展示这个概念,我将向您展示一个手工编码的版本。其首页如图图 9-13 所示。
图 9-13。 示例向导的第一页
向导只不过是应用程序其余部分的一个对话框。清单 9-11 显示了Wizard
对话框类的声明。公共接口只包含一个构造器。界面的私有部分由“下一个”和“上一个”按钮的位置组成,后面是一些指向组成对话框的不同部件的指针。
清单 9-11。 巫师类的宣言
class Wizard : public QDialog
{
Q_OBJECT
public:
Wizard();
private slots:
void doNext();
void doPrev();
private:
QPushButton *next;
QPushButton *previous;
QStackedWidget *pages;
PageOne *pageOne;
PageTwo *pageTwo;
PageThree *pageThree;
};
在向导中,我选择将所有逻辑放在Wizard
类中,因此所有页面都简单地处理视觉细节。稍后可以访问的控件,比如复选框和带有用户配置的行编辑,在页面类中成为公共成员。图 9-13 的第一页如清单 9-12 所示。
清单以类声明开始。对于第一页,只有构造器和接受规则的复选框可用,因为Wizard
类需要能够判断下一步按钮是被启用还是被禁用。
清单的另一半由构造器的实现组成,在其中创建、设置小部件,并放入布局中。QTextEdit
小部件被用作阅读器,所以在使用setHtml
设置文本之前,readOnly
属性被设置为true
。
清单 9-12。 向导的第一页
class PageOne : public QWidget
{
public:
PageOne( QWidget *parent = 0 );
QCheckBox *acceptDeal;
};
PageOne::PageOne( QWidget *parent ) : QWidget(parent)
{
QGridLayout *layout = new QGridLayout( this );
QTextEdit *textEdit = new QTextEdit;
textEdit->setReadOnly( true );
textEdit->setHtml( tr("<h1>The Rules</h1>"
"<p>The rules are to be followed!</p>") );
acceptDeal = new QCheckBox( tr("I accept") );
layout->addWidget( textEdit, 0, 0, 1, 2 );
layout->addWidget( acceptDeal, 1, 1 );
}
在您可以在向导对话框中显示第一页之前,还缺少一部分:构造器。构造器负责创建下一个、上一个和取消按钮;创建页面;并在应用布局和进行所需连接之前将它们放入堆栈中。
构建器的源代码如清单 9-13 中的所示。按照自上而下的代码,从创建布局和小部件开始。然后,在配置按钮之前,将小部件放置在布局中。“下一个”和“上一个”从一开始就被禁用,因为没有什么可返回的,并且用户必须批准规则才能继续。这些按钮连接到doNext()
和doPrev()
插槽,而取消按钮连接到关闭对话框的reject()
插槽。
当按钮连接后,页面被创建并添加到小部件堆栈中。最后一步是将第一页复选框的toggled(bool)
信号连接到下一个按钮的setEnabled(bool)
槽。
清单 9-13。 向导的构造者
Wizard::Wizard() : QDialog()
{
QGridLayout *layout = new QGridLayout( this );
QPushButton *cancel = new QPushButton( tr("Cancel") );
next = new QPushButton( tr("Next") );
previous = new QPushButton( tr("Previous" ) );
pages = new QStackedWidget;
layout->addWidget( pages, 0, 0, 1, 5 );
layout->setColumnMinimumWidth( 0, 50 );
layout->addWidget( previous, 1, 1 );
layout->addWidget( next, 1, 2 );
layout->setColumnMinimumWidth( 3, 5 );
layout->addWidget( cancel, 1, 4 );
previous->setEnabled( false );
next->setEnabled( false );
connect( next, SIGNAL(clicked()), this, SLOT(doNext()) );
connect( previous, SIGNAL(clicked()), this, SLOT(doPrev()) );
connect( cancel, SIGNAL(clicked()), this, SLOT(reject()) );
pages->addWidget( pageOne = new PageOne( pages ) );
pages->addWidget( pageTwo = new PageTwo( pages ) );
pages->addWidget( pageThree = new PageThree( pages ) );
connect( pageOne->acceptDeal, SIGNAL(toggled(bool)),
next, SLOT(setEnabled(bool)) );
}
当用户勾选该框并点击下一步按钮时,显示如图图 9-14 所示的对话框。当点击下一个按钮时,有许多事情需要处理:下一个按钮的enabled
属性不再依赖于复选框的状态,上一个按钮需要被启用,你不能忘记显示下一页。所有这些都在doNext
槽中管理。
图 9-14。 示例向导第二页
doNext
插槽的源代码如清单 9-14 所示。该方法的基础是一个switch
操作,该操作根据用户单击 Next 按钮时所在的页面来决定要做什么。因为该向导包含三个页面,所以有三种情况需要处理。当离开第一页时,处理下一个按钮的 enabled 属性的连接被断开,上一个按钮被启用。当离开第二页进入最后一页时,下一步按钮的文字变为完成,如图图 9-15 所示。
清单 9-14。 处理下一步按钮
void Wizard::doNext()
{
switch( pages->currentIndex() )
{
case 0:
previous->setEnabled( true );
disconnect( pageOne->acceptDeal, SIGNAL(toggled(bool)),
next, SLOT(setEnabled(bool)) );
break;
case 1:
next->setText( tr("Finish") );
break;
case 2:
QMessageBox::information( this, tr("Finishing"),
tr("Here is where the action takes place.") );
accept();
return;
}
pages->setCurrentIndex( pages->currentIndex()+1 );
}
图 9-15。 示例向导的最后一页
当离开最后一页时,在从插槽返回之前,使用accept
方法关闭对话框之前,会显示一个消息框。这是您通过实际操作完成向导的地方。实际的工作可以在对话框中完成,也可以在打开对话框的代码中完成。因为您在这里使用accept
并在所有其他情况下使用reject
来关闭对话框,所以您可以检查对话框结果并在对话框被接受时采取行动。
doNext
槽的最后一个任务是更新小部件堆栈的currentIndex
属性,显示下一页。因为这是为所有页面做的,所以它的代码被放在了switch
块之外。
完成向导所需的最后一部分是返回的能力,这是从清单 9-15 中所示的doPrev
槽处理的。其原理与在doNext
插槽中使用的相同:一个开关操作,根据点击按钮时显示的页面来决定做什么。
清单 9-15。 处理上一个按钮
void Wizard::doPrev()
{
switch( pages->currentIndex() )
{
case 1:
previous->setEnabled( false );
next->setEnabled( pageOne->acceptDeal->isChecked() );
connect( pageOne->acceptDeal, SIGNAL(toggled(bool)),
next, SLOT(setEnabled(bool)) );
break;
case 2:
next->setText( tr("Next") );
break;
}
pages->setCurrentIndex( pages->currentIndex()-1 );
}
正在执行的动作可以追溯到doNext
槽。当从第 1 页移动到第 0 页时,将切换的信号重新连接到下一个按钮的 enabled 属性,并禁用上一个按钮。当从第 2 页移动到第 1 页时,将“下一页”按钮的文本重置为“下一页”。
如您所见,创建向导是一项相当简单的任务。因为所有的向导都是依赖于应用程序的,所以每个向导都有大量特定于应用程序的代码。通过使用 Qt Designer 设计向导,您可以减少实现一个doNext
和一个doPrev
插槽的工作量。几乎所有其他代码都只是为了处理对话框和不同页面的外观。
协助用户
当然,您可能希望依赖为用户提供帮助的事实上的标准:F1 键。参考文档可以通过 Qt 附带的 Qt 助手获得。当您需要提供帮助时,也可以使用 Assistant 作为应用程序的帮助系统。这样做需要两个阶段:配置 Assistant,然后在应用程序中集成 Assistant。
创建帮助文档
Qt Assistant 可以呈现 HTML 文档,因此您必须使用 HTML 格式来格式化您的帮助文件,以便利用这个特性。HTML 文件和图像放在可执行文件旁边的目录中,旁边还有 Assistant 需要的另外两个文件。第一个也是最重要的文件是名为qtbookexample.adp
的辅助文档概要文件。此文件配置助手,以便使用正确的文档集并正确设置窗口标题。你可以在清单 9-16 中看到文件的内容。
Assistant 需要的第二个文件是用于在 Assistant 中自定义 about 框的about.txt
文件。你可以看到它是从adp
文件的profile
部分引用的。profile
部分配置 Assistant 的外观,用窗口标题、图标、起始页、about 菜单的文本、包含 about 框文本的文件以及其余文档的相对路径来配置。
清单 9-16。 助理文档配置文件
<!DOCTYPE DCF>
<assistantconfig version="3.2.0">
<profile>
<property name="name">qtbookexample</property>
<property name="title">Qt Book Example</property>
<property name="applicationicon">img/qt.png</property>
<property name="startpage">index.html</property>
<property name="aboutmenutext">About The Qt Book Example</property>
<property name="abouturl">about.txt</property>
<property name="assistantdocs">.</property>
</profile>
<DCF ref="index.html" icon="img/qt.png" title="Qt Book Example">
<section ref="./basics.html" title="Basics">
<section ref="./index.html" title="The first basic thing" />
<section ref="./index.html" title="The second basic thing" />
<section ref="./easystuff.html" title="Another basic topic" />
<keyword ref="./index.html">Basic Thing One</keyword>
<keyword ref="./index.html">Basic Thing Two</keyword>
<keyword ref="./easystuff.html">Another Basic Thing</keyword>
</section>
<section ref="./advanced.html" title="Advanced Topics">
<section ref="./adv1.html" title="The first advanced thing" />
<section ref="./adv2.html" title="The second advanced thing" />
<keyword ref="./adv1.html">Advanced Topic One</keyword>
<keyword ref="./adv2.html">Advanced Topic Two</keyword>
</section>
<section ref="./appendix.html" title="Appendix" />
<section ref="./faq.html" title="F.A.Q." />
</DCF>
</assistantconfig>
adp
文件的后半部分包含不同的部分和要使用的关键字。图 9-16 显示了信息如何显示在助手的目录和索引标签中。
其他选项卡会自行处理。书签由用户添加,搜索选项卡提供对从adp
文件引用的所有文件的搜索。
要使用助手测试您的adp
文件,您可以使用参数-profile
启动助手,然后参考您的个人资料。例如,assistant -profile qtbookexample.adp
用qtbookexample.adp
文档启动助手,如图图 9-16 所示。
图 9-16。 文档配置文件在助手中显示为目录树和关键字列表。
把它放在一起
要使用 Assistant 作为您的帮助文档浏览器,您需要创建一个QAssistantClient
对象。确保为整个应用程序只创建一个对象,如果同时启动几个助手实例,用户可能会感到困惑。
清单 9-17 展示了如何创建一个助理客户端对象。给构造器的第一个参数是助手可执行文件的路径。如果您假设用户安装了一个有效的 Qt 开发环境,那么您可以使用QLibraryInfo
对象来查找可执行文件。在最常见的情况下,用户没有安装 Qt,因此您必须将助手可执行文件与您的应用程序一起提供,并将其放置在相对于您的应用程序可执行文件的位置。您可以通过使用QApplication::applicationDirPath()
方法找到文件的位置。
清单 9-17。 创建和配置助手
QAssistantClient *assistantClient =
new QAssistantClient( QApplication::applicationDirPath(), qApp );
QStringList arguments;
arguments << "-profile" << "./documentation/qtbookexample.adp";
assistantClient->setArguments( arguments );
当您想要显示助手时,只需调用助手客户端对象的openAssistant()
或showPage(const QString&)
方法之一。当你的应用程序关闭时,确保在你的客户端对象上调用closeAssistant()
来关闭任何打开的助手实例。
为了能够使用QAssistantClient
类构建项目,您必须将行CONFIG += assistant
添加到您的项目文件中。
总结
提供帮助不仅仅是响应 F1 键;它是关于提供一个直观的用户界面,并在用户需要时增加支持。必须通过用户知道的渠道提供支持,这样帮助才是直观的。通过提供工具提示和对大多数小部件的帮助,可以避免很多问题。
当工具提示不再有帮助时,可以使用向导,或者您可以尝试重新设计用户界面来避免问题。后者必须永远是一个选项,但有时向导是最好的选择。
为了使信息可用,您可以使用状态栏为用户提供相同的信息,而不管用户在做什么。但是不要指望用户一直看到状态栏——如果工作模式被意外改变,用户通常不会去找状态栏;相反,当变化发生时,他们去他们在的任何地方。
帮助系统的最后一部分是在线文档。Qt Assistant 可以通过为您的文档提供一个良好的界面来帮助您。只需将您的文档编译成一组 HTML 文档,创建一个文档配置文件,并将助手用作您的帮助客户端。
十、国际化和本地化
当你为国际市场部署你的应用程序时,你必须提供本地化版本。这样做的原因远远超出了世界人口所使用的不同语言;事实上,在时间、日期和货币价值的表示上存在差异;甚至更复杂的书面语言问题,如文本应该从右边还是从左边读。
提示国际化和本地化实际上是同一个流程的两个部分。国际化就是让您的应用程序摆脱与特定位置的任何联系,使其独立于任何特定的语言或文化。本地化是下一步——采用国际化的应用程序,并使其适应具有特定语言和文化的特定位置。
在开始处理为了成功适应应用程序的不同语言和文化而必须管理的所有细节之前,先看看 Qt 提供的管理工具。
提示你知道国际化经常被写成 i18n,其中 18 是去掉的字符数吗?本地化往往可以看做 l10n(用同样的方式缩写)。
翻译应用程序
要开始,你需要一个应用程序来翻译。您将使用第四章的中的 SDI 应用程序,以及在第八章中扩展的附加特性(当添加了文件处理支持时)。在图 10-1 中可以看到应用程序的截图。因为我的母语是瑞典语,所以任务是将应用程序翻译成瑞典语。
**图 10-1。**SDI 申请
翻译以两种不同的文件格式保存:ts
和qm
。ts
文件是在开发过程中使用的,它以一种易于维护的 XML 文件格式包含了应用程序中的所有单词。qm
文件在运行时使用,包含可移植的压缩格式的短语。这个想法是在开发过程中使用ts
文件作为源文件。然后将ts
文件编译成实际应用程序使用的可分发的qm
格式。编译被称为发布翻译。
在开始翻译应用程序之前,您需要通知 Qt 您的意图。因为目标语言是在瑞典使用的瑞典语,并且该地区常用的代码是sv_SE
,所以您可以将它添加到应用程序名称的末尾:SDI_sv_SE
。
注意名称的sv_SE
部分是由 ISO 639-1 的语言代码和 ISO 3166-1 的国家代码组合而成的。应用程序名称只是应用程序的非正式名称。这种命名约定只是约定俗成的——您可以随意命名您的翻译。
要将此翻译添加到项目中,只需将下面一行添加到项目文件中:
TRANSLATIONS += sdi_sv_SE.ts
通过适当地添加新的`TRANSLATION +=`行,您可以向项目添加任意数量的翻译。您也可以通过用空格或制表符分隔来一次指定多个翻译。
提取字符串
当项目文件被一个或多个翻译更新后,是时候从应用程序中的各种`tr()`调用中提取需要翻译的字符串了。也有其他情况,但将在以后讨论。
`lupdate`工具用于提取短语——它创建或更新给定项目文件中列出的所有`ts`文件。很高兴知道,当它更新一个现有的文件时,它不会删除任何东西——所有已经完成的翻译都保持不变。因为项目文件名为`sdi.pro`,所以在命令行输入的命令是`lupdate sdi.pro`。这将从项目文件的源代码中找到的字符串创建`sdi_sv_SE.ts`文件。
虽然 Qt 附带了一个软件翻译工具,但并不是所有的翻译业务都希望使用定制工具。幸运的是,`ts`文件非常容易处理,因为它们是 XML 格式的。清单 10-1 显示了未翻译的`sdi_sv_SE.ts`文件的摘录。
**清单 10-1。** *一个未翻译的内容的例子* `ts` *文件*
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS><TS version="1.1">
<context>
<name>SdiWindow</name>
<message>
<location filename="sdiwindow.cpp" line="254"/>
<source>%1[*] - %2</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="sdiwindow.cpp" line="19"/>
...
</context>
</TS>
正如您从摘录中看到的,将它转换成您的翻译公司喜欢的格式并返回应该不难。
语言学家:翻译的工具
Qt 与*语言学家*工具捆绑在一起,该工具为翻译人员提供了要翻译的字符串及其各自状态的方便概述:完成、未知或缺失。它还提供了一些简单的检查来确保翻译是正确的。例如,它检查原始字符串和翻译字符串中的最终标点符号是否相同。
启动语言学家产生如图 10-2 所示的用户界面。该图显示了打开翻译并翻译了几个字符串后的应用程序。
如果你仔细观察图 10-2 ,你可以看到语言界面由三个面板组成。在上下文面板中(左边)是包含字符串的类和它们各自的字符串。当前选定的字符串以其原始和翻译的形式显示在主面板中(右上角)。在短语面板中,Qt 通过查看早期的翻译和你可以加载的短语手册来推荐翻译。(这里不涉及短语书。)
**图 10-2。** *语言学家用一个新鲜的翻译文件加载了*
在语言学家中最简单的方法是从上下文面板中选择一个字符串,翻译它,然后按 Ctrl + Enter。如果四个验证器都没问题的话,这将把您带到下一个未翻译的字符串。可以从验证菜单中打开和关闭验证器。它们的功能如下:
- 加速器:如果原始字符串中有加速器,这个函数确保翻译中有加速器。
- 结尾标点:这个函数确保原文和译文的结尾标点匹配。
- 短语匹配:这个函数检查原始字符串是否匹配一个已知的短语。在这种情况下,翻译应该与已知短语的翻译相同。
- 位置标记匹配:该函数确保原始字符串中的位置标记(例如,
%1
,%2
)也存在于翻译中。
如果验证器不接受,可以保留一个翻译,但是 Ctrl + Enter 快捷键不会自动移动(确保你主动决定忽略验证器)。当一个验证程序反对一个翻译时,它会在状态栏中显示一条消息(见图 10-3 )。
图 10-3。 验证器反对该翻译,因为该翻译没有引用与源文本中相同的位置标记。
随着翻译的进行,您可以在状态栏的右侧看到您的状态。翻译完所有字符串后,破折号两边的数字将匹配。您可以随时保存您的翻译,稍后继续工作。语言学家和lupdate
不会丢失任何信息,除非你自己覆盖或删除它。
当您的翻译准备好并保存后,您必须编译或发布它,以便能够通过使用lrelease
工具在您的应用程序中使用它。只需将您的项目名称作为参数传递。对于sdi.pro
应用程序,您可以从命令行运行lrelease sdi.pro
,从您的ts
文件构建所需的qm
文件。
设置一个翻译对象
当翻译准备好并发布后,就该将它们加载到应用程序中了。因为语言是在应用程序级别设置的,所以目标是在QApplication
对象上安装一个包含正确翻译的QTranslator
对象。
在担心QTranslator
对象之前,您需要确定用户期望用哪种语言编写应用程序。这些信息可以在QLocale
类中找到。一个QLocale
对象代表一个特定的本地化区域和语言。对象知道该区域和语言的大多数本地化细节。要获得表示计算机的区域和语言的对象,可以使用名为QLocale::system
的静态方法。
这个名字在清单 10-2 中使用,通过调用installTranslator(QTranslator*)
在安装之前将翻译加载到QTranslator
对象中。正如您在清单中看到的,没有指定翻译文件的文件扩展名。如果load
调用失败,翻译器将没有任何作用,应用程序将以源代码中使用的语言显示。
清单 10-2。 翻译被加载到安装在应用程序上的翻译器中。
int main( int argc, char **argv )
{
QApplication app( argc, argv );
QTranslator translator;
translator.load( QString("sdi_")+QLocale::system().name() );
app.installTranslator( &translator );
QTranslator qtTranslator;
qtTranslator.load( QString("qt_")+QLocale::system().name() );
app.installTranslator( &qtTranslator );
SdiWindow *window = new SdiWindow;
window->show();
return app.exec();
}
命名翻译文件没有规则。它可能被称为swedish.qm
或12345.qm
——这不重要。将地区名称与翻译器联系起来的好处是,您可以使用QLocale::system
来找到正确的语言。
提示您可以将您的qm
文件添加到一个资源文件中,以便将翻译集成到您的应用程序中。它增加了可执行文件的重量,但是减少了对其他文件的依赖。这可以使应用程序更容易部署。
Qt 字符串
如果您现在部署应用程序,那么只有部分内容会被翻译。在 Qt 打开和保存文档的标准对话框和 About Qt 对话框中,使用了嵌入在 Qt 库中的字符串。这些字符串被lupdate
遗漏了,因为它只出现在当前项目的源代码中。相反,您必须安装另一个翻译器来处理嵌入在 Qt 标准对话框中的字符串。
在开始编写添加这样一个翻译器的代码之前,先看看 Qt 提供的翻译。Qt 库包含大约 2200 个单词(你可以看到语言学家在图 10-4 中加载了 Qt 翻译)。Qt 附带了这些单词的翻译,用于将默认语言(英语)翻译成法语和德语。还包括其他语言,但是它们没有得到 Trolltech 的官方支持。所有的翻译都可以从 Qt 安装目录下的translations
子目录中获得。注意,如果您需要支持一种新的语言,您可以使用qt_untranslated.ts
文件作为起点。你也应该在网上搜索,因为许多开发者会发布他们的翻译供他人使用。
图 10-4。 一个 Qt 翻译载入语言学家
因为 Qt 字符串不是你的应用程序的一部分,你必须手动释放它。你可以通过使用语言学家打开文件并从文件菜单中释放它(如图图 10-5 所示),或者你可以将ts
文件作为参数给lrelease
而不是你的项目文件。
提示另一种方法是将你的ts
文件基于适当的 Qt 翻译。因为lupdate
从不删除任何东西,这与合并翻译是一样的,这使得发布过程更容易。
图 10-5。 您可以使用文件菜单中的发布选项发布当前翻译。
当您已经将 Qt 字符串的翻译创建或复制到项目目录中,发布它,并给结果文件一个合适的名称时,是时候将它加载到一个翻译器中并安装它了。在瑞典语的情况下,文件被称为qt_sv_SE
,加载如清单 10-3 所示。如您所见,该过程与应用程序字符串的翻译加载是相同的。
清单 10-3。 为 Qt 的字符串加载和安装翻译器
int main( int argc, char **argv )
{
QApplication app( argc, argv );
QTranslator translator;
translator.load( QString("sdi_")+QLocale::system().name() );
app.installTranslator( &translator );
QTranslator qtTranslator;
qtTranslator.load( QString("qt_")+QLocale::system().name() );
app.installTranslator( &qtTranslator );
SdiWindow *window = new SdiWindow;
window->show();
return app.exec();
}
当两个翻译器都被加载和安装后,用户界面被翻译。在图 10-6 中可以看到翻译瑞典语旁边的英文原文。
图 10-6。 英语和瑞典语的 SDI 应用
处理其他翻译案件
当您在tr
调用中包含字符串时,会发生两件事:lupdate
找到字符串并将其交给翻译器;然后字符串通过QApplication::translate
方法传递。
所以有两种特殊情况需要注意:确保lupdate
可以找到所有的字符串,并确保所有的字符串都以允许方法正确翻译的方式通过translate
。
寻找所有字符串
有时你写的代码中你的字符串不会出现在tr
调用中。在这种情况下,您可以使用宏QT_TR_NOOP
或QT_TRANSLATE_NOOP
。请看清单 10-4 中的作为例子。
这两个宏的区别在于QT_TR_NOOP
没有上下文参数。这对于texts2
中的字符串来说很好,它们不太可能与应用程序中的其他字符串混淆。然而,texts
中的琴弦很容易混淆。例如,Title
是指网页的标题还是指某个人的标题?在瑞典语中,网页标题的翻译是Överskrift
,人名标题的翻译是Befattning
——差别很大。
当字符串可能不明确时,QT_TRANSLATE_NOOP
宏就派上了用场。它使得为译者和翻译机制添加上下文成为可能。图 10-7 显示了来自清单 10-4 的字符串出现在语言学家中的样子。
清单 10-4。使用 QT_TR_NOOP
和 QT_TRANSLATE_NOOP
宏可以使 lupdate
调用之外的 字符串可见。
char *texts[] = { QT_TRANSLATE_NOOP("main","URL"),
QT_TRANSLATE_NOOP("main","Title"),
QT_TRANSLATE_NOOP("main","Publisher") };
char *texts2[] = { QT_TR_NOOP( "This is a very special string."),
QT_TR_NOOP( "And this is just as special.") };
从继承以Q_OBJECT
开始的QObject
的类中捕获的字符串被自动放置在以该类命名的上下文中。
使用来自外部的字符串很容易。只需使用应用程序对象中可用的translate
方法。如果你的字符串没有上下文,可以传递一个空字符串(0
);否则,将上下文作为第一个参数,字符串作为第二个参数。下面一行使用了来自texts
和texts2
向量的字符串:
QMessageBox::information( 0, qApp->translate("main",texts[2]), qApp-
>translate(0,texts2[1]) );
**区分字符串**
如前所述,有些字符串可能是不明确的。例如, *address* 这个词可以指邮政地址、web URL 或者计算机主内存中的内存地址。不同句子的翻译可以根据意思和上下文而有所不同。如果在一个上下文中使用了这些含义中的几个,您可以为每个字符串添加一个注释,以便翻译人员能够区分它们。
**图 10-7。** *使用* `QT_TRANSLATE_NOOP` *宏找到的字符串在上下文中找到。*
清单 10-5 展示了如何在`tr`调用中指定注释的例子。注释只是作为第二个参数发送给`tr`方法。
**清单 10-5。** *添加注释以区分不同意思的同一个词*
new QLabel( tr("Address:", "Postal address"), this );
new QLabel( tr("Address:", "Website address"), this );
当翻译器打开`ts`文件时,注释显示在要翻译的实际字符串下面。清单 10-5 中的字符串显示在图 10-8 中。
**图 10-8。** *注释显示在原字符串下方给译者看。*
**您更改了** ** * n * ** **文件**
当`translate`方法试图翻译一个字符串时,它需要得到一个精确的匹配,所以清单 10-6 中只有一个字符串有效。在`tr`调用(`line1`)中使用`+`操作符合并字符串的问题是`lupdate`不能正确地找到字符串。在`tr`调用(`line2`)之后合并字符串的问题是,词序或多或少是固定的。通过使用在`line3`赋值中显示的`arg`调用,翻译器可以自由地改变单词的顺序,并且不管`n`的值是多少,字符串都会被匹配。
**清单 10-6。** *三种建串方式:一对两错*
QString line1 = tr(“You have altered " + QString::number(n) + " file(s).”);
QString line2 = tr(“You have altered “) + QString::number(n) + tr(” file(s).”);
QString line3 = tr(“You have altered %1 file(s).”).arg(n);
关于`line3`赋值有一个恼人的问题:即`(s)`部分。可以让翻译器为`n`的不同值提供字符串;清单 10-7 中`line4`的代码向展示了它是如何完成的。`tr`调用有三个参数:实际的字符串、一个注释和一个用于确定字符串是单数还是复数形式的值。
**清单 10-7。** *处理复数字符串*
QString line4 = tr(“You have altered %1 file.”, “”, n).arg(n);
当找到带有值的`tr`调用时,翻译器就有能力提供字符串的单数和复数版本。有些语言有其他特殊形式,比如*paucal*——Qt 也处理它们。`line4`的管柱如图图 10-9 所示。
**图 10-9。** *语言学家*中一个字符串的单数和复数版本
找到丢失的字符串
有时很容易忘记给`tr`或`translate`打电话;或者从`tr`、`QT_TR_NOOP`或`QT_TRANSLATE_NOOP`标记中省略一个字符串。这导致字符串在运行时不被翻译或被`lupdate`工具错过,从而在`translate`被调用时丢失。
有工具可以定位丢失的字符串。例如,Qt 4 附带了`findtr` perl 脚本。如果你在 Unix 系统上工作,你也可以使用更简单的`grep`命令`grep -n '"' *.cpp | grep -v 'tr('`。
另一种方法是在源代码中使用虚假的语言(例如,在所有字符串之前添加`FOO`,在它们之后添加`BAR`——这样普通的菜单栏就会显示为`FOOFileBAR`、`FOOEditBAR`和`FOOHelpBAR`)。这使得发现没有被翻译的字符串变得容易,因此在测试过程中所有的字符串都有可能被定位。
这两个技巧都不是万无一失的,所以你需要注意你的琴弦以及你对它们做了什么。在翻译中遗漏一个字符串会很快给你的用户传达一个糟糕的信息。
**提示**找到遗漏的`tr()`调用的一种方法是阻止 Qt 自动将`char*`字符串转换为`QString`对象,这将导致编译器在您遗漏调用`tr()`的所有时间都出错。您可以通过在项目文件中添加一行`DEFINES += QT_NO_CAST_FROM_ASCII`来禁用转换。
即时翻译
有时,您可能希望您的应用程序能够在不同的语言之间动态切换。用户应该能够选择一种语言,然后整个环境立即被翻译成所选择的语言。要尝试这样做,请看一下图 10-10 中的应用程序。只有两种语言可供选择,但是相同的解决方案适用于任何数量的语言。
**图 10-10。** *正在翻译的应用程序*
原理很简单。当用户选中一个单选按钮时,`toggled`信号连接到一个插槽。该插槽将新的翻译加载到已安装的`QTranslator`对象中,这将导致对`tr`的所有调用返回所选语言的字符串。唯一的问题是所有的`tr`调用都需要重新做一遍。在这种情况下,最好知道当一个新的翻译被加载时,一个`QEvent::LanguageChange`事件被发送给所有的`QObject`。它的工作原理是将所有的`setText`和`setTitle`调用放在一个函数中,一旦发生语言改变事件就调用那个函数。
这在理论上听起来不错,所以让我们看看实际的源代码。清单 10-8 显示了`DynDialog`类的声明,它是应用程序中使用的对话框。您需要保留对所有显示文本的小部件的引用——`languages`分组框和两个单选按钮。
**清单 10-8。***`DynDialog`*类声明**```
class DynDialog : public QDialog
{
Q_OBJECT
public:
DynDialog();
protected:
void changeEvent( QEvent* );
private slots:
void languageChanged();
private:
void translateUi();
QGroupBox *languages;
QRadioButton *english;
QRadioButton *swedish;
};
```cpp
该构造器表明该对话框旨在被动态翻译。在清单 10-9 的所示的源代码中,小部件被创建、配置并放置在布局中,但是没有一个对`setText`或`setTitle`的调用。相反,在最后调用了`translateUi`方法。
**清单 10-9。***`DynDialog`*对话框的构造器——注意没有设置文本**
DynDialog::DynDialog() : QDialog( 0 )
{
languages = new QGroupBox( this );
english = new QRadioButton( this );
swedish = new QRadioButton( this );
english->setChecked( true );
qTranslator->load( “english” );
QVBoxLayout *baseLayout = new QVBoxLayout( this );
baseLayout->addWidget( languages );
QVBoxLayout *radioLayout = new QVBoxLayout( languages );
radioLayout->addWidget( english );
radioLayout->addWidget( swedish );
connect( english, SIGNAL(toggled(bool)), this, SLOT(languageChanged()) );
connect( swedish, SIGNAL(toggled(bool)), this, SLOT(languageChanged()) );
translateUi();
}
`translateUi`方法如清单 10-10 所示。这里,用户可见的所有字符串都通过`tr`传递,然后被设置。
**清单 10-10。** *一次更新所有用户可见字符串*
void DynDialog::translateUi()
{
languages->setTitle( tr(“Languages”) );
english->setText( tr(“English”) );
swedish->setText( tr(“Swedish”) );
}
参考清单 10-9 中的可以看到,当用户选择另一种语言时(也就是切换其中一个单选按钮),槽`languageChanged`被调用。插槽实现如清单 10-11 所示。如您所见,`qTranslator`为不同的用户选择加载了不同的翻译器。`qTranslator`指针是一个应用全局指针,指向已安装的`QTranslation`对象。该对象被创建并安装在`main`功能中。
**清单 10-11。** *加载译文*
void DynDialog::languageChanged()
{
if( english->isChecked() )
qTranslator->load( “english” );
else
qTranslator->load( “swedish” );
}
当加载新的翻译时,`QEvent::LanguageChanged`事件被发送到所有的`QObject`实例。这个事件可以在受保护的`changeEvent`方法中被捕获,如清单 10-12 所示。一旦遇到事件,就会再次调用`translateUi`方法,使用新加载的翻译器更新所有可见文本。
**清单 10-12。** *观察* `QEvent::LanguageChanged` *事件,遇到时更新用户界面。*
void DynDialog::changeEvent( QEvent *event )
{
if( event->type() == QEvent::LanguageChange )
{
translateUi();
}
else
QDialog::changeEvent( event );
}
* * *
**提示**您可以在`changeEvent`方法中观察更多的国际化事件。当地区改变时,发送`QEvent::LocaleChange`。
* * *
为了能够构建系统,使用了一个带有行`TRANSLATIONS += english.ts swedish.ts`的项目文件。使用`lupdate`生成`ts`文件,语言学家翻译字符串,使用`lrelease`生成`qm`文件。然后运行`qmake`和`make`来构建应用程序。
### 其他注意事项
当执行应用程序的实际本地化时,有几个问题需要注意。这不仅仅是翻译文本的问题;您还必须处理不同的键入数字、显示图像、处理货币以及处理时间和日期的方式。
#### 处理文字
因为 Qt 在内部处理 Unicode 字符,所以`QString`和`QChar`类可以处理几乎任何可能的字符。但是这意味着标准库`isalpha`、`isdigit`、`isspace`等不能在所有平台上正常工作,因为它们有时在西欧或美国环境下运行。
* * *
我有时会在英文网站上注册我的街道地址时遇到麻烦,因为我居住的城镇叫做 Alingså。字母“不被认为是合法字符。
* * *
解决方案是坚持这些方法的特定于 Qt 的实现。`QChar`类包含了方法`isAlpha`、`isDigit`、`isSpace`以及更多等同于标准函数的方法。
考虑 Unicode 不仅在验证用户输入时很重要,在解析文件时也很重要。要将 Unicode `QString`转换为`char*`向量(通过`QByteArray`,可以使用`toAscii`或`toLatin1`将字符串转换为每字符 8 位的格式。结果是 ASCII 字符串或 Latin1 (ISO 8859-1)字符串。如果您想转换为当前的 8 位格式,您可以使用`toLocal8Bit`方法,该方法会转换为系统设置所指示的 8 位编码。
您也可以使用`toUtf8`将其转换为 UTF8。UTF8 格式表示许多字符,就像在 ASCII 中一样,但是通过将它们编码为多字节序列来支持所有 Unicode 字符。
绘制文字时,Qt 尊重文字的方向。有些语言是从右向左书写的,所以在定制小部件时必须考虑到这一点。最简单的方法是使用矩形而不是点来指定文本的位置。通过这种方式,Qt 可以将文本放置在用户期望的位置。
#### 图片
当谈到图像时,有两件重要的事情需要考虑:小心使用图像来交流文字游戏,避免敏感的符号。设计有效的图标是一门艺术,遵循这些规则会使它变得更加困难。
文字游戏的一个经典例子是将一棵树的日志显示为日志查看器的图标。这在英语环境中是非常合乎逻辑的,但是在瑞典语中,表示一棵树的原木的单词是 stock。这个图标可以说是代表一个股票市场交易工具——这在英语环境中是一个糟糕的文字游戏。
当涉及敏感符号时,有许多事情要避免。排在首位的是宗教符号。另一个有文化内涵的例子是红十字会(在一些国家,红新月会更常见)。避免政治和军事符号也是明智的,因为它们在不同的国家有很大的不同。关键是运用你的判断力,记住人们很容易被冒犯。
#### 数字
数字可能是一个棘手的问题——无论是打印还是解释。`QLocale`类可以处理不同的负号、小数点、组分隔符、指数字符和代表零的字符。所有这些都给了你很多出错的细节。
根据我的经验,关于数字的表示,最常见的混淆问题是用于小数点和组分隔符(将数字分成三组)的字符。以数字 1.234 和 1,234 为例。如何解读这些数字取决于你所在的国家——在一些国家,第一个数字读作*一千二百三十四*;在其他地方,它读作*一点二三四*。加两个小数更好,但不完美:1.234,00 和 1,234.00。两者都有效,但是小数点和组分隔符不同。
* * *
**提示**能够处理系统的小数点字符非常重要。不同的键盘在数字小键盘上有不同的小数点字符。不得不在数字键盘和主键盘之间移动来写一个小数点,这可能*非常*烦人。
* * *
使用`QLocale`类及其方法`toString`将数字转换成文本;使用`toFloat`、`toInt`等将字符串转换为数字。虽然这适用于处理显示给用户的数字和字符串,但是在将数字作为文本存储在文件中时,请记住坚持一种格式,因为文件可以在不同的国家之间移动(并且无论当前的语言环境如何,您仍然必须能够正确地读取数字)。
* * *
**提示**系统区域设置`QString::toDouble`和好友用于将字符串转换为数值。
* * *
清单 10-13 显示了一个使用给定的`QLocale`来转换和打印三个值的函数。给定一个`QLocale( QLocale::Swedish, QLocale::Sweden )`和一个`QLocale( QLocale::English, QLocale::UnitedStates )`的函数的输出可以在清单 10-14 中看到。注意使用的不同小数点和组分隔符。
**清单 10-13。** *使用给定的区域设置打印三个值*
`void printValues( QLocale loc )
{
QLocale::setDefault( loc );
double v1 = 3.1415;
double v2 = 31415;
double v3 = 1000.001;
qDebug() << loc.toString( v1 );
qDebug() << loc.toString( v2 );
qDebug() << loc.toString( v3 );
}`
**清单 10-14。** *使用不同的语言环境打印相同的三个值*
Swedish
“3,1415”
“31 415”
“1 000”
US English
“3.1415”
“31,415”
“1,000”
**货币**
处理货币是不借助 Qt 也要做的事情。这没什么,因为货币可以被视为一个有限精度的数字——通常是两位小数,但有时没有或只有三位。
当您向用户显示货币值时,记住一些基础知识是很重要的。首先,您可以在值(例如,280,00 SEK 或 8.75 美元)后面加上三个字母的货币代码(ISO 4217)。请注意,我根据示例中的货币使用了适当的小数点符号。(当然,您应该根据用户的偏好选择一个小数点符号。)
所有的货币都有名字。例如, *SEK* 是瑞典克朗的简称或者只是克朗(复数为克朗)。这也是可以放在被呈现的值之后的东西。
一些货币有一个标志或符号,可以用来代替在值后加上代码或名称。这个符号可以放在值的前面,也可以放在值的后面,或者作为小数点符号。例如 12.50 英镑和€12.50 欧元。有更多的符号可用于其他货币。一些符号是普遍使用的,而另一些只在使用该货币的当地市场使用。
从国际化的角度来看,我建议使用 ISO 4217 代码,因为它是中立的(代码是国际标准的一部分)并且易于处理(代码总是跟在值后面)。
#### 日期和时间
在全球范围内,日期和时间以多种不同的方式呈现,这对开发人员来说是一个困难的挑战。尽管 Qt 提供了处理复杂性的类,但是存在误解用户输入和通过输出混淆用户的风险。
让我们先来看看时间以及它是如何呈现给用户的。以文本形式表示的时间通常表示为数字时钟,两位数表示小时,两位数表示分钟。小时和分钟由冒号或简单的点分隔。这里的问题是时钟可以是 24 小时制的,时间从 0 到 23。时钟也可以是 12 小时制,其中时间从 0 到 11 运行两次。在后一种情况下,分钟后面跟 AM 或 PM,表示时间是指早上还是晚上。
你可以使用`QTime`方法`toString`和`fromString`(结合`QLocale`类的`timeFormat`方法)或者直接使用`QLocale`中的`toString`方法,以用户期望的方式处理输入和输出。请确保您不会将 12 小时制的 PM 时间解释为后面跟有一些无意义字符的 24 小时制时间。
清单 10-15 显示了一个使用给定地区打印时间的函数。结果输出如清单 10-16 所示。语言环境是`QLocale( QLocale::Swedish, QLocale::Sweden )`和`QLocale( QLocale::English, QLocale::UnitedStates )`。
**清单 10-15。** *使用不同地区的打印时间*
void printTimes( QLocale loc )
{
QLocale::setDefault( loc );
QTime t1( 6, 15, 45 );
QTime t2( 12, 00, 00 );
QTime t3( 18, 20, 25 );
qDebug() << “short”;
qDebug() << loc.toString( t1, QLocale::ShortFormat );
qDebug() << loc.toString( t2, QLocale::ShortFormat );
qDebug() << loc.toString( t3, QLocale::ShortFormat );
qDebug() << “long”;
qDebug() << loc.toString( t1, QLocale::LongFormat );
qDebug() << loc.toString( t2, QLocale::LongFormat );
qDebug() << loc.toString( t3, QLocale::LongFormat );
qDebug() << “default”;
qDebug() << loc.toString( t1 );
qDebug() << loc.toString( t2 );
qDebug() << loc.toString( t3 );
}
**清单 10-16。** *打印时产生的字符串使用不同的区域设置*
Swedish
short
“06.15.45”
“12.00.00”
“18.20.25”
long
“kl. 06.15.45 W. Europe Daylight Time”
“kl. 12.00.00 W. Europe Daylight Time”
“kl. 18.20.25 W. Europe Daylight Time”
default
“kl. 06.15.45 W. Europe Daylight Time”
“kl. 12.00.00 W. Europe Daylight Time”
“kl. 18.20.25 W. Europe Daylight Time”
US English
short
“6:15:45 AM”
“12:00:00 PM”
“6:20:25 PM”
long
“6:15:45 AM W. Europe Daylight Time”
“12:00:00 PM W. Europe Daylight Time”
“6:20:25 PM W. Europe Daylight Time”
default
“6:15:45 AM W. Europe Daylight Time”
“12:00:00 PM W. Europe Daylight Time”
“6:20:25 PM W. Europe Daylight Time”
说到表示日期,还有其他问题要处理。月份在不同的国家有不同的名称,一周中的日子也是如此。书写日期时,不同国家的日、月、年的顺序不同。让事情变得更复杂的是,一周的第一天可以是周日,也可以是周一,这取决于你所在的位置。为了帮助您管理这些,`QLocale`类可以处理大部分问题。
通过使用来自`QDate`类的`toString`和`fromString`方法和来自`QLocale`的`dateFormat`方法,或者直接使用`QLocale`的`toString`方法,您可以正确地表示和解释日期。
为了比较区域设置`QLocale( QLocale::Swedish, QLocale::Sweden )`和`QLocale( QLocale::English, QLocale::UnitedStates )`对日期格式的影响,我使用了清单 10-17 中所示的函数。结果输出可以在清单 10-18 中看到。
**清单 10-17。** *使用不同地区打印日期*
void printDates( QLocale loc )
{
QLocale::setDefault( loc );
QDate d1( 2006, 10, 12 );
QDate d2( 2006, 01, 31 );
QDate d3( 2006, 06, 06 );
qDebug() << “short”;
qDebug() << loc.toString( d1, QLocale::ShortFormat );
qDebug() << loc.toString( d2, QLocale::ShortFormat );
qDebug() << loc.toString( d3, QLocale::ShortFormat );
qDebug() << “long”;
qDebug() << loc.toString( d1, QLocale::LongFormat );
qDebug() << loc.toString( d2, QLocale::LongFormat );
qDebug() << loc.toString( d3, QLocale::LongFormat );
qDebug() << “default”;
qDebug() << loc.toString( d1 );
qDebug() << loc.toString( d2 );
qDebug() << loc.toString( d3 );
}
**清单 10-18。** *使用不同语言环境打印日期时产生的字符串*
Swedish
short
“12 okt 2006”
“31 jan 2006”
“6 jun 2006”
long
“torsdag 12 oktober 2006”
“tisdag 31 januari 2006”
“tisdag 6 juni 2006”
default
“torsdag 12 oktober 2006”
“tisdag 31 januari 2006”
“tisdag 6 juni 2006”
US English
short
“Oct 12, 2006”
“Jan 31, 2006”
“Jun 6, 2006”
long
“Thursday, October 12, 2006”
“Tuesday, January 31, 2006”
“Tuesday, June 6, 2006”
default
“Thursday, October 12, 2006”
“Tuesday, January 31, 2006”
“Tuesday, June 6, 2006”
注意在清单 10-14 和清单 10-18 中,默认格式都是长格式。如果我必须在长格式和短格式之间做出选择,我会认为短格式在大多数情况下更容易阅读(除非我真的需要关于工作日和时区的所有细节)。
#### 救命
Qt 附带的翻译工具捕获了您提供的大部分帮助:工具提示、状态消息和这是什么字符串,只要它们包含在`tr`调用中就可以找到。不要忘记您的在线帮助文档。您必须负责翻译您的帮助文档,并确保在用户请求帮助时显示正确的语言。不是很复杂;这只是一些你不能忘记的事情,因为 Qt 工作流没有捕捉到它。
### 总结
国际化和本地化不仅仅是翻译应用程序。您不能再依赖于许多您认为理所当然的东西:日期格式、时间格式、数字格式、用户理解的图标、合法字符等等。这个过程实际上是关于理解目标文化及其习俗。这就是为什么在全球范围内部署应用程序是一项如此艰巨的任务。
通过使用`lupdate`、`lrelease`和语言学家以及`QLocale`类,您已经取得了很大的进步。尽可能将您的文本放在`QString`和`QChar`中,以确保使用 Unicode(使您不必总是考虑字符编码)。
在部署之前,请确保在所有目标语言环境中进行测试。如果可能的话,尝试使用本地测试人员——他们可能会比你发现更多的错误。**