계산기 예제

이 예에서는 신호와 슬롯을 사용하여 계산기 위젯의 기능을 구현하는 방법과 QGridLayout 을 사용하여 그리드에 하위 위젯을 배치하는 방법을 보여 줍니다.

계산기 예제 스크린샷

이 예제는 두 개의 클래스로 구성됩니다:

  • Calculator 는 모든 계산기 기능을 갖춘 계산기 위젯입니다.
  • Button 는 각 계산기 버튼에 사용되는 위젯입니다. QToolButton 에서 파생됩니다.

Calculator 을 검토하는 것으로 시작하여 Button 을 살펴볼 것입니다.

계산기 클래스 정의

class Calculator : public QWidget
{
    Q_OBJECT

public:
    Calculator(QWidget *parent = nullptr);

private slots:
    void digitClicked();
    void unaryOperatorClicked();
    void additiveOperatorClicked();
    void multiplicativeOperatorClicked();
    void equalClicked();
    void pointClicked();
    void changeSignClicked();
    void backspaceClicked();
    void clear();
    void clearAll();
    void clearMemory();
    void readMemory();
    void setMemory();
    void addToMemory();

Calculator 클래스는 간단한 계산기 위젯을 제공합니다. QDialog 에서 상속되며 계산기 버튼과 관련된 여러 개의 비공개 슬롯이 있습니다. QObject::eventFilter()는 계산기 디스플레이의 마우스 이벤트를 처리하기 위해 재구현되었습니다.

버튼은 동작에 따라 카테고리로 그룹화됩니다. 예를 들어, 모든 숫자 버튼(레이블이 0 ~ 9)은 현재 피연산자에 숫자를 추가합니다. 이러한 경우 여러 개의 버튼을 동일한 슬롯에 연결합니다(예: digitClicked()). 범주에는 숫자, 단항 연산자(Sqrt, , 1/x), 덧셈 연산자(+, -), 곱셈 연산자(×, ÷)가 있습니다. 다른 버튼에는 자체 슬롯이 있습니다.

private:
    template<typename PointerToMemberFunction>
    Button *createButton(const QString &text, const PointerToMemberFunction &member);
    void abortOperation();
    bool calculate(double rightOperand, const QString &pendingOperator);

비공개 createButton() 함수는 위젯 구성의 일부로 사용됩니다. abortOperation() 은 0으로 나누기 또는 음수에 제곱근 연산을 적용할 때마다 호출됩니다. calculate() 은 이진 연산자(+, -, ×, 또는 ÷)를 적용합니다.

    double sumInMemory;
    double sumSoFar;
    double factorSoFar;
    QString pendingAdditiveOperator;
    QString pendingMultiplicativeOperator;
    bool waitingForOperand;

이러한 변수는 계산기 디스플레이의 내용( QLineEdit)과 함께 계산기의 상태를 인코딩합니다:

  • sumInMemory 계산기의 메모리에 저장된 값을 포함합니다( MS, M+, 또는 MC).
  • sumSoFar 는 지금까지 누적된 값을 저장합니다. 사용자가 = 을 클릭하면 sumSoFar 이 다시 계산되어 디스플레이에 표시됩니다. Clear All 을 클릭하면 sumSoFar 이 0으로 재설정됩니다.
  • factorSoFar 곱셈과 나눗셈을 할 때 임시 값을 저장합니다.
  • pendingAdditiveOperator 사용자가 마지막으로 클릭한 덧셈 연산자를 저장합니다.
  • pendingMultiplicativeOperator 사용자가 마지막으로 클릭한 곱셈 연산자를 저장합니다.
  • waitingForOperand 는 계산기가 사용자가 피연산자를 입력할 것으로 예상할 때 true 입니다.

덧셈 연산자와 곱셈 연산자는 우선 순위가 다르기 때문에 다르게 취급됩니다. 예를 들어 ÷+ 보다 우선 순위가 높기 때문에 1 + 2 ÷ 31 + (2 ÷ 3) 으로 해석됩니다.

아래 표는 사용자가 수학식을 입력할 때 계산기 상태의 변화를 보여줍니다.

사용자 입력표시지금까지의 합계Add. Op.지금까지의 인수Mult. Op.피연산자를 기다리는 중?
00true
110false
1 +11+true
1 + 221+false
1 + 2 ÷21+2÷true
1 + 2 ÷ 331+2÷false
1 + 2 ÷ 3 -1.666671.66667-true
1 + 2 ÷ 3 - 441.66667-false
1 + 2 ÷ 3 - 4 =-2.333330true

Sqrt 와 같은 단항 연산자는 특별한 처리가 필요하지 않으며, 연산자 버튼을 클릭할 때 피연산자를 이미 알고 있기 때문에 즉시 적용할 수 있습니다.

