¿Cómo hacer una calculadora con interfaz gráfica para DOS?

Mensajes
1,139
Oro
234,993
En este corto texto vamos a ver cómo programar desde cero una sencilla calculadora con interfaz gráfica de usuario para un entorno como MS DOS en donde esta no existe, y vamos a hacerlo usando QBasic (o QuickBasic si deseamos compilar); como muchos deben saber ese entorno de desarrollo de la era de MS DOS no provee tampoco la posibilidad de una interfaz gráfica si no la programamos usando rutinas gráficas, lo cual se ha hecho en este caso.

En realidad no podré poner todos los detalles de la creación de la GUI por obvios motivos, sin embargo, sí veremos todo el código de la calculadora, y de todos modos el código fuente completo estará disponible.

Nota: El código mostrado en este artículo fue tomado de un tutorial más amplio y detallado dedicado a la creación paso a paso de una interfaz gráfica de usuario (GUI) con QBasic.

En resumen, una vez terminado nuestro pequeño programa deberá mostrarse como en la Figura 1 a continuación si lo corremos en MS DOS (o en FreeDos o DOSBox):

Calc_with_GUI_in_QBasic.jpg
Figura 1: La calculadora básica desarrollada en QBasic usando la GUI.
En todo caso, entre los archivos fuente de la calculadora simple, también se ha incluido un programa compilado con QB64 para permitir su corrida en un Windows de 64 bits moderno, además del compilado con QuickBasic de 16 bits para MS DOS clasico.

En primer lugar, para empezar vamos a ver el código del programa principal, situado en la sección con ese mismo nombre señalizada con un comentario:

'******************************************************

'* Código del programa principal *

'******************************************************



‘El modo de video 9 (640 x 350 pixels con 16 colores) es establecido (esto es más bien parte de la GUI, no obstante, se hace en la

‘misma sección del programa)

SCREEN 9, , 1, 0



‘Las dimensiones actuales de la pantalla se guardan en las variables correspondientes

XResolution = 640

YResolution = 350



CLS



‘Las constantes para las dimensiones de la ventana de la calculadora

CONST WINDOWWIDTH = 148

CONST WINDOWHEIGHT = 220



‘Las constantes para los tipos de entradas a ser recibidas por la calculadora (refleja los tipos de operaciones a realizar con los

‘distintos botones)

CONST LI.CLEAR = 1 '(LI = Last Input)

CONST LI.NUMBER = 2

CONST LI.OPERATOR = 3



‘Las constantes para las operaciones matemáticas a ser realizadas por la calculadora (en este caso son las operaciones como tal)

CONST LO.NOTHING = 1 '(LO = Last Operator)

CONST LO.MUL = 2

CONST LO.DIV = 3

CONST LO.PLUS = 4

CONST LO.MINUS = 5

CONST LO.SQR = 6

CONST LO.PERCENT = 7

CONST LO.INVSGN = 8



‘Las constantes para el estado de operación de la calculadora

CONST OS.NORMAL = 1 '(OS = Operation State)

CONST OS.ERROR = 2



‘La constante para la cantidad de decimales mostrados en la pantalla de la calculadora cuando se da la respuesta,

‘pueden cambiarla para obtener más decimales dentro de lo posible dada la precisión del propio BASIC usado

CONST DISPLAYPRECISION = 4



‘Las variables para retener las constantes antes mencionadas y también para los cálculos intermedios (AuxMem), con las cuales se

‘consigue seguir la pista del estado de las operaciones

DIM SHARED LastInput AS INTEGER ‘La variable para retener el tipo de la última entrada

DIM SHARED LastOperator AS INTEGER ‘La variable para retener el último operador solicitado

DIM SHARED AuxMem AS DOUBLE ‘La variable acumulador para los cálculos intermedios

DIM SHARED OperationState AS INTEGER ‘La variable para el estado de la operación

DIM SHARED OperationCode AS INTEGER ‘La variable para el código de operación (es un parche debido a un error de operación)



‘Las variables para la creación de la ventana y los controles gráficos sobre esta

DIM Rect AS RectType

DIM WindowHandle AS LONG

DIM TextBoxHandle AS LONG

DIM ButtonHandle AS LONG



‘Los valores iniciales de las variables son establecidos

LastInput = LI.CLEAR ‘La última entrada al inicio se considera LI.CLEAR, como si se hubiera presionado el botón “C” de la ventana

LastOperator = LO.NOTHING ‘El ultimo operador al inicio se considera LO.NOTHING, o sea, ninguno

AuxMem = 0 ‘La memoria auxiliar o acumulador se inicia en 0

OperationState = OS.NORMAL ‘El estado al inicio se considera OS.NORMAL o estado normal



