Capítulo 7 Control básico Edit

Tal como hemos definido nuestro diálogo en el capítulo 6, no tiene mucha utilidad. Los diálogos se usan para intercambiar información entre la aplicación y el usuario, en ambas direcciones. El ejemplo 4 sólo lo hace en una de ellas.

En el capítulo anterior hemos usado dos controles (un texto estático y un botón), aunque sin saber exactamente cómo funcionan. En este capítulo veremos el uso del control de edición.

Un control edit es una ventana de control rectangular que permite al usuario introducir y editar texto desde el teclado.

Cuando está seleccionado muestra el texto que contiene y un cursor intermitente que indica el punto de inserción de texto. Para seleccionarlo el usuario puede hacer un click con el ratón en su interior o usar la tecla [TAB]. El usuario podrá entonces introducir texto, cambiar el punto de inserción, o seleccionar texto para ser borrado o movido usando el teclado o el ratón.

Un control de este tipo puede enviar mensajes a su ventana padre mediante WM_COMMAND, y la ventana padre puede enviar mensajes a un control edit en un cuadro de diálogo llamando a la función SendDlgItemMessage. Veremos algunos de estos mensajes en este capítulo, y el resto el capítulos más avanzados.

Fichero de recursos

Empezaremos definiendo el control edit en el fichero de recursos, y lo añadiremos a nuestro dialogo de prueba.

#include <windows.h>
#include "win005.h"

Menu MENU
BEGIN
 POPUP "&Principal"
 BEGIN
  MENUITEM "&Diálogo", CM_DIALOGO
 END
END

DialogoPrueba DIALOG 0, 0, 118, 48
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION
CAPTION "Diálogo de prueba"
FONT 8, "Helv"
BEGIN
 CONTROL "Texto:", -1, "STATIC",
    SS_LEFT | WS_CHILD | WS_VISIBLE,
    8, 9, 28, 8
 CONTROL "", ID_TEXTO, "EDIT",
    ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP,
    36, 9, 76, 12
 CONTROL "Aceptar", IDOK, "BUTTON",
    BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
    56, 26, 50, 14
END

Hemos hecho algunas modificaciones más. Para empezar, el control static se ha convertido en una etiqueta para el control edit, que indica al usuario qué tipo de información debe suministrar.

Hemos añadido el control edit a continuación del control static. Veremos que el orden en que aparecen los controles dentro del cuadro de diálogo es muy importante, al menos en aquellos controles que tengan el estilo WS_TABSTOP, ya que ese orden será el mismo en que se activen los controles cuando usemos la tecla TAB. Para más detalles acerca de los controles edit ver controles edit.

Pero ahora veamos cómo hemos definido nuestro control edit:

CONTROL "", ID_TEXTO, "EDIT",
   ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP,
   36, 9, 76, 12
  • CONTROL es una palabra clave que indica que vamos a definir un control.
  • A continuación, en el parámetro text, introducimos el texto que se mostrará en el interior del control, en este caso, ninguno.
  • id es el identificador del control. Los controles edit necesitan un identificador para que la aplicación pueda acceder a ellos. Usaremos un identificador definido en win005.h.
  • class es la clase de control, en nuestro caso "EDIT".
  • style es el estilo de control que queremos. En nuestro caso es una combinación de un estilo edit y varios de ventana:
    • ES_LEFT: indica que el texto en el interior del control se alineará a la izquierda.
    • WS_CHILD: crea el control como una ventana hija.
    • WS_VISIBLE: crea una ventana inicialmente visible.
    • WS_BORDER: se crea un control que tiene de borde una línea fina.
    • WS_TABSTOP: define un control que puede recibir el foco del teclado cuando el usuario pulsa la tecla TAB. Presionando la tecla TAB, el usuario mueve el foco del teclado al siguiente control con el estilo WS_TABSTOP.
  • coordenada x del control.
  • coordenada y del control.
  • width: anchura del control.
  • height: altura del control.

El procedimiento de diálogo y los controles edit

Para manejar el control edit desde nuestro procedimiento de diálogo tendremos que hacer algunas modificaciones.

