QT 开发基础知识(二)

原文:Foundations of Qt Development

协议:CC BY-NC-SA 4.0

四、主窗口

到目前为止,在本书中,你主要使用对话框与用户交流。然而,虽然当您需要一个小部件来保存小部件并指导用户完成特定任务或配置特定主题的选项时,对话框是一个很好的解决方案,但大多数应用程序并不仅仅基于一个特定的任务,而是基于一个文档。这是主窗口进入画面的地方。

主窗口是应用程序所基于的顶层窗口。它可以有菜单栏、工具栏、状态栏以及工具箱和其他支持窗口可以停靠的区域。可以从主窗口打开应用程序的对话框,主窗口包含工作文档。


除非另有说明,在本书的上下文中,术语文档不指用于文字处理目的的文件。相反,在 Qt 的上下文中,文档是指用户与之交互的实际数据。这些数据可以代表任何东西,从供观看的电影到宇宙飞船的 CAD 模型。定义一个文档代表什么以及用户可以对它做什么几乎就是桌面应用程序开发的全部内容。


窗口和文档

在 windows 中排列文档有两种思路:单文档界面(SDI)和多文档界面(MDI)。区别在于每个文档是位于一个新窗口中,还是应用程序对所有文档分别只使用一个窗口。图 4-1 展示了两者的对比。MDI 界面的例子有 Qt Designer 和 Photoshop 流行的 SDI 应用程序有写字板、谷歌地球和一个无标签的网络浏览器。

MDI 概念在 Windows 3.x 时代非常普遍,而 SDI 在 X11 上一直占主导地位。大约在 Windows 95 的时候,微软的政策开始转变,今天大多数 Windows 产品都有 SDI 接口。

为了比较这两种架构和它们带来的结构,您将围绕QTextEdit小部件构建两个应用程序,其中文本编辑器将充当文档小部件。

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

图 4-1。 单文档界面与多文档界面的对比

单文档界面

让我们从单个文档界面开始。在 SDI 驱动的环境中,每个主窗口对应一个文档。文档本身保存在一个名为中心小部件的小部件中。每个主窗口都有一个中心小部件,它出现在添加了所有菜单栏、停靠小部件、工具栏等的窗口的中心区域。

这为我们的应用程序提供了一个围绕主窗口及其中心小部件构建的结构。这两个对象一起将包含几乎所有对用户交互作出反应的槽,所以对用户动作的所有响应都是从这两个类中的一个发起的。

主窗口的窗口与诸如禁用和启用菜单项、创建新文件和关闭窗口之类的任务相关联——内务处理任务。中央小部件的插槽处理修改实际文档的用户交互——工作任务。这些任务可以包括标准的剪贴板操作,例如使用剪切、复制和粘贴;执行特定于文档的操作,例如旋转图像;停止播放;或者运行向导——任何适用于相关应用程序文档的操作。

文本编辑器

让我们基于QTextEdit小部件创建一个简单的 SDI 驱动的应用程序,它可以用作多行QLineEdit的等价物或简单的文字处理器。你可以在清单 4-1 所示的主窗口的构造器中看到它和一些 SDI 特有的细节。应用程序的截图如图 4-2 所示。

**清单 4-1。**SDI 主窗口的构造器

SdiWindow::SdiWindow( QWidget *parent ) : QMainWindow( parent )

{

  setAttribute( Qt::WA_DeleteOnClose );

  setWindowTitle( QString("%1[*] - %2" ).arg("unnamed"-).arg(-"SDI") );

  docWidget = new QTextEdit( this );

  setCentralWidget( docWidget );

  connect( docWidget->document(), SIGNAL(modificationChanged(bool)),

    this, SLOT(setWindowModified(bool)) );

  createActions();

  createMenus();

  createToolbars();

  statusBar()->showMessage( "Done" );

}

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

图 4-2。 一个单文档应用两个文档

让我们研究一下这段代码。首先,将 window 属性设置为Qt::WA_DeleteOnClose,这样 Qt 会在窗口关闭后立即从内存中删除它。这意味着需要担心的内存管理更少。

接下来,窗口标题被设置为QString("%1[*] - %2" ).arg("unnamed").arg("SDI")arg方法调用插入"unnamed""SDI"字符串,其中%1%2符号出现在第一个字符串中。最左边的arg代替了%1;下一个替换%2;等等。使用这种方法,最多可以将九个字符串与一个主字符串合并。

您可以使用setWindowTitle来设置任何窗口标题。您使用前面例子中显示的标题,因为它允许 Qt 帮助我们管理部分标题(例如,指示当前文档是否已被修改)。这解释了命令的一部分,但是没有解释为什么第一个字符串在对tr的调用中,或者为什么你不马上使用"unnamed[*] - SDI"。你希望能够支持其他语言(你会在第十章中了解到更多)。

现在,记住显示给用户的所有字符串都需要包含在对tr()的调用中。虽然这是由 Designer 自动完成的,但是当你通过代码创建用户界面和设置文本时,你需要自己管理它。


提示脚本可以用来查找丢失的字符串tr()。如果您使用的是 Unix shell,您可以使用下面这行代码来查找它们:grep -n '"' *.cpp | grep -v 'tr('。另一种方法是阻止 Qt 自动将char*字符串转换成QString对象。这将导致编译器错误的所有时间,你错过了调用tr()。您可以通过在项目文件中添加一行DEFINES += QT_NO_CAST_FROM_ASCII来禁用转换。


您使用arg方法是因为从翻译者的角度来看,字符串unnamedSDI是独立的。比如字符串SDI用的地方比较多。通过分割字符串,您可以确保它被翻译一次,避免任何可能的不一致。此外,通过使用一个插入了unnamedSDI字符串的主字符串,您可以让翻译者重新排序这些字符串,并在它们周围添加更多的文本,使应用程序更能适应其他文化和语言。

关于设置主窗口标题的另一件事是:字符串[*]充当一些应用程序使用的文档修改标记的占位符。当windowModified属性设置为true时,显示标记;也就是文档被修改的时间。让 Qt 处理标记的显示有两个原因。首先,它避免了在所有应用程序中重复处理它的代码。在 Mac OS X 上,标题文本的颜色用于指示文档是否已被修改。通过在窗口标题中不加星号,明确地使用您自己的代码并让 Qt 来处理,您也让 Qt 处理了所支持的不同平台的任何其他方面。

这是一个窗口标题的大量信息!继续向下清单 4-1 到创建QTextEdit的行,并将其设置为主窗口的中心小部件。这意味着它将填充整个主窗口,并作为用户的文档视图。

下一行将文本编辑器文档的修改状态连接到主窗口的windowModified属性。它让 Qt 在修改文档时显示星号并改变标题文本的颜色。信号从docWidget-> document()发出,而不是直接从docWidget发出,因为格式化的文本由QTextDocument表示。QTextEdit只是格式化文本的查看器和编辑器,所以文档是被修改的,而不是编辑器——因此信号是从文档发出的。

采取行动

继续回顾清单 4-1 中的,你会看到设置菜单、工具栏和状态栏的四行代码。在创建这些实际菜单之前,创建动作。包含在类QAction中的动作可以将文本、工具提示、键盘快捷键、图标等存储到一个类中。每个动作都会发出信号triggered()——当被用户调用时,还可能发出信号toggled(bool)。当动作配置为可检查时,会发出切换信号。动作的工作方式很像按钮,既可以是可检查的,也可以是可点击的。

好的一面是,同样的操作可以添加到菜单和工具栏中,所以如果用户通过按工具栏按钮进入高级编辑模式,相应的菜单项会被自动选中。这也适用于启用和停用操作时,菜单和按钮会自动同步。此外,唯一需要的连接是从动作到动作插槽的连接。

清单 4-2 向您展示了如何在方法createActions中创建动作,该方法是从清单 4-1 中所示的构造器中调用的。我对清单进行了略微的删减,向您展示了所使用的三种主要类型的操作。在考虑差异之前,先看看相似之处;例如,每个动作都被创建为一个QActionQAction构造器接受一个可选的QIcon,后跟一个文本和一个父对象。对于需要键盘快捷键的动作,调用setShortcut(const QKeySequence&)方法。使用setStatusTip(const QString& ),每个动作被分配一个提示,当该动作作为一个菜单项并被悬停时,该提示将显示在状态栏上。(试试吧!)这个图标奇怪的文件路径是一个所谓的资源路径(它的用法将在下面的资源部分解释)。

清单 4-2。 为 SDI 应用程序创建动作

void SdiWindow::createActions() {   newAction = new QAction( QIcon(":/img/new.png"), tr("&New"), this );   newAction->setShortcut( tr("Ctrl+N") );   newAction->setStatusTip( tr("Create a new document") );   connect( newAction, SIGNAL(triggered()), this, SLOT(fileNew()) ); ...   cutAction = new QAction( QIcon(":/img/cut.png"), tr("Cu&t"), this );   cutAction->setShortcut( tr("Ctrl+X") );   cutAction->setStatusTip( tr("Cut") );   cutAction->setEnabled(false);   connect( docWidget, SIGNAL(copyAvailable(bool)),     cutAction, SLOT(setEnabled(bool)) );   connect( cutAction, SIGNAL(triggered()), docWidget, SLOT(cut()) ); ...   aboutQtAction = new QAction( tr("About &Qt"), this );   aboutQtAction->setStatusTip( tr("About the Qt toolkit") );   connect( aboutQtAction, SIGNAL(triggered()), qApp, SLOT(aboutQt()) ); }

首先是newAction,它连接到主窗口中的一个插槽。这是合乎逻辑的地方,因为创建新文档不是由文档本身来处理的(除了初始化,而是放在文档的构造器中)。相反,文档的创建和关闭是由主窗口处理的。请注意,使用setShortcut设置的键盘快捷键包含在tr()调用中,这给了翻译人员将快捷键更改为本地化版本的自由。

接下来是cutAction。它的triggered信号在用户调用动作时发出,连接到文档中的一个槽。这也是合乎逻辑的,因为剪切会从文档中获取数据并修改文档。从copyAvailablesetEnabled的连接是如何启用和禁用动作的一个例子。一旦选择了,就会发出copyAvailable,并以true作为参数。当没有可用选项时,参数为false。因此,该操作在适用时被启用,在所有其他时间被禁用。

最后一个动作是aboutQtAction,它连接到qApp对象。application 对象管理应用程序全局任务,例如关闭所有窗口和显示一个对话框,其中包含有关正在使用的 Qt 版本的信息。


注意全局qApp指针变量总是被设置为指向激活的QApplication对象。要访问这个指针,你一定不要忘记在你使用它的文件中包含<QApplication>头文件。


菜单和工具栏

回头看看清单 4-1 中的,你可以看到在调用createActions之后,接下来的步骤是createMenuscreateToolbars方法。这些方法采用新创建的动作,并将它们放在正确的位置。

清单 4-3 显示了文件菜单和文件操作的工具栏是如何被动作填充的。因为每个动作已经有了文本和图标,所以只需要调用addAction(QAction*)就可以让文本和图标出现在菜单中。menuBar()addToolBar(const QString&)调用是主窗口类的一部分。第一次调用menuBar时,会创建一个菜单栏。后面的调用将引用这个菜单栏,因为每个窗口只有一个菜单。工具栏是用addToolBar方法创建的,你可以为每个窗口创建任意数量的工具栏。使用addSeparator()方法,你可以把动作分成组,在菜单和工具栏中都可以使用。

清单 4-3。 菜单和工具栏被填充。

void SdiWindow::createMenus()

{

  QMenu *menu;

  menu = menuBar()->addMenu( tr("&File") );

  menu->addAction( newAction );

  menu->addAction( closeAction );

  menu->addSeparator();

  menu->addAction( exitAction );

...

}

void SdiWindow::createToolbars()

{

  QToolBar *toolbar;

  toolbar = addToolBar( tr("File") );

  toolbar->addAction( newAction );

...

}

再次参考清单 4-1 中的——你会看到,在动作被添加到菜单和工具栏后,构造器中的最后一个调用创建了一个状态栏,并在其中显示了消息"Done"statusBar()方法的工作方式就像menuBar()一样:在第一次调用时创建并返回一个条,然后在随后的调用中返回一个指向它的指针。

