En esta página

Ejemplo de calculadora

El ejemplo muestra cómo utilizar señales y ranuras para implementar la funcionalidad de un widget de calculadora, y cómo utilizar QGridLayout para colocar widgets hijos en una cuadrícula.

Aplicación básica de calculadora

Captura de pantalla del ejemplo de calculadora

El ejemplo consta de dos clases:

  • Calculator es el widget calculadora, con toda la funcionalidad de la calculadora.
  • Button es el widget utilizado para cada uno de los botones de la calculadora. Deriva de QToolButton.

Comenzaremos revisando Calculator, luego echaremos un vistazo a Button.

Definición de la clase Calculadora

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

La clase Calculator proporciona un widget de calculadora simple. Hereda de QDialog y tiene varias ranuras privadas asociadas con los botones de la calculadora. QObject::eventFilter() se reimplementa para manejar los eventos del ratón en la pantalla de la calculadora.

Los botones se agrupan en categorías según su comportamiento. Por ejemplo, todos los botones de dígitos (etiquetados 0 a 9) añaden un dígito al operando actual. Para ellos, conectamos varios botones a la misma ranura (por ejemplo, digitClicked()). Las categorías son dígitos, operadores unarios (Sqrt, , 1/x), operadores aditivos (+, -) y operadores multiplicativos (×, ÷). Los demás botones tienen sus propias ranuras.

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

La función privada createButton() se utiliza como parte de la construcción del widget. abortOperation() se llama siempre que se produce una división por cero o cuando se aplica una operación de raíz cuadrada a un número negativo. calculate() aplica un operador binario (+, -, ×, o ÷).

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

Estas variables, junto con el contenido de la pantalla de la calculadora (a QLineEdit), codifican el estado de la calculadora:

  • sumInMemory contiene el valor almacenado en la memoria de la calculadora (mediante MS, M+, o MC).
  • sumSoFar almacena el valor acumulado hasta el momento. Cuando el usuario pulsa =, sumSoFar se vuelve a calcular y se muestra en la pantalla. Clear All pone a cero sumSoFar.
  • factorSoFar almacena un valor temporal cuando se realizan multiplicaciones y divisiones.
  • pendingAdditiveOperator almacena el último operador aditivo pulsado por el usuario.
  • pendingMultiplicativeOperator almacena el último operador multiplicativo pulsado por el usuario.
  • waitingForOperand es true cuando la calculadora espera que el usuario empiece a escribir un operando.

Los operadores aditivos y multiplicativos se tratan de forma diferente porque tienen precedencias distintas. Por ejemplo, 1 + 2 ÷ 3 se interpreta como 1 + (2 ÷ 3) porque ÷ tiene mayor precedencia que +.

La siguiente tabla muestra la evolución del estado de la calculadora a medida que el usuario introduce una expresión matemática.

Entrada del usuarioVisualizaciónSuma hasta ahoraSuma Op.Factor hasta ahoraMult. Op.¿Esperando Operando?
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

Los operadores unarios, como Sqrt, no requieren ningún tratamiento especial; pueden aplicarse inmediatamente puesto que el operando ya se conoce cuando se pulsa el botón del operador.

    QLineEdit *display;

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

Por último, declaramos las variables asociadas a la pantalla y a los botones utilizados para mostrar los numerales.

Implementación de la Clase Calculadora

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

En el constructor inicializamos el estado de la calculadora. Las variables pendingAdditiveOperator y pendingMultiplicativeOperator no necesitan ser inicializadas explícitamente, porque el constructor QString las inicializa a cadenas vacías. También es posible inicializar esas variables directamente en la cabecera. Esto se llama member-initializaton y evita una larga lista de inicialización.

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

Creamos el QLineEdit que representa la pantalla de la calculadora y configuramos algunas de sus propiedades. En particular, establecemos que sea de sólo lectura.

También ampliamos la fuente de display en 8 puntos.

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

Para cada botón, llamamos a la función privada createButton() con la etiqueta de texto adecuada y una ranura para conectar con el botón.

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

El diseño se gestiona con un único QGridLayout. La llamada a QLayout::setSizeConstraint() garantiza que el widget Calculator se muestre siempre en su tamaño óptimo (su size hint), evitando que el usuario cambie el tamaño de la calculadora. La sugerencia de tamaño viene determinada por el tamaño y size policy de los widgets hijos.

La mayoría de los widgets hijos ocupan sólo una celda en el diseño de la rejilla. Para ellos, sólo necesitamos pasar una fila y una columna a QGridLayout::addWidget(). Los widgets display, backspaceButton, clearButton, y clearAllButton ocupan más de una columna; para ellos también debemos pasar un tramo de fila y un tramo de columna.

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

Al pulsar uno de los botones de dígitos de la calculadora se emitirá la señal clicked() del botón, que activará la ranura digitClicked().

En primer lugar, averiguamos qué botón envió la señal utilizando QObject::sender(). Esta función devuelve el emisor como un puntero QObject. Como sabemos que el remitente es un objeto Button, podemos hacer un casting seguro de QObject. Podríamos haber utilizado un cast estilo C o un C++ static_cast<>(), pero como técnica de programación defensiva utilizamos qobject_cast(). La ventaja es que si el objeto es de tipo incorrecto, se devuelve un puntero nulo. Los fallos debidos a punteros nulos son mucho más fáciles de diagnosticar que los fallos debidos a castings inseguros. Una vez que tenemos el botón, extraemos el operador utilizando QToolButton::text().

La ranura necesita considerar dos situaciones en particular. Si display contiene "0" y el usuario pulsa el botón 0, sería una tontería mostrar "00". Y si la calculadora está en un estado en el que está esperando un nuevo operando, el nuevo dígito es el primer dígito de ese nuevo operando; en ese caso, cualquier resultado de un cálculo anterior debe ser borrado primero.

