可扩展性
当您为多个不同的移动设备平台开发应用程序时,您将面临以下挑战:
- 移动设备平台支持具有不同屏幕配置的设备:大小、纵横比、方向和密度。
- 不同的平台有不同的 UI 习惯,您需要满足每个平台上用户的期望。
Qt Quick 使您能够开发可在不同类型设备上运行的应用程序,例如平板电脑和手机。特别是,它们可以处理不同的屏幕配置。但是,为了创建每个目标平台的最佳用户体验,通常需要一定程度的修复和润色。
您需要考虑以下情况的可扩展性:
- 您想将应用程序部署到多个设备平台,例如 Android 和 iOS,或多个设备屏幕配置。
- 您想要为新设备做好准备,这些设备可能在初始部署后出现在市场上。
使用 Qt Quick 实现可扩展的应用程序
- 使用提供一系列 UI 控件的 Qt Quick Controls 设计 UI。
- 使用可以调整其项目的 Qt Quick Layouts 定义布局。
- 使用 属性绑定 实现布局未覆盖的使用案例。例如,在像素密度高和低的屏幕上显示图像的替代版本,或根据当前屏幕方向自动调整视图内容。
- 选择一个参考设备,并计算一个 缩放比例 以调整图像和字体大小以及边距以适应实际屏幕大小。
- 使用 文件选择器 加载平台特定的资源。
- 通过使用 Loader 按需加载组件。
在设计应用程序时考虑以下模式
- 在所有屏幕尺寸上的视图内容可能非常相似,但有扩展的内容区域。如果您使用 Qt Quick Controls 中的 ApplicationWindow QML 类型,它将自动根据其内容项的大小计算窗口大小。如果您使用 Qt Quick Layouts 来定位内容项,它们将自动调整被推送给它们的项的大小。
- 在较小设备上的整个页面的内容可以是较大设备上布局的组件元素。因此,请考虑将其作为单独的组件(即,在单独的 QML 文件中定义)创建。在较小设备上,视图将简单地包含该组件的一个实例。在较大设备上,可能有足够的空间使用加载器显示额外的项目。例如,在电子邮件查看器中,如果屏幕足够大,可以同时显示电子邮件列表视图和电子邮件阅读器视图。
- 对于游戏,您通常希望创建一个不进行缩放的棋盘,这样不会给大屏幕上的玩家带来不公平的优势。一种解决方案是定义一个 安全区,使其适合具有最小支持纵横比(通常是 3:2)的屏幕,并在 4:3 或 16:9 屏幕上将隐藏的空间添加仅用于装饰的内容。
动态调整应用程序窗口大小
Qt Quick 控件提供了一组UI控件,用于在Qt Quick中创建用户界面。通常,您会将ApplicationWindow控件声明为应用程序的根项。该ApplicationWindow控件的添加,为对其他控件进行定位(如MenuBar、ToolBar和StatusBar)提供了方便,并且以平台无关的方式。在计算实际窗口的有效尺寸约束时,ApplicationWindow控件使用内容项的尺寸约束作为输入。
除了定义应用程序窗口标准部分的控件外,还提供了创建视图和菜单的控件,以及向用户接收或发送输入的控件。您可以使用Qt Quick 控件样式将对预定义控件的定制样式应用于其中。
Qt Quick 控件(如ToolBar)不提供自己的布局,但要求您定位其内容。为此,您可以使用Qt Quick 布局。
动态布局屏幕控件
Qt Quick 布局提供了一组方式,通过使用RowLayout、ColumnLayout和GridLayout QML类型在行、列或网格中布局屏幕控件。这些QML类型的属性保存了它们的布局方向和单元格之间的间距。
您可以使用Qt Quick 布局的QML类型将附加属性附加到推送到布局中的项。例如,您可以指定项目高度、宽度和大小的最小值、最大值和首选值。
布局确保在窗口和屏幕更改大小时UI能够正确缩放,并且始终使用可用的最大空间。
使用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、width或height属性捆绑到绑定上,因为这会与布局的目标冲突,并导致绑定循环。
- 不要定义频繁评估的复杂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显示上尝试加载[email protected]
Image {
source: "artwork.png"
}
处理像素密度
一些QML类型,例如Image、BorderImage和Text,则会根据为它们指定的属性自动进行缩放。如果指定了Image的宽度和高度,则自动将图像缩放到该大小。通过设置fillMode
属性,可以改变这种表现,允许图像被拉伸和平铺。然而,在较高DPI显示器上,原始图像大小可能显得太小。
BorderImage用于通过缩放或平铺每个图像的部分来从图像中创建边框。它将源图像划分成9个区域,根据属性值进行缩放或平铺。然而,角落不会进行任何缩放,这可能在较高DPI显示器上导致结果不够理想。
Text QML类型会尝试确定所需的空间,并根据需要设置width
和height
属性,除非它们被显式设置。fontPointSize
属性以设备无关的方式设置点大小。然而,以点和像素指定字体以及其他大小会导致问题,因为点是独立于显示密度的。在低DPI显示器上看起来正确的字符串边框在较高DPI显示器上可能会变得太小,导致文本被裁剪。
对高DPI的支持级别和支持平台的用法各不相同。以下各节介绍了不同方法来缩放高DPI显示器上的屏幕内容。
有关Qt和支支持平台高DPI支持更多信息,请参阅High DPI。
macOS和iOS上的高DPI缩放
在macOS和iOS上,应用程序使用的是与传统DPI缩放不同的高DPI缩放。在传统方法中,应用以一个DPI值呈现,该值用于放大字体大小、布局等。在新方法中,操作系统为Qt提供一个缩放比率,该比率用于缩放图形输出:分配更大的缓冲区并设置缩放变换。
这种方法的优点是矢量图形和字体可以自动缩放,并且现有应用程序无需修改即可工作。但是,对于光栅内容则需要高分辨率的备用资源。
缩放已对QtQuick和QtWidgets栈实现,同时也提供了QtGui和Cocoa平台插件的通用支持。
操作系统对窗口、事件和桌面几何进行缩放。Cocoa平台插件将缩放比例设置为QWindow::devicePixelRatio()或QScreen::devicePixelRatio(),以及在后备存储中。
对于QtWidgets,QPainter从后备存储中获取devicePixelRatio()并将其解释为缩放比例。
然而,在OpenGL中,像素始终是设备像素。例如,传递给glViewport()的几何形状需要通过devicePixelRatio()进行缩放。
指定的字体大小(以点或像素为单位)不会改变,与UI的其他部分相比,字符串保留其相对大小。字体作为绘图的一部分进行缩放,因此大小为12的字体实际上变成了具有2倍缩放的大号24字体,无论其是按点还是按像素指定。px单位被解释为设备无关像素,以确保在高清显示上字体不会显得太小。
计算缩放比例
您可以选择一个高DPI设备作为参考设备,并计算缩放比例以调整图像、字体大小和边距以适应实际屏幕尺寸。
以下代码示例使用Nexus 5 Android设备提供的DPI、高度和宽度参考值,以及由代码全局指针返回的实际屏幕大小返回值和屏幕的逻辑DPI值来计算图像大小和边距的缩放比例(m_ratio
)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)); }
当目标设备的屏幕尺寸相差不大时,该技术可以给出合理的结果。如果差异很大,请考虑创建具有不同参考值的几个不同的布局。
根据平台加载文件
您可以使用QQmlFileSelector将QFileSelector应用于QML文件加载。这使您可以根据应用程序运行的平台加载不同的备用资源。例如,您可以使用+android文件选择器在Android设备上运行时加载不同的图像文件。
您可以使用文件选择器与单例对象一起使用,以访问指定平台上的单个对象实例。
文件选择器是静态的,并强制执行一个文件结构,其中特定平台的文件存储在以平台命名的子文件夹中。如果您需要一个更动态的解决方案来按需加载UI的部分,则可以使用Loader。
目标平台可能会以不同的方式自动加载不同显示密度下的替代资源。在Android和iOS上,使用@2x文件名后缀来表示图形的高DPI版本。如果提供了图像和图标,则Image QML类型和QIcon类会自动加载@2x版本的图像和图标。类QImage和QPixmap会自动将@2x版本图像的devicePixelRatio
设置为2
,但您需要添加代码以实际使用@2x版本。
if ( QGuiApplication::primaryScreen()->devicePixelRatio() >= 2 ) { imageVariant = "@2x"; } else { imageVariant = ""; }
Android定义了通用的屏幕大小(小、正常、大、超大)和密度(ldpi、mdpi、hdpi、xhdpi、xxhdpi和xxxhdpi),可以为这些尺寸创建替代资源。Android在运行时检测当前设备配置,并为您的应用程序加载适当资源。但是,从Android 3.2(API级别13)开始,出于对基于可用屏幕宽度的新的屏幕尺寸管理技术的支持,已经弃用这些大小组。
按需加载组件
Loader可以加载QML文件(通过使用source
属性)或组件对象(通过使用sourceComponent
属性)。它在延迟创建组件直到需要时很有用。例如,当需要按需创建组件或为了性能原因应避免不必要地创建组件时。
您还可以使用加载器来响应您的UI的某些部分在特定平台上不需要的情况,因为平台不支持某些功能。您可以在不需要显示设备上运行的设备上的视图时确定视图被隐藏,并使用加载器在它的位置显示其他内容。
切换方向
附加属性Screen.orientation中包含屏幕的当前方向,来自加速度计(如果可用)。在桌面计算机上,该值通常不会改变。
如果primaryOrientation
跟随orientation
,则表示屏幕会自动旋转所有显示的内容,具体取决于您如何握持设备。即使primaryOrientation
没有改变,方向改变时,设备可能不会旋转其自己的显示。在这种情况下,您可能需要使用Item.rotation或Item.transform来旋转您的内容。
应用程序顶级页面定义和可重用组件定义应使用一个QML布局定义来定义布局结构。这个单个定义应包括针对不同的设备方向和宽高比的布局设计。原因在于在方向切换时的性能至关重要,因此确保在方向改变时加载两个方向都需要的所有组件是一个好主意。
相反,如果您选择使用Loader来加载在不同方向中需要的额外QML,您应进行彻底的测试,因为这将影响方向更改的性能。
为了在方向之间启用布局动画,锚点定义必须位于同一容器组件中。因此,页面或组件的结构应包括一组公共子组件,一组公共锚点定义,以及一组表示组件支持的不同宽高比的状态(在StateGroup中定义)。
如果一个页面中的组件需要以多种不同的形式因素定义托管,那么视图的布局状态应取决于页面的宽高比(即其直接容器)。同样地,一个组件的不同实例可能位于UI中的多个不同容器中,因此其布局状态应该由其父组件的宽高比决定。结论是,布局状态应始终遵循直接容器的宽高比(而不是当前设备屏幕的“方向”)。
在每一个布局状态中,您应该使用本机QML布局定义来定义项目之间的关系。更多信息请参阅下文。在状态之间的转换期间(由顶级方向更改触发),在锚定布局的情况下,可以锚定动画元素来控制转换。在某些情况下,您也可以对某项的宽度等属性使用数值动画。请记住,在动画的每一帧中避免复杂的JavaScript计算。在大多数情况下,使用简单的锚定定义和锚定动画有助于解决这个问题。
还有一些其他情况需要考虑。
- 如果您有一个页面,在不同方向下的外观完全不同,即所有子项都不同,那怎么办?对于每个页面,有两个子组件,具有独立的布局定义,并在每个状态下使一个或多个项具有零不透明度。您可以通过简单地在一个数值动画转换中将不透明度应用于其中一个项来使用交叉淡入淡出动画。
- 如果您有一个页面在不同的方向下共享了30%或更多的布局内容,那怎么办?在这种情况下,考虑有一个具有横向和纵向状态的单个组件,以及一组独立的子项,其不透明度(或位置)取决于方向状态。这将使您能够在方向之间共享的项目上使用布局动画,而其他项目则可以淡入/淡出,或者动画显示/消失。
- 如果您在手持设备的两个页面上需要同时显示内容,例如在一个较大的形式因素设备上,那怎么办?在这种情况下,请注意,您的视图组件将不再占据整个屏幕。因此,在所有组件(尤其是列表代理项)中,应依赖于容器宽度而不是屏幕宽度。在这种情况下,可能需要将宽度设置在Component.onCompleted()处理器中,以确保在设置值之前已经构建了列表项代理。
- 如果两种方向同时占用太多内存无法同时保留,那么如果无法同时保留视图的两种版本,则在必要时使用加载器,注意在布局转换期间对交叉淡入淡出动画的性能影响。一个解决方案是在Page的子项中拥有两个“启动屏幕”项,然后在这两个项之间交叉淡入淡出。然后您可以使用加载器来加载另一个子组件,该子组件在另一个子项中加载模型数据,并在加载器完成时将其交叉淡入。
© 2024 Qt公司有限公司。此处包含的文档贡献的版权归各自的拥有者所有。此处提供的文档是在由自由软件基金会发布的GNU自由文档许可1.3版的条款下授权的。Qt和相应的标志是芬兰和/或全球其他国家的Qt公司有限公司的商标。所有其他商标均为它们各自所有者的财产。