4、QML布局与样式:从基础到实战
布局和样式,说白了就是界面的骨架和皮肤。QML 里这两件事绑得很紧,你写布局的时候就得想着样式,写样式的时候又得回头调布局。我刚开始接触 QGC 插件开发时,在这上面栽过不少跟头,今天把经验掰开了讲。
4.1 Row/Column/Grid:最基础的排列方式
这三个布局组件,是 QML 里最常用的「排列三兄弟」。Row 横着排,Column 竖着排,Grid 排成网格。听起来简单,但用起来有不少门道。
4.1.1 Row 和 Column
先看一个最典型的场景——QGC 工具栏上的按钮排列:
Row {
spacing: 8
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: ["起飞", "返航", "悬停", "降落"]
delegate: ToolButton {
text: modelData
width: 80
height: 36
}
}
}
这里有个细节:spacing 控制元素间距,我习惯设成 8 或 12,太挤了不好看,太大了又浪费空间。另外 layoutDirection 属性可以控制排列方向,从右往左排在某些国际化场景下会用到。
fillWidth: true 或 fillHeight: true,让子元素自动撑满。手动写死宽度容易在窗口缩放时出问题。
4.1.2 Grid 布局
Grid 适合做参数面板、状态矩阵这类规整的界面。比如 QGC 里显示传感器状态的 2×2 网格:
Grid {
columns: 2
columnSpacing: 12
rowSpacing: 8
anchors.centerIn: parent
Repeater {
model: sensorModel
delegate: SensorCard {
width: 160
height: 100
}
}
}
嗯,这里要注意 columns 和 rows 的优先级——你设了 columns,Grid 会自动算行数;反过来也一样。我建议只设一个,另一个让系统自己算,省得搞混。
4.2 Anchor 布局:精细定位的利器
Row/Column 适合「排排坐」,但遇到「这个按钮要贴在右上角,那个标签要居中」的需求,就得靠 Anchor 了。
Anchor 的核心思路是:把元素的边和另一个元素的边绑在一起。语法很直观:
Rectangle {
id: parentRect
width: 400; height: 300
Rectangle {
id: childRect
width: 100; height: 50
anchors.right: parentRect.right
anchors.bottom: parentRect.bottom
anchors.margins: 10
}
}
这段代码的意思是:childRect 的右边和底边,分别绑定到 parentRect 的右边和底边,再留 10 像素的边距。效果就是 childRect 贴在右下角。
Anchor 的常用锚点组合:
| 锚点属性 | 绑定目标 | 典型场景 |
|---|---|---|
| anchors.left | 目标元素的左边 | 左对齐 |
| anchors.right | 目标元素的右边 | 右对齐 |
| anchors.top | 目标元素的顶边 | 顶部对齐 |
| anchors.bottom | 目标元素的底边 | 底部对齐 |
| anchors.horizontalCenter | 目标元素的水平中线 | 水平居中 |
| anchors.verticalCenter | 目标元素的垂直中线 | 垂直居中 |
| anchors.centerIn | 目标元素的中心 | 完全居中 |
我个人最喜欢 anchors.centerIn,一行代码搞定居中,比算坐标省心多了。
4.3 自定义主题与样式
QGC 的界面风格统一,靠的是 QGCTheme 这个单例对象。你想想看,如果每个按钮都写死颜色,改一次主题得翻遍所有文件——那画面太美我不敢看。
4.3.1 QGCTheme 的结构
QGCTheme 定义在 QGCTheme.qml 里,核心是几个 palette 对象:
// QGCTheme.qml(简化版)
QtObject {
property QtObject palette: QtObject {
property color primary: "#4A90D9"
property color primaryDark: "#357ABD"
property color background: "#F5F5F5"
property color text: "#333333"
property color textLight: "#888888"
property color danger: "#E74C3C"
property color success: "#27AE60"
property color warning: "#F39C12"
}
property real spacingSmall: 4
property real spacingMedium: 8
property real spacingLarge: 16
property font fontSmall: Qt.font({ pixelSize: 12 })
property font fontMedium: Qt.font({ pixelSize: 14 })
property font fontLarge: Qt.font({ pixelSize: 18 })
}
然后在你的插件里引用:
Rectangle {
color: QGCTheme.palette.background
Text {
text: "状态正常"
color: QGCTheme.palette.success
font: QGCTheme.fontMedium
}
}
4.3.2 扩展主题
如果你想让插件支持暗色模式,可以在 QGCTheme 里加一个 darkPalette:
property QtObject darkPalette: QtObject {
property color background: "#1E1E1E"
property color text: "#E0E0E0"
// ...
}
property bool darkMode: false
property QtObject currentPalette: darkMode ? darkPalette : palette
然后在界面里统一用 QGCTheme.currentPalette.xxx。切换模式时,所有绑定了这个属性的元素会自动刷新——QML 的绑定机制就是这么爽。
4.4 QML 中的 CSS-like 样式
QML 没有真正的 CSS,但它的属性绑定机制可以实现类似的效果。说白了,就是把样式抽成可复用的属性块。
4.4.1 使用 property alias 复用样式
// Style.qml
QtObject {
property QtObject button: QtObject {
property color bgColor: "#4A90D9"
property color textColor: "#FFFFFF"
property real radius: 4
property real height: 36
}
property QtObject card: QtObject {
property color bgColor: "#FFFFFF"
property color borderColor:"#DDDDDD"
property real radius: 8
property real padding: 12
}
}
使用时:
Rectangle {
color: Style.button.bgColor
radius: Style.button.radius
height: Style.button.height
Text {
text: "点击"
color: Style.button.textColor
anchors.centerIn: parent
}
}
4.4.2 用 Loader 实现样式切换
如果你需要根据不同状态加载不同样式,Loader 是个好选择:
Loader {
id: styleLoader
sourceComponent: isDarkMode ? darkStyle : lightStyle
}
Component {
id: lightStyle
QtObject {
property color bg: "#FFFFFF"
property color fg: "#333333"
}
}
Component {
id: darkStyle
QtObject {
property color bg: "#1E1E1E"
property color fg: "#E0E0E0"
}
}
然后在界面里引用 styleLoader.item.bg。切换 isDarkMode 时,Loader 自动换组件,所有绑定自动更新。
pragma Singleton 声明为单例。这样整个插件都能直接引用,不用到处 import。
4.5 实战:组合使用
最后看一个综合例子——QGC 飞行状态面板:
// FlightStatusPanel.qml
Rectangle {
width: 300; height: 200
color: QGCTheme.currentPalette.background
radius: 8
Column {
anchors.fill: parent
anchors.margins: QGCTheme.spacingMedium
spacing: QGCTheme.spacingSmall
Text {
text: "飞行状态"
font: QGCTheme.fontLarge
color: QGCTheme.currentPalette.text
}
Grid {
columns: 2
columnSpacing: QGCTheme.spacingMedium
rowSpacing: QGCTheme.spacingSmall
width: parent.width
Repeater {
model: statusModel
delegate: Row {
spacing: 4
Text { text: modelData.label + ":"; font: QGCTheme.fontSmall; color: QGCTheme.currentPalette.textLight }
Text { text: modelData.value; font: QGCTheme.fontSmall; color: QGCTheme.currentPalette.text }
}
}
}
Rectangle {
height: 36
width: parent.width
color: QGCTheme.currentPalette.primary
radius: 4
Text {
text: "刷新数据"
color: "#FFFFFF"
anchors.centerIn: parent
font: QGCTheme.fontMedium
}
}
}
}
这个例子把 Row、Column、Grid、Anchor、主题全部串起来了。你想想看,如果哪天要换暗色主题,只需要改 QGCTheme 里的颜色值,整个面板自动适配——这才是工程化的做法。
Repeater 或者拆成子组件。