如何在所有桌面平台上使用 Qt 实现美观
Qt 工具包最初的设计不仅是为了易于使用,而且还允许在不同平台之间移动应用程序源代码。如今,三大主要桌面环境都得到了支持:X11、OS X 和 Windows。由于可移植性是该工具包的关键目标之一,因此它很少遇到常见问题,例如特定平台上缺少功能或应用程序在某些环境中无法很好地集成。
Qt 的成名之旅实际上始于十多年前的 KDE 项目。作为 KDE 的基石之一,Qt 后来的版本尝试与 GTK+ 和 GNOME 集成,这可能会让您感到惊讶。它甚至允许整合 glib 事件循环,所有这些都是为了实现提供在所有平台上看起来和感觉都正确的可移植代码的使命。
在讨论可移植的 GUI 源代码时,首先想到的可能是图形用户界面。提供在所有平台上看起来都正确的窗口小部件是一项工程壮举。这需要许多技巧才能使用原生绘制方法、适应样式并总体上融入其中。再加上子类化和自定义窗口小部件的能力,您需要整合相当多的东西。
而且,要使应用程序在视觉上在所有平台上都感觉正确,需要做更多的工作。边距、间距、对齐方式——甚至某些窗口小部件的顺序——都需要考虑在内。Qt 解决了所有这些问题。一个基本的对话框窗口可以用来演示如何做到这一点。
图 1 显示了一个属性对话框,左侧是一组标签,右侧是用于编辑的字段。底部是标准的“帮助”、“应用”、“确定”和“取消”按钮。这看起来可能是一个简单的对话框,但请将其与图 2 和图 3 进行比较。它是同一个对话框,但在不同的平台上。
平台强制对话框底部按钮的顺序、属性标签的对齐方式以及表示属性值的字段的扩展策略。所有这些都需要根据当前平台的规则进行处理。
在某些情况下,盲目地遵循当前平台的外观和感觉并不是您想要的。有时您可能想要巧妙地向用户提供提示。例如,您可能想要突出显示所有必填字段或更改进度条的颜色。通常,这意味着子类化源窗口小部件以使其专门化。然后,您将对所有必填字段使用您的特殊窗口小部件。现在,想象一下不仅有文本字段,还有复选框、下拉列表等等。
在 Qt 中,您可以通过两种方式解决此问题。您可以创建一个自定义调色板对象,并将其应用于所有您想要突出显示或更改颜色的字段。或者,您可以使用样式表。
使用样式表的优点是它们允许更高级的操作。图 4 分三个步骤显示了这一点。顶行的小部件使用标准样式,第二行使用以下样式表
QLineEdit { background-color: rgb(255, 255, 185); } QCheckBox::indicator:unchecked { image: url(:/images/cb-unchecked.png); } QRadioButton::indicator:unchecked { image: url(:/images/rb-unchecked.png); }