‘Los valores del recuadro de la ventana son asignados; con esto se consigue situarla en medio de la pantalla al crearla

Rect.Left = XResolution / 2 - WINDOWWIDTH / 2

Rect.Top = YResolution / 2 - WINDOWHEIGHT / 2

Rect.Right = WINDOWWIDTH

Rect.Bottom = WINDOWHEIGHT



‘La ventana es creada con la cadena de texto “Calculadora” por título y la cadena de texto “MainFRM” como su nombre

WindowHandle = CreateWindow(Rect, "Calculadora", "MainFRM")



‘Los valores del recuadro del cuadro de texto de la pantalla de la calculadora son asignados

Rect.Left = 5

Rect.Top = 4

Rect.Right = 134

Rect.Bottom = 16



‘El cuadro de texto de la pantalla de la calculadora es creado, se hace inactivo, y se alinea el texto en su interior a la derecha

TextBoxHandle = CreateTextBox(Rect, "0", "Display", WindowHandle)

SetEnabled TextBoxHandle, FALSE, WSYS.GRAPHICCKTEXTBOX

SetTextAlign TextBoxHandle, WSYS.TEXTRIGHTJUSTIFY, WSYS.GRAPHICCKTEXTBOX



‘Los valores del recuadro del botón “C” (Limpiar) son asignados, y dicho botón es creado

Rect.Left = 5

Rect.Top = 30

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "C", "btnCLEAR", WindowHandle)



‘Los valores del recuadro del botón “√” (Raíz cuadrada) son asignados, y dicho botón es creado

Rect.Left = 41

Rect.Top = 30

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "√", "btnSQR", WindowHandle)



‘Los valores del recuadro del botón “%” (Porciento) son asignados, y dicho botón es creado

Rect.Left = 77

Rect.Top = 30

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "%", "btnPRCT", WindowHandle)



‘Los valores del recuadro del botón “¸” (Dividir) son asignados, y dicho botón es creado

Rect.Left = 113

Rect.Top = 30

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "¸", "btnDIV", WindowHandle)



‘Los valores del recuadro del botón “7” son asignados, y dicho botón es creado

Rect.Left = 5

Rect.Top = 64

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "7", "btnSEVEN", WindowHandle)



‘Los valores del recuadro del botón “8” son asignados, y dicho botón es creado

Rect.Left = 41

Rect.Top = 64

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "8", "btnEIGHT", WindowHandle)



‘Los valores del recuadro del botón “9” son asignados, y dicho botón es creado

Rect.Left = 77

Rect.Top = 64

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "9", "btnNINE", WindowHandle)



‘Los valores del recuadro del botón “*” (Multiplicar) son asignados, y dicho botón es creado

Rect.Left = 113

Rect.Top = 64

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "*", "btnMUL", WindowHandle)



‘Los valores del recuadro del botón “4” son asignados, y dicho botón es creado

Rect.Left = 5

Rect.Top = 98

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "4", "btnFOUR", WindowHandle)



‘Los valores del recuadro del botón “5” son asignados, y dicho botón es creado

Rect.Left = 41

Rect.Top = 98

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "5", "btnFIVE", WindowHandle)



‘Los valores del recuadro del botón “6” son asignados, y dicho botón es creado

Rect.Left = 77

Rect.Top = 98

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "6", "btnSIX", WindowHandle)



‘Los valores del recuadro del botón “-“ (Restar) son asignados, y dicho botón es creado

Rect.Left = 113

Rect.Top = 98

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "-", "btnMINUS", WindowHandle)



‘Los valores del recuadro del botón “1” son asignados, y dicho botón es creado

Rect.Left = 5

Rect.Top = 132

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "1", "btnONE", WindowHandle)



‘Los valores del recuadro del botón “2” son asignados, y dicho botón es creado

Rect.Left = 41

Rect.Top = 132

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "2", "btnTWO", WindowHandle)



‘Los valores del recuadro del botón “3” son asignados, y dicho botón es creado

Rect.Left = 77

Rect.Top = 132

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "3", "btnTHREE", WindowHandle)



‘Los valores del recuadro del botón “+” (Sumar) son asignados, y dicho botón es creado

Rect.Left = 113

Rect.Top = 132

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "+", "btnPLUS", WindowHandle)



‘Los valores del recuadro del botón “0” son asignados, y dicho botón es creado

Rect.Left = 5

Rect.Top = 166

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "0", "btnZERO", WindowHandle)



‘Los valores del recuadro del botón "±" (Invertir signo) son asignados, y dicho botón es creado

Rect.Left = 41

Rect.Top = 166

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "±", "btnINSGN", WindowHandle)



‘Los valores del recuadro del botón "." (Punto decimal) son asignados, y dicho botón es creado

Rect.Left = 77