Para empezar, los controles edit también pueden generar mensajes WM_COMMAND, de modo que debemos diferenciar el control que originó dicho mensaje y tratarlo de diferente modo según el caso.

        case WM_COMMAND:
           if(LOWORD(wParam) == IDOK) EndDialog(hDlg, FALSE);
           return TRUE;

En nuestro caso sigue siendo sencillo: sólo cerraremos el diálogo si el mensaje WM_COMMAND proviene del botón "Aceptar".

La otra modificación afecta al mensaje WM_INITDIALOG.

        case WM_INITDIALOG:
           SetFocus(GetDlgItem(hDlg, ID_TEXTO));
           return FALSE;

De nuevo es una modificación sencilla, tan sólo haremos que el foco del teclado se coloque en el control edit, de modo que el usuario pueda empezar a escribir directamente, tan pronto como el diálogo haya aparecido en pantalla.

Para hacer eso usaremos la función SetFocus. Pero esta función requiere como parámetro el manipulador de ventana del control que debe recibir el foco, este manipulador lo conseguimos con la función GetDlgItem, que a su vez necesita como parámetros un manipulador del diálogo y el identificador del control.

Variables a editar en los cuadros de diálogo

Quizás has notado que a nuestro programa le falta algo.

Efectivamente, podemos introducir y modificar texto en el cuadro de diálogo, pero no podemos asignar valores iniciales al control de edición ni tampoco podemos hacer que la aplicación tenga acceso al texto introducido por el usuario.

Lo primero que tenemos que tener es algún tipo de variable que puedan compartir los procedimientos de ventana de la aplicación y el del diálogo. En nuestro caso se trata sólo de una cadena, pero según se añadan más parámetros al cuadro de edición, estos datos pueden ser más complejos, así que usaremos un sistema que nos valdrá en todos los casos.

Se trata de crear una estructura con todos los datos que queremos que el procedimiento de diálogo comparta con el procedimiento de ventana:

typedef struct stDatos {
   char Texto[80];
} DATOS;

Lo más sencillo es que estos datos sean globales, pero no será buena idea ya que no es buena práctica el uso de variables globales.

Tampoco parece muy buena idea declarar los datos en el procedimiento de ventana, ya que este procedimiento se usa para todas las ventanas de la misma clase, y tendríamos que definir los datos como estáticos.

Pero recordemos que tenemos un modo de pasar parámetros al cuadro de diálogo, usando la función DialogBoxParam, a través del parámetro lParam.

Aunque esta opción parece que nos limita a valores enteros, y sólo permite pasar valores al procedimiento de diálogo, en realidad se puede usar para pasar valores en ambos sentidos, bastará con enviar un puntero en lugar de un entero.

Para ello haremos un casting del puntero al tipo LPARAM. Dentro del procedimiento de diálogo haremos otro casting de LPARAM al puntero.

Esto nos permite declarar la variable que contiene los datos dentro del procedimiento de ventana, en este caso, de forma estática.

static DATOS Datos;
...
   DialogBoxParam(hInstance, "DialogoPrueba", hwnd, DlgProc, (LPARAM)&Datos);

En el caso del procedimiento de diálogo:

    static DATOS *Datos;
...
        case WM_INITDIALOG:
           Datos = (DATOS *)lParam;

Daremos valores iniciales a las variables de la aplicación, dentro del procedimiento de ventana, al procesar el mensaje WM_CREATE:

        case WM_CREATE:
           /* Inicialización de los datos de la aplicación */
           strcpy(Datos.Texto, "Inicial");

Iniciar controles edit

Ahora tenemos que hacer que se actualice el contenido del control edit al abrir el cuadro de diálogo.

El lugar adecuado para hacer esto es en el proceso del mensaje WM_INITDIALOG:

        case WM_INITDIALOG:
           SendDlgItemMessage(hDlg, ID_TEXTO, EM_LIMITTEXT, 80, 0L);
           Datos = (DATOS *)lParam;
           SetDlgItemText(hDlg, ID_TEXTO, Datos->Texto);
           SetFocus(GetDlgItem(hDlg, ID_TEXTO));
           return FALSE;