    QLineEdit *display;

    enum { NumDigitButtons = 10 };
    Button *digitButtons[NumDigitButtons];
};

마지막으로 숫자를 표시하는 데 사용되는 디스플레이 및 버튼과 관련된 변수를 선언합니다.

계산기 클래스 구현

Calculator::Calculator(QWidget *parent)
    : QWidget(parent), sumInMemory(0.0), sumSoFar(0.0)
    , factorSoFar(0.0), waitingForOperand(true)
{

생성자에서는 계산기의 상태를 초기화합니다. pendingAdditiveOperatorpendingMultiplicativeOperator 변수는 QString 생성자가 빈 문자열로 초기화하므로 명시적으로 초기화할 필요가 없습니다. 헤더에서 직접 이러한 변수를 초기화할 수도 있습니다. 이를 member-initializaton 이라고 하며 긴 초기화 목록을 피할 수 있습니다.

    display = new QLineEdit("0");
    display->setReadOnly(true);
    display->setAlignment(Qt::AlignRight);
    display->setMaxLength(15);

    QFont font = display->font();
    font.setPointSize(font.pointSize() + 8);
    display->setFont(font);

계산기의 디스플레이를 나타내는 QLineEdit 을 생성하고 몇 가지 속성을 설정합니다. 특히 읽기 전용으로 설정합니다.

또한 display 의 글꼴을 8포인트 확대합니다.

    for (int i = 0; i < NumDigitButtons; ++i)
        digitButtons[i] = createButton(QString::number(i), &Calculator::digitClicked);

    Button *pointButton = createButton(tr("."), &Calculator::pointClicked);
    Button *changeSignButton = createButton(tr("\302\261"), &Calculator::changeSignClicked);

    Button *backspaceButton = createButton(tr("Backspace"), &Calculator::backspaceClicked);
    Button *clearButton = createButton(tr("Clear"), &Calculator::clear);
    Button *clearAllButton = createButton(tr("Clear All"), &Calculator::clearAll);

    Button *clearMemoryButton = createButton(tr("MC"), &Calculator::clearMemory);
    Button *readMemoryButton = createButton(tr("MR"), &Calculator::readMemory);
    Button *setMemoryButton = createButton(tr("MS"), &Calculator::setMemory);
    Button *addToMemoryButton = createButton(tr("M+"), &Calculator::addToMemory);

    Button *divisionButton = createButton(tr("\303\267"), &Calculator::multiplicativeOperatorClicked);
    Button *timesButton = createButton(tr("\303\227"), &Calculator::multiplicativeOperatorClicked);
    Button *minusButton = createButton(tr("-"), &Calculator::additiveOperatorClicked);
    Button *plusButton = createButton(tr("+"), &Calculator::additiveOperatorClicked);

    Button *squareRootButton = createButton(tr("Sqrt"), &Calculator::unaryOperatorClicked);
    Button *powerButton = createButton(tr("x\302\262"), &Calculator::unaryOperatorClicked);
    Button *reciprocalButton = createButton(tr("1/x"), &Calculator::unaryOperatorClicked);
    Button *equalButton = createButton(tr("="), &Calculator::equalClicked);

각 버튼에 대해 적절한 텍스트 레이블과 버튼에 연결할 슬롯을 사용하여 비공개 createButton() 함수를 호출합니다.

    QGridLayout *mainLayout = new QGridLayout;
    mainLayout->setSizeConstraint(QLayout::SetFixedSize);
    mainLayout->addWidget(display, 0, 0, 1, 6);
    mainLayout->addWidget(backspaceButton, 1, 0, 1, 2);
    mainLayout->addWidget(clearButton, 1, 2, 1, 2);
    mainLayout->addWidget(clearAllButton, 1, 4, 1, 2);

    mainLayout->addWidget(clearMemoryButton, 2, 0);
    mainLayout->addWidget(readMemoryButton, 3, 0);
    mainLayout->addWidget(setMemoryButton, 4, 0);
    mainLayout->addWidget(addToMemoryButton, 5, 0);

    for (int i = 1; i < NumDigitButtons; ++i) {
        int row = ((9 - i) / 3) + 2;
        int column = ((i - 1) % 3) + 1;
        mainLayout->addWidget(digitButtons[i], row, column);
    }

    mainLayout->addWidget(digitButtons[0], 5, 1);
    mainLayout->addWidget(pointButton, 5, 2);
    mainLayout->addWidget(changeSignButton, 5, 3);

    mainLayout->addWidget(divisionButton, 2, 4);
    mainLayout->addWidget(timesButton, 3, 4);
    mainLayout->addWidget(minusButton, 4, 4);
    mainLayout->addWidget(plusButton, 5, 4);

    mainLayout->addWidget(squareRootButton, 2, 5);
    mainLayout->addWidget(powerButton, 3, 5);
    mainLayout->addWidget(reciprocalButton, 4, 5);
    mainLayout->addWidget(equalButton, 5, 5);
    setLayout(mainLayout);

    setWindowTitle(tr("Calculator"));
}

