39 Control Edit avanzado

En este capítulo, y en los siguientes vamos a comentar con más detalles los controles básicos que ya hemos visto previamente. Veremos algunas características más avanzadas de cada uno, los mensajes de notificación, y los mensajes que aún no conocemos de cada uno.

Empezaremos por el control edit, y veremos todo lo que no se explicó en el capítulo 7.

Insertar controles edit durante la ejecución

En general, el sistema que vamos a comentar puede ser aplicado a cualquier control, aunque veremos ejemplos para cada caso.

En realidad, un control no es otra cosa que una ventana, en el caso del control edit, se trata de una ventana de la clase "EDIT". Como ventana que es tiene su propio procedimiento de ventana, y por supuesto, se pueden crear esta clase de ventanas usando las funciones CreateWindow y CreateWindowEx.

Este sistema nos permite insertar controles en cualquier ventana o cuadro de diálogo, en lugar de usar siempre cuadros de diálogo y ficheros de recursos para insertar los controles. Cuando las aplicaciones sean sencillas será frecuente que nos baste una ventana para realizar todas las entradas y salidas.

Por supuesto, la posibilidad de insertar controles durante la ejecución nos proporciona más flexibilidad, y mayor control sobre la aplicación en ciertas circunstancias, por ejemplo, evitar que los cuadros de diálogo se puedan editar usando herramientas de edición de recursos.

Veamos un ejemplo de cómo insertar un control de edición en una ventana:

    HWND hctrl;
...
        case WM_CREATE:
           hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
           /* Insertar control Edit */
           hctrl = CreateWindowEx(
              0,
              "EDIT",          /* Nombre de la clase */
              "",              /* Texto del título, no tiene */
              ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, /* Estilo */
              36, 20,          /* Posición */
              120, 20,         /* Tamaño */
              hwnd,            /* Ventana padre */
              (HMENU)ID_TEXTO, /* Identificador del control */
              hInstance,       /* Instancia */
              NULL);           /* Sin datos de creación de ventana */
           /* Inicialización de los datos de la aplicación */
           SetDlgItemText(hwnd, ID_TEXTO, "Inicial");
           SetFocus(hctrl);
           return 0;

Como vemos, usamos los mismos valores que en el fichero de recursos: identificador, clase de ventana (en este caso "EDIT"), estilo, posición y dimensiones.

El identificador del control se suministra a través del parámetro hMenu, por lo que será necesario hacer un casting del identificador al tipo HMENU.

Ahora será nuestro procedimiento de ventana el encargado de procesar los mensajes procedentes del control. Recordemos que en los ejemplos que hemos visto hasta ahora esto lo hacía el procedimiento de diálogo.

Cambiar la fuente de un control edit

Veremos a continuación algunas formas de personalizar los controles edit.

Al insertar los controles edit en un cuadro de diálogo, usando el fichero de recursos, podemos especificar la fuente que queremos usar, pero al insertarlo directamente mediante la función CreateWindow o CreateWindowEx, Windows siempre usa la fuente del sistema por defecto.

Esto afecta al aspecto estético de nuestros controles, (la verdad es que la fuente del sistema es bastante fea), y una de las ventajas de las aplicaciones gráficas es precisamente poder elegir el aspecto que queremos que tengan.

Pero hay una solución, es posible modificar la fuente de un control edit enviando un mensaje WM_SETFONT. El lugar apropiado es, por supuesto, al procesar el mensaje WM_INITDIALOG, cuando se trate de cuadros de diálogo, o al procesar el mensaje WM_CREATE, cuando se trate de ventanas.

En el parámetro wParam pasamos un manipulador de fuente, y usaremos la macro MAKELPARAM para crear un valor LPARAM, en el que especificaremos la opción de repintar el control, que se almacena en la palabra de menor peso de LPARAM.

Esto nos permite modificar la fuente durante la ejecución, reflejando los cambios en pantalla.

   static HFONT hfont;
...
           hfont = CreateFont(24, 0, 0, 0, 300,
              FALSE, FALSE, FALSE, DEFAULT_CHARSET,
              OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS,
              PROOF_QUALITY, DEFAULT_PITCH | FF_ROMAN,
              "Times New Roman");
           SendMessage(hctrl, WM_SETFONT, (WPARAM)hfont, MAKELPARAM(TRUE, 0));
...
        case WM_DESTROY:
           DeleteObject(hfont);
...

En el caso de crear una fuente especial para nuestros controles, debemos recordar destruirla cuando ya no sea necesaria, generalmente al destruir la ventana.

Por supuesto, también podemos usar una fuente de stock:

           hfont = (HFONT)GetStockObject( DEFAULT_GUI_FONT );
           SendMessage(hctrl, WM_SETFONT, (WPARAM)hfont, MAKELPARAM(TRUE, 0));

Cambiar los colores de un control edit

Podemos personalizar más nuestros controles edit, cambiando los colores del texto y del fondo. Para ello deberemos procesar el mensaje WM_CTLCOLOREDIT.

Este mensaje se envía a la ventana padre del control justo antes de que el sistema lo vaya a dibujar, y nos permite cambiar los colores del texto y fondo. Para ello nos suministra en el parámetro wParam un manipulador del contexto de dispositivo del control, y en lParam un manipulador del control, mediante el cual podemos saber a qué control concreto se refiere el mensaje.

