Programming/qt2026. 1. 13. 14:39

qt quick 으로 작성된 계산기 어플 소스 분석

 

실행하면 평범한 계산기가 뜬다.

 

가로로 길게 늘리면 공학계산기로 바뀐다.

 

프로젝트는 아래와 같이 구성되어 있고

 

main.cpp

언제나 그렇듯(?) main.cpp 는 조촐하고 별 내용이 없다.

// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickStyle>

int main(int argc, char *argv[])
{
    QCoreApplication::setOrganizationName("QtProject");
    QCoreApplication::setApplicationName("Calqlatr");

    QGuiApplication app(argc, argv);

    QQuickStyle::setStyle("Basic");

    QQmlApplicationEngine engine;
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
            &app, []() { QCoreApplication::exit(-1); },
            Qt::QueuedConnection);
    engine.loadFromModule("demos.calqlatr", "Main");

    return app.exec();
}

 

Main.qml

전체 모양을 그리는 녀석인데, design 에서 보면 아래처럼 보인다.

 

다만 qt designer의 버그인지 ApplicationState 의 id: state 구문이 에러가 나는데

위지윅 에디터에서 보려면 해당 라인을 주석처리 하면 된다. (물론 실행하면 작동안하게 되는 버그 발생)

 

 

column, row Layout 으로 배치를 어떻게 하는것 같고

Keys.onPressed: function (event) {} 를 통해서 키 입력시 state에 추가하는 식으로 작동하게 된다.

// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Layouts

Window {
    visible: true
    width: 320
    height: 480
    minimumWidth: Math.max(numberPad.portraitModeWidth, display.minWidth) + root.margin * 2
    minimumHeight: display.minHeight + numberPad.height + root.margin * 3
    color: root.backgroundColor

    Item {
        id: root
        anchors.fill: parent

        anchors.topMargin: parent.SafeArea.margins.top
        anchors.leftMargin: parent.SafeArea.margins.left
        anchors.rightMargin: parent.SafeArea.margins.right
        anchors.bottomMargin: parent.SafeArea.margins.bottom

        readonly property int margin: 18
        readonly property color backgroundColor: "#222222"
        readonly property int minLandscapeModeWidth: numberPad.landscapeModeWidth
                                                     + display.minWidth + margin * 3

        property bool isPortraitMode: root.width < root.minLandscapeModeWidth

        ApplicationState {
            id: state
            display: display
        }

        Display {
            id: display
            readonly property int minWidth: 210
            readonly property int minHeight: 60

            Layout.minimumWidth: minWidth
            Layout.fillWidth: true
            Layout.fillHeight: true
            Layout.margins: root.margin

            // remove the margin on the side that the numberPad is on, to prevent a double margin
            Layout.bottomMargin: root.isPortraitMode ? 0 : root.margin
            Layout.rightMargin: root.isPortraitMode ? root.margin : 0
        }

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

            isPortraitMode: root.isPortraitMode
            state: state
        }

        // define the responsive layouts
        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
            }
        }

        Keys.onPressed: function (event) {
            switch (event.key) {
                case Qt.Key_0: state.digitPressed("0"); break;
                case Qt.Key_1: state.digitPressed("1"); break;
                case Qt.Key_2: state.digitPressed("2"); break;
                case Qt.Key_3: state.digitPressed("3"); break;
                case Qt.Key_4: state.digitPressed("4"); break;
                case Qt.Key_5: state.digitPressed("5"); break;
                case Qt.Key_6: state.digitPressed("6"); break;
                case Qt.Key_7: state.digitPressed("7"); break;
                case Qt.Key_8: state.digitPressed("8"); break;
                case Qt.Key_9: state.digitPressed("9"); break;
                case Qt.Key_E: state.digitPressed("e"); break;
                case Qt.Key_P: state.digitPressed("π"); break;
                case Qt.Key_Plus: state.operatorPressed("+"); break;
                case Qt.Key_Minus: state.operatorPressed("-"); break;
                case Qt.Key_Asterisk: state.operatorPressed("×"); break;
                case Qt.Key_Slash: state.operatorPressed("÷"); break;
                case Qt.Key_Enter:
                case Qt.Key_Return: state.operatorPressed("="); break;
                case Qt.Key_Comma:
                case Qt.Key_Period: state.digitPressed("."); break;
                case Qt.Key_Backspace: state.operatorPressed("bs"); break;
            }
        }
    }
}

 