레이아웃은 단일 QGridLayout 로 처리됩니다. QLayout::setSizeConstraint () 호출은 Calculator 위젯이 항상 최적의 크기( size hint)로 표시되도록 하여 사용자가 계산기의 크기를 조정할 수 없도록 합니다. 크기 힌트는 하위 위젯의 크기와 size policy 에 의해 결정됩니다.

대부분의 자식 위젯은 그리드 레이아웃에서 하나의 셀만 차지합니다. 이러한 경우 QGridLayout::addWidget()에 행과 열만 전달하면 됩니다. display , backspaceButton, clearButton, clearAllButton 위젯은 두 개 이상의 열을 차지하므로 행 스팬과 열 스팬도 전달해야 합니다.

void Calculator::digitClicked()
{
    Button *clickedButton = qobject_cast<Button *>(sender());
    int digitValue = clickedButton->text().toInt();
    if (display->text() == "0" && digitValue == 0.0)
        return;

    if (waitingForOperand) {
        display->clear();
        waitingForOperand = false;
    }
    display->setText(display->text() + QString::number(digitValue));
}

계산기의 숫자 버튼 중 하나를 누르면 버튼의 clicked() 신호가 발생하여 digitClicked() 슬롯이 트리거됩니다.

먼저 QObject::sender()를 사용하여 어떤 버튼이 신호를 보냈는지 알아냅니다. 이 함수는 발신자를 QObject 포인터로 반환합니다. 발신자가 Button 객체라는 것을 알고 있으므로 QObject 을 안전하게 캐스팅할 수 있습니다. C 스타일 캐스팅이나 C++ static_cast<>() 을 사용할 수도 있었지만 방어적인 프로그래밍 기법으로 qobject_cast()을 사용합니다. 이 방법은 객체의 타입이 잘못된 경우 널 포인터가 반환된다는 장점이 있습니다. 널 포인터로 인한 크래시는 안전하지 않은 형변환으로 인한 크래시보다 진단하기가 훨씬 쉽습니다. 버튼이 있으면 QToolButton::text()를 사용하여 연산자를 추출합니다.

이 슬롯은 특히 두 가지 상황을 고려해야 합니다. display 에 "0"이 포함되어 있고 사용자가 0 버튼을 클릭하는 경우 "00"을 표시하는 것은 어리석은 일입니다. 그리고 계산기가 새 피연산자를 기다리는 상태인 경우 새 숫자는 새 피연산자의 첫 번째 숫자이며, 이 경우 이전 계산의 결과를 먼저 지워야 합니다.

마지막에는 새 숫자를 디스플레이의 값에 추가합니다.

