可扩展性

当您为多个不同的移动设备平台开发应用程序时,您会面临以下挑战:

  • 移动设备平台支持不同屏幕配置的设备:尺寸、宽高比、方向和密度。
  • 不同的平台有不同的用户界面约定,您需要满足每个平台上用户的期望。

Qt Quick 移动设备应用程序》使您能够开发可在平板电脑和手机等不同类型设备上运行的应用程序。特别是,它们可以应对不同的屏幕配置。不过,要为每个目标平台创建最佳的用户体验,总是需要进行一定的修正和打磨。

在以下情况下,您需要考虑可扩展性

  • 您希望将应用程序部署到多个设备平台,如 Android 和 iOS,或多个设备屏幕配置。
  • 您希望为初次部署后市场上可能出现的新设备做好准备。

要实施可扩展的应用程序,请使用 Qt Quick:

  • 使用 Qt Quick Controls设计用户界面。
  • 使用Qt Quick Layouts(可调整项目大小)定义布局。
  • 使用属性绑定实现布局未涵盖的用例。例如,在像素密度较低和较高的屏幕上显示不同版本的图像,或根据当前屏幕方向自动调整视图内容。
  • 选择参考设备并计算缩放比例,以便根据实际屏幕尺寸调整图像、字体大小和边距。
  • 使用文件选择器加载特定平台的资产。
  • 使用加载器按需加载组件。

在设计应用程序时,请考虑以下模式:

  • 视图的内容可能在所有屏幕尺寸上都非常相似,但内容区域会扩大。如果使用ApplicationWindow QML 类型(来自Qt Quick Controls ),它会根据内容项的大小自动计算窗口大小。如果使用Qt Quick Layouts 来定位内容项,它们会自动调整推送给它们的内容项的大小。
  • 在较小的设备中,整个页面的内容可以构成较大设备中布局的一个组件元素。因此,可以考虑将其作为一个单独的组件(即在单独的 QML 文件中定义),在较小的设备中,视图将只包含该组件的一个实例。在较大的设备上,可能有足够的空间使用加载器来显示其他项目。例如,在电子邮件查看器中,如果屏幕足够大,就有可能同时显示电子邮件列表视图和电子邮件阅读器视图。
  • 对于游戏,您通常希望创建一个不缩放的游戏棋盘,以免给大屏幕玩家带来不公平的优势。一种解决方案是定义一个安全区域,该区域应适合支持最小宽高比的屏幕(通常为 3:2),并在该空间中添加装饰性内容,这些内容在 4:3 或 16:9 屏幕上将被隐藏。

动态调整应用程序窗口大小

Qt Quick ControlsQt Quick 中提供了一组用户界面控件来创建用户界面。通常,您会声明一个ApplicationWindow 控件作为应用程序的根项目。ApplicationWindow 为其他控件(如MenuBarToolBar 和 StatusBar)的定位提供了便利,而且与平台无关。在计算实际窗口的有效尺寸限制时,ApplicationWindow 将内容项的尺寸限制作为输入。

除了定义应用程序窗口标准部分的控件外,还提供了用于创建视图和菜单以及显示或接收用户输入的控件。您可以使用Qt Quick Controls Styles对预定义控件应用自定义样式。

Qt Quick Controls有些控件(如ToolBar )本身不提供布局,但需要您对其内容进行定位。为此,您可以使用Qt Quick 布局。

动态布局屏幕控件

Qt Quick 布局提供了使用 、 和 QML 类型在行、列或网格中布局屏幕控件的方法。这些 QML 类型的属性包含布局方向和单元格之间的间距。RowLayout ColumnLayout GridLayout

您可以使用Qt Quick LayoutsQML 类型为推送到布局的项目附加其他属性。例如,您可以为项目高度、宽度和尺寸指定最小值、最大值和首选值。

布局可确保您的用户界面在调整窗口和屏幕大小时缩放正确,并始终使用最大可用空间。

GridLayout 类型的一个特定用例是根据屏幕方向将其用作一行或一列。

下面的代码片段使用flow 属性设置了当屏幕宽度大于屏幕高度时网格从左到右的流向(作为行),反之则从上到下(作为列):