ApplicationState.qml

말로는 qml  인데 design에서 보여지는 요소는 존재하지 않고

main.qml 에서 호출되는 operatorPressed() 나 digitPressed()와 같은 함수가 존재한다.

특이한건 import as 인데 js를 불러서 CalcEngine 으로 사용하는 부분 정도?

그 와중에 Display는 display.qml 에서 끌려오는건가?

// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQml
import "calculator.js" as 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);
    }
}

 

calculator.js

평범한(?) js로 작성된 코드가 존재한다.

// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

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

function isOperationDisabled(op, display) {
    if (digits !== "" && lastButton !== "=" && (op === "π" || op === "e"))
        return true
    if (digits === "" && !((op >= "0" && op <= "9") || op === "π" || op === "e" || op === "AC"))
        return true
    if (op === "bs" && (display.isOperandEmpty() || !((lastButton >= "0" && lastButton <= "9")
                                                      || lastButton === "π" || lastButton === "e" || lastButton === ".")))
        return true
    if (op === '=' && pendingOperator.length != 1)
        return true
    if (op === "." && digits.search(/\./) != -1)
        return true
    if (op === "√" &&  digits.search(/-/) != -1)
        return true
    if (op === "AC" && display.isDisplayEmpty())
        return true

    return false
}

function digitPressed(op, display) {
    if (isOperationDisabled(op, display))
        return
    if (lastButton === "π" || lastButton === "e")
        return
    // handle mathematical constants
    if (op === "π") {
        lastButton = op
        digits = Math.PI.toPrecision(display.maxDigits - 1).toString()
        display.appendDigit(digits)
        return
    }
    if (op === "e") {
        lastButton = op
        digits = Math.E.toPrecision(display.maxDigits - 1).toString()
        display.appendDigit(digits)
        return
    }

    // append a digit to another digit or decimal point
    if (lastButton.toString().length === 1 && ((lastButton >= "0" && lastButton <= "9") || lastButton === ".") ) {
        if (digits.length >= display.maxDigits)
            return
        digits = digits + op.toString()
        display.appendDigit(op.toString())
    // else just write a single digit to display
    } else {
        digits = op.toString()
        display.appendDigit(digits)
    }
    lastButton = op
}

