使用 Qt 快速构建用户界面

作者 Johan Thelin

由于诺基亚收购了 Trolltech,Qt 在移动设备领域的能力受到了广泛关注。这不仅意味着速度优化和对更多平台(如 Symbian(以及 Android,如果你关注社区的努力))的支持,而且还意味着诺基亚人所说的设备用户界面受到了相当多的关注。

设备用户界面基本上是一种外观和感觉,可以很好地与所使用的设备集成。它还提供了现代消费者所期望的:流畅的过渡、图形效果和精致的外观。这导致了从基于小部件的用户界面到基于场景的用户界面的转变。

Qt 仍然支持小部件,而且许多(如果不是大多数)应用程序仍然使用它们。事实上,新的用户界面在一个特定的小部件 QGraphicsView 中运行。QGraphicsView 反过来显示一个 QGraphicsScene,其中包含 QGraphicsItem 实例。所有这些都由 Qt Quick 管理。

Qt Quick 概念由两部分组成。第一部分是 QML 语言,用于构建 Qt Quick 用户界面。另一部分是 QtDeclarative 模块,它提供了执行 QML 组件并将它们与 C++ 代码集成的手段。

开发 QML 的原因是使用 C++ 创建流畅的用户界面变得越来越复杂。通过专门为完成该任务而设计一种语言,所需的工作量大大减少。这样做的方式是,Qt 和 C++ 仍然可以用于它们的优点,通过使用 QML 实现用户界面,并使用 C++ 实现业务逻辑和需要性能的部分。作为副作用,用户界面代码和应用程序其余部分之间一直需要的拆分得以强制执行,因为这些部分是使用不同的语言实现的。

为了理解如何使用 Qt Quick,让我们看看三个方面。首先,是 QML 概述,然后是 QML 如何用于构建动态用户界面,最后是 QML 和 C++ 如何协同工作。

QML 简介

QML 是一种声明性语言,基于 JavaScript。它基于组件(声明的组件)和属性(绑定的属性)的概念。一个简单的例子是一个空的矩形场景

import Qt 4.7

Rectangle {
    id: theRect

    width: 400
    height: width*1.5
}

在此代码片段中,组件 Rectangle 被实例化。所有以大写字母开头的单词都会实例化组件。在矩形声明中,三个属性被绑定到值。id 属性是特殊的;它命名项目。将来,矩形可以被引用为 theRect。要访问矩形的属性(例如其宽度),可以使用 theRect.width。

接下来,宽度被绑定到值 400,高度被绑定到宽度乘以 1.5。请注意,高度被绑定到 width*1.5,而不是赋值为乘法的结果。这意味着如果宽度发生变化,高度会自动更新。

同样值得注意的是第一行,它导入了 Qt 4.7 版本的所有组件。这导入了一组组件,例如矩形类,这些组件是使用 C++ 定义和实现的。可以导入更多基于 C++ 的组件、用 QML 编写的组件或 QML 组件的整个模块。

我在这里不会详细介绍 QML 组件。基本上,组件是给定 qml 源文件的内容。导入名为 Foo.qml 的文件后,可以将其内容实例化为 Foo { ... }。模块是包含组件的目录。导入模块只是意味着导入目录的所有组件。一个非常酷的功能是,模块可以从 Internet 上的远程位置加载。

状态和过渡

状态和状态之间的过渡是 QML 中深度集成的概念。Qt 4.6 版本引入了用于支持此功能的 C++ 类。但是,使用 QML,使用状态和过渡是很自然的事情。

清单 1 中显示的代码演示了许多 QML 概念。首先是如何声明项目层次结构的示例。场景矩形包含红色和蓝色矩形。红色矩形又包含一个文本项目和一个鼠标区域项目。

清单 1. 状态和过渡

import Qt 4.7