void Calculator::unaryOperatorClicked()
{
    Button *clickedButton = qobject_cast<Button *>(sender());
    QString clickedOperator = clickedButton->text();
    double operand = display->text().toDouble();
    double result = 0.0;

    if (clickedOperator == tr("Sqrt")) {
        if (operand < 0.0) {
            abortOperation();
            return;
        }
        result = std::sqrt(operand);
    } else if (clickedOperator == tr("x\302\262")) {
        result = std::pow(operand, 2.0);
    } else if (clickedOperator == tr("1/x")) {
        if (operand == 0.0) {
            abortOperation();
            return;
        }
        result = 1.0 / operand;
    }
    display->setText(QString::number(result));
    waitingForOperand = true;
}

unaryOperatorClicked() 슬롯은 단항 연산자 버튼 중 하나를 클릭할 때마다 호출됩니다. 다시 클릭한 버튼에 대한 포인터는 QObject::sender()를 사용하여 검색됩니다. 피연산자는 버튼의 텍스트에서 추출되어 clickedOperator 에 저장됩니다. 피연산자는 display 에서 가져옵니다.

그런 다음 연산을 수행합니다. Sqrt 을 음수에 적용하거나 1/x 을 0에 적용하면 abortOperation() 을 호출합니다. 모든 것이 잘되면 줄 편집에 연산 결과를 표시하고 waitingForOperandtrue 으로 설정합니다. 이렇게 하면 사용자가 새 숫자를 입력하면 해당 숫자가 현재 값에 추가되는 대신 새 피연산자로 간주됩니다.

void Calculator::additiveOperatorClicked()
{
    Button *clickedButton = qobject_cast<Button *>(sender());
    if (!clickedButton)
      return;
    QString clickedOperator = clickedButton->text();
    double operand = display->text().toDouble();

additiveOperatorClicked() 슬롯은 사용자가 + 또는 - 버튼을 클릭할 때 호출됩니다.

클릭한 연산자에 대해 실제로 작업을 수행하기 전에 보류 중인 작업을 처리해야 합니다. 곱셈 연산자는 덧셈 연산자보다 우선순위가 높으므로 곱셈 연산자부터 시작합니다:

    if (!pendingMultiplicativeOperator.isEmpty()) {
        if (!calculate(operand, pendingMultiplicativeOperator)) {
            abortOperation();
            return;
        }
        display->setText(QString::number(factorSoFar));
        operand = factorSoFar;
        factorSoFar = 0.0;
        pendingMultiplicativeOperator.clear();
    }

× 또는 ÷ 을 먼저 클릭한 경우, 나중에 = 을 클릭하지 않고 디스플레이의 현재 값이 × 또는 ÷ 연산자의 오른쪽 피연산자가 되어 마침내 연산을 수행하고 디스플레이를 업데이트할 수 있습니다.

    if (!pendingAdditiveOperator.isEmpty()) {
        if (!calculate(operand, pendingAdditiveOperator)) {
            abortOperation();
            return;
        }
        display->setText(QString::number(sumSoFar));
    } else {
        sumSoFar = operand;
    }

+ 또는 - 을 이전에 클릭한 경우 sumSoFar 이 왼쪽 피연산자이고 디스플레이의 현재 값이 연산자의 오른쪽 피연산자가 됩니다. 보류 중인 덧셈 연산자가 없는 경우 sumSoFar 이 디스플레이의 텍스트로 설정됩니다.

    pendingAdditiveOperator = clickedOperator;
    waitingForOperand = true;
}

마지막으로 방금 클릭한 연산자를 처리하면 됩니다. 아직 오른쪽 피연산자가 없으므로 클릭한 연산자를 pendingAdditiveOperator 변수에 저장합니다. 나중에 오른쪽 피연산자가 생기면 sumSoFar 을 왼쪽 피연산자로 사용하여 연산을 적용할 것입니다.

void Calculator::multiplicativeOperatorClicked()
{
    Button *clickedButton = qobject_cast<Button *>(sender());
    if (!clickedButton)
      return;
    QString clickedOperator = clickedButton->text();
    double operand = display->text().toDouble();

    if (!pendingMultiplicativeOperator.isEmpty()) {
        if (!calculate(operand, pendingMultiplicativeOperator)) {
            abortOperation();
            return;
        }
        display->setText(QString::number(factorSoFar));
    } else {
        factorSoFar = operand;
    }

    pendingMultiplicativeOperator = clickedOperator;
    waitingForOperand = true;
}

