QML 与 JavaScript 交互方式:内联函数、外部文件、信号槽与工作线程

QML代码用于描述界面,JavaScript用于处理逻辑。这篇文章用四个 demo 讲清 QML 里调用 JS 的四种常见方式。

  • 内联 JS 函数 --- 直接在 QML 中定义函数,属性绑定和信号槽都能调用
  • 导入外部 JS 文件 --- 把通用逻辑抽到 .js 文件,多个 QML 文件共享
  • JS 函数作为信号槽 --- 用 Connections 把按钮信号接到 JS 函数
  • WorkerScript 工作线程 --- 把耗时计算放到独立线程,避免阻塞主 UI

Demo 1 内联 JS 函数

在 QML 根元素上直接定义函数,既能在属性绑定里用,也能在信号处理器里调用。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts

FadeInAnimation {
    function calculateArea(w, h) {
        return w * h
    }

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15

        // ... 省略标题组件 TitleSeparator ...

        Text {
            text: "计算面积(属性绑定): " + calculateArea(this.width, this.height)
            font.pointSize: 12
        }

        Button {
            text: "点击计算(在槽函数中调用)"
            font.pointSize: 12
            onClicked: {
                var result = calculateArea(100, 100)
                textResult.text = "Area: " + result
            }
        }

        Text {
            id: textResult
            font.pointSize: 10
        }

        Item { Layout.fillHeight: true }
    }
}

关键逻辑解析

内联函数写在 FadeInAnimation 根元素上,作用域覆盖整个 QML 文件。

  • 属性绑定中调用:text: "..." + calculateArea(this.width, this.height),界面尺寸变化时结果自动刷新。
  • 信号槽中调用:onClicked 里直接 calculateArea(100, 100),和写普通 JS 没区别。

适合把一段小逻辑封装起来复用,比如格式化显示、简单计算。逻辑复杂或需要跨文件共享时,就适合拆成外部 JS 文件。

Demo 2 导入外部 JS 文件

把日期格式化、字符串处理这类通用函数抽到 Utility.js,通过 import "Utility.js" as Util 引入。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts
import "Utility.js" as Util

FadeInAnimation {
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 20

        // ... 省略标题组件 TitleSeparator ...

        Text {
            text: "调用JS接口(formatDate): \n" + Util.formatDate(new Date())
            font.pointSize: 12
        }

        Button {
            text: "调用JS接口(toUpperCase)"
            font.pointSize: 12
            onClicked: {
                textResult.text = Util.toUpperCase(textResult.text)
            }
        }

        Text {
            id: textResult
            text: "   abcdefg   "
            font.pointSize: 10
        }

        Item { Layout.fillHeight: true }
    }
}

Utility.js:

js 复制代码
.pragma library

function formatDate(date) {
    return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate()
}

function toUpperCase(name) {
    return name.trim().toUpperCase()
}

关键逻辑解析

  • .pragma library 必须加,它让所有 QML 实例共享同一个 JS 上下文。如果不加,每个引用文件都会有独立的一份状态。
  • import "Utility.js" as Util 给模块起别名,调用时用 Util.xxx()。
  • 外部 JS 文件适合放工具函数、常量、数据转换逻辑,业务层 QML 文件只负责调用。

Demo 3 JS 函数作为信号槽

QML 里普通 JS 函数可以直接当槽用,配合 Connections 把信号和函数连起来。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts

FadeInAnimation {
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15

        // ... 省略标题组件 TitleSeparator ...

        Text {
            text: "定义一个 JS 函数作为槽"
            font.pointSize: 12
        }

        Button {
            id: myButton
            text: "触发信号"
            font.pointSize: 12
        }

        Text {
            property int counter: 0

            id: myText
            text: "Counter: " + counter
            font.pointSize: 12

            function handleButtonClick() {
                myText.counter++
            }

            Connections {
                target: myButton
                function onClicked() { myText.handleButtonClick() }
            }
        }

        Item { Layout.fillHeight: true }
    }
}

关键逻辑解析

  • handleButtonClick() 是定义在 Text 上的普通 JS 函数。
  • Connections 的 target 指向 myButton,内部的 onClicked 会在按钮点击时触发。
  • 这种方式把信号源和响应逻辑解耦:按钮不知道 Text 的存在,Text 只负责更新自己。

和直接在 Button 的 onClicked 里改 myText.counter 相比,Connections 更适合跨组件、一对多的信号处理。

Demo 4 WorkerScript 工作线程