ApplicationWindow {
    id: root
    visible: true
    width: 480
    height: 620

    GridLayout {
        anchors.fill: parent
        anchors.margins: 20
        rowSpacing: 20
        columnSpacing: 20
        flow:  width > height ? GridLayout.LeftToRight : GridLayout.TopToBottom
        Rectangle {
            Layout.fillWidth: true
            Layout.fillHeight: true
            color: "#5d5b59"
            Label {
                anchors.centerIn: parent
                text: "Top or left"
                color: "white"
            }
        }
        Rectangle {
            Layout.fillWidth: true
            Layout.fillHeight: true
            color: "#1e1b18"
            Label {
                anchors.centerIn: parent
                text: "Bottom or right"
                color: "white"
            }
        }
    }
}

不断调整屏幕大小和重新计算屏幕是有性能代价的。例如,移动设备和嵌入式设备可能没有足够的能力在每一帧中重新计算动画对象的大小和位置。如果在使用布局时遇到性能问题,可以考虑使用绑定等其他方法。

以下是一些不应该使用布局的事项:

  • 不要对布局中项的 x、y、宽度或高度属性进行绑定,因为这会与布局的目标相冲突,而且还会导致绑定循环。
  • 不要定义定期评估的复杂 JavaScript 函数。这会导致性能低下,尤其是在动画过渡期间。
  • 不要假设容器的大小或子项的大小。尽量使用灵活的布局定义,以适应可用空间的变化。
  • 如果希望设计达到完美像素,请不要使用布局。内容项将根据可用空间自动调整大小和位置。

使用绑定

如果Qt Quick 布局不能满足您的需求,您可以使用属性绑定。绑定可使对象根据其他对象属性的变化或某些外部事件的发生自动更新其属性。

为对象的属性赋值时,既可以为其分配静态值,也可以将其绑定到 JavaScript 表达式。在前一种情况下,除非为属性分配新值,否则属性值不会改变。在后一种情况下,QML 引擎会创建一个属性绑定,只要求值表达式的值发生变化,属性值就会自动更新。

这种类型的定位动态性最强。不过,不断求值 JavaScript 表达式需要付出性能代价。

在没有自动支持的平台(如 Android、macOS 和 iOS)上,您可以使用绑定来处理低像素密度和高像素密度问题。下面的代码片段使用Screen.pixelDensity 附加属性来指定在像素密度低、高或正常的屏幕上显示不同的图像:

Image {
    source: {
        if (Screen.pixelDensity < 40)
        "image_low_dpi.png"
        else if (Screen.pixelDensity > 300)
        "image_high_dpi.png"
        else
        "image.png"
        }
    }

在 Android、macOS 和 iOS 上,你可以使用相应的标识符(如@2x@3x@4x)为图标和图像提供分辨率更高的替代资源,并将它们放在资源文件中。与屏幕像素密度相匹配的版本会被自动选择使用。

例如,以下代码段将尝试在 Retina 显示屏上加载artwork@2x.png:

Image {
    source: "artwork.png"
}

处理像素密度

一些 QML 类型,如ImageBorderImageText ,会根据为其指定的属性自动缩放。如果未指定 Image 的宽度和高度,它会自动使用源图像的大小,使用source 属性指定。默认情况下,指定宽度和高度会使图像按此尺寸缩放。可以通过设置fillMode 属性来改变这种行为,使图像被拉伸和平铺。不过,原始图像尺寸在高 DPI 显示器上可能会显得太小。

BorderImage 用于通过缩放或平铺每幅图像的部分内容来创建图像边框。它将源图像分成 9 个区域,并根据属性值进行缩放或平铺。不过,边角完全没有缩放,这可能会导致在高 DPI 显示器上效果不理想。

Text QML 类型会尝试确定需要多少空间,并相应设置widthheight 属性,除非它们被明确设置。fontPointSize 属性以与设备无关的方式设置点大小。不过,以点为单位指定字体而以像素为单位指定其他尺寸会造成问题,因为点与显示密度无关。在低 DPI 显示器上看起来正确的字符串边框,在高 DPI 显示器上可能会变得太小,导致文本被剪切。