Rect.Top = 166

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, ".", "btnDOT", WindowHandle)



‘Los valores del recuadro del botón "=" (Igual) son asignados, y dicho botón es creado

Rect.Left = 113

Rect.Top = 166

Rect.Right = 26

Rect.Bottom = 24

ButtonHandle = CreateButton(Rect, "=", "btnEQUAL", WindowHandle)



‘La ventana de la calculadora se manda a mostrar en la pantalla, o sea, se hace visible para ser dibujada por PresentationProcess

ShowWindow WindowHandle



MouseInitialize

MouseShowPointer



‘El tratamiento de errores es activado, y se designa la subrutina ErrorHandler para tratarlos; esto debería estar en la subrutina

‘ExecuteEQUAL, no obstante, no funciona en ese sitio imagino por un error de QBasic (por eso se creó OperationCode)

‘El emulador de MS DOS, DOSBox, tampoco parece comportarse exactamente como cuando se corre el programa sobre hardware ‘real, no lanza ciertas excepciones del procesador como la de división por cero o por lo menos QBasic no las recibe.

ON ERROR GOTO ErrorHandler



‘El ciclo principal del programa no termina hasta que la variable Terminate se hace True

DIM SHARED Terminate AS INTEGER

DO UNTIL Terminate

MouseProcess ‘Procesa la actividad del ratón

PresentationProcess ‘Procesa la presentación en pantalla

LOOP



END



‘El manipulador de errores debería colocarse dentro de la subrutina ExecuteEQUAL en donde se realizan los cálculos, no obstante,

‘como comento arriba, por lo visto por un error de QBasic no funciona en ese sitio como lo dice la documentación debería hacerlo

ErrorHandler:

SELECT CASE ERR

CASE 6 'Desbordamiento

OperationState = OS.ERROR

OperationCode = 6

RESUME NEXT

CASE 11 'División por cero

OperationState = OS.ERROR

OperationCode = 11

RESUME NEXT

CASE 120 'Raíz cuadrada de número negativo

OperationState = OS.ERROR

OperationCode = 120

RESUME NEXT

CASE ELSE

SCREEN 0

PRINT "Error inesperado..."

END

END SELECT

El código de la calculadora mostrado arriba se limita hasta ahora a la creación de la interfaz gráfica de usuario de esta usando código previamente creado para hacerlo, primero creando y posicionando la ventana y los controles gráficos, y luego estableciendo algunos valores iniciales de las variables para su correcta inicialización.

En esta oportunidad, para cerrar el programa no sirve de nada presionar escape en el teclado, y deberemos utilizar el botón cerrar de la ventana (primer botón de la barra de título), dado en la notificación Close del gestor de ventanas es en donde se establece a True la variable Terminate encargada de concluir el bucle principal del programa.

Nota: Los botones de la barra de título de la ventana no poseen icono porque eso no está todavía implementado; el primero es el encargado de cerrar la ventana y terminar el programa y el segundo la maximiza o la restaura.

Por lo demás, como pueden ver en los comentarios del código (los cuales son abundantes porque es código de tutorial), me encontré con un extraño comportamiento del QBasic, y debido a eso no pude colocar la activación del tratamiento de errores, y la correspondiente subrutina para tratarlos, dentro de la subrutina en donde deberían de haber estado colocados. Por eso debí hacer un parche creando la variable OperationCode, y poner tanto la activación del tratamiento de errores como su subrutina en el módulo principal del programa, aun si dicha variable no era para nada necesaria de no ser por esto. Pero la cuestión no termina ahí, y además de todo lo mencionado, debí de comprobar los valores de algunas variables, como verán más adelante, para disparar por mí mismo los errores, puesto estos no se producían por sí mismos como era de esperarse, como cuando se realizaba una división por cero, todo parece indicar por culpa del emulador DOSBox.

Por su parte, el código encargado de lanzar la realización de las operaciones de la calculadora está situado en la notificación OnClick del gestor de ventanas:

SUB OnClick (WT AS WindowType, Handle AS LONG, Kind AS INTEGER)

DIM GraphicButton AS ButtonType

DIM TextBox AS TextBoxType

DIM KeepOp AS INTEGER



SELECT CASE RTRIM$(WT.WindowName) ‘El nombre de la ventana generadora de la notificación es comprobado

CASE "MainFRM" ‘¿La ventana se llama “MainFRM”?

IF Handle <> -1 THEN ‘¿El manipulador del control recibido indica un control operativo?

SELECT CASE Kind ‘La clase del control es comprobada

CASE WSYS.GRAPHICCKBUTTON ‘¿El control generador de la notificación es un botón?

IF GetButton(Handle, GraphicButton) THEN ‘¿La copia del botón generador de la notificación pudo ser obtenida?

