Qt Quick 视觉元素、交互与自定义组件(七)

11 阅读8分钟

适合人群: 已理解 QML 基础语法,想掌握 Qt Quick 核心视觉元素的开发者

前言

上一篇我们系统学习了 QML 的语法机制——对象、属性、绑定、信号。本篇进入 Qt Quick 模块本身:它提供了哪些视觉元素,如何导入外部资源,如何处理用户输入,以及如何用 JavaScript 扩展交互逻辑。

Qt Quick 是建立在 QML 语言之上的标准组件库,是你构建实际界面的直接工具。

一、搭建项目基础

本文所有示例基于以下 CMakeLists.txt 配置:

cmake_minimum_required(VERSION 3.16)
project(QtQuickDemo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Qt6 REQUIRED COMPONENTS Quick)
qt_standard_project_setup(REQUIRES 6.5)

qt_add_executable(QtQuickDemo main.cpp)

qt_add_qml_module(QtQuickDemo
    URI QtQuickDemo
    VERSION 1.0
    QML_FILES Main.qml
    RESOURCES
        images/background.jpg
        fonts/MyFont.ttf
)

target_link_libraries(QtQuickDemo PRIVATE Qt6::Quick)

main.cpp 使用标准模板不作修改,所有开发工作集中在 QML 文件中。


二、核心视觉元素

2.1 Item — 所有视觉元素的基类

Item 是 Qt Quick 中所有可视元素的基类,它本身不可见,但提供了所有视觉元素共有的属性:

import QtQuick

Item {
    width: 400
    height: 300

    // Item 的核心属性
    x: 0; y: 0            // 位置
    z: 0                  // 层叠顺序,数值大的在上层
    opacity: 1.0          // 透明度 0.0 ~ 1.0
    visible: true         // 是否显示
    clip: false           // 是否裁剪超出边界的子元素
    rotation: 0           // 旋转角度(度)
    scale: 1.0            // 缩放比例
}

2.2 Rectangle — 矩形

最常用的容器元素:

import QtQuick

Rectangle {
    width: 200
    height: 120
    color: "#4A90E2"
    radius: 12              // 圆角

    // 边框
    border.width: 2
    border.color: "#2C5F9E"

    // 渐变色
    gradient: Gradient {
        GradientStop { position: 0.0; color: "#6AB0F5" }
        GradientStop { position: 1.0; color: "#2C72C7" }
    }
}

2.3 Text — 文本

import QtQuick

Text {
    text: "Qt Quick 文本示例"

    // 字体设置
    font.family: "Arial"
    font.pixelSize: 18
    font.bold: true
    font.italic: false
    font.letterSpacing: 1.5    // 字间距

    // 颜色与对齐
    color: "#333333"
    horizontalAlignment: Text.AlignHCenter
    verticalAlignment: Text.AlignVCenter

    // 多行处理
    width: 300
    wrapMode: Text.WordWrap        // 自动换行
    elide: Text.ElideRight         // 超出显示省略号

    // 富文本
    textFormat: Text.RichText
    text: "普通文字 <b>加粗</b> <i>斜体</i> <font color='red'>红色</font>"
}

2.4 Image — 图片

import QtQuick

Image {
    width: 200
    height: 200
    source: "images/photo.jpg"     // 相对路径

    // 缩放模式
    fillMode: Image.PreserveAspectFit    // 保持比例,完整显示
    // fillMode: Image.PreserveAspectCrop  // 保持比例,裁剪填满
    // fillMode: Image.Stretch             // 拉伸填满(可能变形)

    // 加载状态
    onStatusChanged: {
        if (status === Image.Ready)
            console.log("图片加载完成")
        else if (status === Image.Error)
            console.log("图片加载失败")
    }
}

加载网络图片:

Image {
    source: "https://example.com/image.jpg"
    // 网络图片加载是异步的,status 会经历 Loading → Ready
}

三、导入外部资源

3.1 使用自定义字体

第一步:CMakeLists.txtRESOURCES 中注册字体文件。

第二步: 在 QML 中加载字体:

import QtQuick

Item {
    // 加载自定义字体
    FontLoader {
        id: customFont
        source: "fonts/MyFont.ttf"
    }

    Text {
        text: "自定义字体效果"
        font.family: customFont.name    // 使用加载的字体
        font.pixelSize: 24
    }
}

3.2 使用图片资源

注册到 CMakeLists.txt 后,在 QML 中直接用相对路径引用:

// 项目内资源(推荐)
Image { source: "images/logo.png" }