新建文档并关闭打开的文档

您将使用QTextEdit类作为您的文档类,因为它包含了您需要的所有功能。它可以处理创建和编辑文本,以及从剪贴板复制和粘贴。这使得您只需要实现创建新文档和关闭任何打开的文档的功能。

创建新文档很容易。所有需要做的就是打开一个新的主窗口——清单 4-1 中的构造器将会完成所有困难的工作。清单 4-4 显示了fileNew()插槽的简单实现。它创建一个新窗口,然后显示它。

清单 4-4。 创建新文档

void SdiWindow::fileNew()

{

  (new SdiWindow())->show();

}

关闭文档更复杂,因为文档(或包含文档的窗口)可以用许多不同的方式关闭。一个可能的原因是窗口管理器由于各种原因告诉窗口关闭。例如,用户可能试图通过单击标题栏中的关闭按钮来关闭窗口。或者计算机正在关闭。或者用户从应用程序的文件菜单中选择退出或关闭。

为了拦截所有这些试图关闭当前窗口的用户操作,您可以通过覆盖closeEvent(QCloseEvent*)方法来实现 close 事件的事件处理程序。清单 4-5 展示了 SDI 应用程序的实现。

清单 4-5。 关闭文档

void SdiWindow::closeEvent( QCloseEvent *event )

{

  if( isSafeToClose() )

    event->accept();

  else

    event->ignore();

}

bool SdiWindow::isSafeToClose()

{

  if( isWindowModified() )

{

    switch( QMessageBox::warning( this, tr("SDI"),

      tr("The document has unsaved changes.\n"

         "Do you want to save it before it is closed?"),

         QMessageBox::Discard | QMessageBox::Cancel ) )

    {

    case QMessageBox::Cancel:

      return false;

    default:

      return true;

    }

  }

  return true;

}

你可以选择accept()ignore()一个事件:忽略一个关闭事件让窗口打开,接受它关闭窗口。为了确保关闭窗口是安全的,使用isSafeToClose方法,该方法使用isWindowModified()确定文档是否被修改。如果文档没有被修改,关闭它是安全的。如果文档已经被修改,询问用户是否可以使用QMessageBox放弃修改。


向用户显示简短的信息时,提示 QMessageBox非常有用。四个静态方法informationquestionwarningcritical可以用来显示不同重要性的消息。这四种方法都接受五个参数:父部件、标题文本、消息文本、要显示的按钮组合以及将用作默认按钮的按钮。按钮和默认按钮都有默认设置。

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

按钮可以通过对QMessageBox::StandardButtons枚举类型的成员进行“或”运算来配置。可用按钮有:OkOpenSaveCancelCloseDiscardApplyResetRestoreDefaultsHelpSaveAllYesYesToAllNoNoToAllAbortRetryIgnore。可以从同一列表中选择默认按钮,但只允许将一个按钮设置为默认按钮。四种方法之一的返回值是选中的按钮,如列表中所示。


如果文档没有被修改,或者如果用户选择用Discard按钮关闭消息框,并且closeEvent成员接受事件,那么isSafeToClose成员的结果是true。如果用户点击Cancel,关闭事件被忽略。

关闭事件可以有几个来源:用户可能点击了关闭或退出文件菜单,或者用户可能使用当前平台的功能关闭了窗口。如果 close 事件的来源是正在退出的应用程序,那么被忽略的 close 事件意味着不再有窗口被关闭。用户取消退出的整个过程,而不仅仅是当前窗口的关闭,这使得使用单个文档中显示的QMessageBoxCancel按钮来取消整个应用程序的整个关闭过程成为可能

在第八章的中,你将会了解到如果你扩展isSafeToClose方法,在关闭时整合保存的更改真的很容易。该结构现在看起来不必要的复杂,因为您还需要能够处理关闭前保存选项。

构建应用

要从SdiWindow类创建,需要提供一个普通的main函数,在创建和显示SdiWindow之前初始化一个QApplication对象。然后,应用程序自行运行,为新文档创建新窗口,并在所有文档关闭后结束。

要构建它,您还必须创建一个项目文件——使用通过运行qmake -project创建的文件就足够了。然后简单地运行qmake然后运行make来编译和链接应用程序。

多单据界面

为了比较 SDI 和 MDI 方法并了解它们的区别,您将基于上一节中使用的相同主题创建一个 MDI 应用程序。在图 4-3 中提供了应用程序的屏幕截图。

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

图 4-3。 一个多文档应用有两个文档

在应用程序中,每个文档在主窗口中都有一个较小的窗口,这是使用一个文档小部件类和一个QWorkspace实现的。工作区是包含所有文档窗口的区域。

从用户的角度来看,MDI 应用程序与 SDI 应用程序相同,除了图 4-4 中的所示的窗口菜单,它可以排列文件窗口并移动到当前活动文件以外的文件。

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

图 4-4。 窗口菜单

文档和主窗口

在 SDI 应用程序中,可能的用户操作分为文档、主窗口和应用程序。这同样适用于 MDI 应用程序,只是文档的所有事件都必须通过主窗口,因为主窗口必须决定将事件传递给哪个文档小部件。让我们先来看看文档小部件类。你可以在清单 4-6 中看到类的定义。

**清单 4-6。**MDI 应用程序的文档小部件类

class DocumentWindow : public QTextEdit

{

  Q_OBJECT

public:

  DocumentWindow( QWidget *parent = 0 );

protected:

  void closeEvent( QCloseEvent *event );

  bool isSafeToClose();

};

MDI 应用程序中的 document 类可以与 SDI 应用程序主窗口的精简版本相比较。它包含的所有内容都是文档的细节,因此它需要剥离所有应用程序全局代码以及用于创建新文档的函数。

该类继承了QTextEdit类并获得了相同的接口。isSafeToClosecloseEvent方法的交互就像 SDI 示例一样,而构造器看起来略有不同。清单 4-7 显示了构造器,它告诉 Qt 在设置标题和在文档的修改状态和文档窗口本身的windowModified属性之间建立联系之前,一旦关闭文档窗口就删除文档窗口。

清单 4-7。 文档控件类的构造器

DocumentWindow::DocumentWindow( QWidget *parent ) : QTextEdit( parent )

{

  setAttribute( Qt::WA_DeleteOnClose );

  setWindowTitle( QString("%1[*]" ).arg("unnamed") );

  connect( document(), SIGNAL(modificationChanged(bool)),

    this, SLOT(setWindowModified(bool)) );

}

这就是文档窗口的全部内容——只需设置一个标题并建立一个连接,让 Qt 指示文档是否被修改过。同样,使用arg方法将unnamed添加到窗口标题的方法给了翻译人员更多修改文本的自由。Qt 使用窗口标题的[*]部分来显示或隐藏星号,以表明文件是否被修改。

让我们转到主窗口。它显示在清单 4-8 中,看起来非常像 SDI 应用程序构造器的其余部分——除了一点小的增加。

清单中突出显示的行显示了如何创建一个QWorkspace并将其设置为主窗口的中心小部件。工作区是一个小部件,它将放入其中的所有小部件视为 MDI 子部件。(参见图 4-3——这两个文档是放在工作区内的小部件。)

接下来,来自工作区的信号windowActivated连接到主窗口的enableActions。无论是因为用户更改了文档还是因为用户关闭了最后一个文档,当前活动窗口一改变,就会发出windowActivated信号。无论哪种方式,您都必须确保只启用相关的操作。(你很快就会回到这个话题。)

清单 4-8。 主窗口的构造器,高亮显示 MDI 和 SDI 之间的差异

MdiWindow::MdiWindow( QWidget *parent ) : QMainWindow( parent )

{

  setWindowTitle( tr( "MDI" ) );

  workspace = new QWorkspace;

  setCentralWidget( workspace );

  connect( workspace, SIGNAL(windowActivated(QWidget *)),

    this, SLOT(enableActions()));

  mapper = new QSignalMapper( this );

  connect( mapper, SIGNAL(mapped(QWidget*)),

    workspace, SLOT(setActiveWindow(QWidget*)) );

  createActions();

  createMenus();

  createToolbars();

  statusBar()->showMessage( tr("Done") );

  enableActions();

}

接下来,创建并连接一个名为QSignalMapper的信号映射对象。信号映射器用于将信号源与另一个信号的自变量联系起来。在这个例子中,对应于窗口菜单中每个窗口的菜单项的动作被绑定到实际的文档窗口。动作依次连接到mapper。当动作发出triggered信号时,发送动作已经与对应文档窗口的QWidget*关联。这个指针被用作信号映射对象发出的mapped(QWidget*)信号中的参数。

建立信号映射对象后,就像在 SDI 应用程序中一样建立操作、菜单和工具栏。然后,构造器的最后一行确保动作被正确启用。

管理动作

在创建主窗口的动作时,这个过程与 SDI 应用程序的过程非常相似。主要区别如下:

  • 文档窗口是通过从工作区中移除它们来关闭的,而不是通过关闭包含文档的主窗口来关闭的。
  • 窗口菜单的操作包括平铺窗口、层叠窗口、下一个窗口和上一个窗口。
  • 直接连接到 SDI 应用程序中的文档的动作连接到 MDI 应用程序中的主窗口。

清单 4-9 显示了createActions方法的部分内容。首先,你可以看到closeAction连接到workspacecloseActiveWindow()。然后你可以看到一个窗口菜单项:tileAction。它连接到workspace的相应插槽,并使工作区平铺所有包含的文档,以便可以一次看到所有文档。排列文档窗口的其他操作有层叠窗口、下一个窗口和上一个窗口。它们的设置方式与 tile 动作相同:只需将动作的triggered信号连接到工作空间的适当位置。下一个动作是separatorAction,它作为一个分隔符。为什么在这里创建它将很快变得清楚。你现在只需要知道,它是用来让窗口菜单看起来像预期的那样。

清单 4-9。 为 MDI 应用程序创建动作

void MdiWindow::createActions()

{

...

  closeAction = new QAction( tr("&Close"), this );

  closeAction->setShortcut( tr("Ctrl+W") );

  closeAction->setStatusTip( tr("Close this document") );

  connect( closeAction, SIGNAL(triggered()), workspace, SLOT(closeActiveWindow()) );

...

  tileAction = new QAction( tr("&Tile"), this );

  tileAction->setStatusTip( tr("Tile windows") );

  connect( tileAction, SIGNAL(triggered()), workspace, SLOT(tile()) );

...

  separatorAction = new QAction( this );

  separatorAction->setSeparator( true );

...

}

确保只启用可用的操作是很重要的,这样可以防止用户因显示可用的菜单项和工具栏按钮而产生混淆,这些菜单项和按钮用于在应用程序的当前状态下无效的任务。例如,当你没有打开一个文档时,你不能粘贴一些东西——这是没有意义的。因此,只要没有活动文档,就必须禁用pasteAction动作。

在清单 4-10 中,方法enableActions()显示在助手方法activeDocument()旁边。后者从QWorkspace::activeWindow获取QWidget*返回值,并使用qobject_cast将其转换成句柄DocumentWindow*qobject_cast函数使用可用于所有QObject和下降类的类型信息来提供类型安全转换。如果不能进行所请求的造型,则返回0

如果没有活动窗口或者活动窗口不是DocumentWindow类型,则activeDocument方法返回NULL(或0)。它被用在enableActions法中。两个布尔值用来使代码更容易阅读:hasDocumentshasSelection。如果工作区有一个正确类型的活动文档,大多数项目都被启用,并且separatorAction是可见的。复制和剪切操作不仅需要一个文档,还需要一个有效的选择,因此只有当hasSelectiontrue时才启用。

清单 4-10。 启用和禁用动作

DocumentWindow *MdiWindow::activeDocument()

{

  return qobject_cast<DocumentWindow*>(workspace->activeWindow());

}

void MdiWindow::enableActions()

