4、QML布局与样式:从基础到实战

布局和样式,说白了就是界面的骨架和皮肤。QML 里这两件事绑得很紧,你写布局的时候就得想着样式,写样式的时候又得回头调布局。我刚开始接触 QGC 插件开发时,在这上面栽过不少跟头,今天把经验掰开了讲。

本章核心脉络: Row/Column/Grid 做基础排列 → Anchor 做精细定位 → 主题系统做全局统一 → CSS-like 样式做细节打磨。四层递进,缺一不可。
QML 布局与样式知识体系 Row/Column/Grid Anchor 布局 自定义主题 CSS-like 样式 核心属性 spacing / layoutDirection fillWidth / fillHeight Repeater 动态生成 锚点系统 anchors.left/right/top/bottom anchors.centerIn anchors.margins 主题机制 QGCTheme 单例 palette 颜色体系 字体/间距常量 样式写法 property alias QML 属性绑定 Loader 动态加载 实战输出: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 属性可以控制排列方向,从右往左排在某些国际化场景下会用到。

我的习惯: Row 和 Column 里尽量用 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 会导致循环绑定,QML 引擎会报错。比如 A 的左边绑 B 的右边,B 的左边又绑 A 的右边——死循环。遇到这种情况,加一个占位的 Item 做中间层就能解决。

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
    }
}
核心原则: 永远不要写死颜色值。所有颜色、字体、间距都走 QGCTheme。这样换皮肤时只需要改一个文件。

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 自动换组件,所有绑定自动更新。

一个小技巧: 把样式定义放在独立的 QML 文件里,用 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 里的颜色值,整个面板自动适配——这才是工程化的做法。

最后提醒一句: 布局嵌套不要太深。超过 5 层嵌套,性能会明显下降,而且代码可读性极差。遇到深层嵌套,考虑用 Repeater 或者拆成子组件。