// 也可以用 qrc:/ 前缀显式引用
Image { source: "qrc:/QtQuickDemo/images/logo.png" }

四、定位:anchors 锚点系统

anchors 是 Qt Quick 中最灵活的定位机制,通过将一个元素的边与另一个元素的边对齐来定位。

4.1 基本锚点

Rectangle {
    id: parent_rect
    width: 400; height: 300

    // 贴左边
    Rectangle {
        width: 100; height: 100
        color: "red"
        anchors.left: parent.left
        anchors.top: parent.top
        anchors.margins: 10         // 所有方向留 10px 间距
    }

    // 贴右下角
    Rectangle {
        width: 100; height: 100
        color: "blue"
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        anchors.rightMargin: 10
        anchors.bottomMargin: 10
    }

    // 水平居中,垂直方向在顶部
    Rectangle {
        width: 100; height: 40
        color: "green"
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.top: parent.top
        anchors.topMargin: 10
    }
}

4.2 填满父容器

Rectangle {
    anchors.fill: parent               // 完全填满父容器
    anchors.margins: 16                // 四周留 16px 边距
}

4.3 相对于兄弟元素定位

Rectangle {
    id: firstBox
    width: 100; height: 100
    color: "orange"
    anchors.left: parent.left
    anchors.top: parent.top
    anchors.margins: 20
}

Rectangle {
    width: 100; height: 100
    color: "purple"
    anchors.left: firstBox.right      // 紧跟在 firstBox 右边
    anchors.leftMargin: 10
    anchors.top: firstBox.top         // 与 firstBox 顶部对齐
}

五、处理用户输入

5.1 MouseArea — 鼠标与触控

import QtQuick

Rectangle {
    width: 200
    height: 100
    color: clickArea.pressed ? "#2C72C7" : "#4A90E2"    // 按下时变深色
    radius: 8

    MouseArea {
        id: clickArea
        anchors.fill: parent

        // 常用信号
        onClicked: console.log("点击,位置:" + mouse.x + "," + mouse.y)
        onDoubleClicked: console.log("双击")
        onPressAndHold: console.log("长按")
        onEntered: console.log("鼠标进入")
        onExited: console.log("鼠标离开")

        // 接受右键
        acceptedButtons: Qt.LeftButton | Qt.RightButton
        onClicked: {
            if (mouse.button === Qt.RightButton)
                console.log("右键点击")
        }
    }

    Text {
        anchors.centerIn: parent
        text: clickArea.pressed ? "按住中..." : "点击我"
        color: "white"
        font.pixelSize: 16
    }
}

5.2 鼠标悬停效果

Rectangle {
    id: card
    width: 180; height: 100
    radius: 10
    color: "#f5f5f5"
    border.width: 1
    border.color: hoverArea.containsMouse ? "#4A90E2" : "#e0e0e0"
    scale: hoverArea.containsMouse ? 1.03 : 1.0     // 悬停时轻微放大

    // scale 属性变化自动有过渡效果(需配合 Behavior,见后续课程)

    MouseArea {
        id: hoverArea
        anchors.fill: parent
        hoverEnabled: true    // 必须启用才能检测 containsMouse
    }

    Text {
        anchors.centerIn: parent
        text: "悬停查看效果"
        color: hoverArea.containsMouse ? "#4A90E2" : "#666"
        font.pixelSize: 14
    }
}

六、使用 JavaScript 扩展逻辑

QML 原生支持 JavaScript,可以直接在属性绑定和信号处理器中写逻辑,也可以定义函数。

6.1 内联 JavaScript

import QtQuick

Rectangle {
    width: 300; height: 200

    property int score: 75

    Text {
        anchors.centerIn: parent
        font.pixelSize: 18

        // 三元表达式
        text: score >= 90 ? "优秀"
            : score >= 75 ? "良好"
            : score >= 60 ? "及格"
            :                "不及格"

        color: score >= 75 ? "#1D9E75" : "#E24A4A"
    }
}

6.2 定义函数

import QtQuick
import QtQuick.Controls