{

  bool hasDocuments = (activeDocument() != 0 );

  closeAction->setEnabled( hasDocuments );

  pasteAction->setEnabled( hasDocuments );

  tileAction->setEnabled( hasDocuments );

  cascadeAction->setEnabled( hasDocuments );

  nextAction->setEnabled( hasDocuments );

  previousAction->setEnabled( hasDocuments );

  separatorAction->setVisible( hasDocuments );

  bool hasSelection = hasDocuments && activeDocument()->textCursor().hasSelection();

  cutAction->setEnabled( hasSelection );

  copyAction->setEnabled( hasSelection );

}

助手函数activeDocument用在了几个地方。一个示例将信号从主窗口传递到实际的文档窗口。做这件事的函数如清单 4-11 所示。在构建基于 MDI 的应用程序时,所有的QActions如菜单项和工具栏按钮都必须像这样通过主窗口。

清单 4-11。 将信号从主窗口传递到文档控件

void MdiWindow::editCut()

{

  activeDocument()->cut();

}

void MdiWindow::editCopy()

{

  activeDocument()->copy();

}

void MdiWindow::editPaste()

{

  activeDocument()->paste();

}

窗口菜单

与启用和禁用操作密切相关的是处理窗口菜单的功能。窗口菜单(参见图 4-4 )允许用户排列文件窗口和在不同文件之间切换。

清单 4-12 展示了菜单是如何创建的。除了窗口菜单之外的所有菜单都是通过将操作放入其中来创建的,就像在 SDI 应用程序中一样。窗口菜单是不同的,因为它随着文档的打开和关闭而变化。因为您需要能够改变它,所以指向它的指针—称为windowMenu—保存在类中。现在,来自菜单的信号aboutToShow()被连接到填充菜单的自定义插槽updateWindowList(),而不是向菜单添加动作。aboutToShow信号在菜单显示给用户之前发出,因此菜单总是有有效的内容。

清单 4-12。 创建窗口菜单

void MdiWindow::createMenus()

{

  QMenu *menu;

  menu = menuBar()->addMenu( tr("&File") );

  menu->addAction( newAction );

  menu->addAction( closeAction );

  menu->addSeparator();

  menu->addAction( exitAction );

...

  windowMenu = menuBar()->addMenu( tr("&Window") );

  connect( windowMenu, SIGNAL(aboutToShow()), this, SLOT(updateWindowList()) );

...

}

清单 4-13 中的显示了updateWindowList插槽。在该槽中,在添加预定义的动作之前,菜单被清除。之后,每个窗口都被添加为一个操作,前九个窗口都有一个数字作为前缀,如果使用键盘导航(用户已经按下 Alt+W 到达窗口菜单),该数字将作为快捷方式。图 4-5 中的显示了一个打开了九个以上文件的窗口菜单。

清单 4-13。 更新窗口菜单

void MdiWindow::updateWindowList()

{

  windowMenu->clear();

  windowMenu->addAction( tileAction );

  windowMenu->addAction( cascadeAction );

  windowMenu->addSeparator();

  windowMenu->addAction( nextAction );

  windowMenu->addAction( previousAction );

  windowMenu->addAction( separatorAction );

  int i=1;

  foreach( QWidget *w, workspace->windowList() )

  {

    QString text;

    if( i<10 )

      text = QString("&%1 %2").arg( i++ ).arg( w->windowTitle() );

    else

      text = w->windowTitle();

    QAction *action = windowMenu->addAction( text );

    action->setCheckable( true );

    action->setChecked( w == activeDocument() );

    connect( action, SIGNAL(triggered()), mapper, SLOT(map()) );

    mapper->setMapping( action, w );

  }

}

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

图 4-5。 窗口菜单有九个以上打开的文档

在列出窗口的foreach循环中,每个窗口由一个QAction表示。这些动作是从一个QString创建的,并且属于windowMenu对象,这意味着调用插槽中的第一个clear()可以正确地删除它们。来自每个动作的triggered信号被连接到信号映射对象的map()槽。然后对setMapping(QObject*, QWidget*)的调用将发出的动作与正确的文档窗口关联起来。如您所知,来自信号映射对象的mapped信号连接到workspacesetActiveWindow插槽。信号映射对象确保右边的QWidget*作为参数发送,而mapped信号取决于连接到map的原始信号源。

如果没有要添加到列表中的文档窗口,separatorAction将作为一个分隔符悬空,下面没有任何项目——这就是为什么它在enableActions槽中是隐藏的而不是禁用的。

创建和关闭单据

SDI 应用程序和 MDI 应用程序的区别在于处理文档的方式。这种差异在创建和关闭新文档的方法中表现得非常明显。

从清单 4-14 中的所示的主窗口的fileNew()槽开始,你可以看到诀窍是创建一个新的文档窗口而不是一个新的主窗口。随着新窗口的创建,一些连接也需要注意。一旦发出copyAvailable(bool)信号,当前活动文档就会丢失选择或有新的选择。这必须通过复制和剪切动作来反映,这就是两个connect调用所做的。

当另一个文档被激活时,复制和剪切启用的状态在enableActions()槽中管理。

清单 4-14。 创建新文档

`void MdiWindow::fileNew()
{
  DocumentWindow *document = new DocumentWindow;
  workspace->addWindow( document );

connect( document, SIGNAL(copyAvailable(bool)),
    cutAction, SLOT(setEnabled(bool)) );
  connect( document, SIGNAL(copyAvailable(bool)),
    copyAction, SLOT(setEnabled(bool)) );

document->show();
}`

当用户试图关闭主窗口时,所有文档都必须关闭。如果任何文档有未保存的更改,DocumentWindow类会询问用户是否可以关闭(如果不可以就取消事件)。主窗口的closeEvent试图使用QWorkspacecloseAllWindows()方法关闭所有文档窗口。在关闭主窗口之前,它会检查是否有任何文档处于打开状态。如果是这样,关闭事件被取消,因为用户已经选择保留文档。您可以在清单 4-15 中看到主窗口关闭事件的源代码。

清单 4-15。 关闭所有文件和主窗口

void MdiWindow::closeEvent( QCloseEvent *event )

{

  workspace->closeAllWindows();

  if( activeDocument() )

    event->ignore();

}

构建应用

类似于 SDI 应用程序过程,您需要一个简单的 main 函数来开始。在这种情况下,该函数需要做的就是初始化QApplication对象,然后创建并显示一个MdiWindow对象。

运行qmake -project,然后运行qmakemake,应该可以编译并链接应用程序。

比较单个和多个文档界面

如果比较单文档和多文档界面方法,您会很快注意到几个重要的区别。对用户来说,最重要的区别是 SDI 应用程序通常符合普通用户的期望。在 MDI 应用程序中很容易丢失文档——至少在最大化一个文档时是这样。使用 SDI 意味着所有文档都出现在任务栏中,每个窗口总是对应一个文档。

从软件开发的角度来看,SDI 应用程序更简单。测试一个窗口就足够了,因为每个窗口只处理一个文档。从开发的角度来看,MDI 方法有一个优点:文档与主窗口明显分离。这在 SDI 案例中也是可以实现的,但是需要更多的训练。您绝不能在主窗口中添加影响文档的功能;而是放在文档小部件类中。

MDI 方法还有另一个优点:可以有几种类型的文档窗口,同时仍然保持使用单一应用程序的感觉。这可能是一个不寻常的要求,但有时它是有用的。

因为 SDI 和 MDI 都很容易使用 Qt 实现,而且这两种方法都很常见,所以最终的决定取决于您。记得评估所需的开发工作,看看你的用户将如何使用应用程序;然后选择最适合你项目的。

应用资源

在创建动作的代码中,您可能已经注意到图标是如何创建的。代码看起来像这样:QIcon(":/img/new.png")。查看QIcon的构造器,可以看到唯一一个以QString作为参数的构造器期望一个文件名,这就是:/img/new.png的内容。

冒号(:)前缀通知 Qt 文件处理方法,正在讨论的文件将从应用程序资源中获取,这是一个在构建时嵌入到应用程序中的文件。因为它不是外部文件,所以您不必担心它在文件系统中的位置。如您所见,您仍然可以使用资源中的路径和目录来引用文件。资源文件包含一个自己的小文件系统。

资源文件

因此,您可以使用:前缀从应用程序资源中访问文件。但是如何将文件放入资源中呢?关键在于扩展名为qrc的 Qt 资源文件。之前的 SDI 和 MDI 应用程序使用了图 4-6 中所示的四个图标。图像文件位于project目录下的一个名为images的目录中。

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

**图 4-6。**SDI 和 MDI 应用中使用的四个图标

图像的基于 XML 的 Qt 资源文件如清单 4-16 中的所示。这是您创建的一个文件,用来告诉 Qt 将哪些文件作为资源嵌入。


提示你可以在设计器中创建资源文件。从工具菜单中调出资源编辑器,开始添加文件。


DOCTYPERCC和 qresource 标签都是必需的。每个要包含的文件都列在一个file标签中。在清单 4-16 所示的文件中,file标签以最简单的形式使用,没有任何属性。

**清单 4-16。**SDI 和 MDI 应用程序的 Qt 资源文件

<!DOCTYPE RCC>< RCC version="1.0">

<qresource>

    <file>img/new.png</file>

    <file>img/cut.png</file>

    <file>img/copy.png</file>

    <file>img/paste.png</file>

</qresource>

</RCC>

如果您想通过一个名称而不是用于构建资源的文件来引用一个资源文件,您可以使用alias属性。如果您为不同的平台使用不同的资源,这样做可能会很方便。通过别名化文件名,您可以在应用程序中引用单个文件名,并根据目标平台将不同的文件放入资源中。清单 4-17 展示了如何使用alias属性来改变一个文件的名称或者仅仅是改变资源文件中的位置。

清单 4-17。 使用 alias 来改变资源文件名

<file alias="other-new.png">img/new.png</file>

<file alias="new.png">img/new.png</file>

如果你想改变一个资源文件中几个文件的位置,你可以使用qresource标签的prefix属性。它可用于将资源文件的文件分组到虚拟目录中。清单 4-18 展示了如何使用多个qresource标签将图像划分到文件和编辑目录中。例如,在生成的应用程序中,new.png文件可以作为:/file/img/new.png被访问。

清单 4-18。 使用 prefix 来改变资源文件的位置

<qresource prefix="/file">

    <file>img/new.png</file>

</qresource>

<qresource prefix="/edit">

    <file>img/cut.png</file>

    <file>img/copy.png</file>

    <file>img/paste.png</file>

</qresource>
项目文件

在您可以从您的应用程序访问资源之前,您必须告诉 Qt 您需要哪些资源文件。没有限制资源文件的数量—您可以有一个、几个或者没有。

使用资源编译器rcc将资源文件编译成 C++ 源文件。这是由 QMake 处理的,就像mocuic一样。只需在项目文件中添加一行RESOURCES += filename .qrc,然后重新构建。

生成的文件被命名为qrc_filename.cpp,因此foo.qrc生成qrc_foo.cpp,它被编译并链接到应用程序中,就像任何其他 C++ 源文件一样。当 Qt 遇到以:开头的文件名时,它会将资源文件中的文件添加到 Qt 使用的虚拟文件树中。

应用图标

到目前为止,你看到的所有应用程序都使用标准的 Qt 图标。相反,您可能希望在应用程序窗口的标题栏中显示您自己的图标。您可以通过用方法setWindowIcon为所有顶层窗口和小部件设置一个窗口图标来做到这一点。例如,在 SDI 和 MDI 应用程序中,在每个主窗口的构造器中添加一个对setWindowIcon( QIcon(":/img/logo.png") )的调用就可以做到这一点。

这个过程确保了正在运行的应用程序的所有窗口都显示正确的图标。如果你想改变应用程序可执行文件的图标,即应用程序图标,你需要区别对待每个平台。


注意您需要辨别应用程序图标和 windows 图标之间的区别。它们可以相同,但不要求必须相同。


窗户

