本页内容

Calqlatr

一款专为横屏和竖屏设备设计的计算器。其用户界面采用Qt Quick 自定义组件和响应式布局,应用逻辑则由JavaScript实现。

计算器示例的用户界面

Calqlatr展示了各种 QML 和 Qt Quick 功能,例如显示自定义组件和使用响应式布局。应用程序逻辑采用 JavaScript 实现,用户界面则采用 QML 实现。

运行示例

您可以通过以下方式运行该示例:

运行示例后,您应该能够像使用标准计算器一样使用该应用程序。将手机从纵向模式切换为横向模式——或者在桌面端调整主窗口大小——以激活自适应布局。

使用自定义组件

Calqlatr应用程序使用了自定义类型。其中大部分在各自独立的 .qml 文件中定义:

  • Main.qml
  • content/ApplicationState.qml
  • content/BackspaceButton.qml
  • content/CalculatorButton.qml
  • content/Display.qml
  • content/NumberPad.qml

Main.qml 包含顶级窗口和根项。它使用了content 目录中的其他自定义类型。例如,NumberPad 类型(由content/NumberPad.qml 定义)在Main.qml 中用于创建计算器的数字键盘:

        NumberPad {
            id: numberPad
            Layout.margins: root.margin

            isPortraitMode: root.isPortraitMode
            applicationState: appState
        }

内联组件允许您在.qml 文件内声明多个组件。该示例在NumberPad.qml 中使用内联组件定义了两个组件:DigitButton 和OperatorButton :

    component DigitButton: CalculatorButton {
        onClicked: {
            controller.applicationState.digitPressed(text);
            controller.updateDimmed();
        }
    }

    component OperatorButton: CalculatorButton {
        dimmable: true
        implicitWidth: 48
        textColor: controller.qtGreenColor

        onClicked: {
            controller.applicationState.operatorPressed(text);
            controller.updateDimmed();
        }
    }

这两个组件的类型均为CalculatorButton (如CalculatorButton.qml 中所定义),但为 clicked 信号提供了自定义处理程序,并调整了一些属性的默认值。随后在NumberPad.qml 中实例化了DigitButton 和OperatorButton :

                DigitButton {
                    text: "e"
                    dimmable: true
                    implicitWidth: 48
                }
                OperatorButton { text: "ln" }

有关定义自定义 QML 组件的更多详细信息,请参阅《通过 QML 文档定义对象类型》。

响应式布局

在此示例中,响应式布局会根据纵向和横向模式调整不同 UI 组件的排列方式。可在Main.qml 中看到这一点,该文件定义了纵向模式下的ColumnLayout 以及横向模式下的RowLayout :

        ColumnLayout {
            id: portraitMode
            anchors.fill: parent
            visible: root.isPortraitMode

            LayoutItemProxy {
                target: calcDisplay
                Layout.minimumHeight: calcDisplay.minHeight
            }
            LayoutItemProxy {
                target: numberPad
                Layout.alignment: Qt.AlignHCenter
            }
        }

        RowLayout {
            id: landscapeMode
            anchors.fill: parent
            visible: !root.isPortraitMode

            LayoutItemProxy {
                target: calcDisplay
            }
            LayoutItemProxy {
                target: numberPad
                Layout.alignment: Qt.AlignVCenter
            }
        }

ColumnLayout 代表应用程序的纵向布局,RowLayout 代表横向布局。visible 属性负责在特定时刻决定使用哪种布局。NumberPad 和Display 组件的id属性用于设置LayoutItemProxy 类型的target 属性。这使得两种布局都能使用相同的内容项。此外,可以在LayoutItemProxy 项内将属性转发给target 本身。例如,当NumberPad 被实例化时,两种布局都需要不同的Layout.alignment 。