不同平台对高 DPI 的支持程度以及所支持平台使用的技术各不相同。下文将介绍在高 DPI 显示器上缩放屏幕内容的不同方法。

有关 Qt 高 DPI 支持和支持平台的更多信息,请参阅高DPI

高 DPI 缩放

如果目标设备支持高 DPI 缩放,操作系统会为 Qt 提供一个缩放比例,用于缩放图形输出。

这种方法的优点是矢量图形和字体会自动缩放,现有应用程序往往无需修改即可运行。但对于光栅内容,则需要高分辨率的替代资源。

Qt QuickQt Widgets 堆栈实现了缩放,Qt XML GUI 中也提供了一般支持。

低级图形应用程序接口以设备像素为单位运行。这包括使用 OpenGL API 的代码和使用QRhi API 的代码。例如,这意味着QWindowsize() 大小为 1280x720,QWindow::devicePixelRatio() 大小为 2,其渲染目标(swapchain)的设备像素大小为 2560x1440。

操作系统会缩放窗口、事件和桌面几何图形。Cocoa 平台插件将缩放比例设置为QWindow::devicePixelRatio() 或QScreen::devicePixelRatio() 以及后备存储。

对于Qt WidgetsQPainter 从后备存储中获取devicePixelRatio() 并将其解释为缩放比例。

然而,在 OpenGL 中,像素始终是设备像素。例如,传递给 glViewport() 的几何图形需要按 devicePixelRatio() 缩放。

指定的字体大小(以点或像素为单位)不会改变,字符串与用户界面其他部分相比仍保持相对大小。字体的缩放是绘制的一部分,因此无论指定的是点数还是像素,12 号字体在缩放 2 倍后实际上就变成了 24 号字体。px单位被解释为与设备无关的像素,以确保字体在高 DPI 显示器上不会显得更小。

计算缩放比例

您可以选择一个高 DPI 设备作为参考设备,然后计算缩放比例,以便根据实际屏幕尺寸调整图像和字体大小以及页边距。

下面的代码片段使用了 Nexus 5 Android 设备的 DPI、高度和宽度参考值、QRect 类返回的实际屏幕尺寸以及qApp 全局指针返回的屏幕逻辑 DPI 值,从而计算出图像尺寸和边距的缩放比 (m_ratio) 以及字体大小的缩放比 (m_ratioFont):

qreal refDpi = 216.;
qreal refHeight = 1776.;
qreal refWidth = 1080.;
QRect rect = QGuiApplication::primaryScreen()->geometry();
qreal height = qMax(rect.width(), rect.height());
qreal width = qMin(rect.width(), rect.height());
qreal dpi = QGuiApplication::primaryScreen()->logicalDotsPerInch();
m_ratio = qMin(height/refHeight, width/refWidth);
m_ratioFont = qMin(height*refDpi/(dpi*refHeight), width*refDpi/(dpi*refWidth));

为了获得合理的缩放比例,高度和宽度值必须根据参考设备的默认方向(本例中为纵向)来设置。

如果字体缩放比小于 1 会导致字体过小,则下面的代码片段会将字体缩放比设置为1