function operatorPressed(op, display) {
    if (isOperationDisabled(op, display))
        return

    if (op === "±") {
        digits = Number(digits.valueOf() * -1).toString()
        display.setDigit(display.displayNumber(Number(digits)))
        return
    }

    if (op === "bs") {
        digits = digits.slice(0, -1)
        if (digits === "-")
            digits = ""
        display.backspace()
        return
    }

    lastButton = op

    if (pendingOperator === "+") {
        digits = (Number(accumulator) + Number(digits.valueOf())).toString()
    } else if (pendingOperator === "−") {
        digits = (Number(accumulator) - Number(digits.valueOf())).toString()
    } else if (pendingOperator === "×") {
        digits = (Number(accumulator) * Number(digits.valueOf())).toString()
    } else if (pendingOperator === "÷") {
        digits = (Number(accumulator) / Number(digits.valueOf())).toString()
    }


    if (op === "+" || op === "−" || op === "×" || op === "÷") {
        pendingOperator = op
        accumulator = digits.valueOf()
        digits = ""
        display.displayOperator(pendingOperator)
        return
    }

    accumulator = 0
    pendingOperator = ""

    if (op === "=") {
        display.newLine("=", Number(digits))
    }

    if (op === "√") {
        digits = (Math.sqrt(digits.valueOf())).toString()
        display.newLine("√", Number(digits))
    } else if (op === "⅟x") {
        digits = (1 / digits.valueOf()).toString()
        display.newLine("⅟x", Number(digits))
    } else if (op === "x²") {
        digits = (digits.valueOf() * digits.valueOf()).toString()
        display.newLine("x²", Number(digits))
    } else if (op === "x³") {
        digits = (digits.valueOf() * digits.valueOf() * digits.valueOf()).toString()
        display.newLine("x³", Number(digits))
    } else if (op === "|x|") {
        digits = (Math.abs(digits.valueOf())).toString()
        display.newLine("|x|", Number(digits))
    } else if (op === "⌊x⌋") {
        digits = (Math.floor(digits.valueOf())).toString()
        display.newLine("⌊x⌋", Number(digits))
    } else if (op === "sin") {
        digits = Number(Math.sin(digits.valueOf())).toString()
        display.newLine("sin", Number(digits))
    } else if (op === "cos") {
        digits = Number(Math.cos(digits.valueOf())).toString()
        display.newLine("cos", Number(digits))
    } else if (op === "tan") {
        digits = Number(Math.tan(digits.valueOf())).toString()
        display.newLine("tan", Number(digits))
    } else if (op === "log") {
        digits = Number(Math.log10(digits.valueOf())).toString()
        display.newLine("log", Number(digits))
    } else if (op === "ln") {
        digits = Number(Math.log(digits.valueOf())).toString()
        display.newLine("ln", Number(digits))
    }

    if (op === "AC") {
        display.allClear()
        accumulator = 0
        lastButton = ""
        digits = ""
        pendingOperator = ""
    }
}

 

Display.qml

design 상에서는 별 내용이 없어 보이지만

 

코드에서는 라인별로 추가하는 등 제법 ui를 건드리는 작동을 많이 한다.

// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

pragma ComponentBehavior: Bound

import QtQuick