Windows 系统上的可执行文件通常有一个应用程序图标。图标是一个ico文件格式的图像。您可以使用许多免费工具创建ico文件,例如 Gimp ( [www.gimp.org](http://www.gimp.org))或 png2ico ( [www.winterdrache.de/freeware/png2ico/index.html](http://www.winterdrache.de/freeware/png2ico/index.html))。你也可以使用微软的 Visual Studio 来创建ico文件。

创建了一个ico文件后,必须使用下面一行将它放入一个特定于 Windows 的资源文件中:

IDI_ICON1 ICON DISCARDABLE "filename.ico"
该行的文件名部分是图标的文件名。将 Windows 资源文件另存为`filename` `.rc`,其中`filename`是资源文件的名称(可以不同于图标)。最后,在 QMake 项目文件中添加一行代码`RC_FILE =` `filename` `.rc`。
 **Mac OS X** 
在 Mac OS X 系统上,可执行文件通常有一个应用程序图标。图标使用的文件格式是`icns`。您可以使用 Iconverter 等免费工具轻松创建`icns`文件。你也可以使用 OS X 附带的苹果图标编辑器来完成这项任务。
现在你所要做的就是将图标应用到你的可执行文件中,将行`ICON =` `filename` `.icns`添加到你的 QMake 项目文件中。
 **Unix 桌面**
在 Unix 环境中,应用程序的可执行文件没有图标(这个概念在平台上是未知的)。然而,现代的 Unix/Linux 桌面使用由 freedesktop.org 组织指定的桌面入口文件。它看起来很好,也很有结构,但问题是不同的发行版使用不同的文件位置来存储图标。(这个话题在第十五章中有更详细的介绍。)
可停靠的部件
虽然示例 SDI 和 MDI 应用程序只使用了一个文档窗口,但有时显示文档的其他方面也很有用。在其他时候,工具栏过于有限,无法显示您需要提供的工具范围。这就是`QDockWidget`进入画面的地方。
图 4-7 显示了停靠窗口小部件可以出现在中央窗口小部件的周围,但是在工具栏内部。该图显示了可以放置工具栏和 dock 小工具的位置。如果它们不占用空间,中央的小部件会伸展以填充尽可能多的区域。

**4-7** *每个主窗口都有一个中央小部件,周围是可停靠的小部件和工具栏。*

 **注意**顺便问一下,你知道工具栏可以移动和隐藏吗?尝试构建如下所述的应用程序,然后右键单击其中一个工具栏将其隐藏。也试着拖动工具栏的手柄来移动它。

Dock 窗口小部件也可以显示、隐藏和移动,以贴在主窗口的不同部分。此外,它们可以在主窗口外分离和移动。(一个 *dock widget* 是一个放置在`QDockWidget`中的普通 widget。)然后,`QDockWidget`对象被添加到主窗口,一切正常。图 4-8 显示了多种显示停靠的方式:停靠、浮动和选项卡式。

**4-8** *码头可以用许多不同的方式展示。*
使用 SDI 应用程序作为基础,尝试添加一个 dock 小部件。它将通过`QTextEdit::document()`方法监听来自`QTextDocument`的`contentsChange(int, int, int)`信号。文本文档一更改,就会发出信号,告诉您更改发生在哪里,删除了多少字符,添加了多少字符。将创建一个名为`InfoWidget`的新小部件,它监听信号并显示来自最新发射信号的信息。
清单 4-19 显示了`InfoWidget`的类声明。如您所见,小部件基于`QLabel`,由一个构造器和一个插槽组成。

**清单 4-19** `InfoWidget` **
class InfoWidget : public QLabel

{

  Q_OBJECT

public:

  InfoWidget( QWidget *parent=0 );

public slots:

  void documentChanged( int position, int charsRemoved, int charsAdded );

}; 
现在你到达了`InfoWidget`的构造器。源代码如清单 4-20 所示。代码使用`setAlignment(Qt::Alignment)`设置标签来显示水平和垂直居中的文本。如果需要,通过将`wordWrap`属性设置为`true`,确保文本被换行。最后,初始文本被设置为`Ready`。

**清单 4-20***`InfoWidget`**的构造器*```
InfoWidget::InfoWidget( QWidget *parent ) : QLabel( parent )

{

  setAlignment( Qt::AlignCenter );

  setWordWrap( true );

  setText( tr("Ready") );

}
```cpp

`InfoWidget`类有趣的部分是插槽的实现。插槽参数是三个名为`position`、`charsRemoved`和`charsAdded`的整数,与`QTextDocument::contentsChange`信号完全匹配。清单 4-21 中的代码采用`charsRemoved`和`charsAdded`,然后在每次发出信号时为小部件构建一个新的文本。`tr()`方法的`tr(QString,QString,int)`版本用于允许翻译者定义复数形式,这意味着`charsRemoved`和`charsAdded`值用于挑选翻译。它不影响英文版本,因为`"1 removed"`和`"10 removed"`都是有效文本。(对于其他语言,情况并不总是如此。你会在第十章学到更多。)

**清单 4-21** *插槽根据参数更新文本。*

void InfoWidget::documentChanged( int position, int charsRemoved, int charsAdded )

{

QString text;

if( charsRemoved )

text = tr(“%1 removed”, “”, charsRemoved).arg( charsRemoved );

if( charsRemoved && charsAdded )

text += tr(", ");

if( charsAdded )

text += tr(“%1 added”, “”, charsAdded).arg( charsAdded );

setText( text );

}


如果你认为创建`InfoWidget`很简单,你会发现使用它甚至更容易。这些变化影响了`SdiWindow`类,其中添加了一个名为`createDocks()`的新方法(见清单 4-22 )。创建 dock 小部件的步骤是创建一个新的`QDockWidget`,创建您的小部件——`InfoWidget`,并将其放入 dock 小部件中,最后调用`addDockWidget(Qt:: DockWidgetArea, QDockWidget*)`将 dock 小部件添加到主窗口中。将它添加到主窗口时,您还必须指定希望它出现的位置:左侧、右侧、顶部或底部。使用`QDockWidget`的`allowedAreas`属性,您可以控制添加 dock 的位置。这个属性的缺省值是`AllDockWidgetAreas`,它给予用户完全的控制权。

在`createDocks`方法准备好之前,从文本文档到`InfoWidget`的信号被连接。

**清单 4-22** *创建 dock widget* 

void SdiWindow::createDocks()

{

dock = new QDockWidget( tr(“Information”), this );

InfoWidget *info = new InfoWidget( dock );

dock->setWidget( info );

addDockWidget( Qt::LeftDockWidgetArea, dock );

connect( docWidget->document(), SIGNAL(contentsChange(int, int, int)),

info, SLOT(documentChanged(int, int, int)) );

}


这就是启用 dock 小部件的全部内容,但是因为用户可以关闭它,所以您还必须为用户提供一个显示它的方法。这通常在视图菜单中处理(或者可能在工具或窗口菜单中,取决于应用程序)。添加一个视图菜单,并使显示和隐藏 dock 小部件变得非常容易。因为这是一个常见的任务,`QDockWidget`类已经为此提供了`QAction`。该操作可通过`toggleViewAction()`方法获得。对`SdiWindow`的`createMenus`方法需要做的修改如清单 4-23 所示。

**清单 4-23** *为主窗口创建新的视图菜单*

void SdiWindow::createMenus()

{

QMenu *menu;

menu = menuBar()->addMenu( tr(“&File”) );

menu->addAction( newAction );

menu->addAction( closeAction );

menu->addSeparator();

menu->addAction( exitAction );

menu = menuBar()->addMenu( tr(“&Edit”) );

menu->addAction( cutAction );

menu->addAction( copyAction );

menu->addAction( pasteAction );

menu = menuBar()->addMenu( tr(“&View”) );

menu->addAction( dock->toggleViewAction() );

menu = menuBar()->addMenu( tr(“&Help”) );

menu->addAction( aboutAction );

menu->addAction( aboutQtAction );

}


在构建修改后的 SDI 应用程序之前,必须确保将头文件和源文件`InfoWidget`添加到项目文件中。然后运行`qmake`和`make`来构建可执行文件。图 4-9 显示了运行两个文档的应用程序:一个文档有一个浮动信息 dock 另一个文档停靠在主窗口。

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

**4-9** *带有 dock widgets 的 SDI 应用*

### 总结

有些应用程序最好以单个对话框的形式实现,但大多数应用程序都是基于文档的。对于这些应用程序,主窗口是应用程序窗口的最佳基础类,因为它提供了一个沿着工具栏、菜单、状态栏和可停靠部件的文档视图。

使用 Qt 的`QMainWindow`类,你可以在已建立的单文档和多文档界面之间进行选择,也可以“滚动你自己的”自定义界面。你所要做的就是在主窗口中提供一个中心部件。对于 SDI 应用程序,中心小部件是您的文档小部件;对于 MDI 应用程序,它是一个`QWorkspace`小部件,您可以在其中添加文档小部件。

对话框、SDI 应用程序和 MDI 应用程序的开发方法是相同的。设置用户界面,并将用户动作发出的所有有趣信号连接到执行实际工作的插槽。

信号可以来自菜单项、键盘快捷键、工具栏按钮或任何其他可以想到的来源。要管理它,你可以使用`QAction`对象,这使你能够在不同的地方放置相同的动作,并使用一个单一的信号到插槽连接来处理所有的信号源。

当提供工具栏(还有菜单)时,能够给每个动作添加图标是很好的。为了避免将应用程序可执行文件与图标图像文件集合一起发布,可以使用参考资料。通过构建一个基于 XML 的`qrc`文件并在项目文件中添加一行`RESOURCES`,您可以将文件嵌入到您的可执行文件中。在运行时,你可以通过在文件名前加上前缀`:`来访问文件。

使用 Qt 时,为应用程序的可执行文件提供图标是您必须管理的少数依赖于平台的任务之一。对于 Windows 和 Mac OS X,有一些标准化的方法可以将图标添加到可执行文件中;在 Unix 上,您仍然需要将安装包定位到特定的发行版。这里已经做了很多工作,所以我相信很快就会有一个标准的方法。

本章向您展示了通过使用 Qt 中主窗口可用的框架可以做些什么。在本书的后面,您将在应用程序中使用`QMainWindow`类,所以还会有更多内容!* 

五、模型-视图框架

M 模型和视图是在所有类型的软件中频繁出现的设计模式。通过将数据分离到一个模型中,并通过视图将该模型呈现给用户,就创建了一个健壮的、可重用的设计。

模型用于描述图 5-1 所示的结构:列表、表格和树。一个列表是数据的一维向量。一个是一个列表,但是有多个列——一个二维数据结构。一个仅仅是一个表,但是有另一个维度,因为数据可能隐藏在其他数据中。

当您考虑如何构建应用程序时,您会发现这些结构几乎可以在所有情况下使用,因此您可以构建一个以良好方式表示您的数据结构的模型。同样重要的是要记住,您不需要改变实际存储数据的方式,您可以提供一个表示数据的模型类,然后将模型化数据中的每一项映射到应用程序数据结构中的实际项。

所有这些结构都可以用许多不同的方式表现出来。例如,列表可以显示为列表(一次显示所有项目)或组合框(仅显示当前项目)。每个值也可以以不同的方式显示,例如,显示为文本、值甚至图像。这就是视图进入画面的地方——它的任务是向用户显示来自模型的数据。

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

图 5-1。 一个列表,一个表格,一棵树

在经典的模型-视图-控制器(MVC)设计模式中(见图 5-2 ),模型保存数据,视图将数据呈现给显示单元。当用户想要编辑数据时,控制器类处理数据的所有修改。

Qt 以稍微不同的方式处理这种模式。视图没有控制器类,而是通过使用一个委托类来处理数据更新(参见图 5-2 )。委托有两个任务:帮助视图呈现每个值,以及在用户想要编辑值时帮助视图。将经典的 MVC 模式与 Qt 的方法进行比较,你可以说控制器和视图已经合并,但是视图使用委托来处理控制器的部分工作。

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

图 5-2。 MVC 与模型-视图和代理的比较

使用视图显示数据

Qt 提供了三种不同的默认视图:树、列表和表格。在第二章电话簿示例中,您通过QListWidget看到了列表视图。QListWidget类是QListView的特殊版本,但是QListWidget包含列表中显示的数据,而QListView从模型中访问它的数据。QListWidget有时被称为便利类,因为它不太灵活,但与使用QListView和模型相比,在不太复杂的情况下更方便。

就像列表小部件与列表视图相关联一样,QTreeWidget-QTreeViewQTableWidget-QTableView对也相关联。

让我们从一个例子开始,展示如何创建一个模型,填充它,并使用所有三个视图显示它。为了简单起见,它是由一个单独的main函数创建的。

首先要做的是创建小部件。在清单 5-1 中,您可以看到QTreeViewQListViewQTableView被创建并放入一个QSplitter中。一个分割器是一个小部件,它将可移动的条放在其子部件之间。这意味着用户可以自由地在树、列表和表格之间划分空间。你可以在图 5-3 中看到分离器的动作。

清单 5-1。 创建视图并将它们放入分割器

  QTreeView *tree = new QTreeView;

  QListView *list = new QListView;

  QTableView *table = new QTableView;

  QSplitter splitter;

  splitter.addWidget( tree );

  splitter.addWidget( list );

  splitter.addWidget( table );

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

图 5-3。 使用分割器可以调整树、列表和表格的大小。顶部窗口是默认的开始状态,而下面窗口中的拆分条已被移动。

当创建小部件时,您必须创建并填充一个模型。首先使用QStandardItemModel,这是 Qt 附带的标准型号之一。

清单 5-2 展示了模型是如何被填充的。填充过程由三个循环组成:行(r)、列(c)和项(i)。这些循环创建了五行两列,其中第一列有三个子项。

清单 5-2。 创建并填充模型

`  QStandardItemModel model( 5, 2 );
  for( int r=0; r<5; r++ )
    for( int c=0; c<2; c++)
    {
      QStandardItem *item =
        new QStandardItem( QString(“Row:%1, Column:%2”).arg®.arg© );

if( c == 0 )
        for( int i=0; i<3; i++ )
          item->appendRow( new QStandardItem( QString(“Item %1”).arg(i) ) );

model.setItem(r, c, item);
    }`

让我们仔细看看人口是如何构成的。首先,QStandardItemModel被创建,构造器被告知要使它变成 5 行 2 列。然后对行和列运行一对循环,其中为每个位置创建一个QStandardItem。通过使用setItem(int, int, QStandardItem*)方法将该项目放入模型中。对于第一列中的所有项目,其中c等于0,创建了三个新的QStandardItem对象,并使用appendRow(QStandardItem*)方法将其作为子项目。图 5-4 显示了模型在树形视图中的样子。每个列和行位置的项目以表格形式显示。在表中,第二行已经展开,显示了三个子项。

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

图 5-4。 模型以树形视图显示,第二行打开显示子项目

在小示例应用程序显示模型之前,你必须通过使用setModel(QAbstractItemModel*)方法告诉视图使用什么模型,如清单 5-3 所示。

清单 5-3。 为所有视图设置模型

  tree->setModel( &model );

  list->setModel( &model );

  table->setModel( &model );

虽然设置模型是启动和运行的全部要求,但是我想使用选择模型来演示模型之间的差异,所以在继续之前还有一个步骤要执行。

选择模型管理模型中的选择。每个视图都有自己的选择模型,但是可以使用setSelectionModel(QItemSelectionModel*)方法分配一个模型。通过在列表和表格中设置树的模型,如清单 5-4 所示,选择将被共享。这意味着,如果您在一个视图中选择了某个项目,该项目在其他两个视图中也会被选中。

清单 5-4。 分享选择模式

  list->setSelectionModel( tree->selectionModel() );

  table->setSelectionModel( tree->selectionModel() );

将所有这些封装在一个main函数和一个QApplication对象中,就可以得到一个可以用 QMake 构建的工作应用程序。图 5-3 和图 5-4 显示了运行应用。您可以在应用程序中尝试很多东西,这些东西可以让您了解模型和视图在 Qt 中是如何工作的:

  • 尝试在任一视图中一次选取一个项目,并研究该选择在其他视图中的显示位置。请注意,列表仅显示第一列,子项仅影响树视图。
  • 尝试在按住 Ctrl 或 Shift 键的情况下拾取项目(然后尝试同时按住这两个键)。
  • 尝试从每个视图中选取一行。当您选择列表中的一行时,只会选择第一列。
  • 尝试在表中选择列(单击标题),看看在其他视图中会发生什么。确保选择第二列并观察列表视图。
  • 双击任何项目并修改文本。默认情况下,对象是可编辑的。
  • 不要忘记用间隔棒做实验。
提供标题

视图和标准模型是灵活的。您可能不喜欢应用程序中的一些细节,所以让我们开始查看这些细节。您可以从在标题中设置一些描述性文本开始:通过使用setHorizontalHeaderItem(int, QStandardItem*)setVerticalHeaderItem(int, QStandardItem*)QStandardItem s 插入到模型中。清单 5-5 显示了添加到main函数中的行,用于添加水平标题。

清单 5-5。 向标准项目模型添加标题

  model.setHorizontalHeaderItem( 0, new QStandardItem( "Name" ) );

  model.setHorizontalHeaderItem( 1, new QStandardItem( "Phone number" ) );
限制编辑

然后是用户可编辑的项目的问题。editable 属性在项目级别进行控制。通过在树形视图中显示的每个子项上使用setEditable(bool)方法,你可以使它们成为只读的(参见清单 5-6 中的内部循环)。

清单 5-6。 在标准项目模型中创建只读项目

      if( c == 0 )

        for( int i=0; i<3; i++ )

        {

          QStandardItem *child = new QStandardItem( QString("Item %1").arg(i) );

          child->setEditable( false );

          item->appendRow( child );

        }
限制选择行为

有时候,限制选择的方式是有帮助的。例如,您可能希望限制用户一次只能选择一项(或者只能选择整行)。这个限制是由每个视图的selectionBehaviorselectionMode属性控制的。因为它是在视图级别上控制的,所以重要的是要记住,一旦选择模型在两个视图之间共享,两个视图都需要正确设置它们的selectionBehaviorselectionMode属性。

选择行为可以设置为SelectItemsSelectRowsSelectColumns(分别限制选择单个项目、整行或整列)。属性不限制用户可以选择的项数、行数或列数。它由selectionMode属性控制。选择模式可设置为以下值:

  • NoSelection:用户不能在视图中进行选择。
  • SingleSelection:用户可以在视图中选择单个项目、行或列。
  • ContiguousSelection:用户可以在视图中选择多个项目、行或列。选择区域必须是一个整体,彼此相邻,没有任何间隙。
  • ExtendedSelection:用户可以在视图中选择多个项目、行或列。选择区域是独立的,可以有间隙。用户可以通过单击和拖动来选择项目,同时按下 Shift 或 Ctrl 键来选择项目。
  • MultiSelection:相当于ExtendedSelection从程序员的角度来看,选择区域是独立的,可以有间隙。用户通过单击项目来切换所选状态。不需要使用 Shift 或 Ctrl 键。

在清单 5-7 中,表格视图被配置为只允许选择一整行。尝试使用树视图和列表视图选择多个项目和单个项目。

清单 5-7。 改变选择行为

  table->setSelectionBehavior( QAbstractItemView::SelectRows );

  table->setSelectionMode( QAbstractItemView::SingleSelection );
单列列表

对于真正简单的列表,Qt 提供了QStringListModel。因为项目列表通常保存在 Qt 应用程序的QStringList对象中,所以最好有一个采用字符串列表并能与所有视图一起工作的模型。

清单 5-8 展示了如何创建和填充QStringList对象list。创建一个QStringListModel,并用setStringList(const QStringList&)设置列表。最后,在列表视图中使用该列表。

清单 5-8。 使用 QStringListModel 来填充一个 QListView

  QListView list;

  QStringListModel model;

  QStringList strings;

  strings << "Huey" << "Dewey" << "Louie";

  model.setStringList( strings );

  list.setModel( &model );

创建自定义视图

能够通过现有的视图显示模型是有用的,但是有时您需要能够根据自己的需要定制视图。有两种方法:要么从QAbstractItemDelegate类构建一个委托,要么从QAbstractItemView类创建一个完全自定义的视图。

创建代理是最简单的方法,所以从这里开始。Qt 附带的视图都使用代理来绘制和编辑它的项目。通过创建用于绘制行或列(或视图中的所有项目)的委托,您通常可以获得所需的外观。

画画的代表

首先创建一个委托,将整数值显示为条形。在图 5-5 所示的表格视图中可以看到代表的动作。条形的范围从 0 到 100,其中 0 只是一条蓝色的细线,100 是一条完整的绿色条形。如果该值超过 100,条形会变成红色,表示超出范围。

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

图 5-5。BarDelegate类用于将整数值显示为条形。

*因为它是一个显示条形图的委托,所以新类被称为BarDelegate并建立在QAbstractItemDelegate类的基础上。抽象项委托类是所有委托的基类。清单 5-9 中显示了类声明。这段代码可以被认为是所有管理值显示的委托的样板文件,因为覆盖的两种方法在QAbstractItemDelegate基类的文档中都有明确的说明。该方法的用途从其名称就很容易猜到。paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&)方法绘制项目,而sizeHint(const QStyleOptionViewItem&, const QmodelIndex&)指示每个项目想要多大。

清单 5-9。【自定义委托】的类声明

class BarDelegate : public QAbstractItemDelegate

{

public:

  BarDelegate( QObject *parent = 0 );

  void paint( QPainter *painter,

              const QStyleOptionViewItem &option,

              const QModelIndex &index ) const;

  QSize sizeHint( const QStyleOptionViewItem &option,

                  const QModelIndex &index ) const;

};

清单 5-10 中的显示了sizeHint方法。它只是返回一个足够大但不超过大小限制的大小。记住这只是一个提示;实际大小可以由 Qt 针对布局问题进行更改,也可以由用户通过调整行和列的大小来进行更改。

清单 5-10。 返回自定义委托的大小提示

QSize BarDelegate::sizeHint( const QStyleOptionViewItem &option,

                             const QModelIndex &index ) const

{

  return QSize( 45, 15 );

}

sizeHint方法非常简单;paint方法更有趣(见清单 5-11 )。第一个if语句通过测试样式选项的状态来检查该项是否被选中。(样式选项用于控制 Qt 应用程序中所有东西的外观。)负责使 Qt 应用程序看起来像本机应用程序的样式化系统将样式选项对象用于调色板、区域、可视状态以及影响对象在屏幕上的外观的所有其他内容。有许多样式对象类——几乎每个图形元素都有一个。全部继承QStyleOption类。

清单 5-11。 为自定义代理绘制值

void BarDelegate::paint( QPainter *painter,

  const QStyleOptionViewItem &option, const QModelIndex &index ) const

{

  if( option.state & QStyle::State_Selected )

    painter->fillRect( option.rect, option.palette.highlight() );

  int value = index.model()->data( index, Qt::DisplayRole ).toInt();

  double factor = (double)value/100.0;

  painter->save();

  if( factor > 1 )

  {

    painter->setBrush( Qt::red );

    factor = 1;

  }

  else

    painter->setBrush( QColor( 0, (int)(factor*255), 255-(int)(factor*255) ) );

  painter->setPen( Qt::black );

  painter->drawRect( option.rect.x()+2, option.rect.y()+2,

    (int)(factor*(option.rect.width()-5)), option.rect.height()-5 );

  painter->restore();

}

如果样式选项指示该项已被选中,则背景将填充平台的选定背景颜色,该颜色也是从样式选项中获得的。对于绘图,使用QPainter对象和填充给定矩形的fillRect(const QRect&, const QBrush&)方法。

下一行从模型中选取值,并将其转换为整数。代码请求带有索引的DisplayRole值。每个模型项可以有几个不同角色的数据,但是要显示的值有DisplayRole。该值作为一个QVariant返回。variant 数据类型可以保存任何类型的值:字符串、整数、实值、布尔值等等。toInt(bool*)方法试图将当前值转换成整数,这是委托所期望的。

获得该项目的选择状态和值的两行被突出显示。这些线条必须总是以某种形式出现在代理绘制方法中。

模型中的值用于计算一个因子,该因子告诉您该值是 100 的几分之一。该因子用于计算条形的长度和填充条形的颜色。

下一步是保存画师的内部状态,这样就可以更改钢笔颜色和画笔,然后调用restore()让画师保持原样。(QPainter类在第七章中有更详细的讨论。)

if语句检查factor是否超过 1,并负责给用于填充条的画笔着色。如果因子大于 1,条形变为红色;否则,计算颜色时,接近零的因子给出蓝色,接近 1 的因子给出绿色。因为该因子用于控制条的长度,所以如果它太大,该因子被限制为 1,这确保了您不会试图在指定的矩形之外进行绘制。

设置画笔颜色后,在绘制线条之前,使用drawRect(int, int, int, int)方法将钢笔颜色设置为黑色。optionrect成员告诉你物品有多大。最后,画师恢复到方法结束前保存的状态。

为了测试委托,在main函数中创建了一个表视图和一个标准模型。这方面的源代码如清单 5-12 所示。该模型有两列:一个包含字符串的只读行和一个包含整数值的只读行。

在清单末尾突出显示的行中创建和设置了代理。setItemDelegateForColumn(int, QAbstractItemDelegate*)代表被分配到第二列。如果您不想定制一个行,您可以使用setItemDelegateForRow(int, QAbstractItemDelegate*)将一个代表分配给一个行,或者您可以使用setItemDelegate(QAbstractItemDelegate*)将一个代表分配给整个模型。

清单 5-12。 创建并填充一个模型;然后为第二列设置代表

  QTableView table;

  QStandardItemModel model( 10, 2 );

  for( int r=0; r<10; ++r )

  {

    QStandardItem *item = new QStandardItem( QString("Row %1").arg(r+1) );

    item->setEditable( false );

    model.setItem( r, 0, item );

    model.setItem( r, 1, new QStandardItem( QString::number((r*30)%100 )) );

  }

  table.setModel( &model );

  BarDelegate delegate;

  table.setItemDelegateForColumn( 1, &delegate );

产生的应用程序在图 5-5 的中显示运行。问题是用户不能编辑条形后面的值,因为没有编辑器从委托的createEditor方法返回。

自定义编辑

要使用户能够编辑使用自定义委托显示的项,必须扩展委托类。在清单 5-13 中,带有新成员的行被突出显示。它们都与为模型项提供编辑小部件有关。根据下面的列表,每种方法都有一个要处理的任务:

  • createEditor(...):创建一个编辑器小部件,并将 delegate 类用作事件过滤器
  • setEditorData(...):用来自给定模型项的数据初始化编辑器小部件
  • setModelData(...):将模型项的值设置为编辑器小部件中的值
  • updateEditorGeometry(...):更新几何图形(即位置和大小)或编辑小工具

清单 5-13。 支持自定义编辑小工具的自定义代理

class BarDelegate : public QAbstractItemDelegate

{

public:

  BarDelegate( QObject *parent = 0 );

  void paint( QPainter *painter,

              const QStyleOptionViewItem &option,

              const QModelIndex &index ) const;

  QSize sizeHint( const QStyleOptionViewItem &option,

                  const QModelIndex &index ) const;

  QWidget *createEditor( QWidget *parent,

                         const QStyleOptionViewItem &option,

                         const QModelIndex &index ) const;

  void setEditorData( QWidget *editor,

                      const QModelIndex &index ) const;

  void setModelData( QWidget *editor,

                     QAbstractItemModel *model,

                     const QModelIndex &index ) const;

  void updateEditorGeometry( QWidget *editor,

                             const QStyleOptionViewItem &option,

                             const QModelIndex &index ) const;

};

因为该值显示为水平增长的条形,所以使用了在水平方向移动的滑块作为编辑器。这意味着滑块的水平位置将对应于条的水平范围,如图图 5-6 所示。

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

图 5-6。 自定义委托将值显示为一个条,并使用一个自定义编辑小部件:滑块来编辑值。

让我们看看清单 5-14 中显示的createEditorupdateEditorGeometry方法。更新几何图形的成员非常简单——它只需要通过option获得rect,并相应地设置editor的几何图形。

清单 5-14。 创建自定义编辑小工具并调整其大小

QWidget *BarDelegate::createEditor( QWidget *parent,

  const QStyleOptionViewItem &option, const QModelIndex &index ) const

{

  QSlider *slider = new QSlider( parent );

  slider->setAutoFillBackground( true );

  slider->setOrientation( Qt::Horizontal );

  slider->setRange( 0, 100 );

  slider->installEventFilter( const_cast<BarDelegate*>(this) );

  return slider;

}

void BarDelegate::updateEditorGeometry( QWidget *editor,

  const QStyleOptionViewItem &option, const QModelIndex &index ) const

{

  editor->setGeometry( option.rect );

}


提示使用setGeometry(const QRect&)方法来设置小部件的位置和大小似乎是个好主意,但是在 99%的情况下,布局是更好的选择。此处使用它是因为显示模型项目的区域是已知的,并且如果使用了布局,它是从布局直接或间接确定的。


创建编辑器的方法包含的代码稍微多一点,但是并不复杂。首先,设置一个QSlider来绘制背景,以便模型项的值被小部件覆盖。然后在委托类作为事件过滤器安装之前设置方向和范围。事件过滤功能包含在基类QAbstractItemDelegate中。


注意 事件过滤是一种在事件到达小部件之前查看发送到小部件的事件的方法。这将在第六章的中详细讨论。


在编辑小部件为用户准备好之前,它必须从模型中获取当前值。这是setEditorData方法的责任。清单 5-15 中的方法从模型中获取值。使用toInt(bool*)将该值转换为整数,因此非数字值将被转换为零值。最后,使用setValue(int)方法设置编辑器小部件的值。

清单 5-15。 根据模型值初始化编辑器控件

void BarDelegate::setEditorData( QWidget *editor, const QModelIndex &index ) const {   int value = index.model()->data( index, Qt::DisplayRole ).toInt();   static_cast<QSlider*>( editor )->setValue( value ); }

编辑器小部件可以正确地创建、放置和调整大小,然后用当前值进行初始化。然后,用户可以以有意义的方式编辑该值,但是新值无法到达模型。这是setModelData(QWidget*, QAbstractItemModel*, const QModelIndex&)处理的任务。你可以在清单 5-16 中看到这个方法。代码相当简单,即使由于强制转换而有点模糊。发生的情况是,来自编辑器小部件的值被获取并在一个setData(const QModelIndex&, const QVariant&, int)调用中使用。受影响的模型索引index被作为参数传递给setModelData方法,因此没有真正的障碍。

清单 5-16。 从编辑器小部件获取值并更新模型

void BarDelegate::setModelData( QWidget *editor,

  QAbstractItemModel *model, const QModelIndex &index ) const

{

  model->setData( index, static_cast<QSlider*>( editor )->value() );

}

生成的应用程序将值显示为条形,并允许用户使用滑块编辑它们。(运行应用参见图 5-6 。)

创造自己的观点

当您觉得通过使用可用的视图、委托或任何其他技巧无法到达您想要的位置时,您将面临一种情况,您必须实现自己的视图。

图 5-7 显示了一个表格和一个显示所选项目的自定义视图。自定义视图一次显示一个项目(如果一次选择多个项目,则显示一段解释文字)。它基于一个QAbstractItemView并使用一个QLabel来显示文本。

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

图 5-7。 行动中的自定义视图

当实现一个定制视图时,你必须提供一大堆方法的实现。有些方法很重要;其他的只是提供一个有效的返回值。哪些方法需要复杂的实现很大程度上取决于您正在实现的视图类型。

在清单 5-17 中,你可以看到自定义视图SingleItemView的类声明。除了updateText()之外的所有方法都是必需的,因为它们在QAbstractItemView中被声明为纯抽象方法。


提示纯抽象方法是在基类声明中设置为零的虚方法。这意味着该方法没有实现,并且该类不能被实例化。为了能够创建继承基类的类的对象,必须实现方法,因为必须实现所有对象的所有方法。


类声明中的方法告诉您视图的职责:显示模型的视图,对模型中的变化做出反应,以及对用户动作做出反应。

清单 5-17。 包含所有必需成员的自定义视图

class SingleItemView : public QAbstractItemView

{

  Q_OBJECT

public:

  SingleItemView( QWidget *parent = 0 );

  QModelIndex indexAt( const QPoint &point ) const;

  void scrollTo( const QModelIndex &index, ScrollHint hint = EnsureVisible );

  QRect visualRect( const QModelIndex &index ) const;

protected:

  int horizontalOffset() const;

  bool isIndexHidden( const QModelIndex &index ) const;

  QModelIndex moveCursor( CursorAction cursorAction,

                          Qt::KeyboardModifiers modifiers );

  void setSelection( const QRect &rect, QItemSelectionModel::SelectionFlags flags );

  int verticalOffset() const;

  QRegion visualRegionForSelection( const QItemSelection &selection ) const;

protected slots:

  void dataChanged( const QModelIndex &topLeft, const QModelIndex &bottomRight );

  void selectionChanged( const QItemSelection &selected,

                         const QItemSelection &deselected );

private:

  void updateText();

  QLabel *label;

};

SingleViewItem的构造器在QAbstractItemView小部件的视图部分设置了一个QLabelQAbstractItemView类继承了QAbstractScrollArea,用于创建可能需要滚动条的小部件。可滚动区域内部是查看端口小部件。

清单 5-18 中显示的构造器的源代码展示了如何让标签填充视口。首先,为视口创建布局,然后将标签添加到布局中。为了确保标签填满可用区域,其大小策略设置为向所有方向扩展。最后,标签被配置为在设置标准文本之前在可用区域的中间显示文本。

清单 5-18。 在自定义视图的视口中设置标签

SingleItemView::SingleItemView( QWidget *parent ) : QAbstractItemView( parent )

{

  QGridLayout *layout = new QGridLayout( this->viewport() );

  label = new QLabel();

  layout->addWidget( label, 0, 0 );

  label->setAlignment( Qt::AlignCenter );

  label->setSizePolicy(

    QSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding ) );

  label->setText( tr("<i>No data.</i>") );

}