Rectangle {
  width: 300; height: 150
  id: scene

  Rectangle {
    id: red
    x: 50; y: 50
    width: 50; height: 50
    color: "red"

    Text {
      anchors.centerIn: parent
      text: "Red"
    }

    MouseArea {
      anchors.fill: parent
      onClicked: {
        if(scene.state == "redFocus")
          scene.state="";
        else
          scene.state = "redFocus";
      }
    }
  }

  Rectangle {
    id: blue
    x: 200; y: 50
    width: 50; height: 50
    color: "blue"

    MouseArea {
      anchors.fill: parent
      onClicked: {
        if(scene.state == "blueFocus")
          scene.state="";
        else
          scene.state = "blueFocus";
      }
    }
  }

  Text {
    anchors.centerIn: blue
    text: "Blue"
  }

  states: [
    State {
      name: "redFocus"
      PropertyChanges { target: red; scale: 2.5 }
      PropertyChanges { target: blue; rotation: 30 }
    },

    State {
      name: "blueFocus"
      PropertyChanges { target: red; rotation: 30 }
      PropertyChanges { target: blue; scale: 2.5 }
    }
  ]

  transitions: [
    Transition {
      NumberAnimation { properties: "scale";
        duration: 2000; easing.type: Easing.OutBounce }
      NumberAnimation { properties: "rotation";
        duration: 750; easing.type: Easing.InOutCubic }
    }
  ]
}

红色矩形中的文本项目演示了另一个功能:锚布局。项目可以相互锚定,可以锚定到它们的侧面或中心线。锚点可以使用边距进行偏移,不同的项目可以用于锚定同一项目的不同部分。基本上,您所有的布局需求都应该由锚布局覆盖。在这个特定的例子中,文本的中心锚定到父矩形的中心。

再往下,声明了另一个文本项目。这一次,它位于蓝色矩形的中心。请注意,文本项目不必是它所居中的项目的子项。这将在稍后产生影响。

继续在红色矩形中,我们到达鼠标区域。这是 QML 中的另一个概念——交互区域没有紧密映射到视觉效果。鼠标区域用于与鼠标事件交互。可以将其视为一个不可见的矩形,可以像视觉项目一样锚定到其他项目。

在鼠标区域中,onClicked 信号绑定到一段 JavaScript。在这种情况下,它会更改场景项目的 state 属性。这使我们进入状态和过渡。

QML 中的项目具有状态列表和过渡列表。在示例中,状态列表包含两个状态:redFocus 和 blueFocus。每个状态包含多个 PropertyChange 项目。这些项目修改目标项目的属性。在 redFocus 的情况下,红色项目的比例和蓝色项目的旋转被更改。其他项目可以在状态中使用,例如,ParentChange 将项目在项目层次结构中移动。

回顾绑定到 onClicked 事件的 JavaScript,state 属性的更改在 states 属性中列出的状态之间移动。当状态设置为空字符串时,将使用默认状态。这意味着所有属性都设置为它们的初始、未更改的值。

谜题的最后一块是 transitions 属性,它是不同属性的值更改的行为列表。可以控制每个过渡方向的每个项目的每个单独属性。但是,在示例中,我们仅控制所有项目和所有过渡的每个属性。NumberAnimation 项目控制每次更改所需的时间以及更改的进行方式。比例会反弹,而旋转则根据三次曲线加速和减速,从而形成平滑的运动。

查看图 1 中的屏幕截图,您可以看到两个文本之间的区别。在红色矩形的情况下,文本是矩形的子项。这意味着矩形的旋转和缩放也应用于文本。在蓝色矩形的情况下,文本只是保持居中。它不受应用于矩形的转换的影响,因为它现在是它的子项。

Quick User Interfaces with Qt

Quick User Interfaces with Qt

Quick User Interfaces with Qt

图 1. 状态和过渡

模型和视图

在构建用户界面时,常见的场景是显示数据列表。正如您已经猜到的,QML 也为此提供了支持。清单 2 显示了如何使用它。