Item {
    id: display
    property int fontSize: 22
    readonly property int maxDigits: Math.min((width / fontSize) + 1, 9)
    readonly property color backgroundColor: "#262626"
    readonly property color qtGreenColor: "#2CDE85"
    property string displayedOperand: ""
    readonly property string errorString: qsTr("ERROR")
    readonly property bool isError: displayedOperand === errorString
    property bool enteringDigits: false

    function displayOperator(operator) {
        calculationsListView.model.append({
                                              "operator": operator,
                                              "operand": ""
                                          });
        enteringDigits = true;
        calculationsListView.positionViewAtEnd();
    }

    function newLine(operator, operand) {
        displayedOperand = displayNumber(operand);
        calculationsListView.model.append({
                                              "operator": operator,
                                              "operand": displayedOperand
                                          });
        enteringDigits = false;
        calculationsListView.positionViewAtEnd();
    }

    function appendDigit(digit) {
        if (!enteringDigits)
            calculationsListView.model.append({
                                                  "operator": "",
                                                  "operand": ""
                                              });
        const i = calculationsListView.model.count - 1;
        calculationsListView.model.get(i).operand = calculationsListView.model.get(i).operand
                + digit;
        enteringDigits = true;
        calculationsListView.positionViewAtEnd();
    }

    function setDigit(digit) {
        const i = calculationsListView.model.count - 1;
        calculationsListView.model.get(i).operand = digit;
        calculationsListView.positionViewAtEnd();
    }

    function backspace() {
        const i = calculationsListView.model.count - 1;
        if (i >= 0) {
            let operand = calculationsListView.model.get(i).operand.toString().slice(0, -1);
            if (operand === "-")
                operand = "";
            calculationsListView.model.get(i).operand = operand;
            return;
        }
        return;
    }

    function isOperandEmpty() {
        const i = calculationsListView.model.count - 1;
        return i >= 0 ? calculationsListView.model.get(i).operand === "" : true;
    }

    function isDisplayEmpty() {
        const i = calculationsListView.model.count - 1;
        return i == -1 ? true : (i == 0 ? calculationsListView.model.get(0).operand === "" : false);
    }

    function clear() {
        displayedOperand = "";
        if (enteringDigits) {
            const i = calculationsListView.model.count - 1;
            if (i >= 0)
                calculationsListView.model.remove(i);
            enteringDigits = false;
        }
    }

    function allClear() {
        display.clear();
        calculationsListView.model.clear();
        enteringDigits = false;
    }

    // Returns a string representation of a number that fits in
    // display.maxDigits characters, trying to keep as much precision
    // as possible. If the number cannot be displayed, returns an
    // error string.
    function displayNumber(num) {
        if (typeof (num) !== "number")
            return errorString;

        // deal with the absolute
        const abs = Math.abs(num);

        if (abs.toString().length <= maxDigits) {
            return isFinite(num) ? num.toString() : errorString;
        }

        if (abs < 1) {
            // check if abs < 0.00001, if true, use exponential form
            // if it isn't true, we can round the number without losing
            // too much precision
            if (Math.floor(abs * 100000) === 0) {
                const expVal = num.toExponential(maxDigits - 6).toString();
                if (expVal.length <= maxDigits + 1)
                    return expVal;
            } else {
                // the first two digits are zero and .
                return num.toFixed(maxDigits - 2);
            }
        } else {
            // if the integer part of num is greater than maxDigits characters, use exp form
            const intAbs = Math.floor(abs);
            if (intAbs.toString().length <= maxDigits)
                return parseFloat(num.toPrecision(maxDigits - 1)).toString();

            const expVal = num.toExponential(maxDigits - 6).toString();
            if (expVal.length <= maxDigits + 1)
                return expVal;
        }
        return errorString;
    }

    Item {
        anchors.fill: parent

        Rectangle {
            anchors.fill: parent
            radius: 8
            color: display.backgroundColor

            ListView {
                idcalculationsListView
                x: 5
                y: 10
                width: parent.width
                height: parent.height - 2 * y
                clip: true
                delegate: Item {
                    height: display.fontSize * 1.1
                    width: calculationsListView.width

                    required property string operator
                    required property string operand

                    Text {
                        x: 6
                        font.pixelSize: display.fontSize
                        color: display.qtGreenColor
                        text: parent.operator
                        Accessible.name: parent.operator
                    }
                    Text {
                        font.pixelSize: display.fontSize
                        anchors.right: parent.right
                        anchors.rightMargin: 16
                        text: parent.operand
                        Accessible.name: parent.operand
                        color: "white"
                    }
                }
                model: ListModel {}
                onHeightChanged: positionViewAtEnd()
            }
        }
    }
}

 

 

Numpad.qml

계산기의 버튼 부분인것 같은데 rectangle 안에 Rowlayout  안에 GridLayout이 있는데 그래서 그런가 이상하게 보여지는 느낌.

 

그나저나 제곱이나 sin tan 같은 과학계산기도 넣으려다가 흔적만 남은건가?

// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

pragma ComponentBehavior: Bound

import QtQuick
import QtQuick.Layouts

Item {
    id: controller

    required property bool isPortraitMode
    required property ApplicationState state

    readonly property color qtGreenColor: "#2CDE85"
    readonly property color backspaceRedColor: "#DE2C2C"
    readonly property int spacing: 5

    property int portraitModeWidth: mainGrid.width
    property int landscapeModeWidth: scientificGrid.width + mainGrid.width

    implicitWidth: isPortraitMode ? portraitModeWidth : landscapeModeWidth
    implicitHeight: mainGrid.height

    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);
        }
    }

    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();
        }
    }

    Component.onCompleted: updateDimmed()

    Rectangle {
        id: numberPad
        anchors.fill: parent
        radius: 8
        color: "transparent"

        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
    }
}

 

 