IF GraphicButton.Parent = WT.Handle THEN ‘¿El botón generador de la notificación pertenece a la ventana “MainFRM”?

SELECT CASE RTRIM$(GraphicButton.ButtonName) ‘El nombre del botón presionado es comprobado

CASE "btnCLEAR" ‘El botón “C” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

LastInput = LI.CLEAR

LastOperator = LO.NOTHING

OperationState = OS.NORMAL

OperationCode = 0

AuxMem = 0

SetText (TextBox.Handle), "0", WSYS.GRAPHICCKTEXTBOX

END IF

CASE "btnSQR" ‘El botón “√” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

LastInput = LI.OPERATOR

KeepOp = LastOperator

LastOperator = LO.SQR

ExecuteEQUAL TextBox

LastInput = LI.NUMBER

LastOperator = KeepOp

END IF

END IF

CASE "btnPRCT" ‘El botón “%” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

LastInput = LI.OPERATOR

KeepOp = LastOperator

LastOperator = LO.PERCENT

ExecuteEQUAL TextBox

LastInput = LI.NUMBER

LastOperator = KeepOp

END IF

END IF

CASE "btnDIV" ‘El botón “¸” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

IF LastInput = LI.NUMBER THEN

IFLastOperator = LO.NOTHING THEN

AuxMem = VAL(TextBox.Text)

ELSE

ExecuteEQUAL TextBox

END IF

END IF

LastInput = LI.OPERATOR

LastOperator = LO.DIV

END IF

END IF

CASE "btnSEVEN" ‘El botón “7” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

InputDigit TextBox, RTRIM$(TextBox.Text), "7"

END IF

END IF

CASE "btnEIGHT" ‘El botón “8” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

InputDigit TextBox, RTRIM$(TextBox.Text), "8"

END IF

END IF

CASE "btnNINE" ‘El botón “9” fue presiondo

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

InputDigit TextBox, RTRIM$(TextBox.Text), "9"

END IF

END IF

CASE "btnMUL" ‘El botón “*” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

IF LastInput = LI.NUMBER THEN

IF LastOperator = LO.NOTHING THEN

AuxMem = VAL(TextBox.Text)

ELSE

ExecuteEQUAL TextBox

END IF

END IF

LastInput = LI.OPERATOR

LastOperator = LO.MUL

END IF

END IF

CASE "btnFOUR" ‘El botón “4” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

InputDigit TextBox, RTRIM$(TextBox.Text), "4"

END IF

END IF

CASE "btnFIVE" ‘El botón “5” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

InputDigit TextBox, RTRIM$(TextBox.Text), "5"

END IF

END IF

CASE "btnSIX" ‘El botón “6” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

InputDigit TextBox, RTRIM$(TextBox.Text), "6"

END IF

END IF

CASE "btnMINUS" ‘El botón “-” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

IF LastInput = LI.NUMBER THEN

IF LastOperator = LO.NOTHING THEN

AuxMem = VAL(TextBox.Text)

ELSE

ExecuteEQUAL TextBox

END IF

END IF

LastInput = LI.OPERATOR

LastOperator = LO.MINUS

END IF

END IF

CASE "btnONE" ‘El botón “1” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

InputDigit TextBox, RTRIM$(TextBox.Text), "1"

END IF

END IF

CASE "btnTWO" ‘El botón “2” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

InputDigit TextBox, RTRIM$(TextBox.Text), "2"

END IF

END IF

CASE "btnTHREE" ‘El botón “3” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

InputDigit TextBox, RTRIM$(TextBox.Text), "3"

END IF

END IF

CASE "btnPLUS" ‘El botón “+” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

IF LastInput = LI.NUMBER THEN

IF LastOperator = LO.NOTHING THEN

AuxMem = VAL(TextBox.Text)

ELSE

ExecuteEQUAL TextBox

END IF

END IF

LastInput = LI.OPERATOR

LastOperator = LO.PLUS

END IF

END IF

CASE "btnZERO" ‘El botón “0” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

IF LEN(RTRIM$(TextBox.Text)) > 1 THEN

InputDigit TextBox, RTRIM$(TextBox.Text), "0"

ELSE

IF INSTR(1, RTRIM$(TextBox.Text), "0") = 0 THEN

InputDigit TextBox, RTRIM$(TextBox.Text), "0"

ELSE

LastInput = LI.NUMBER

END IF

END IF

END IF

END IF

CASE "btnINSGN" ‘El botón “±” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

LastInput = LI.OPERATOR

KeepOp = LastOperator

LastOperator = LO.INVSGN

ExecuteEQUAL TextBox

LastInput = LI.NUMBER

LastOperator = KeepOp

END IF

END IF

CASE "btnDOT" ‘El botón “.” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

