Qt Widget-Based Calculator: UI Validation, Infix Parsing, RPN Conversion, and Evaluation
User interface uses QWidget as the top-level window, QLineEdit to show and edit the expression/result, and QPushButton for keypad input.
- Normalize user input as it’s typed to simplify later evaluation.
- Parentheses are tracked sothat a closing parenthesis is only allowed when a matching opening parenthesis exists.
Input validation rules
-
Parentheses must be balanced and properly ordered
- On "(": increase balance
- On ")": valid only if balance > 0, then decrease balance
-
Digits cannot immediately follow a right parenthesis
- Example: 10+3)5*2 is invalid because digit 5 follows ')'
-
Decimal point
- A dot must have a digit before it
- Each contiguous number may contain at most one dot
- Example: 1.23.45 is invalid
-
Plus/Minus
- Cannot follow a decimal point
- Avoid two consecutive binary operators (+ - * /) directly preceding another operator
- Example:
- 7*-+10 is invalid (two operators before the number)
- 7.+ is invalid (dot before plus)
- 7-(--5) is invalid (double minus before a number inside parentheses)
-
Multiply/Divide cannot follow: empty, '(', '.', '+', '-', '*', '/'
- Example: *1+(/5+10 is invalid
-
Left parenthesis '('
- Cannot follow ')', a digit, or '.'
- Avoid two consecutive operators right before '('
- Example: ()+10(11+10 is invalid (no operand before '(' appropriately)
-
Right parenthesis ')'
- Cannot follow empty string, operator (+ - * /), '.', or '('
- Must have a matching '('
- Example: )+(10+5.) is invalid
Expression engine
Tokenization (infix splitting)
- Scan the expression and split into numbers and operators.
- Distinguish unary +/− from binary +/−:
- At the beginning of the expression, treat +/− as a sign for the following number
- Immediately following '(', '+', '-', '*', or '/', treat +/− as a sign
- Otherwise, +/− is an operator
Example: +9+(-3--1)*-5 splits into
- "+9", "+", "(", "-3", "-", "-1", ")", "*", "-5"
Converting infix tokens to RPN (postfix)
- Use a stack for operators and an output queue for the final postfix sequence.
- Algorithm
- If token is a number: push to output
- If token is '+' or '-': pop operators from the stack until hitting '(' or stack is empty; then push the operator
- If token is '' or '/': pop while the top of stack is '' or '/' (and not '('); then push the operator
- If token is '(': push too stack
- If token is ')': pop operators to output until '(' is found; discard the '('
- After all tokens processed: pop remaining operators to output
Example postfix for +9+(-3--1)*-5
- +9, -3, -1, -, -5, *, +
Evaluating RPN
- Use a value stack.
- For a number: push it
- For an operator: pop right then left, compute, then push result
- Division needs explicit zero check
- At the end: the single stack value is the result
Code: Expression engine (header)
#ifndef EXPRENGINE_H
#define EXPRENGINE_H
#include <QString>
#include <QVector>
#include <QStack>
class ExprEngine {
public:
QString result(const QString& expr);
private:
QVector<QString> lex(const QString& s);
QVector<QString> toRpn(const QVector<QString>& tokens);
QString evalRpn(const QVector<QString>& rpn);
QString trimNumber(const QString& s);
bool isOperator(const QString& t) const;
int precedence(const QString& op) const;
};
#endif // EXPRENGINE_H
Code: Expression engine (implementation)
#include "ExprEngine.h"
static bool isDigitOrDot(QChar c) {
return (c >= '0' && c <= '9') || c == '.';
}
bool ExprEngine::isOperator(const QString& t) const {
return t == "+" || t == "-" || t == "*" || t == "/";
}
int ExprEngine::precedence(const QString& op) const {
if (op == "+" || op == "-") return 1;
if (op == "*" || op == "/") return 2;
return 0;
}
QVector<QString> ExprEngine::lex(const QString& s) {
QVector<QString> out;
QString num;
auto flushNum = [&]() {
if (!num.isEmpty()) {
out.push_back(num);
num.clear();
}
};
for (int i = 0; i < s.size(); ++i) {
const QChar ch = s[i];
if (isDigitOrDot(ch)) {
num.append(ch);
continue;
}
if (ch == '(' || ch == ')' || ch == '*' || ch == '/') {
flushNum();
out.push_back(QString(ch));
continue;
}
if (ch == '+' || ch == '-') {
const bool atStart = (i == 0);
const QChar prev = atStart ? '\0' : s[i - 1];
const bool prevIsOp = (prev == '(' || prev == '+' || prev == '-' || prev == '*' || prev == '/');
if (atStart || prevIsOp) {
// unary sign becomes part of the number token
num.append(ch);
} else {
flushNum();
out.push_back(QString(ch));
}
continue;
}
}
flushNum();
return out;
}
QVector<QString> ExprEngine::toRpn(const QVector<QString>& tokens) {
QVector<QString> out;
QStack<QString> ops;
for (const QString& t : tokens) {
bool ok = false;
t.toDouble(&ok);
if (ok) {
out.push_back(t);
continue;
}
if (t == "(") { ops.push(t); continue; }
if (t == ")") {
while (!ops.isEmpty() && ops.top() != "(") {
out.push_back(ops.pop());
}
if (!ops.isEmpty() && ops.top() == "(") ops.pop();
continue;
}
if (isOperator(t)) {
while (!ops.isEmpty() && isOperator(ops.top()) && precedence(ops.top()) >= precedence(t)) {
out.push_back(ops.pop());
}
ops.push(t);
continue;
}
}
while (!ops.isEmpty()) {
out.push_back(ops.pop());
}
return out;
}
QString ExprEngine::trimNumber(const QString& s) {
if (!s.contains('.')) return s;
QString t = s;
// remove trailing zeros
while (t.size() > 1 && t.endsWith('0')) t.chop(1);
if (t.endsWith('.')) t.chop(1);
return t;
}
QString ExprEngine::evalRpn(const QVector<QString>& rpn) {
QStack<QString> st;
auto apply = [&](const QString& a, const QString& op, const QString& b) -> QString {
const double left = a.toDouble();
const double right = b.toDouble();
double res = 0.0;
if (op == "+") res = left + right;
else if (op == "-") res = left - right;
else if (op == "*") res = left * right;
else if (op == "/") {
if (qAbs(right) < 1e-15) return QStringLiteral("DIV_ZERO");
res = left / right;
}
QString out = QString::number(res, 'f', 12);
return trimNumber(out);
};
for (const QString& t : rpn) {
bool ok = false;
t.toDouble(&ok);
if (ok) {
st.push(t);
continue;
}
if (isOperator(t)) {
if (st.size() < 2) return QStringLiteral("ERR");
const QString right = st.pop();
const QString left = st.pop();
QString r = apply(left, t, right);
if (r == "DIV_ZERO") return r;
st.push(r);
}
}
if (st.size() != 1) return QStringLiteral("ERR");
return st.pop();
}
QString ExprEngine::result(const QString& expr) {
const auto tokens = lex(expr);
const auto rpn = toRpn(tokens);
return evalRpn(rpn);
}
Code: Calculator window (header)
#ifndef CALCWINDOW_H
#define CALCWINDOW_H
#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QVector>
#include <QString>
#include "ExprEngine.h"
class CalcWindow : public QWidget {
Q_OBJECT
public:
static CalcWindow* create(QWidget* parent = nullptr);
void showFixed();
private:
explicit CalcWindow(QWidget* parent = nullptr);
void buildUi();
void wireUp();
void handleKey(const QString& key);
bool canAppendDigit(const QString& line) const;
bool canAppendDot(const QString& line) const;
bool canAppendPlusMinus(const QString& line) const;
bool canAppendMulDiv(const QString& line) const;
bool canAppendLParen(const QString& line) const;
bool canAppendRParen(const QString& line) const;
int currentParenBalance(const QString& line) const;
private slots:
void onKey();
private:
ExprEngine engine_;
QLineEdit* display_ { nullptr };
QVector<QPushButton*> keys_;
bool clearOnNext_ { false };
};
#endif // CALCWINDOW_H
Code: Calculator window (implementation)
#include "CalcWindow.h"
#include <QGridLayout>
#include <QHBoxLayout>
#include <QVBoxLayout>
CalcWindow* CalcWindow::create(QWidget* parent) {
return new CalcWindow(parent);
}
CalcWindow::CalcWindow(QWidget* parent) : QWidget(parent) {
buildUi();
wireUp();
}
void CalcWindow::buildUi() {
setWindowTitle("Calculator");
display_ = new QLineEdit(this);
display_->setAlignment(Qt::AlignRight);
display_->setReadOnly(true);
auto grid = new QGridLayout();
// Layout map similar to: <-, CE, digits, operators, parentheses, equals
const QVector<QString> rows[] = {
{ "<-", "CE" },
{ "7", "8", "9", "+", "(" },
{ "4", "5", "6", "-", ")" },
{ "1", "2", "3", "*", "=" },
{ "0", ".", "/" }
};
int r = 0;
for (const auto& row : rows) {
int c = 0;
for (const auto& label : row) {
auto* btn = new QPushButton(label, this);
keys_.push_back(btn);
if (label == "CE") {
grid->addWidget(btn, r, c, 1, 2);
c += 2;
continue;
}
if (label == "=") {
grid->addWidget(btn, r, c, 2, 1); // span to next row for emphasis
// next items in the same column may skip placement
++c;
continue;
}
if (label == "0") {
grid->addWidget(btn, r, c, 1, 2);
c += 2;
continue;
}
grid->addWidget(btn, r, c);
++c;
}
++r;
}
auto vbox = new QVBoxLayout(this);
vbox->addWidget(display_);
vbox->addLayout(grid);
setLayout(vbox);
}
void CalcWindow::wireUp() {
for (auto* b : keys_) {
connect(b, &QPushButton::clicked, this, &CalcWindow::onKey);
}
}
int CalcWindow::currentParenBalance(const QString& line) const {
int bal = 0;
for (const auto ch : line) {
if (ch == '(') ++bal;
else if (ch == ')') --bal;
}
return bal;
}
bool CalcWindow::canAppendDigit(const QString& line) const {
if (!line.isEmpty()) {
const QChar last = line.back();
if (last == ')') return false;
}
return true;
}
bool CalcWindow::canAppendDot(const QString& line) const {
if (line.isEmpty()) return false; // dot cannot start a number
const QChar last = line.back();
if (!(last >= '0' && last <= '9')) return false; // dot must follow a digit
// ensure current numeric run has no dot
for (int i = line.size() - 1; i >= 0; --i) {
const QChar ch = line[i];
if (ch == '.' ) return false; // already has a dot
if (!(ch >= '0' && ch <= '9')) break;
}
return true;
}
bool CalcWindow::canAppendPlusMinus(const QString& line) const {
if (!line.isEmpty() && line.back() == '.') return false;
if (line.size() >= 2) {
const QChar a = line[line.size() - 2];
const QChar b = line[line.size() - 1];
const auto isOp = [](QChar c){ return c=='+'||c=='-'||c=='*'||c=='/'; };
if ((a=='(' || isOp(a)) && isOp(b)) return false;
}
return true;
}
bool CalcWindow::canAppendMulDiv(const QString& line) const {
if (line.isEmpty()) return false;
const QChar last = line.back();
if (last=='(' || last=='.' || last=='+' || last=='-' || last=='*' || last=='/') return false;
return true;
}
bool CalcWindow::canAppendLParen(const QString& line) const {
if (line.isEmpty()) return true;
const QChar last = line.back();
if (last==')' || (last>='0'&&last<='9') || last=='.') return false;
if (line.size() >= 2) {
const QChar a = line[line.size() - 2];
const QChar b = line[line.size() - 1];
const auto isOp = [](QChar c){ return c=='+'||c=='-'||c=='*'||c=='/'; };
if (isOp(a) && isOp(b)) return false;
}
return true;
}
bool CalcWindow::canAppendRParen(const QString& line) const {
if (line.isEmpty()) return false;
if (currentParenBalance(line) <= 0) return false;
const QChar last = line.back();
if (last=='+'||last=='-'||last=='*'||last=='/'||last=='.'||last=='(') return false;
return true;
}
void CalcWindow::handleKey(const QString& key) {
QString line = display_->text();
if (clearOnNext_) {
line.clear();
clearOnNext_ = false;
}
if (key.size() == 1 && key[0] >= '0' && key[0] <= '9') {
if (!canAppendDigit(line)) return;
line += key;
} else if (key == ".") {
if (!canAppendDot(line)) return;
line += key;
} else if (key == "+" || key == "-") {
if (!canAppendPlusMinus(line)) return;
line += key;
} else if (key == "*" || key == "/") {
if (!canAppendMulDiv(line)) return;
line += key;
} else if (key == "(") {
if (!canAppendLParen(line)) return;
line += key;
} else if (key == ")") {
if (!canAppendRParen(line)) return;
line += key;
} else if (key == "<-") {
if (!line.isEmpty()) line.chop(1);
} else if (key == "CE") {
line.clear();
} else if (key == "=") {
if (line.isEmpty()) {
display_->setText(line);
return;
}
const QString res = engine_.result(line);
if (res == "DIV_ZERO") {
line += " : Division by zero";
} else if (res == "ERR") {
line += " : Syntax error";
} else {
line += "=" + res;
}
clearOnNext_ = true;
}
display_->setText(line);
}
void CalcWindow::onKey() {
if (auto* btn = qobject_cast<QPushButton*>(sender())) {
handleKey(btn->text());
}
}
void CalcWindow::showFixed() {
QWidget::show();
setFixedSize(width(), height());
}
Code: main.cpp
#include <QApplication>
#include "CalcWindow.h"
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
CalcWindow* w = CalcWindow::create();
w->showFixed();
return app.exec();
}