BackspaceButton.qml

numpad 에서 호출되는 버튼. 그 외에는 크게 눈에 띄진 않네

// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls

RoundButton {
    id: button
    implicitWidth: 48
    implicitHeight: 38
    radius: buttonRadius
    icon.source: getIcon()
    icon.width: 38
    icon.height: 38
    icon.color: getIconColor()
    // include this text property as the calculator engine
    // differentiates buttons through text. The text is never drawn.
    text: "bs"
    Accessible.name: "backspace"

    property bool dimmable: true
    property bool dimmed: false
    readonly property color backgroundColor: "#222222"
    readonly property color borderColor: "#A9A9A9"
    readonly property color backspaceRedColor: "#DE2C2C"
    readonly property int buttonRadius: 8

    function getBackgroundColor() {
        if (button.dimmable && button.dimmed)
            return backgroundColor;
        if (button.pressed)
            return backspaceRedColor;
        return backgroundColor;
    }

    function getBorderColor() {
        if (button.dimmable && button.dimmed)
            return borderColor;
        if (button.pressed || button.hovered)
            return backspaceRedColor;
        return borderColor;
    }

    function getIconColor() {
        if (button.dimmable && button.dimmed)
            return Qt.darker(backspaceRedColor);
        if (button.pressed)
            return backgroundColor;
        return backspaceRedColor;
    }

    function getIcon() {
        if (button.dimmable && button.dimmed)
            return "images/backspace.svg";
        if (button.pressed)
            return "images/backspace_fill.svg";
        return "images/backspace.svg";
    }

    background: Rectangle {
        radius: button.buttonRadius
        color: button.getBackgroundColor()
        border.color: button.getBorderColor()
    }
}

 

CalculatorButton.qml

numpad 에서 호출되는 버튼. 그 외에는 크게 눈에 띄진 않네 2

// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls

RoundButton {
    id: button
    implicitWidth: 38
    implicitHeight: 38
    radius: buttonRadius

    property bool dimmable: false
    property bool dimmed: false
    readonly property int fontSize: 22
    readonly property int buttonRadius: 8
    property color textColor: "#FFFFFF"
    property color accentColor: "#2CDE85"
    readonly property color backgroundColor: "#222222"
    readonly property color borderColor: "#A9A9A9"

    function getBackgroundColor() {
        if (button.dimmable && button.dimmed)
            return backgroundColor;
        if (button.pressed)
            return accentColor;
        return backgroundColor;
    }

    function getBorderColor() {
        if (button.dimmable && button.dimmed)
            return borderColor;
        if (button.pressed || button.hovered)
            return accentColor;
        return borderColor;
    }

    function getTextColor() {
        if (button.dimmable && button.dimmed)
            return Qt.darker(textColor);
        if (button.pressed)
            return backgroundColor;
        if (button.hovered)
            return accentColor;
        return textColor;
    }

    background: Rectangle {
        radius: button.buttonRadius
        color: button.getBackgroundColor()
        border.color: button.getBorderColor()
    }

    contentItem: Text {
        text: button.text
        font.pixelSize: button.fontSize
        horizontalAlignment: Text.AlignHCenter
        verticalAlignment: Text.AlignVCenter
        color: button.getTextColor()
        Behavior on color {
            ColorAnimation {
                duration: 120
                easing.type: Easing.OutElastic
            }
        }
    }
}

'Programming > qt' 카테고리의 다른 글

qt qml loader  (0) 2026.01.14
qstackedwidget, qstackedlayout  (0) 2026.01.13
qt qml view  (0) 2026.01.13
qt quick 이미지 클릭  (0) 2026.01.12
qt creator 18.0.1 design 활성화 하기  (0) 2026.01.09
Posted by 구차니