El valor de retorno, cuando se procesa este mensaje, debe ser un manipulador de pincel con el color de fondo del control.

    static HBRUSH pincel;
    HWND hcrtl;

    switch (msg)                  /* manipulador del mensaje */
    {
        case WM_CREATE:
           hcrtl = CreateWindowEx(...);
           pincel = CreateSolidBrush(RGB(0,255,0));
           SetFocus(hcrtl);
           return 0;
        case WM_CTLCOLOREDIT:
           SetBkColor((HDC)wParam, RGB(0,255,0));
           SetTextColor((HDC)wParam, RGB(255,255,255));
           return (LRESULT)pincel;
        case WM_DESTROY:
           DeleteObject(pincel);
           PostQuitMessage(0);    /* envía un mensaje WM_QUIT a la cola de mensajes */
           break;
...

Ejemplo 49

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 49 win049.zip 2007-03-15 3104 bytes 687

Contoles edit de sólo lectura

Uno de los estilos que se pueden aplicar a un control edit es el de sólo lectura. ES_READONLY. Cuando se activa estilo el contenido del control no podrá ser modificado por el usuario.

Esto es, aparentemente, una contradicción. Bien pensado, un control edit cuyo contenido no puede ser modificado es un control estático. Sin embargo, en determinadas circunstancias puede que no sea tan absurdo, sobre todo si tenemos en cuenta que este estilo se puede modificar durante la ejecución. Esto puede ser útil si en ciertas situaciones, determinados valores están predefinidos. Por ejemplo, dependiendo de la opción seleccionada en un conjunto de RadioButtons, determinadas entradas de texto pueden ser innecesarias, o tener valores predefinidos o predecibles, que no necesitan ser editados. Otro ejemplo puede ser un programa en el que, dependiendo del nivel de privilegios de un usuario, determinados valores puedan o no ser modificados.

Además, si queremos ser precisos, un control edit de sólo lectura no es en todo equivalente a un control estático. Por ejemplo, el texto del control edit siempre puede ser marcado y copiado al portapapeles, algo que no se puede hacer con los textos de los controles estáticos.

Para modificar esta opción para un control edit se envía un mensaje EM_SETREADONLY.

Para averiguar si un control edit tiene el estilo ES_READONLY se debe usar la función GetWindowLong usando la constante GWL_STYLE.

Para ilustar esto, modificaremos el ejemplo 5 para añadir un checkbox que active y desactive el control edit. Empezaremos por modificar la definición del diálogo en el fichero de recursos:

DialogoPrueba DIALOG 0, 0, 118, 58
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION
CAPTION "Diálogo de prueba"
FONT 8, "Helv"
BEGIN
 CONTROL "Sólo lectura", ID_ACTIVAR, "BUTTON",
    BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
    12, 4, 60, 12
 CONTROL "Texto:", -1, "STATIC",
    SS_LEFT | WS_CHILD | WS_VISIBLE,
    8, 20, 28, 8
 CONTROL "", ID_TEXTO, "EDIT",
    ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP,
    36, 20, 76, 12
 CONTROL "Aceptar", IDOK, "BUTTON",
    BS_DEFPUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
    8, 36, 45, 14
 CONTROL "Cancelar", IDCANCEL, "BUTTON",
    BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
    61, 36, 45, 14
END

Añadiremos un identificador para el control CheckBox en win050.h:

#define ID_ACTIVAR 101

Añadiremos un dato a la estructura de datos del cuador de diálogo para almacenar el estado del CheckBox:

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

Por supuesto, asignaremos un valor inicial a ese estado en el procedimiento de ventana:

LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static HINSTANCE hInstance;
    /* Variables para diálogo */
    static DATOS Datos;

    switch (msg)                  /* manipulador del mensaje */
    {
        case WM_CREATE:
           hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
           /* Inicialización de los datos de la aplicación */
           strcpy(Datos.Texto, "Inicial");
           Datos.Estado = FALSE;
           return 0;
...

Y modificaremos el procedimiento de diálogo para tratar este nuevo control:

BOOL CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static DATOS *Datos;

    switch (msg)                  /* manipulador del mensaje */
    {
        case WM_INITDIALOG:
           SendDlgItemMessage(hDlg, ID_TEXTO, EM_LIMITTEXT, 80, 0L);
           Datos = (DATOS *)lParam;
           SetDlgItemText(hDlg, ID_TEXTO, Datos->Texto);
           /* Acticar el estilo según el valor actual de estado */
           SendMessage(GetDlgItem(hDlg, ID_TEXTO), EM_SETREADONLY, Datos->Estado, 0);
           /* Aplicar el estado actual del CheckBox */
           CheckDlgButton(hDlg, ID_ACTIVAR,
              Datos->Estado ? BST_CHECKED : BST_UNCHECKED);
           SetFocus(GetDlgItem(hDlg, ID_TEXTO));
           return FALSE;
        case WM_COMMAND:
           switch(LOWORD(wParam)) {
              case ID_ACTIVAR:
                 Datos->Estado = !Datos->Estado;
                 SendMessage(GetDlgItem(hDlg, ID_TEXTO), EM_SETREADONLY, Datos->Estado, 0);
                 break;
              case IDOK:
                 GetDlgItemText(hDlg, ID_TEXTO, Datos->Texto, 80);
                 EndDialog(hDlg, FALSE);
                 break;
              case IDCANCEL:
                 EndDialog(hDlg, FALSE);
                 break;
           }
           return TRUE;
    }
    return FALSE;
}

Ejemplo 50

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 50 win050.zip 2005-11-07 3301 bytes 663

Leer contraseñas

A veces no nos interesa que el texto que se introduce en un control edit no se muestre en pantalla de forma que pueda ser reconocido. El caso más frecuente es cuando se introducen contraseñas. En esos casos se hace que el texto introducido se sustituya por otros caracteres. El usuario que introduce la contraseña sabe qué escribe, porque es el que maneja el teclado, pero una persona que observe este proceso no podrá reconocer el texto en pantalla, y le resultará complicado deducir el texto mirando el teclado.

Para que un control edit se comporte de este modo bastará con activar el estilo ES_PASSWORD al crear el control.

Las funciones para asignar valores iniciales o recuperarlos del control funcionanrán igual que con los controles normales, el estilo sólo afecta al modo en que se visualiza el texto, no a su contenido.

Por defecto, el carácter que se usa para sustituir los introducidos es el asterisco, pero esto se puede modificar usando el mensaje EM_SETPASSWORDCHAR. Si se utiliza un carácter nulo se mostrará el texto que introduzca el usuario.

También podemos usar el mensaje EM_GETPASSWORDCHAR para averiguar el carácter que se usa actualmente para sustituir lo introducidos por el usuario.

Estos mensajes sólo están disponibles para controles edit de una línea.

Crearemos otro programa de ejemplo basado en el ejemplo 5. En este caso añadiremos tres RadioButtons con tres opciones distintas de caracteres: el '*', el '·' y el nulo.

El primer paso es modificar el fichero de recursos para añadir los tres botones:

DialogoPrueba DIALOG 0, 0, 118, 98
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION
CAPTION "Diálogo de prueba"
FONT 8, "Helv"
BEGIN
 CONTROL "Grupo 1", ID_GRUPO1, "BUTTON",
    BS_GROUPBOX | WS_CHILD | WS_VISIBLE | WS_GROUP,
    4, 5, 76, 52
 CONTROL "Asteriscos", ID_RADIOBUTTON1, "BUTTON",
    BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
    11, 15, 60, 12
 CONTROL "Puntos", ID_RADIOBUTTON2, "BUTTON",
    BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
    11, 28, 60, 12
 CONTROL "Visible", ID_RADIOBUTTON3, "BUTTON",
    BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
    11, 41, 60, 12
 CONTROL "Texto:", -1, "STATIC",
    SS_LEFT | WS_CHILD | WS_VISIBLE,
    8, 64, 28, 8
 CONTROL "", ID_TEXTO, "EDIT",
    ES_LEFT | ES_PASSWORD | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP,
    36, 64, 76, 12
 CONTROL "Aceptar", IDOK, "BUTTON",
    BS_DEFPUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
    8, 80, 45, 14
 CONTROL "Cancelar", IDCANCEL, "BUTTON",
    BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
    61, 80, 45, 14
END

También añadiremos las constantes necesarias en el fichero win051.h:

#define ID_GRUPO1 101
#define ID_RADIOBUTTON1 102
#define ID_RADIOBUTTON2 103
#define ID_RADIOBUTTON3 104

Tendremos que modificar la estructura de datos para pasar al procedimiento de diálogo:

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

En el procedimiento de ventana principal iniciaremos los datos miembro:

LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static HINSTANCE hInstance;
    /* Variables para diálogo */
    static DATOS Datos;

    switch (msg)                  /* manipulador del mensaje */
    {
        case WM_CREATE:
           hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
           /* Inicialización de los datos de la aplicación */
           strcpy(Datos.Texto, "Inicial");
           Datos.Estado = 0;
           return 0;
           break;
...

Por último, procesaremos los mensajes procedentes de los RadioButtons e iniciaremos los controles en el procedimiento de diálogo:

BOOL CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static DATOS *Datos;
    char opcion[3] = "*·\000";

    switch (msg)                  /* manipulador del mensaje */
    {
        case WM_INITDIALOG:
           SendDlgItemMessage(hDlg, ID_TEXTO, EM_LIMITTEXT, 80, 0L);
           Datos = (DATOS *)lParam;
           SetDlgItemText(hDlg, ID_TEXTO, Datos->Texto);
           /* Aplicar el carácter según el valor de estado */
           SendMessage(GetDlgItem(hDlg, ID_TEXTO), EM_SETPASSWORDCHAR, opcion[Datos->Estado], 0);
           /* Activar el radiobutton */
           CheckRadioButton(hDlg, ID_RADIOBUTTON1, ID_RADIOBUTTON3,
              ID_RADIOBUTTON1+Datos->Estado);
           SetFocus(GetDlgItem(hDlg, ID_TEXTO));
           return FALSE;
        case WM_COMMAND:
           switch(LOWORD(wParam)) {
              case ID_RADIOBUTTON1:
              case ID_RADIOBUTTON2:
              case ID_RADIOBUTTON3:
                 Datos->Estado = LOWORD(wParam)->ID_RADIOBUTTON1;
                 SendMessage(GetDlgItem(hDlg, ID_TEXTO), EM_SETPASSWORDCHAR, opcion[Datos->Estado], 0);
                 SetFocus(GetDlgItem(hDlg, ID_TEXTO));
                 break;
...

A pesar de que la documentación del API afirma que el control edit se actualiza tan pronto recibe un mensaje EM_SETPASSWORDCHAR, lo cierto es que no parece que sea así, de modo que en este ejemplo hemos optado por asignar el foco al control edit, esto obliga a que se actualice su aspecto.

Ejemplo 51

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 51 win051.zip 2005-11-07 3426 bytes 638

Mayúsculas y minúsculas

Disponemos de dos estilos para los controles edit que nos sirven para limitar el tipo de caracteres que se pueden usar. El estilo ES_LOWERCASE convierte cualquier carácter en mayúscula a minúscula. El contenido del control edit será sólo de letras en minúsculas, números y caracteres especiales. El estilo ES_UPPERCASE convierte cualquier carácter en minúscula a mayúsculas.

El estilo no afecta al contenido de los caracteres del control, podemos asignar valores con mayúsculas o minúsculas con cualquiera de los estilos. Estos estilos afectan sólo a los nuevos caracteres introducidos por el usuario.

Los estilos se pueden asignar en el momento de la creación del control, como hemos hecho hasta ahora mediante ficheros de recursos o mediante las funciones CreateWindow y CreateWindowEx, o se pueden modificar durante la ejecución, usando la función SetWindowLong, y la función GetWindowLong para obtener el estilo actual:

   ActivarEstilo(GetDlgItem(hDlg, ID_TEXTO), estado);
...
void ActivarEstilo(HWND hctrl, int estado)
{
   LONG estiloActual;

   estiloActual = GetWindowLong(hctrl, GWL_STYLE);
   switch(estado) {
      case 0: /* Normal */
         estiloActual &= ~ES_UPPERCASE;
         estiloActual &= ~ES_LOWERCASE;
         break;
      case 1: /* Mayúsculas */
         estiloActual &= ~ES_LOWERCASE;
         estiloActual |= ES_UPPERCASE;
         break;
      case 2: /* Minúsculas */
         estiloActual &= ~ES_UPPERCASE;
         estiloActual |= ES_LOWERCASE;
         break;
   }
   SetWindowLong(hctrl, GWL_STYLE, estiloActual);
}

Ejemplo 52

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 52 win052.zip 2005-11-07 3523 bytes 615

Mensajes de notificación

Windows envía un tipo especial de mensajes, denominados mensajes de notificación, a la ventana padre de un control edit. Estos mensajes sirven para informar a la aplicación de determinadas circunstancias relativas a un control.

Los mensajes de notificación se reciben a través de un mensaje WM_COMMAND. En la palabra de menor peso del parámetro wParam se envía el identificador del control. El manipulador del control se envía en el parámetro lParam y el código del mensaje de notificación en la palabra de mayor peso de wParam.

Nota:

En el API de Windows 3.x el código del mensaje de notificación se envía en el parámetro lParam. Hay que tener esto en cuenta si se intenta portar código entre estas plataformas.

Veamos a continuación los mensajes de notificación que existen para los controles edit:

Modificación

Cada vez que el usuario modifica el texto de un control edit, primero se actualiza el contenido del control en pantalla, y a continuación se genera un mensaje EN_CHANGE.

Actualización

Cada vez que el usuario modifica el texto del un control Edit, y antes de que este nuevo texto se muestre en pantalla, Windows envía un mensaje EN_UPDATE.

Este mensaje está pensado para permitir a la aplicación redimensionar el tamaño del control en función de su contenido.

Falta espacio

Cuando el control edit no puede consegurir espacio de memoria suficiente para realizar una operación se envía un mensaje de notificación EN_ERRSPACE.

Desplazamiento horizontal y vertical

Cuando el usuario hace clic sobre una barra de desplazamiento de un control edit, horizontal o vertical, se envía el mensaje EN_HSCROLL o EN_VSCROLL, respectivamente, antes de que la pantalla se actualice.

Pérdida y recuperación de foco

Cada vez que el usuario selecciona otro control se envía un mensaje de notificación EN_KILLFOCUS.

Cuando el usuario selecciona un control edit, se envía un mensaje de notificación EN_SETFOCUS.

Texto máximo

El mensaje de notificación EN_MAXTEXT se envía si el usuario intenta escribir más caracteres de los especificados para un control edit.

También se envía este mensaje si la anchura de la cadena introducida en el control es mayor de la anchura del control y no se ha especificado el estilo ES_AUTOHSCROLL, o si el número total de líneas a insertar en un control edit multilínea excede la altura del control y no se ha especificado el estilo ES_AUTOVSCROLL.

Por ejemplo, para gestionar los mensajes de notificación de un control edit con el identificador ID_TEXTO, usaríamos un código parecido a este:

        case WM_COMMAND:
           switch(LOWORD(wParam)) {
              case ID_TEXTO:
                /* Mensajes de notificación */
                switch(HIWORD(wParam)) {
                   case EN_MAXTEXT:
                      MessageBox(hwnd, "Imposible insertar más caracteres", "Control edit", MB_OK);
                      break;
                   case EN_ERRSPACE:
                      ...
                   case EN_HSCROLL:
                      ...
                   case EN_VSCROLL:
                      ...
                   case EN_KILLFOCUS:
                      ...
                   case EN_SETFOCUS:
                      ...
                   case EN_UPDATE:
                      ...
                   case EN_CHANGE:
                      ...
                }
             ...

El buffer de texto

Hasta ahora no nos hemos preocupado nunca del espacio de memoria necesario para almacenar y editar el contenido de un control edit. Windows se encarga de crear un buffer, y de aumentar su tamaño si es necesario, hasta cierto límite, dependiendo del tipo de control edit.

En el capítulo 7 vimos que podíamos fijar el límite máximo que el usuario podía editar mediante el mensaje EM_LIMITTEXT.

Sin embargo este mensaje no limita el tamaño del buffer. El mensaje no tiene efecto si el control ya contiene más caracteres que el límite establecido, y sigue siendo posible insertar más caracteres usando el mensaje WM_SETTEXT. De hecho, este mensaje no debería usarse, ya que ha sido sustituido por EM_SETLIMITTEXT.

Para limitar el tamaño del buffer se usa el mensaje EM_SETLIMITTEXT, y para obtener el valor del tamaño del buffer se usa el mensaje EM_GETLIMITTEXT.

En versiones de Windows de 16 bits es posible asumir, por parte de nuestra aplicación, todas las operaciones de control del buffer de memoria asociado a un control edit multilínea. Para ello, lo primero que debemos hacer es crear el control edit en una ventana que use el estilo DS_LOCALEDIT. Además disponemos de los mensajes EM_GETHANDLE y EM_SETHANDLE, para obtener un manipulador de memoria local del buffer del control, o asignar uno nuevo, respectivamente.

El proceso consiste en:

  • Obtener un manipulador del buffer local actual, mediante EM_GETHANDLE.
  • Liberar ese buffer usando la función LocalFree.
  • Crear un nuevo buffer local, usando LocalAlloc.
  • Asignar el nuevo manipulador de memoria al control, mediante el mensaje EM_SETHANDLE.

Cada vez que el buffer se quede pequeño recibiremos un mensaje de notificación EN_ERRSPACE.

Este proceso es inútil en el API de 32 bits, ya que en este caso toda la memoria pertenece al espacio de direcciones de memoria virtual, y no hay distinción entre memoria local y global.

Controles multilínea

Hasta ahora sólo hemos trabajado con controles edit de una línea, pero también es posible crear controles edit multilínea. Para ello bastará con crearlos con el estilo ES_MULTILINE. Pero estos controles tienen algunas peculiaridades que los hace algo más complicados de usar que los de una línea.

Para empezar, cuando se ejecuta un cuadro de diálogo, la tecla ENTER tiene el efecto de activar el botón por defecto. Esto nos crea un problema con los controles edit multilínea, ya que no podremos usar la tecla de [ENTER] para insertar un retorno de línea. Para evitar este comportamiento por defecto en los cuadros de diálogo se usa el estilo ES_WANTRETURN. Este estilo hace que las pulsaciones de la tecla ENTER, cuando el control tiene el foco del teclado, se conviertan en retornos de línea, y no se active el botón por defecto.

Otro detalle importante es que con frecuencia el texto no va a caber en el área visible del control, por lo que tendremos que desplazar el contenido tanto horizontal como verticalmente.

Para lograr esto disponemos, por una parte, de dos estilos propios de los controles edit: ES_AUTOHSCROLL y ES_AUTOVSCROLL. Cuando se activan estos estilos el texto se desplaza de forma automática en sentido horizontal o vertical, respectivamente, cada vez que el usuario llegue a un borde del área del control mientras escribe texto.

Además de esta posibilidad tenemos una segunda que consiste en añadir las barras de desplazamiento. Estas barras se añaden con los estilos de ventana WS_HSCROLL y WS_VSCROLL, respectivamente, y activan de forma automática los dos estilos anteriores: ES_AUTOHSCROLL y ES_AUTOVSCROLL.

La diferencia es que de esta segunda forma se muestran las barras, y de la primera no.

Iniciar controles multilínea

Otra dificultad añadida a la hora de usar estos controles es la inicialización. Las líneas dentro de un control edit multilínea se separan con dos retornos de línea y un avance de línea, es decir, dos caracteres '\r' y uno '\n', a esta secuencia se le denomina una ruptura de línea blanda. Por otra parte, si queremos convertir un retorno de línea normal en una ruptura de línea blanda, hay que saber que Windows añade de forma automática un carácter '\r' cada vez que se añade un carácter '\n', es decir, Windows sustituye el carácter '\n' por la secuencia "\r\n".

En cualquier caso, esto nos obliga a hacer un tratamiento de cada línea del texto de inicialización para sustituir las secuencias "\n" o "\r\n" por otra "\r\r\n".

Tenemos, pues, tres opciones a la hora de inicializar controles edit multilínea.

Una consiste en crear un buffer de texto con el contenido, sustituyendo los cambios de línea por rupturas blandas, y asignar el texto al control mediante un mensaje WM_SETTEXT.

void AsignarTexto(HWND hctrl, char *texto) {
   char* buffer;

   /* Crear buffer */
   buffer = (char *)malloc(strlen(texto)+1);
   buffer[0] = 0;

   /* Hacer la lectura */
   SustituirCambiosDeLinea(texto, buffer);

   SendMessage(hctrl, WM_SETTEXT, 0, (LPARAM)buffer);
   free(buffer);
}

Otra es usar el mismo buffer, creado con la función LocalAlloc, y asignar ese buffer al control edit directamente.

void AsignarTexto(HWND hctrl, char *texto) {
   char* buffer;
   HLOCAL hloc;

   /* Obtener manipulador de buffer actual: */
   hloc = (HLOCAL)SendMessage(hctrl, EM_GETHANDLE, 0, 0);
   /* Liberar buffer actual */
   LocalFree(hloc);
   /* Crear un buffer nuevo */
   hloc = LocalAlloc(LMEM_MOVEABLE, l+1);

   /* Bloquear el buffer para su uso */
   buffer = (char *)LocalLock(hloc);
   buffer[0] = 0;

   /* Hacer la lectura */
   SustituirCambiosDeLinea(texto, buffer);

   /* Desbloquear buffer */
   LocalUnlock(hloc);

   /* Asignar el nuevo buffer el control edit */
   SendMessage(hctrl, EM_SETHANDLE, (WPARAM)hloc, 0);
}

Una tercera opción consiste en enviar el contenido del control carácter a carácter, mediante mensajes WM_CHAR. La ventaja de este método es que no hay que insertar rupturas blandas, ya que el sistema lo hace por nosotros:

void AsignarTexto(HWND hctrl, char *texto) {
   int i;

   for(i = 0; i < strlen(texto); i++)
      SendMessage(hctrl, WM_CHAR, (WPARAM)texto[i], 0);
}

Mensajes para controles multilínea

Disponemos de varios mensajes útiles cuando se trabaja con controles edit multilínea, veremos ahora algunos de ellos:

Para obtener el número de caracteres en un control edit se usa el mensaje WM_GETTEXTLENGTH. Los caracteres están indexados empezando en cero, pero es muy importante tener en cuenta que en esta indexación no se incluyen los caracteres de los retornos de línea.

Otro mensaje, más específico de controles edit, y que también se usa para calcular longitudes es EM_LINELENGTH. Este mensaje está orientado a obtener longitudes de líneas en controles multilínea.

El mensaje EM_LINELENGTH requiere un parámetro, que es el índice de un carácter, y devolverá la longitud de la línea a la que pertenece ese carácter. Si ese índice es -1, se devolverá la longitud del texto de las líneas con texto seleccionado, excluyendo la longitud del propio texto seleccionado.

   int longitud;

   /* Longitud de la o las líneas con texto seleccionado, excluyendo el texto seleccionado */
   longitud = SendMessage(hctrl, EM_LINELENGTH, (WPARAM)-1, 0);
   sprintf(mensaje, "Longitud = %d", longitud);
   MessageBox(hwnd, mensaje, "Control edit multilínea", MB_OK);
Selección múltiple
Selección múltiple

En el ejemplo anterior, si recuperamos la longitud del texto con el índice -1, se calculará a partir de los caracteres no seleccionados en la quinta y sexta línea, ya que la selección ocupa ambas líneas.

Los mensajes WM_GETTEXTLENGTH y EM_LINELENGTH funcionan tanto con controles edit de una línea como con los multilínea.

El mensaje EM_GETFIRSTVISIBLELINE obtiene el índice de la primera línea visible en un control multilínea, empezando en cero, o el índice del primer carácter visible en un control edit de una línea.

   int pos;

   pos = SendMessage(hctrl, EM_GETFIRSTVISIBLELINE, 0, 0);
   sprintf(mensaje, "Primera línea visible = %d", pos);
   MessageBox(hwnd, mensaje, "Control edit multilínea", MB_OK);

El mensaje EM_GETLINE sirve para obtener el contenido de una línea determinada. Para usar este mensaje se pasa en wParam el índice de la línea a leer, y en lParam un puntero al buffer que recibirá el contenido de la línea. Cuando se envía el mensaje hay que colocar en la primera palabra de ese buffer el tamaño máximo de la cadena a leer:

   char linea[512];
   int longitud;

   *(WORD*)linea = 512; /* Longitud del buffer en primera palabra */
   longitud = SendMessage(hctrl, EM_GETLINE, (WPARAM)1, (LPARAM)linea);
   MessageBox(hwnd, linea, "Línea 1", MB_OK);

El mensaje EM_GETLINECOUNT nos devuelve el número de líneas total que contiene un control edit multilínea:

   char mensaje[512];
   int nLineas;

   nLineas = SendMessage(hctrl, EM_GETLINECOUNT, 0, 0);
   sprintf(mensaje, "Número de líneas = %d", nLineas);
   MessageBox(hwnd, mensaje, "Contol edit multilínea", MB_OK);

Los mensajes EM_GETLINE y EM_GETLINECOUNT, usados de forma conjunta, nos permiten leer y tratar (por ejemplo guardar en un fichero), el contenido de un control edit multilínea, sin preocuparnos de las rupturas de línea blandas:

void Guardar(HWND hctrl, char *fichero) {
   FILE *fs;
   int nLineas, longitud, i;
   char linea[1024];

   fs = fopen(fichero, "w");
   if(fs) {
      nLineas = SendMessage(hctrl, EM_GETLINECOUNT, 0, 0);
      for(i = 0; i < nLineas; i++) {
         *(WORD*)linea = 1024;
         longitud = SendMessage(hctrl, EM_GETLINE, (WPARAM)i, (LPARAM)linea);
         linea[longitud] = 0;
         fprintf(fs, "%s\n", linea);
      }
      fclose(fs);
   }
}

El mensaje EM_LINEINDEX se usa para averiguar el índice del primer carácter de una línea determinada:

   int pos;

   pos = SendMessage(hctrl, EM_LINEINDEX, (WPARAM)1, 0);
   sprintf(mensaje, "Índice del primer carácter de la línea 1 = %d", pos);
   MessageBox(hwnd, mensaje, "Control edit multilínea", MB_OK);

El mensaje EM_LINEFROMCHAR devuelve el índice de la línea que contiene el carácter determinado por un índice dado:

   int pos;

   pos = SendMessage(hctrl, EM_LINEFROMCHAR, (WPARAM)95, 0);
   sprintf(mensaje, "Índice de la línea que contiene el carácter 95 = %d", pos);
   MessageBox(hwnd, mensaje, "Contol edit multilínea", MB_OK);

Los mensajes EM_LINEINDEX y EM_LINEFROMCHAR sólo son válidos para controles edit multilínea.

Ejemplo 53

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 53 win053.zip 2005-11-07 4079 bytes 644

Operaciones sobre selecciones de texto

El usuario puede seleccionar una parte del texto incluido en un control edit, bien usando el ratón (que será lo más frecuente), o bien mediente el teclado, (manteniendo pulsada la tecla de mayúsculas y desplazando el cursor mediante el las teclas de movimiento del cursor).

Estamos acostumbrados ya a las operaciones frecuentes que se pueden hacer sobre una selección: copiar, cortar, borrar o pegar. Veamos ahora cómo podemos realizar estas operaciones en nuestros controles edit.

Cualquier control edit procesará los mensajes WM_CUT, WM_COPY, WM_CLEAR y WM_PASTE que reciba.

En el caso del mensaje WM_CUT, se copiará el texto seleccionado en el portapapeles y después se eliminará.

En el caso del mensaje WM_COPY, el texto seleccionado se copiará en el portapapeles.

En el caso del mensaje WM_CLEAR, el texto seleccionado se eliminará, sin copiarse en el portapapeles.

Y en el caso del mensaje WM_PASTE, el texto que esté en el portapapeles se copiará en la posición actual del caret en el control edit o sustituyendo al texto seleccionado, si existe.

Estas acciones están ya implementadas en el procedimiento de ventana de la clase "EDIT", por lo que bastará con enviar cualquiera de estos mensajes a un control para que funcionen.

Es más, las versiones actuales de Windows desplegarán un menú contextual al pulsar el botón derecho del ratón sobre cualquier control edit. Ese menú contendrá estos cuatro comandos: cortar, copiar, pegar y eliminar, y otros dos: deshacer y seleccionar todo.

También funcionan, cuando el control edit tiene el foco, las combinaciones de teclas para estas seis acciones: [control]+x para cortar, [control]+c para copiar, [control]+v para pegar y [supr] para borrar y [control]+z para deshacer.

Por nuestra parte, podemos añadir estos mensajes a nuestros menús, y enviarlos al control edit que tenga el foco en el momento en que se seleccionen.

Además, disponemos de otros mensajes para controlar la selección de texto.

El mensaje EM_GETSEL nos sirve para obtener los índices de los caracteres correspondientes al inicio y final de la selección actual. Más concretamente, obtendremos el índice del primer carácter de la selección y el del primero no seleccionado a continuación de la selección. Por ejemplo, si la selección incluye los caracteres 10º al 15º de texto de un control, obtendremos los valores 9 y 15:

Texto de ejemplo para ilustrar la selección.
00000000001111111111222222222233333333334444
01234567890123456789012345678901234567890123
   int inicio, final;

   SendMessage(hctrl, EM_GETSEL, (WPARAM)&inicio, (LPARAM)&final);
   sprintf(mensaje, "Selección actual de %d a %d", inicio, final);
   MessageBox(hwnd, mensaje, "Ejemplo de control edit", MB_OK);

De forma simétrica, podemos seleccionar una parte del texto mediante el mensaje EM_SETSEL. Si seleccionamos una parte del texto que no es visible en pantalla, Windows no lo mostrará de forma automática, y el caret quedará fuera de la parte visible del control. Si queremos que el caret sea visble debemos usar el mensaje EM_SCROLLCARET:

   SendMessage(hctrl, EM_SETSEL, 1500, 1554);
   SendMessage(hctrl, EM_SCROLLCARET, 0, 0);

Por último, el mensaje EM_REPLACESEL nos permite sustituir el texto actualmente seleccionado por otro:

   char nuevotexto[64];

   strcpy(nuevotexto, "TEXTO SUSTITUIDO");
   SendMessage(hctrl, EM_REPLACESEL, TRUE, (LPARAM)nuevotexto);

El parámetro wParam de este mensaje es un valor booleano que indica si la operación de sustitución puede ser deshecha o no. TRUE significa que se podrá deshacer, FALSE que no.

Selecciones siempre visibles

Por defecto, si no indicamos lo contrario, el texto seleccionado en un control edit, permanecerá resaltado sólo mientras el control tiene el foco del teclado. Sin embargo, si creamos el control con el estilo ES_NOHIDESEL, el resaltado permanece aunque el control pierda el foco.

Deshacer cambios (undo)

Otra tarea que realiza Windows con los controles edit es la de deshacer cambios, aunque el soporte para esta tarea es limitado, ya que sólo es posible deshacer el último cambio realizado en un control edit.

Al igual que sucede con los mensaje WM_CUT, WM_COPY, WM_PASTE y WM_CLEAR, el mensaje EM_UNDO se procesa de forma automática por el procedimiento de ventana del control edit. Por lo tanto bastará con enviar este mensaje a un control edit, y si es posible, se deshará el último cambio realizado.

SendMessage(hctrl, EM_UNDO, 0, 0);

Disponemos de otros dos mensajes relacionados con estas operaciones. El mensaje EM_CANUNDO nos permite averiguar si es posible deshacer alguna operación de edición. Es decir, si al enviar el mensaje EM_UNDO se deshará algún cambio.

Por último, el mensaje EM_EMPTYUNDOBUFFER vacía el buffer de deshacer, es decir, anula la posibilidad de deshacer cualquier operación de edición.

Para los controles edit sólo se almacena una operación de deshacer, es decir, que enviar repetidamente el mensaje EM_UNDO sólo deshará la última operación de edición, no las anteriores. Si lo último que se hizo fue eliminar un fragmento de texto, el primer mensaje EM_UNDO restaurará el texto eliminado, el segundo volverá a eliminarlo, el tercero lo restaurará de nuevo, y así sucesivamente.

Para operaciones de deshacer más elaboradas deberemos programar nosotros mismos las rutinas necesarias.

Modificación del texto

Windows mantiene una bandera para cada control edit que indica si su contenido ha sido modificado por el usuario o no.

Esta bandera nos permite tomar decisiones en función de si el contenido de un control edit ha sido o no modificado. Por ejemplo, si el contenido de un control correspondiente a un editor de texto no se ha modificado, no tendrá sentido leerlo y actualizar el fichero original.

Windows desactiva esta bandera automáticametne al crear el control, y la activa cada vez que el usuario edita el contenido del control.

Para leer el valor actual de esta bandera se usa el mensaje EM_GETMODIFY:

   BOOL modif;

   modif = SendMessage(hctrl, EM_GETMODIFY, 0, 0);
   if(modif) strcpy(mensaje, "Texto modificado");
   else strcpy(mensaje, "Texto no modificado");
   MessageBox(hwnd, mensaje, "Ejemplo de control edit", MB_OK);

En ocasiones nos interesará volver a desactivar esta bandera, por ejemplo, cuando hemos guardado el contenido actual de nuestro editor en disco, consideraremos que el contenido actual no ha sido modificado, puesto que es el mismo que en el fichero. Para modificar el valor de la bandera se usa el mensaje EM_SETMODIFY.

También podemos combinar el estado de esta bandera con el mensaje de notificación EN_CHANGE. Esto es lo que se suele hacer para mostrar el estado de modificación de un texto en un editor. Cada vez que se recibe un mensaje de notificación EN_CHANGE, se consulta la bandera de modificación, y en función de su valor, se pone la marca que indica si es necesario guardar el contenido o no. También se pueden inhibir las opciones de guardar si la bandera de modificación no está activa.

      case WM_COMMAND:
         switch(LOWORD(wParam)) {
            case CM_GUARDAR:
               Guardar(hctrl, "texto.txt");
               SendMessage(hctrl, EM_SETMODIFY, FALSE, 0);
               ActualizarMenu(hwnd, hctrl);
               break;
            case ID_TEXTO:
               if(HIWORD(wParam) == EN_CHANGE)
                  if(EnableMenuItem(hctrl, EM_GETMODIFY, 0, 0))
                     EnableMenuItem(GetMenu(hwnd), CM_GUARDAR, MF_BYCOMMAND | MF_ENABLED);
               break;
            ...

void ActualizarMenu(HWND hwnd, HWND hctrl) {
   if(SendMessage(hctrl, EM_GETMODIFY, 0, 0))
      EnableMenuItem(GetMenu(hwnd), CM_GUARDAR, MF_BYCOMMAND | MF_ENABLED);
   else
      EnableMenuItem(GetMenu(hwnd), CM_GUARDAR, MF_BYCOMMAND | MF_GRAYED);
}

En este fragmento vemos cómo manipular el mensaje de notificación EN_CHANGE para verificar la bandera de modificación, y activar la opción de menú de "guardar" cuando el contenido del control haya sido modificado.

El procesamiento del mensaje de "guardar" guarda el contenido del control edit, y a continuación elimina la bandera de modificación y actualiza el estado del menú.

Márgenes y tabuladores

Si no indicamos nada, el control edit usará toda la superficie de su área de cliente para mostrar el texto. Pero podemos cambiar esto de varias formas, definiendo los márgenes izquierdo y derecho, o especificando un rectángulo, dentro del área de cliente, que se usará para mostrar el texto.

El mensaje EM_SETMARGINS nos permite fijar los márgenes izquierdo y/o derecho del texto dentro del control edit. En el parámetro wParam indicamos qué márgenes vamos a definir, y en qué unidades se expresan. Para ello podemos combinar los valores EC_LEFTMARGIN, EC_RIGHTMARGIN y EC_USEFONTINFO. El primero para definir el margen izquierdo, el segundo para definir el derecho, y el tercero para indicar que usaremos la anchura del carácter "A" de la fuente actual para el margen izquierdo, y el del carácter "C" para el derecho. Si no usamos este valor, el parámetro lParam indicará la anchura en pixels.

El parámetro lParam indica el márgen izquierdo en la palabra de menor peso, y el derecho en la de mayor peso. Para combinar estos valores se puede usar la macro MAKELONG.

   SendMessage(hctrl, EM_SETMARGINS,
      EC_LEFTMARGIN | EC_RIGHTMARGIN, MAKELONG(50, 30));

El mensaje EM_GETMARGINS se puede usar para recuperar los valores de los márgenes actuales de un control edit.

Otra opción es usar el mensaje EM_SETRECT para definir el rectángulo que se usará para delimitar el texto. En este mensaje se usa el parámetro lParam para indicar un puntero a una estructura RECT que define el rectángulo delimitador.

El mensaje EM_SETRECTNP es idéntico, con la diferencia de que no se actualiza el control edit para reflejar el nuevo aspecto del control.

   GetClientRect(hctrl, &re);
   re.left += 60;
   re.top += 20;
   re.right -= 40;
   re.bottom -= 40;
   SendMessage(hctrl, EM_SETRECT, 0, (LPARAM)&re);

La ventaja de este método es que nos permite definir los márgenes superior e inferior, además del derecho e izquierdo. La desventaja es que estas definiciones no son permanentes, al contrario que en el caso del mensaje EM_SETMARGINS. De modo que si cambiamos el tamaño de la ventana del control, deberemos volver a definir los márgenes.

Para recuperar el rectángulo delimitador actual de un control edit se puede usar el mensaje EM_GETRECT.

Por último, hablaremos de los tabuladores. En los controles edit multilínea, los tabuladores no se despliegan como un número fijo de espacios, sino que corresponden a distancias fijas con respecto al borde izquierdo de la ventana del control. Cada vez que se introduce un carácter tabulador, se añade el espacio necesario para desplazar el caret a la siguiente posición del tabulador. Recordemos que los controles edit pueden usar fuentes de espacio proporcional, y que el uso principal de los tabuladores es crear tablas, por lo tanto, este es el comportamiento más lógico.

Podemos fijar las marcas de tabulación mediante el mensaje EM_SETTABSTOPS. Estas distancias se miden con respecto al borde izquierdo, y se expresan en unidades de diálogo.

Si sólo indicamos una marca de tabulación, todas las marcas se situarán a distancias iguales, si indicamos más, las primeras n marcas se situarán a las distancias indicadas, y el resto serán equidistantes.

   DWORD lista[10] = {10,25,40,65,95,130,160,200,250,360};
   SendMessage(hctrl, EM_SETTABSTOPS, 10, (LPARAM)&lista);

Desplazar texto

Ya vimos más arriba, cuando hablamos del mensaje para seleccionar texto, que podemos desplazar el contenido de un control edit hasta el punto donde se encuentre el caret, con el fin de hacerlo visible. Para esto usamos el mensaje EM_SCROLLCARET:

   ...
   SendMessage(hctrl, EM_SETSEL, 1500, 1559);
   SendMessage(hctrl, EM_SCROLLCARET, 0, 0);
   ...

Pero disponemos de otros dos mensajes para desplazar el contenido de un control edit. Por una parte, el mensaje EM_LINESCROLL, nos permite desplazar el texto verticalmente un número de líneas especificado, y desplazar horizontalmente el número de caracteres especificado:

   /* Desplazar texto 20 caracteres a la derecha
      y 10 líneas hacia abajo */
   SendMessage(hctrl, EM_LINESCROLL, 20, 10);

El otro mensaje es EM_SCROLL, que equivale a usar el mensaje WM_VSCROLL, y permite desplazar el texto verticalmente, línea a línea o página a página:

   case CM_PAGINAARRIBA:
      SendMessage(hctrl, EM_SCROLL, SB_PAGEUP, 0);
      break;
   case CM_LINEAARRIBA:
      SendMessage(hctrl, EM_SCROLL, SB_LINEUP, 0);
      break;
   case CM_LINEAABAJO:
      SendMessage(hctrl, EM_SCROLL, SB_LINEDOWN, 0);
      break;
   case CM_PAGINAABAJO:
      SendMessage(hctrl, EM_SCROLL, SB_PAGEDOWN, 0);
      break;

Ejemplo 54

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 54 win054.zip 2007-03-15 6385 bytes 673

Caracteres y posiciones

Por último, disponemos de dos mensajes que relacionan los puntos físicos de la pantalla con los caracteres que ocupan esas posiciones. El mensaje EM_CHARFROMPOS nos devuelve el índice del carácter situado en las coordenadas especificadas.

Las coordenadas se proporcionan en el parámetro lParam, la x en la palabra de menor peso, y la y en la de mayor peso. Para crear un valor LPARAM a partir de las coordenadas podemos usar la macro MAKELPARAM.

El valor obtenido contiene en la palabra de mayor peso el índice de la línea, y en la de menor peso, el índice del carácter:

   int indice, indicecaracter;
...
   indice = SendMessage(hctrl, EM_CHARFROMPOS, 0, MAKELPARAM(86,52));
   indicecaracter = LOWORD(indice);
   /* Seleccionar carácter en esa coordenada */
   SendMessage(hctrl, EM_SETSEL, indicecaracter, indicecaracter+1);

El mensaje EM_POSFROMCHAR es el inverso al anterior: a partir de un índice de carácter nos devuelve las coordenadas de ese carácter en pantalla.

Para ello indicaremos en el parámetro wParam el índice del carácter, el valor de retorno contiene las coordenas correspondientes, la x en la palabra de menor peso y la y en la de mayor.

   LRESULT punto;
   char mensaje[128];
...
   punto = SendMessage(hctrl, EM_POSFROMCHAR, 153, 0);
   sprintf(mensaje, "Coordenadas del carácter 153: "
      "(%d, %d)", LOWORD(punto), HIWORD(punto));
   MessageBox(hwnd, mensaje, "EM_POSFROMCHAR", MB_OK);

Ejemplo 55

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 55 win055.zip 2007-03-15 5491 bytes 674