multiplicativeOperatorClicked() 슬롯은 additiveOperatorClicked() 과 유사합니다. 곱셈 연산자가 덧셈 연산자보다 우선하기 때문에 여기서 보류 중인 덧셈 연산자에 대해 걱정할 필요가 없습니다.

void Calculator::equalClicked()
{
    double operand = display->text().toDouble();

    if (!pendingMultiplicativeOperator.isEmpty()) {
        if (!calculate(operand, pendingMultiplicativeOperator)) {
            abortOperation();
            return;
        }
        operand = factorSoFar;
        factorSoFar = 0.0;
        pendingMultiplicativeOperator.clear();
    }
    if (!pendingAdditiveOperator.isEmpty()) {
        if (!calculate(operand, pendingAdditiveOperator)) {
            abortOperation();
            return;
        }
        pendingAdditiveOperator.clear();
    } else {
        sumSoFar = operand;
    }

    display->setText(QString::number(sumSoFar));
    sumSoFar = 0.0;
    waitingForOperand = true;
}

additiveOperatorClicked() 에서와 마찬가지로 보류 중인 곱셈 연산자와 덧셈 연산자를 처리하는 것으로 시작합니다. 그런 다음 sumSoFar 을 표시하고 변수를 0으로 재설정합니다. 변수를 0으로 재설정하는 것은 값이 두 번 계산되는 것을 방지하기 위해 필요합니다.

void Calculator::pointClicked()
{
    if (waitingForOperand)
        display->setText("0");
    if (!display->text().contains('.'))
        display->setText(display->text() + tr("."));
    waitingForOperand = false;
}

pointClicked() 슬롯은 display 의 콘텐츠에 소수점을 추가합니다.

void Calculator::changeSignClicked()
{
    QString text = display->text();
    double value = text.toDouble();

    if (value > 0.0) {
        text.prepend(tr("-"));
    } else if (value < 0.0) {
        text.remove(0, 1);
    }
    display->setText(text);
}

changeSignClicked() 슬롯은 display 의 값의 부호를 변경합니다. 현재 값이 양수이면 마이너스 기호를 앞에 붙이고, 현재 값이 음수이면 값에서 첫 번째 문자(마이너스 기호)를 제거합니다.

void Calculator::backspaceClicked()
{
    if (waitingForOperand)
        return;

    QString text = display->text();
    text.chop(1);
    if (text.isEmpty()) {
        text = "0";
        waitingForOperand = true;
    }
    display->setText(text);
}

backspaceClicked() 은 디스플레이에서 가장 오른쪽 문자를 제거합니다. 빈 문자열이 나오면 "0"을 표시하고 waitingForOperandtrue 으로 설정합니다.

void Calculator::clear()
{
    if (waitingForOperand)
        return;

    display->setText("0");
    waitingForOperand = true;
}

clear() 슬롯은 현재 피연산자를 0으로 재설정합니다. 이는 Backspace 을 여러 번 클릭하여 피연산자 전체를 지우는 것과 같습니다.

void Calculator::clearAll()
{
    sumSoFar = 0.0;
    factorSoFar = 0.0;
    pendingAdditiveOperator.clear();
    pendingMultiplicativeOperator.clear();
    display->setText("0");
    waitingForOperand = true;
}

clearAll() 슬롯은 계산기를 초기 상태로 재설정합니다.

void Calculator::clearMemory()
{
    sumInMemory = 0.0;
}

void Calculator::readMemory()
{
    display->setText(QString::number(sumInMemory));
    waitingForOperand = true;
}

void Calculator::setMemory()
{
    equalClicked();
    sumInMemory = display->text().toDouble();
}

void Calculator::addToMemory()
{
    equalClicked();
    sumInMemory += display->text().toDouble();
}