计算斐波那契数列这类递归任务会阻塞主 UI,用 WorkerScript 把它扔到独立线程。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts
import QtQml.WorkerScript

FadeInAnimation {
    property bool isProcessing: false
    property string resultText: ""

    WorkerScript {
        id: myWorker
        source: "script.mjs"

        onMessage: function (msgObj) {
            isProcessing = false
            if (msgObj.error) {
                resultText = "错误: " + msgObj.error
            } else {
                resultText = "计算结果: " + msgObj.result +
                        "\n耗时: " + msgObj.time + " ms" +
                        "\n任务ID: " + msgObj.taskId +
                        "\n原始消息: " + JSON.stringify(msgObj.originalMessage)
            }
        }
    }

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15

        // ... 省略标题组件 TitleSeparator ...

        Row {
            spacing: 15
            Button {
                id: btnCalc
                text: "计算斐波那契数列"
                font.pointSize: 12
                enabled: !isProcessing && myWorker.ready
                onClicked: {
                    isProcessing = true
                    resultText = "计算斐波那契数列 ..."
                    myWorker.sendMessage({
                        'taskId': 1001,
                        'n': textField.text,
                        'timestamp': Date.now()
                    })
                }
            }

            TextField {
                id: textField
                text: "36"
                width: 60
                height: btnCalc.height
                placeholderText: "请输入数值"
                font.pointSize: 10
            }
        }

        Text {
            text: resultText
            font.pointSize: 10
        }

        Item { Layout.fillHeight: true }
    }
}

script.mjs:

js 复制代码
function fibonacci(n) {
    function fib(n) {
        if (n <= 1) return n
        return fib(n - 1) + fib(n - 2)
    }
    return fib(n)
}

WorkerScript.onMessage = function(message) {
    var startTime = Date.now()
    var result = fibonacci(message.n || 30)
    var processingTime = Date.now() - startTime

    WorkerScript.sendMessage({
        result: result,
        time: processingTime,
        taskId: message.taskId,
        originalMessage: message
    })
}

关键逻辑解析

  • WorkerScript 的 source 指向 .mjs 脚本,该脚本在独立线程中运行。
  • 主线程通过 sendMessage() 发数据,工作线程通过 WorkerScript.onMessage 接收并处理。
  • 工作线程处理完后用 WorkerScript.sendMessage() 把结果发回主线程,主线程在 onMessage 里更新界面。
  • enabled: !isProcessing && myWorker.ready 防止重复提交和线程未就绪时点击。

注意:WorkerScript 里不能访问 QML 组件和主线程变量,只能通过消息对象传递数据。

对比表格

场景 推荐方式 原因
单文件内的小段逻辑 内联 JS 函数 写法最直接,和 QML 融为一体
多文件共享工具函数 外部 .js 文件 + .pragma library 一处维护,全局共享状态
跨组件信号响应 JS 函数 + Connections 发送端和接收端解耦
耗时计算、大数据处理 WorkerScript 不阻塞主 UI 线程

运行验证

  1. Qt Creator 打开 qml_js_interaction/CMakeLists.txt;
  2. 按 Ctrl+R 运行;
  3. 分别点击四个 demo 的按钮,观察面积计算、字符串转换、计数器和斐波那契结果。

扩展复用方向

  • 把内联的面积计算函数改成通用坐标转换函数,供多个控件调用。
  • 外部 JS 文件里增加网络请求参数拼接、日期格式化等工具函数。
  • 用 Connections 把多个按钮的信号接到同一个槽函数,实现统一的事件分发。
  • WorkerScript 里做图片解码、文件解析、排序等耗时任务,主线程只做进度展示。

已验证环境:

相关推荐
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
李少兄3 天前
JavaScript 隐式全局变量解析
javascript
慧都小项3 天前
当边缘AI上产线:QtitanDocking 如何让工业 HMI 实现多视图协同
人工智能·qt·ui·边缘计算·qt6.3
沙蒿同学3 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端
paopaokaka_luck3 天前
智慧社区综合服务小程序(人脸识别、AI问答、Echarts图形化分析)
前端·javascript·spring boot·spring·数据分析·echarts
Hilaku3 天前
为什么死磕 1px 的团队,用户体验反而更差?
前端·javascript·程序员
Quz3 天前
QML SwipeDelegate 滑动委托
qt
陈拉斯3 天前
给公司年会写了个大屏抽奖系统:动画在前端跑,凭什么说结果没被改?
javascript
Quz3 天前
QML 列表中的四种委托:ItemDelegate、CheckDelegate、RadioDelegate、SwitchDelegate
qt