БлогNot. Qt: пишем калькулятор

Qt: пишем калькулятор

Установив и настроив QT, реализуем что-нибудь поинтереснее HelloWorld'а. Для этого после запуска QT Creator кликнем меню Файл, "Новый файл или проект", шаблон "Приложение Qt Widgets". Назовём проект Calculator и выберем папку для его хранения. Нам не понадобятся файлы mainwindow.cpp и mainwindow.h, их можно исключить из проекта, щёлкнув правой кнопкой мыши на имени файла в окне проектов и выбрав "Удалить". В этом же окне щёлкнем правой кнопкой мыши на имени проекта и скажем "Добавить новый...":

Добавление класса в проект Qt
Добавление класса в проект Qt

Далее выбираем "Класс C++", жмём кнопку Выбрать..., вводим следующие данные:
Имя класса: Calculator
Базовый класс: QWidget
Тип класса: производный от QWidget
Путь: проверяем, что указана папка, где лежат остальные исходники.

Жмём "Далее, в следующем окне можно просто нажать "Завершить".

Текст модуля main.cpp немного изменится по отношению к сгенерированному системой:

#include "calculator.h"
#include <QApplication>

int main(int argc, char *argv[]) {
   QApplication app(argc, argv);
   Calculator calculator;
   calculator.setWindowTitle("Calculator");
   calculator.resize(230,200);
   calculator.setFixedSize(300,250);
   calculator.show();
   return app.exec();
}

Обратите внимание на установку и фиксацию размеров окна приложения.

В заголовочном файле класса калькулятора calculator.h опишем свойства и прототипы функций калькулятора:

#ifndef _Calculator_h_
 #define _Calculator_h_
 #include <QWidget>
 #include <QStack>
 #include <QLabel>
 #include <QPushButton>
 #include <QGridLayout>

 class Calculator : public QWidget {
  Q_OBJECT //макрос, нужен в начале всех наших классов
  private:
   QLabel *displaystring;
   QStack <QString> stack;
  public:
   Calculator (QWidget* pwgt = 0);
   QPushButton* createButton (const QString& str);
   void calculate ();
  public slots: //Общедоступные обработчики событий
   void slotButtonClicked ();
 };
#endif

В метку displaystring будем выводить результаты вычислений (и показывать там числа/операции в процессе их набора), для хранения двух чисел и выбранной над ними операции используем реализованный в QT стек (QStack <QString> stack;). Единственный конструктор класса может быть вызван с параметром-указателем на родительский виджет или без него. Метод createButton будет создавать одну кнопку с указанной параметром подписью, метод calculate займётся вычислением, а единственный обработчик событий (слот) калькулятора с именем slotButtonClicked будет выполнять всю основную работу по реагированию на нажатия кнопок.

Осталось написать файл calculator.cpp. Конструктор создаст в окне виджета метку displaystring и набор кнопок, заодно продемонстрировав современный подход к логическому проектированию интерфейсов "как в Java", то есть, на основе относительного позиционирования в компоненте типа QGridLayout.

Нам понадобится "сетка" из ячеек для метки и кнопок размерностью 6 строк на 4 столбца, например, такое размещение кнопок ничем не хуже других:

План размещения кнопок с помощью QGridLayout
План размещения кнопок с помощью QGridLayout

Вот текст конструктора калькулятора, реализующего данную конфигурацию интерфейса (здесь и далее пишем файл Calculator.cpp):

#include "calculator.h"

Calculator::Calculator (QWidget *parent) : QWidget(parent) {
 displaystring = new QLabel("");
 displaystring->setMinimumSize (150, 50);
 QChar aButtons[4][4] = {
  {'7', '8', '9', '/'},
  {'4', '5', '6', '*'},
  {'1', '2', '3', '-'},
  {'0', '.', '=', '+'}
 };

 QGridLayout *myLayout = new QGridLayout;
 myLayout->addWidget(displaystring, 0, 0, 1, 4);
 myLayout->addWidget(createButton("CE"), 1, 3);
 for (int i = 0; i < 4; ++i) {
  for (int j = 0; j < 4; ++j) {
   myLayout->addWidget(createButton(aButtons[i][j]), i + 2, j);
  }
 }
 setLayout(myLayout);
}

Служебный метод createButton создаёт одну кнопку и назначает ей обработчиком нажатия (стандартного сигнала "clicked()") метод (слот) с именем slotButtonClicked(). Если пока не знакомы с сигналами и слотами, считайте для простоты, что это события и их обработчики, как в любом другом ООП:

QPushButton* Calculator::createButton (const QString& str) {
 QPushButton* pcmd = new QPushButton(str);
 pcmd->setMinimumSize(40, 40);
 connect(pcmd, SIGNAL(clicked()), this, SLOT(slotButtonClicked()));
 return pcmd;
}

Метод calculate будет очень прост, ведь мы собираемся ограничиться стеком из 3 элементов, а вычисление выполнять либо по нажатию "=", либо когда выбрана следующая операция, например, при наборе 5+3*6, как только нажата "*" вычислим "5+3" и вернём в стек "8" и "*" для последующего умножения на 6 или другое число. Это избавит нас от необходимости возиться со скобками и учитывать старшинство операций. Также в методе не проверяется деление на ноль и т.п., в этом плане даже новичок легко улучшит его самостоятельно:

