Qt GUI 工具包
开发带有图形用户界面 (GUI) 的应用程序既耗时又费力。使这些应用程序跨不同的操作系统工作可能更加复杂。传统上,应用程序是为一个平台开发的,然后需要重写大量代码才能将应用程序移植到其他平台。多平台 GUI 工具包改变了这一过程。
多平台 GUI 工具包使应用程序在平台之间移植变得更容易。与直接使用窗口系统(例如,X11 或 Windows)相比,使用 GUI 工具包开发应用程序也相当容易且工作量少得多。Qt 工具包是一个多平台 C++ GUI 工具包(类库),已经开发了 4 年。Troll Tech AS 公司在 2 年半前成立,以确保 Qt 的未来发展。
作为 Qt 开发人员之一,我可以向您介绍并概述 Qt。在此过程中,我将加入我对通用 GUI 编程技术的一些看法。
以下章节可以在本文中找到
Qt 的故事——关于 Qt 的背景信息
信号与槽——Qt 的对象通信机制
Qt 绘图引擎——使用 Qt 绘制图形。
Qt 事件处理——如何在 Qt 中获取用户点击和窗口系统事件
双缓冲——一种众所周知且非常有用的 GUI 编程技术
制作您自己的软件组件——如何编写新的软件构建块
对话框——将所有内容放在一起并使其运行
提示与技巧——我对 GUI 编程经验的一些看法。
Qt 的第一个公开发布版本于 1995 年 5 月发布。版本 0.98 最近发布(1996 年 7 月),其中包括 X11 的完整源代码。版本 1.0 计划于 1996 年 9 月发布。用于 X11 的 Qt 具有非商业许可证,该许可证授予任何开发人员使用 Qt 开发自由软件社区软件的权利。Qt 的非商业版本包括完整的 X11 源代码。通过此许可证,Troll Tech 希望促进高质量自由软件的开发。Qt 是一个模拟 GUI 工具包,它允许程序员在 Motif 和 Windows 外观和感觉之间进行选择。它实现了自己的小部件(用户界面元素),并且 X11 版本的 Qt 直接在 Xlib 之上实现,既不使用 Xt 也不使用 Motif。Qt 中几乎所有类和成员函数都有文档记录。该文档以 HTML、postscript、文本和手册页的形式提供。HTML 版本完全交叉引用,并链接到代码示例。此外,还有一个针对 Qt 初学者的教程。您可以在网络上查看文档:http://www.troll.no/qt/。
Troll Tech 已将 Linux 用作其主要开发平台超过 2 年。所有 X11 开发首先在 Linux 上完成,然后将源代码移动到其他平台进行测试和移植。Qt 目前在多种 UNIX 变体、Windows 95 和 Windows NT 下运行。
让我们首先看一下 Qt 中可能与其他 GUI 工具包最不同的部分——对象通信机制。GUI 编程中最令人恐惧和 hackish 的方面之一一直是可怕的回调函数。在大多数工具包中,小部件为它们触发的每个操作都有一个指向函数的指针。任何使用过函数指针的人都知道这可能会变得非常混乱。Qt 以一种全新的方式解决了 GUI 对象(以及其他对象)之间的通信问题。Qt 引入了信号和槽的概念,消除了对函数指针的需求,并提供了一种类型安全的方式来发送任何类型的参数。所有 Qt 对象(从 QObject 或其后代继承的类,例如 QWidget)都可以包含任意数量的信号和槽。当对象以可能对外界有趣的方式更改其内部状态时,它会发出一个信号(不要与 UNIX 进程间信号混淆),然后继续愉快地处理自己的事务,永远不知道或关心是否有人接收到该信号。这个重要特性允许该对象用作真正的软件组件。槽是可以连接到信号的成员函数。槽不知道或不关心是否有信号连接到它。同样,该对象与世界的其余部分隔离,可以用作真正的软件组件。这两个简单的概念构成了一个强大的组件编程系统。当第一次遇到它们时,它们可能看起来很笨拙,但它们比替代方案更直观,更易于学习和使用。让我们看看如何在类声明中指定信号和槽。以下类是代码清单 3 中所示的类的简化版本
class PixmapRotator : public QWidget { Q_OBJECT public: PixmapRotator( QWidget *parent=0, const char *name=0 ); public slots: void setAngle( int degrees ); signals: void angleChanged( int ); private: int ang; };
信号和槽在类声明中使用 C++ 类别以语法方式指定。上面定义的这个类有一个名为 setAngle 的槽。槽是普通的成员函数,并且必须具有访问说明符。与其他成员函数一样,它们由程序员实现,并且可以被重载或虚拟化。
PixmapRotator 类有一个信号,angleChanged,当其角度值发生变化时,它会发出该信号。信号由程序员在类声明中声明,但实现是自动生成的。要发出信号,请输入
emit signal( arguments )
槽的实现如下所示
void PixmapRotator::setAngle( int degrees ) { // keep in range <-360, 360> degrees = degrees % 360; // actual state change? if ( ang == degrees ) return; ang = degrees; // a new angle emit angleChanged( ang ); // tell world ... }
请注意,setAngle 仅在值实际更改时才发出信号(正如信号的名称所暗示的那样)。只有在状态更改发生时才应发出信号。
要将信号连接到槽,请使用 QObject 静态成员函数 connect,例如
connect( scrollBar, SIGNAL(valueChanged(int)), rotator, SLOT(setAngle(int)) );
这里,QScrollBar scrollBar 的信号 valueChanged 连接到 PixmapRotator rotator 的槽 setAngle。此语句确保每当滚动条更改其值时(例如,如果用户单击其箭头之一),PixmapRotator 对象的角度将相应地更改。只要第三方建立连接,这两个对象就可以在彼此不知情的情况下进行交互。
如您所见,信号和槽可以有参数。可以丢弃信号的最后一个参数,但否则参数必须匹配才能建立连接。
任意数量的槽可以连接到单个信号,反之亦然。
从技术上讲,信号和槽是使用 Qt 元对象编译器 (moc) 实现的。它解析 C++ 头文件并生成 Qt 处理信号和槽所需的 C++ 代码。signals、slots 和 emit 关键字是宏,因此编译器预处理器会更改或删除它们。
信号和槽是高效的。当然,它们不如直接函数指针调用快,但差异很小。在 SPARC2 上,信号触发槽已被测量约为 50 微秒。
Qt 包含一个设备无关的绘图引擎,在类 QPainter 中实现。QPainter 经过高度优化,并包含多种缓存机制以加速绘图。在 X11 下,它缓存 GC(图形上下文),这通常使其比原生 X11 程序更快。QPainter 包含人们对专业 2D 图形库所期望的所有功能。
QPainter 的坐标系可以使用标准 2D 变换(平移、缩放、旋转和剪切)进行变换。这些变换可以直接完成,也可以通过变换矩阵 (QWMatrix) 完成,就像在 postscript 中一样。这是一个取自 www.troll.no/qt 的小例子,展示了坐标变换的使用
void LJWidget::drawLJWheel( int x, int y, QPainter *p ) { // set center point to 0,0 p->translate( x, y ); // 24 point bold Times p->setFont(QFont("Times", 24, QFont::Bold)); // save graphics state p->save(); // full circle for( int i = 0 ; i < 360/15 ; i++ ) { // rotate 15 degrees more p->rotate( 15 ); // draw rotated text p->drawText( 0, 0, "Linux" ); } p->restore(); // restore graphics state p->setPen( green ); // green 1 pixel width pen // draw unrotated text p->drawText( 0, 0, "Linux Journal" ); }
此成员函数绘制一个文本“轮”,其中心位于指定点。首先,变换坐标系,使给定点成为新坐标系中的点 (0,0)。接下来,设置字体,并保存图形状态。然后,坐标系每次顺时针旋转 15 度,并绘制文本“Linux”以形成文本“轮”。然后恢复图形状态,将笔设置为绿色笔,并显示文本“Linux Journal”。请注意,严格来说没有必要保存图形状态,因为我们进行了完整的 360 度旋转。保存图形状态严格来说是一种防御性编程——如果我们更改执行旋转的 for 循环,我们仍然可以保证最后一个文本将水平输出。
Qt 在 QFont 类中实现了一个字体抽象。字体可以根据字体系列、磅值和多个字体属性来指定。如果指定的字体不可用,Qt 将使用最接近的匹配字体。
drawLJWheel 函数可用于在任何设备上生成输出,因为它仅使用指向 QPainter 的指针。它不知道 painter 正在操作什么类型的设备。该函数被放入代码中的小部件中,请参见 http://www.troll.no/qt。 运行它会产生 图 1 中所示的结果。
图 1. LJ 小部件输出
Qt 还包含一组通用类和许多集合类,以简化多平台应用程序的开发。生成可移植代码最困难的部分一直是操作系统相关的功能。Qt 对这些功能(例如,时间/日期、文件/目录和 TCP/IP 套接字)具有平台无关的支持。有时可能需要直接使用底层窗口系统资源,例如,当与其他库接口时。Qt 提供对所有低级窗口 ID 和其他资源 ID 的直接访问。Troll Tech 已使用此访问权限编写了一个小部件,使在 Qt 小部件中使用 OpenGL/mesa 成为可能。
任何 GUI 程序的结构都基于事件。这个基础是 GUI 编程和非 GUI 编程之间的主要区别。GUI 程序不“控制”应用程序;它只是等待事件,做出响应,然后等待下一个事件。
程序通常会设置一个顶级小部件来调用主事件循环,然后主事件循环会在收到来自用户或系统其他部分的事件时分派事件。
通过在 Qt 也使用的经典 C++ 事件机制中使用子类和重新实现虚函数,可以在面向对象的语言中优雅地应用此模型。QWidget 类为每种事件类型包含一个虚函数。通过子类化 QWidget(或其后代)来创建一种新型的小部件。您可以简单地为您希望接收的每种事件类型重新实现事件函数。事件函数与 Qt 绘图引擎一起构成了创建自定义小部件的强大工具箱。
小部件接收到的最重要事件是绘制事件。每当小部件需要绘制自身的一部分时,主事件循环都会调用它。以下是来自代码的简单绘制事件示例:http://www.troll.no/qt:
void CustomWidget::paintEvent( QPaintEvent *e ) { // necessary to draw? if ( rect.intersects( e->rect() )) { QPainter p; p.begin( this ); // paint this widget p.setBrush( color ); // fill color p.drawRect( rect ); // draw rectangle p.end(); } }
CustomWidget 包含类型为 QRect 的成员变量 rect,其中包含一个带有 1 像素黑色轮廓并填充颜色的矩形。另一个成员变量是类型为 QColor 的 color,其中包含用于填充矩形的颜色。
首先,我们检查矩形是否与要更新的小部件部分相交。如果相交,我们实例化一个 QPainter,在小部件上打开它,将其笔刷设置为正确的颜色,并绘制矩形(默认笔刷为一个像素粗细且为黑色)。
所有事件函数都将指向事件对象的指针作为其单个参数。QPaintEvent 包含必须重绘的小部件的矩形区域。
在同一个小部件中,我们还会收到如下所示的调整大小事件
void CustomWidget::resizeEvent( QResizeEvent * ) { // widget size - 20 pixel border rect = QRect( 20, 20, width() - 40, height() - 40 ); }
此事件函数将矩形设置为小部件大小减去四边各 20 像素的边框。在调整大小事件中永远不需要重绘小部件,因为当小部件被调整大小时,Qt 始终会在调整大小事件之后发送绘制事件。
CustomWidget 还会收到如下所示的鼠标按下、移动和释放事件
void CustomWidget::mousePressEvent( QMouseEvent *e ) { // left button click if ( e->button() == LeftButton && // on rectangle? rect.contains( e->pos() ) ) { // set rectangle color to red color = red; // remember that it was clicked clicked = TRUE; // repaint without erase repaint( FALSE ); } } void CustomWidget::mouseMoveEvent(QMouseEvent *) { // clicked and first time? if ( clicked && color != yellow ) { color = yellow; // set color to yellow repaint( FALSE ); // repaint without erase } } void CustomWidget::mouseReleaseEvent(QMouseEvent *e) { if ( clicked ) { // need to reset color color = green; // set color to green repaint( FALSE ); // repaint without erase clicked = FALSE; } }
如果左键单击矩形内部,则鼠标按下事件会将矩形颜色设置为红色。当在单击矩形后移动鼠标时,颜色将变为黄色。最后,当释放鼠标按钮时,颜色将重置为绿色。
对 repaint 的调用会导致整个小部件被重绘。FALSE 参数指示 Qt 在发送绘制事件之前不要擦除小部件(用背景色填充)。我们可以使用 FALSE,因为我们知道 paintEvent 将绘制一个覆盖旧矩形的新矩形。以这种方式绘制可以大大减少闪烁;否则,应使用双缓冲。
完整的窗口小部件代码可以在 www.troll.no/qt 中找到。运行它会产生 图 2. 自定义小部件输出 中所示的结果
闪烁是图形编程中常见的问题。一些 GUI 程序通过清除小部件区域然后绘制不同的图形元素来进行更新。此过程通常需要足够的时间让眼睛注意到清除和绘制过程。小部件闪烁,程序看起来不专业,并且会使用户的眼睛疲劳。
可以使用一种称为双缓冲的技术来解决此问题。使用 pixmap(即,像素图——用作屏幕光栅缓冲区一部分的屏幕外内存段),并且所有绘图都在此 pixmap 上进行屏幕外绘制。然后将 pixmap 以闪电般快速的操作传输到屏幕。此 pixmap 传输通常非常快,以至于在大多数系统上,它对人眼来说是瞬时的。
有时使用与要更新的小部件大小相同的 pixmap,在其他情况下,仅对小部件的某些部分进行双缓冲。哪种方法最有效必须在每种情况下考虑。
Pixmap 通常包含大量数据,并且创建和处理速度通常很慢(在 CPU 时间方面)。一种好的技术是将缓冲区 pixmap 存储为小部件的一部分。当小部件需要更新自身时(在 Qt 中,每当它收到绘制事件时),它只需将缓冲区 pixmap 的所需部分复制到必须重绘的小部件部分。
通常,将脏标志作为小部件的一部分包含在内很有用。然后,所有影响小部件视觉外观的状态更改(即,对成员变量的更改)都可以简单地将此标志设置为 TRUE,告诉小部件重绘自身。然后,绘制事件函数检查脏标志,并在更新屏幕之前更新缓冲区 pixmap。这确保了所有小部件绘制代码都位于一个位置,从而使小部件更易于维护和调试。
我发现这种技术非常有用和强大,并且已将其用于各种 GUI 系统以及 Qt。
好的,现在我们已经了解了 Qt 的不同部分,让我们使用它来构建一个自定义的软件组件,该组件可以显示图像并按角度旋转它。此小部件应包含带有指令的槽,以允许用户在磁盘上选择文件,并且应具有在打印机上打印旋转图像的能力。
首先,我们决定给它以下信号和槽
public slots: void setAngle( int degrees ); void load(); void print(); signals: void angleChanged( int ); void filePathChanged( const char * );
我们现在可以设置旋转角度 setAngle,让用户选择新的图像文件 load,并打印图像 print。我们选择首先实现第一个版本所需的功能。稍后,可以扩展此组件以包含诸如 setPixmap(QPixmap) 或 setFilePath(QString) 之类的槽。
这两个信号告诉世界旋转角度 (angleChanged) 或正在显示的图像文件 (filePathChanged) 的变化。
接下来,我们包含两个成员函数来获取角度和文件路径
public: int angle() const { return ang; } const char *filePath() const { return name; }
我们包含以下成员变量
private: int ang; QString name; QPixmap pix; QPrinter printer; QFileDialog fileDlg; QPixmap bufferPix; bool dirty;
通过设置这些变量,我们在组件中存储角度、文件名、pixmap、打印机和文件选择对话框。我们希望小部件平滑地更新自身,并已决定使用双缓冲技术,因此我们存储一个缓冲区 pixmap。此外,我们有一个脏标志,当小部件需要更新缓冲区 pixmap 时,会设置该标志。缓冲区 pixmap 和脏标志的组合是一种非常有用且强大的 GUI 技术,可以用于各种小部件。
此外,该小部件具有绘制事件函数和调整大小事件函数,有关完整的类声明,请参见代码清单 3。
由于我们希望能够绘制到屏幕和打印机,因此我们将绘制代码放在对 QPainter 进行操作的私有成员函数中
void PixmapRotator::paintRotatedPixmap(QPainter *p) { // need device width and height QPaintDeviceMetrics m( p->device() ); // center point p-$gt;translate( (m.width())/2, (m.height()) / 2 ); p->rotate( ang ); p->drawPixmap( - (pix.width())/2, - (pix.height())/2, pix ); }
首先,我们获取 painter 正在操作的设备的度量标准。我们使用设备的宽度和高度将坐标系的原点 (0,0) 放在设备的中间。接下来,我们将坐标系按所需的角度旋转,并绘制 pixmap,其中心点位于 (0,0)。换句话说,pixmap 的中心点放置在设备的中心点。
绘制事件函数如下所示
void PixmapRotator::paintEvent( QPaintEvent *e ) { if ( dirty ) { // buffer needs update? // same size as widget bufferPix.resize( size() ); // clear pixmap bufferPix.fill( backgroundColor() ); QPainter p; // paint on buffer pixmap p.begin( &bufferPix ); paintRotatedPixmap( &p ); p.end(); dirty = FALSE; // buffer now new and clean } // update exposed region: bitBlt( this, e->rect().topLeft(), &bufferPix, e->rect() ); }
如果小部件是“脏的”,我们需要更新缓冲区 pixmap。我们将它的大小设置为小部件的大小,清除它并调用我们的本地绘制函数。更新缓冲区 pixmap 后,不要忘记重置脏标志。
最后,我们使用 QPaintEvent 指针来找出必须更新的小部件的哪一部分,并调用 bitBlt。bitBlt 是一个全局函数,可以尽可能快地将数据从一个绘制设备传输到另一个绘制设备。bitBlt 是 GUI 速记,表示“位块传输”。
使用双缓冲和脏标志,调整大小事件函数变得微不足道
void PixmapRotator::resizeEvent( QResizeEvent *e ) { dirty = TRUE; // need to redraw }
同样,在调整大小事件中永远不需要重绘小部件,因为 Qt 会在调整大小事件之后自动发送绘制事件。
使用通用的绘制函数,打印也很容易
void PixmapRotator::print() { // opens printer dialog if ( printer.setup(this) ) { QPainter p; p.begin( &printer ); // paint on printer paintRotatedPixmap( &p ); p.end(); // send job to printer } }
首先,我们让用户设置打印机,然后在该打印机上打开 painter,最后,调用绘制函数。
加载新图像需要更多代码
void PixmapRotator::load() { QString newFile; QPixmap tmpPix; while ( TRUE ) { // open file dialog if ( fileDlg.exec() != QDialog::Accepted ) return; // the user clicked cancel // get the file path newFile = fileDlg.selectedFile(); // is it an image? if ( tmpPix.load( newFile ) ) break; // yes, break the loop QString s; // build a message string s.sprintf("Could not load \"%s\"", newFile.data() ); // sorry! QMessageBox::message( "Error", s ); } pix = tmpPix; // keep the pixmap name = newFile; // new file name emit filePathChanged( name ); // tell world dirty = TRUE; // need to redraw repaint( FALSE ); // paint the whole widget }
我们设置一个循环,打开文件对话框。如果用户选择了无法加载的文件,我们会告诉她并让她重试。如果选择了有效文件,我们将复制新的 pixmap 和文件路径。最后,我们发出一个信号告诉世界,将小部件标记为脏并重绘所有内容(FALSE 参数表示 Qt 在发送绘制事件之前不应清除小部件)。
我们现在制作了一个新的软件组件,可以通过信号/槽机制将其连接到其他组件。有关 PixmapRotator 小部件的完整代码,请参见 www.troll.no/qt。
最后,让我们使用我们的组件来组合一个对话框。大多数 GUI 应用程序都包含这些框。对话框是带有许多小部件作为子窗口的窗口。对话框的典型示例是数据库的输入表单,每个数据库字段都有一个文本字段。
Qt 包含构建大多数用途对话框所需的标准小部件。此自定义构建的对话框类取自 www.troll.no/qt
class RotateDialog : public QDialog { Q_OBJECT public: RotateDialog( QWidget *parent=0, const char *name=0 ); void resizeEvent( QResizeEvent * ); private slots: void updateCaption(); private: QPushButton *quit; QPushButton *load; QPushButton *print; QScrollBar *scrollBar; QFrame *frame; PixmapRotator *rotator; };
RotateDialog 类继承自 QDialog,并包含 3 个按钮、一个滚动条、一个框架和自定义的 pixmap 旋转器。对话框只有三个成员函数。构造函数初始化对话框中的不同小部件,resizeEvent 设置小部件的位置和大小,槽更新对话框的标题文本。
让我们看一下构造函数
RotateDialog::RotateDialog( QWidget *parent, const char *name ) : QDialog( parent, name ) { frame = new QFrame( this, "frame" ); frame->setFrameStyle( QFrame::WinPanel | QFrame::Sunken ); rotator = new PixmapRotator("this, rotator"); rotator->raise(); // put it in front of frame quit = new QPushButton("Quit", this, "quit"); quit->setFont(QFont("Times", 14, QFont::Bold)); load = new QPushButton("Load", this, "load"); load->setFont( quit->font() ); print = new QPushButton("Print", this, "print"); print->setFont( quit->font() ); scrollBar = new QScrollBar(QScrollBar::Horizontal, this, "scrollBar" ); scrollBar->setRange( -180, 180 ); scrollBar->setSteps( 1, 10 ); scrollBar->setValue( 0 ); connect( quit, SIGNAL(clicked()), qApp, SLOT(quit()) ); connect( load, SIGNAL(clicked()), rotator, SLOT(load()) ); connect( print, SIGNAL(clicked()), rotator, SLOT(print()) ); connect( scrollBar, SIGNAL(valueChanged(int)), rotator , SLOT(setAngle(int)) ); connect( rotator, SIGNAL(angleChanged(int)), SLOT(updateCaption()) ); connect( rotator, SIGNAL(filePathChanged(const char *)),, SLOT(updateCaption()) ); setMinimumSize( 200, 200 ); }
现在已经实例化和初始化了不同的小部件。我们还想在 pixmap 旋转器周围放置一个框架,因此将其提升(弹出到窗口堆栈的前面),以确保它位于框架的前面。
滚动条设置为表示范围 [-180,180] 中的值,行步长和页步长分别设置为 1 和 10。当用户单击滚动条箭头时使用行步长,当用户在箭头和滚动条滑块之间单击时使用页步长。
然后连接不同的小部件。quit 按钮连接到应用程序的 quit 槽(qApp 是指向 Qt 应用程序对象的指针)。load 和 print 按钮连接到 pixmap 旋转器中各自的槽,滚动条连接到 pixmap 旋转器的角度值。
接下来,我们将旋转器的信号连接到私有槽 updateCaption。我们在此处使用仅接受三个参数的 connect 函数,其中 this 指针作为接收器是隐式的。请注意,我们连接到的槽的参数少于信号。如果我们不希望在槽中接收信号的所有或某些最后一个参数,则始终可以以这种方式丢弃它们。
请注意我们如何使用标准小部件来控制我们的自定义小部件。PixmapRotator 是一个新的软件组件,可以插入到许多不同的标准界面控件中。通过将其插入旋转器的槽中,我们可以轻松地添加一个菜单。键盘加速器或用于输入角度数值的文本字段同样可以添加,而无需对 PixmapRotator 进行任何更改。
最后,我们告诉 Qt 不应允许此小部件的尺寸小于 200x200 像素。请注意,该类没有析构函数。子小部件始终在父小部件被删除时由 Qt 删除。
在撰写本文时(1996 年 7 月),Qt 还没有交互式对话框构建器,但正在开发一个,在您阅读本文时可能会准备就绪。查看 Troll Tech 的主页以获取详细信息 (http://www.troll.no/)。调整大小事件的实现如下
const int border = 10; const int spacing = 10; const int buttonH = 25; const int buttonW = 50; const int scrollBarH = 15; void RotateDialog::resizeEvent( QResizeEvent * ) { quit->setGeometry(border, border, buttonW, buttonH ); load->setGeometry((width() - buttonW)/2, border, buttonW, buttonH ); print->setGeometry(width() - buttonW - border, border, buttonW, buttonH ); scrollBar->setGeometry( border, quit->y() + quit->height() + spacing, width() - border*2, scrollBarH ); int frameTop = scrollBar->y() + scrollBar->height() + spacing; frame->setGeometry( border, frameTop, width() - border*2, height() - frameTop - border ); rotator->setGeometry( frame->x() + 2, frame->y() + 2, frame->width() - 4, frame->height() - 4 ); }
每个小部件都根据对话框的宽度和高度移动和调整大小。这三个按钮放置在对话框顶部 10 像素处,每侧各一个,中间一个。滚动条放置在它们的正下方,然后是框架。旋转器放置在框架内部。
编写像上面这样的代码并不困难,但并不总是易于阅读。几何管理器以更优雅的方式解决了对话框的大小调整问题。Qt 的几何管理器目前正在 Troll Tech 进行内部测试,并将很快添加到工具包中。槽的实现如下所示
void RotateDialog::updateCaption() { QString s; // we do not want the full path QFileInfo fi( rotator->filePath() ); s = fi.fileName(); // only the filename if ( rotator->angle() != 0 ) { // rotated? s += " rotated "; QString num; // convert number to string num.setNum( rotator->angle() ); s += num; s += "degrees"; } setCaption( s ); // set dialog caption }
我们通过使用图像文件名及其旋转角度来构建消息字符串。然后,该字符串用作对话框的标题。
有关完整代码,请参见 www.troll.no/qt。运行时,用户可以按下 load 按钮,Qt 的标准文件对话框将弹出。如果单击 print 按钮,标准打印对话框将弹出,让用户选择输出是转到文件还是打印机。在 X 下,Qt 生成 postscript 打印机输出;在 Windows 下,使用 Windows 打印机驱动程序系统。
在 图 3 中,您可以看到 RotateDialog 和打印对话框的屏幕截图。
如果您是 GUI 编程新手,您可能会发现您的第一个小部件在屏幕上显示为空白或根本不显示。发生这种情况有两个非常常见的原因。
首先,窗口系统可以随时覆盖小部件在屏幕上的窗口。窗口系统不会为您存储内容,它只是在小部件需要刷新时调用小部件的绘制事件函数。因此,如果您在小部件上绘制,但绘制事件函数没有完全重现绘制,您可能看不到小部件上的任何效果。通常,最好的方法是在绘制事件函数中完成所有绘制,并且仅在小部件的状态更改时指示小部件执行重绘。
其次,当您创建一个小部件时,它是不可见的。在 Qt 中,您必须调用 show 成员函数才能使小部件可见;其他工具包具有类似的功能。
最后,我想提及我在设计和实现 GUI 程序时认为重要的一些要点
保持简单。当您构建新的小部件和对话框时,请尽量保持其界面尽可能小巧而优雅。不要添加将来可能需要的大量功能。
不要使屏幕拥挤。尽量使您的对话框尽可能直观和简约。对话框应兼具功能性和美观性。
使用双缓冲。至少当小部件的全部或部分内容会在一段时间内发生显着变化时。不闪烁的程序看起来比闪烁的程序更专业。
疯狂缓存。如果您的程序中有某些部分执行耗时的操作,例如,生成 pixmap,请保存结果,以便您不必一遍又一遍地执行相同的操作。
保持您的成员变量私有——这是良好的面向对象实践。在 GUI 编程中,这一点更为重要。变量的更改通常意味着您必须更新屏幕。如果您通过成员函数设置变量,则可以保证屏幕始终是最新的。
将您的绘图代码放在一个位置——最好在一个成员函数中。成员变量中的值与小部件显示内容之间的关系中的错误然后位于一个位置。(您会很高兴您这样做了。)
使用标准类型作为信号和槽的参数。如果您的信号包含一个 int,则它可以连接到大量槽。如果它包含 MyNumber 类型的参数,则小部件或对话框将不如组件有用。
Eirik Eng (iriken@troll.no) 是 Troll Tech AS 的联合创始人,并在那里担任开发人员。他拥有挪威理工学院的 siv.ing. (M.S.) 学位,自 1991 年以来一直从事 GUI 和 OOP 工作。他的主要爱好是办公室园艺(非常适合在办公室花费大量时间的人)。今年,他特别为他 4 英尺高(且仍在生长)的茄子和罗望子树感到自豪。