图 4. 从标准样式到极限
如您所见,该语法在很大程度上受到了 Web 设计中使用的层叠样式表 (CSS) 的启发。文本字段是 QLineEdit 类的一个实例。对于它,只需指定背景颜色就足够了。对于单选按钮和复选框,您需要提供表示指示器的图像。此处需要包含比未选中状态更多的状态,但为了简化此示例,已省略了这些状态。
仅仅更改背景颜色可以通过更改特定窗口小部件的调色板来轻松实现。但是,图 4 中的最后一行表明您可以更进一步。此处使用的样式表更改了字体、文本颜色、边框和背景。对于 QLineEdit 类,样式表如下所示
QLineEdit { color: red; font: 75 14pt "DejaVu Sans"; border: 2px solid rgb(0, 112, 157); border-radius: 3px; background: qlineargradient(spread:pad, ↪x1:0, y1:0, x2:0, y2:1, ↪stop:0 black, stop:1 rgb(0, 112, 157)); }
如您所见,颜色更改不仅限于纯色。背景是渐变色,并且边框的整体形状已更改——所有这些,同时仍然保持源代码的跨平台可移植性。
到目前为止,我们讨论的内容仅影响视觉效果。您可以在 Qt Designer 或 QtCreator 中尝试所有这些操作,而无需编写一行源代码(不包括样式表)。但是,跨平台编程不仅仅是外观和感觉。例如,如何在多个平台上遍历文件系统,而无需为每个平台提供唯一的源代码?
Qt 为此提供了类。例如,以下简短代码片段显示了给定计算机的每个驱动器的根目录中包含的目录。在 Windows 机器上,它逐个列出驱动器,而在 UNIX 机器上,它仅列出根驱动器 /(请注意,foreach 是 Qt 提供的 C++ 宏)
foreach( QFileInfo drv, QDir::drives() ) { qDebug( "%s contains", qPrintable(drv.absolutePath()) ); foreach( QString name, drv.absoluteDir().entryList( QDir::Dirs ) ) { qDebug( " %s", qPrintable(name) ); } }
通过使用 QDir 类访问文件系统,您可以以平台无关的方式执行此操作。该类包含用于常见入口点的静态函数,例如驱动器、用户的主目录、当前目录以及用于临时文件的系统目录。
跨平台问题的另一个常见来源发生在更基本的层面上——文本和数据的编码。Qt 提供了一个自定义类来处理名为 QString 的文本字符串。它在所有平台上提供 Unicode 表示。字符串类本身可以与 UTF-8、ASCII 和 Latin1 之间进行转换。它还可以使用文本编解码器与大多数其他字符串表示形式之间进行转换。Qt 附带了各种编解码器,但也可以创建自定义编解码器来处理特殊情况。
当从文件中读取和写入文本时,通过使用 QTextStream 类来尊重编码。此类提供基于 << 和 >> 运算符的流接口。它通常会自动检测编码,但您可以使用 setCodec 函数将其强制设置为特定设置。为了说明这一点,以下简短的代码片段从以 UTF-32 编码的大端系统上的文本文件中读取一行
QTextStream stream( &file ); stream.setCodec( QTextCodec::codecForName("UTF-32BE") ); QString myString = stream.readLine();
说到字节序,这通常是在处理跨平台代码时出现的问题。字节序的问题在于,当您写入二进制数据(例如 32 位值(四个字节))时,您可以选择以两个不同的方向写入字节:从左到右或从右到左,即大端和小端。
写入字节的默认顺序取决于程序运行所在系统的字节序。某些架构(例如 IA32 和 VAX)使用小端序。其他架构(例如 PowerPC、ColdFire 和 SPARC)使用大端序。还有一些架构(例如 ARM、MIPS、IA64 和 Sparc V9)能够执行任一操作(尽管通常必须在构建硬件时将使用哪种字节序硬连线到系统中)。基于这些架构中大多数架构的系统通常是 Qt 的目标。
为了确保二进制数据的跨平台兼容性,您需要在写入时显式指定顺序,并在读取时再次指定顺序。通过使用 QDataStream 处理二进制文件格式,字节序不再是问题。您只需指定要使用的字节顺序,然后使用流运算符,它就可以正常工作。
下面的代码片段显示了这一点。它还包含 setVersion 函数,让您可以指定要使用的 Qt 复杂数据类型编码的版本。例如,如果颜色内部表示在 Qt 版本 2 和 4 之间发生了更改,通过指定旧版本,您仍然可以使用相同的流类以旧格式读取和写入数据。这在处理来自现代代码的旧版文件格式时非常方便
QDataStream stream( &file ); stream.setByteOrder( QDataStream::BigEndian ); stream.setVersion( QDataStream::Qt_4_0 ); int value; stream >> value;
在处理用户偏好设置时,Windows 有注册表。UNIX 系统通常依赖于隐藏目录,每个应用程序一个。OS X 有一种用于偏好设置的 XML 格式。这对用户来说很好。他们通常不依赖于能够在他们的计算机之间移动他们的偏好设置,特别是如果他们不使用相同的操作系统。从软件开发人员的角度来看,情况有所不同。
为了解决这个问题,Qt 提供了 QSettings 类。它提供了对每个平台首选方法的访问。它也可以用于在平台系统外部创建和读取 INI 文件,用户可以在平台之间移动这些文件。
QSettings 类依赖于应用程序的名称和应用程序提供商。然后,您只需使用 setValue 和 value 函数进行写入和读取。返回的值是 QVariant 类型。此类型可用于保存任何类型的数据。基本类型(例如整数)直接处理。更复杂的类型(例如 QColor)依赖于数据流运算符
QSettings settings( "The App Company", "The App" ); int v = settings.value("myInt").toInt(); QColor c = settings.value("myColour").value<QColor>();
在平台之间移动代码时,会出现更多问题。Qt 的解决方案是提供 Qt API。此 API 几乎消除了特定平台的所有痕迹,同时尝试支持每个相关平台上的所有功能。比此处显示的更复杂的情况涉及多线程、数据库访问、网络等等。
到目前为止,本次讨论仅侧重于在不同桌面之间移动代码,这只是 Qt 雄心壮志的一半。Qt 有三种嵌入式版本:嵌入式 Linux、Windows CE 和 Symbian S60。
Windows CE 和 S60 端口使得在手机和平板电脑上运行 Qt 应用程序成为可能。每个端口都考虑了目标设备的样式,并将应用程序无缝集成。在撰写本文时,S60 端口仅作为技术预览版提供;完整版本计划于 2009 年晚些时候发布。
嵌入式 Linux 版本使得可以直接在帧缓冲区上运行 Qt。这大大减少了系统的占用空间,使其可嵌入。窗口需求由集成的窗口管理器 QWS(Qt 窗口系统)覆盖,但通常,这些系统以全屏模式运行其应用程序。
一个有趣的功能是能够在虚拟帧缓冲区中运行应用程序,从而可以在开发机器上模拟正确的分辨率、位深度和输入行为。这使您可以在项目周期的早期开始开发软件。它还可以简化调试,因为您可以避免远程调试。
从桌面到嵌入式系统的迁移步骤通常比在桌面或嵌入式系统之间迁移要大。框架无法解决许多问题。最常见的问题是可用屏幕空间、计算能力不足和内存不足。随着嵌入式系统的功率、内存和屏幕分辨率的提高,所有这些领域都变得不再那么令人担忧。
Qt 提供了样式化和拉伸界面以适应屏幕的能力。您还可以设置全局支柱。这是任何用户界面元素可以拥有的最小尺寸。通过调整此因素,您可以调整窗口小部件,使其可以使用手指、触控笔或鼠标。
Qt 提供了一个可以在各种平台上使用的 API。所有主要桌面平台都得到了支持,主要的可嵌入平台也得到了支持。Qt 的优势在于可以通过一个 API 访问所有这些平台。该 API 由一个库、一组目标和一种构建 API 的方法提供。为了充分利用 Qt 的跨平台能力,您应该在所有领域都拥抱 Qt 的使用。如果您这样做,您可以像编译代码一样轻松地移动代码。
通过可移动 API 使用平台特定功能
Qt 可能会提供一个可以覆盖几乎所有情况的跨平台 API,但您仍然可能想要使用平台特定功能。例如,在 Windows 中以最大化方式打开窗口,在 OS X 和 X11 中以正常方式打开窗口。为了处理这些情况,Qt 提供了预处理器定义,描述您正在运行的操作系统以及您正在使用的窗口系统。例如,在 Linux 上,您会找到 Q_OS_LINUX,可能还会找到 Q_WS_X11。
当您知道您正在哪个系统上运行时,您可以通过重新实现 QApplication 类的 x11EventFilter 函数来访问所有 X11 事件。在 OS X 上,您可以从每个 QWidget 的 macCGHandle 函数中获取 CoreGraphics 句柄。
如果您想避免编写平台特定代码,您仍然可以提供平台特定提示。例如,您可以向 QDialog 提供提示,表明它是一个工作表。这是一个出现在另一个窗口或对话框内的对话框,它提供了较大窗口的部分功能。您可以通过将对话框的窗口标志设置为 Qt::Sheet 来执行此操作。
在 X11 上,这种类型的提示依赖于窗口管理器理解它的能力。这意味着提示必须用作提示,而不是设置。如果您想要完全控制,请传递 Qt::X11BypassWindowManagerHint。这试图完全避免窗口管理器,这不是一件好事,但可能是必要的。
使用跨平台环境进行跨平台开发
Qt 附带了一组工具,这些工具可以单独使用,也可以从相当新的 QtCreator 应用程序中使用。QtCreator 是使用 Qt 创建的,并提供了一个高级代码编辑器、文档、Qt Designer 的集成版本以及 Qt 特定文件(例如项目文件和资源文件)的编辑器。
由于所有 Qt 工具也可以单独使用,因此通常使用另一个 IDE 或仅使用文本编辑器和命令行。Qt Software 为 Microsoft Visual Studio、Xcode 和 Eclipse 提供了集成。还有一系列免费的 IDE 项目,例如 Edyuk、QDevelop 和 KDevelop。
那么,QtCreator 提供了其他工具没有提供的什么呢?首先,它是 Qt SDK 的一部分。Qt 的 SDK 版本作为单个下载提供,其中预构建版本的 Qt 和 QtCreator 已设置好并可以使用。其次,它提供了一个图形调试器界面,让您可以在 Qt 支持的所有桌面平台上以最简单的方式使用 gdb。调试器了解 Qt,并提供宏,以便轻松查看 QString 对象以及查看 Qt 的列表类内部。
Johan Thelin 自 1995 年以来一直从事软件开发工作,自 2000 年以来一直从事 Qt 工作。在见证了服务器端企业软件、桌面应用程序和 Web 解决方案之后,他现在作为一名顾问,专注于嵌入式系统。可以通过 johan@thelins.se 联系 Johan。