在构造器中,设置一个标准文本;在updateText方法中,设置实际的文本。清单 5-19 显示了该方法的实现。它通过查看从选择模型的selection方法中获得的QModelIndex个对象的数量来工作。selection方法返回模型中所有选中项目的索引。如果选择的项目数为零,文本被设置为No data。当选择一个项目时,将显示该项目的值。否则,意味着不止一个选择的项目,显示文本通知用户只能显示一个项目。

通过模型的data方法和currentIndex方法检索所选项的值。只要至少选择了一项,这些方法的组合将从当前项返回值。

清单 5-19。 更新标签的文本

void SingleItemView::updateText()

{

  switch( selectionModel()->selection().indexes().count() )

  {

    case 0:

      label->setText( tr("<i>No data.</i>") );

      break;

    case 1:

      label->setText( model()->data( currentIndex() ).toString() );

      break;

    default:

      label->setText( tr("<i>Too many items selected.<br>"

                         "Can only show one item at a time.</i>") );

      break;

  }

}

因为视图的大部分工作是显示项目,所以视图需要有方法来告诉什么是可见的以及在哪里。因为视图只显示了一个项目,所以您只能面对全有或全无的情况。清单 5-20 中的所示的方法visualRect,返回一个包含给定模型索引的矩形。该方法只是检查它是否是可见项,如果是,则返回整个视图的区域;否则,返回一个空矩形。

