鼠标与键盘之间的编码,第二部分
在本系列的第一部分[LJ,2002 年 9 月]中,我们使用 Qt Designer 创建了一个小型文本编辑器的 GUI。现在,我们通过使用我们最喜欢的编辑器以及 make 和 g++ C++ 编译器来添加缺失的功能并将应用程序翻译成其他语言,并使用 Qt Linguist 作为翻译期间吸引人的工作环境。
我们使用新的 qmake 实用程序为我们的 Qt 应用程序编写 Makefile;qmake 取代了 tmake,tmake 是在旧版 Qt 中广泛使用的 Perl 工具。
我们的编辑器 GUI 已经可以关闭窗口和整个应用程序,但我们仍然缺少一个用户对话框,询问是否应保存或放弃旧数据,或者用户是否希望保留旧文件。“新建”、“保存”和“另存为”操作目前没有任何作用,但其他一切都已完成。GUI 已经支持复制、剪切和粘贴、撤消和重做。它可以将字体特征切换为斜体、粗体、下划线以及这三者的任意组合。这些 QTextEdit 操作已经可以在 Qt Designer 预览中进行测试。“帮助”菜单中的“关于”条目将在我们将 GUI 编译成 C++ 程序后完全可用。
我们可以使用 Qt Designer 的内置代码编辑器编写其余函数。但是,由于无法在 Designer 中编译项目,因此使用自己喜欢的文本编辑器会更快。
现在我们已经在 ui 文件中准备好了用户界面描述,并且 ui.h 文件中包含了已编写的代码,现在是时候实现剩余的功能了。首先,我们必须使用用户界面编译器 uic 将 XML 转换为 C++,但使用 qmake,我们无需担心此细节。让我们将 Designer 生成的项目文件提供给它
qmake -o Makefile lj-article.pro
将创建一个 Makefile,其中包含一个名为 .ui 的子目录,该子目录应该存储从 ljeditor.ui 和 ljeditor.ui.h 文件生成的 C++ 代码。如果此操作失败并且要求您设置 QMAKEPATH,请将此变量设置为 Qt 安装的 mkspecs 子目录,该子目录描述了您的操作系统和编译器。例如
QMAKEPATH=$QTDIR/mkspecs/linux-g++ export QMAKEPATH根据您安装 Qt 的位置,请确保搜索路径包含所用 Qt 版本的 $QTDIR/bin 目录。
要生成 C++ 文件,只需键入
make .ui/ljeditor.h make .ui/ljeditor.cpp
如果出现问题,请检查环境变量 QTDIR、PATH 和 LD_LIBRARY_PATH。第一个变量应指向 Qt 3.0 子目录 lib 和 include 的父目录。uic、qmake 和 designer 所在的目录应包含在路径中,并且 $QTDIR/lib 应添加到链接器路径。
编辑这两个生成的文件意味着当 ui 文件移动到 Designer 并且需要进行新的转换时,所做的更改将丢失。因此,我们从 ljeditor 派生出一个子类,并将我们的更改添加到其中,而不是添加到 ljeditor。
uic 提供命令行开关 -subdecl classname 和 -subimpl classname 来构建适当的代码框架。使用
uic -o editor.h -subdecl Editor .ui/ljeditor.h \ ljeditor.ui
我们获得了 editor.h,即新 Editor 子类的头文件。另一方面(注意参数头文件),以下行在 editor.cpp 中创建实现框架
uic -o editor.cpp -subimpl Editor editor.h \ ljeditor.ui这些新文件需要通过添加两行添加到项目文件中
HEADERS += editor.h SOURCES += editor.cpp到 lj-article.pro。或者您可以启动 Qt Designer,打开项目文件并通过“项目”®“添加文件”添加它们。请记住将“文件类型”设置为“C++ 文件”,否则文件对话框将找不到它们)。如果您喜欢 Designer 中包含的文本编辑器,您甚至可以在那里编辑它们。
uic 生成的子类代码始终包含父类中存在的所有函数的框架。因此,最好删除您不打算在子类中重新实现的所有函数的声明和函数框架。删除以下行
void fileExit(); void helpAbout(); void fileClose();
从 editor.h 和 editor.cpp 中的相关代码框架中删除,这些框架如下所示
void Editor::fileExit() { qWarning( "Editor::fileExit() " "not yet implemented!" ); }对于完整的程序,我们仍然需要一个 main() 函数。我们可以手动编写它,但 Qt Designer 可以为您提供一些帮助。从菜单中选择“文件”®“新建”®“C++ Main-File (main.cpp)”以及后续对话框。
图 1 中显示的对话框要求我们命名主文件(我们选择 ljedit.cpp)和主窗口部件。Designer 在此处不提供 Editor 子类;因此我们别无选择,只能选择 ljeditor。