IF LastInput = LI.NUMBER THEN

IF INSTR(1, RTRIM$(TextBox.Text), ".") = 0 THEN

Display TextBox, RTRIM$(TextBox.Text) + "."

END IF

ELSE

Display TextBox, "0."

END IF

LastInput = LI.NUMBER

END IF

END IF

CASE "btnEQUAL" ‘El botón “=” fue presionado

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN

IF OperationState = OS.NORMAL THEN

ExecuteEQUAL TextBox

LastInput = LI.OPERATOR

LastOperator = LO.NOTHING

END IF

END IF

END SELECT

END IF

END IF

END SELECT

END IF

END SELECT

END SUB

El listado de código anterior puede parecer un poco complicado a primera vista, sin embargo, no lo es para nada si lo miramos de la manera correcta; o sea, como diría Jack el Destripador, debemos mirarlo por partes en vez de como un todo.

En primer lugar, nos encontramos con una sección de código encargada de determinar de dónde provino la notificación OnClick, puesto ésta es la misma para todas las ventanas y controles gráficos de nuestra GUI. En un gestor de ventanas completo, esta notificación serviría nada más para eso, para determinar de dónde proviene la notificación y generar el evento adecuado para el control correspondiente. En un entorno de desarrollo como Visual Basic (versión 6.0 y anteriores, no Visual Basic .Net), las subrutinas de evento están formadas por el nombre del control, un símbolo de subrayado, y el nombre del evento en sí; así, para la subrutina del evento Click de un botón de nombre “cmdOK”, se utilizaría como nombre de la subrutina de evento cmdOk_Click(). Pero en nuestro caso no hemos desarrollado un gestor de ventanas completo, puesto como he dicho esto se trata nada más de un tutorial y no podemos crear una GUI de punta a cabo en un contexto como este porque una implementación real, con todas las características, podría tomarle meses a una sola persona (y eso a pesar de no dedicarle tiempo a escribir los textos explicativos del tutorial).

En todo caso, en este programa en particular nada más tenemos una ventana creada, y por eso una buena parte del código para la determinación del origen de la notificación sobra y se ha colocado nada más con fines de demostración, para cuando se tengan muchas ventanas con variados controles en pantalla.

El código realmente perteneciente a la calculadora se encuentra situado en las secciones del listado en donde se ha identificado el botón presionado, y en este caso nos encontramos con varias situaciones, dependiendo del botón en cuestión.

Por una parte tenemos los botones numéricos, por otra los botones para realizar las operaciones matemáticas, y como un caso especial tenemos los botones como “C”, “.”, y el “0”.

El código para la operación de los botones numéricos es el más sencillo de todos, como podemos observar estudiando el código para el botón “4” (es igual para cada número):

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN ‘¿La copia del control cuadro de texto pudo obtenerse?

IF OperationState = OS.NORMAL THEN ‘¿La calculadora está en un estado de operación normal?

InputDigit TextBox, RTRIM$(TextBox.Text), "4" ‘El número 4 es escrito en la pantalla de la calculadora

END IF

END IF

En el listado anterior primero se consigue una copia (instancia) del cuadro de texto de la pantalla de la calculadora utilizando su nombre, en este caso “Display”, y a continuación se comprueba si se ha detectado un error en las operaciones probando el valor de la variable OperationState. En caso de haberse detectado un error, en la pantalla de la calculadora debería de estar escrito su causa como lo muestra la Figura 2, y por eso no deberíamos escribir en ella a menos el usuario presione primero el botón “C” para reiniciar la calculadora. En caso de no haber sido detectado un error en las operaciones previas, el número se coloca en la pantalla usando la subrutina InputDigit, la cual decide cómo se debe ponerlo según si antes se había escrito un número o se había solicitado una operación matemática.

Nota: En realidad no es necesario llamar a GetTextBoxByName cada vez, puesto podríamos utilizar el manipulador recibido al crear el cuadro de texto para mandar a escribir los datos en este, y usar la función GetText con dicho manipulador para obtenerlo; mas en ese caso la diferencia en el procedimiento no sería mucha, ni tampoco resultaría en un incremento de eficiencia, así pues no es incorrecto hacer las llamadas a GetTextBoxByName como se lo ha hecho.

Calc_operation_error.jpg
Figura 2: La calculadora ha detectado un error de división por cero.

Por su parte, el código de la subrutina InputDigit, utilizada para colocar dígitos en la pantalla de la calculadora, se muestra a continuación:

SUB InputDigit (DisplayBox AS TextBoxType, Content AS STRING, NewData AS STRING)

IF LastInput = LI.NUMBER THEN ‘¿La última entrada fue un número?

Display DisplayBox, Content + NewData ‘El nuevo número se concatena con el contenido existente en la pantalla