清单 2. 列表视图

import Qt 4.7

Rectangle {
  width: 200; height: 200

  ListModel {
    id: countries

    ListElement {
      name: "Denmark"; capital: "Copenhagen"
    }

    ...

    ListElement {
      name: "Sweden"; capital: "Stockholm"
    }
  }

  Component {
    id: countryDelegate

    Item {
      width: listView.width; height: 50

      MouseArea {
        anchors.fill: parent
        onClicked: { listView.currentIndex = index; }
      }

      Rectangle {
        x: 3; y: 3
        width: parent.width-6
        height: parent.height-6

        color: listView.currentIndex==index?"white":"lightgray"
        radius: 8

        Column {
          anchors.fill: parent
          anchors.margins: 5
          Text {
            font.bold: true; font.pixelSize: 18
            color: "#444444"; text: name
          }
          Text {
            font.italic: true; font.pixelSize: 10
            color: "#666666"; text: capital
  } } } } }

  Component {
    id: highlightFrame

    Item {
      width: listView.width; height: 50;
      y: listView.currentItem.y
      Rectangle {
        x: 3; y: 3
        width: parent.width-6
        height: parent.height-6
        radius: 8
        border.width: 4; border.color: "darkGray"
      }
    }
  }

  ListView {
    id: listView
    anchors.fill: parent
    model: countries
    delegate: countryDelegate
    highlight: highlightFrame
    focus: true
    highlightFollowsCurrentItem: true
  }
}

清单 2 中的示例由四个主要部分组成:countries 模型、countryDelegate 组件、highlightFrame 组件和 ListView 项目,它们将所有内容组合在一起。从底部开始,列表视图项目引用模型、委托和高亮显示。这些是先前实现的模型和组件。除此之外,还需要对视图的行为进行一些调整,以允许键盘导航与鼠标导航并行。

回到示例的顶部,模型是一个 ListModel,其中包含一组 ListElement 项目。列表元素的属性通过视图可用,如果您继续进入 countryDelegate 组件及其文本项目,您可以看到这一点。

countryDelegate 组件是列表视图用于可视化列表的每个项目的组件。它由一个鼠标区域和一个包含两个文本的矩形组成。如果单击项目,则鼠标区域设置列表的当前项目,而文本显示模型的数据。请注意,项目的 text 属性绑定到模型列表元素中使用的属性名称。这使得将委托中的项目绑定到模型数据变得容易。

让我们继续 highlightFrame 组件。这是一个视图放置在当前项目之上的框架。在这种情况下,它为项目添加边框。如果 countryDelegate 是当前项目,则它会更改自己的背景颜色。这是因为当前项目在没有背景颜色的情况下显示,而不是有背景颜色。这仅使用高亮显示框架是无法实现的。

最后,列表视图将所有内容组合在一起。结果如图 2 所示。

Quick User Interfaces with Qt

图 2. 带有委托和高亮矩形的列表视图

QML 运行时

在开发 QML 应用程序时,通常依赖 QML 查看器工具。使用 QML 填充模型数据(无论是虚假数据还是真实数据)也很常见。大多数状态管理和工作也可以在 JavaScript 的帮助下从 QML 执行。但是,在大多数情况下,任何 QML 应用程序都需要一个本机应用程序作为运行时环境。这就是 QtDeclarative 模块发挥作用的地方。

对于熟悉 Qt 的读者来说,很高兴知道 QML 由语言引擎、脚本执行的上下文和在其中操作的 QGraphicsScene 组成。所有这些组件都可以手动设置,甚至可以将 QML 组件添加到现有场景中。这样,您可以逐步升级现有应用程序。

如果您从头开始使用 QML 应用程序,则 QDeclarativeView 将所有这些组件封装到一个类中,该类也恰好是一个小部件。对于仅依赖 QML 作为其用户界面的应用程序,这就是它所需要的全部。

