QT 开发基础知识(一)

原文:Foundations of Qt Development

协议:CC BY-NC-SA 4.0

一、C++ 的 Qt 方式

Qt 是一个跨平台、图形化的应用开发工具包,使您能够在 Windows、Mac OS X、Linux 和不同品牌的 Unix 上编译和运行您的应用。Qt 的很大一部分致力于为一切事物提供平台中立的接口,从在内存中表示字符到创建多线程图形应用程序。


注意尽管 Qt 最初是为了帮助 C++ 程序员而开发的,但是绑定也适用于许多语言。Trolltech 提供了 C++、Java 和 JavaScript 的官方绑定。第三方提供了许多语言的绑定,包括 Python、Ruby、PHP 和。NET 平台。


本章从一个普通的 C++ 类开始,将它与 Qt 集成在一起,使它更易于重用和使用。在这个过程中,您将看到用于编译和链接 Qt 应用程序的构建系统,以及在您的平台上安装和设置 Qt。

本章然后讨论 Qt 如何使你能够构建能够以非常灵活的方式互连的组件。这就是 Qt 如此强大的原因——它使得构建可重用、可交换和可互连的组件变得容易。最后,您将了解 Qt 提供的集合和助手类。

安装 Qt 开发环境

在开始开发 Qt 应用程序之前,您需要下载并安装 Qt。您将使用 Qt 的开源版本,因为它对所有人都是免费的。如果您有 Qt 的商业许可证,您会收到它的安装说明。