Hemos añadido dos llamadas a dos nuevas funciones del API. La primera es a SendDlgItemMessage, que envía un mensaje a un control. En este caso se trata de un mensaje EM_LIMITTEXT, que sirve para limitar la longitud del texto que se puede almacenar y editar en el control edit. Es necesario que hagamos esto, ya que el texto que puede almacenar nuestra estructura de datos está limitado a 80 caracteres.

También hemos añadido una llamada a la función SetDlgItemText, que hace exactamente lo que pretendemos: cambiar el contenido del texto en el interior de un control edit.

Devolver valores a la aplicación

También queremos que cuando el usuario esté satisfecho con los datos que ha introducido, y pulse el botón de aceptar, el dato de nuestra aplicación se actualice con el texto que hay en el control edit.

Esto lo podemos hacer de varios modos. Como veremos en capítulos más avanzados, podemos responder a mensajes que provengan del control cada vez que cambia su contenido.

Pero ahora nos limitaremos a leer ese contenido cuando procesemos el comando generado al pulsar el botón de "Aceptar".

        case WM_COMMAND:
           if(LOWORD(wParam) == IDOK)
           {
              GetDlgItemText(hDlg, ID_TEXTO, Datos->Texto, 80);
              EndDialog(hDlg, FALSE);
           }
           return TRUE;

Para eso hemos añadido la llamada a la función GetDlgItemText, que es simétrica a SetDlgItemText.

Ahora puedes comprobar lo que pasa cuando abres varias veces seguidas el cuadro de diálogo modificando el texto cada vez.

Con esto parece que ya controlamos lo básico de los controles edit, pero aún hay algo más.

Añadir la opción de cancelar

Es costumbre dar al usuario la oportunidad de arrepentirse si ha modificado algo en un cuadro de diálogo y, por la razón que sea, cambia de idea.

Para eso se suele añadir un segundo botón de "Cancelar".

Empecemos por añadir dicho botón en el fichero de recursos:

DialogoPrueba DIALOG 0, 0, 118, 48
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION
CAPTION "Diálogo de prueba"
FONT 8, "Helv"
{
 CONTROL "Texto:", -1, "static",
    SS_LEFT | WS_CHILD | WS_VISIBLE,
    8, 9, 28, 8
 CONTROL "", ID_TEXTO, "EDIT",
    ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP,
    36, 9, 76, 12
 CONTROL "Aceptar", IDOK, "BUTTON",
    BS_DEFPUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
    8, 26, 45, 14
 CONTROL "Cancelar", IDCANCEL, "BUTTON",
    BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
    61, 26, 45, 14
}

Hemos cambiado las coordenadas de los botones, para que el de "Aceptar" aparezca a la izquierda. Además, el botón de "Aceptar" lo hemos convertido en el botón por defecto, añadiendo el estilo BS_DEFPUSHBUTTON. Haciendo eso, podemos simular la pulsación del botón de aceptar pulsando la tecla de "intro".

El identificador del botón de "Cancelar" es IDCANCEL, y está definido en Windows.h.

Ahora tenemos que hacer que nuestro procedimiento de diálogo manipule el mensaje del botón de "Cancelar".

        case WM_COMMAND:
           switch(LOWORD(wParam)) {
              case IDOK:
                 GetDlgItemText(hDlg, ID_TEXTO, Datos->Texto, 80);
                 EndDialog(hDlg, FALSE);
                 break;
              case IDCANCEL:
                 EndDialog(hDlg, FALSE);
                 break;
           }
           return TRUE;

Como puedes ver, sólo leemos el contenido del control edit si se ha pulsado el botón de "Aceptar".

Ejemplo 5

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 5 win005.zip 2004-05-17 3048 bytes 207

Editar números

En muchas ocasiones necesitaremos editar valores de números enteros en nuestros diálogos.

Para eso, el API tiene previstas algunas constantes y funciones, (aunque no es así para números en coma flotante, para los que tendremos que crear nuestros propios controles).

Bien, vamos a modificar nuestro ejemplo para editar valores numéricos en lugar de cadenas de texto.

Fichero de recursos para editar enteros

Empezaremos añadiendo una constante al fichero de identificadores: "win006.h":

#define ID_NUMERO 100

Y redefiniendo el control edit en el fichero de recursos, al que añadiremos el flag ES_NUMBER para que sólo admita caracteres numéricos:

DialogoPrueba DIALOG 0, 0, 118, 48
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION
CAPTION "Diálogo de prueba"
FONT 8, "Helv"
BEGIN
 CONTROL "Número:", -1, "STATIC",
    SS_LEFT | WS_CHILD | WS_VISIBLE,
    8, 9, 28, 8
 CONTROL "", ID_NUMERO, "EDIT",
    ES_NUMBER | ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP,
    36, 9, 76, 12
 CONTROL "Aceptar", IDOK, "BUTTON",
    BS_DEFPUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
    8, 26, 45, 14
 CONTROL "Cancelar", IDCANCEL, "BUTTON",
    BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
    61, 26, 45, 14
END

Variables a editar en los cuadros de diálogo

Ahora modificaremos la estructura de los datos para que el dato a editar sea de tipo numérico:

typedef struct stDatos {
   int Numero;
} DATOS;

Al igual que antes, daremos valores iniciales a las variables del diálogo al procesar el mensaje WM_CREATE.

        case WM_CREATE:
           /* Inicialización de los datos de la aplicación */
           Datos.Numero = 123;

Por supuesto, pasaremos un puntero a esta estructura a la función DialogBoxParam, haciendo uso el parámetro lParam:

static DATOS Datos;
...
   DialogBoxParam(hInstance, "DialogoPrueba", hwnd, DlgProc, (LPARAM)&Datos);

Iniciar controles edit de enteros

Ahora tenemos que hacer que se actualice el contenido del control edit al abrir el cuadro de diálogo.

El lugar adecuado para hacer esto es en el proceso del mensaje WM_INITDIALOG:

    static DATOS *datos;
...
        case WM_INITDIALOG:
           datos = (DATOS *)lParam;
           SetDlgItemInt(hDlg, ID_NUMERO, (UINT)datos->Numero, FALSE);
           SetFocus(GetDlgItem(hDlg, ID_NUMERO));
           return FALSE;

En este caso no es necesario limitar el texto que podemos editar en el control, ya que, como veremos, las propias funciones del API se encargan de capturar y convertir el contenido del control en un número, de modo que no tenemos que preocuparnos de que no quepa en nuestra variable.

También hemos modificado la función a la que llamamos para modificar el contenido del control, ahora usaremos SetDlgItemInt, que cambia el contenido de un control edit con un valor numérico.

Devolver valores a la aplicación

Por último leeremos el contenido cuando procesemos el comando generado al pulsar el botón de "Aceptar".

    BOOL NumeroOk;
    int numero;
...
        case WM_COMMAND:
           switch(LOWORD(wParam)) {
              case IDOK:
                 numero = GetDlgItemInt(hDlg, ID_NUMERO, &NumeroOk, FALSE);
                 if(NumeroOk) {
                    datos->Numero = numero;
                    EndDialog(hDlg, FALSE);
                 }
                 else
                    MessageBox(hDlg, "Número no válido", "Error",
                       MB_ICONEXCLAMATION | MB_OK);
                 break;

Para eso hemos añadido la llamada a la función GetDlgItemInt, que es simétrica a SetDlgItemInt. El proceso difiere del usado para capturar cadenas, ya que en este caso la función nos devuelve el valor numérico del contenido del control edit.

También devuelve un parámetro que indica si ha habido algún error durante la conversión. Si el valor de ese parámetro es TRUE, significa que la conversión se realizó sin problemas, si es FALSE, es que ha habido un error. Si nuestro programa detecta un error visualizará un mensaje de error y no permitirá abandonar el cuadro de diálogo.

Pero si ha habido un error, el valor de retorno de GetDlgItemInt será cero. Esto nos causa un problema. Si leemos el valor directamente en datos->Numero y el usuario introduce un valor no válido, y después pulsa "Cancelar", el valor devuelto no será el original, sino 0. Para evitar eso hemos usado una variable local, y el valor de datos->Numero sólo se actualiza antes de salir con "Aceptar" y con un valor válido.

Por último, hemos usado el flag BM_ICONEXCLAMATION en el MessageBox, que añade un icono al cuadro de mensaje y el sonido predeterminado para alertar al usuario.

Ejemplo 6

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 6 win006.zip 2004-05-17 3104 bytes 215