clearMemory() 슬롯은 메모리에 저장된 합계를 지우고, readMemory() 슬롯은 합계를 피연산자로 표시하며, setMemory() 슬롯은 메모리의 합계를 현재 합계로 바꾸고, addToMemory() 슬롯은 메모리의 값에 현재 값을 더합니다. setMemory()addToMemory() 의 경우 equalClicked() 를 호출하여 sumSoFar 및 디스플레이의 값을 업데이트합니다.

template<typename PointerToMemberFunction>
Button *Calculator::createButton(const QString &text, const PointerToMemberFunction &member)
{
    Button *button = new Button(text);
    connect(button, &Button::clicked, this, member);
    return button;
}

생성자에서 비공개 createButton() 함수를 호출하여 계산기 버튼을 생성합니다.

void Calculator::abortOperation()
{
    clearAll();
    display->setText(tr("####"));
}

비공개 abortOperation() 함수는 계산이 실패할 때마다 호출됩니다. 이 함수는 계산기 상태를 재설정하고 "####"를 표시합니다.

bool Calculator::calculate(double rightOperand, const QString &pendingOperator)
{
    if (pendingOperator == tr("+")) {
        sumSoFar += rightOperand;
    } else if (pendingOperator == tr("-")) {
        sumSoFar -= rightOperand;
    } else if (pendingOperator == tr("\303\227")) {
        factorSoFar *= rightOperand;
    } else if (pendingOperator == tr("\303\267")) {
        if (rightOperand == 0.0)
            return false;
        factorSoFar /= rightOperand;
    }
    return true;
}

비공개 calculate() 함수는 이진 연산을 수행합니다. 오른쪽 피연산자는 rightOperand 입니다. 덧셈 연산자의 경우 왼쪽 피연산자는 sumSoFar 이며, 곱셈 연산자의 경우 왼쪽 피연산자는 factorSoFar 입니다. 0으로 나눗셈이 발생하면 false 함수를 반환합니다.

버튼 클래스 정의

이제 Button 클래스를 살펴보겠습니다:

class Button : public QToolButton
{
    Q_OBJECT

public:
    explicit Button(const QString &text, QWidget *parent = nullptr);

    QSize sizeHint() const override;
};

Button 클래스에는 텍스트 레이블과 부모 위젯을 취하는 편리한 생성자가 있으며, QWidget::sizeHint()을 재구현하여 QToolButton 이 일반적으로 제공하는 것보다 더 많은 공간을 텍스트 주위에 제공합니다.

버튼 클래스 구현

Button::Button(const QString &text, QWidget *parent)
    : QToolButton(parent)
{
    setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
    setText(text);
}

버튼의 모양은 계산기 위젯의 레이아웃에 따라 레이아웃의 하위 위젯의 크기와 size policy 를 통해 결정됩니다. 생성자에서 setSizePolicy() 함수를 호출하면 버튼이 가로로 확장되어 사용 가능한 모든 공간을 채우도록 합니다(기본적으로 QToolButton는 사용 가능한 공간을 채우기 위해 확장되지 않습니다). 이 함수를 호출하지 않으면 같은 열에 있는 여러 버튼의 너비가 달라집니다.

QSize Button::sizeHint() const
{
    QSize size = QToolButton::sizeHint();
    size.rheight() += 20;
    size.rwidth() = qMax(size.width(), size.height());
    return size;
}

sizeHint()에서는 대부분의 버튼에 보기 좋은 크기를 반환하려고 합니다. 기본 클래스(QToolButton)의 크기 힌트를 재사용하되 다음과 같은 방식으로 수정합니다:

  • 크기 힌트의 height 컴포넌트에 20을 추가합니다.
  • 크기 힌트의 width 컴포넌트를 height 만큼 크게 만듭니다.

이렇게 하면 대부분의 글꼴에서 Backspace, Clear, Clear All 버튼의 텍스트가 잘리지 않고 숫자 및 연산자 버튼이 정사각형이 됩니다.

아래 스크린샷은 생성자에서 가로 크기 정책을 QSizePolicy::Expanding설정하지 않고 QWidget::sizeHint()를 다시 구현하지 않은 경우 Calculator 위젯이 어떻게 보이는지 보여줍니다.

기본 크기 정책과 크기 힌트가 포함된 계산기 예시

예제 프로젝트 @ code.qt.io

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