ELSE ‘La última entrada no fue un número, o sea, pudo ser un operador, una operación de inicialización (botón “C”), etc.

Display DisplayBox, NewData ‘El nuevo número se escribe en pantalla sobrescribiendo el contenido de ésta

LastInput = LI.NUMBER ‘La última entrada se establece como un número

END IF

END SUB

En este caso el código es bastante simple, y podría serlo más dado no es necesario pasar como un parámetro el contenido del cuadro de texto de la pantalla de la calculadora si se está pasando dicho control (esto sucede un muchas subrutinas del programa); la subrutina se escribió de esta manera para poder cambiarla durante las pruebas, y así pasarle como parámetro el manipulador del cuadro de texto en lugar del cuadro de texto mismo sin vernos en la necesidad de llamar a GetText para la obtención de su contenido.

Por lo demás, tal y como se comentó antes, InputDigit se limita a comprobar si la última entrada recibida en la calculadora fue un número o algo distinto, y actúa en correspondencia utilizando la subrutina Display, encargada de escribir en la pantalla de la calculadora; si la última entrada resulta haber sido un número, el nuevo dígito entrado se concatena con el contenido de la pantalla de la calculadora en ese instante, y en caso contrario, en la pantalla se coloca solamente el último dígito y se declara fue un número la última entrada recibida.

Por último, antes de pasar a los botones para las operaciones matemáticas, vamos a ver la subrutina Display, como se comentó dedicada a actualizar la pantalla de la calculadora:

SUB Display (DisplayBox AS TextBoxType, Value AS STRING)

DIM ResultText AS STRING

Value = LTRIM$(Value)



IF LEFT$(Value, 1) = "-" THEN

IF MID$(Value, 2, 1) = "." THEN

ResultText = "-0" + MID$(Value, 2)

ELSE

ResultText = Value

END IF

ELSE

IF LEFT$(Value, 1) = "." THEN

ResultText = "0" + Value

ELSE

ResultText = Value

END IF

END IF



IF DisplayFit(DisplayBox, ResultText) THEN

SetText (DisplayBox.Handle), ResultText, WSYS.GRAPHICCKTEXTBOX

ELSE

OperationState = OS.ERROR

SetText (DisplayBox.Handle), "Display OFF", WSYS.GRAPHICCKTEXTBOX

END IF

END SUB

La subrutina Display, como también pasa con la subrutina InputDigit, es lo bastante sencilla como para poder entenderla sin comentarios, dado su misión nada más consiste en garantizar la correcta presentación de los resultados en la pantalla utilizando una serie de condicionales. En ella también se llama a una función de nombre DisplayFit para comprobar si el resultado a mostrar cabe en las dimensiones del cuadro de texto de la pantalla de la calculadora, y reportar un error si no lo hace. Pero si aun así alguien no comprendiera una parte de esta subrutina, no debe abstenerse de preguntar a través de un comentario, y de esa manera podré aclarar su duda con más detalles, puesto para eso estamos.

En todo caso, a continuación vamos a ver el funcionamiento de los botones para las operaciones matemáticas, entre las cuales podemos distinguir por lo menos un par de situaciones distintas aun si bastante parecidas. Por un lado tenemos las operaciones en donde se ven implicados dos operandos, como en “4 + 5”, y por otro tenemos las operaciones que actúan nada más sobre el número en pantalla, como pudiera ser la extracción de la raíz cuadrada. Pero vamos a ver primero el caso de las operaciones en donde están implicados más de un operando con más detalle, y luego repasaremos las más inmediatas.

La sección de código siguiente se corresponde con la operación de multiplicación:

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN ‘¿La copia del cuadro de texto de la pantalla pudo ser obtenida?

IF OperationState = OS.NORMAL THEN ‘¿La calculadora está en un estado de operación normal?

IF LastInput = LI.NUMBER THEN ‘¿La última entrada recibida fue un número?

IF LastOperator = LO.NOTHING THEN

AuxMem = VAL(TextBox.Text)

ELSE

ExecuteEQUAL TextBox

END IF

END IF

LastInput = LI.OPERATOR

LastOperator = LO.MUL

END IF

END IF

En el código anterior podemos ver como en primer lugar se obtiene una copia del cuadro de texto de la pantalla de la calculadora, como mismo se lo hace en el caso de haber digitado un número; y después se comprueba si la calculadora está en un estado normal, y por tanto se puede escribir en su pantalla sin eliminar un mensaje de error en ella. El paso siguiente es comprobar si la última entrada fue un número, y de ser así se comprueba no se ha utilizado un operador matemático; en ese caso el contenido de la pantalla simplemente se deposita en la memoria auxiliar o acumulador para su posterior procesamiento, cuando se obtenga el operando faltante. En caso de haber sido un número la entrada anterior de la calculadora, pero haberse presionado previamente un botón de operación, se considera se dispone de lo necesario (una parte en el acumulador y una parte en pantalla), y se realiza una llamada a la subrutina ExecuteEQUAL para la obtención del resultado. En toda situación se declara como la última entrada de la calculadora un operador, y en esta ocasión en particular se establece como el último operador solicitado el operador de multiplicación, dado fue ése el botón presionado.