ApplicationWindow {
    width: 360; height: 300
    visible: true

    property real celsius: 0

    // 在 Item 内定义函数
    function celsiusToFahrenheit(c) {
        return (c * 9 / 5 + 32).toFixed(1)
    }

    function getTemperatureLabel(c) {
        if (c < 0)   return "冰点以下"
        if (c < 15)  return "寒冷"
        if (c < 25)  return "舒适"
        if (c < 35)  return "温热"
        return "炎热"
    }

    Column {
        anchors.centerIn: parent
        spacing: 16
        width: 280

        Slider {
            width: parent.width
            from: -20; to: 50; value: 0
            onValueChanged: celsius = value
        }

        Text {
            anchors.horizontalCenter: parent.horizontalCenter
            text: celsius.toFixed(1) + "°C  =  " + celsiusToFahrenheit(celsius) + "°F"
            font.pixelSize: 22
            font.bold: true
        }

        Text {
            anchors.horizontalCenter: parent.horizontalCenter
            text: getTemperatureLabel(celsius)
            font.pixelSize: 16
            color: "#888"
        }
    }
}

6.3 将 JS 逻辑抽离到独立文件

当 JavaScript 逻辑较复杂时,可以放入独立的 .js 文件:

新建 utils.js

// utils.js
.pragma library    // 声明为共享库,多个 QML 文件导入时只加载一次

function formatNumber(num) {
    return num.toLocaleString()
}

function clamp(value, min, max) {
    return Math.max(min, Math.min(max, value))
}

在 QML 中导入使用:

import "utils.js" as Utils

Text {
    text: Utils.formatNumber(1234567)    // 输出:1,234,567
}

七、创建自定义组件

7.1 提取独立组件文件

把一个"卡片"封装成可复用的组件 InfoCard.qml

// InfoCard.qml
import QtQuick

Rectangle {
    id: root

    // 对外暴露的属性接口
    property string title: "标题"
    property string subtitle: "副标题"
    property color accentColor: "#4A90E2"

    // 对外暴露的信号
    signal tapped()

    width: 240
    height: 90
    radius: 12
    color: "#ffffff"
    border.width: 1
    border.color: "#e8e8e8"

    // 左侧色条
    Rectangle {
        width: 4
        height: parent.height
        radius: 2
        color: root.accentColor
    }

    Column {
        anchors {
            left: parent.left
            leftMargin: 20
            verticalCenter: parent.verticalCenter
        }
        spacing: 4

        Text {
            text: root.title
            font.pixelSize: 16
            font.bold: true
            color: "#222"
        }

        Text {
            text: root.subtitle
            font.pixelSize: 13
            color: "#888"
        }
    }

    MouseArea {
        anchors.fill: parent
        onClicked: root.tapped()
    }
}

在主文件中使用:

import QtQuick
import QtQuick.Controls

ApplicationWindow {
    width: 360; height: 400
    visible: true

    Column {
        anchors.centerIn: parent
        spacing: 12

        InfoCard {
            title: "今日步数"
            subtitle: "8,432 步"
            accentColor: "#1D9E75"
            onTapped: console.log("点击了步数卡片")
        }

        InfoCard {
            title: "活跃时间"
            subtitle: "47 分钟"
            accentColor: "#4A90E2"
            onTapped: console.log("点击了时间卡片")
        }

        InfoCard {
            title: "消耗热量"
            subtitle: "312 千卡"
            accentColor: "#E2934A"
            onTapped: console.log("点击了热量卡片")
        }
    }
}

7.2 组件的属性别名(alias)

alias 让外部可以直接访问组件内部某个子元素的属性:

// SearchBar.qml
import QtQuick
import QtQuick.Controls

Rectangle {
    id: root
    height: 44
    radius: 22
    color: "#f5f5f5"
    border.width: 1
    border.color: "#e0e0e0"

    // alias 将内部 textField  text 属性暴露出去
    property alias searchText: textField.text
    property alias placeholderText: textField.placeholderText

    signal searchSubmitted(string query)

    TextField {
        id: textField
        anchors {
            left: parent.left
            right: submitBtn.left
            verticalCenter: parent.verticalCenter
            leftMargin: 16; rightMargin: 8
        }
        background: Item {}     // 去掉默认背景
        placeholderText: "搜索..."
        onAccepted: root.searchSubmitted(text)
    }

    Button {
        id: submitBtn
        anchors {
            right: parent.right
            verticalCenter: parent.verticalCenter
            rightMargin: 8
        }
        text: "搜索"
        flat: true
        onClicked: root.searchSubmitted(textField.text)
    }
}

使用时直接访问 searchText

SearchBar {
    id: bar
    width: 300
    onSearchSubmitted: function(query) {
        console.log("搜索:" + query)
    }
}

Text {
    text: "当前输入:" + bar.searchText    // 通过 alias 访问内部属性
}

八、综合示例:个人资料卡片