有更多的方法以同样的方式工作:visualRegionForSelectionisIndexHiddenindexAt。所有这些方法都检查给定的模型索引是否是显示的那个,然后相应地返回。

清单 5-20。 确定什么是可见的,什么是不可见的

QRect SingleItemView::visualRect( const QModelIndex &index ) const

{

  if( selectionModel()->selection().indexes().count() != 1 )

    return QRect();

  if( currentIndex() != index )

    return QRect();

  return rect();

}

一些方法的目的是返回有效值来维护一个预定义的接口,这是清单 5-21 中显示的方法的工作。因为滚动条没有被使用,并且一次只显示一个项目,所以这些方法尽可能接近于空的。

清单 5-21。 返回有效响应而不采取行动

int SingleItemView::horizontalOffset() const

{

  return horizontalScrollBar()->value();

}

int SingleItemView::verticalOffset() const

{

  return verticalScrollBar()->value();

}

QModelIndex SingleItemView::moveCursor( CursorAction cursorAction,

                                        Qt::KeyboardModifiers modifiers )

{

  return currentIndex();

}

void SingleItemView::setSelection( const QRect &rect,

                                   QItemSelectionModel::SelectionFlags flags )

{

  // do nothing

}

void SingleItemView::scrollTo( const QModelIndex &index, ScrollHint hint )

