Calqlatr

A calculator designed for landscape and portrait devices. It uses Qt Quick custom components and responsive layouts for the user interface, and JavaScript for the application logic.

The calculator example UI

Calqlatr demonstrates various QML and Qt Quick features, such as displaying custom components and using responsive layouts. The application logic is implemented in JavaScript and the UI is implemented in QML.

Running the Example

To run the example from Qt Creator, open the Welcome mode and select the example from Examples. For more information, see Qt Creator: Tutorial: Build and run. After running the example, you should be able to use the application as a standard calculator. Change the phone from portrait to landscape mode - or, on the desktop, resize the main window - to activate the responsive layout.

Using Custom Components

The Calqlatr application uses custom types. Most of them are defined in their own separate .qml file:

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

Main.qml contains the top-level window and root item. It makes use of the other custom types in content directory. For example, the NumberPad type (defined by content/NumberPad.qml) is used in Main.qml to create the number pad of the calculator:

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

            isPortraitMode: root.isPortraitMode
            state: state
        }

Inline components allow you to declare multiple components inside of a .qml file. The example uses inline components in NumberPad.qml to define two components, DigitButton and OperatorButton:

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

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

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

Both components are of type CalculatorButton (as defined in CalculatorButton.qml), but provide a custom handler for the clicked signal, and tweak some property defaults. DigitButton and OperatorButton are then instantiated later on in NumberPad.qml:

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

Check out Defining Object Types through QML Documents for more details on defining custom QML components.

Responsive Layouts

In this example, responsive layouts arrange the different UI components for both portrait and landscape modes. You can notice this in Main.qml, which defines a ColumnLayout for portrait mode, and RowLayout for landscape:

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

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

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

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

The ColumnLayout represents the portrait layout for the application, and the RowLayout represents the landscape layout. The visible properties handle which layout is used at a given time. The id attribute of the NumberPad and Display components are used to set the target property of the LayoutItemProxy types. This allows both layouts to use the same content items. In addition, properties can be forwarded within the LayoutItemProxy item to the target itself. For example, when the NumberPad is instantiated, both layouts require a different Layout.alignment.

A responsive layout is also used in NumberPad.qml when defining the portrait and landscape layout for the NumberPad itself:

        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.state.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

Depending on controller.isPortraitMode, the number pad contains only the buttons of a simple calculator (mainGrid), or also some more advanced functionality via the scientificGrid.

Animating Button Text Colors

In the CalculatorButton.qml file, the text colors of the number pad buttons are also animated.

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

The color changes are animated by defining a Behavior on the color property. When a button is set to dimmed = true, the button appears darker. When a button is pressed, it lights up green. In order to dynamically change the dimmed property of all the buttons on the NumberPad, the buttonPressed signal calls the NumberPad's updateDimmed() function.

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

Performing Calculations

The calculator.js and ApplicationState.qml files define the calculator's engine. calculator.js contains the logical state of the calculator, as well as operations to change the state. ApplicationState.qml exposes this API through a QML type.

Let's first have a look at calculator.js:

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

accumulator, pendingOperator, lastButton, represent the logical state of the calculator.

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

isOperationDisabled() returns true if an operation op should be disabled, based on the current engine and display state. Otherwise, it returns false.

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

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

digitPressed() and operatorPressed() functions update the engine and display state based on the pressed buttons.

ApplicationState.qml is exposing the JavaScript functions defined in calculator.js behind a nicer QML API:

import QtQml
import "calculator.js" as CalcEngine

calculator.js is imported under the name 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 makes sure that an instance of Display.qml is always available in the ApplicationState under the name display. display is in turn used in the operatorPressed(), digitPressed() and isButtonDisabled() functions.

Source Files

Squish GUI testing

This application comes with Squish GUI tests targeted for Qt for Android. The tests are made with Squish for Qt and are written in Python and can be found under the application test directory.

When using Squish for Qt for Android, make sure to use the Qt built-in hook and forward Squish port with adb. If you are getting the error "Cannot load library" with dlopen failing, disable QT_USE_TARGET_ANDROID_BUILD_DIR from CMake configuration, or clear it from Projects->Build Settings->CMake->Current Configuration in Qt Creator.

If you don't have a Squish license, you can get a free trial.

Example project @ code.qt.io

See also QML Applications.

© 2025 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.