整合本文所有知识点,构建一个完整的个人资料展示组件:

// ProfileCard.qml
import QtQuick
import QtQuick.Controls

Rectangle {
    id: root

    property string avatarSource: ""
    property string name: "用户名"
    property string bio: "个人简介"
    property int followerCount: 0
    property int followingCount: 0

    signal followClicked()

    width: 320
    height: 200
    radius: 16
    color: "#ffffff"
    border.width: 0.5
    border.color: "#e8e8e8"

    // 顶部背景条
    Rectangle {
        width: parent.width
        height: 70
        color: "#4A90E2"
        radius: 16

        // 修复底部圆角
        Rectangle {
            width: parent.width
            height: 16
            anchors.bottom: parent.bottom
            color: parent.color
        }
    }

    // 头像
    Rectangle {
        id: avatarFrame
        width: 64; height: 64
        radius: 32
        color: "#e0e0e0"
        border.width: 3
        border.color: "white"
        anchors {
            left: parent.left
            leftMargin: 20
            top: parent.top
            topMargin: 38
        }

        Image {
            anchors.fill: parent
            anchors.margins: 2
            source: root.avatarSource
            fillMode: Image.PreserveAspectCrop
            layer.enabled: true
            layer.effect: null
        }

        // 无头像时显示首字母
        Text {
            anchors.centerIn: parent
            text: root.name.length > 0 ? root.name[0].toUpperCase() : "?"
            font.pixelSize: 24
            font.bold: true
            color: "#888"
            visible: root.avatarSource === ""
        }
    }

    // 关注按钮
    Button {
        text: "关注"
        anchors {
            right: parent.right
            rightMargin: 16
            top: parent.top
            topMargin: 80
        }
        onClicked: root.followClicked()
    }

    // 名字和简介
    Column {
        anchors {
            left: parent.left
            leftMargin: 20
            top: avatarFrame.bottom
            topMargin: 8
        }
        spacing: 2

        Text {
            text: root.name
            font.pixelSize: 16
            font.bold: true
            color: "#222"
        }

        Text {
            text: root.bio
            font.pixelSize: 13
            color: "#888"
            width: 200
            elide: Text.ElideRight
        }
    }

    // 粉丝数据
    Row {
        anchors {
            right: parent.right
            rightMargin: 20
            bottom: parent.bottom
            bottomMargin: 14
        }
        spacing: 16

        Column {
            horizontalItemAlignment: Qt.AlignHCenter
            Text {
                anchors.horizontalCenter: parent.horizontalCenter
                text: root.followerCount
                font.pixelSize: 15
                font.bold: true
                color: "#222"
            }
            Text {
                text: "粉丝"
                font.pixelSize: 11
                color: "#aaa"
            }
        }

        Column {
            horizontalItemAlignment: Qt.AlignHCenter
            Text {
                anchors.horizontalCenter: parent.horizontalCenter
                text: root.followingCount
                font.pixelSize: 15
                font.bold: true
                color: "#222"
            }
            Text {
                text: "关注"
                font.pixelSize: 11
                color: "#aaa"
            }
        }
    }
}

在主文件中使用:

import QtQuick
import QtQuick.Controls

ApplicationWindow {
    width: 400; height: 300
    visible: true

    ProfileCard {
        anchors.centerIn: parent
        name: "林小明"
        bio: "Qt 开发爱好者 · 嵌入式工程师"
        followerCount: 1248
        followingCount: 362
        onFollowClicked: console.log("点击关注")
    }
}

九、下一步

掌握了 Qt Quick 的核心视觉元素之后,建议继续:

  1. Introduction to Qt Quick Controls — 学习更完整的 UI 控件库(按钮、输入框、菜单、对话框等)
  2. Positioners and Layouts — 深入布局系统,让 UI 自适应不同屏幕尺寸
  3. QML Fluid Elements and Animation — 为界面加入流畅动画

总结

元素 / 概念用途
Item所有视觉元素的基类,提供位置、透明度、旋转等基础属性
Rectangle矩形容器,支持圆角、边框、渐变
Text文本显示,支持富文本、换行、省略
Image图片显示,支持多种缩放模式和异步加载
FontLoader加载自定义字体文件
anchors锚点定位系统,通过边对齐实现灵活布局
MouseArea处理鼠标和触控事件
JavaScript 函数在 QML 中定义逻辑函数,复杂逻辑可抽离到 .js 文件
自定义组件独立 .qml 文件封装,property 暴露接口,alias 透传内部属性