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 函数。
  • Connectionstarget 指向 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
    })
}

关键逻辑解析

  • WorkerScriptsource 指向 .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 里做图片解码、文件解析、排序等耗时任务,主线程只做进度展示。

已验证环境

相关推荐
Quz1 小时前
QML ToolTip 组件:延迟提示与自定义外观
qt
Allshadow1 小时前
Nest.js - 连接数据库
javascript·nestjs
冰暮流星1 小时前
javascript之date对象
开发语言·javascript·ecmascript
橘子星1 小时前
别光看教程!手把手拆解 React Todo 项目,一文吃透 5 个核心概念
前端·javascript
wWYy.1 小时前
RPC详解
qt·rpc·php
蜡台2 小时前
使用 uni-popup 实现数据选择器Data-Picker
前端·javascript·html·uniapp·uni-popup·data-picker
blns_yxl2 小时前
Promise封装Fetch + 重试机制(HTML+JS)
前端·javascript·html
无巧不成书02182 小时前
Vue+Vue-CLI全平台零基础搭建教程
前端·javascript·vue.js·vue·前端开发·vue-cli·前端环境搭建