Nota: En el resto de las operaciones con operadores de dos operandos se hace lo mismo descrito, salvo por establecer como último operador solicitado el operador correspondiente a cada una.

Por otra parte, el código correspondiente para los operadores de un solo operando podemos verlo en el siguiente listado, tomando como muestra la sección de código para la obtención de la raíz cuadrada:

IF GetTextBoxByName((WT.Handle), "Display", TextBox) THEN ‘¿La copia del cuadro de texto de la pantalla pudo ser obtenida?

IF OperationState = OS.NORMAL THEN ‘¿La calculadora está en un estado de operación normal?

LastInput = LI.OPERATOR

KeepOp = LastOperator

LastOperator = LO.SQR

ExecuteEQUAL TextBox

LastInput = LI.NUMBER

LastOperator = KeepOp

END IF

END IF

En el caso de los operadores de un solo operando, como pueden ver, también comenzamos consiguiendo una copia del cuadro de texto de la pantalla de la calculadora, y después nos aseguramos de estar en un estado de operación normal. Por esa parte el código no difiere del utilizado para los operadores de dos operandos como los de la suma, la resta, la multiplicación y la división, o del código para los números. En realidad la diferencia fundamental se encuentra en la realización de una salva del último operador utilizado en la variable KeepOp, y en establecer la variable LastOperator a la operación solicitada antes de la llamada a la subrutina ExecuteEQUAL, en donde se realizan las operaciones realmente. Por ser el operador para extracción de la raíz cuadrada un operador de un solo operando, funciona de manera más inmediata, puesto no necesita esperar la llegada del segundo operando para procesar la solicitud. Por último, el operador en la variable LastOperator es restablecido desde KeepOp una vez la subrutina antes mencionada retorna, y con ello se garantiza la continuidad de la operación en curso del operador de dos operandos en caso de haber estado utilizándose uno al ordenar la extracción de la raíz cuadrada.

En resumen, ahora nada más nos faltaría la subrutina ExecuteEQUAL para haber recorrido todo el código de programa de la calculadora:

SUB ExecuteEQUAL (DisplayBox AS TextBoxType)

DIM Result AS DOUBLE

DIM ResultText AS STRING

DIM DotPosition AS INTEGER

DIM DecimalPlaces AS INTEGER



SELECT CASE LastOperator ‘El ultimo operador utilizado es comprobado

CASE LO.PLUS ‘El último operador fue para la suma

AuxMem = AuxMem + VAL(DisplayBox.Text) ‘El contenido de la pantalla se suma con el acumulador y se guarda el resultado

Result = AuxMem

CASE LO.MINUS ‘El último operador fue para la resta

AuxMem = AuxMem - VAL(DisplayBox.Text) ‘El contenido de la pantalla se resta de acumulador y se guarda el resultado

Result = AuxMem

CASE LO.MUL ‘El último operador fue para la multiplicación

AuxMem = AuxMem * VAL(DisplayBox.Text) ‘El contenido de la pantalla se multiplica con el acumulador y se guarda el resultado

Result = AuxMem

CASE LO.DIV ‘El último operador fue para la división

IF VAL(DisplayBox.Text) <> 0 THEN ‘¿El contenido de la pantalla (divisor) se evalúa como distinto de 0?

AuxMem = AuxMem / VAL(DisplayBox.Text) ‘El acumulador se divide entre el contenido de la pantalla y se guarda el resultado

Result = AuxMem

ELSE ‘El contenido de la pantalla fue evaluado como 0, lanzar error de división por cero.

ERROR 11

END IF

CASE LO.SQR ‘El último operador fue para la extracción de la raíz cuadrada

IF VAL(DisplayBox.Text) >= 0 THEN ‘¿El contenido de la pantalla se evalúa como mayor o igual a cero?

Result = SQR(VAL(DisplayBox.Text)) ‘El contenido de la pantalla se usa como parámetro para extraer la raíz cuadrada

ELSE ‘El contenido de la pantalla fue evaluado como un número negativo, lanzar error de número no válido

ERROR 120 'El parámetro de SQR no puede ser negativo

END IF

CASE LO.PERCENT ‘El último operador fue para calcular un porciento