{

  // cannot scroll

}

对变化做出反应

视图的最后一个任务是对模型中的变化和用户动作做出反应(例如,通过改变选择)。方法dataChangedselectionChanged通过使用updateText更新显示的文本来对这些事件做出反应。你可以在清单 5-22 中看到这两种方法的实现。

清单 5-22。 对模型和选择的变化做出反应

void SingleItemView::dataChanged( const QModelIndex &topLeft,

                                  const QModelIndex &bottomRight )

{

  updateText();

}

void SingleItemView::selectionChanged( const QItemSelection &selected,

                                       const QItemSelection &deselected )

{

  updateText();

}

使用定制视图就像使用 Qt 附带的视图一样简单。清单 5-23 展示了它的样子(填充模型被省略了)。使用一对嵌套的for回路来使用和填充QStandardItemModel。如您所见,使用视图和共享选择模型非常容易。(应用见图 5-7 。)

清单 5-23。 利用单项视图结合表格视图

int main( int argc, char **argv )

{

  QApplication app( argc, argv );

  QTableView *table = new QTableView;

  SingleItemView *selectionView = new SingleItemView;

  QSplitter splitter;

  splitter.addWidget( table );

  splitter.addWidget( selectionView );

...

  table->setModel( &model );

  selectionView->setModel( &model );

  selectionView->setSelectionModel( table->selectionModel() );

  splitter.show();

  return app.exec();

}

创建定制模型

到目前为止,您一直在查看自定义视图和代理。模型都是QStandardItemModelQStringListModel的,所以模型-视图架构的一个要点被忽略了:定制模型。

通过提供您自己的模型,您可以将应用程序的数据结构转换成一个模型,该模型可以显示为表格、列表、树或任何其他视图。通过让模型转换您现有的数据,您不必保留数据集——一个用于应用程序的内部,一个用于显示。这带来了另一个好处:您不必确保这两个集合是同步的。

定制模型有四种方法:

  • 您可以将应用程序的数据保存在模型中,并通过视图使用的模型预定义的类接口来访问它。
  • 您可以将应用程序的数据保存在模型中,并通过视图使用的预定义接口旁边实现的自定义类接口来访问它。
  • 您可以将应用程序的数据保存在外部对象中,并让模型充当您的数据和视图所需的类接口之间的包装器。
  • 您可以动态地为模型生成数据,并通过视图使用的类接口提供结果。

本节讨论表格和树,以及只读和可编辑模型。所有模型都使用不同的方法来保存和向视图提供数据;所有视图都可以与标准视图以及您使用的任何自定义视图一起使用。

只读表格模型

首先,您将看到一个动态生成数据的只读表模型。名为MulModel的模型类显示了乘法表的可配置部分。类别声明如清单 5-24 所示。

该类基于QAbstractTableModel,在创建二维模型时,这是一个很好的开始类。所有的模型实际上都基于QAbstractItemModel类,但是抽象表模型类为一些需要的方法提供了存根实现。MulModel类的方法各有特殊的责任:

  • flags:告诉视图可以对每个项目做什么(是否可以编辑、选择等等)
  • data:给视图返回给定角色的数据
  • headerData:将表头数据返回给视图
  • rowCountcolumnCount:将模型的尺寸返回视图

清单 5-24。 自定义模型类声明

class MulModel : public QAbstractTableModel

{

public:

  MulModel( int rows, int columns, QObject *parent = 0 );

  Qt::ItemFlags flags( const QModelIndex &index ) const;

  QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const;

  QVariant headerData( int section, Qt::Orientation orientation,

                       int role = Qt::DisplayRole ) const;

  int rowCount( const QModelIndex &parent = QModelIndex() ) const;

  int columnCount( const QModelIndex &parent = QModelIndex() ) const;

private:

  int m_rows, m_columns;

};

构造器只是记住要显示的行数和列数,然后将父类传递给基类构造器。rowCountcolumnCount方法和构造器一样简单,因为它们只是返回给构造器的维度。你可以在清单 5-25 中看到这些方法。

清单 5-25。 构造器、 rowCount columnCount 方法

MulModel::MulModel( int rows, int columns, QObject *parent ) :

  QAbstractTableModel( parent )

{

  m_rows = rows;

  m_columns = columns;

}

int MulModel::rowCount( const QModelIndex &parent ) const

{

  return m_rows;

}

int MulModel::columnCount( const QModelIndex &parent ) const

{

  return m_columns;

}

data方法返回给定角色的数据。数据总是以QVariant的形式返回,这意味着它可以被转换成图标、大小、文本和值。角色定义了数据的用途,如下表所示:

  • Qt::DisplayRole:要显示的数据(文本)
  • Qt::DecorationRole:用于装饰物品的数据(图标)
  • Qt::EditRole:可用于编辑器的格式的数据
  • Qt::ToolTipRole:显示为工具提示的数据(文本)
  • Qt::StatusTipRole:显示为状态栏信息的数据(文本)
  • Qt::WhatsThisRole:这是什么中要显示的数据?信息
  • Qt::SizeHintRole:视图的尺寸提示

MulModeldata方法支持DisplayRoleToolTipRole。显示角色是当前乘法的值;显示的工具提示是乘法表达式本身。该方法的源代码如清单 5-26 所示。

清单 5-26。 从自定义模型中提供数据

QVariant MulModel::data( const QModelIndex &index, int role ) const

{

  switch( role )

  {

  case Qt::DisplayRole:

    return (index.row()+1) * (index.column()+1);

  case Qt::ToolTipRole:

    return QString( "%1 × %2" ).arg( index.row()+1 ).arg( index.column()+1 );

  default:

    return QVariant();

  }

}

为不同的角色返回标题数据,就像为实际项目数据返回一样。当返回头数据时,注意方向通常是很重要的(即,请求的信息是针对Horizontal还是Vertical头)。因为它与乘法表无关,所以清单 5-27 所示的方法非常简单。

清单 5-27。 为自定义模型提供表头

QVariant MulModel::headerData( int section,

                                Qt::Orientation orientation, int role ) const

{

  if( role != Qt::DisplayRole )

    return QVariant();

  return section+1;

}

最后,flags返回的标志用于控制用户可以对项目做什么。清单 5-28 中显示的方法告诉视图所有的项目都可以被选择和启用。还有更多可用的标志。请参考以下列表进行快速概述:

  • Qt::ItemIsSelectable:可以选择项目。
  • Qt::ItemIsEditable:该项目可以编辑。
  • Qt::ItemIsDragEnabled:可以从模型中拖动项目。
  • Qt::ItemIsDropEnabled:可以将数据拖放到项目上。
  • Qt::ItemIsUserCheckable:用户可以勾选和取消勾选该项。
  • Qt::ItemIsEnabled:该项被启用。
  • Qt::ItemIsTristate:项目在树形状态之间循环。

清单 5-28。 用于控制用户可以对模型项目做什么的标志

Qt::ItemFlags MulModel::flags( const QModelIndex &index ) const

{

  if(!index.isValid())

    return Qt::ItemIsEnabled;

  return Qt::ItemIsSelectable | Qt::ItemIsEnabled;

}

这是模型需要的所有方法。在继续之前,看一下图 5-8 ,它显示了显示工具提示的MulModel。使用带有QTableViewMulModel的代码如清单 5-29 所示。

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

图 5-8。 MulModel 类与 QTableView类配合使用

清单 5-29。 使用带有表格视图的自定义模型

int main( int argc, char **argv )

{

  QApplication app( argc, argv );

  MulModel model( 12, 12 );

  QTableView table;

  table.setModel( &model );

  table.show();

  return app.exec();

}
属于你自己的一棵树

尽管创建一个二维表并不困难,但是创建树模型稍微复杂一些。要理解表格和树的区别,请看一下图 5-9 ,它显示了 Qt 中的一棵树。

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

图 5-9。 树实际上是一个表格,其中每个单元格可以包含更多的表格。

让树模型工作的诀窍是将树结构映射到模型的索引。这使得可以返回每个索引的数据以及每个索引可用的行数和列数(即每个索引可用的子项数)。

我选择将模型建立在所有 Qt 应用程序中都可用的树结构上:QObject所有权树。每个QObject都有一个父节点,并且可以有子节点,这就构建了一个模型将要表示的树。


注意这里展示的模型显示了一个QObject树的快照。如果通过添加或删除对象来修改树,模型将失去同步,并且必须重置。


将要实施的应用程序在图 5-10 中显示。

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

图 5-10。 树形模型通过 QTreeView显示 QObjects

*让我们先来看看类声明(参见清单 5-30 )。该类名为ObjectTreeModel,基于QAbstractItemModel。清单中突出显示的行显示了与MulModel相比增加的方法。

清单 5-30。 树模型的类声明

class ObjectTreeModel : public QAbstractItemModel

{

public:

  ObjectTreeModel( QObject *root, QObject *parent = 0 );

  Qt::ItemFlags flags( const QModelIndex &index ) const;

  QVariant data( const QModelIndex &index, int role ) const;

  QVariant headerData( int section, Qt::Orientation orientation,

                       int role = Qt::DisplayRole ) const;

  int rowCount( const QModelIndex &parent = QModelIndex() ) const;

  int columnCount( const QModelIndex &parent = QModelIndex() ) const;

  QModelIndex index( int row, int column,

                     const QModelIndex &parent = QModelIndex() ) const;

  QModelIndex parent( const QModelIndex &index ) const;

private:

  QObject *m_root;

};

构造器和MulModel类一样简单。它不是记住乘法表的维数,而是存储一个指向根QObject的指针作为m_root

在清单 5-31 中显示的headerData方法比MulModel方法稍微复杂一些,因为它只返回水平标题。从方法中可以看出,所有树节点都有两列:一列用于对象名,一列用于类名。

清单 5-31。 树形模型的表头功能

QVariant ObjectTreeModel::headerData(int section,

                                     Qt::Orientation orientation, int role ) const

{

  if( role != Qt::DisplayRole || orientation != Qt::Horizontal )

    return QVariant();

  switch( section )

  {

  case 0:

    return QString( "Object" );

  case 1:

    return QString( "Class" );

  default:

    return QVariant();

  }

}

如果您将index方法与ObjectTreeModel类和MulModel类进行比较,您可以看到一些真正的差异,这是意料之中的,因为数据以不同的方式表示(并且索引也不同)。在MulModel中,您不必提供一个index方法,因为QAbstractTableModel已经为您实现了它。

ObjectTreeModel class’ index方法接受一个模型索引、parent、一列和一行;它在树的表中给出一个位置。索引到实际树的映射是通过模型索引的internalPointer()方法来处理的。这个方法使得在每个索引中存储一个指针成为可能,并且你可以存储一个指向被索引的QObject的指针。

如果索引是有效的,您可以获得适当的QObject,对于它,您希望每个子元素对应一行。这意味着通过使用row作为从children()返回的数组的索引,您可以构建一个指向新的QObject的指针,您可以用它来构建一个新的索引。使用QAbstractItemModel中可用的createIndex方法构建索引(参见清单 5-32 )。

index方法中,做了一个假设。如果视图请求一个无效的索引,它将获得树的根,这为视图提供了一个开始的方法。

清单 5-32。 老黄牛——把 QObject s 变成指标

QModelIndex ObjectTreeModel::index(int row, int column,

                                   const QModelIndex &parent ) const