为了将 C++ 对象集成到 QML 中,使用了 QObject 元系统。这意味着任何 QObject 派生类都可以暴露给 QML。从 QML 中,属性、信号和槽将可用。由于 QML 属性绑定到值,而不是赋值,因此应用程序的 C++ 部分的任何更改都会自动反映在 QML 部分中。

本文的范围之外是详细介绍这一点,但在第一个示例中,状态可以从 C++ 驱动。这将使 QML 处理它擅长的内容:视觉效果和动态过渡。在第二个示例中,典型的应用程序将从 C++ 提供模型,再次让 QML 专注于视觉效果。

这种方法有很多好处。第一个好处是使用 QML 可以快速创建用户界面,因为整个语言都专注于该目标。另一个好处是,您被迫在用户界面和应用程序的其余部分之间保持清晰的划分。这导致更结构化和更好的代码。

未来

展望 Qt Quick 的未来,可能会发生很多事情。在 MeeGo 中,MeeGo 触摸计划正在使用 Qt Quick 实现新的小部件。在 KDE 中,Plasma 正在支持 Qt Quick。这样做的一个效果是您可以使用 QML 编写 Plasmoids。在 Qt 工具部门,开发人员正在为 Qt Quick 开发可视化设计器。它已经有一些有趣的功能——例如,图层可以直接从 GIMP 和 Photoshop 导入到设计器中。

展望 Qt,我不认为我们已经看到了最后一个基于小部件的应用程序。实际上,在为重要任务创建严肃的软件时,我看不到任何不使用小部件的理由。但是,随着对移动设备的新的关注,不仅在旧的 Trolltech 内部,而且在整个 Linux 社区中,我认为 Qt Quick 将成为一种非常常用的工具。

Qt Quick 入门

由于 Qt 4.7 最近发布,Qt Quick 正通过大多数发行版的存储库提供。一些发行版选择将 Qt 打包成多个软件包,因此请确保您获得 Qt 开发软件包、Qt Creator 软件包和所有 Qt 模块,尤其是那些引用 Qt declarative 的模块。在 Linux 世界中,我建议您使用发行版提供的工具来安装和维护您的软件。但是,对于那些需要特定版本的 Qt 工具的人,或者如果您使用的发行版不包含 Qt,您可以从诺基亚的网站下载 Qt SDK。

您需要的软件包是 Qt SDK,可从 qt.nokia.com/downloads 获取。只需下载文件,chmod使其可执行并运行安装程序。如果您没有 root 访问权限,则可以将其安装在您的主目录中。SDK 包括工具、演示、源代码和文档,所有这些都在一个方便的软件包中。

小部件的局限性

从使用小部件构建用户界面施加了许多限制,这些限制在切换到基于图形视图的方法时得到解决。一个明显的限制是小部件是矩形的,并且喜欢并排排列,这使得以良好的方式排列非矩形项目变得困难。

另一个限制是小部件会剪切它们的子项,这意味着子项不能延伸到它们的父小部件之外。以一个简单的效果为例,例如让用户界面的某些部分爆炸。在这种情况下,剪切是一个限制因素。

基于小部件的系统通常不支持的另一个功能是项目尺寸和位置的亚像素分辨率。此外,小部件不支持转换,例如缩放和旋转。在场景中,所有这些功能都可以用于实现最佳的视觉体验。

随着时间的推移进行转换,很明显小部件不适合滑动、弹跳或通常移动。它们旨在基于网格、列和行排列在布局中,并且它们为用户提供了标准化的、结构化的用户界面。当用户将计算机用作计算机时,这非常好。当用户使用设备时,这种类型的计算机界面不是最合适的解决方案。

Johan Thelin 是一位热情的 Qt 和开源用户。他在 Pelagicore 工作,在汽车行业从事 Linux 和开源工作。晚上,他担任顾问和自由撰稿人。

加载 Disqus 评论