Result = AuxMem * VAL(DisplayBox.Text) / 100 ‘El contenido de la pantalla se usa como un porcentaje del acumulador

CASE LO.INVSGN ‘El ultimo operador fue para invertir el signo del número en pantalla

Result = VAL(DisplayBox.Text) * -1 ‘El signo del número en pantalla es invertido

END SELECT



‘Las líneas siguientes comprueban si el resultado obtenido contiene un punto decimal y en caso afirmativo se utiliza la constante

‘DISPLAYPRECISION para redondear el valor y mostrar nada más ese número de decimales

ResultText = STR$(Result)

DotPosition = INSTR(1, ResultText, ".")



IF DotPosition <> 0 THEN

DecimalPlaces = LEN(RIGHT$(ResultText, LEN(ResultText) - DotPosition))

IF DecimalPlaces > DISPLAYPRECISION THEN

Result = RoundX(Result, DISPLAYPRECISION)

ResultText = STR$(Result)

ResultText = LTRIM$(LEFT$(ResultText, DotPosition)) + MID$(ResultText, DotPosition + 1, DISPLAYPRECISION)

END IF

END IF



‘En este lugar es donde debía de estar la subrutina ErrorHandler para el tratamiento de los errores, sin embargo, debí dar un rodeo

‘utilizando OperationCode puesto QBasic no asimilaba On Error Goto ErrorHandler dentro de la subrutina ExecuteEQUAL

IF OperationState = OS.NORMAL THEN ‘¿La calculadora está en un estado de operación normal?

Display DisplayBox, ResultText ‘La respuesta es mostrada en el cuadro de texto de pantalla

ELSE ‘La calculadora no está en un estado de operación normal

SELECT CASE OperationCode ‘Las líneas siguientes escriben en la pantalla de la calculadora la causa del error detectado

CASE 6 ‘Desbordamiento

Display DisplayBox, "Overflow"

CASE 11 ‘División por cero

Display DisplayBox, "Error #¡DIV/0!"

CASE 120 ‘Raíz cuadrada de un número negativo

Display DisplayBox, "Error #¡NUM!"

END SELECT

END IF

END SUB

La subrutina ExecuteEQUAL reúne en sí todo el código verdaderamente importante de la calculadora, por lo cual se puede decir es la calculadora. En ella se decide la operación a realizar según el valor guardado en la variable LastOperator, y se lanzan los errores en caso de ser necesario. En el caso de los operadores de dos operandos, el resultado es guardado en el acumulador de modo podamos utilizarlo en las subsecuentes operaciones. En cambio, cuando se trata de operadores de un solo operando, esto no se hace por ser estos más inmediatos, y la respuesta sólo se escribe en la pantalla. En adición a lo comentado, la respuesta es también redondeada, para poder mostrarla en pantalla según la precisión declarada por medio de la constante DISPLAYPRECISION, aun si el contenido del acumulador no se redondea para minimizar los errores de redondeo.

Nota: El código encargado del redondeo asume como símbolo decimal el punto (“.”), por eso no funcionará correctamente si se utiliza para esto la coma (“,”).

Por lo demás, como mencioné al comienzo, se utiliza la notificación OnClose, en donde se manda a cerrar el programa poniendo a True la variable compartida Terminate para concluir el bucle de proceso principal del programa; el código comprueba si la notificación proviene de la ventana de la calculadora, y de ser así, establece el valor de la variable, sin embargo, no se listará por ser en extremo sencillo y nada más lo menciono porque debe de ser tenido en cuenta.

Nota: El código de la calculadora presentada en esta entrega no se ha depurado nada y por eso es susceptible de ser bastante mejorado y simplificado, a pesar de no ser esto necesario, por ser nada más una demostración de la creación desde cero de una GUI y de su posible uso. En adición a lo antes dicho, entre sus líneas podrán encontrar ciertas instrucciones comentadas, y tal vez alguna variable declarada y después no utilizada, sin mencionar los posibles errores. Por todo esto les pido disculpas de antemano, aun cuando sería bueno se reportaran los errores en caso de ser encontrados por alguno, y también las posibles omisiones en las subrutinas y funciones tanto de la calculadora como de la interfaz gráfica (todavía bastante incompleta en este punto).

Me despido con la esperanza de recibir sus comentarios con su propio parecer de lo realizado en este artículo dedicado a crear una calculadora simple con una interfaz gráfica en un entorno sin ella como MS DOS.

El código correspondiente pueden descargarlo desde el enlace (como comenté contiene un programa compilado para MS DOS y otro capaz de ser corrido en un Windows moderno): Calculadora.zip

En caso de preferir ver este mismo texto como un documento en formato PDF para ver mejor la indentación del código pueden descargarlo usando este enlace: Calculadora.pdf
 
Atrás
Arriba