{

  QObject *parentObject;

  if( !parent.isValid() )

    parentObject = m_root;

  else

    parentObject = static_cast<QObject*>( parent.internalPointer() );

  if( row >= 0 && row < parentObject->children().count() )

    return createIndex( row, column, parentObject->children().at( row ) );

  else

    return QModelIndex();

}

给定index方法,返回可用行数和列数的方法(如清单 5-33 所示)很容易实现。总是有两列,行数简单地对应于children数组的大小。

清单 5-33。 计算行数,返回 2 为列数

int ObjectTreeModel::rowCount(const QModelIndex &parent ) const

{

  QObject *parentObject;

  if( !parent.isValid() )

    parentObject = m_root;

  else

    parentObject = static_cast<QObject*>( parent.internalPointer() );

  return parentObject->children().count();

}

int ObjectTreeModel::columnCount(const QModelIndex &parent ) const

{

  return 2;

}

获取数据几乎和计算行数一样简单。第一列的对象名可以通过objectName属性获得,而您必须通过QMetaObject来获得第二列的类名。你还必须确保只为DisplayRole归还。清单 5-34 的中省略了ToolTipRole,但是您可以看到DisplayRole数据是如何被检索的。

清单 5-34。 返回每个指标的实际数据

QVariant ObjectTreeModel::data( const QModelIndex &index, int role) const

{

  if( !index.isValid() )

    return QVariant();

  if( role == Qt::DisplayRole )

  {

    switch( index.column() )

    {

    case 0:

      return static_cast<QObject*>( index.internalPointer() )->objectName();

    case 1:

      return static_cast<QObject*>( index.internalPointer() )->

        metaObject()->className();

    default:

      break;

    }

  }

  else if( role == Qt::ToolTipRole )

  {

...

  }

  return QVariant();

}

最后一个方法的实现稍微复杂一些:父方法(见清单 5-35 )返回一个给定索引的父索引。很容易找到从索引中获得的QObject的父级,但是还需要获得该父级的行号。

解决方案是,如果父对象不是根对象,它也必须有一个祖父对象。对祖父级的children数组使用indexOf方法,可以得到父级的行。知道自己孩子的顺序很重要!

清单 5-35。 为父节点构建索引需要向祖父节点请求 indexOf 方法。

QModelIndex ObjectTreeModel::parent(const QModelIndex &index) const

{

  if( !index.isValid() )

    return QModelIndex();

  QObject *indexObject = static_cast<QObject*>( index.internalPointer() );

  QObject *parentObject = indexObject->parent();

  if( parentObject == m_root )

    return QModelIndex();

  QObject *grandParentObject = parentObject->parent();

  return createIndex( grandParentObject->children().indexOf( parentObject ),

                      0, parentObject );

}

要尝试全新的ObjectTreeModel,你可以使用清单 5-36 中的main函数。main函数的最大部分用于构建一棵QObjects树。创建一个带有指向根对象的指针的模型并将其传递给视图只需要四行代码(包括创建和显示视图)。运行应用如图 5-10 中的所示。

清单 5-36。 构建 QObjects 的树,然后使用自定义树模型显示

int main( int argc, char **argv )

{

  QApplication app( argc, argv );

  QObject root;

  root.setObjectName( "root" );

  QObject *child;

  QObject *foo = new QObject( &root );

  foo->setObjectName( "foo" );

  child = new QObject( foo );

  child->setObjectName( "Mark" );

  child = new QObject( foo );

  child->setObjectName( "Bob" );

  child = new QObject( foo );

  child->setObjectName( "Kent" );

  QObject *bar = new QObject( &root );

  bar->setObjectName( "bar" );

...

  ObjectTreeModel model( &root );

  QTreeView tree;

  tree.setModel( &model );

  tree.show();

  return app.exec();

}
编辑模型

之前的两个模型——一个二维数组和一棵树——显示了复杂的结构,但它们是只读的。这里显示的IntModel非常简单——只是一个整数列表——但是可以编辑。

清单 5-37 显示了IntModel的类声明,它基于最简单的抽象模型库:QAbstractListModel(这意味着正在创建一个一维列表)。

这个类的方法比MulModelObjectTreeModel少。唯一的新闻是用于使模型可写的setData方法。

清单 5-37。IntModel的方法比 MulModel 少,但是 MulModel 没有 setData

class IntModel : public QAbstractListModel

{

public:

  IntModel( int count, QObject *parent = 0 );

  Qt::ItemFlags flags( const QModelIndex &index ) const;

  QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const;

  int rowCount( const QModelIndex &parent = QModelIndex() ) const;

  bool setData( const QModelIndex &index, const QVariant &value,

                int role = Qt::EditRole );

private:

  QList<int> m_values;

};

因为IntModel是一个非常简单的模型,所以它也有许多简单的方法。首先,如清单 5-38 所示的构造器用通过count指定的值的数量初始化列表。

清单 5-38。 易如一、二、三…构造器只是填充列表。

IntModel::IntModel( int count, QObject *parent )

{

  for( int i=0; i<count; ++i )

    m_values << i+1;

}

行数等于m_values列表的count属性。这意味着rowCount就像清单 5-39 一样简单。

清单 5-39。 行数是列表中项目的数量。

int IntModel::rowCount( const QModelIndex &parent ) const

{

  return m_values.count();

}

返回每个索引的数据也很容易(见清单 5-40);你可以使用indexrows属性在m_values列表中查找正确的值。返回与EditRole相同的DisplayRoleQVariantEditRole代表用于初始化编辑器的值。如果忽略它,用户每次都必须从一个空的编辑器开始。

清单 5-40。 返回值就像在列表中查找一样简单。

QVariant IntModel::data( const QModelIndex &index, int role ) const

{

  if( role != Qt::DisplayRole || role != Qt::EditRole )

    return QVariant();

  if( index.column() == 0 && index.row() < m_values.count() )

    return m_values.at( index.row() );

  else

    return QVariant();

}

要使一个项目可编辑,返回标志值ItemIsEditableItemIsSelectable是很重要的。通过返回ItemIsEnabled,这个项目看起来也是活动的。flag方法如清单 5-41 所示。

清单 5-41。 标记可编辑性、可选择性和被启用

Qt::ItemFlags IntModel::flags( const QModelIndex &index ) const

{

  if(!index.isValid())

    return Qt::ItemIsEnabled;

  return Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled;

}

清单 5-42 显示了setData方法,这是整个IntModel类中最复杂的方法,仍然适合七行代码。它首先检查给定的索引是否有效,角色是否是EditRole。(EditRole是适合编辑的格式的数据,是用户编辑一个值后从视图中得到的。)

在您确定索引和角色都很好之后,您必须确保已经发生了实际的变化。如果值没有发生变化(或者如果索引或角色无效),则返回false,表示没有发生变化。

当实际变化发生时,模型的值被更新,并且在返回true之前发出dataChanged信号。不要忘记发出信号并返回正确的值;否则,模型和视图之间的交互将会失败。

清单 5-42。 根据编辑动作更新模型

bool IntModel::setData( const QModelIndex &index, const QVariant &value, int role )

{

  if( role != Qt::EditRole ||

      index.column() != 0 ||

      index.row() >= m_values.count() )

    return false;

  if( value.toInt() == m_values.at( index.row() ) )

    return false;

  m_values[ index.row() ] = value.toInt();

  emit dataChanged( index, index );

  return true;

}

清单 5-43 和图 5-11 显示了使用中的IntModel。可编辑的模型不会以任何方式影响main功能。这是模型和视图同意使用模型的flag方法的返回值。

清单 5-43。 IntModel QListView

int main( int argc, char **argv )

{

  QApplication app( argc, argv );

  IntModel model( 25 );

  QListView list;

  list.setModel( &model );

  list.show();

  return app.exec();

}

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

**图 5-11。**T3T0正在编辑

排序和过滤模型

来自模型的数据通常是未排序的,但是您可以通过实现模型的sort方法来启用排序。如果您使用一个树形视图或者表格视图来显示您的模型,您可以通过将属性sortingEnabled设置为true来允许用户点击标题进行排序。

只要您使用QStandardItemModel模型并坚持使用QVariant处理的类型,排序马上就能工作。但是,您肯定会遇到不希望更改模型来执行排序的情况。这就是代理模型的用武之地。

一个代理模型是一个将另一个类包装在其自身中,转换它,并取代它的模型。包装后的模型通常被称为源模型。在代理模型上执行的所有操作都被转发到源模型,并且源模型中的所有更改都被传播到代理模型。要实现一个代理模型,从QAbstractProxyModel类开始(如果您想要排序或过滤一个模型,使用QSortFilterProxyModel类)。

首先,让我们通过代理模型提供自定义排序。在你开始实现代理模型之前,你可能想看看清单 5-44 中的main函数。main功能显示代理模型sorter被插入到源模型(model和视图(table)之间。通过使用setSourceModel(QAbstractItemModel*)方法将源模型分配给代理模型。然后代理被用作视图中的模型,而不是直接使用源。

清单 5-44。 源模型被分配给代理模型,然后被视图使用,而不是直接使用源模型。

`int main( int argc, char **argv )
{
  QApplication app( argc, argv );

QStringListModel model;
  QStringList list;
  list << “Totte” << “Alfons” << “Laban” << “Bamse” << “Skalman”;
  model.setStringList( list );

SortOnSecondModel sorter;
  sorter.setSourceModel( &model );

QTableView table;
  table.setModel( &sorter );
  table.setSortingEnabled( true );
  table.show();

return app.exec();
}`

如果您想通过继承QSortFilterProxyModel的类提供自定义排序,您需要覆盖lessThan(const QModelIndex&, const QModelIndex&)方法。代理类本身非常简单——它只需要一个构造器和一个覆盖方法。示例排序代理模型在按字母顺序对字符串进行排序之前会忽略字符串的第一个字母。这个类叫做SortOnSecondModel,声明如清单 5-45 所示。

清单 5-45。 自定义排序代理模型的类声明

class SortOnSecondModel : public QSortFilterProxyModel

{

public:

  SortOnSecondModel( QObject *parent = 0 );

protected:

  bool lessThan( const QModelIndex &left, const QModelIndex &right ) const;

};

SortOnSecondModel的构造器很简单;它只是将父对象传递给基类的构造器。该类的代码包含在清单 5-46 所示的lessThan方法中。

清单 5-46。lessThan方法在比较字符串之前会忽略它们的第一个字符。

bool SortOnSecondModel::lessThan( const QModelIndex &left,

                                  const QModelIndex &right ) const

{

  QString leftString = sourceModel()->data( left ).toString();

  QString rightString = sourceModel()->data( right ).toString();

  if( !leftString.isEmpty() )

    leftString = leftString.mid( 1 );

  if( !rightString.isEmpty() )

    rightString = rightstring.mid( 1 );

  return leftString < rightString;

}

在该方法中,您使用sourceModel()方法获取对源模型的引用,并从中获取实际数据进行比较。在比较字符串之前,从左右字符串中截取第一个字母(如果有)。图 5-12 显示了应用程序运行时,源模型按照代理模型的排序顺序进行排序。

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

图 5-12。 自定义排序代理模型在行动

当模型的数据改变时,排序不会自动更新,但是可以通过将代理模型的dynamicSortFilter属性设置为true来改变。在使用这种方法之前,请确保您的模型足够小,以便在它再次发生变化之前有时间进行排序。

之前的应用只使用了QSortFilterProxyModel的排序功能。如果您需要过滤一个模型以省去几行,您可以重新实现filterAcceptsRow方法。使用filterAcceptsColumn对列进行过滤。这些方法接受源索引和行(或列),如果要显示行(或列),则返回布尔值 true。

总结

使用模型和视图似乎是一种过于复杂的做事方式,但是最终的软件是用一种已经被证明是灵活和强大的结构构建的。

当您处理需要以多种方式显示相同数据的情况时,应该考虑使用模型-视图方法;处理常见的选择;或者只显示列表、树或数据表。

使用带有自定义代理和模型的标准视图通常是比提供完全自定义的小部件更好的解决方案。****

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值