Пример "Calculator"
Файлы:
Пример показывает, как использовать сигналы и слоты для реализации функциональности виджета калькулятора и как использовать QGridLayout для размещения дочерних виджетов в сетке. Пример состоит из двух классов:
Начнём с обзора Calculator, затем изучим Button. Определение класса Calculatorclass Calculator : public QDialog { Q_OBJECT public: Calculator(QWidget *parent = 0); 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, x?, 1/x), аддитивные операторы (+, -) и мультипликативные операторы (?, ?). У остальных кнопок имеются собственные слоты. private: Button *createButton(const QString &text, const QColor &color, const char *member); void abortOperation(); bool calculate(double rightOperand, const QString &pendingOperator); Закрытая функция createButton() используется как часть структуры виджета. abortOperation() вызывается всякий раз, когда происходит деление на ноль или когда выполняется операция извлечения квадратного корня из отрицательного числа. calculate() использует бинарный оператор (+, -, ?, or ?). double sumInMemory; double sumSoFar; double factorSoFar; QString pendingAdditiveOperator; QString pendingMultiplicativeOperator; bool waitingForOperand; Эти переменные вместе с содержимым экрана калькулятора (QLineEdit), кодируют состояние калькулятора:
Аддитивные и мультипликативные операторы рассматриваются по-разному поскольку у них разный приоритет. Например, 1 + 2 ? 3 интерпретируется как 1 + (2 ? 3) поскольку ? имеет более высокий приоритет, чем +. В таблице ниже показана эволюция состояния калькулятора как пользователь вводит математическое выражение.
Унарные операторы, такие как Sqrt, не требуют особой обработки; они могут быть выполнены немедленно как только операнд станет известен при щелчке по кнопке оператора. QLineEdit *display; enum { NumDigitButtons = 10 }; Button *digitButtons[NumDigitButtons]; }; В заключение, объявляем переменные, связанные с экраном и кнопками, используемыми для отображения цифр. Реализация класса CalculatorCalculator::Calculator(QWidget *parent) : QDialog(parent) { sumInMemory = 0.0; sumSoFar = 0.0; factorSoFar = 0.0; waitingForOperand = true; В конструкторе инициализируем состояние калькулятора. Переменные pendingAdditiveOperator и pendingMultiplicativeOperator не нужно инициализировать явно, поскольку конструктор QString инициализирует их пустой строкой. 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 изображающий экран калькулятора и настраиваем некоторые его свойства. В частности, мы установили режим только для чтения. Также мы увеличили шрифт экрана на 8 пунктов. QColor digitColor(150, 205, 205); QColor backspaceColor(225, 185, 135); QColor memoryColor(100, 155, 155); QColor operatorColor(155, 175, 195); for (int i = 0; i < NumDigitButtons; ++i) { digitButtons[i] = createButton(QString::number(i), digitColor, SLOT(digitClicked())); } Button *pointButton = createButton(tr("."), digitColor, SLOT(pointClicked())); Button *changeSignButton = createButton(tr("\261"), digitColor, SLOT(changeSignClicked())); Button *backspaceButton = createButton(tr("Backspace"), backspaceColor, SLOT(backspaceClicked())); Button *clearButton = createButton(tr("Clear"), backspaceColor, SLOT(clear())); Button *clearAllButton = createButton(tr("Clear All"), backspaceColor.light(120), SLOT(clearAll())); Button *clearMemoryButton = createButton(tr("MC"), memoryColor, SLOT(clearMemory())); Button *readMemoryButton = createButton(tr("MR"), memoryColor, SLOT(readMemory())); Button *setMemoryButton = createButton(tr("MS"), memoryColor, SLOT(setMemory())); Button *addToMemoryButton = createButton(tr("M+"), memoryColor, SLOT(addToMemory())); Button *divisionButton = createButton(tr("\367"), operatorColor, SLOT(multiplicativeOperatorClicked())); Button *timesButton = createButton(tr("\327"), operatorColor, SLOT(multiplicativeOperatorClicked())); Button *minusButton = createButton(tr("-"), operatorColor, SLOT(additiveOperatorClicked())); Button *plusButton = createButton(tr("+"), operatorColor, SLOT(additiveOperatorClicked())); Button *squareRootButton = createButton(tr("Sqrt"), operatorColor, SLOT(unaryOperatorClicked())); Button *powerButton = createButton(tr("x\262"), operatorColor, SLOT(unaryOperatorClicked())); Button *reciprocalButton = createButton(tr("1/x"), operatorColor, SLOT(unaryOperatorClicked())); Button *equalButton = createButton(tr("="), operatorColor.light(120), SLOT(equalClicked())); We define four colors by specifying the red, green, and blue components on a scale from 0 to 255. Then, for each button, we call the private createButton() function with the proper text label, the associated color, and a slot to connect to the button. To make the Clear All and = buttons stand out, we call QColor::light() with a factor of 120%, making these buttons 20% brighter than their neighbors. 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 всегда выводится на экран с оптимальными размерами (его подсказка размера), не допуская изменения размеров калькулятора. Подсказка размера определяется размерами и политикой размера дочерних виджетов. Большинство дочерних виджетов занимают только одну ячейку в компоновке-сетке. Поэтому нам нужно передавать только строку и столбец в QGridLayout::addWidget(). Виджеты display, backspaceButton, clearButton и clearAllButton занимают более одного столбца; для них мы должны также передавать объединение ячеек по вертикали (row span) и горизонтали (column span). 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 или же static_cast<>() C++, но в качестве защитного приёма программирования мы используем 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 = sqrt(operand); } else if (clickedOperator == tr("x\262")) { result = 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 - к нулю, то вызываем abortOperation(). Если все прошло нормально, выводим на экран результат операции в однострочное поле ввод и устанавливаем waitingForOperand в значение true. Это гарантирует, что если пользователь наберёт новую цифру, цифра будет рассматриваться как новый операнд, а ее добавится к текущему значению. void Calculator::additiveOperatorClicked() { Button *clickedButton = qobject_cast<Button *>(sender()); 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()); 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 и сбрасываем значение переменной в ноль. Сброс переменной в ноль необходим чтобы избежать повторного вычисления значения. 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" и устанавливаем waitingForOperand в значение true. void Calculator::clear() { if (waitingForOperand) return; display->setText("0"); waitingForOperand = true; } Слот clear() сбрасывает значение текущего операнда в ноль. Это эквивалентно нажатию на кнопку 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 и значения на экране. Button *Calculator::createButton(const QString &text, const QColor &color, const char *member) { Button *button = new Button(text, color); connect(button, SIGNAL(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("\327")) { factorSoFar *= rightOperand; } else if (pendingOperator == tr("\367")) { if (rightOperand == 0.0) return false; factorSoFar /= rightOperand; } return true; } Закрытая функция calculate() выполняет бинарную операцию. Правый операнд предоставляется rightOperand. Для аддитивных операторов левым операндом является sumSoFar; для мультипликативных операторов левым оператором является factorSoFar. Функция возвращает false в случае деления на ноль. Определение класса ButtonДавайте теперь рассмотрим класс Button: class Button : public QToolButton { Q_OBJECT public: Button(const QString &text, const QColor &color, QWidget *parent = 0); QSize sizeHint() const; }; The Button class has a convenience constructor that takes a text label and a color, and it reimplements QWidget::sizeHint() to provide more space around the text than what QToolButton normally provides. Реализация класса ButtonButton::Button(const QString &text, const QColor &color, QWidget *parent) : QToolButton(parent) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); setText(text); QPalette newPalette = palette(); newPalette.setColor(QPalette::Button, color); setPalette(newPalette); } Внешний вид кнопок определяется компоновкой виджета калькулятора посредством размера и политики размера дочерних виджетов компоновки. Вызов функции setSizePolicy() в конструкторе гарантирует, что кнопка будет растянута по горизонтали чтобы заполнить все доступное пространство; по умолчанию, инструментальные кнопки QToolButton не растягиваются для заполнения всего доступного пространства. Без этого вызова разные кнопки в одном и том же столбце будут иметь разную ширину. QSize Button::sizeHint() const { QSize size = QToolButton::sizeHint(); size.rheight() += 20; size.rwidth() = qMax(size.width(), size.height()); return size; } В sizeHint() мы стараемся вернуть размер, который лучше всего подходит для большинства кнопок. Мы используем повторно подсказку размера базового класса (QToolButton), но модифицировали её следующим образом:
Этим обеспечиваем квадратную форму кнопок цифр и операторов с большинством шрифтов, без усечения текста в кнопках Backspace, Clear и Clear All buttons. Снимок экрана ниже показывает, как виджет Calculator будет выглядеть, если мы не установим горизонтальную политику размера QSizePolicy::Expanding в конструкторе и если мы не переопределим QWidget::sizeHint().
|
Попытка перевода Qt документации. Если есть желание присоединиться, или если есть замечания или пожелания, то заходите на форум: Перевод Qt документации на русский язык... Люди внесшие вклад в перевод: Команда переводчиков |