int tempTimeColumnWidth = 600;
int tempTrackHeaderWidth = 270;
if (m_ratioFont < 1.) {
    m_ratioFont = 1;

您应该对目标设备进行试验,找出需要额外计算的边缘情况。有些屏幕可能太短或太窄,无法容纳所有计划的内容,因此需要自己的布局。例如,您可能需要在长宽比为 1:1 等非典型屏幕上隐藏或替换某些内容。

缩放比例可应用于QQmlPropertyMap 中的所有尺寸,以缩放图片、字体和页边距:

m_sizes = new QQmlPropertyMap(this);
m_sizes->insert(QLatin1String("trackHeaderHeight"), QVariant(applyRatio(270)));
m_sizes->insert(QLatin1String("trackHeaderWidth"), QVariant(applyRatio(tempTrackHeaderWidth)));
m_sizes->insert(QLatin1String("timeColumnWidth"), QVariant(applyRatio(tempTimeColumnWidth)));
m_sizes->insert(QLatin1String("conferenceHeaderHeight"), QVariant(applyRatio(158)));
m_sizes->insert(QLatin1String("dayWidth"), QVariant(applyRatio(150)));
m_sizes->insert(QLatin1String("favoriteImageHeight"), QVariant(applyRatio(76)));
m_sizes->insert(QLatin1String("favoriteImageWidth"), QVariant(applyRatio(80)));
m_sizes->insert(QLatin1String("titleHeight"), QVariant(applyRatio(60)));
m_sizes->insert(QLatin1String("backHeight"), QVariant(applyRatio(74)));
m_sizes->insert(QLatin1String("backWidth"), QVariant(applyRatio(42)));
m_sizes->insert(QLatin1String("logoHeight"), QVariant(applyRatio(100)));
m_sizes->insert(QLatin1String("logoWidth"), QVariant(applyRatio(286)));

m_fonts = new QQmlPropertyMap(this);
m_fonts->insert(QLatin1String("six_pt"), QVariant(applyFontRatio(9)));
m_fonts->insert(QLatin1String("seven_pt"), QVariant(applyFontRatio(10)));
m_fonts->insert(QLatin1String("eight_pt"), QVariant(applyFontRatio(12)));
m_fonts->insert(QLatin1String("ten_pt"), QVariant(applyFontRatio(14)));
m_fonts->insert(QLatin1String("twelve_pt"), QVariant(applyFontRatio(16)));

m_margins = new QQmlPropertyMap(this);
m_margins->insert(QLatin1String("five"), QVariant(applyRatio(5)));
m_margins->insert(QLatin1String("seven"), QVariant(applyRatio(7)));
m_margins->insert(QLatin1String("ten"), QVariant(applyRatio(10)));
m_margins->insert(QLatin1String("fifteen"), QVariant(applyRatio(15)));
m_margins->insert(QLatin1String("twenty"), QVariant(applyRatio(20)));
m_margins->insert(QLatin1String("thirty"), QVariant(applyRatio(30)));

以下代码段中的函数将缩放比例应用于字体、图像和页边距:

int Theme::applyFontRatio(const int value)
{
    return int(value * m_ratioFont);
}

int Theme::applyRatio(const int value)
{
    return qMax(2, int(value * m_ratio));
}

当目标设备的屏幕尺寸相差不大时,这种技术能带来合理的结果。如果差异很大,可以考虑使用不同的参考值创建几个不同的布局。

根据平台加载文件

您可使用QQmlFileSelectorQFileSelector 应用于 QML 文件加载。这样,您就可以根据运行应用程序的平台加载其他资源。例如,在 Android 设备上运行时,可使用+android 文件选择器加载不同的图像文件。

您可以将文件选择器与单例对象一起使用,以便在特定平台上访问对象的单个实例。

文件选择器是静态的,并强制执行一种文件结构,其中特定平台的文件存储在以平台命名的子文件夹中。如果需要更动态的解决方案来按需加载用户界面的部分内容,可以使用加载器。

目标平台可能会以各种方式自动加载用于不同显示密度的替代资源。在 Android 和 iOS 上,@2x文件名后缀用于表示高 DPI 版本的图像。如果提供 @2x 版本的图片和图标,ImageQML 类型和QIcon 类会自动加载。QImageQPixmap 类会自动将 @2x 版本图像的devicePixelRatio 设置为2 ,但您需要添加代码才能实际使用 @2x 版本:

if ( QGuiApplication::primaryScreen()->devicePixelRatio() >= 2 ) {
    imageVariant = "@2x";
} else {
    imageVariant = "";
}

Android 定义了通用的屏幕尺寸(小、普通、大、xlarge)和密度(ldpi、mdpi、hdpi、xhdpi、xxhdpi 和 xxxhdpi),您可以为其创建替代资源。Android 会在运行时检测当前设备配置,并为应用程序加载相应的资源。不过,从 Android 3.2(API 第 13 级)开始,这些尺寸组将被弃用,转而使用一种新技术来根据可用屏幕宽度管理屏幕尺寸。

按需加载组件

Loader 可以加载 QML 文件(使用source 属性)或组件对象(使用sourceComponent 属性)。这对于延迟创建组件直到需要时才创建非常有用。例如,应按需创建组件,或出于性能原因不应不必要地创建组件。

您还可以使用加载器来应对因特定平台不支持某些功能而不需要用户界面的情况。您可以确定该视图已被隐藏,然后使用加载器显示其他视图,而不是在应用程序运行的设备上显示不需要的视图。

切换方向

Screen.orientation 附加属性包含屏幕的当前方向(如果有加速度计)。在台式电脑上,该值通常不会改变。

如果primaryOrientation 紧跟orientation ,则表示屏幕会根据您握持设备的方式自动旋转显示的所有内容。如果primaryOrientation 不变,但方向发生了变化,则设备可能无法旋转自己的显示屏。在这种情况下,您可能需要使用Item.rotationItem.transform 来旋转内容。

应用程序顶层页面定义和可重用组件定义应使用一个 QML 布局定义来定义布局结构。此单一定义应包括针对不同设备方向和宽高比的布局设计。这样做的原因是,方向切换时的性能至关重要,因此确保在方向改变时加载两种方向所需的所有组件是个好主意。

相反,如果您选择使用Loader 来加载不同方向所需的额外 QML,则应进行全面测试,因为这将影响方向转换的性能。

为了在不同方向之间实现布局动画,锚点定义必须位于同一包含组件中。因此,页面或组件的结构应由一组共同的子组件、一组共同的锚点定义和一组状态(定义在StateGroup 中)组成,这些状态代表组件支持的不同纵横比。

如果页面中包含的组件需要以多种不同的外形尺寸定义托管,那么视图的布局状态应取决于页面(其直接容器)的纵横比。同样,一个组件的不同实例可能位于用户界面中的多个不同容器中,因此其布局状态应取决于其父级容器的纵横比。总之,布局状态应始终遵循直接容器的纵横比(而不是当前设备屏幕的 "方向")。

在每个布局State 中,应使用本地 QML 布局定义来定义项之间的关系。更多信息请参阅下文。在状态之间的转换期间(由顶层方向变化触发),如果是锚布局,AnchorAnimation 元素可用于控制转换。在某些情况下,也可以使用NumberAnimation 来控制项目的宽度等。切记避免在每一帧动画中进行复杂的 JavaScript 计算。在大多数情况下,使用简单的锚点定义和锚点动画可以解决这个问题。

还有一些情况需要考虑:

  • 如果您有一个页面,横向和纵向看起来完全不同,也就是说,所有的子项目都不同,该怎么办?对于每个页面,可以使用两个子组件,分别定义布局,并使其中一个或另一个项目在每种状态下的不透明度为零。您可以使用交叉渐变动画,只需在不透明度上应用NumberAnimation 过渡即可。
  • 如果有一个页面在纵向和横向之间共享 30%或更多的相同布局内容,该怎么办?在这种情况下,可以考虑使用一个具有横向和纵向状态的组件,以及一系列独立的子项,这些子项的不透明度(或位置)取决于方向状态。这样,您就可以对在两种方向之间共享的项目使用布局动画,而其他项目要么淡入/淡出,要么动画开/关屏。
  • 如果在手持设备上有两个页面需要同时显示在屏幕上,例如在外形尺寸较大的设备上,该怎么办?在这种情况下,请注意您的视图组件将不再占据整个屏幕。因此,请务必记住,所有组件(尤其是列表委托项)都应取决于包含组件的宽度大小,而不是屏幕宽度。在这种情况下,可能有必要在 Component.onCompleted() 处理程序中设置宽度,以确保在设置值之前已构建了列表委托项。
  • 如果两个方向占用太多内存,无法同时在内存中设置怎么办?如果无法同时将两个版本的视图保存在内存中,可以在必要时使用Loader ,但要注意布局切换时交叉渐变动画的性能。一种解决方案是将两个 "闪屏 "项目作为页面的子项,然后在旋转过程中在两个项目之间交叉淡入淡出。然后,您可以使用Loader 加载另一个子组件,将实际模型数据加载到另一个子项,并在Loader 完成后交叉淡入淡出。

另请参阅 Qt Quick Responsive Layouts(响应式布局)。

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.