在NumberPad.qml 中,定义NumberPad 本身的纵向和横向布局时,也使用了响应式布局:

        RowLayout {
            spacing: controller.spacing

            GridLayout {
                id: scientificGrid
                columns: 3
                columnSpacing: controller.spacing
                rowSpacing: controller.spacing
                visible: !controller.isPortraitMode

                OperatorButton {
                    text: "x²"
                    Accessible.name: "x squared"
                }
                OperatorButton {
                    text: "⅟x"
                    Accessible.name: "one over x"
                }
                OperatorButton { text: "√" }
                OperatorButton {
                    text: "x³"
                    Accessible.name: "x cubed"
                }
                OperatorButton {
                    text: "sin"
                    Accessible.name: "sine"
                }
                OperatorButton {
                    text: "|x|"
                    Accessible.name: "absolute value"
                }
                OperatorButton { text: "log" }
                OperatorButton {
                    text: "cos"
                    Accessible.name: "cosine"
                }
                DigitButton {
                    text: "e"
                    dimmable: true
                    implicitWidth: 48
                }
                OperatorButton { text: "ln" }
                OperatorButton { text: "tan" }
                DigitButton {
                    text: "π"
                    dimmable: true
                    implicitWidth: 48
                }
            }

            GridLayout {
                id: mainGrid
                columns: 5
                columnSpacing: controller.spacing
                rowSpacing: controller.spacing

                BackspaceButton {
                    onClicked: {
                        controller.applicationState.operatorPressed(this.text);
                        controller.updateDimmed();
                    }
                }

                DigitButton { text: "7" }
                DigitButton { text: "8" }
                DigitButton { text: "9" }
                OperatorButton {
                    text: "÷"
                    implicitWidth: 38
                }

                OperatorButton {
                    text: "AC"
                    textColor: controller.backspaceRedColor
                    accentColor: controller.backspaceRedColor
                }
                DigitButton { text: "4" }
                DigitButton { text: "5" }
                DigitButton { text: "6" }
                OperatorButton {
                    text: "×"
                    implicitWidth: 38
                }

                OperatorButton {
                    text: "="
                    implicitHeight: 81
                    Layout.rowSpan: 2
                }
                DigitButton { text: "1" }
                DigitButton { text: "2" }
                DigitButton { text: "3" }
                OperatorButton {
                    text: "−"
                    implicitWidth: 38
                }

                OperatorButton {
                    text: "±"
                    implicitWidth: 38
                }
                DigitButton { text: "0" }
                DigitButton {
                    text: "."
                    dimmable: true
                }
                OperatorButton {
                    text: "+"
                    implicitWidth: 38
                }
            }
        } // RowLayout

根据controller.isPortraitMode 的不同,数字键盘可能仅包含简单计算器(mainGrid )的按钮,也可能通过scientificGrid 提供一些更高级的功能。

按钮文字颜色的动画效果

在CalculatorButton.qml 文件中,数字小键盘按钮的文字颜色也会进行动画效果。

        ...
        color: button.getTextColor()
        Behavior on color {
            ColorAnimation {
                duration: 120
                easing.type: Easing.OutElastic
            }
        }
    }

通过为color 属性定义Behavior 来实现颜色变化的动画效果。当按钮设置为dimmed = true 时,按钮会显示为较深的颜色;按下按钮时,按钮会亮起绿色。为了动态更改NumberPad 上所有按钮的dimmed 属性,buttonPressed 信号会调用NumberPad 的updateDimmed() 函数。

    function updateDimmed() {
        for (let i = 0; i < mainGrid.children.length; i++) {
            mainGrid.children[i].dimmed = applicationState.isButtonDisabled(mainGrid.children[i].text);
        }
        for (let j = 0; j < scientificGrid.children.length; j++) {
            scientificGrid.children[j].dimmed = applicationState.isButtonDisabled(
                        scientificGrid.children[j].text);
        }
    }

执行计算

calculator.js 和ApplicationState.qml 文件定义了计算器的核心引擎。calculator.js 包含计算器的逻辑状态,以及用于更改状态的操作。ApplicationState.qml 通过一个QML类型暴露了该API。

让我们先来看看 `calculator.js`:

let accumulator = 0
let pendingOperator = ""
let lastButton = ""
let digits = ""

accumulator,pendingOperator,lastButton,represent 计算器的逻辑状态。

function isOperationDisabled(op, display) {
    ...
}

isOperationDisabled() 根据当前引擎和显示状态,如果应禁用某项运算(op ),则返回true ;否则,返回false 。

function digitPressed(op, display) {
    ...
}

function operatorPressed(op, display) {
    ...
}

digitPressed() 而operatorPressed() 函数会根据按下的按钮更新引擎和显示状态。

ApplicationState.qml 通过更友好的 QML API 暴露了在calculator.js 中定义的 JavaScript 函数:

import QtQml
import "calculator.js" as CalcEngine

calculator.js 该功能以CalcEngine 为名称被导入。

QtObject {
    required property Display display

    function operatorPressed(operator) {
        CalcEngine.operatorPressed(operator, display);
    }
    function digitPressed(digit) {
        CalcEngine.digitPressed(digit, display);
    }
    function isButtonDisabled(op) {
        return CalcEngine.isOperationDisabled(op, display);
    }
}

required property 确保在ApplicationState 中始终可以通过名称display 访问Display.qml 的实例。display 随后被用于operatorPressed() 、digitPressed() 和isButtonDisabled() 函数中。

源文件

Squish GUI 测试

该应用程序附带针对 Qt for Android 的Squish GUI 测试。这些测试使用Squish for Qt生成,采用 Python 编写,位于应用程序的 test 目录下。

在使用 Squish for Qt for Android 时,请确保使用Qt 内置的钩子,并通过 adb转发 Squish 端口。如果运行dlopen 时出现“无法加载库”的错误,请在 CMake 配置中禁用QT_USE_TARGET_ANDROID_BUILD_DIR ,或从Qt Creator 中的Projects->Build Settings->CMake->Current Configuration 文件中将其删除。

如果您没有 Squish 许可证,可以申请免费试用。

示例项目 @ code.qt.io

另请参阅 QML 应用程序。

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.