43 Control ComboBox avanzado
Seguimos con el repaso de los controles más comunes del API de Window.
En el capítulo 11 vimos cómo usar de una forma básica los combo boxes. También vimos que estos controles son la combinación de un control edit o un control de texto estático y un list box, así como el modo de crearlos a partir de un fichero de recursos, de inicializarlos y de leer el valor de su selección.
En este capítulo veremos mucho más sobre ellos, cómo personalizarlos, qué estilos pueden tener, sus mensajes de notificación, etc.
Ya que son la combinación de un control edit (o static) y de un control listbox, estos controles tendrán muchas de las características de los controles de los que se derivan, y por lo tanto, disponen de los equivalentes de algunos de sus estilos, mensajes de notificación y mensajes de control.
Tipos de combo boxes
Ya vimos que existen tres tipos básicos de combo boxes, dependiendo de que se seleccione uno de los tres estilos que los definen:
- Simple: si se usa el estilo CBS_SIMPLE
- Desplegable: (Drop-down) si se usa el estilo CBS_DROPDOWN. La lista se puede desplegar mediante el teclado mediante F4, o pulsando sobre el icono de la flecha abajo. El texto de entrada se puede introducir en la zona de edición, o se puede elegir un valor de la lista. La siguiente imagen muestra el mismo combo box con la lista plegada y desplegada.
- Lista desplegable: (Drop-down list) si se usa el estilo CBS_DROPDOWNLIST. Al igual que en el tipo desplegable, la lista se puede desplegar mediante el teclado o el ratón, pero en este caso, el texto sólo se puede elegir entre los valores de la lista. Se puede usar el teclado para elegir el elemento de la lista, tecleando el principio de la cadena. La imagen siguiente muestra uno de estos controles con la lista plegada y desplegada.
Insertar controles combo box durante la ejecución
No hay nada nuevo en este tema, del mismo modo que hemos visto para los controles anteriormente tratados, también es posible insertar controles combo box durante la ejecución. En este caso tendremos que insertar una ventana de la clase "COMBOBOX". Para insertar el control también usaremos las funciones CreateWindow y CreateWindowEx.
HWND hctrl; ... case WM_CREATE: hInstance = ((LPCREATESTRUCT)lParam)->hInstance; /* Insertar control Edit */ hctrl = CreateWindowEx( 0, "COMBOBOX", /* Nombre de la clase */ NULL, /* Texto del título */ CBS_SIMPLE | WS_VSCROLL | WS_CHILD | WS_VISIBLE | WS_TABSTOP, /* Estilo */ 5, 5, /* Posición */ 120, 85 /* Tamaño */ hwnd, /* Ventana padre */ (HMENU)ID_COMBO1,/* Identificador del control */ hInstance, /* Instancia */ NULL); /* Sin datos de creación de ventana */
Como vemos, usamos los mismos valores que en el fichero de recursos: un identificador, la clase de ventana (en este caso "COMBOBOX"), una combinación de estilos, la posición y las dimensiones.
Al igual que en los demás controles, el identificador del control se suministra a través del parámetro hMenu, por lo que será necesario hacer un casting a HMENU.
Cambiar la fuente de un control combo box
En esto tampoco hay novedades, podemos cambiar la fuente de un control combo box mediante un mensaje WM_SETFONT:
static HFONT hfont; ... hfont = (HFONT)GetStockObject( DEFAULT_GUI_FONT ); SendMessage(hctrl, WM_SETFONT, (WPARAM)hfont, MAKELPARAM(TRUE, 0));
Cambiar colores en combo box
Al tratarse de controles híbridos, formados mediante controles más simples, para cambiar el color de los controles combo box deberemos responder a varios de los mensajes de cambio de color, según la zona que nos interese personalizar.
Así, respondiendo al mensaje WM_CTLCOLORLISTBOX, podremos modificar el color de fondo y el del texto de la parte de la lista.
Mediante el mensaje WM_CTLCOLOREDIT, podremos modificar el color de fondo y del texto de la parte correspondiente al control de edición o estática (según el tipo de combo box).
Por supuesto, en cada caso deberemos retornar con un manipulador de pincel con el color usado para el fondo.
static HBRUSH pincel, pincel2; ... case WM_CREATE: ... pincel = CreateSolidBrush(RGB(0,255,0)); pincel2 = CreateSolidBrush(RGB(0,255,255)); ... case WM_CTLCOLOREDIT: SetTextColor((HDC)wParam, RGB(255,255,0)); SetBkColor((HDC)wParam, RGB(0,255,255)); return (LRESULT)pincel2; case WM_CTLCOLORLISTBOX: SetTextColor((HDC)wParam, RGB(255,0,0)); SetBkColor((HDC)wParam, RGB(0,255,0)); return (LRESULT)pincel; ... case WM_DESTROY: ... DeleteObject(pincel); DeleteObject(pincel2); ...
Mensajes de notificación
Entre los mensajes de notificación de los controles combo box, los hay relativos a la parte del control de edición y a la parte de la lista.
Como en todos los casos, 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. En el parámetro lParam se envía el manipulador del control 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.
Cambio en selección de lista
El mensaje de notificación CBN_SELCHANGE se envía a la ventana padre cada vez que la selección en el list box de un combo box vaya a cambiar por una acción del usuario.
Validar selección
El mensaje de notificación CBN_SELENDOK se envía a la ventana padre si después de hacer una selección se cierra la lista. El mensaje CBN_SELENDCANCEL se envía si después de una selección, la lista no se pliega, sino que se cierra el cuadro de diálogo o el foco pasa a otro control.
Estos mensajes sólo se envían a combo boxes que no tengan el estilo CBS_SIMPLE, ya que en el caso de los combo boxes simples, la lista no puede ser cerrada.
Despliegue de lista
El mensaje de notificación CBN_DROPDOWN se envía cada vez que la lista de un combo box es desplegada. De forma simétrica, se envía un mensaje CBN_CLOSEUP cada vez que la lista se pliega de nuevo.
Doble clic
Cada vez que el usuario hace doble clic sobre uno de los elementos del list box de un combo box, se envía un mensaje de notificación CBN_DBLCLK a su ventana padre.
Falta espacio
Si no es posible conseguir memoria para completar una operación sobre el list box, se envía un mensaje de notificación CBN_ERRSPACE.
Modificación
Cada vez que el usuario modifica el texto de la parte del control de edición, primero se actualiza el contenido del control en pantalla, y a continuación se genera un mensaje CBN_EDITCHANGE.
Actualización
Cada vez que el usuario modifica el texto de la parte del control de edición, y antes de que este nuevo texto se muestre en pantalla, Windows envía un mensaje CBN_EDITUPDATE.
Pérdida y recuperación de foco
Cada vez que el usuario selecciona otro control se envía un mensaje de notificación CBN_KILLFOCUS.
Cuando el usuario selecciona un control list box, se envía un mensaje de notificación CBN_SETFOCUS.
Otros estilos para combo box
Entre los estilos también hay es evidente que estos controles son una mezcla de un control de edición o estático con un control de lista. Esto se ve en que hay estilo que sólo afectan a una de las partes o a las dos.
Estilos para la parte de edición
- CBS_AUTOHSCROLL: cuando el texto que se introduce en la parte del control de edición de un combo box no cabe en el área destinada a mostrarlo, se desplaza automáticamente a la derecha. Si no se activa este estilo, sólo podrá introducirse el texto que cabe dentro de los límites del cuadro de edición.
- CBS_LOWERCASE: cualquier texto introducido en el control edit del combo box se convierte a minúsculas.
- CBS_UPPERCASE: cualquier texto introducido en el control edit del combo box se convierte a mayúsculas.
Estilos para la lista
Ya hemos visto antes algunos de estos estilos, pero comentaremos todos de nuevo:
- CBS_DISABLENOSCROLL: se muestra siempre la barra de scroll vertical del list box, aunque aparece deshabilitada cuando no hay suficientes elementos para llenar la lista. Si no se especifica este estilo, la barra de scroll se oculta, en lugar de deshabilitarse, cuando no hay suficientes elementos en la lista.
- CBS_HASSTRINGS: como en el caso de los controles list box, este estilo está siempre activo en combo boxes que no tengan el estilo owner-drawn (controles que son actualizados por la ventana padre). Este estilo indica que el control contiene elementos que son cadenas de caracteres. El combo box es el encargado de mantener la memoria y las direcciones de las cadenas, de modo que la aplicación puede usar el mensaje CB_GETLBTEXT para recuperar el texto de un elemento en particular.
- CBS_NOINTEGRALHEIGHT: especifica que el tamaño de la lista del combo box es exactamente el indicado por la aplicación cuando se creó. Normalmente, Windows cambia el tamaño del combo box para que se muestren líneas completas, y no se corte el texto correspondiente a la última línea de la lista.
- CBS_SORT: ordena alfabéticamente, de forma automática las cadenas introducidas en la lista.
Ejemplo 71
Nombre | Fichero | Fecha | Tamaño | Contador | Descarga |
---|---|---|---|---|---|
Ejemplo 71 | win071.zip | 2007-03-15 | 3583 bytes | 649 |
Mensajes correspondientes a la lista
La lista del combo box normalmente se inicializa por la aplicación, y el usuario puede elegir uno de los elementos de la lista, y salvo en el estilo de lista desplegable, también se podrán introducir valores que no estén en la lista.
Añadir ítems
Mediante el envío de un mensaje CB_ADDSTRING podemos añadir cadenas a la lista de un combo box, la dirección de la cadena se envía en el parámetro lParam.
Si el combo box tiene el estilo CBS_SORT, la nueva cadena se inserta en la posición adecuada para mantener el orden alfabético entre los elementos de la lista.
sprintf(cad, "NUEVA CADENA"); SendMessage(hctrl, CB_ADDSTRING, 0, (LPARAM)cad);
El mensaje CB_INSERTSTRING nos permite insertar cadenas en la lista en una posición determinada, independientemetne de que el combo box tenga el estilo CBS_SORT, la cadena siempre se insertará en la posición especificada por el parámetro wParam. La cadena se indica en el parámetro lParam.
/* Insertar un ítem antes del seleccionado actualmente */ strcpy(cad, "CADENA INSERTADA"); SendMessage(hctrl, CB_INSERTSTRING, (WPARAM)3, (LPARAM)cad);
Recuperar información
Los mensajes CB_GETLBTEXT y CB_GETLBTEXTLEN nos sirven para leer cadenas desde la lista de un combo box. Para el primero se indica en el parámetro wParam el índice del ítem a recuperar, y en lParam la dirección del buffer donde se almacena la cadena leída. El segundo mensaje nos sirve para obtener la longitud de la cadena de un ítem, indicado mendiante su índice en el parámetro wParam.
/* Obtener cadena seleccionada */ int i, l; char *cad; ... i = SendMessage(hctrl, CB_GETCURSEL, 0, 0); l = SendMessage(hctrl, CB_GETLBTEXTLEN, (WPARAM)i, (LPARAM)cad); cad = (char*)malloc(l+1); SendMessage(hctrl, CB_GETLBTEXT (WPARAM)i, (LPARAM)cad);
El mensaje CB_GETCOUNT no tiene parámetros, y sirve para obtener el número de elementos que contiene la lista de un combo box.
/* Obtener número de ítems */ int i; ... i = SendMessage(hctrl, CB_GETCOUNT, 0, 0); sprintf(cad, "Número de ítems: %d", i); MessageBox(hwnd, cad, "Combo Box", MB_OK);
Para obtener el índice del ítem actualmente seleccionado en la lista de un combo box, se usa el mensaje CB_GETCURSEL. Este mensaje no precisa parámetros.
int i; ... i = SendMessage(hctrl, CB_GETCURSEL, 0, 0);
En el caso de controles combo box con el estilo CBS_DROPDOWN, en general nos interesará recuparar el texto que aparezca en el control de edición. Ya que el usuario puede escribir un texto que no aparece en la lista, recuparar el texto del control de edición asegura que siempre recuperamos el valor introducido por el usuario.
Para recuperar ese valor usaremos el mensaje WM_GETTEXT:
SendDlgItemMessage(hwnd, ID_COMBO, WM_GETTEXT, 128, (LPARAM)cad);
O bien:
hctrl = GetDlgItem(hwnd, ID_COMBO); SendMessage(hctrl, WM_GETTEXT, 128, (LPARAM)cad);
Recordemos que en parámetro wParam indicaremos la longitud máxima de la cadena a recuperar y en lParam pasaremos un puntero a char, es decir, la dirección donde almacenaremos la cadena.
Cambiar la selección
El mensaje CB_SETCURSEL nos permite seleccionar un ítem, indicado por el parámetro wParam. Además, se elimina cualquier selección previa, y el contenido de la lista se desplaza, si es necesario, para mostrar la nueva cadena seleccionada.
/* Seleccionar ítem siguiente al actual */ int i; ... i = SendMessage(hctrl, CB_GETCURSEL, 0, 0); SendMessage(hctrl, CB_SETCURSEL, (WPARAM)i+1, 0);
El mensaje CB_SELECTSTRING sirve para seleccionar una cadena determinada. En el parámetro wParam se envía el índice en que debe comenzar la búsqueda y en lParam la dirección de la cadena a buscar.
/* Seleccionar primera cadena que empiece por "E", después del 6º ítem */ int i=6; ... SendMessage(hctrl, CB_SELECTSTRING, (WPARAM)i, (LPARAM)"E");
Buscar ítems
El mensaje CB_FINDSTRING nos permite buscar una cadena que coincida con el prefijo especificado en el parámetro lParam, a partir del índice indicado en wParam:
/* Seleccionar la primera cadena que empiece por "EN" */ int i; ... i = SendMessage(hctrl, CB_FINDSTRING, (WPARAM)-1, (LPARAM)"EN"); SendMessage(hctrl, CB_SETCURSEL, (WPARAM)i, 0);
El mensaje CB_FINDSTRINGEXACT es parecido, pero no usa el parámetro lParam como un prefijo, sino que busca una cadena que coincida exactamente con ese parámetro.
/* Seleccionar la cadena igual a "Enero" */ int i; ... i = SendMessage(hctrl, CB_FINDSTRINGEXACT, (WPARAM)-1, (LPARAM)"Enero"); SendMessage(hctrl, CB_SETCURSEL, (WPARAM)i, 0);
En cualquiera de los dos casos, si no se encuentra la cadena buscada, el valor de retorno es CB_ERR.
Borrar ítems
Para eliminar ítems se usa el mensaje CB_DELETESTRING, en el que indicaremos en el parámetro wParam el valor del índice a eliminar.
/* Eliminar cadena actualmente seleccionada */ int i; ... i = SendMessage(hctrl, CB_GETCURSEL, 0, 0); SendMessage(hctrl, CB_DELETESTRING, (WPARAM)i, 0);
Mediante el mensaje CB_RESETCONTENT, sin parámetros, podemos vaciar una lista de un combo box por completo.
/* Vaciar list box */ SendMessage(hctrl, CB_RESETCONTENT, 0, 0);
Otros mensajes
El mensaje CB_GETTOPINDEX sirve para recuperar el índice del primer ítem visible de la lista de un combo box. Este mensaje no tiene parámetros.
De forma simética, el mensaje CB_SETTOPINDEX sirve para asegurar que un determinado ítem estará en la parte visible de la lista de un combo box. En el parámetro wParam se indica el índice del ítem que queremos que sea visible.
Mediante el mensaje CB_SHOWDROPDOWN podemos mostrar u ocultar la lista desplegable asociada a un combo box con el estilo CBS_DROPDOWN o CBS_DROPDOWNLIST.
También podemos obtener el estado de la lista desplegable mediante el mensaje CB_GETDROPPEDSTATE. Este mensaje no tiene parámetros, y el valor de retorno indica si la lista está desplegada (TRUE) o plegada (FALSE).
Ejemplo 72
Nombre | Fichero | Fecha | Tamaño | Contador | Descarga |
---|---|---|---|---|---|
Ejemplo 72 | win072.zip | 2007-03-15 | 3471 bytes | 644 |
El dato del ítem
Ya sabemos que cada ítem tiene asociado un índice y una cadena. Pero también tiene asociado un dato entero de 32 bits: el ítem data, o dato de ítem.
A cada ítem le podemos asignar un valor entero mediante el mensaje CB_SETITEMDATA, y recuperarlo mediante CB_GETITEMDATA.
Podemos aprovechar que el valor de retorno del mensaje CB_ADDSTRING es el índice del ítem insertado, y usar ese valor en el mensaje CB_SETITEMDATA, para asignar el valor del índice en el array:
void IniciarLista(HWND hctrl) { char* mes[] = {"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"}; int i; int actual; for(i = 0; i < 12; i++) { actual = SendMessage(hctrl, CB_ADDSTRING, 0, (LPARAM)mes[i]); SendMessage(hctrl, CB_SETITEMDATA, (WPARAM)actual, i); } }
Ya veremos que el dato de ítem tiene otras utilidades, pero en muchos casos nos proporciona una forma útil de almacenar un dato relativo a un ítem. Al tratarse de un entero de 32 bits también puede contener punteros.
Interfaces de usuario
Existen dos comportamientos diferentes de los combo boxes con listas desplegables (con el estilo CBS_DROPDOWN o CBS_DROPDOWNLIST), con respecto al teclado. Cada uno de estos comportamientos viene definido por un interfaz.
En el interfaz por defecto, la tecla F4 despliega u oculta la lista desplegable, y las teclas de flecha arriba y abajo nos permiten desplazarnos a través de las distintas opciones.
En el interfaz extendido la tecla F4 no tiene ninguna función, y la lista se despliega tan pronto como se pulsa una de las teclas de flecha arriba o abajo.
Las flechas funcionan tanto si la lista está desplegada como si no.
Para cambiar el interfaz de un control combo box se usa el mensaje CB_SETEXTENDEDUI, si se usa el valor FALSE en el parámetro wParam, se activa el interfaz por defecto, y con el valor TRUE se activa el interfaz extendido.
Mediante el mensaje CB_GETEXTENDEDUI se puede obtener el valor actual del interfaz para un control combo box determinado. Este mensaje no tiene parámetros, y el valor de retorno indica el interfaz asociado al control. TRUE si es el extendido y FALSE si es el interfaz por defecto.
Funciones para ficheros y directorios
Como vimos con los list boxes, en los combo boxes también es posible iniciar la lista usando los nombres de ficheros de una unidad de disco o un directorio.
La función DlgDirListComboBox nos permite iniciar el contenido de una lista asociada a un combo box a partir de los ficheros, carpetas, unidades de disco, etc.
Esta función necesita cinco parámetros. El primero es un manipulador de la ventana o diálogo que contiene el combo box que vamos a inicializar. El segundo es un puntero a una cadena con el camino del directorio a mostrar. El tercer parámetro es el identificador del combo box. El cuarto el identificador de un control estático, que se usa para mostrar el camino actualmente mostrado en el combo box. El último parámetro nos permite seleccionar el tipo de entradas que se mostrarán.
Mediante este último parámetro podemos restringir el tipo de entradas, impidiendo o permitiendo que se muestren directorios o unidades de almacenamiento, o limitando los atributos de los ficheros y directorios a mostrar.
Ya hemos dicho que se necesita un control estático.
HWND hestatico; ... hestatico = CreateWindowEx( 0, "STATIC", /* Nombre de la clase */ "", /* Texto del título, no tiene */ WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, /* Estilo */ 9, 4, /* Posición */ 344, 18, /* Tamaño */ hwnd, /* Ventana padre */ (HMENU)ID_TITULO,/* Identificador del control */ hInstance, /* Instancia */ NULL); /* Sin datos de creación de ventana */ SendMessage(hestatico, WM_SETFONT, (WPARAM)hfont, MAKELPARAM(TRUE, 0));
Por supuesto, podemos usar los comodines '*' y '?' para los nombres de fichero:
... IniciarCombo(hwnd, "*.c"); ... void IniciarCombo(HWND hwnd, char* p) { char path[512]; strcpy(path, p); DlgDirListComboBox( hwnd, /* manipulador de cuadro de diálogo con list box */ path, /* puntero a cadena de camino o nombre de fichero */ ID_COMBO, /* identificador de list box */ ID_TITULO, /* identificador de control estático */ DDL_DIRECTORY | DDL_DRIVES /* atributos de ficheros a mostrar */ ); }
Por otra parte, la función DlgDirSelectComboBoxEx nos permite leer la selección actual de un combo box inicializado mediante la función DlgDirListComboBox. Si el valor de retorno de esta función es distinto de cero, la selección actual es un directorio o unidad de almacenamiento, por lo que será posible hacer un cambio de directorio. Si el valor de retorno es cero, se trata de un fichero.
Aprovecharemos esto para navegar a lo largo de los discos de nuestro ordenador, para lo que responderemos al mensaje de notificación CBN_DBLCLK, cambiando a la nueva ubicación o mostrando el nombre del fichero seleccionado:
case WM_COMMAND: switch(LOWORD(wParam)) { case ID_COMBO: switch(HIWORD(wParam)) { case CBN_CLOSEUP: if(DlgDirSelectComboBoxEx(hwnd, cad, 512, ID_COMBO)) { strcat(cad, "*.c"); IniciarCombo(hwnd, cad); } else {MessageBox(hwnd, cad, "Fichero seleccionado", MB_OK); break; ...
También existe un mensaje relacionado con este tema.
El mensaje CB_DIR tiene un uso equivalente a la función DlgDirListComboBox. En el parámetro wParam se indican los atributos de los ficheros a mostrar, así como si se deben mostrar directorios y unidades de almacenamiento. En el parámetro lParam se suministra el nombre de fichero, que puede tener comodines) o el camino de los ficheros a insertar.
Por ejemplo, podemos añadir los ficheros de cabecera al contenido del combo box, de modo que se muestren los ficheros fuente en c y los de cabecera:
void IniciarLista(HWND hwnd, char* p) { char path[512]; strcpy(path, p); DlgDirComboBox(hwnd, path, ID_LISTA, ID_TITULO, DDL_DIRECTORY | DDL_DRIVES); strcpy(path, "*.h"); SendMessage(GetDlgItem(hwnd, ID_LISTA), CB_DIR, (WPARAM)0, (LPARAM)path);
Juegos de caracteres
Disponemos de un estilo específico para controles combo box que contienen nombres de ficheros, se trata del estilo CBS_OEMCONVERT. Cuando un control tiene este estilo, las cadenas insertadas se convierten desde juego de caracteres de Windows al juego de caracteres OEM y después se vuelve a convertir al juego de caracteres de Windows. Esto asegura una conversión correcta si la aplicación llama a la función CharToOem para convertir una cadena Windows del combo box a una cadena OEM. Este sólo se puede aplicar a controles con el estilo CBS_SIMPLE o CBS_DROPDOWN.
Procesar CBN_CLOSEUP
Es aconsejable procesar el mensaje de notificación CBN_CLOSEUP cuando se trabaja con combo boxes que contienen directorios o ficheros, sobre todo cuando la elección de una de las opciones requiera algún tipo de proceso, por ejemplo, al cambiar de directorio se deberá limpiar la lista y volver a generarla. En estos casos no sería lógico procesar el mensaje de notificación CBN_SELCHANGE.
Selección actual
Es el ítem de la lista del combo box seleccionado por el usuario. Su texto se copia en el campo de selección, tanto si se trata de un control de edición o de uno estático. Salvo en el caso de la lista desplegable, existe otra forma de introducir datos en un combo box: teclearlos en el control edit.
Ya hemos visto que es posible recuperar el valor de la selección actual mediante un mensaje CB_GETCURSEL, o cambiarlo mediante un mensaje CB_SETCURSEL o CB_SELECTSTRING.
También hemos visto que existe un mensaje de notificación CBN_SELCHANGE que se envía a la ventana padre del combo box cada vez que el usuario cambia la selección actual. Hay que tener en cuenta que este mensaje de notificación no se envía si la selección actual se modifica directamente usando un mensaje CB_SETCURSEL.
Ejemplo 73
En este ejemplo hemos usado algunas funciones y estructuras del API, relacionadas con información sobre ficheros y manejo de tiempos, que no hemos comentado con anterioridad:
- GetFileAttributesEx sirve para obtener información sobre un fichero, sus atributos, fechas de creación, modificación y último acceso y tamaño.
- WIN32_FILE_ATTRIBUTE_DATA es la estructura usada por la función anterior para devolver los datos de un fichero.
- FileTimeToSystemTime las fechas devueltas por la función anterior están en formato de fecha de fichero, esta función permite traducir ese formato a formato de fecha de sistema, que es más manejable.
- FILETIME estructura que almacena una fecha en forma de número entero.
- SYSTEMTIME estructura que almacena una fecha en forma de campos individuales.
Veremos todos estos conceptos en capítulos futuros dedicados específicamente a ello.
Nombre | Fichero | Fecha | Tamaño | Contador | Descarga |
---|---|---|---|---|---|
Ejemplo 73 | win073.zip | 2007-03-15 | 4107 bytes | 663 |
El control de edición
El campo de selección de un combo box puede ser un control de edición o un control estático. El la parte que contiene el valor de la selección actual o el texto introducido por el usuario.
Podemos usar el mensaje WM_GETTEXT para obtener el texto que contiene actualmente, o el mensaje WM_SETTEXT para modificarlo.
También podemos usar el mensaje CB_GETEDITSEL para obtener las posiciones de inicio y final de la selección de texto actual, si existe; o usar el mensaje CB_SETEDITSEL para seleccionar parte del texto dentro del control de edición.
Si se usa el estilo CBS_AUTOHSCROLL es posible introducir más texto del que se puede visualizar en la anchura del control, en caso contrario la cantidad de texto que es posible introducir estará limitada por esa anchura.
También se puede limitar el número de caracteres que el usuario puede teclear mediante el mensaje CB_LIMITTEXT.
Hay dos mensajes de notificación relacionados con el control de edición. Cuando el se modifica el contenido del control de edición, la ventana padre recibe primero el mensaje de notificación CBN_EDITUPDATE, para indicar que el texto se ha alterado. Después de que el texto se ha mostrado, Windows envía el mensaje CBN_EDITCHANGE.
Actualizaciones de gran número de ítems
Hay dos posibles situaciones de potencialmente peligrosas en las las actualizaciones que afecten a muchos ítems en un combo box.
Por una parte, el proceso puede requerir una cantidad importante de memoria, cuando se añaden muchos ítems.
Por otra parte, el proceso puede requerir mucho tiempo, ya sea porque se deben añadir muchos ítems o porque se deben hacer muchas modificaciones que impliquen el borrado e inserción de ítems.
Optimizar la memoria
En versiones de Windows anteriores al uso de la memoria virtual, era necesario tener en cuenta la memoria disponible antes de insertar un gran número de ítems en un combo box. Para eso se usaba el mensaje CB_INITSTORAGE, en el que indicamos en el parámetro wParam el número de ítems a añadir, y en el parámetro lParam la candidad de memoria estimada necesaria para acomodar esos ítems.
/* Prepararse para insertar 10000 ítems de 32 bytes por ítem, aproximadamente */ SendMessage(hctrl, CB_INITSTORAGE, 10000, 320000); IniciarLista(hctrl);
No es necesario ser demasiado preciso con la canditad de memoria requerida, se trata sólo de una estimación, si nos quedamos cortos, los ítems que no quepan se insertarán del modo normal. Si nos quedamos largos, la memoria sobrante se podrá aprovechar en nuevas inserciones.
Este mensaje sólo es necesario en Windows 95, en NT no nos preocupa la memoria necesaria para almacenar los ítems, ya que el modelo de memoria virtual dispone de una cantidad prácticamente ilimitada.
Optimizar el tiempo
El problema del tiempo sí es importante. Cada vez que se añade o elimina un ítem, el combo box intenta actualizar la pantalla para reflejar los cambios, al menos en combo boxes con el estilo CBS_SIMPLE donde la lista está permanentemente desplegada. Esto, cuando los cambios son muy numerosos, hará que aparentemente la aplicación no responda, y que el tiempo invertido en las actualizaciones sea mayor del necesario.
En el caso de los controles combo box no disponemos de un estilo equivalente al estilo LBS_NOREDRAW de los list box, que inhibe las actualizaciones de la lista. En el caso de los combo boxes, cuando puedan contener muchos valores en la lista, será mejor usar otros estilos diferentes de CBS_SIMPLE.
Aspectos gráficos del combo box
En cuanto al aspecto gráfico del combo box tenemos otras opciones que podemos controlar.
Ajustar la anchura de un combo box
Por una parte, ya vimos que podemos añadir una barra de desplazamiento horizontal creando nuestro combo box con el estilo WS_HSCROLL. Puede ser útil si la anchura de los ítems sobrepasa la del list box.
Sin embargo, usar este estilo no asegura que la barra de desplazamiento sea mostrada. Para que la barra aparezca hay que ajustar la extensión horizontal del list box mediante un mensaje CB_SETHORIZONTALEXTENT, indicando en el parámetro wParam la nueva extensión horizontal, en pixels.
Si la extensión horizontal es mayor que la anchura del combo box, se mostrará la barra de desplazamiento, en caso contrario la barra no aparecerá.
Esto nos plantea una duda, ¿cómo calcular la extensión necesaria según las longitudes de las cadenas contenidas en el combo box?
Bueno, podríamos hacerlo a ojo, pero esta técnica es arriesgada, ya que si nos quedamos cortos no será posible visualizar por completo algunos ítems.
Lo mejor es calcular la longitud de cada cadena al insertarla, y si es mayor que la extensión actual, actualizar el valor de la extensión. Para obtener el valor de la extensión actual se usa el mensaje CB_GETHORIZONTALEXTENT.
Claro que esto plantea un problema si se eliminan ítems, ya que nos obligaría a calcular las longitudes de todas las cadenas que quedan en el combo box. Sin embargo, podemos ignorar estos casos, y mantener la extensión, ya que la visibilidad de todos los ítems está asegurada.
Para calcular la longitud de una cadena en pixes, vimos en el capítulo 24, que podemos usar la función GetTextExtentPoint32, por ejemplo, en la siguiente función:
int CalculaLongitud(HWND hwnd, char *cad) { HDC hdc; SIZE tam; HFONT hfont; hfont = (HFONT)GetStockObject( DEFAULT_GUI_FONT ); hdc = GetDC(hwnd); SelectObject(hdc, hfont); GetTextExtentPoint32(hdc, cad, strlen(cad), &tam); /*LPtoDP(hdc, (POINT *)&tam, 1);*/ ReleaseDC(hwnd, hdc); return tam.cx; }
Para que el cálculo sea correcto debemos seleccionar en el DC la misma fuente que usamos en el combo box. Además, habría que tener en cuenta que la función GetTextExtentPoint32 devuelve el tamaño de la cadena en unidades lógicas, y en rigor habría que convertir esos valores a unidades de dispositivo. Pero esto es innecesario, ya que en un control no se realiza ninguna proyección.
Así, cada vez que insertemos un ítem en el combo box, deberemos comprobar si resulta ser el más largo:
char item[300]; int x; int eActual; eActual = SendMessage(hlista, CB_GETHORIZONTALEXTENT, 0, 0); strcpy(item, "Ítem de una anchura tal que no cabe en " "el combo box que hemos definido, o al menos no debería caber, " "si las cosas salen tal y como las hemos calculado, claro."); x = CalculaLongitud(hlista, cad); if(x > eActual) eActual = x; SendMessage(hlista, CB_ADDSTRING, 0, (LPARAM)cad); SendMessage(hlista, CB_SETHORIZONTALEXTENT, eActual, 0);
Ajustar la altura de los ítems
Por defecto, la altura de los ítems se calcula en función de la fuente asignada al list box. Podemos obtener el valor de la altura del ítem mediante el mensaje CB_GETITEMHEIGHT. Si se trata de un combo box con un estilo owner-draw cada ítem puede tener una altura diferente, y se puede especificar el índice del ítem en el parámetro wParam. En los combo box normales, el valor de wParam debe ser cero.
h = SendMessage(hctrl, CB_GETITEMHEIGHT, 0, 0);
Para modificar la altura de un ítem se usa el mensaje CB_SETITEMHEIGHT, en el caso de combo boxes con un estilo owner-draw se puede asignar una altura diferente a cada ítem. En ese caso, especificaremos el índice del ítem en el parámetro wParam, y la altura deseada en la palabra de menor peso del parámetro lParam, usando la macro MAKELPARAM. Veremos esto con más detalle al estudiar los estilos owner-draw.
SendMessage(hctrl, CB_SETITEMHEIGHT, 0, MAKELPARAM(30,0)); InvalidateRect(hctrl, NULL, TRUE);
Localizaciones
Ya hemos visto que en los controles combo box los ítems se muestran por orden alfabético, al menos en los que hemos usado hasta ahora. Pero el orden alfabético no es algo universal, y puede cambiar dependiendo del idioma.
Generalmente esto no nos preocupará, ya que el idioma usado para elegir el orden se toma del propio sistema. Sin embargo, puede haber casos en que nos interese modificar o conocer el idioma usado en un combo box.
Para obtener el valor de la localización actual se usa el mensaje CB_GETLOCALE. El valor de retorno es un entero de 32 bits, en el que la palabra de menor peso contiene el código de país, y el de mayor peso el del lenguaje, este último a su vez, se compone de un identificador de lenguaje primario y un identificador de sublenguaje.
Se pueden usar las macros PRIMARYLANGID y SUBLANGID para obtener el identificador de lenguaje primario y el de sublenguaje, respectivamente.
int i; char cad[120]; ... i = SendMessage(hctrl, CB_GETLOCALE, 0, 0); sprintf(cad, "País %d, id lenguaje primario %d, " "id de sublenguaje %d", HIWORD(i), PRIMARYLANGID(LOWORD(i)), SUBLANGID(LOWORD(i))); MessageBox(hwnd, cad, "Localización", MB_OK);
También podemos modificar la localización actual mediante un mensaje CB_SETLOCALE, indicando en el parámetro wParam el nuevo valor de localización. Podemos crear uno de estos valores mediante las macros MAKELCID y MAKELANGID:
SendMessage(hctrl, CB_SETLOCALE, MAKELCID(MAKELANGID(LANG_SPANISH, SUBLANG_SPANISH), SSORT_DEFAULT), 0);
La macro MAKELCID crea un identificador de localización a partir de un identificador de lenguaje y una constante que debe ser SSORT_DEFAULT.
La macro MAKELANGID crea un identificador de lenguaje a partir de un identificador de lenguaje primario y de un identificador de sublenguaje.
Combo boxes owner draw
El funcionamiento de los controles combo box owner-draw es muy similar al de los controles list box. Mucho de lo que se comentó para estos controles se aplica iguamente a los combo box.
De modo que también existen dos estilos distintos owner-draw que se pueden aplicar a los controles combo box CBS_OWNERDRAWFIXED y CBS_OWNERDRAWVARIABLE.
El primero define controles combo box owner-draw en los que la altura de todos los ítems es la misma. En el segundo caso, las alturas de cada ítem pueden ser diferentes.
Como también pasa en los controles list box, en los combo box con estilos owner-draw tampoco se activa por defecto el estilo CBS_HASSTRINGS. Si estamos personalizando nuestros controles, lo más probable que éste no contenga cadenas, o al menos, no sólo cadenas.
Sin embargo, es posible que aún tratándose de un combo box con un estilo owner-draw, nuestro control contenga cadenas. En ese caso podemos activar el estilo CBS_HASSTRINGS, sobre todo si queremos que los ítems se muestren por orden alfabético.
hctrl = CreateWindowEx( 0, "COMBOBOX" /* Nombre de la clase */ "", /* Texto del título */ CBS_HASSTRINGS | CBS_OWNERDRAWVARIABLE | CBS_DROPDOWNLIST | CBS_SORT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, /* Estilo */ 9, 19, /* Posición */ 320, 250, /* Tamaño */ hwnd, /* Ventana padre */ (HMENU)ID_COMBO, /* Identificador del control */ hInstance, /* Instancia */ NULL); /* Sin datos de creación de ventana */
Si no activamos el estilo CBS_HASSTRINGS, el valor que usemos al insertar el ítem será almacenado en el dato del ítem de 32 bits.
void IniciarCombo(HWND hctrl) { int i; for(i = 0; i < 10; i++) SendMessage(hctrl, CB_ADDSTRING, 0, i); }
Combo box owner-draw de altura fija
La ventana propietaria del control recibirá el mensaje WM_MEASUREITEM cuando el control combo box sea creado.
En el parámetro lParam recibiremos un puntero a una estructura MEASUREITEMSTRUCT que contiene las dimensiones del control.
En el parámetro wParam recibiremos el valor del identificador del control, o lo que es lo mismo, el valor del miembro CtlID de la estructrura MEASUREITEMSTRUCT apuntada por el parámetro lParam. Este valor identifica el control del que procede el mensaje WM_MEASUREITEM.
Tengamos en cuenta que pueden existir varios controles con el estilo owner-draw, y no tienen por qué ser necesariamente del tipo combo box. Si este valor es cero, el mensaje fue enviado por un menú. Si el valor es distinto de cero, el mensaje fue enviado por un combobox o por un listbox.
Nuestra aplicación debe rellenar de forma adecuada la estructura MEASUREITEMSTRUCT apuntada por el parámetro lParam regresar. De este modo se indica al sistema operativo qué dimensiones tiene el control.
El mensaje WM_MEASUREITEM se envía a la ventana propietaria del combo box antes de enviar el mensaje WM_INITDIALOG o WM_CREATE, de modo que en ese momento Windows aún no ha determinado la altura y anchura de la fuente usada en el control.
Si se procesa este mensaje se debe retornar el valor TRUE.
switch(msg) /* manipulador del mensaje */ { case WM_CREATE: ... case WM_MEASUREITEM: lpmis = (LPMEASUREITEMSTRUCT) lParam; lpmis->itemHeight = 40; return TRUE; ...
Combo box owner-draw de altura variable
En este caso, la ventana propietaria del control recibirá el mensaje WM_MEASUREITEM cada vez que se inserte un nuevo ítem en el control combo box. Esto nos permitirá ajustar la altura de cada ítem con valores diferentes.
El proceso del mensaje es idéntico que con el estilo CBS_OWNERDRAWFIXED. La diferencia es que este mensaje se enviará para cada ítem, y siempre después del mensaje WM_INITDIALOG o WM_CREATE.
Dibujar cada ítem
Tanto en un caso como en el otro, Windows enviará un mensaje cada vez que se inserte un nuevo ítem, cuando el estado de un ítem cambie o cuando un ítem deba ser mostrado.
Esto se hace mediante un mensaje WM_DRAWITEM. En el parámetro wParam recibiremos el identificador del control del que procede el mensaje, o cero si es un menú. En el parámetro lParam recibiremos un puntero a una estructura DRAWITEMSTRUCT, que contiene toda la información relativa al ítem que hay que mostrar.
Si se procesa este mensaje hay que retornar el valor TRUE.
Procesar este mensaje puede ser un proceso bastante complejo, ya que el estado de un ítem puede tomar varios valores diferentes, y seguramente, cuando decidimos crear un control owner-draw es porque queremos hacer algo especial.
La estructura DRAWITEMSTRUCT tiene esta forma:
typedef struct tagDRAWITEMSTRUCT { // dis UINT CtlType; UINT CtlID; UINT itemID; UINT itemAction; UINT itemState; HWND hwndItem; HDC hDC; RECT rcItem; DWORD itemData; } DRAWITEMSTRUCT;
En nuestro caso, CtlType tendrá el valor ODT_COMBOBOX, pero tengamos en cuenta que habrá que discriminar este miembro si tenemos controles owner-draw de distintos tipos.
CtlID contiene el identificador del control, igual que el parámetro wParam.
itemID contiene el índice del ítem . Si el combo box está vacío, el valor será -1.
itemAction puede tener tres valores diferentes, que en ocasiones requerirán un tratamiento distinto por parte de nuestro programa:
- ODA_DRAWENTIRE indica que el ítem debe ser dibujado por entero.
- ODA_FOCUS indica que el control ha perdido o recuperado el foco. Para saber si se trata de uno u otro caso se debe comprobar el miembro itemState.
- ODA_SELECT indica que el estado de selección del ítem ha cambiado. Para saber si el ítem está ahora seleccionado o no también se debe comprobar el miembro itemState.
itemState indica el estado del ítem. El valor puede ser uno o una combinación de los siguientes:
- ODS_COMBOBOXEDIT se está dibujando el campo de selección (control edit) del combo box owner drawn.
- ODS_DEFAULT se trata del ítem por defecto.
- ODS_DISABLED el ítem está deshabilitado.
- ODS_FOCUS el ítem tiene el foco.
- ODS_SELECTED el ítem está seleccionado.
hwndItem contiene el manipulador de ventana del control.
hDC contiene el manipulador de contexto de dispositivo del control. Este valor nos será muy útil, ya que el proceso de este mensaje será en encargado de dibujar el ítem.
rcItem contiene un rectángulo que define el contorno del ítem que estamos dibujando. Además este rectángulo define una región de recorte, de modo que no podremos dibujar nada fuera de él.
itemData contiene el valor del 32 bits asociado al ítem.
Con esto tenemos toda la información necesaria para dibujar cada ítem, y nuestro programa será el responsable de diferenciar los distintos estados de cada uno.
case WM_DRAWITEM: lpdis = (LPDRAWITEMSTRUCT) lParam; if(lpdis->itemID == -1) { /* Se trata de un menú, no hacer nada */ break; } switch (lpdis->itemAction) { case ODA_SELECT: case ODA_DRAWENTIRE: case ODA_FOCUS: /* Borrar el contenido previo */ FillRect(lpdis->hDC, &lpdis->rcItem, (HBRUSH)(COLOR_WINDOW+1)); /* Obtener datos de las medidas de la fuente */ GetTextMetrics(lpdis->hDC, &tm); /* Calcular la coordenada y para escribir el texto de ítem */ y = (lpdis->rcItem.bottom + lpdis->rcItem.top - tm.tmHeight) / 2; /* Cada tipo de comida se muestra en un color diferente */ if(comida[lpdis->itemData].tipo == 'p') SetTextColor(lpdis->hDC, RGB(0,128,0)); else if(comida[lpdis->itemData].tipo == 'c') SetTextColor(lpdis->hDC, RGB(0,0,255)); else if(comida[lpdis->itemData].tipo == 'b') SetTextColor(lpdis->hDC, RGB(255,0,0)); /* Mostrar el icono */ icono = LoadIcon(hInstance, MAKEINTRESOURCE(Icono+lpdis->itemData)); DrawIcon(lpdis->hDC, 4, lpdis->rcItem.top+2, icono); DeleteObject(icono); /* Mostrar el texto */ TextOut(lpdis->hDC, 42, y, comida[lpdis->itemData].nombre, strlen(comida[lpdis->itemData].nombre)); /* Si el ítem está seleccionado, trazar un rectángulo negro alrededor */ if (lpdis->itemState & ODS_SELECTED) { SetTextColor(lpdis->hDC, RGB(0,0,0)); DrawFocusRect(lpdis->hDC, &lpdis->rcItem); } } break; ...
Este ejemplo usa la lista de países de ejemplos anteriores, hemos hecho que los países de más de 92391 km2 se muestren en color verde, y el resto en azul.
Por supuesto, esta es una aplicación muy sencilla de un combo box owner-draw. Es posible personalizar tanto como queramos estos controles, mostrando mapas de bits o cualquier gráfico que queramos.
Otros mensajes para combo box con estilos owner-draw
Disponemos de otros mensajes destinados a controles owner-draw.
El mensaje CB_GETITEMHEIGHT se puede usar para obtener la altura de los ítems en un combo box owner-draw. Si el control tiene el estilo CBS_OWNERDRAWFIXED tanto el parámetro lParam como wParam deben ser cero. Si el control tiene el estilo CBS_OWNERDRAWVARIABLE, el parámetro wParam debe contener el índice del ítem cuya altura queramos recuperar.
De forma simétrica, disponemos del mensaje CB_SETITEMHEIGHT para ajustar la altura de los ítems. Si se trata de un control con el estilo CBS_OWNERDRAWFIXED debe indicarse cero para el parámetro wParam, y la altura se especifica en el parámetro lParam, para lo que será necesario usar la macro MAKELPARAM:
SendMessage(hctrl, CB_SETITEMHEIGHT, 0, MAKELPARAM(23, 0));
Si se trata de un control con el estilo CBS_OWNERDRAWVARIABLE procederemos del mismo modo, pero indicando en el parámetro wParam el índice del ítem cuya altura queremos modificar.
El mensaje WM_DELETEITEM
Cuando se elimina un ítem de un combo box cuyo dato de ítem no sea nulo, en Windows 95; o para ítems pertenecientes a controles owner draw, en el caso de Windows NT, el sistema envía un mensaje WM_DELETEITEM al procedimiento de ventana de la ventana propietaria del control. Concretamente, esto ocurre cuando se usan los mensajes CB_DELETESTRING o CB_RESETCONTENT o cuando el propio control es destruído.
Esto nos da una oportunidad de tomar ciertas decisiones o realizar ciertas tareas cuando algunos ítems concretos son eliminados.
En el parámetro wParam recibiremos el identificador del control en el que se ha eliminado el ítem. En el parámetro lParam recibiremos un puntero a una estructura DELETEITEMSTRUCT. Esta estructura está definida como:
typedef struct tagDELETEITEMSTRUCT { // ditms UINT CtlType; UINT CtlID; UINT itemID; HWND hwndItem; UINT itemData; } DELETEITEMSTRUCT;
CtlType contiene el valor ODT_COMBOBOX.
CtlId contiene el valor del identificador del control.
itemID el valor del índice del ítem eliminado.
hwndItem el manipulador de ventana del control.
itemData el dato del ítem asignado al ítem eliminado.
Dimensiones de la lista desplegable
Ya sólo quedan por comentar dos mensajes más relacionados con los combo boxes.
Los dos están relacionados con el tamaño de la lista desplegable, uno de ellos nos premite obtener el rectángulo correspondiente a esa lista, CB_GETDROPPEDCONTROLRECT, el parámetro wParam no se usa, y debe ser cero, el parámetro lParam se usa para pasar un puntero a una estructura RECT en la que se nos devolverán las coordenadas que definen ese rectángulo.
El otro mensaje es CB_SETDROPPEDWIDTH, que nos permite modificar la anchura de la lista. La nueva anchura se especifica mediante el parámetro wParam, el parámetro lParam no se usa, y debe ser cero.
Definición del orden
Por último, cuando un control combo box tiene el estilo CBS_SORT, el procedimiento de ventana de la ventana propietaria del control recibe uno o varios mensajes WM_COMPAREITEM para determinar la posición de cada nuevo ítem insertado en el control.
Esto nos permite definir nuestro propio orden para los ítems en el control, en lugar de usar el orden alfabético por defecto.
El mensaje se puede recibir varias veces para cada ítem insertado, ya que generalmente no será suficiente una comparación para determinar el orden.
En el parámetro wParam recibiremos el identificador del control, y en lParam un puntero a una estructura COMPAREITEMSTRUCT, con todos los datos necesarios para determinar el orden entre dos ítems del combo box. Esta estructura tiene esta definición:
typedef struct tagCOMPAREITEMSTRUCT { // cis UINT CtlType; UINT CtlID; HWND hwndItem; UINT itemID1; DWORD itemData1; UINT itemID2; DWORD itemData2; } COMPAREITEMSTRUCT;
CtlType contendrá el valor ODT_COMBOBOX.
CtlID el valor del identificador del control.
hwndItem el manipulador de ventana del control.
itemID1 el índice del primer ítem a comparar.
itemData1 el valor del dato del ítem del primer ítem a comparar.
itemID2 el índice del segundo ítem a comparar.
itemData2 el valor del dato del ítem del segundo ítem a comarar.
El valor de retorno debe ser -1, 0 ó 1, dependiendo de si el primer ítem precede al segundo en el orden establecido, si son iguales o si el segundo precede al primero, respectivamente.
Por ejemplo, si para nuestra aplicación establecemos que el orden depende del valor del dato del ítem, de menor a mayor, devolveremos -1 si itemData1 es menor que itemData2, 0 si son iguales y 1 si el valor de itemData1 es mayor que itemData2.
O como en el ejemplo 74, hemos definido un orden para mostrar las comidas según su tipo, primero las comidas, después los postres y al final las bebidas. Dentro de cada tipo se aplica el orden alfabético:
case WM_COMPAREITEM: lpcis = (LPCOMPAREITEMSTRUCT) lParam; /* Establecer un orden: 1) Por tipos, primero comidas, después postres y último bebidas 2) Dentro de cada tipo, usar el orden alfabético */ if(comida[lpcis->itemData1].tipo == comida[lpcis->itemData2].tipo) return strcmp(comida[lpcis->itemData1].nombre, comida[lpcis->itemData2].nombre); else if(comida[lpcis->itemData1].tipo == 'c') return -1; else if(comida[lpcis->itemData1].tipo == 'b') return 1; else if(comida[lpcis->itemData2].tipo == 'c') return 1; else return -1; break;
Ejemplo 74
Nombre | Fichero | Fecha | Tamaño | Contador | Descarga |
---|---|---|---|---|---|
Ejemplo 74 | win074.zip | 2007-03-15 | 12642 bytes | 663 |