Al final, añadimos el nuevo dígito al valor de la pantalla.

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

La ranura unaryOperatorClicked() se activa cada vez que se pulsa uno de los botones del operador unario. De nuevo se recupera un puntero al botón pulsado utilizando QObject::sender(). El operador se extrae del texto del botón y se almacena en clickedOperator. El operando se obtiene de display.

A continuación se realiza la operación. Si Sqrt se aplica a un número negativo o 1/x a cero, llamamos a abortOperation(). Si todo va bien, mostramos el resultado de la operación en la línea de edición y establecemos waitingForOperand en true. Esto asegura que si el usuario teclea un nuevo dígito, éste será considerado como un nuevo operando, en lugar de ser anexado al valor actual.

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

La ranura additiveOperatorClicked() se activa cuando el usuario hace clic en el botón + o -.

Antes de que podamos hacer algo con el operador pulsado, debemos manejar cualquier operación pendiente. Comenzamos con los operadores multiplicativos, ya que tienen mayor precedencia que los operadores aditivos:

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

Si se ha pulsado antes × o ÷, sin pulsar después =, el valor actual de la pantalla es el operando derecho del operador × o ÷ y podemos finalmente realizar la operación y actualizar la pantalla.

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

Si se ha pulsado antes + o -, sumSoFar es el operando izquierdo y el valor actual de la pantalla es el operando derecho del operador. Si no hay ningún operador aditivo pendiente, sumSoFar se establece simplemente como el texto de la pantalla.

    pendingAdditiveOperator = clickedOperator;
    waitingForOperand = true;
}

Por último, podemos ocuparnos del operador que se acaba de pulsar. Como aún no tenemos el operando de la derecha, almacenamos el operador pulsado en la variable pendingAdditiveOperator. Aplicaremos la operación más tarde, cuando tengamos un operando derecho, con sumSoFar como operando izquierdo.

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

La ranura multiplicativeOperatorClicked() es similar a additiveOperatorClicked(). No necesitamos preocuparnos de los operadores aditivos pendientes aquí, porque los operadores multiplicativos tienen precedencia sobre los operadores aditivos.

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

Como en additiveOperatorClicked(), comenzamos manejando cualquier operador multiplicativo y aditivo pendiente. Luego mostramos sumSoFar y ponemos la variable a cero. Poner la variable a cero es necesario para evitar contar el valor dos veces.

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

La ranura pointClicked() añade un punto decimal al contenido en 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);
}

La ranura changeSignClicked() cambia el signo del valor en display. Si el valor actual es positivo, añadimos un signo menos; si el valor actual es negativo, eliminamos el primer carácter del valor (el signo menos).

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

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

En backspaceClicked() eliminamos el carácter situado más a la derecha. Si obtenemos una cadena vacía, mostramos "0" y ponemos waitingForOperand en true.

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

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

La ranura clear() pone a cero el operando actual. Es equivalente a pulsar Backspace suficientes veces para borrar todo el operando.

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

La ranura clearAll() reinicia la calculadora a su estado inicial.

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

La ranura clearMemory() borra la suma guardada en memoria, readMemory() muestra la suma como operando, setMemory() reemplaza la suma en memoria por la suma actual, y addToMemory() añade el valor actual al valor en memoria. Para setMemory() y addToMemory(), comenzamos llamando a equalClicked() para actualizar sumSoFar y el valor en la pantalla.

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

La función privada createButton() es llamada desde el constructor para crear los botones de la calculadora.

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

La función privada abortOperation() es llamada cada vez que falla un cálculo. Reinicia el estado de la calculadora y muestra "####".

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

La función privada calculate() realiza una operación binaria. El operando derecho viene dado por rightOperand. Para los operadores aditivos, el operando izquierdo es sumSoFar; para los operadores multiplicativos, el operando izquierdo es factorSoFar. La función devuelve false si se produce una división por cero.

Definición de la clase Button

Veamos ahora la clase Button:

class Button : public QToolButton
{
    Q_OBJECT

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

    QSize sizeHint() const override;
};

La clase Button tiene un constructor conveniente que toma una etiqueta de texto y un widget padre, y reimplementa QWidget::sizeHint() para proporcionar más espacio alrededor del texto que la cantidad que QToolButton proporciona normalmente.

Implementación de la clase Button

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

La apariencia de los botones está determinada por el diseño del widget calculadora a través del tamaño y size policy de los widgets hijos del diseño. La llamada a la función setSizePolicy() en el constructor asegura que el botón se expandirá horizontalmente para llenar todo el espacio disponible; por defecto, QToolButtons no se expanden para llenar el espacio disponible. Sin esta llamada, los distintos botones de una misma columna tendrían anchuras diferentes.

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

En sizeHint(), intentamos devolver un tamaño que se vea bien para la mayoría de los botones. Reutilizamos la sugerencia de tamaño de la clase base (QToolButton) pero la modificamos de la siguiente manera:

  • Añadimos 20 al componente height de la sugerencia de tamaño.
  • Hacemos que el componente width de la sugerencia de tamaño sea al menos tan grande como height.

Esto garantiza que, con la mayoría de los tipos de letra, los botones de dígitos y operadores serán cuadrados, sin truncar el texto de los botones Backspace, Clear y Clear All.

La siguiente captura de pantalla muestra el aspecto que tendría el widget Calculator si no estableciéramos la política de tamaño horizontal en QSizePolicy::Expanding en el constructor y si no reimplementáramos QWidget::sizeHint().

Calculadora con botones desalineados

Ejemplo de calculadora con políticas de tamaño y sugerencias de tamaño predeterminadas

Proyecto de ejemplo @ code.qt.io

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