根据您计划用于开发的平台,安装过程会略有不同。因为 Mac OS X 和 Linux 都基于 Unix,所以两者的安装过程是相同的(以及所有的 Unix 平台)。另一方面,Windows 则不同,它是单独介绍的。从[www.trolltech.com/products/qt/downloads](http://www.trolltech.com/products/qt/downloads)下载适合自己平台的版本就可以启动所有三个平台。

在 Unix 平台上安装

除了 Windows 之外的所有平台都可以说是 Unix 平台。然而,Mac OS X 不同于其他系统,因为它不使用 X 窗口系统,通常称为 X11,来处理图形。所以 Mac OS X 需要一个不同的 Qt 版本;必要的文件(qt-mac-opensource-src- version .tar.gz)可以从奇趣下载。基于 X11 的 Unix 平台使用来自 Trolltech 的qt-x11-opensource-src- version .tar.gz文件。


注意 Qt 依赖于其他组件,如编译器、连接器和开发库。根据 Qt 的配置方式,需求会有所不同,因此如果遇到问题,您应该研究参考文档。


下载完文件后,过程是这样的:解包、配置和编译。让我们一个接一个地完成这些步骤。最简单的方法是在命令提示符下工作。

要解压缩该文件,请下载它,将其放在一个目录中,然后在您的命令 shell 中进入该目录。然后输入如下内容(用 x11mac 代替版本,并使用你已经下载的版本):

tar xvfz qt-edition-opensource-src-version.tar.gz
这段代码将文件解压到一个名为`qt-`edition` -opensource-src- `version`` 的文件夹中。使用`cd`命令进入该目录:
cd qt-`edition`-opensource-src-`version`
在构建 Qt 之前,您需要使用`configure`脚本及其选项来配置它。像这样运行脚本:
./configure `options`
有许多选项可供选择。最好的起点是使用`-help`,它会向您显示可用选项的列表。大多数选项通常可以保留为默认选项,但是`-prefix`选项很好用。您可以通过在选项后指定一个路径来指示安装转到特定位置。例如,要在您的主目录中的一个名为`inst/qt4`的目录中安装 Qt,使用下面的`configure`命令:
./configure -prefix ~/inst/qt4
Mac OS X 平台还有另外两个值得注意的选项。首先,添加`-universal`选项使用 Qt 创建通用二进制文件。如果您计划使用基于 PowerPC 的计算机进行开发,您必须添加`-sdk`选项。
`configure`脚本还让您接受开源许可(除非您有商业许可),然后检查所有的依赖项是否都已就绪,并开始在源代码树中创建配置文件。脚本完成后,您可以使用以下命令构建 Qt:
make
这个过程需要相对较长的时间来完成,但是完成之后,您可以使用下一行来安装 Qt:
make install

 **注意**如果你试图在你的主目录之外安装 Qt,安装命令可能需要 root 权限。

安装 Qt 后,您需要将 Qt 添加到您的`PATH`环境变量中。如果你使用的编译器不支持`rpath`,你也必须更新`LD_LIBRARY_PATH`环境变量。
如果您在运行`configure`时使用了`$HOME/inst/qt4`前缀,您需要添加`$HOME/inst/qt4/bin`到`PATH`的路径。如果您使用的是 bash shell,请使用赋值来更改变量:
export PATH=$HOME/inst/qt4/bin:$PATH
如果您希望在每次启动命令 shell 时都运行这个命令,那么您可以将它添加到您的`.profile`文件中,就在显示为`export PATH`的一行之前。这会将新的`PATH`环境变量导出到命令行会话。

**注意**设置环境变量的方法因 shell 而异。如果您没有使用 bash,请参考参考文档,了解如何为您的系统设置`PATH`变量。

如果您一次安装了几个 Qt 版本,请确保您打算使用的版本首先出现在`PATH`环境变量中,因为使用的`qmake`二进制文件知道 Qt 安装在哪里。
如果您必须更改`LD_LIBRARY_PATH`环境变量,请将`$HOME/inst/qt4/lib`目录添加到变量中。在 Mac OS X 和 Linux(使用 Gnu 编译器集合[GCC])上,不需要这一步。
在 Windows 上安装
如果您计划使用 Windows 平台进行 Qt 开发,请从 Trolltech 下载一个名为`qt-win-opensource-` `version` `-mingw.exe`的文件。这个文件是一个安装程序,它将设置 Qt 和 mingw 环境。

**** *mingw* ,是极简 GNU for Windows 的简称,是 Windows 常用 GNU 工具的发行版。Qt 开源版使用这些工具,包括 GCC 和 make,进行编译和链接。

安装程序就像一个向导,询问你在哪里安装 Qt。确保选择一个没有空格的目录路径,因为这可能会在以后给你带来问题。安装 Qt 后,你会看到一个名为`Qt by Trolltech (OpenSource)`的开始菜单文件夹。该文件夹包含 Qt 工具和文档的条目以及 Qt 命令提示符。从这个命令提示符中访问 Qt 是很重要的,因为它正确地设置了环境变量,比如 ??。简单地运行在开始菜单的`Accessories`文件夹中的命令提示符将会失败,因为变量没有正确配置。
制作 c++“Qt-er”
因为这是一本关于编程的书,你会马上从一些代码开始(见清单 1-1 )**清单 1-1** *一个简单的 C++*
#include <string>

using std::string;

class MyClass

{

public:

  MyClass( const string& text );

  const string& text() const;

  void setText( const string& text );

  int getLengthOfText() const;

private:

  string m_text;

}; 
在清单 1-1 中显示的类是一个简单的字符串容器,带有获取当前文本长度的方法。实现比较琐碎,`m_text`简单设置或者返回,或者返回`m_text`的大小。让我们通过使用 Qt 使这个类更加强大。但是首先,看看已经“Qt 化”的部分:

  • 类名以大写字母开头,单词使用驼峰式大小写分开。也就是说,每个新单词都以大写字母开头。这是命名 Qt 类的常用方法。
  • 这些方法的名称都以小写字母开头,单词也用字母大小写来区分。这是命名 Qt 方法的常用方式。
  • 属性文本的 getter 和 setter 方法被命名为text (getter)和setText (setter)。这是命名 getters 和 setters 的常见方式。

都是 Qt 的特质。这看起来没什么大不了的,但是在实际编写代码时,用结构化的方式命名可以节省大量时间。

继承 Qt

您将对代码进行的第一个特定于 Qt 的调整非常简单:您将简单地让您的类继承QObject类,这将使动态管理类的实例变得更加容易,方法是给实例提供负责删除它们的父类。


注意所有 Qt 类的前缀都是大写的 q。因此,如果你找到了类QDialogDialog,你可以马上判断出QDialog是 Qt 类,而Dialog是你的应用程序或第三方代码的一部分。一些第三方库使用QnnClassName命名约定,这意味着该类属于一个扩展 Qt 的库。前缀中的nn告诉你这个类属于哪个库。例如,类QwtDial属于技术应用程序库的 Qt Widgets,它提供了图形、刻度盘等类。(您可以在附录中找到更多关于这个和其他第三方 Qt 扩展的信息。)


对代码的改动很小。首先,类的定义被稍微修改了一下,如清单 1-2 所示。为了方便起见,parent参数也被添加到构造器中,因为QObject有一个函数setParent,可以用来在创建后将对象实例分配给父对象。然而,通常——也是推荐的——将 parent 作为参数传递给构造器,作为第一个默认参数,以避免必须为从该类创建的每个实例键入setParent

清单 1-2。 继承 QObject 并接受一个父

#include <QObject>

#include <string>

using std::string;

class MyClass : public QObject

{

public:

  MyClass( const string& text, QObject *parent = 0 );

...

};


注意要访问QObject类,必须包含头文件<QObject>。这适用于大多数 Qt 类;简单地包含一个与类同名的头文件,省略掉.h,一切都应该正常工作。


父参数简单地传递给QObject构造器,如下所示:

MyClass::MyClass( const string& text, QObject *parent ) : QObject( parent )

让我们看看变化的影响,从清单 1-3 开始。它显示了一个动态使用MyClass类的main函数,没有 Qt。

清单 1-3。 没有 Qt 的动态记忆

`#include
int main( int argc, char **argv )
{
  MyClass *a, *b, *c;

a = new MyClass( “foo” );
  b = new MyClass( “ba-a-ar” );
  c = new MyClass( “baz” );

std::cout << a->text() << " (" << a->getLengthOfText() << “)” << std::endl;
  a->setText( b->text() );
  std::cout << a->text() << " (" << a->getLengthOfText() << “)” << std::endl;

int result = a->getLengthOfText() - c->getLengthOfText();

delete a;
  delete b;
  delete c;

return result;
}`

每个new调用后必须跟随一个对delete的调用,以避免内存泄漏。虽然在退出main函数时这不是一个大问题(因为大多数现代操作系统在应用程序退出时会释放内存),但是析构函数并没有像预期的那样被调用。在非无循环main函数的位置,当系统用尽空闲内存时,泄漏最终会导致系统崩溃。与清单 1-4 中的相比,清单 1-4 中的使用了一个父类,当main函数退出时,这个父类会被自动删除。家长负责为所有孩子调用delete和——哒哒!—内存被释放。


注意在清单 1-4 所示的代码中,添加了parent对象来展示这个概念。在现实生活中,它可能是一个执行某种任务的对象——例如,QApplication对象,或者(在对话框或窗口的情况下)window类的this指针。


清单 1-4。 用 Qt 动态记忆

#include <QtDebug>

int main( int argc, char **argv )

{

  QObject parent;

  MyClass *a, *b, *c;

  a = new MyClass( "foo", &parent );

  b = new MyClass( "ba-a-ar", &parent );

  c = new MyClass( "baz", &parent );

  qDebug() << QString::fromStdString(a->text())

           << " (" << a->getLengthOfText() << ")";

  a->setText( b->text() );

  qDebug() << QString::fromStdString(a->text())

           << " (" << a->getLengthOfText() << ")";

  return a->getLengthOfText() - c->getLengthOfText();

}

您甚至省去了将计算结果保存在变量中的额外步骤,因为动态创建的对象可以直接从return语句中使用。拥有这样的父对象可能看起来很奇怪,但是大多数 Qt 应用程序使用一个QApplication对象作为父对象。


清单 1-4 从使用std::cout打印调试信息切换到qDebug()。使用qDebug()的好处是它可以在所有平台上把信息发送到正确的地方。关闭也很容易:编译时只需定义QT_NO_DEBUG_OUTPUT符号。如果您有调试消息,在此之后您想要终止应用程序,Qt 提供了qFatal()功能,它的工作方式就像qDebug()一样,但是在消息之后终止应用程序。两者之间的折衷是使用qWarning(),它表示比调试消息更严重的事情,但不是致命的。用于调试消息的 Qt 函数会在每次调用后自动追加一个换行符,因此您不必再包含std::endl


在比较清单 1-3 和清单 1-4 中的代码复杂度时,看看不同的内存情况,如图图 1-1 所示。父实例是灰色的,因为它是在堆栈上分配的,因此会被自动删除,而MyClass的实例是白色的,因为它们在堆上,必须手动处理。因为您使用父级来跟踪子级,所以您信任父级会在删除子级时删除它们。因此,只要根对象在堆栈上(或者如果您跟踪它),您就不必再跟踪动态分配的内存。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-1。 堆栈上有父内存和无父内存的动态内存差异

使用 Qt 字符串

使用 Qt 的另一个步骤是用相应的 Qt 类替换 C++ 标准模板库(STL)中的任何类。虽然这不是必需的(Qt 和 STL 一起工作很好),但是它确实可以避免依赖第二个框架。不使用 STL 的好处是,您可以使用与 Qt 相同的容器、字符串和助手,因此最终的应用程序很可能会更小。当在平台和编译器之间移动时,您还可以避免跟踪兼容性问题和与 STL 标准的奇怪偏差——您甚至可以在没有 STL 实现的平台上进行开发。

查看当前的类,发现string类是唯一使用的 STL 类。对应的 Qt 类叫做QString。你可以无缝地混合QString对象和string对象,但是只使用QString意味着性能的提高和更多的特性。例如,QString支持所有平台上的 Unicode,这使得国际用户使用您的应用程序更加容易。

清单 1-5 展示了用QString替换所有出现的string后,你的代码是什么样子。如您所见,对该类的更改非常小。

清单 1-5。 MyClass QString 代替 string

#include <QString>

#include <QObject>

class MyClass : public QObject

{

public:

  MyClass( const QString& text, QObject *parent = 0 );

  const QString& text() const;

  void setText( const QString& text );

  int getLengthOfText() const;

private:

  QString m_text;

};


提示当混合stringQString时,使用QString方法toStdStringfromStdString将 Qt Unicode 格式转换为string类使用的 ASCII 表示。


建立 Qt 程序

编译和构建这个应用程序应该与构建原始应用程序没有任何不同。您所要做的就是确保编译器能够找到 Qt 头文件,并且链接器能够找到 Qt 库文件。

为了以跨平台的方式顺利处理所有这些,Qt 附带了 QMake 工具,它可以为一系列不同的编译器创建 Makefiles。如果您愿意,它甚至会为您创建项目定义文件。

尝试构建一个简单的应用程序。首先创建一个名为testing的目录。然后将来自清单 1-6 的代码放到这个目录中。您可以将该文件命名为任何名称,只要它的扩展名是cpp

清单 1-6。 一个微不足道的例子

`#include

int main( )
{
    qDebug() << “Hello Qt World!”;

return 0;
}`

现在打开一个命令行,将您的工作目录更改为您刚刚创建的目录。然后输入qmake -project并按回车键,这会生成一个名为testing.pro的文件。我的版本如清单 1-7 所示。


提示如果你在 Windows 中运行 Qt 的开源版本,在安装 Qt 时创建的开始菜单文件夹中有一个类似 Qt 4.2.2 命令提示符的应用程序。运行该应用程序并使用cd命令更改目录。例如,首先使用资源管理器定位您的文件夹;然后复制整个路径(应该和c:\foo\bar\baz\testing差不多)。现在在命令提示符下键入cd,后跟一个空格,然后右键单击,选择粘贴,然后按回车键。这应该能让你很快找到正确的工作目录。


清单 1-7。 一个生成的项目文件


`######################################################################

Automatically generated by qmake (2.00a) to 10. aug 17:06:34 2006

######################################################################

TEMPLATE = app
TARGET +=
DEPENDPATH += .
INCLUDEPATH += .

Input

SOURCES += anything.cpp`


该文件由一组使用=设置或使用+=扩展的变量组成。有趣的部分是SOURCES变量,它告诉你 QMake 已经找到了anything.cpp文件。下一步是使用 QMake 生成特定于平台的 Makefile。因为工作目录只包含一个项目文件,只需键入qmake并按回车键。这将为您提供一个 Makefile 和特定于平台的助手文件。


注意在 GNU/Linux 上,结果是一个名为Makefile的文件。在 Windows 上,如果你使用开源版本和 mingw,你会得到MakefileMakefile.ReleaseMakefile.Debug和两个目录:debugrelease


最后一步是从生成的 Makefile 构建项目。如何做到这一点取决于您使用的平台和编译器。你通常应该键入make并按回车键,但是gmake(在 Berkeley Software Distribution[BSD]系统上很常见)和nmake(在微软编译器上)也是其他常见的选择。如果你第一次不能让它工作,试着看看你的编译器手册。


提示运行 Windows 时,应用程序默认不会得到控制台输出。这意味着默认情况下,Windows 应用程序不能向命令行用户写入输出。要查看来自qDebug()的任何输出,您必须在项目文件中添加一行内容CONFIG += console。如果您构建了可执行文件,然后看到了这个提示,请尝试修复项目文件;然后运行make clean,接着运行make。这个过程确保项目被完全重新构建,并且新的配置被考虑在内。


现在剩下唯一要做的就是运行应用程序并观察这条消息:Hello Qt World!。可执行文件将与您使用的目录同名。对于 Windows 用户,可执行文件在发布目录中以文件扩展名exe结束,因此您可以通过运行以下命令来启动它:

release\testing.exe
在其他平台上,它通常直接位于工作目录中,因此您可以通过键入以下命令来启动它:
./testing
在所有平台上,结果都是一样的:将`Hello Qt World!`消息打印到控制台。在 Windows 平台上产生的命令提示符如图 1-2 中的所示。

**1-2** *一个从命令行运行的 Qt 应用程序*
信号、插槽和元对象
Qt 给 C++ 带来的两个最大优势是*信号**插槽*,它们是非常灵活的对象互连方式,有助于使代码易于设计和重用。
一个*信号*是一个被调用时发出而不是执行的方法。所以从你作为程序员的角度来看,你声明了可能发出的信号的原型。不执行信号;只需在类的`signals`部分的类声明中声明它们。
一个**是一个成员函数,可以作为信号发射的结果被调用。你必须通过将方法放在这些部分中的一个来告诉编译器将哪些方法视为槽:`public slots`、`protected slots`或`private slots`。保护级别仅在插槽用作方法时保护插槽。您仍然可以将一个`private`插槽或一个`protected`插槽连接到您从另一个类接收的信号。
说到连接信号和插槽,您可以将任意数量的信号连接到任意数量的插槽。这意味着单个插槽可以连接到多个信号,单个信号可以连接到多个插槽。对如何互连对象没有限制。当发出一个信号时,所有连接到它的插槽都会被调用。调用的顺序是不确定的,但是它们确实会被调用。让我们来看一些代码,这些代码显示了一个声明了信号和插槽的类(参见清单 1-8 )**清单 1-8** *有一个信号和一个插槽的类*
#include <QString>

#include <QObject>

class MyClass : public QObject

{

`  Q_OBJECT`

public:

  MyClass( const QString &text, QObject *parent = 0 );

  const QString& text() const;

  int getLengthOfText() const;

`public slots:`

  void setText( const QString &text );

`signals:`

`  void textChanged( const QString& );`

private:

  QString m_text;

}; 
代码是你在这一章中一直使用的类`MyClass`的新化身。清单中三个重点区域的信号和插槽都有变化。从底部开始,新的部分标记为`signals:`。这告诉您,本节中声明的函数将不会由您实现;它们只是这个类可以发出的信号的原型。这个类有一个信号:`textChanged`。
再往上,还有一个新的板块:`public slots:`。像其他成员一样,插槽可以是公共的、受保护的或私有的——只需在关键字`slots`前添加适当的保护级别。插槽可以被认为是一个可以连接到信号的成员函数。真的没有其他区别;它就像该类的任何其他成员函数一样被声明和实现。

**提示** Setter 方法是自然槽。通过设置所有的 setters 插槽,可以保证将信号连接到类中所有感兴趣的部分。唯一一个 setter 不应该也是 slot 的时候是当 setter 接受一些非常定制的类型,而你确信这些类型永远不会来自一个信号。

在类声明的最顶端,您可以找到`Q_OBJECT`宏。重要的是,这个宏首先出现在类声明的主体中,因为它将该类标记为需要元对象的类。在继续之前,让我们看看什么是元对象。
单词 *meta* 表示前缀的单词是关于它自己的。所以*元对象*是描述对象的对象。在 Qt 的情况下,元对象是类 `QMetaObject`的实例,包含关于类的信息,比如它的名称、它的超类、它的信号、它的槽以及许多其他有趣的东西。现在要知道的重要事情是元对象知道信号和槽。
这就引出了这个特性的下一个含义。到目前为止,所有的例子都可以很好地放入一个源代码文件中。这样继续下去是可能的,但是如果你把每个类分成一个头文件和一个源文件,这个过程会顺利得多。一个名为*元对象编译器*、`moc`的 Qt 工具解析类声明,并从中生成一个 C++ 实现文件。这听起来可能很复杂,但是只要您使用 QMake 来处理项目,对您来说没有什么不同。
这种新方法意味着来自清单 1-8 的代码将被放入一个名为`myclass.h`的文件中。实现进入`myclass.cpp`,`moc`从头文件生成另一个名为`moc_myclass.cpp`的 C++ 文件。生成文件的内容可以在 Qt 版本之间改变,这没什么好担心的。清单 1-9 包含了由于信号和插槽而改变的部分实现。

**清单 1-9** *用信号和时隙*实现 `MyClass` ```
void MyClass::setText( const QString &text )

{

  if( m_text == text )

    return;

  m_text = text;

  emit textChanged( m_text );

}
```cpp

发出信号`textChanged`的变化可分为两部分。前半部分是检查文本是否真的发生了变化。如果您在将`textChanged`信号连接到同一个对象的`setText`插槽之前没有检查这一点,您将会以一个无限循环结束(或者像用户所说的,应用程序将会挂起)。变化的后半部分是实际发出信号,这是通过使用 Qt 关键字`emit`后跟信号的名称和参数来完成的。

**信号和机罩下的插槽**

Qt 使用函数指针来实现信号和槽。当以信号作为参数调用`emit`时,实际上调用的是信号。该信号是在由`moc`生成的源文件中实现的功能。这个函数使用持有连接插槽的对象的元对象来调用连接到信号的任何插槽。

元对象包含指向插槽的函数指针,以及它们的名称和参数类型。它们还包含可用信号及其名称和参数类型的列表。当调用`connect`时,您要求元对象将插槽添加到信号的调用列表中。如果参数匹配,则建立连接。

当匹配参数时,只对插槽接受的参数进行匹配检查。这意味着不带任何参数的槽匹配所有信号。插槽不接受的参数会被发出信号的代码直接丢弃。

####  建立连接

为了测试`MyClass`中的信号和插槽,创建了`a`、`b`和`c`实例:

QObject parent;

MyClass *a, *b, *c;

a = new MyClass( “foo”, &parent );

b = new MyClass( “bar”, &parent );

c = new MyClass( “baz”, &parent );
现在连接它们。使用QObject::connect方法连接信号和插槽。论据有source objectSIGNAL(source signaldestination objectSLOT(destination slot)。宏SIGNALSLOT是必需的;否则,Qt 拒绝建立连接。源和目标对象是指向QObject或继承QObject的类的对象的指针。源信号和目标插槽是所涉及的信号和插槽的名称和参数类型。下面显示了它在代码中的样子。图 1-3 显示了对象实例是如何连接的。
  QObject::connect(

a, SIGNAL(textChanged(const QString&)),

b, SLOT(setText(const QString&)) );

QObject::connect(

b, SIGNAL(textChanged(const QString&)),

c, SLOT(setText(const QString&)) );

QObject::connect(

c, SIGNAL(textChanged(const QString&)),

b, SLOT(setText(const QString&)) );

注意连接时试图指定信号或插槽参数会导致你的代码在运行时失败。connect函数只理解参数types

图 1-3。a``bc之间的联系** **下面一行显示了对其中一个对象的调用:

  b->setText( "test" );

尝试追踪从b开始的呼叫,其中有从"bar""test"的变化;通过连接到c,这里有从"baz""test"的变化;并且通过连接到b、没有变化的地方。结果是a不变,而bc将文本设置为"test"这在图 1-4 中有说明,其中你可以看到文本"test"是如何通过对象传播的。现在尝试跟踪下面的调用。你能说出结果会是什么吗?

  a->setText( "Qt" );

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-4。 通过连接追踪文本


提示通过为每个插槽提供一个信号(例如,textChanged对应于setText,你可以将两个物体绑在一起。在前面的例子中,对象bc总是具有相同的值,因为一个对象的变化会触发另一个对象的变化。当一个对象是图形用户界面的一部分时,这是一个非常有用的特性,您将在后面看到。


重温构建过程

上次提到构建 Qt 应用程序时,使用 QMake 工具的原因是平台独立性。另一个重要原因是 QMake 处理元对象的生成,并将它们包含在最终的应用程序中。图 1-5 显示了一个标准的 C++ 项目是如何构建的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-5。 一个标准的 C++ 项目被构建。

使用 QMake 时,所有头文件都由元对象编译器解析:mocmoc寻找包含Q_OBJECT宏的类,并为这些类生成元对象。然后,生成的元对象被自动链接到最终的应用程序中。图 1-6 展示了这是如何融入构建过程的。作为开发人员,QMake 使这一点对您完全透明。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-6。 正在构建元对象。


提示 记住 Qt 只是简单的标准 C++ 混合了一些宏和moc代码生成器。如果您收到编译器或链接器消息,抱怨缺少函数,这些函数的名称告诉您它们是信号,则信号的代码没有生成。最常见的原因是该类不包含Q_OBJECT宏。如果没有继承QObject(直接或间接)并且仍然使用Q_OBJECT宏,或者在类中插入或删除宏后忘记运行qmake,也有可能出现奇怪的编译错误。


与新事物的联系

信号和插槽是非常松散的连接类型,所以唯一重要的是参数的类型在信号和插槽之间匹配。被调用的类不需要知道调用类的任何信息,反之亦然。这意味着可以对简单的示例类进行测试——让它与一组 Qt 的类进行交互。

计划是将MyClass放在让用户输入文本的小部件QLineEdit和显示文本的小部件QLabel之间。一个小部件是一个可视组件,比如一个按钮、一个滑块、一个菜单项或者任何其他图形用户界面的一部分。(小部件在第三章的中有详细描述。)通过将来自QLineEdit对象的textChanged信号连接到MyClass对象的setText槽,然后将来自MyClass对象的textChanged信号连接到QLabel对象的setText槽,可以使MyClass对象作为一座桥梁,将文本从用户可编辑字段传送到标签。整个设置如图 1-7 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-7。 MyClass 充当 QLineEdit QLabel之间的桥梁

这个例子的主要功能可以分为三个部分:创建相关的对象实例,建立连接,然后运行应用程序。清单 1-10 展示了如何创建相关的组件。首先,有一个QApplication对象。对于所有的图形 Qt 应用程序,必须有一个(且只有一个)可用的应用程序实例。应用程序对象包含所谓的事件循环。在这个循环中,应用程序等待某件事情发生——等待某个事件发生。例如,用户按下一个键或移动鼠标,或者已经过了某一段时间。一旦事件发生,它就被转换成对适当的QObject的调用。例如,按键事件将转到具有键盘焦点的小部件。事件由接收对象处理,有时会发出信号。在按键场景中,发出一个textChanged信号;在按钮和键被输入或空格的情况下,发出一个pressed信号。然后,信号被连接到执行应用程序实际任务的插槽。

花点时间回顾一下清单 1-10 。创建了QApplication对象,以及三个小部件:一个普通的QWidgetQLineEdit和一个QLabelQWidget充当另外两个的容器。这就是为什么您创建了一个QVBoxLayout——这是一个垂直的盒子布局,它将小部件堆叠在彼此之上。然后,在将布局分配给小部件之前,在 box 布局中放置行编辑和标签。产生的小部件如图 1-8 所示。

最后,您创建一个MyClass的实例,这是您将需要的最后一个对象。

清单 1-10。 创建一个应用程序、小部件、布局和一个 MyClass 对象

`#include

int main( int argc, char **argv )

{

QApplication app( argc, argv );

QWidget widget;

QLineEdit *lineEdit = new QLineEdit;

QLabel *label = new QLabel;

QVBoxLayout *layout = new QVBoxLayout;

layout->addWidget( lineEdit );

layout->addWidget( label );

widget.setLayout( layout );

MyClass *bridge = new MyClass( “”, &app );`

根据图 1-7 ,你需要做两个连接(见清单 1-11 )。重要的是要记住信号和插槽的名称(textChangedsetText)恰好与MyClass中的名称相同。对 Qt 唯一重要的是作为参数发送和接受的类型:QString

清单 1-11。 设置连接

`QObject::connect(

lineEdit, SIGNAL(textChanged(const QString&)),

bridge, SLOT(setText(const QString&)) );

QObject::connect(

bridge, SIGNAL(textChanged(const QString&)),

label, SLOT(setText(const QString&)) );`

您可能担心显示用户界面,然后开始事件循环是最难的部分。事实上,恰恰相反。清单 1-12 显示了所有相关的代码。因为行编辑和标签包含在普通小部件中,所以小部件一显示,它们就显示出来。当你试图显示小部件时,Qt 意识到它缺少了一个窗口,并自动将它放在一个窗口中。然后,应用程序方法exec运行事件循环,直到所有窗口都关闭,只要一切按预期运行,就返回零。

清单 1-12。 显示用户界面并执行事件循环

  widget.show();

  return app.exec();

}

一旦事件循环启动并运行,一切都会迎刃而解。键盘活动最终出现在行编辑小部件中。按键被处理,文本相应地改变。这些变化导致textChanged信号从行编辑发送到MyClass对象。这个信号通过MyClass对象传播到标签,当标签用新文本重绘时,用户可以看到这个变化。来自应用程序的截图如图 1-8 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-8。 表面上看不出来,但是 MyClass 在这个应用中起着重要的作用。

重要的是要记住,MyClassQLineEditQLabel一无所知,反之亦然——它们在相互连接的主函数中相遇。不需要事件、委托或信号类为相关类所熟知。唯一的共同点是他们继承了QObject;其余所需的信息可以在运行时从元对象中获得。

集合和迭代器

Qt 的类取代了 C++ STL 的类(到目前为止,你已经看到了QString类)。这一节着眼于 Qt 必须提供的容器和迭代器。

Qt 的容器是模板类,可以包含任何其他可变类。有一系列不同的容器,包括不同的列表、堆栈、队列、映射和哈希表。伴随这些类而来的是迭代器——既有 STL 兼容的迭代器,也有 Qt 的 Java 版本。迭代器是轻量级的对象,用于在容器中移动,并访问保存在容器中的数据。


提示所有的 Qt 集合类都是隐式共享的,所以在列表被修改之前不会复制它。将列表作为参数传递或返回列表作为结果是低成本的性能内存方面。将对列表的引用作为参数或结果传递甚至更便宜,因为它保证不会无意中做出任何改变。


遍历 QList

我们先来看一下QList类。清单 1-13 展示了如何创建和填充一个QString对象列表。使用<<操作符追加数据使得用信息填充列表变得容易。当列表被填充时,foreach宏用于打印列表的内容。

清单 1-13。 填充一个 QList 并打印内容

  QList<QString> list;

  list << "foo" << "bar" << "baz";

  foreach( QString s, list )

    qDebug() << s;

清单 1-13 展示了 Qt 开发者认为列表应该如何:易于使用。使用foreach宏缩短了代码,但是迭代器实例在幕后使用。

Qt 提供了 STL 风格的迭代器和 Java 风格的迭代器。清单 1-14 中的代码展示了如何使用这两个迭代器。列表顶部的while循环使用 Java 风格的迭代器QListIterator。函数hasNext检查列表中是否还有有效的条目,而next方法返回当前条目并将迭代器移动到下一个条目。如果你想在不移动迭代器的情况下查看下一项,使用peekNext方法。

清单末尾的for循环使用 STL 风格的迭代器。迭代器名称可以使用 STL 命名或 Qt 命名来指定— const_iteratorConstIterator是同义词,但是后者更“Qt 化”

for循环中迭代时,使用++iterator而不是iterator++是有价值的。这为您提供了更高效的代码,因为编译器避免了为for循环的上下文创建临时对象。

清单 1-14。 * STL 风格迭代器和 Java 风格迭代器并排*

  QList<int> list;

  list << 23 << 27 << 52 << 52;

  QListIterator<int> javaIter( list );

  while( javaIter.hasNext() )

    qDebug() << javaIter.next();

  QList<int>::const_iterator stlIter;

  for( stlIter = list.begin(); stlIter != list.end(); ++stlIter )

    qDebug() << (*stlIter);

在比较 STL 和 Java 风格的迭代器时,重要的是要记住 STL 风格的迭代器效率略高。然而,Java 风格迭代器提供的可读性和代码清晰性可能是使用它们的足够动机。


提示通常使用typedef来避免到处键入QList<>::Iterator。例如,MyClass条目的列表可以用一个叫做MyClassListIterator的迭代器叫做MyClassList(像这样创建类型:typedef QList<MyClass> MyClassList)(像这样创建类型:typedef QList<MyClass>::Iterator MyClassListIterator)。这个过程有助于使使用 STL 风格迭代器的代码更具可读性。


清单 1-14 向你展示了如何使用常量迭代器,但是有时候当你迭代时,修改列表是必要的。使用 STL 风格的迭代器,这意味着跳过名字的const部分。对于 Java 风格的迭代,使用QMutableListIterator。清单 1-15 显示了使用 Qt 类迭代和修改列表内容:

清单 1-15。 使用迭代器修改列表

  QList<int> list;

  list << 27 << 33 << 61 << 62;

  QMutableListIterator<int> javaIter( list );

  while( javaIter.hasNext() )

  {

    int value = javaIter.next() + 1;

    javaIter.setValue( value );

    qDebug() << value;

  }

  QList<int>::Iterator stlIter;

  for( stlIter = list.begin(); stlIter != list.end(); ++stlIter )

  {

    (*stlIter) = (*stlIter)*2;

    qDebug() << (*stlIter);

  }

清单 1-15 显示 Java 风格的循环使用next读取下一个值*,然后使用setValue设置当前。这意味着清单中的循环将列表中的所有值加 1。这也意味着在next作为迭代器被调用之前setValue不应该被使用;然后,它指向实际列表之前不存在的值。*


注意当通过删除或插入项目来修改列表时,迭代器可能会失效。在修改实际的列表(而不是列表的内容)时,请注意这一点。


在 STL 风格的循环中,什么都没有改变,只是这次迭代器引用的项可以被修改。这个例子使用了名称Iterator而不是iterator,这不会影响结果(它们是同义词)。

不仅可以单向迭代,对于 STL 风格的迭代器,也可以使用--操作符和++操作符。对于 Java 风格的迭代器,可以使用方法nextpreviousfindNextfindPrevious。使用nextprevious时,使用hasNexthasPrevious保护代码以避免未定义的结果是很重要的。


当你选择使用迭代器时,尽可能使用常量迭代器,因为它们能提供更快的代码,并防止你错误地修改列表项。


当你需要以一种特殊的方式迭代或者只是想访问一个特定的条目时,你可以使用带有[]操作符或者at方法的索引访问。对于一个QList来说,这个过程非常快。例如,下面一行计算列表中第六和第八个元素的总和:

  int sum = list[5] + list.at(7);
填写清单
到目前为止,您已经使用`<<`操作符填充了列表,这意味着将新数据追加到列表的末尾。也可以预先考虑数据;例如,将它放在列表的开头或在中间插入数据。清单 1-16 展示了在列表中放置条目的不同方式。
图 1-9 显示了列表中的每个插入。首先,字符串`"first"`被追加到一个空列表中,然后字符串`"second"`被追加到列表的末尾。之后,字符串`"third"`被添加到列表中。最后,字符串`"fourth"`和`"fifth"`被插入到列表中。

**清单 1-16** *追加、前置、插入*
  QList<QString> list;

  list << "first";

  list.append( "second" );

  list.prepend( "third" );

  list.insert( 1, "fourth" );

  list.insert( 4, "fifth" );

**1-9** *追加、前置、插入时的列表内容*
更多列表
`QList`不是唯一可用的列表类;对于不同的场景有几个列表。当选择使用哪个列表类时,正确的答案几乎总是`QList`。使用`QList`的唯一缺点是,当你在大列表中间插入项目时,它会变得非常慢。
另外两个列表类更加专门化,但是它们不应该被认为是特例。第一个是`QVector`类,它保证包含的条目在内存中保持有序,所以当你在列表的开始和中间插入条目时,列表中后面的所有条目都必须被移动。好处是索引访问和迭代很快。
第二个选择是`QLinkedList`,它提供了一个链表实现,可以快速迭代,但是没有索引访问。它还支持常量时间插入,而与新项目在列表中的插入位置无关。另一个好的方面是,只要元素还在列表中,迭代器就一直有效——可以自由地在列表中删除和插入新的元素,同时仍然使用迭代器。表 1-1 将链表和向量类与`QList`进行了比较。
**1-1***`QList``QVector`*`QLinkedList`的比较**
```** 
```cpp* 
| **类** | **在开始处插入** | **插入中间的** | **结尾插入** | **通过 索引**访问 | **通过 迭代器**访问 | | --- | --- | --- | --- | --- | --- | | `QList` | 快的 | 在大列表上非常慢 | 快的 | 快的 | 快的 | | `QVector` | 慢的 | 慢的 | 快的 | 快的 | 快的 | | `QLinkedList` | 中等 | 中等 | 中等 | 无法使用 | 快的 |

****#### 特殊列表

到目前为止,您已经查看了用于通用目的的列表。Qt 也有一套专门的列表。让我们先来看看QStringList

字符串列表

string list 类继承了QList<QString>,并且可以这样处理。但是,它也有一些特定于字符串的方法,这使它很有用。首先,您需要创建一个列表并用一些内容填充它。这应该不会带来任何惊喜:

  QStringList list;   list << "foo" << "bar" << "baz";

这将给出一个包含"foo""bar""baz"的列表。你可以用你选择的一串连接它们。这是一个逗号:

  QString all = list.join(",");

在这个操作之后,字符串all将包含"foo,bar,baz"。另一件要做的事情是替换列表中包含的所有字符串。例如,您可以将所有出现的"a"替换为"oo":

  list.replaceInStrings( "a", "oo" );

替换操作产生一个新的列表,包含以下内容:"foo""boor""booz"。除了joinQString还有一种方法叫split。这个方法通过给定字符串的每次出现来分割给定字符串,并返回一个QStringList,它可以很容易地添加到已经存在的列表中。在本例中,您用每个逗号进行分割:

  list << all.split(",");

最终列表将包含项目"foo""boor""booz""foo""bar""baz"

堆栈和队列

string list 接受一个列表,并用方法扩展它,使它更容易处理内容。其他类型的特殊列表用于将新项目放入列表的特定部分,并从一个特定部分获取项目。类是QStackQQueue,其中堆栈类可以归类为 LIFO(后进先出)列表,队列归类为 FIFO(先入先出)列表。

使用堆栈时,使用push向堆栈中添加或推送新的项目。top方法用于查看当前项目。通过调用pop,当前项被返回并从堆栈中移除。这被称为弹出堆栈。在尝试弹出堆栈之前,您可以通过使用isEmpty方法来检查是否有东西要获取。清单 1-17 显示了这些方法是如何使用的。当清单中显示的代码执行后,字符串result将包含文本"bazbarfoo"。注意,第一个被压入堆栈的项出现在字符串的最后——LIFO。

清单 1-17。 使用堆栈

`  QStack stack;

stack.push( “foo” );
  stack.push( “bar” );
  stack.push( “baz” );

QString result;
  while( !stack.isEmpty() )
    result += stack.pop();`

对于队列,对应的方法有enqueue用于添加项目、dequeue用于从队列中取出项目、head用于查看当前项目。就像堆栈一样,有一个名为isEmpty的方法来指示是否有任何东西排队。清单 1-18 展示了这些方法的实际应用。代码执行后,结果字符串将包含文本"foobarbaz"。也就是说,首先排队的项目首先出现在字符串 FIFO 中。

清单 1-18。 使用队列

  QQueue<QString> queue;

  queue.enqueue( "foo" );

  queue.enqueue( "bar" );

  queue.enqueue( "baz" );

  QString result;

  while( !queue.isEmpty() )

    result += queue.dequeue();

映射和哈希

列表有利于保存东西,但有时将东西联系起来也很有趣,这就是地图和散列进入画面的地方。让我们从看一看QMap类开始,它使您能够将项目保存在键值对中。例如,您可以将一个值关联到一个字符串,如清单 1-19 所示。当您创建一个QMap时,模板参数是键的类型,然后是值的类型。

清单 1-19。 创建一个将字符串与整数相关联的映射,并用信息填充它

  QMap<QString, int> map;

  map["foo"] = 42;

  map["bar"] = 13;

  map["baz"] = 9;

要在地图中插入一个新项目,你所要做的就是用[]操作符分配它。如果该键已经存在,新项将替换现有项。如果键对于地图是新的,则创建一个新项目。

您可以使用contains函数查看一个键是否存在,或者使用keys方法获取所有键的列表。清单 1-20 展示了如何获取键并遍历地图中的所有条目。

清单 1-20。 显示调试控制台上所有的键值对

  foreach( QString key, map.keys() )

    qDebug() << key << " = " << map[key];

可以直接在 map 上使用迭代器,而不是遍历一个键列表,如清单 1-21 所示。这使得可以通过迭代器即时访问键和值,从而节省了每次循环迭代的查找时间。

清单 1-21。 遍历所有键值对

  QMap<QString, int>::ConstIterator ii;

  for( ii = map.constBegin(); ii != map.constEnd(); ++ii )

    qDebug() << ii.key() << " = " << ii.value();

在清单 1-20 中,[]操作符用于访问列表中已知的条目。如果使用[]操作符来获取一个不存在的项目(如下所示),则会创建一个新项目。新项等于零或使用默认构造器创建。

  sum = map["foo"] + map["ingenting"];
如果使用`[]`操作符而不是`value`方法,将会阻止地图创建新项目。相反,会返回零或默认构造项,而不会添加到地图中。建议使用`value`,因为它可以避免用来自一个很难发现的 bug 的无意义项目填充内存:
  sum = map["foo"] + map.value("ingenting");
创建映射时,用作键的类型必须定义操作符`==`和`<`,因为映射必须能够比较键并对它们进行排序。`QMap`提供良好的查找性能,因为它总是保持键排序。这在执行清单 1-20 时很明显,其中结果按照`bar` - `baz` - `foo`的顺序返回,而不是按照它们被插入的顺序。如果这对您的应用程序不重要,您可以使用`QHash`来获得更高的性能。
`QHash`类可以像`QMap`一样使用,但是键的顺序是任意的。散列中用于键的类型必须有一个`==`操作符和一个名为`qHash`的全局函数。`qHash`函数应该返回一个称为哈希键的无符号整数,用于在哈希列表中查找条目。对该函数的唯一要求是,它应该总是为相同的数据返回相同的值。Qt 为最常见的类型提供了这样的函数,但是如果您想将自己的类放在一个 hash 列表中,就必须提供这样的函数。
哈希列表的性能取决于它可以预期的冲突数量;也就是说,产生相同哈希键的键的数量。通过利用您对可能出现的键的了解,您可以使用散列函数来提高性能。例如,在电话簿应用程序中,人们可能有相同的姓名,但通常不共享姓名和电话号码。清单 1-22 显示了保存有姓名和号码的人的类`Person`。

**清单 1-22** *持姓名和班级编号*
class Person

{

public:

  Person( const QString& name, const QString& number );

  const QString& name() const;

  const QString& number() const;

private:

  QString m_name, m_number;

}; 
对于这个类,您必须提供一个`==`操作符和一个`qHash`函数(如清单 1-23 所示)。`==`操作符确保名字和号码匹配。`qHash`函数从`qHash(QString)`函数获取姓名和号码的散列,并使用 XOR 逻辑运算符(`^`)将它们连接起来。

**清单 1-23** *哈希函数为* `Person` **

bool operator==( const Person &a, const Person &b )

{

return (a.name() == b.name()) && (a.number() == b.number());

}

uint qHash( const Person &key )

{

return qHash( key.name() ) ^ qHash( key.number() );

}


为了试验在清单 1-23 中实现的散列函数,创建一个散列列表并在试图查找现有和不存在的项目之前放入几个项目。这显示在清单 1-24 中。每行`qDebug`后的注释显示了预期的结果。

**清单 1-24** *哈希* `Person` **

QHash<Person, int> hash;

hash[ Person( “Anders”, “8447070” ) ] = 10;

hash[ Person( “Micke”, “7728433” ) ] = 20;

qDebug() << hash.value( Person( “Anders”, “8447070” ) ); // 10

qDebug() << hash.value( Person( “Anders”, “8447071” ) ); // 0

qDebug() << hash.value( Person( “Micke”, “7728433” ) ); // 20

qDebug() << hash.value( Person( “Michael”, “7728433” ) ); // 0


有时候有趣的事情不是将一个值映射到一个键,而是记住哪些键是有效的。在这种情况下,可以使用`QSet`类。一个集合是一个没有值的散列,所以对于键必须有一个`qHash`函数和一个`==`操作符。此外,键的顺序是任意的。清单 1-25 显示了你使用和填充一个列表相同的操作符来填充一个集合。再往下,可以看到这两种访问方法。您可以使用迭代器来访问这些键,也可以调用`contains`来查看这些键是否是集合的一部分。

**清单 1-25** *填充一个*`QSet`*;然后显示按键并测试按键*和`"FORTRAN"`

QSet set;

set << “Ada” << “C++” << “Ruby”;

for( QSet::ConstIterator ii = set.begin(); ii != set.end(); ++ii )

qDebug() << *ii;

if( set.contains( “FORTRAN” ) )

qDebug() << “FORTRAN is in the set.”;

else

qDebug() << “FORTRAN is out.”;


**每键多项**
`QMap`和`QHash`类为每个键存储一个项目。当你想为每一个键都列出一个条目时,你可以使用`QMultiMap`和`QMultiHash`。这些类之间的关系就像`QMap`与`QHash`之间的关系一样——键的顺序被保存在地图中;散列更快,但是任意排列密钥。
本节讨论的是`QMultiMap`类,但是我所说的也适用于`QMultiHash`类。`QMultiMap`类没有`[]`操作符;相反,`insert`方法用于添加值,而`values`方法用于访问插入的项目。因为`QMultiMap`可以包含一个键的多个元素,所以`values`方法返回一个`QList`,其中包含与给定键相关联的项目。在请求列表之前,可以使用`count`方法查看有多少项与给定的键相关联。

 **注意**多集合`QMultiMap`和`QMultiHash`类只是`QMap`和`QHash`类的包装器。通过使用`insertMulti`方法,可以将`QMap`和`QHash`类用作多集合,但是使用`[]`操作符或`insert`方法很容易意外地覆盖一个项目列表。使用多集合可以在编译时检测到任何此类错误,并降低难以发现的错误的风险。

清单 1-26 显示了如何创建和填充一个`QMultiMap`。这段代码不包含任何惊喜。然而,`QMultiMap`和`QMap`的关系表明,如果你看一下从`keys`方法返回的列表,`foo`出现了两次。找到所有唯一键的最好方法是将所有键添加到一个`QSet`中,然后遍历它。清单 1-27 展示了如何首先找到所有的键,然后遍历它们,显示每个键的所有条目。

**清单 1-26** *创建并填充一个* `QMultiMap`

QMultiMap<QString, int> multi;

multi.insert( “foo”, 10 );

multi.insert( “foo”, 20 );

multi.insert( “bar”, 30 );


**清单 1-27** *找到唯一的键,然后遍历每个键及其相关项*

QSet keys = QSet::fromList(multi.keys());

foreach( QString key, keys )

foreach( int value, multi.values(key) )

qDebug() << key << ": " << value;


有一种更快的方法可以找到一个`QMultiMap`中的所有条目:使用迭代器。一个`QMultiMap::iterator`有成员函数`key`和`value`,用来获取它包含的信息。迭代器也可以用来高效地查找给定键的所有项。使用`find`方法,可以得到一个迭代器,指向属于给定键的第一项。随着键的排序,你可以通过迭代到达属于一个给定键的所有条目,直到从`find`开始的迭代器到达`QMultiMap`或另一个键的末尾(清单 1-28 显示了一个例子)。迭代器方法还避免了必须构建一个包含属于该键的所有条目的列表,这是使用`values`方法时会发生的情况——节省了内存和时间。

**清单 1-28** *使用迭代器查找给定关键字的条目*

QMultiMap<QString, int> int>::ConstIterator ii = multi.find( “foo” );

while( ii != multi.end() && ii.key() == “foo” )

{

qDebug() << ii.value();

++ii;

}


在这一节的开始,我说过所有的信息也适用于`QMultiHash`类。清单 1-29 通过执行与清单 1-261-271-28 中相同的任务来显示这一点。突出显示的行包含所需的更改——只包含要使用的类的更改。结果中唯一可能的差异是键以任意顺序返回。注意,这并不意味着`find`和 iterate 方法失败——键以任意顺序出现,但仍然是有序的。

**清单 1-29** *使用迭代器查找给定关键字的条目*

QMultiHash<QString, int> multi;

multi.insert( “foo”, 10 );

multi.insert( “foo”, 20 );

multi.insert( “bar”, 30 );

QSet keys = QSet::fromList(multi.keys());

foreach( QString key, keys )

foreach( int value, multi.values(key) )

qDebug() << key << ": " << value;

QMultiHash<QString, int>::ConstIterator ii = multi.find( “foo” );

while( ii != multi.end() && ii.key() == “foo” )

{

qDebug() << ii.value();

++ii;

}


总结
Qt 有一个推荐的命名方案,因为它使得猜测类和方法的名字变得更加容易。所有元素都使用驼色外壳;也就是每个生词都是以大写字母开头,像这样:`ThisIsAnExample`。
类名以大写字母开头,Qt 类以 q 为前缀,这是一个 Qt 类的例子:`QMessageBox`,这是另一个类:`MyClass`。以一个 Q 和一组小写字母为前缀的类是第三方 Qt 类;例如:`QjColorPicker`。
当使用 Qt 类时,确保包含与该类同名的头文件(在大多数平台上区分大小写)而没有任何文件扩展名(例如,`#include <QMessageBox>`包含了类`QMessageBox`)。
方法名以小写字母开头(例如,`thisIsAMethod`)。Getter 和 setter 方法分别被命名为`foo`和`setFoo`。如果有反映`foo`变化的信号,通常称为`fooChanged`。在这里的例子中,`foo`被称为一个属性。
关于信号和插槽:setters 是插槽的天然候选对象,也是发出关于变化的信号的好地方。如果您发出这样的信号,请确保检查 setter 接收到的是一个新值,而不是相同的值。这样做可以避免无限递归循环。
插槽可以是公共的、受保护的或私有的。这些部分被标记为`public slots:`、`protected slots:`或`private slots:`。信号是信号原型,放在`signals:`标签之后。插槽实现为任何其他成员函数,尽管您从不实现信号——只需在类定义中声明它们,并让元对象编译器处理细节。
当连接信号和插槽时,记住`connect`方法不能处理参数值,只能处理参数类型。参数的值必须来自发出的对象。
当使用信号和槽时,必须继承`QObject`并用`Q_OBJECT`宏开始类声明。这将添加所需的代码,并告诉元对象编译器该类需要一个元对象。
一旦你继承了`QObject`,你就可以给一个对象分配一个父对象和任意数量的子对象。每个父对象负责调用其子对象上的`delete`,因此只要您确保删除所有对象的祖先,所有对象都会被删除。
Qt 有处理通常由 C++ 标准模板库 STL 处理的任务的类。Qt 等价物更适合与 Qt 结合使用,但是可以轻松地与 STL 等价物交互。
对于处理文本,使用`QString`类。它支持 Unicode,并且与`QStringList`类交互良好。string list 类提供了在列表中包含的所有字符串中进行搜索和替换的方法,以及用您选择的分隔符连接字符串的方法。
为了保存任何类型对象的列表,Qt 有模板类`QList`、`QLinkedList`和`QVector`。各有利弊,但`QList`通常是正确的选择。当在一个非常大的列表中间插入项目时,当需要固定时间插入和快速顺序访问时,使用`QLinkedList`。`QVector`擅长随机存取,当项目需要在连续内存中按顺序存储时。
对于队列和堆栈,`QQueue`和`QStack`类工作良好;它们提供快速插入和从其名称所指示的末端进入。当你使用一个堆栈时,你的`push`和`pop`到顶;当你使用一个队列时,你将`enqueue`项放在尾部而`dequeue`项放在头部。
`QMap`和`QHash`类将条目与键相关联。`QHash`类以任意顺序对项目进行排序,但执行速度比`QMap`类稍快。地图总是按关键字对项目进行排序。为了管理每个键的几个项目,最好使用`QMultiMap`或`QMultiHash`类。
如果您不需要将任何条目与一个键相关联,但是想要维护一个键列表,那么`QSet`类就很适合您。它作为一个散列来工作,但是没有任何关联的值。

```****

## 二、Qt 快速应用开发

**A** 虽然 Qt 最初是作为一个开发带有图形用户界面的跨平台应用程序的工具,但它已经扩展成为一个用于构建所有类型软件的工具——命令行应用程序、嵌入式软件和用于大型工作站应用程序的图形用户界面。

Qt 的历史根源使得创建图形用户界面和围绕它构建应用程序变得非常容易。本章通过几个简单的步骤,从最初的想法一直到一个工作的应用程序。

### 素描

当开发软件时,有一个计划总是好的——一个显示你试图实现的目标的草图。这一章的目标是一个非常简单的电话簿,其中包含了联系人和电话号码的列表。

从现在开始,图形用户界面 UI 将围绕两个对话框构建:一个用于显示列表和可用操作,另一个用于编辑联系人。图 2-1 显示了这两个对话框的初稿。

![image](https://gitee.com/OpenDocCN/vkdoc-c-cpp-zh/raw/master/docs/fund-qt-dev/img/P0201.jpg)

**2-1** *用户界面的初稿*

流程的下一步是将草图中的想法转化为可以实施的结构。为此,您必须理解 Qt 应用程序是如何工作的。

### 事件驱动的应用程序

所有的 Qt 应用程序都是事件驱动的,所以你不能直接跟踪从`main`函数到应用程序所有部分的执行路径。相反,您从`main`函数初始化您的应用程序,然后`main`函数调用`QApplication`对象上的`exec`方法。这将启动应用程序的事件循环。(事件可以是从网络上接收到的新包、经过了一定时间、或者用户按下了键或移动了鼠标。)

`QApplication`对象等待这些事件,并将它们传递给任何受影响的`QObject`。例如,当用户点击图 2-1 所示电话簿对话框中的 *Clear All* 按钮时,应用程序的事件循环会接收到该点击。然后,`QApplication`对象接受`clicked`事件并将其传递给受影响的对象:在本例中,是代表按钮的`QPushButton`对象。然后,这个按钮对象对事件做出反应,并发出相关信号。

通过将用于被点击的按钮和被选择的列表项的信号连接到实现应用程序的实际功能的插槽,用户界面被设置为对用户交互做出反应。因此,开发应用程序时,一个好的起点是识别用户可以通过图 2-1 所示的 UI 采取的动作。

* * *

提示这里确定的行为非常像统一建模语言(UML)中的用例,这意味着这两种方法非常兼容。

* * *

*   第一步是启动应用程序。发生这种情况时,会显示列表对话框。
*   从列表对话框中,用户添加一个新项目。这将显示一个空的编辑对话框。
*   从列表对话框中,用户编辑当前选定的项目。这将显示一个已填写的编辑对话框。
*   用户从列表对话框中移除当前选定的项目。
*   从列表对话框中,用户清除列表。
*   从列表对话框中,用户退出应用程序。
*   在编辑对话框中,用户批准所做的更改。这意味着更改将反映在列表对话框中。
*   从编辑对话框中,用户取消所做的更改。

从列表的顶部开始,主机操作系统必须负责启动应用程序。你在这个过程中的角色是从`main`函数中显示列表对话框。其余的操作显示为组成 UI 的两个对话框上的按钮。

总结一下:这个应用程序由一个主函数、一个列表对话框和一个编辑对话框组成。每个对话框由一个表单——也就是 UI 的 XML 描述——和一个组成 Qt 感兴趣的实际`QDialog`的类组成。这些信息足以创建一个项目文件。结果如清单 2-1 所示。注意,它从应用程序模板`app`开始,这是所有 Qt 应用程序的起点。项目文件的其余部分只是一个需要创建的文件列表,这也是您在本章余下部分要做的事情。

**清单 2-1** *电话簿应用的项目文件*

```cpp
TEMPLATE = app

TARGET = addressbook

SOURCES += main.cpp editdialog.cpp listdialog.cpp

HEADERS += editdialog.h listdialog.h

FORMS   += editdialog.ui listdialog.ui

现在为应用程序创建一个新目录,并将项目文件放入其中。当您将本章中显示的其余文件放在该目录中时,您将得到一个完整的应用程序。

使用设计器

Designer 是 Qt 附带的用于设计用户界面的工具。本节向您展示如何使用设计器来构建列表对话框。然后学习编辑对话框的规范,这样您就可以自己组装了。

让我们从启动 Designer 开始。你会看到如图 2-2 所示的对话框。对于列表对话框,选择创建底部带有按钮的对话框,然后单击创建。


提示如果你运行的是 Windows,可以从开始菜单中选择 designer,或者启动 Qt 命令提示符,然后在控制台键入 designer 来启动 Designer。运行 Mac OS X 的人可以使用 Finder 找到 Designer 并启动它。在 Unix 平台上,这个过程可能稍有不同——尤其是如果您同时安装了 Qt 的第 3 版和第 4 版。可能的命令可以是designerdesigner-qt4。如果你已经使用软件包管理器安装了 Qt 4,你很可能在你的程序菜单中找到它。阅读发行版的文档以获得更多信息。


外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-2。 用于创建新表单的设计器对话框

出现设计者的用户界面。让我们先快速概述一下这个界面。设计器可以以两种模式运行:停靠窗口多个顶层窗口。您可以通过选择编辑外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传用户界面模式来更改设置。拥有多个顶层窗口对于多屏设置来说是非常好的,但是如果您同时运行多个应用程序和 Designer,可能会导致工作空间混乱。尝试两种配置,以确定您更喜欢哪一种。

在任一 UI 模式中,设计器都由下面列出的许多组件组成。这些组件中的每一个都可以从工具菜单中显示或隐藏。我不喜欢总是显示所有的组件——通常小部件框和属性编辑器对我来说已经足够了——但是可以自由地进行实验以获得您喜欢的工作环境。

  • 小部件框,如图 2-3 所示,包含所有可用小部件的列表,这些小部件被分成多个类别。
  • 图 2-4 中的所示的属性编辑器显示了工作表单中当前选中的小部件的所有可设计属性。
  • 如图 2-5 中的所示,对象检查器显示了哪个对象是哪个对象的父对象。
  • 信号/插槽编辑器,也称为连接编辑器,如图 2-6 中的所示,用于管理组成工作表单的对象之间的连接。
  • 资源编辑器,如图 2-7 所示,用于管理编译成可执行文件的图标等资源。
  • 动作编辑器,如图图 2-8 所示,用于管理动作;也就是说,在 UI 中的许多地方都表示的对象,例如菜单栏、工具栏和键盘快捷键。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-3。 设计师的小部件框连同工具栏和菜单

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-4。 设计师的属性编辑

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-5。 设计师的对象检查器

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-6。 设计师的信号/槽编辑

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-7。 设计师的资源编辑器

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-8。 设计师的动作编辑

图 2-9 显示了从模板创建的表单。内容由包含两个按钮的按钮框组成:OK 和 Cancel。按钮盒是一个小部件,所有使用 Qt 构建的对话框和窗口都由小部件和布局组成。小部件是 UI 的一部分,例如按钮、标签或滑块。小部件按布局组织。使用布局而不是仅仅记住每个小部件的坐标的原因是,你可以自由地调整字体和对话框的大小。此外,翻译人员可以编写任何标签文本,因为标签可以根据文本调整大小。小部件和布局有许多方面需要更详细地介绍(第三章会更详细地讨论)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-9。 表单从模板中新鲜出炉


注意我将对话框称为表单,因为可以使用 Designer 设计包含其他窗口小部件、主窗口和对话框的窗口小部件。它们都在 Designer 中显示为一个表单,但最终结果是不同的。


通过选择对话框中的按钮框并按 Delete 键,可以在 Designer 中开始工作。您会看到如图图 2-10 所示的清除对话框。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-10。 从按钮中清除表单

删除小组件后,您现在可以开始添加小组件。确保您处于编辑小部件的模式。从图 2-11 所示的工具栏中选择工作模式。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-11。 工作模式为(从左至右):编辑小工具、编辑连接、编辑好友、编辑标签顺序。

现在浏览部件框并找到按钮(在按钮组中)。当您单击并按住按钮时,鼠标指针会变成一个实际的按钮。将该按钮拖到表单上,并将其放在右上角。在第一个按钮下方的垂直行中再添加两个按钮;然后在右下角添加第四个按钮之前留出一个间隙。完成后,表格看起来应该类似于图 2-12 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-12。 带按钮的表单

现在在小部件框中找到垂直间隔(它在顶部附近的间隔组中)。将垫片拖动到对话框中,将其放置在上面三个按钮和下面一个按钮之间的空隙中,如图图 2-13 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-13。 添加间隔符后的形状

现在选择四个按钮和弹簧,然后应用垂直布局,这样你就得到如图图 2-15 所示的表单。通过单击并按住 Shift 键或拖动包含要选择的项目的框,可以选择多个项目。请注意,您不希望从小部件框添加布局。相反,在布局中选择你想要的部件,并点击工具栏中的垂直布局按钮,如图图 2-14 所示。这些按钮如下(从左到右):

  • 应用水平布局将小部件放置在水平行中。
  • 应用垂直布局将小部件放置在垂直行中。
  • 水平分割器将小部件放置在水平行中,但也允许用户调整小部件的大小。
  • 垂直分割器将小部件放置在垂直行中,但也允许用户调整小部件的大小。
  • 应用网格布局将小部件放置在可拉伸的网格中。
  • 中断布局移除任何当前布局。
  • 调整大小调整当前布局的大小,以适合包含的小部件。

尝试将指针放在工具栏按钮上,找到工具提示垂直排列的按钮,这就是您想要的按钮。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-14。 布局工具栏

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-15。 垂直布局的所有部件

您可以在小部件框的组项目小部件中找到列表小部件。将它放在表单上自由空间的中间。然后点击表单上的一个空闲点,这样就选择了实际的表单。通过查看对象检查器,您可以看到您已经选择了实际的表单。当对话框被选中时,你就有了正确的选择。现在,通过单击工具栏中的相应按钮来应用网格布局。在选择了包含其他小部件的小部件时应用布局会将该布局应用于表单(布局是父小部件的属性,而不是其中的子小部件的属性)。图 2-16 显示了添加列表小部件后的表单,图 2-17 显示了应用布局后的表单。


提示如果在调整对话框大小时,对话框的内容没有被拉伸,问题很可能是你忘记添加顶层布局了。选择对话框表单本身并应用一个布局——这应该可以解决问题。


外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-16。 增加了列表控件

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-17。 表格布局已经应用到表格本身及其所有内容

现在,您已经在布局中放置了许多小部件,形成了一个对话框。您可以使用“表单”菜单中的预览功能尝试不同样式的对话框。尝试调整对话框的大小,看看布局是如何交互的,并在 Qt 支持的不同平台上尝试不同的样式来查看对话框。然而,在对话结束之前,还有一些细节需要整理。首先,必须设置所有文本和小部件名称。

选择一个按钮会在属性编辑器中显示其属性。只需点击该值,并编辑它来改变它。表 2-1 自上而下显示了应用于按钮的名称和文本。请注意,对话框和列表小部件都有需要更改的属性。图 2-18 显示了修改后的对话框。

表 2-1。 属性改变

| 小部件 | **属性** | **值** | | --- | --- | --- | | 顶部按钮 | `name` | `addButton` | | 顶部按钮 | `text` | 添加新的 | | 第二个按钮 | `name` | `editButton` | | 第二个按钮 | `text` | 编辑 | | 第三个按钮 | `name` | `deleteButton` | | 第三个按钮 | `text` | 删除 | | 底部按钮 | `name` | `clearButton` | | 底部按钮 | `text` | 清理所有 | | 列表小部件 | `name` | `list` | | 对话 | `name` | `ListDialog` | | 对话 | `window title` | 电话簿 |

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-18。 姓名和文字已更新

name属性用于给每个小部件一个变量名,这是您稍后从源代码访问小部件时将使用的名称。这意味着name属性必须是有效的 C++ 标识符名称;也就是说,不要以数字开头,只使用英文字母、数字和下划线。


提示如果你想调整一个小部件的主要属性(例如,标签或按钮的文本),只需选择小部件并按 F2 键。


在 Designer 中构建表单的一个好处是可以用图形方式建立联系。从工作模式工具栏中选择编辑连接的模式。然后点击并从clearButton值拖动到list值。当在列表上释放鼠标按钮时,显示如图图 2-19 所示的对话框。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-19。 通过选择左边的信号和右边的插槽进行连接

左侧显示了来自clearButton值的可用信号;在右边,显示了list值的槽。选择clicked()信号和clear()插槽,然后按 OK。由此产生的连接在表格中显示为一个箭头(见图 2-20 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-20。 将连接直接显示在表单中

连接也可以在连接编辑器中看到,如图图 2-21 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-21。 连接编辑器中显示的连接

准备表单的最后一步是设置 tab 键顺序,这是用户使用 Tab 键在小部件之间跳转时访问它们的顺序。为此,首先从工作模式工具栏中选择 tab 键顺序模式。现在,每个小部件都用一个数字显示在一个蓝框中,这就是 tab 键顺序。开始按你觉得正确的顺序点击蓝框,数字会变。图 2-22 显示了带有我的标签顺序的对话框——如果你喜欢,可以随意使用其他顺序。当你感到满意时,预览对话框,并通过按 Tab 键移动部件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-22。 用 tab 键顺序设置表单

现在剩下的就是保存你的工作成果。将文件另存为listdialog.ui,与清单 2-1 中的项目文件放在同一个目录下。

为了试验您的新设计技能,我将编辑对话框的细节展示如下,但是您必须自己创建它。请注意,如果您从底部有按钮的模板开始,所有连接都是自动设置的。图 2-23 显示了结果对话框,以及标签、按钮和对话框的文本属性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-23。 编辑对话框

对象检查器如图 2-24 所示。您可以从该视图中分辨出不同对象的名称,以及哪些对象属于哪个布局。要创建网格布局,请按某种顺序放置小部件,选择它们,然后应用网格布局。Designer 通常在第一次尝试时就能得到正确的网格,但有时可能需要中断布局(可从布局工具栏获得),重新排列小部件,然后再次应用它。这是一个熟能生巧的地方。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

**图 2-24。**编辑对话框中的对象

图 2-25 显示了对话框中的连接。它们已经在模板中制作好了,所以您不应该对它们做任何事情。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

**图 2-25。**编辑对话框中的连接

最后,图 2-26 显示了我选择的 tab 顺序。请随意设置适合您的标签顺序。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-26。 编辑对话框的标签顺序

为了确保对话框以正确的方式组合在一起,请确保对象检查器视图和表单本身看起来百分之百正确。连接和 tab 键顺序也很重要,但是其他两个视图是最容易出现错误的地方。完成后,将对话框和其他文件保存为editdialog.ui

从设计师到代码

在 Designer 中创建的文件是用户界面的定义。如果在文本编辑器中打开它们,可以看到它们是 XML 文件。


注意如果你习惯于使用 Qt 和 Designer 的早期版本,你会注意到事情已经发生了变化。Qt 4 带来了一个全新的设计器应用程序,以及一种全新的从应用程序代码中使用设计的方法。您不能再使用设计器向项目中添加代码;相反,您可以从代码中使用 Designer 的结果。


通过在项目文件中包含对这些 XML 文件的引用(如清单 2-1 所示),在构建项目时会自动生成一个 C++ 文件。如果设计器文件名为foo.ui,则生成的 C++ 文件名为ui_foo.h。如果设计的表单被命名为FooDialog,那么产生的类就是Ui::FooDialog


注意Ui::FooDialog被放在Ui名称空间中以避免名称空间冲突,因为你可能想要调用你的最终对话框类FooDialog。生成的文件也在全局名称空间中创建了一个类。它叫做Ui_FooDialog,和Ui::FooDialog一模一样。我更喜欢使用来自Ui名称空间的类,因为它感觉比在类名前面加上Ui_更正确,但是您可以自由地做您想做的。


生成的 C++ 文件由用户界面编译器(uic)创建。它与构建过程的交互有点像元对象编译器,但它不是采用 C++ 头文件,而是采用用户界面的 XML 描述。图 2-27 显示了它们是如何组合在一起的。通过使用 QMake 来生成 Makefile,一切都是自动处理的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

**图 2-27。**Qt 项目是由源代码、生成的元对象和用户界面描述构建而成的。

在 Qt 应用程序中,所有对话框都继承自QDialog类。uic 生成的代码不继承该类;事实上,它甚至没有继承QObject。结论是你必须创建一个基于QDialog的类。让我们从查看列表对话框开始。

清单 2-2 显示了列表对话框的头文件。创建了一个名为ListDialog的类,它继承了QDialog。该类有插槽,所以Q_OBJECT宏必须在那里。然后,在最后,Ui::ListDialog类被用来创建私有成员变量ui

清单 2-2。 头文件为 ListDialog

#ifndef LISTDIALOG_H

#define LISTDIALOG_H

#include <QDialog>

#include "ui_listdialog.h"

class ListDialog : public QDialog

{

  Q_OBJECT

public:

  ListDialog();

private slots:

  void addItem();

  void editItem();

  void deleteItem();

private:

  Ui::ListDialog ui;

};

#endif // LISTDIALOG_H

ui对象由一组指针组成,指向组成对话框的所有小部件和布局。它还包含两个函数:setupUi(用于用窗口小部件和布局填充QDialog)和retranslateUi(用于国际化应用程序——在第十章中有更详细的介绍)。

ListDialog构造器的实现展示了如何使用ui对象(参见清单 2-3 )。首先,调用setupUi来创建对话框的 UI。当调用setupUi时,在 Designer 中建立的连接被设置。其余的连接通过调用connect手动完成。在调用中,ui对象用于访问对话框中的小部件。

不需要手动连接。通过实现一个名为on_addButton_clicked()的插槽,setupUi调用自动将来自addButtonclicked信号连接到那个插槽。这适用于使用on_ widget name_signal name( signal arguments )方案命名的所有插槽。即使这是可能的,我也建议不要使用它,因为它不鼓励为插槽提供清晰的名称来反映它们的功能。此外,当连接几个信号导致相同的动作时,这种方法会失败。您最终会有几个插槽调用同一个函数,或者——更糟糕的是——包含相同的代码。在对话框类的构造器中建立所有的连接确保了代码易于理解和阅读——您刚刚创建了一个表格,显示了用户界面如何连接到执行实际工作的插槽。

清单 2-3。ListDialog的构造器

ListDialog::ListDialog() : QDialog()

{

  ui.setupUi( this );

  connect( ui.addButton, SIGNAL(clicked()), this, SLOT(addItem()) );

  connect( ui.editButton, SIGNAL(clicked()), this, SLOT(editItem()) );

  connect( ui.deleteButton, SIGNAL(clicked()), this, SLOT(deleteItem()) );

}


注意 除了这里显示的方法之外,还有更多方法可以使用在 Designer 中从QDialog对象创建的 UI。这里使用的方法叫做单一继承方法。在 Designer 用户手册中,描述了两种替代方法:多重继承方法*(继承QDialogUi类)和直接方法*(使用对话框从方法中创建一个QDialog和一个Ui)。我更喜欢使用单一继承方法,并将在本书中通篇使用。它通过ui对象将生成的代码与手动编写的源代码分开——这有助于使更改更加可控。如果你想的话,请随意查阅设计者用户手册并尝试其他选择。**


清单 2-4 显示了addItem插槽的实现。该函数看起来非常简单,使用了EditDialog类(还没有讨论)。在继续之前,让我们看看对话框是如何使用的。首先,创建了dlg变量。传递给EditDialogthis指针将dlg的父指针设置为列表对话框。然后调用对话框的exec方法,该方法显示处于应用程序模态状态的对话框。一个对话框是应用程序模态的,这意味着在该对话框关闭之前,应用程序的其他对话框或窗口都不能获得 UI 焦点,这迫使用户使用或关闭显示的对话框。

exec方法从对话框返回一个状态,其中Qt::Accepted意味着 OK 按钮是最后被点击的(或者说accept插槽被调用来关闭对话框)。另一个可能的结果是Qt::Rejected,意味着对话框从标题栏被关闭或取消。

当使用exec显示对话框,并且结果是Qt::Accepted时,一个新的项目被添加到列表小部件:ui.list。新条目是使用编辑对话框中的namenumber getter 成员构建的(你将在本章后面看到它们)。

清单 2-4。 向列表添加新项目

`void ListDialog::addItem()
{
  EditDialog dlg( this );

if( dlg.exec() == Qt::Accepted )
    ui.list->addItem( dlg.name() + " – " + dlg.number() );
}`

添加一个新条目的反义词如清单 2-5 所示。删除一个列表小部件条目只需要在上面调用delete就可以了。当前选中的项目是从currentItem方法返回的,所以只需删除该方法返回的内容。

如果没有选择任何项目,返回值是0(零,一个空指针),但是在调用delete时这不是问题——它只是被忽略。

清单 2-5。 删除列表中的一项

void ListDialog::deleteItem()

{

  delete ui.list->currentItem();

}

当试图编辑当前项目时,确保currentItem是一个有效的指针是很重要的,这就是为什么清单 2-6 中的editItem槽通过检查它开始。如果返回的指针是一个空指针,那么槽不做任何事情就返回。

如果遇到一个有效的指针,那么使用split方法将当前列表小部件项目的文本分成一个名称和一个数字。它们用于设置编辑对话框。当设置名称和编号时,分割文本的部分被修剪,这意味着从字符串的末端移除所有额外的空白(空白由所有占用空间但不显示的字符组成)。空白的例子有空格、制表符、换行符、换行符等等。

编辑对话框一旦建立,代码看起来就非常像addItem槽,只是当前项目的文本被改变,而不是向列表小部件添加新的项目。

清单 2-6。 编辑列表中的一项

void ListDialog::editItem()

{

  if( !ui.list->currentItem() )

    return;

  QStringList parts = ui.list->currentItem()->text().split( "--" );

  EditDialog dlg( this );

  dlg.setName( parts[0].trimmed() );

  dlg.setNumber( parts[1].trimmed() );

  if( dlg.exec() == Qt::Accepted )

    ui.list->currentItem()->setText( dlg.name() + " -- " + dlg.number() );

}

现在你已经使用了两次编辑对话框,所以是时候看看它了。在清单 2-7 中,你可以看到类声明。EditDialog类继承了QDialog,并有一个名为ui的私有变量,包含用户界面的生成代码。这很像ListDialog级。

该类包含两个属性的 getters 和 setter:namenumber。因为对话框是专门为应用程序设计的,根本不可能在其他环境中重用,所以我冒昧地避开了 getters 和 setters 的策略。设置器不是插槽,也没有在属性改变时发出的任何信号。当一个类显然不会被重用时,过度设计它以使其可重用是没有意义的。

因为没有信号或插槽,所以省略了Q_OBJECT宏,所以该类没有元对象。这可以在运行时节省内存,并使编译稍微快一些。

清单 2-7。 编辑对话框类

class EditDialog : public QDialog

{

public:

  EditDialog( QWidget *parent=0 );

  const QString name() const;

  void setName( const QString& );

  const QString number() const;

  void setNumber( const QString& );

private:

  Ui::EditDialog ui;

};

如清单 2-8 所示,构造器非常简单。因为所有的连接都是在 Designer 中完成的,所以只需要对setupUi进行一次调用。查看 Designer 中的连接,您会看到来自按钮盒的acceptedrejected信号连接到acceptreject插槽。当用户点击确定时发出accepted信号,取消时发出rejectedacceptreject插槽将从exec返回的结果设置为Qt::AcceptedQt::Rejected,然后关闭对话框。这意味着从调用者的角度来看,对话已经按预期工作了。

清单 2-8。 编辑列表中的一项

EditDialog::EditDialog( QWidget *parent ) : QDialog( parent )

{

  ui.setupUi( this );

}

namenumber属性以相同的方式实现。在清单 2-9 中,显示了name属性。设置器setName很简单,只是将值传递给右边的QLineEdit。getter,name,稍微复杂一些。它不是简单地从行编辑中返回文本,而是使用replace删除所有出现的双破折号("--")。所有出现的双破折号都被替换为空字符串,这与删除它们是一回事。它们必须被删除,因为在列表对话框中名称和编号被双破折号分开,编辑槽editItem(见清单 2-9 )依赖于此。在返回没有双破折号的字符串之前,它还调用trimmed来删除文本末尾的任何空格。这可以防止用户不小心在名称后留下空格或制表符。

清单 2-9。 编辑列表中的一项

const QString EditDialog::name() const

{

  return ui.nameEdit->text().replace("--","").trimmed();

}

void EditDialog::setName( const QString &name )

{

  ui.nameEdit->setText( name );

}

number属性的实现看起来与name属性的实现相同。唯一不同的是所涉及的QLineEdit的名称:nameEdit用于名称,numberEdit用于编号。

最后一笔

现在项目文件中唯一缺少的部分是main函数。在清单 2-10 中,你可以看到实现。首先,创建一个QApplication对象;然后创建列表对话框。在应用程序的exec方法被调用之前,这个对话框就会显示出来。

调用exec意味着QApplication对象开始处理系统事件,并将它们传递给适当的QObject实例——应用程序是事件驱动的。该函数在所有窗口和对话框关闭后立即返回,因此当您关闭列表对话框时,exec返回,应用程序到达其结尾。

清单 2-10。 编辑列表中的一项

int main( int argc, char **argv )

{

  QApplication app( argc, argv );

  ListDialog dlg;

  dlg.show();

  return app.exec();

}

回头看看您希望用户能够执行的用户操作列表,您可以看到大多数操作都是由一个连接表示的。可以在设计器中建立连接,也可以在对话框类的构造器中使用connect调用来建立连接。让应用程序运行的最后一步是main函数。它的工作是显示列表对话框并启动事件循环。

为了测试这个应用程序,首先在您开始的项目文件上运行qmake来生成一个 Makefile。现在使用make或您的系统的等价物构建应用程序,这会为您生成一个可执行文件。在图 2-28 中,我第一次测试这个应用程序——看起来一切正常。

该应用程序不是很有用,因为它不能保存和加载数据。但是,用户界面功能齐全。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-28。 应用程序投入使用。

总结

本章展示了 Qt 应用程序中可用的两类对话框:主动的或被动的;聪明还是愚蠢。

列表对话框包含用户可以执行的每个操作的位置。这称为主动或智能对话。任何需要用户尽可能简单的输入的对话框都可以激活。小的活动元素可以使对话框更容易使用。

编辑对话框不包含任何插槽;它仅仅依赖于内置在所使用的小部件和acceptreject插槽中的智能。这对于非常简单的对话框来说已经足够了,在这些对话框中,用户可以填写不同类型的字段。这被称为被动或无声对话。一个应用程序中有几个被动对话框是很常见的;事实上,没有它们,应用程序就无法工作。

尽管编辑对话框对用户来说是被动的,但它对开发人员来说也不一定是被动的。编辑对话框使用namenumber属性很好地隐藏了图形用户界面的实际实现。这使得保持ui变量私有成为可能,代价是几行琐碎的代码。通过这样做,您可以确保在不使用编辑对话框更改代码的情况下更改 UI。在将来维护和扩展应用程序时,将应用程序的 UI 和代码分开通常会有所帮助。*

三、小部件和布局

ll 图形用户界面(ui)围绕使用布局排列的小部件构建。在这一章中,你将学习 Qt 提供了哪些小部件以及它们是如何使用的。您还将了解如何使用布局来创建所需的设计。本章在直接使用代码和使用 Designer 可视化地构建用户界面之间切换,这将教您理解 Designer 生成的代码。

**### 在 Qt 中创建对话框

正如你在上一章中了解到的,对话框是一个顶层窗口,所有的对话框都是由小部件构建的。此外,小部件是使用布局来组织的,这使得构建灵活的对话框成为可能。

布局有助于使 Qt 与众不同。使用布局可以很容易地构建适应屏幕分辨率、字体大小和不同语言变化的对话框。使用布局的另一种方法是静态布局,它确保所有的小部件都有一个大小和位置。因此,如果一个译者想在不同的语言中使用不同长度的文本,对话的设计必须适应最长的文本。使用布局,设计描述了部件的相对位置,而不是它们的绝对大小和位置。然后,小部件告诉布局它们需要多少空间,并相应地放在对话框中。

让我们通过使用 Designer 开始探索。启动设计器,从底部模板的按钮创建一个新的对话框。然后在对话框中添加一个分组框,一个行编辑,一个标签,一个垂直间隔符,如图图 3-1 所示。确保行编辑和标签在分组框内。你可以试着移动分组框。如果其他部件在其中,它们应该和分组框一起移动。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-1。 小部件拖放到对话框表单上

选择分组框并应用水平布局;然后选择对话框表单本身并应用垂直布局。你的对话框现在看起来应该类似于图 3-2 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-2。 布局已经应用。

图 3-3 显示了对话框的对象检查器。包含其他小部件的所有小部件也具有布局的信息是不可见的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-3。 对象检查器,显示对话框中的小部件

只是为了测试布局的概念,试着输入supercalifragilisticiexpalidocious作为标签文本(使用鼠标右键调出上下文菜单,并从菜单中选择更改文本)。如图 3-4 所示,标签展开,行编辑器收缩。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-4。 标签文字变成 supercalifaristiceexpalidocious。

规模政策

那么在这个例子中到底发生了什么呢?当计算小部件的大小时,布局查看小部件的大小提示和大小策略。如果您在 Designer 中查看sizePolicy属性,您可以看到标签在水平和垂直方向上都有一个Preferred大小类型(hSizeTypevSizeType)。线条编辑器有一个Fixed高度(垂直方向),但有一个Expanding宽度(水平方向)。这一切意味着什么?

每个小部件在运行时计算一个大小提示——小部件的首选大小。它还具有控制它可以接受的最小和最大尺寸的属性(minimumSizemaximumSize属性)。

当一个小部件说它的尺寸策略是在一个方向上保持一个Preferred尺寸时,这意味着如果需要的话,它可以变得比尺寸提示更大或更小,但是不喜欢这样。它不想增长,除非被布局和周围的小部件强迫。例如,如果用户增加窗口的大小,而周围的窗口小部件被配置为不增长,则窗口小部件会增长超过其首选大小。

行编辑有一个Fixed高度,所以小部件的高度是不可协商的;它总是使用大小提示来表示大小。Expanding策略意味着小部件可以缩小,但更喜欢尽可能大;它想要成长。

有几种政策可供选择(总结在表 3-1 )。

表 3-1。 大小政策及其行为

| **规模政策** | **可以生长** | **可以收缩** | **想要成长** | **使用尺寸提示** | | --- | --- | --- | --- | --- | | `Fixed` | 不 | 不 | 不 | 是 | | `Minimum` | 是 | 不 | 不 | 是 | | `Maximum` | 不 | 是 | 不 | 是 | | `Preferred` | 是 | 是 | 不 | 是 | | `Expanding` | 是 | 是 | 是 | 是 | | `MinimumExpanding` | 是 | 不 | 是 | 是 | | `Ignored` | 是 | 是 | 是 | 不 |

您可以通过在 Designer 中使用大小策略来了解它们的作用,因为一旦您将布局应用到小部件,策略更改就会直接反映在表单中。首先将标签的水平尺寸类型设置为Expanding,这使得标签和行编辑尽可能大,以便它们共享给定的空间。您也可以将策略设置为Maximum,然后尝试改变对话框的宽度。使用规模调整策略和布局是一项技能,技能是通过实践来学习的,所以不要害怕尝试。


提示你也可以为间隔符设置大小策略和大小提示,这对于加强空间和将对话框项目组合在一起非常有用。


在代码中设置大小策略

现在,您已经了解了使用 Designer 的布局和大小策略的基本知识。你如何用代码实现同样的事情?知道如何做到这一点很重要,因为由 Designer 生成的文件被 uic 工具转换成代码。要使用这些文件并解决编译问题,您需要了解文件中包含的内容。您也可能直接在代码中创建更小的用户界面元素,因为在这种情况下使用 Designer 是多余的。

当我用代码创建对话框时,我试图将我做的事情分组到逻辑组中——所以首先我创建所有的小部件(如清单 3-1 所示)。我不想给任何部件分配父部件,因为一旦部件被放入布局中,布局就会对部件负责。

清单 3-1。 小部件被创建。

   QDialog dlg;

   QGroupBox *groupBox = new QGroupBox( "Groupbox" );

   QLabel *label =

     new QLabel( "Supercalifragilisticexpialidocious" );

   QLineEdit *lineEdit = new QLineEdit;

   QDialogButtonBox *buttons =

     new QDialogButtonBox( QDialogButtonBox::Ok |

                           QDialogButtonBox::Cancel );

下一步是将小部件放到布局中。与 Designer 中的对话框一样,您可以使用垂直布局和水平布局。从上往下看清单 3-2 ,你会看到它从水平布局开始。代表水平布局的 Qt 类是QHBoxLayout,其中H代表水平方向。您可以看到它将应用于groupBox,因为它是作为父级传递的。然后从左到右添加小部件,首先添加label,然后添加lineEdit。当它们被添加时,hLayout成为它们的父项,它们被放置在分组框内的父项中。

QVBoxLayout(用于管理垂直布局)应用于对话框本身。其中,小部件是自上而下添加的。首先添加分组框;然后添加间隔物。间隔不会作为小部件添加;事实上,没有间隔小部件。通过调用addStretch方法,一个QSpacerItem被插入到布局中。这个项目作为一个间隔,所以效果是一样的,当你使用设计师。最后buttons被添加到布局的底部。

清单 3-2。 把小部件摆好。

   QHBoxLayout *hLayout = new QHBoxLayout( groupBox );

   hLayout->addWidget( label );

   hLayout->addWidget( lineEdit );

   QVBoxLayout *vLayout = new QVBoxLayout( &dlg );

   vLayout->addWidget( groupBox );

   vLayout->addStretch();

   vLayout->addWidget( buttons );

这两个列表都会导致如图 3-4 所示的对话框。如果您想使用代码中的布局策略,您需要知道要使用哪些属性和方法。所有小部件都有一个sizePolicy属性,由一个QSizePolicy对象表示。minimumSizemaximumSize属性是QSize对象。


提示当我引用一个属性名时,例如sizePolicy,可以理解为有一个 getter 方法叫做sizePolicy,还有一个 setter 方法叫做setSizePolicy。有一些没有 setter 的只读属性,但它们并不常见。


让我们从通过代码设置自定义大小策略开始。清单 3-3 展示了如何复制、修改和应用定制策略。首先,复制来自label的大小策略。拉伸系数最好为1。拉伸系数会更改,并且策略会应用于标签。然后拉伸因子被设置为1,策略被应用到lineEdit

清单 3-3。 修改和应用自定义策略

   QSizePolicy policy = label->sizePolicy();

   policy.setHorizontalStretch( 3 );

   label->setSizePolicy( policy );

   policy = lineEdit->sizePolicy();

   policy.setHorizontalStretch( 1 );

   lineEdit->setSizePolicy( policy );

清单 3-3 中的代码显示了两件事。首先,它向您展示了如何使用sizePolicysetSizePolicy来复制和应用策略。它还显示了拉伸因子,使用这些因子可以控制对话框中小部件的相对大小。显示了三个按钮(见图 3-5 ,所有按钮都被分配了水平尺寸策略Preferred。它们的拉伸系数是(从左到右)1、3 和 2。这意味着第一个按钮占用可用宽度的 1/(1+3+2)-六分之一;第二个按钮取 3/(1+3+2)—二分之一;而第三个用 2/(1+3+2)—三分之一。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-5。 带拉伸系数的按钮(从左到右:1、3 和 2)

布局

到目前为止,您已经了解了大小策略,并使用了水平和垂直布局。在 Designer 中,你可以获得三种最常见的布局:水平、垂直和网格。

通过类QHBoxLayout(水平的)和QVBoxLayout(垂直的)可以得到盒子布局(你已经看过几次了)。他们只是将小部件从左到右或从上到下排成一行或一列。图 3-6 和 3-7 显示了这两个类别的运行情况。在示例中,小部件按以下顺序添加:foobarbaz。当与拉伸因子和大小策略结合使用时,它们可以用作许多不同对话框布局的基础。


提示如果需要,您可以使用setDirection方法改变小部件的添加方向。这意味着您可以从右到左向水平布局添加小部件,或者向上向垂直布局添加小部件。


外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-6。 横框布局

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-7。 垂直方框布局

盒子布局中更强大的老大哥是网格布局QGridLayout。使用网格布局,您可以将小部件添加到类似表格的网格中。默认情况下,每个 widget 占用一个表格单元格,但是您可以让它跨越多个单元格。清单 3-4 向你展示了如何用三个按钮填充一个网格布局,最终的布局如图 3-8 所示。小部件是通过使用addWidget( QWidget *widget, int row, int col, int height=1, int width=1)方法添加的。barbaz按钮被添加到下一行的单元格中,并在两个方向上跨越一个单元格。foo按钮更大(它跨越两个单元格宽),从左上角开始——第一行第一列。

清单 3-4。 网格布局被填充。

   QGridLayout layout( &widget );

   layout.addWidget( new QPushButton( "foo" ), 0, 0, 1, 2 );

   layout.addWidget( new QPushButton( "bar" ), 1, 0 );

   layout.addWidget( new QPushButton( "baz" ), 1, 1 );

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-8。 网格布局

对于布局,所涉及的小部件的尺寸策略起着重要的作用。例如,默认情况下,按钮部件Fixed在垂直方向。这意味着如果你从清单 3-4 的中旋转布局,使列成为行(反之亦然),结果将看起来像图 3-9 。按钮不会拉伸以填充两个单元格;相反,它垂直居中,但保持小部件的大小提示的高度。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-9。 带有固定高度小部件的网格布局

可以使用其他布局类,但直接使用它们并不常见。方框布局和网格布局通常是你所需要的;结合拉伸因子和尺寸策略,你可以构建几乎任何可以想象的对话框布局。


提示你想尝试大小政策和布局吗?在设计器中执行此操作,以便在更改属性值时立即收到可视反馈。


常用小工具

所有的用户界面都是从布局和小部件开始的,几乎所有的用户操作都是从小部件开始的,所以在设计应用程序时,了解可用的小部件非常重要。

本节介绍了最常见的小部件,以及它们在主要平台上的截图。您还将了解密切相关的小部件以及每个小部件最有用的信号和插槽。

qushbutton

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

按钮是对话框中最常见的按钮。由于它的标准行为(它只是对点击做出反应),最有趣的信号是clicked()。如果希望按钮在按下和释放状态之间切换,可以将checkable属性设置为true。这样做使toggled(bool)信号变得有趣,因为它携带了当前状态,并指示已经发生了点击。

清单 3-5 显示了一个对话框的实现。在构造器中,创建了两个按钮:一个普通按钮和一个切换按钮。按钮以水平布局放置,它们的信号连接到对话框的两个插槽。定制插槽使用来自QMessageBox类的静态information方法来显示消息。


提示buttonToggled槽中,QString arg方法用于组合两个字符串。原始字符串中的%1被赋予arg的参数替换。您可以通过重复调用arg来连接几个(但不超过九个)字符串。例如,QString("%1 %3 %2").arg("foo").arg("bar"). arg("baz")产生字符串"foo baz bar"


清单 3-5。 按钮控件的基本演示

ButtonDialog::ButtonDialog( QWidget *parent ) : QDialog( parent )

{

  clickButton = new QPushButton( "Click me!", this );

  toggleButton = new QPushButton( "Toggle me!", this );

  toggleButton->setCheckable( true );

  QHBoxLayout *layout = new QHBoxLayout( this );

  layout->addWidget( clickButton );

  layout->addWidget( toggleButton );

  connect( clickButton, SIGNAL(clicked()), this, SLOT(buttonClicked()) );

  connect( toggleButton, SIGNAL(clicked()), this, SLOT(buttonToggled()) );

}

void ButtonDialog::buttonClicked()

{

  QMessageBox::information( this, "Clicked!", "The button was clicked!" );

}

void ButtonDialog::buttonToggled()

{

  QMessageBox::information( this, "Toggled!",

    QString("The button is %1!")

      .arg(toggleButton->isChecked()?"pressed":"released") );

}

不同的平台在对话框底部有不同的按钮位置。例如,在 Mac 或 Gnome 桌面中,最右边的按钮是接受按钮(Ok),而在 Windows 中,最右边的按钮通常是关闭或取消。通过使用QDialogButtonBox小部件,可以自动获取普通按钮。您也可以使用addButton添加自己的按钮,并赋予它们一个角色。当你告诉 Qt 哪个按钮有HelpRole哪个按钮有ApplyRole时,按钮被放置在用户期望的位置。

清单 3-6 显示了使用按钮框的对话框的一小部分。首先,创建一个带有方向的按钮框——它可以是HorizontalVertical。然后创建一个按钮,并将其连接到对话框中的一个槽,然后用QDialogButtonBox角色将其添加到按钮框中。图 3-10 显示了 Windows XP 系统上出现的对话框。将此与图 3-11 进行比较,在图 3-11 中,样式被强制为 clean looks——Gnome 桌面的样式。排序适应当前样式,这使得用户体验更好,因为用户可以坚持旧习惯,而不是在点击之前阅读所有按钮上的文本。

清单 3-6。 创建一个按钮,连接它,然后将它与一个角色一起添加到一个按钮框中

   QDialogButtonBox *box = new QDialogButtonBox( Qt::Horizontal );

   button = new QPushButton( "Ok" );

   connect( button, SIGNAL(clicked()), this, SLOT(okClicked()) );

   box->addButton( button, QDialogButtonBox::AcceptRole );


注意不要把按钮连接到清单 3-6 中的插槽,你可以把按钮盒的角色连接成这个connect(box, SIGNAL(accepted()), this, SLOT(okClicked()))


外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-10。 一个 QDialogButtonBox 带有 Windows XP 风格的按钮

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-11。 一个 QDialogButtonBox 带有 Windows XP 风格的按钮

QLabel

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

标签小部件是最常见的小部件之一,用于显示帮助用户更好地理解对话框的文本。当使用QLabel时,可以通过在标签文本中输入一个&符号,就在您想要作为助记符的字母之前,给它一个键盘快捷键或助记符。比如将文字设置为"E&xit",助记符为x,键盘快捷键为 Alt+x

通过使用setBuddy(QWidget*)将一个好友小部件分配给标签,用户通过按助记键将焦点移动到该小部件。清单 3-7 显示了这一点,其中两个标签成为两行编辑的伙伴。

如果您正在使用 Designer,可以从工作模式工具栏进入好友编辑模式。您可以通过绘制箭头将标签连接到它们的好友小部件,就像您进行信号和插槽连接一样。

清单 3-7 显示了一个对话框是如何在一个网格布局中被两个标签和两行编辑填充的。每个线编辑都将标注指定为伙伴。如果您尝试运行该示例,您会发现您可以使用 Alt 键和相关标签的助记符在各行编辑之间移动。

清单 3-7。 标签和线编辑为好友

`   QDialog dlg;

QLabel *fooLabel = new QLabel( “&Foo:” );
   QLabel *barLabel = new QLabel( “&Bar:” );
   QLineEdit *fooEdit = new QLineEdit;
   QLineEdit *barEdit = new QLineEdit;

fooLabel->setBuddy( fooEdit );
   barLabel->setBuddy( barEdit );

QGridLayout *layout = new QGridLayout( &dlg );
   layout->addWidget( fooLabel, 0, 0 );
   layout->addWidget( fooEdit, 0, 1 );
   layout->addWidget( barLabel, 1, 0 );
   layout->addWidget( barEdit, 1, 1 );`

QLineEdit

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

行编辑用于使用户能够编辑单行文本。(对于多行文本,使用QTextEdit小部件。)最常见的用途是让用户输入文本,但您也可以将其用作密码。只需将echoMode属性设置为Password,输入的文本就会显示为星号。

你可以用setText(const QString&)设置行编辑的文本,用text()得到。每当文本改变时,你可以连接到textChanged(const QString&)信号。

如果您想确保用户不会在字段中输入整篇文章,您可以使用maxLength属性来限制文本的长度。

要试用行编辑小部件,您可以在 Designer 中测试它。首先创建一个有六行编辑和四个标签的对话框,如图图 3-12 所示。图中显示了左栏中每个行编辑的textChanged信号连接到右栏中相应小部件的setText插槽的连接。然后,每一行的标签会告诉您左列中的每一行编辑更改了什么属性。


提示如果您想了解一个小部件,尝试使用它的属性并做一个预览(Ctrl+R)来看看它在运行时的行为。通过这种方式,您可以快速获得关于所做更改的反馈。


外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-12。 线条编辑小部件演示对话框及其连接

图 3-13 显示了对话框在预览模式下的样子。中间一排的密码是隐藏的,最下面一排的长度有限。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-13。 线条编辑小工具演示在行动

QCheckBox

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

用户可以选中或取消选中复选框。该类通过一个公共基类与按钮小部件相关,因此编程接口应该是熟悉的。

在默认模式下,您可以使用isChecked()方法来判断复选框是否被选中。在某些情况下,您可能希望有三种状态:未选中、未定义和选中(使用tristate属性来实现)。在这种模式下,你必须使用checkState属性来了解状态。

当检查状态改变时,发出stateChanged(int)信号。对于非tristate复选框,您可以连接到toggled(bool)信号。

qradio

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

单选按钮是复选框的近亲。它的工作方式类似于复选框,只是每次只能选中一个组中的一个。选中组中的一个框后,就不能取消选中。您只能在组内移动它。这意味着,如果您在初始化对话框时以编程方式选中了一个框,就可以保证其中一个框始终处于选中状态。要监控按钮的状态,使用toggled(bool)信号和isChecked方法。

一组单选按钮由具有相同父部件的所有按钮组成。您可以使用分组框将按钮分成不同的组,这也可以在按钮周围放置一个带有标题的漂亮框架。如果你不想直观地分割它们,你可以使用一个QButtonGroup,如清单 3-8 中的所示。图 3-14 表明不在视觉上区分它们可能是个坏主意。

清单可以分为三个部分。首先,创建分组框和按钮;然后使用addButton方法将按钮添加到它们各自的按钮组中。按钮组不以任何方式初始化按钮;它只是确保一次最多选中一个单选按钮。清单的第三部分也是最后一部分是使用addWidget创建网格并在网格中放置按钮。

清单 3-8。 创建四个单选按钮;然后将它们放入按钮组和布局中

   QGroupBox box( "Printing Options" );

   QRadioButton *portrait = new QRadioButton( "Portrait" );

   QRadioButton *landscape = new QRadioButton( "Landscape" );

   QRadioButton *color = new QRadioButton( "Color" );

   QRadioButton *bw = new QRadioButton( "B&W" );

   QButtonGroup *orientation = new QButtonGroup( &box );

   QButtonGroup *colorBw = new QButtonGroup( &box );

   orientation->addButton( portrait );

   orientation->addButton( landscape );

   colorBw->addButton( color );

   colorBw->addButton( bw );

   QGridLayout *grid = new QGridLayout( &box );

   grid->addWidget( portrait, 0, 0 );

   grid->addWidget( landscape, 0, 1 );

   grid->addWidget( color, 1, 0 );

   grid->addWidget( bw, 1, 1 );

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-14。 分组框中的四个单选按钮。你能分辨出哪一组和哪一组吗?

QGroupBox

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

您可以使用分组框来组织对话框的内容。它提供了一个带有标题的框架,您可以在其中放置其他小部件。分组框是一个被动的小部件,只作为其他小部件的容器。

如果您希望能够打开或关闭由分组框中的小部件控制的选项,您可以使用checkable属性使其可检查(这意味着标题中将显示一个复选框)。取消选中该复选框时,其内容被禁用,用户无法使用。可检查分组框有isChecked()方法和toggled(bool)信号。

图 3-15 显示了一个从设计器运行的简单预览。我创建了三个复选框,每个都有一个按钮。最左边的分组框是不可勾选的,看起来和预期的一样,你可以点击它里面的按钮。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-15。 分组框:不可勾选、可勾选(已勾选)、未勾选

中间和最右边的分组框是可勾选的——一个被勾选,另一个不被勾选。在未选中的组中,按钮被禁用,用户不能使用它。这是自动发生的;没有信号连接。所有需要的是按钮在分组框内。


注意在设计器中设置属性时,它们可能设置得太早。例如,如果在“分组框示例”对话框中将checked属性设置为false,按钮将保持启用状态。这是因为按钮是在checked属性被设置后添加到分组框中的,因此保持不变(因为分组框在toggled信号上启用和禁用所有包含的小部件)。相反,在设计器中创建对话框,但是在源代码中调用setupUi之后初始化所有用户可修改的属性。


QListWidget

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Qt 有列表、表格和树的小部件。本章仅限于列表小部件,因为 Qt 有一个非常强大的方法来使用模型和视图处理列表、表格和树(在第五章中有详细介绍)。

list 小部件用于向用户显示项目列表。您可以使用addItem(const QString&)addItems(const QStringList&)方法向列表中添加小部件。当用户改变当前项目时,你可以通过连接currentItemChanged (QListWidgetItem *, QListWidgetItem *)currentTextChanged(const QString&)信号来判断。请注意,并不总是需要选择当前项目,这取决于选择模式。

使用selectionMode属性,您可以让用户只选择一个项目、一系列连续的项目或所有项目。每当选择改变时,就会发出itemSelectionChanged信号。

列表视图的项目可以从文本字符串添加到列表中,但它们存储为QListWidgetItem对象。这些对象归列表小部件所有,当列表小部件被析构时会被自动删除。如果你想从列表中删除一个项目,只需使用currentItem属性或item(int row)方法找到它;然后delete它。

清单 3-9 展示了一个带有列表部件的对话框是如何设置的例子。首先,创建一个带有小部件的布局——两个列表小部件和两个用于在列表之间移动项目的按钮。之后,按钮被连接到 dialog 类中的插槽,这些插槽在填充列表之前执行项目的实际移动。图 3-16 显示了正在使用的列表对话框。

清单 3-9。 创建并填充列表小部件

`ListWidgetDialog::ListWidgetDialog() : QDialog()
{
  QPushButton *left, *right;

QGridLayout *layout = new QGridLayout( this );
  layout->addWidget( left = new QPushButton( “<<” ), 0, 1 );
  layout->addWidget( right = new QPushButton( “>>” ), 1, 1 );
  layout->addWidget( leftList = new QListWidget, 0, 0, 3, 1 );
  layout->addWidget( rightList = new QListWidget, 0, 2, 3, 1 );

connect( left, SIGNAL(clicked()), this, SLOT(moveLeft()) );
  connect( right, SIGNAL(clicked()), this, SLOT(moveRight()) );

QStringList items;
  items << “Argentine” << “Brazilian” << “South African”
        << “USA West” << “Monaco” << “Belgian” << “Spanish”
        << “Swedish” << “French” << “British” << “German”
        << “Austrian” << “Dutch” << “Italian” << “USA East”
        << “Canadian”;
  leftList->addItems( items );
}`

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-16。 动作中的列表小部件对话框

清单 3-10 展示了项目是如何在两个列表部件之间移动的。该代码显示了将项目从左侧列表移动到右侧列表的位置。首先,使用selectedItems().count()方法来确定是否真的有东西要移动。takeItem(int)方法用于从一个列表小部件中移除一个项目,而不必删除它。这个方法告诉 list 小部件您负责管理该项,并将其从 list 小部件中移除。然后,您可以使用addItem(QListWidgetItem*)方法将项目添加到另一个列表小部件中。这种方法使您能够在列表小部件之间移动项目,而无需删除或创建任何内容。

清单 3-10。 用于从右向左移动物品的插槽

`void ListWidgetDialog::moveLeft()
{
  if( rightList->selectedItems().count() != 1 )
    return;

QListWidgetItem *item = rightList->takeItem( rightList->currentRow() );
  leftList->addItem( item );
}`

Q 组合盒

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当只显示当前项目时,组合框可以像列表小部件一样使用。另一种用途是为用户提供一个项目列表,但也使他们能够编写自己的文本。您可以通过使用editable属性来控制用户是否可以输入自定义文本。

当用户从列表中选择一个项目时,会发出activated(int)activated(const QString&)信号。


提示如果你想在通过代码改变当前项目以及用户选择项目时发出信号,使用currentIndexChanged。只有当用户改变当前项目时,才会发出activated信号。


您还可以使用currentIndexcurrentText属性来查找当前项目。当用户输入自定义文本时,您可以通过连接到editTextChanged(const QString&)信号来检测它。

组合框小部件的一个常见用途是使用户能够在字处理器中选择字体和大小。为了选择字体,Qt 从 4.2 版本开始就有了QFontComboBox小部件,它以正确的字体显示每个列表项。

QSpinBox

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当您希望用户在给定范围内选择一个具有某种精度的数字时,数字显示框是理想的选择。因为它只允许用户输入一个值,所以它是精确的。同时,用户可以通过单击上下箭头来更改该值。如果给出某种反馈,箭头可以用来试验不同值的效果。

默认情况下,范围是 0 到 99,每次单击其中一个箭头都会将值改变 1。您可以通过更改minimummaximum属性来更改范围。同样,singleStep属性表示每次点击从当前值中增加或减少了多少。请注意,即使单步大小大于 1,用户仍然可以在框中输入任何值。


提示不要调用setMinimum(min)setMaximum(max),可以调用setRange(min,max),这样可以让代码可读性更好,也省去了你键入整行代码的麻烦。


当数字显示框的值改变时,它发出valueChanged(int)信号。如果你想连接一些东西到旋转盒,可以使用setValue(int)插槽。

为了测试数字显示框部件,我把一个对话框放在一起,这个对话框由一个 LCD 数字(QLCDNumber)和一个数字显示框组成(见图 3-17 )。数字显示盒的valueChanged信号已经连接到 LCD 数字的display(int)插槽。您可以通过更改singleStep属性、键入数字、使用箭头键上下移动、单击上下按钮,甚至使用上下翻页键来使用数字显示框。您将很快掌握如何控制数字显示框小部件来做您想要做的事情。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-17。 一个数字显示框连接着一个液晶显示值

如果您需要处理更高精度的值,可以使用QDoubleSpinBox小工具。它的编程接口类似于QSpinBox的接口,但是decimals属性使您能够控制值的精度。

对于处理时间、日期或两者的组合,可以使用QTimeEditQDateEditQDateTimeEdit。它们的工作方式与数字显示框非常相似,但是用户可以分别控制小时、分钟、秒、年、月和日。编程接口相似但不相同。例如,范围由minimumDatemaximumDate、和maximumTime控制。

如果您喜欢使用类似旋转框的小部件来选择日期,您可以使用QCalendarWidget。它看起来像一个真正的日历,用户可以通过点击它来选择日期。您可以比较图 3-18 中的日历部件和日期编辑部件。哪个更好用?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-18。 一个日历小部件和一个日期编辑小部件

QSlider

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

滑块的使用方式与数字显示框完全相同:使用户能够在给定的范围内选择一个值。QSlider类还使用minimummaximum属性来控制控件的范围,并使用setRange方法来同时更改这两个属性。

说到每次变化的大小,滑块是不一样的。用户可以进行大的改变或小的改变;它们由singleSteppageStep属性控制。当用户单击滑块位置指示器的任一侧时,会进行翻页。为了单步执行,用户必须点击滑块来获得焦点,然后使用键盘的箭头键。就像微调框的步长一样,用户仍然可以通过将位置指示拖动到位来达到单个步长之间的值。

要检测数值变化,连接到valueChanged(int)信号。


注意使用valueChanged来避免通过键盘、拖动或点击丢失更改。无论数值为何变化,总会发出valueChanged信号。


在 Designer 中,slider 小部件显示为两个小部件:水平滑块和垂直滑块。您可以通过使用orientation属性来控制小部件在HorizontalVertical之间的方向。

一个非常类似的小部件是QScrollBar,它告诉用户小部件不仅选择一个值,还选择由滑块大小指示的一系列值。属性指示滑块有多大,并告诉用户选择了多大的范围。

可编程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

滑块、滚动条和数字显示框对于让用户选择一个值都很有用,但是进度条可以用来以只读形式显示一个值。您可以使用minimummaximum属性自定义进度条的范围(是的,还有一个setRange(int, int)方法)。如果你把minimummaximum都设置为零,你会得到一个活动条,它不停地循环,没有一个确定的终点,这对于显示你正在做一些你不能预先判断长度的长任务是很好的。

实际进度是使用setValue(int)方法设置的,您可以使用reset()方法将进度条归零。

您可以使用textVisible属性打开和关闭完成百分比文本,并且可以使用format属性修改文本以适应您的应用程序。format属性是一个字符串,其中任何出现的%p都被替换为当前百分比,%v被替换为当前值,%m被替换为maximum值。

图 3-19 显示了一组在设计器中创建的进度条。对话框顶部的滑块通过valueChanged(int)setValue(int)连接与每个滑块相连。通过移动滑块,您可以设置进度。顶部进度条具有默认样式;即format属性为%p%,文本可见。下一个进度条的format文本设置为"%v out of %m steps completed.",第三个进度条有隐藏文本。底部的进度条将minimummaximum设置为零,这意味着它会一直移动以显示进度。打印出来的图形并没有显示它在连续移动——不需要调用setValue或任何其他方法来获得移动。

测试对话框中的最后一个细节是重置按钮。它的clicked信号连接到所有进度条的reset槽。单击它时,会重置进度条。这意味着每个进度条的值被设置为零,并且进度条的文本是隐藏的,直到移动滑块时发出的valueChanged(int)信号改变了进度条的值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-19。 不同配置的进度条

常见对话框

当让用户做出选择时,有许多用户期望的对话框。有用于打开和保存文件、选择颜色、选择字体等的对话框。这些对话框在 Qt 支持的不同平台上看起来是不同的。

通过使用 Qt 对这些对话框的实现,您可以访问一个类接口,这可以确保您尽可能使用本机版本,并在需要时使用通用版本。

文件

最常见的对话框是用于打开和保存文档的文件对话框。这些对话框都是通过QFileDialog类访问的。因为对话框被反复用于相同的任务,所以这个类已经配备了一组静态方法来处理对话框的显示(和等待)。

开启

要打开一个文件,使用静态的getOpenFileName方法。这显示了一个类似于图 3-20 中所示的文件对话框。该方法接受一大堆参数。理解如何使用它的最简单的方法是查看清单 3-11。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

**图 3-20。**Windows 平台上打开文件的对话框

清单 3-11。 挑选一个文件打开

     QString filename = QFileDialog::getOpenFileName(

         this,

         tr("Open Document"),

         QDir::currentPath(),

         tr("Document files (*.doc *.rtf);;All files (*.*)") );

     if( !filename.isNull() )

     {

...

该方法接受的第一个参数是对话框的父级。该对话框是模态的,因此当它打开时,给定的父级将被阻止与用户交互。第二个参数是窗口的标题;第三个是目录的路径,从这里开始。

第四个也是最后一个参数是由双分号(;;)分隔的过滤器列表。过滤器中的每种文档类型都包含一个文本,后跟一个或多个用括号括起来的过滤器模式。清单中指定的过滤器如图 3-21 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-21。 过滤器控制哪些文件类型可以打开。

该方法的返回值是一个QString。如果用户取消或以其他方式中止了对话,则返回的字符串是空字符串。通过使用isNull方法,您可以看到用户是否选择了一个文件。在清单中的if语句后面的代码块中,您可以打开文件并处理其内容。

图 3-20 中所示的对话框是 Windows 平台上使用的原生版本。当一个本地对话框丢失时,Qt 将退回到它自己的对话框(见图 3-22 )。正如你所看到的,对话框不再在左边提供快捷方式。它也不能显示不同文件类型的正确图标。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-22。 Qt 打开文件的回退对话框

getOpenFileName方法使用户只能选择打开一个文件。有些应用程序让用户一次选择几个文件,这就是可以使用getOpenFileNames的地方。产生的文件对话框与选择一个文件时显示的对话框相同,只是可以一次选择几个文件。

清单 3-12 展示了如何使用该方法。参数与清单 3-11 中的相同,除了方法返回一个QStringList而不是一个QString。如果列表为空,则用户没有选择任何文件。

清单 3-12。 挑选几个文件打开

     QStringList filenames = QFileDialog::getOpenFileName(

         this,

         tr("Open Document"),

         QDir::currentPath(),

         tr("Documents (*.doc);;All files (*.*)") );

...

保存

QFileDialog类有一个在保存文件时询问文件名的方法:getSaveFileName。如果文件已经存在,将显示一个类似于图 3-23 所示的警告对话框。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-23。 Qt 验证用户何时试图替换现有文件。

在清单 3-13 中,你可以看到用于显示图 3-24 中对话框的源代码。如果将清单与打开文件的相应清单进行比较,您会发现参数是相同的。

当指定过滤器时,如果用户没有指定,Qt 有助于强制文件扩展名,这是很好的。这意味着您需要一个All files (*.*)过滤器来让用户自由选择文件扩展名。

清单 3-13 Qt 询问用户保存文件的名称

     QString filename = QFileDialog::getSaveFileName(

         this,

         tr("Save Document"),

         QDir::currentPath(),

         tr("Documents (*.doc)") );

...

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-24。 选择保存文件的名称

打开目录

比询问文件名稍微不常见的是询问目录,但是QFileDialog类对此也有一个静态成员。清单 3-14 显示了正在使用的getExistingDirectory方法。参数与打开和保存文件的方法相同,只是没有给出过滤器,因为在处理目录时没有必要过滤扩展名。

清单 3-14。 向用户询问目录

     QString dirname = QFileDialog::getExistingDirectory(          this,          tr("Select a Directory"),          QDir::currentPath() ); ...

在 Windows 平台上使用时,产生的对话框如图 3-25 中的所示。它使用户能够从对话框中选择一个目录并创建新目录。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-25。 挑选目录

消息

你经常需要告诉用户一些重要的事情,或者询问一个单词或一个数字,这正是消息框和输入对话框派上用场的地方。使用它们可以使您不必设计和实现自己的对话框。相反,你可以通过静态方法使用 Qt 预制的对话框——就像询问文件名一样。

消息

QMessageBox类用来向用户显示消息(它也可以用来询问一些基本问题,比如你想保存文件吗?).让我们先来看看可以显示的三种不同类型的消息。图 3-26 显示了三个具有不同重要性信息的对话框。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-26。 三条不同的消息

这些对话框使用清单 3-15 中的源代码显示。静态方法informationwarningcritical接受相同的参数并以相同的方式工作。区别在于消息的重要性以及它在系统中的公布方式。所有消息都以不同的图标显示,但其他方面也会受到影响。例如,Windows 系统为信息和重要消息播放不同的声音。

发送给这些方法的参数是父对象、对话框标题和消息。可以使用标准的 C 方法对消息进行格式化(例如,\n用作换行符)。

清单 3-15。 向用户显示三种不同的消息

     QMessageBox::information(

         this,

         tr("Application Name"),

         tr("An information message.") );

     QMessageBox::warning(

         this,

         tr("Application Name"),

         tr("A warning message.") );

     QMessageBox::critical(

         this,

         tr("Application Name"),

         tr("A critical message.") );

静态方法question可以用来询问用户问题(清单 3-16 中显示了一个例子)。前三个参数与显示消息时相同:parent、title 和 message。接下来的两个参数指定显示哪些按钮以及哪个按钮将作为默认按钮。您可以看到列表产生的对话框中的按钮如图 3-27 中的所示。这些按钮是“是”、“否”和“取消”,后者是默认值。


注意也可以使用informationwarningcritical提问——只需指定默认 OK 按钮以外的按钮即可。


清单 3-16。 问用户一个问题

     switch( QMessageBox::question(

                 this,

                 tr("Application Name"),

                 tr("An information message."),

                 QMessageBox::Yes |

                 QMessageBox::No |

                 QMessageBox::Cancel,

                 QMessageBox::Cancel ) )

     {

       case QMessageBox::Yes:

...

         break;

       case QMessageBox::No:

...

         break;

       case QMessageBox::Cancel:

...

         break;

       default:

...

         break;

     }

检查方法调用返回值的switch语句决定了点击了哪个按钮。按钮比列表中显示的要多。可用选项如下:

  • QMessageBox::Ok:好的
  • QMessageBox::Open:打开
  • QMessageBox::Save:保存
  • QMessageBox::Cancel:取消
  • QMessageBox::Close:关闭
  • QMessageBox::Discard:丢弃或不保存,取决于平台
  • QMessageBox::Apply:应用
  • QMessageBox::Reset:重置
  • QMessageBox::RestoreDefaults:恢复默认值
  • QMessageBox::Help:救命
  • QMessageBox::SaveAll:全部保存
  • 是的
  • 所有人都同意
  • QMessageBox::No:没有
  • 所有人都不同意
  • QMessageBox::Abort:中止
  • QMessageBox::Retry:重试
  • QMessageBox::Ignore:忽略
  • QMessageBox::NoButton:当你想让 Qt 选择一个默认按钮时使用

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-27。 将问题显示给用户。

输入对话框

如果需要问比是/否/取消稍微高级一点的问题,可以使用QInputDialog类。使用这个类,您可以要求用户输入值和文本,并从给定的列表中选择一项。

让我们从使用getText方法从用户那里获取一段文本开始。你可以在清单 3-17 中看到它。清单中代码显示的对话框如图 3-28 所示。

给予该方法的参数是 parent、dialog title、label、echo mode、initial text,后跟一个指向布尔值的指针。如果用户点击 OK 关闭了对话框,则调用将布尔值设置为true。否则,设置为false

回声模式是对话框中正在使用的行编辑的echoMode属性。将其设置为QLineEdit::Normal以照常显示输入的文本。如果设置为QLineEdit::Password,输入的文本将显示为星号。

当方法调用返回时,检查ok是否为true以及返回的字符串是否包含某些内容。如果是这种情况,您可以继续对返回的文本做一些事情。

清单 3-17。 要求用户输入一些文本

     bool ok;

     QString text = QInputDialog::getText(

                       this,

                       tr("String"),

                       tr("Enter a city name:"),

                       QLineEdit::Normal,

                       tr("Alingsås"),

                       &ok );

     if( ok && !text.isEmpty() )

     {

...

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-28。 要求输入文本时显示给用户的对话框

当您希望用户从给定的列表中选择一个字符串或者输入一个新的字符串时,您可以使用静态的getItem方法。清单 3-18 向你展示了它是如何使用的。出现的对话框如图 3-29 中的所示。

给予该方法的参数类似于请求字符串时使用的参数。列表以父项、对话框标题和标签文本开始,后面是项目列表。这些物品保存在QStringList中。项目列表后跟一个零;这是项目列表中的起始索引。在这种情况下,对话框将从选择"Foo"开始。

索引后面的false表示对话框不允许用户输入自定义字符串。通过将其更改为true,用户可以从列表中选择一个值,也可以写入一个新的字符串。

参数以一个指向布尔值的指针结束,用于指示用户在关闭对话框时是否接受了它。当确定用户实际上是选择了一项还是取消了对话框时,使用该值和返回字符串的内容。

清单 3-18。 要求用户从列表中选择一个项目

     bool ok;

     QStringList items;

     items << tr("Foo") << tr("Bar") << tr("Baz");

     QString item = QInputDialog::getItem(

                       this,

                       tr("Item"),

                       tr("Pick an item:"),

                       items,

                       0,

                       false,

                       &ok );

     if( ok && !item.isEmpty() )

     {

...

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-29。 从列表中选择项目时显示给用户的对话框

QInputDialog还能帮你做一件事:从用户那里获取价值。使用静态getInteger方法显示一个包含数字显示框的对话框(如图 3-30 所示)。用于生成对话框的源代码如清单 3-19 所示。

给予该方法的参数依次是父级、对话框标题和标签文本。接下来是初始值、最小值、最大值和步长。最后一个参数是一个指向布尔值的指针,用来指示用户在关闭对话框时是否接受了它。使用该值确定该数字是由用户给出的还是对话框被取消了。

清单 3-19。 向用户询问整数值

     bool ok;      int value = QInputDialog::getInteger(                      this,                      tr("Integer"),                      tr("Enter an angle:"),                      90,                      0,                      360,                      1,                      &ok );      if( ok )      { ...

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-30。 要求用户输入一个值

如果您需要向用户询问一个浮点值,您可以使用静态的getDouble方法,它使用一个双数字显示框来显示和编辑该值。

更多对话

还存在用户希望出现标准对话框的其他情况。Qt 提供的两个对话框被选来讨论:用于选择颜色和字体的对话框。

颜色

QColorDialog类用于让用户选择一种颜色。对话框如图图 3-31 所示。显示对话框的源代码很简单(见清单 3-20 )。对QColorDialog::getColor的调用接受一个QColor作为起始值和父值。返回值是一个QColor,如果用户取消了对话框,这个值就无效。

清单 3-20。 向用户询问颜色

     QColor color = QColorDialog::getColor(                        Qt::yellow,                        this );      if( color.isValid() )      { ...

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-31。 允许用户选择颜色

来源

当你需要让用户选择一种字体时,使用QFontDialog类。对话框如图 3-32 中的所示。清单 3-21 展示了对话框是如何显示的,结果是如何解释的。

静态的getFont方法显示对话框并返回一个QFont。因为字体不能无效,所以该方法的参数以一个布尔值开始,该值指示用户是否取消了对话框。值true表示返回的字体已经被用户接受。

第二个参数是一个开始的QFont。第三个参数是父部件,最后一个参数是对话框的窗口标题。

清单 3-21。 对话框如何显示,结果如何解释

     bool ok;      QFont font = QFontDialog::getFont(                      &ok,                      QFont( "Arial", 18 ),                      this,                      tr("Pick a font") );      if( ok )      { ...

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-32。 挑选字体

验证用户输入

每当您要求用户在文本字段中输入内容时,您通常会得到一些奇怪的反馈。有时他们输入了几个单词,而你期望的是一个。或者他们没有使用正确的小数点。或者他们把一个数字写成文本——就好像你的应用程序要为他们解析“三”。关键是你不能总是相信用户输入了正确有效的输入——你必须总是验证一切。

验证输入时,检查输入是否正确。这并不总是等同于检查错误。即使你能发现输入中的 15 种错误,在某个地方有人会尝试第 16 种。而且会在最不方便的时间最不方便的地点发生。相信我。

验证器

因为 Qt 开发人员知道用户输入不可信,所以他们提供了QValidator类,该类可用于验证QLineEditQComboBox小部件中的用户输入。

不能直接使用QValidator类。相反,你必须使用它的一个子类或者自己做。

在使用验证器之前,您应该对它们的工作原理有所了解。验证器验证一个字符串,可以是InvalidIntermediateAcceptable。一个Acceptable字符串是您期望用户输入的内容。一个Invalid字符串无效,不能被转换成可接受的字符串。一个Intermediate字符串是不可接受的,但是可以变成一个。当用户输入文本时,无法输入Invalid字符串。Intermediate字符串被接受为输入,然而,Acceptable字符串也是如此。因此,当带有验证器的行编辑器拒绝接受按键时,这可能是因为它会将字符串呈现为Invalid

验证数字

有两个用于验证数字的验证器类:QIntValidator用于整数,QDoubleValidator用于浮点值。这两个类在清单 3-22 中显示。突出显示的行显示了创建和分配验证器的位置,但是先看一下整个清单。

清单显示了一个对话框类及其构造器。在构造器中,创建了两个标签、两个行编辑器和一个按钮,并放在一个网格布局中。产生的对话框如图 3-33 所示。

查看突出显示的行和两个验证器,您可以看到每个验证器类都有相当多的参数。从QIntValidator开始,它需要一个下限、上限和父级。清单中创建的对象允许从 0 到 100 的整数值。QDoubleValidator还需要一个下限、一个上限,然后是父代之前的数字或所需的小数。

要给小部件分配一个验证器,使用setValidator(QValidator*)方法,该方法可用于QLineEditQComboBox类。

清单 3-22。 带有两个经过验证的行编辑器的对话框

`class ValidatingDialog : public QDialog
{
public:
  ValidationDialog()
  {
    QGridLayout *layout = new QGridLayout( this );

QLineEdit *intEdit = new QLineEdit( “42” );
    QLineEdit *doubleEdit = new QLineEdit( “3.14” );
    QPushButton *button = new QPushButton( “Close” );

layout->addWidget( new QLabel(“Integer:”), 0, 0 );
    layout->addWidget( intEdit, 0, 1 );
    layout->addWidget( new QLabel(“Double:”), 1, 0 );
    layout->addWidget( doubleEdit, 1, 1 );
    layout->addWidget( button, 2, 0, 1, 2 );

connect( button, SIGNAL(clicked()), this, SLOT(accept()) );
  }
};`

整数验证器确保输入是好的,但是双精度验证器并不是在所有情况下都这样做。例如,它不强制指定的小数位数。

当将数据作为应用程序的输入时,您必须确保检查验证器是否确实将字符串验证为Acceptable。此外,确保使用QString::toIntQString::toDouble方法,并在使用它们之前查看它们是否真的解析了值。这里的基本教训是,在输入数据时,永远不要相信你的用户。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-33。 一行编辑整数,一行编辑浮点值

正则表达式

在解析基于文本的用户输入时,您真的可以编写大量代码。想象一下,必须验证一个结构类似于+nn(p)aa...a-ll...l的电话号码,其中n代表国家号码,p代表本地区号前缀,a代表区号,l代表该区域内的本地号码。国家编号可以有一到两位数。本地区号前缀可以是089(假设区号中有两到五个数字,本地号码中至少有一个数字)。在这种情况下,正则表达式可以成为你的救星。

正则表达式,通常称为 regexp 或 RE,使您能够定义如何构造字符串。然后,您可以尝试将输入字符串与您的 RE 进行匹配。匹配的字符串是有效的,而不匹配的可以被认为是Invalid。在 Qt 中,regexps 由QRegExp对象表示。

在开始使用QRegExp类之前,您需要理解 re 是如何编写的。REs 几乎可以被认为是一种自己的语言。这篇课文没有深入细节,但解释了基本概念,以便你能理解其思想。

与前面描述的电话号码匹配的 RE 看起来类似于\+\d{1,2}\([089]\)\d{2,5}\-\d+。看到这里,很容易理解为什么一些程序员避免使用 REs。当你理解了基本的构建块,你就可以把它分解成它的组成部分并阅读它。

首先,反斜杠\用于转义字符。例如,因为一个+在 REs 中有意义,我们对它进行转义以告诉QRegExp类尝试匹配一个+,而不是解释它。这就是省略括号(以及破折号-)的原因。


不要忘记 C++ 字符串本身是被转义的。用 C++ 写\d,需要写\\d。要表达\,就得在 RE 中对其进行转义(也就是\\,给 C++ 字符串\\\\)。


\d是所谓的元字符,是代表一个或多个字符的字符。\d代表一个数字。可用的元字符如下所示。请注意,标准的 C 转义也可以工作。例如,\n表示换行符,\t表示制表符。

  • .匹配任何字符。
  • \s匹配空格(QChar::isSpace())。
  • \S匹配非空白。
  • \w匹配一个单词字符(QChar::isLetterOrNumber()QChar::isMark()或下划线_)。
  • \W匹配非单词字符。
  • \d匹配一个数字(QChar::isDigit())。
  • \D匹配非数字。
  • \x nnnn匹配 UNICODE 字符nnnn,其中nnnn代表十六进制数字。
  • \0 nnn匹配 ASCII 字符nnn,其中nnn代表八进制数字。

对于局部区域前缀,表达式为[089],是一个字符组。将字符放在方括号内意味着可以匹配任何一个字符。通过在括号内放置一个^,告诉 RE 匹配任何不在括号内的字符。例如,[⁰⁸⁹]可以匹配除089之外的任何内容。

字符组也可以用范围来表示。假设您想要匹配af之间的所有字符(即abcdef)。您可以通过使用[a-fA-F]组来完成此操作。请注意,您必须有一个小写字符范围和一个大写字符范围。

仅由一个字符组成的字符组可以省去括号,因此a匹配a。由于点匹配任何字符,您必须对其进行转义以使用它来匹配自身。这意味着\。匹配.

在一些元字符之后,你会看到表达式{m,n},其中mn是数字。这告诉 RE 至少匹配前面的元字符或字符组的m个实例。如果m等于n,可以省去n。这意味着{m,m}等于{m}

如果你想匹配一个或多个东西,你可以加一个+来代替{1,n},这里n是一个足够大的数字。同理,*匹配零个或更多的东西,?匹配零个或一个东西。

还有一些特殊字符用作元字符,总结如下:

  • 如果第一个出现在 RE 中,则匹配要匹配的字符串的开头。
  • $如果出现在 RE 中的最后一个,则匹配要匹配的字符串的结尾。
  • \b匹配一个单词边界。单词边界可以是空白,也可以是要匹配的字符串的开头或结尾。
  • \B匹配非单词边界。

返回到匹配电话号码的原始 RE,您必须添加字符串的开头和结尾,以便不匹配给定字符串中间的号码(这给出了下面的 RE: ^\+\d{1,2}\([089]\)\d{2,5}\-\d+$)。分解后得出以下结果:

  • ^表示匹配字符串的开头。
  • \+表示一个+
  • \d{1,2}表示一位数或两位数。
  • \(表示左括号。
  • [089]是指089中的一种。
  • \)表示右括号。
  • \d{2,5}表示二至五位数。
  • \-表示破折号。
  • \d+表示一个或多个数字。
  • $表示匹配字符串的结尾。

现在,让我们将这个 RE 与QRegExp类结合使用(参见清单 3-23 )。首先要注意的是,RE 中的所有\字符都被转义了,因为 RE 被表示为 C++ 字符串。

当试图将一个字符串匹配到 RE 时,使用indexIn(QString)方法。此方法返回字符串匹配部分的开始索引。因为 RE 以^开始,如果字符串匹配,它必须是0,否则是−1。如果您跳过开头的^,第二个字符串会产生一个索引5,因为电话号码从五个字符开始。

清单 3-23。 用正则表达式匹配电话号码

   QRegExp re("^\\+\\d{1,2}\\([089]\\)\\d{2,5}\\-\\d+$");

   qDebug() << re.indexIn("+46(0)31-445566");      // 0

   qDebug() << re.indexIn("Tel: +46(0)31-445566"); // −1

   qDebug() << re.indexIn("(0)31-445566");         // −1

通过在 RE 中添加括号,可以捕获部分匹配的字符串。清单 3-24 增加了四对括号,给出如下 RE: ^\+(\d{1,2})\(([089])\)(\d{2,5})\-(\d+$)。可以使用cap方法提取这些括号中的内容。


注意这就是要匹配的括号转义的原因。


cap方法将一个索引作为参数,其中零返回整个匹配的字符串。从 1 开始的索引从左到右返回括号之间的匹配内容。

清单 3-24。 使用带有捕捉括号的正则表达式捕捉电话号码的不同部分

   QRegExp reCap("^\\+(\\d{1,2})\\(([089])\\)(\\d{2,5})\\-(\\d+)$");

   qDebug() << reCap.indexIn("+46(0)31-445566");   // 0

   qDebug() << reCap.cap(0);  // "+46(0)31-445566"

   qDebug() << reCap.cap(1);  // "46"

   qDebug() << reCap.cap(2);  // "0"

   qDebug() << reCap.cap(3);  // "31"

   qDebug() << reCap.cap(4);  // "445566"

验证文本

因为正则表达式对于验证给定字符串的格式是否正确非常有用,所以 Qt 自然会有一个基于它的验证器。QRegExpValidator将一个QRegExp作为构造器参数,并使用 RE 来验证输入。

清单 3-25 展示了这在真实代码中的样子。包含行编辑器、按钮和标签的对话框类是从清单中窃取和改编的——显示数字的验证器。需要注意的是,正则表达式被视为以一个^开始,以一个$结束,所以它们被省略了。

清单 3-25。 使用正则表达式验证用户输入

class ValidationDialog : public QDialog

{

public:

  ValidationDialog()

  {

    QGridLayout *layout = new QGridLayout( this );

    QLineEdit *reEdit = new QLineEdit( "+46(0)31-445566" );

    QPushButton *button = new QPushButton( "Close" );

    layout->addWidget( new QLabel("Phone:"), 0, 0 );

    layout->addWidget( reEdit, 0, 1 );

    layout->addWidget( button, 1, 0, 1, 2 );

...

    connect( button, SIGNAL(clicked()), this, SLOT(accept()) );

  }

};

当用户输入数据时,QRegExpValidator使所有文本从右边移除。这意味着用户必须加上加号、括号和破折号。这并不总是很清楚,可能会引起混乱。

当输入有效文本时,验证器不会阻碍任何输入,但是当在文本中间进行编辑时,可能会出现问题。例如,根据 re,不可能在添加左括号后立即删除整个国家代码,因为其中必须至少有一位数字。

当用户完成输入数据时,在接受数据之前将字符串与 RE 匹配是很重要的,因为验证器不能确保字符串是完整的。建议您使用cap方法从输入字符串中获取实际数据。请记住,您可以使用cap(0)来获取整个匹配的字符串。与QDoubleValidator相比,它对用户QString::toDouble和检查结果很重要,即使字符串已经被验证器监控。参见图 3-34 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-34。 电话号码的一部分已经输入到已验证的行编辑中。

总结

小部件和布局是所有用户界面的组成部分。确保花时间学习如何使用它们。

Designer 是一个很好的工具,可以帮助您熟悉可用的组件。它使您能够尝试小部件并练习构建适当的布局。记住把所有的部件放到布局中,通过调整对话框的大小来测试你的设计。通过确保它总是看起来很好,您可以确保它可以与不同的语言、屏幕分辨率和字体设置一起工作。

本章最重要的教训如下:

  • 总是将对话框按钮放在QDialogButtonBox中,以确保它们在所有平台上以用户期望的顺序出现。
  • 确保所有的小部件都由一个布局来管理——任何分散的小部件都会使对话框在其他平台和具有不同视觉设置的系统上看起来很糟糕。
  • 当设计一个对话框时,确保总是从用户的角度来看待它。参考图 3-33 并在使用设计时考虑结构、视觉辅助和用户的目的。
  • 不要害怕尝试设计师。您可以通过使用设计器及其预览功能来学习构建任何设计。**
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值