void Calculator::calculate() {
 double dOperand2 = stack.pop().toDouble();
 QString strOperation = stack.pop();
 double dOperand1 = stack.pop().toDouble();
 double dResult = 0;
 if (strOperation == "+") { dResult = dOperand1 + dOperand2; }
 else if (strOperation == "-") { dResult = dOperand1 - dOperand2; }
 else if (strOperation == "/") { dResult = dOperand1 / dOperand2; }
 else if (strOperation == "*") { dResult = dOperand1 * dOperand2; }
 displaystring->setText(QString("%1").arg(dResult, 0, 'f', 3));
}

В последнем операторе результат выводится как вещественное число с 3 знаками после запятой, при необходимости измените.

Наконец, реализуем метод slotButtonClicked, реагирующий на нажатия кнопок и управляющий стеком:

void Calculator::slotButtonClicked() {
 QString str = ((QPushButton*)sender())->text(); //Получаем текст с нажатой кнопки
 if (str == "CE") { //Кнопка Очистить
  stack.clear(); displaystring->setText(""); return;
 }

 QString text = displaystring->text(); //отображаемый текст
 int len = text.length();
 QString last = "";
 if (len>0) last = text.right(1); //самый правый символ ввода

 if (((len==0 && stack.count()==0) ||
      ((stack.count()==2 && len>1 && (last=="+"||last=="-"||last=="*"||last=="/")))) &&
     (str.contains(QRegExp("[0-9]")) || str=="-")) {
  //На экране пусто и стек пуст или введен 1-й операнд и операция
  //и при этом нажата цифра или "-"
  text=str; //Стереть то, что было отображено, и отобразить нажатый символ
 }
 else if ((text+str).contains(QRegExp("^-?[0-9]+\\.?[0-9]*$"))) {
  text+=str; //Пока вводим число - добавлять символ
 }
 else if (text.contains(QRegExp("^-?[0-9]+\\.?[0-9]*$"))) { //Уже набрано число
  if (str=="*"||str=="/"||str=="+"||str=="-"||str=="=") { //Вычислить
   if (stack.count()==2) { //Есть 1-й операнд и число
    stack.push(text); //Положить в стек 2-й операнд
    calculate(); //Вычислить
    text=displaystring->text(); //Показать результат
   }
   if (str!="=") { //Для вычисления "по цепочке"
    stack.push(text); //Положить в стек 1-й операнд
    text+=str; //Отобразить операцию до след.нажатия кнопки
    stack.push(str); //Положить в стек операцию
   }
  }
 }
 displaystring->setText(text);
}

Здесь мы не делаем никакого "лишнего" контроля, так что если нажать "2", "+", "3", "=", получим результат "5.000" и либо сможем дальше вычислять по цепочке ("+", "1"), либо вводимые цифры будут дописываться к числу :) Это легко исправить, введя в класс калькулятора, например, дополнительный флажок, показывающий нужно ли очищать поле ввода после нажатия следующей кнопки.

Для проверки того, вводится ли допустимое число, использован самый простой и естественный путь - регулярное выражение, полученное с помощью встроенного класса QRegExp.

Вот что у нас вышло:

Вид полученного приложения
Вид полученного приложения

Попробуйте воспроизвести этот пример - и минимальный опыт программирования в Qt у Вас появится :) Исходники калькулятора помощнее и с более стандартным видом - здесь.

Попробуй сделать сам

1. Добавить под кнопкой ”C” 4 кнопки для вычисления функций, например, синуса, косинуса, степени и натурального логарифма с помощью методов qSin, qCos, qPow, qLn:

#include <QTCore/qmath.h>

и реализовать их функционал. Как вариант, можно использовать стандартные функции Си из библиотеки math.h.

2. Добавить обработку нажатий клавиш с отображением введённых символов в метке QLabel. Для этого включить в класс Calculator.h прототип метода для обработки нажатий клавиш:

protected:
   virtual void keyPressEvent(QKeyEvent *event);

и подключить библиотеки

#include <QKeyEvent>
#include <Qt>

Реализовать в Calculator.cpp код метода:

void Calculator::keyPressEvent(QKeyEvent *event) {
 int key=event->key();//event->key() - целочисленный код клавиши
 if (key>=Qt::Key_0 && key<=Qt::Key_9) { //Цифровые клавиши 0..9
   QString str = QString(QChar(key));
   displaystring->setText(displaystring->text()+str);
 }
}

P.S. Здесь только набросок, а не решение!

Версия с дополнительной меткой, куда выводятся операнды, и обработкой нажатий клавиш

Код тоже учебный :) Вот изменённый calculator.h:

#ifndef _Calculator_h_
#define _Calculator_h_
#include <QWidget>
#include <QStack>
#include <QLabel>
#include <QPushButton>
#include <QGridLayout>
#include <QTCore/qmath.h>
#include <QKeyEvent>
#include <Qt>

 class Calculator : public QWidget {
  Q_OBJECT
  private:
   QLabel *firststring; //метка для вывода предыдущего операнда или операции
   QLabel *displaystring;
   QStack <QString> stack;
 protected:
    virtual void keyPressEvent(QKeyEvent *event); //Класс будет обрабатывать нажатия клавиш!
  public:
   Calculator (QWidget* pwgt = 0);
   QPushButton* createButton (const QString& str);
   void clearAll (); //Очистить всё
   void input(QString); //Обработка нажатия кнопки или клавиши
   void calculate (); //Вычисление
  public slots:
   void slotButtonClicked ();
 };
#endif

Изменённый calculator.cpp:

#include "calculator.h"

Calculator::Calculator (QWidget *parent) : QWidget(parent) {
 firststring = new QLabel(""); firststring->setMinimumSize (150, 50);
 displaystring = new QLabel(""); displaystring->setMinimumSize (150, 50);
 displaystring->setFont(QFont("sans", 14, QFont::Bold));
 QChar aButtons[4][4] = {
  {'7', '8', '9', '/'},
  {'4', '5', '6', '*'},
  {'1', '2', '3', '-'},
  {'0', '.', '=', '+'}
 };
 QGridLayout *myLayout = new QGridLayout;
 myLayout->addWidget(firststring, 0, 0, 1, 4);
 myLayout->addWidget(displaystring, 1, 0, 1, 4);
 myLayout->addWidget(createButton("CE"), 2, 3);
 for (int i = 0; i < 4; ++i) {
  for (int j = 0; j < 4; ++j) {
   myLayout->addWidget(createButton(aButtons[i][j]), i + 3, j);
  }
 }
 setLayout(myLayout);
}

QPushButton* Calculator::createButton (const QString& str) {
 QPushButton* pcmd = new QPushButton(str);
 pcmd->setMinimumSize(40, 30);
 connect(pcmd, SIGNAL(clicked()), this, SLOT(slotButtonClicked()));
 return pcmd;
}

void Calculator::calculate() {
 double dOperand2 = stack.pop().toDouble();
 QString strOperation = stack.pop();
 double dOperand1 = stack.pop().toDouble();
 double dResult = 0;
 if (strOperation == "+") { dResult = dOperand1 + dOperand2; }
 else if (strOperation == "-") { dResult = dOperand1 - dOperand2; }
 else if (strOperation == "/") { dResult = dOperand1 / dOperand2; }
 else if (strOperation == "*") { dResult = dOperand1 * dOperand2; }
 displaystring->setText(QString("%1").arg(dResult, 0, 'f', 3));
}

void Calculator::input(QString str) {
 QString text = displaystring->text(); //отображаемый текст
 int len = text.length();
 QString last = "";
 if (len>0) last = text.right(1); //самый правый символ ввода
 if (((len==0 && stack.count()==0) ||
      ((stack.count()==2 && len>1 && (last=="+"||last=="-"||last=="*"||last=="/"))))
     && (str.contains(QRegExp("[0-9]")) || str=="-")) {
  //На экране пусто и стек пуст или введен 1-й операнд и операция
  //и при этом нажата цифра или "-"
  firststring->setText(text);
  text=str; //Стереть то, что было отображено, и отобразить нажатый символ
 }
 else if ((text+str).contains(QRegExp("^-?[0-9]+\\.?[0-9]*$"))) {
  text+=str; //Пока вводим число - добавлять символ
 }
 else if (text.contains(QRegExp("^-?[0-9]+\\.?[0-9]*$"))) { //Уже набрано число
  if (str=="*"||str=="/"||str=="+"||str=="-"||str=="=") { //Вычислить
   if (stack.count()==2) { //Есть 1-й операнд и число
    stack.push(text); //Положить в стек 2-й операнд
    firststring->setText(firststring->text()+text+"="); //отобразить вычисляемое выражение
    calculate(); //Вычислить
    text=displaystring->text(); //Показать результат
   }
   if (str!="=") { //Для вычисления "по цепочке"
    stack.push(text); //Положить в стек 1-й операнд
    text+=str; //Отобразить операцию до след.нажатия кнопки
    stack.push(str); //Положить в стек операцию
    firststring->setText(text);
   }
  }
 }
 displaystring->setText(text);
}


void Calculator::clearAll() {
  stack.clear(); displaystring->setText(""); firststring->setText("");
}

void Calculator::slotButtonClicked() {
 QString str = ((QPushButton*)sender())->text(); //Получаем текст с нажатой кнопки
 if (str == "CE") clearAll();
 else input(str);
}

void Calculator::keyPressEvent(QKeyEvent *event) {
 int key=event->key();//event->key() - целочисленный код клавиши
 QString str = QString(QChar(key));
 if (key==Qt::Key_Delete) clearAll(); //Так проверять коды клавиш
 else input(str);
}

P.S. Замена QRegExp на QRegularExpression в Qt6.

 Qt6: пишем калькулятор (файл .pdf) (263 Кб)

 Скачать проект Qt6 из этой статьи в архиве .zip, развернуть в новую папку проекта (3 Кб)

25.04.2014, 19:19 [48860 просмотров]


теги: c++ программирование qt

показать комментарии (2)