from dash import (
Dash,
html,
dcc,
Output,
Input,
State,
callback,
no_update,
ctx,
)
from dash.exceptions import PreventUpdate
import dash_mantine_components as dmc
import traceback
import math
# Estilos para Mantine
stylesheets = [
dmc.styles.DATES,
dmc.styles.CODE_HIGHLIGHT,
dmc.styles.CHARTS,
dmc.styles.CAROUSEL,
dmc.styles.NOTIFICATIONS,
dmc.styles.NPROGRESS,
]
# Inicialización de la aplicación
app = Dash(
__name__,
external_stylesheets=stylesheets,
title="Calculadora - Dash Mantine",
update_title=None,
suppress_callback_exceptions=True,
)
server = app.server
# Función para crear botones de la calculadora
def create_button(label, button_id, color="gray", variant="filled", size="lg"):
return dmc.Button(
label,
id=button_id,
color=color,
variant=variant,
size=size,
style={"height": "60px", "fontSize": "18px", "fontWeight": "bold"},
fullWidth=True
)
# Layout principal
app.layout = dmc.MantineProvider(
forceColorScheme="dark",
children=[
dmc.Container([
dmc.Title("Calculadora Profesional", order=1, ta="center", mb="lg"),
# Pantalla de la calculadora
dmc.Paper([
dmc.TextInput(
id="display",
value="0",
readOnly=True,
size="xl",
style={
"fontSize": "24px",
"fontWeight": "bold",
"textAlign": "right",
"backgroundColor": "#2e2e2e",
"color": "#fff",
"border": "2px solid #404040"
}
),
dmc.Space(h=20),
# Fila 1: Funciones especiales
dmc.Grid([
dmc.GridCol(create_button("C", "btn-clear", "red", "filled"), span=3),
dmc.GridCol(create_button("±", "btn-plusminus", "orange", "filled"), span=3),
dmc.GridCol(create_button("%", "btn-percent", "orange", "filled"), span=3),
dmc.GridCol(create_button("÷", "btn-divide", "orange", "filled"), span=3),
], gutter="sm"),
dmc.Space(h=10),
# Fila 2: 7, 8, 9, ×
dmc.Grid([
dmc.GridCol(create_button("7", "btn-7", "blue", "filled"), span=3),
dmc.GridCol(create_button("8", "btn-8", "blue", "filled"), span=3),
dmc.GridCol(create_button("9", "btn-9", "blue", "filled"), span=3),
dmc.GridCol(create_button("×", "btn-multiply", "orange", "filled"), span=3),
], gutter="sm"),
dmc.Space(h=10),
# Fila 3: 4, 5, 6, −
dmc.Grid([
dmc.GridCol(create_button("4", "btn-4", "blue", "filled"), span=3),
dmc.GridCol(create_button("5", "btn-5", "blue", "filled"), span=3),
dmc.GridCol(create_button("6", "btn-6", "blue", "filled"), span=3),
dmc.GridCol(create_button("−", "btn-subtract", "orange", "filled"), span=3),
], gutter="sm"),
dmc.Space(h=10),
# Fila 4: 1, 2, 3, +
dmc.Grid([
dmc.GridCol(create_button("1", "btn-1", "blue", "filled"), span=3),
dmc.GridCol(create_button("2", "btn-2", "blue", "filled"), span=3),
dmc.GridCol(create_button("3", "btn-3", "blue", "filled"), span=3),
dmc.GridCol(create_button("+", "btn-add", "orange", "filled"), span=3),
], gutter="sm"),
dmc.Space(h=10),
# Fila 5: 0, ., =
dmc.Grid([
dmc.GridCol(create_button("0", "btn-0", "blue", "filled"), span=6),
dmc.GridCol(create_button(".", "btn-decimal", "gray", "filled"), span=3),
dmc.GridCol(create_button("=", "btn-equals", "green", "filled"), span=3),
], gutter="sm"),
dmc.Space(h=20),
# Funciones científicas adicionales
dmc.Text("Funciones Científicas:", size="sm", fw=500, mb="xs"),
dmc.Grid([
dmc.GridCol(create_button("√", "btn-sqrt", "violet", "outline", "md"), span=3),
dmc.GridCol(create_button("x²", "btn-square", "violet", "outline", "md"), span=3),
dmc.GridCol(create_button("1/x", "btn-reciprocal", "violet", "outline", "md"), span=3),
dmc.GridCol(create_button("⌫", "btn-backspace", "red", "outline", "md"), span=3),
], gutter="sm"),
], p="xl", shadow="md", style={"maxWidth": "400px", "margin": "0 auto"}),
# Store para mantener el estado
dcc.Store(id="calculator-state", data={
"display": "0",
"previous_value": None,
"operation": None,
"waiting_for_operand": False,
"history": []
}),
dmc.Space(h=20),
# Historial de operaciones
dmc.Paper([
dmc.Text("Historial de Operaciones:", size="sm", fw=500, mb="xs"),
html.Div(id="history-display", style={"maxHeight": "150px", "overflowY": "auto"})
], p="md", shadow="sm", style={"maxWidth": "400px", "margin": "0 auto"}),
], size="sm", py="xl")
],
)
# Callback principal para manejar todos los botones
@callback(
[Output("display", "value"),
Output("calculator-state", "data"),
Output("history-display", "children")],
[Input("btn-0", "n_clicks"), Input("btn-1", "n_clicks"), Input("btn-2", "n_clicks"),
Input("btn-3", "n_clicks"), Input("btn-4", "n_clicks"), Input("btn-5", "n_clicks"),
Input("btn-6", "n_clicks"), Input("btn-7", "n_clicks"), Input("btn-8", "n_clicks"),
Input("btn-9", "n_clicks"), Input("btn-decimal", "n_clicks"), Input("btn-add", "n_clicks"),
Input("btn-subtract", "n_clicks"), Input("btn-multiply", "n_clicks"), Input("btn-divide", "n_clicks"),
Input("btn-equals", "n_clicks"), Input("btn-clear", "n_clicks"), Input("btn-plusminus", "n_clicks"),
Input("btn-percent", "n_clicks"), Input("btn-sqrt", "n_clicks"), Input("btn-square", "n_clicks"),
Input("btn-reciprocal", "n_clicks"), Input("btn-backspace", "n_clicks")],
[State("calculator-state", "data")],
prevent_initial_call=True
)
def handle_calculator(btn0, btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8, btn9,
decimal, add, subtract, multiply, divide, equals, clear,
plusminus, percent, sqrt, square, reciprocal, backspace, state):
if not ctx.triggered:
return no_update, no_update, no_update
button_id = ctx.triggered[0]['prop_id'].split('.')[0]
# Inicializar estado si es necesario
if not state:
state = {
"display": "0",
"previous_value": None,
"operation": None,
"waiting_for_operand": False,
"history": []
}
display = state["display"]
previous_value = state["previous_value"]
operation = state["operation"]
waiting_for_operand = state["waiting_for_operand"]
history = state.get("history", [])
try:
# Números (0-9)
if button_id.startswith("btn-") and button_id[-1].isdigit():
digit = button_id[-1]
if waiting_for_operand:
display = digit
waiting_for_operand = False
else:
display = digit if display == "0" else display + digit
# Punto decimal
elif button_id == "btn-decimal":
if waiting_for_operand:
display = "0."
waiting_for_operand = False
elif "." not in display:
display += "."
# Operaciones básicas
elif button_id in ["btn-add", "btn-subtract", "btn-multiply", "btn-divide"]:
operations = {
"btn-add": "+",
"btn-subtract": "-",
"btn-multiply": "×",
"btn-divide": "÷"
}
if previous_value is None:
previous_value = float(display)
elif not waiting_for_operand:
# Realizar operación pendiente
current_value = float(display)
if operation == "+":
result = previous_value + current_value
elif operation == "-":
result = previous_value - current_value
elif operation == "×":
result = previous_value * current_value
elif operation == "÷":
if current_value != 0:
result = previous_value / current_value
else:
return "Error", state, format_history(history)
display = str(result)
previous_value = result
history.append(f"{previous_value} {operation} {current_value} = {result}")
operation = operations[button_id]
waiting_for_operand = True
# Igual
elif button_id == "btn-equals":
if operation and previous_value is not None and not waiting_for_operand:
current_value = float(display)
if operation == "+":
result = previous_value + current_value
elif operation == "-":
result = previous_value - current_value
elif operation == "×":
result = previous_value * current_value
elif operation == "÷":
if current_value != 0:
result = previous_value / current_value
else:
return "Error", state, format_history(history)
display = str(result)
history.append(f"{previous_value} {operation} {current_value} = {result}")
previous_value = None
operation = None
waiting_for_operand = True
# Limpiar
elif button_id == "btn-clear":
display = "0"
previous_value = None
operation = None
waiting_for_operand = False
history = []
# Cambiar signo
elif button_id == "btn-plusminus":
if display != "0":
display = str(-float(display))
# Porcentaje
elif button_id == "btn-percent":
display = str(float(display) / 100)
# Raíz cuadrada
elif button_id == "btn-sqrt":
value = float(display)
if value >= 0:
result = math.sqrt(value)
display = str(result)
history.append(f"√{value} = {result}")
waiting_for_operand = True
else:
return "Error", state, format_history(history)
# Cuadrado
elif button_id == "btn-square":
value = float(display)
result = value ** 2
display = str(result)
history.append(f"{value}² = {result}")
waiting_for_operand = True
# Recíproco
elif button_id == "btn-reciprocal":
value = float(display)
if value != 0:
result = 1 / value
display = str(result)
history.append(f"1/{value} = {result}")
waiting_for_operand = True
else:
return "Error", state, format_history(history)
# Backspace
elif button_id == "btn-backspace":
if len(display) > 1:
display = display[:-1]
else:
display = "0"
# Formatear display para mostrar números más legibles
try:
if "." in display:
# Eliminar ceros innecesarios al final
display = str(float(display)).rstrip('0').rstrip('.')
else:
display = str(int(float(display)))
except:
pass
# Actualizar estado
new_state = {
"display": display,
"previous_value": previous_value,
"operation": operation,
"waiting_for_operand": waiting_for_operand,
"history": history[-10:] # Mantener solo las últimas 10 operaciones
}
return display, new_state, format_history(history[-10:])
except Exception as e:
print(f"Error en calculadora: {e}")
return "Error", state, format_history(history)
def format_history(history):
if not history:
return dmc.Text("No hay operaciones aún", size="sm", c="dimmed")
return [
dmc.Text(operation, size="sm", mb="xs")
for operation in reversed(history)
]
if __name__ == "__main__":
app.run(debug=True)