图 1. 配置主文件
选择此名称意味着我们必须更正生成的代码。我们包含 editor.h 而不是 ljeditor.h,并且我们需要 Editor 对象而不是创建 ljeditor 类的新对象。现在我们的 ljedit.cpp 应该如下所示
#include <qapplication.h> #include "editor.h" int main( int argc, char ** argv ) { QApplication a( argc, argv ); Editor *w = new Editor; w->show(); a.connect( &a, SIGNAL( lastWindowClosed() ), &a, SLOT( quit() ) ); return a.exec(); }
与每个常见的 Qt main() 一样,我们创建一个 QApplication 对象,将其传递给可能的命令行参数 (argv) 并创建 Editor 窗口部件 w。然后我们将其展示给世界,并通过 a.exec() 进入应用程序循环。
您可能会注意到缺少定义应用程序主窗口部件的 a.setMainWidget( w ); 行(我们稍后会解释这一点)。但是,如果没有主窗口部件,则应用程序在最后一个窗口关闭时不会退出。因此,我们必须将应用程序对象的信号 lastWindowClosed() 连接到其 quit() 槽。
项目目录中的 make 和后续的 ./lj-article 应该会产生一个正在运行但功能尚未完全完善的文本编辑器。如果您希望使用项目文件基本名称以外的其他名称调用二进制文件,请将行 TARGET = ljedit 添加到 lj-article.pro。
基本上,我们不会做太多事情,只是在我们最喜欢的文本编辑器中使用合理的功能替换 editor.cpp 中说“尚未实现”的 qWarning()。如果 Designer 的内置代码编辑器适合您的需求,您不必担心子类化,但编译和调试会变得更加痛苦,因此我们决定不这样做。
清单 1 显示了 editor.h 的全部内容;清单 2 是 editor.cpp 的摘录。所有清单均可从 Linux Journal FTP 站点 [ftp://ftp.linuxjournal.com/pub/lj/listings/issue102/4722.tgz] 获取。
除了要实现的四个槽之外,我们还重新实现了 closeEvent(),这是一个在窗口部件关闭时调用的函数,因为我们希望用户确认编辑器窗口的关闭,以免意外丢失数据。为了清晰起见,相关的用户对话框在单独的函数 saveAndContinue() 中实现。
此外,我们引入了两个类变量:fileName,用于存储当前编辑文件的文件名,以及 editField,用于保存 QTextEdit 窗口部件的副本。为这些变量提供初始值是 Editor 构造函数的唯一任务。
另一个简单的任务是实现 fileNew() 槽。它创建一个新的 Editor 窗口并显示它。这就是我们不将第一个编辑器窗口作为应用程序的主窗口部件的原因:如果我们这样做,关闭第一个编辑器窗口也会导致所有其他窗口关闭。
但是,当用户关闭窗口或整个应用程序时会发生什么?重新实现的 closeEvent() 调用 saveAndContinue() 并带有一个参数:当用户决定中止关闭过程时应向用户显示的消息(第 159 行)。与所有文本字符串一样,我们用 tr() 将其括起来,以实现本地化。如果 saveAndContinue() 返回“是,继续”,则接受关闭事件;否则,该事件将被驳回。
如果用户已为编辑器内容选择了文件名或在 QTextEdit 窗口部件中输入了一些文本,则可以安全地假设他或她可能希望保留工作。在这种情况下,saveAndContinue() 会弹出一个消息框,使用文件名作为窗口标题,询问:保存文件名?提供三个回复按钮:是、否和取消。内容文件名的 %1 占位符的稍微冗长的表示法对于国际化很重要:其他语言的单词顺序不同,并且翻译人员必须有机会将文件名放在其他位置,例如短语的开头。
如果按下“是”按钮(第 181 行),则编辑器内容将保存在给定的名称下。如果未设置文件名,则 fileSaveAs() 会在存储之前询问文件名。如果答案为“否”,则所有未保存的更改都将丢失。用户将在状态栏中收到 2,000 毫秒的通知(第 187 行)。“取消”按钮会在状态栏中显示中止消息,并且 saveAndContinue() 返回“否,不要继续”(第 194 行)。在没有文件名且没有编辑器内容的情况下,不会出现消息框。然后返回值是 TRUE,“无论如何都继续”(第 201 行)。
saveAndContinue 也从 fileOpen() 槽调用。如果编辑器窗口包含文本或先前定义了文件名,则用户有机会在在此窗口中打开另一个文件之前保存它。
无论是否保存,清除编辑器内容并重置窗口标题都会使用户意识到旧的编辑器内容已消失。借助显示当前目录 (.) 内容的文件对话框,用户有机会选择新文件。
借助每个文本字符串周围的 tr() 函数,我们能够以其他语言发布该程序。此步骤仅需要修改 main() 函数和实际翻译。后者可以使用 Qt Linguist 轻松完成(图 2)。但是在您或其他翻译人员开始之前,我们必须获取要翻译的文本字符串。
首先,我们将另一个变量 TRANSLATIONS 添加到 *.pro 文件中
TRANSLATIONS = ljedit_de.ts \ ljedit_no.ts
此示例说明该应用程序将翻译成德语 (ljedit_de.ts) 和挪威语 (ljedit_no.ts)。包含翻译的文件基本名称以“de”或“no”之类的语言环境缩写结尾非常重要。
选择应用程序所需的翻译语言后,键入 lupdate lj-article.pro 将创建两个 XML 文件,可以将其加载到 linguist 中。现在,翻译人员翻译每个字符串,并在完成后将黄色问号切换为绿色勾号。一旦“范围”窗口(在图 2 的左侧)显示所有类都已完全翻译,则该任务已完成。
专业的翻译人员会在实际开始翻译程序之前编译常用短语翻译对的短语手册。由于 Linguist 目前不允许用户直接将翻译后的字符串从程序复制到短语手册中,因此对于那些不经常翻译的人来说,编译短语手册的工作量太大。这很遗憾,因为短语手册支持在应用程序内甚至在整个程序范围内一致地使用短语。但是,即使没有短语手册,Linguist 也提供质量控制开关。验证®加速键—如果开启—确保加速键(标有 &)在翻译过程中不会被遗忘。另一方面,验证®结尾标点符号检查翻译后的字符串是否以与原始字符串相同的标点符号结尾。
当保存最后一个翻译后的短语时,“文件”®“发布...”会将翻译编译为程序可以使用的 *.qm 二进制文件。要发布整套翻译后的 *.ts 文件,可以使用 lrelease lj-article.pro 代替。如果在之后更改了代码,则新的 lupdate 运行会将相关更改与项目文件中列出的 *.ts 文件集成,并且 lrelease 会更新二进制版本。
清单 3 显示了国际化的 main()。新的是包含 qtranslator.h 和 qtextcodec.h。根据使用的语言环境(由环境变量 LANG 定义),编译翻译文件的基本名称(第 13 行)。如果 LANG 设置为例如 de 或 de_DE,则应用程序将在 /local/lib 中查找名为 ljedit_de 或 ljedit_de.qm 的文件。如果找不到,则使用原始语言版本。不幸的是,没有简单的方法来搜索多个目录和/或避免硬编码的目录名称。
如果找到翻译文件,则 QTranslator 对象会加载它,并将其安装以服务于应用程序(第 15 行)。图 3 显示了 ljedit 的德语版本。