Aplicación SDI con vista de lista, (CListView).
Por: Demian Panello.
Vistas.
Cuando con el AppWizard crea una aplicación SDI o MDI con soporte para la arquitectura Documento/Vista, (ver capítulo 11 Aplicaciones usando MFC), por defecto asigna una vista cuya clase es CView, pero en el último paso, (step 6), se puede cambiar esta característica seleccionando otra clase de vista, (derivadas de CView), como muestra la figura:

Bien, en este artículo, veremos cómo manipular una vista de tipo lista, (CListView), de manera tal que podamos agregar elementos y seleccionarlos además de cambiar el aspecto de la lista, (agregar / quitar encabezados de columnas y mostrar / ocultar iconos).
Aplicación SDI.
Cree una aplicación MFC de Single Document Interface, (SDI), con soporte para Documento/ Vista, de nombre "ejListView". En el último paso, (Step 6), cambie la clase base de vista por CListView, (como muestra la imagen de arriba).
Una aplicación SDI consta de varias clases como se vió en el capítulo 11 de Aplicaciones usando MFC, la clase de la aplicación, la clase CMainFrame, la clase CDocument y la clase de vista, (CView o CListView en nuestro ejemplo).
Según nuestra aplicación inicialmente debería tener las siguientes clases:
- CAboutDlg (la clase del diálogo Acerca de... si dejó marcada la opción al momento de generar la app.).
- CEjListViewApp (la clase de aplicación).
- CEjListViewView (la clase vista)
- CEjListViewDoc (la clase documento).
- CMainFrame (la clase de marco principal, que será el contenedor de nuestra vista).
Tenga presente que en la clase de vista, en la de aplicación y en la de documento, el nombre de la clase está formado por el nombre que le dimos al proyecto con la letra C delante más el sufijo App, View ó Doc, de acuerdo a la clase.
Si ejecuta lo que el AppWizard generó, no notará ninguna diferencia con respecto a la vista CView. La información que una vista de lista muestra puede obtenerse por ejemplo de una base de datos, de un archivo, de un arreglo, etc.
Nosotros, ahora, implementaremos una lista de cadenas, (CStringList), que cargaremos desde un cuadro de diálogo, (pulsando F5 llamaremos al diálogo para ingresar datos), así que lo primero que haremos será declarar una variable de tipo CStringList como dato miembro de nuestra clase de documento, ya que es esta clase la encargada de "mantener" la información y la vista la de representarla.
Protección de la información.
Pulse con el botón derecho del mouse sobre la clase CEjListViewDoc, seleccione la opción Add member variable, entonces especifique una variable de tipo CStringList de nombre m_lstApe privada.
Note que estamos declarando una variable privada, lo que implica que no podremos acceder a ella desde otra clase, entonces, ¿cómo cargaremos o leeremos esta lista?, bueno, deberemos crear una función pública que nos permita el acceso para obtener elementos de la lista y otra función que nos permita agregar elementos,
y esto es una forma de proteger nuestra lista de accesos indebidos.
Agregue dos funciones a la clase documento, una const CStringList& ObtenerElementos() y otra void AgregarElemento(CString nuevoElemento) y escriba:
|
//Funciones
para acceder a la lista. |
El retorno de la función ObtenerElementos() es una constante a una referencia, (&) de tipo CStringList lo que implica que el objeto retornado sólo podrá leerse.
Dialogo para ingresar elementos.
La idea es cargar la lista de cadenas con apellidos y luego "volcarla" a la vista que también es una lista. Para poder ingresar valores necesitaremos un diálogo con un cuadro de edición donde se pueda ingresar el apellido y los botones para ingresar y cerrar el diálogo. Para esto en Resource View pulse con el botón derecho del mouse sobre la carpeta Dialog y seleccione la opción Insert Dialog, y luego pulse Ctrl+W para activar el Class Wizard, el cual detectará la presencia de un nuevo recurso de diálogo y preguntará si usted creará una nueva clase o le asignmará una existente. Seleccione New Class derivada de CDialog y de nombre CInputDialog y pulse OK.
Pulse dos veces sobre el botón Ok del diálogo que acaba de agregar, generará la función OnIDOK(), comente o borre su contenido, ya que no queremos que esta función esté presente. Y diseñe el diálogo más o menos como muestra la siguiente figura:

El botón cuyo texto es "Ingresar" era el botón Aceptar, (Ok), cuyo ID fué cambiado a IDC_INGRESAR y el cuadro de edición es IDC_EDIT_APELLIDO.
Pulse Ctrl+W y en el Class Wizard seleccione Member Variables, marque el ID del cuadro de edición y pulse Add variable. Agregue una variable de tipo CString m_Apellido y especifique 20 como la cantidad máxima de caracteres.
Ya tenemos el diálogo para el ingreso de elementos a la lista, pero si lo piensa bien, nos encontramos con un problema, y es que la lista pertenece a la clase de documento de nuestra aplicación y además la lista es privada. Bueno, para esto último, ya nos habíamos adelantado y escribimos la función AgregarElemento(), el inconveniente ahora es acceder a la clase CEjListViewDoc, (ese es el nombre de nuestra clase derivada de CDocument).
Hay una forma muy simple de hacerle saber al diálogo la existencia de nuestra clase documento y es la de pasarle un puntero a la misma al constructor de la clase del diálogo. O sea, cuando ocurra la llamada a nuestro diálogo, (pulsación de un ítem del menú que aún no hemos escrito), tendremos que pasarle a la función DoModal() un puntero a la clase CEjListViewDoc.
Nuestro constructor del diálogo recibirá un puntero a la clase documento e inicializará una variable miembro pública de tipo CEjListViewDoc que luego usará para acceder a la función AgregarElemento().
Entonces tendremos que modificar nuestro constructor del diálogo tanto en el archivo de encabezado, InputDialog.h donde está la declaración de la clase como en el archivo de implementación, InputDialog.cpp agregando lo siguiente:
InputDialog.h
class CInputDialog : public CDialog
{
// Construction
public:
CEjListViewDoc* m_pDoc;
CInputDialog(CWnd* pParent = NULL, CEjListViewDoc* pDoc =
NULL);
....
InputDialog.cpp
CInputDialog::CInputDialog(CWnd* pParent
/*=NULL*/, CEjListViewDoc*
pDoc)
:CDialog(CInputDialog::IDD, pParent)
{
//{{AFX_DATA_INIT(CInputDialog)
m_Apellido = _T("");
//}}AFX_DATA_INIT
m_pDoc=pDoc;
}
Marcado en amarillo el código agregado.
Ahora que tenemos la llave de acceso a la clase de documento, podremos escribir el código que al pulsar el botón Ingresar permita agregar un nuevo elemento a la lista. Pulse dos veces sobre el botón Ingresar para generar el mensaje OnIngresar() y escriba:
|
void CInputDialog::OnIngresar() |
Agregando elementos.
Es preciso modificar el menú de nuestra aplicación, así que con el ResourceView vaya a menu y modifique el menú como muestra la imagen, (sin olvidar que debe también agregar entradas en el Acelerator Table para identificar el uso, por ejemplo, de teclas como F2, F5 y F6).

Los ID para los elementos del menú Datos son:
ID_DATOS_NUEVO
ID_DATOS_HEADERS
ID_DATOS_ICONOS

Necesitamos un método que permita agregar elementos en el control lista, (o sea que pase los elementos de nuestro arreglo a la vista), para esto agregue en la clase de vista una función pública llamada MostrarLista(), (el retorno es void). Escriba:
|
void
CEjListViewView::MostrarLista() |
Aquí obtenemos el control lista que se encuentra "incrustado" en nuestra vista por medio de la función GetListCtrl(). Primero borramos todos los elementos almacenados en el control, obtenemos la posición del primer elemento de la lista de apellidos, (CStringList) con GetHeadPosition() y mientras pos <> 0, se obtiene el elemento ubicado en esa posición y se lo coloca en la vista en la primer columna, función InsertItem().
Esta función debe ser llamada al pulsar Nuevo en el menú Datos, así que agregue el mensaje WM_COMMAND para el subitem de menú Nuevo, (a este elemento del menú yo le puse el ID: ID_DATOS_NUEVO) y allí escriba:
|
void
CEjListViewView::OnDatosNuevo() |
En esta función llamamos a nuestro diálogo para el ingreso de datos al cual le pasamos un puntero a nuestro documento, (habíamos modificado el constructor de la clase CInputDialog). Luego que regresa de DoModal(), llamamos a la función MostrarLista() y redibujamos la vista.
Vista con encabezados, vista con iconos.
Los elementos en la vista de lista se pueden mostrar como simples textos, (de hecho son textos ya que son apellidos), pero también podemos asociarles iconos. Para poder realizar esto tenemos que modificar el estilo de lista y esto se hace con la función SetWindowLong().
Primero agregue dos variable privadas de tipo BOOL a la clase de vista llamadas Headers e Iconos y una tercera también privada, que usaremos después, que sea un puntero al objeto CImageList llamada m_pImageList, (escriba en la declaración CImageList* m_pImageList).
En el constructor de la vista inicialice las variables Headers e Iconos con el valor FALSE.
Tendremos una vista normal, (sin encabezados) que sólo muestra los apellidos ingresados, otra vista con encabezados, en realidad un sólo encabezado o columna que será "Apellido" y una tercera vista de los elementos representados por un icono.
Primero nos centraremos en fijar el modo de vista de elementos de la lista en forma vertical, agregue una función a la clase de vista llamada VistaVertical(), pública, retorna void y no recibe parámetros.
|
void
CEjListViewView::VistaVertical() |
La función GetWindowLong() devuelve un entero de 32 bits, y esos bits precisamente representan diversas características del objeto, (características que fueron especificadas en su momento cuando en algún lugar ocurrió la llamada a CreateWindowEx()). Supongamos, si el 5º bit está encendido, (en 1), significa que la lista muestra las barras de desplazamiento, es un ejemplo, no sé que significa el 5º bit y de hecho no hace falta saberlo, porque como muchísimas funciones de la API que devuelven números cuyos bits quieren informar muchas cosas, existen constantes que son el número preciso con el que hay que evaluar para saber si hay tal o cual bit encendido o permiten encender tal bit.
La línea (1) permite obtener el estilo actual por medio de la función GetWindowLong() a la que se le pasan los parámetros HWND que es el handle de ventana cuyos estilos queremos obtener, (en nuestro ejemplo la función GetSafeHwnd() del control lista nos da el handle) y la constante GWL_STYLE para indicarle que queremos los bits de estilo.
El indicador de estilo actual queda almacenado en la variable dwStyle, en la línea (2) eliminamos los indicadores de estilo actual con la constante LVS_TYPEMASK.
Luego de (2) tenemos la variable dwStyle sin los indicadores de estilo podemos ahora con un OR a nivel de bits, ( | ), "encender" el o los bits que deseemos, por eso en la línea (3) lo hacemos con la constante LVS_LIST.
A ver si me explico, la constante LVS_LIST es un número, para el caso no importa cual, lo que si importa es que esa configuración de bits es la esperada para setear el estilo de la lista como vertical, por eso es que "sumamos", (eso es lo que hace el OR a nivel de bits), esa configuración de bits con los que tenemos en la variable dwStyle, para así en (4) la pasamos como parámetro a la función SetWindowLong().
Bien, tenemos nuestra función para cambiar a VistaVertical(), (ya veremos pronto donde hacer la llamada a esta función), pero aprovechemos y escribamos también la función para cambiar al modo de vista con encabezados, (LVS_REPORT). Agregue una función a la clase vista de nombre ColocarEncabezados(), (devuelve void). Escriba:
| void CEjListViewView::ColocarEncabezados() { DWORD dwStyle; //Tomo el estilo actual de lista. dwStyle=GetWindowLong(GetListCtrl().GetSafeHwnd(),GWL_STYLE); //Elimino los indicadores de estilo actual. dwStyle&=~LVS_TYPEMASK; //Cambio al estilo de reporte, (con encabezados). dwStyle |= LVS_REPORT; //Pongo el nuevo estilo en la lista. SetWindowLong(GetListCtrl().GetSafeHwnd(), GWL_STYLE, dwStyle); Headers=TRUE; SetRedraw(TRUE); } |
Esta función es idéntica a la anterior, sólo cambian las líneas marcada, porque ahora nos interesa el estilo LVS_REPORT que es justamente el estilo con encabezados, (columnas) y ponemos la variable Headers en TRUE, (variable que luego nos servirá para saber en que estilo se encuentra la lista).
Cuando se inicia la aplicación se ejecuta el código del método InitialUpdate(), entonces allí escribiremos el código necesario para especificarle cual, (o cuales), serán las columnas para cuando se desee mostrar los encabezados y cual, (o cuales), serán los iconos para cuando se desee mostrar los elementos con iconos.
|
void CEjListViewView::OnInitialUpdate() |
En (1) con la función InsertColumn(), indicamos que nuestra única columna será "Apellidos", (la primera por eso el 0 como primer parámetro), la alineación y el ancho.
Luego llamamos a la función VistaVertica() en (2) porque ese será nuestro modo de presentación de los datos.
En (3) creamos un objeto CImageList(), (recuerde que el puntero a esta clase lo declaramos anteriormente).
Un objeto CImageList() es un arreglo de de imágenes, (aunque nosotros ahora sólo tengamos una), que usaremos para asignarle al control lista de la vista.
En (4) creamos una entrada en el objeto CImageList con la función Create(), indicándole que el tamaño de la imagen a almacenar allí es de 32x32, con TRUE que será un icono, la cantidad de imágenes y el máximo de imágenes. En (5) le agregamos el icono y en (6) le asignamos al control lista este objeto CImageList para que las muestre de tamaño normal.
Nos queda, escribir una función que permita cambiar el estilo del control lista a imágenes, las funciones del menú para la selección de diversas formas de ver y que al pulsar un elemento de la lista muestre el texto en por ejemplo la barra de título.
Agregue una función a la clase vista llamada ConIconos() y escriba:
| void CEjListViewView::ConIconos() { DWORD dwStyle; //Tomo el estilo actual de lista. dwStyle=GetWindowLong(GetListCtrl().GetSafeHwnd(),GWL_STYLE); //Elimino los indicadores de estilo actual. dwStyle&=~LVS_TYPEMASK; //Cambio al estilo con iconos dwStyle |= LVS_ICON; //Pongo el nuevo estilo en la lista. SetWindowLong(GetListCtrl().GetSafeHwnd(), GWL_STYLE, dwStyle); Iconos=TRUE; //Para la próxima vez que se pulse en el menu SetRedraw(TRUE); } |
Otra vez, esta función es idéntica a las anteriores que permiten cambiar estilos, salvo que ahora es el estilo LVS_ICON el que deseo especificar y que ponemos la variable Iconos en TRUE.
Agregue en la clase vista los mensaje Command Update_Command_UI y para ID_DATOS_HEADERS e ID_DATOS_ICONOS y escriba:
| void CEjListViewView::OnDatosHeaders() { //Si Headers tiene FALSE es porque se requiere el encabezado. if (!Headers) ColocarEncabezados(); else //caso contrarario muestro el estilo simple. VistaVertical(); } |
| void CEjListViewView::OnUpdateDatosHeaders(CCmdUI* pCmdUI) { //Marco o desmarco el ítem del menú. pCmdUI->SetCheck (Headers); } |
| void CEjListViewView::OnDatosIconos() { //Si Iconos es FALSE es porque se requiere la vista con iconos. if (!Iconos) ConIconos(); else { Iconos=FALSE; VistaVertical(); } } |
| void CEjListViewView::OnUpdateDatosIconos(CCmdUI* pCmdUI) { pCmdUI->SetCheck (Iconos); } |
Y en OnClick() del CListView:
| void CEjListViewView::OnClick(NMHDR* pNMHDR, LRESULT* pResult) { *pResult = 0; //Cadena para almacenar los elementos seleccionados. CString strSeleccionados="Elementos marcados: "; //El índice inicial de GetNextItem() int nSeleccionados=-1; do { //Busco el siguiente elemento marcado nSeleccionados=GetListCtrl().GetNextItem(nSeleccionados, LVNI_SELECTED); //Si hay algún elemento marcado... if (nSeleccionados != -1) { //Lo agrego a la cadena. strSeleccionados += " "+ GetListCtrl().GetItemText(nSeleccionados, 0); } } while(nSeleccionados!= -1); //Coloco la cadena con todos los elementos marcados en la barra de título del documento. GetDocument()->SetTitle (strSeleccionados); } |
Como se permite que seleccione múltiple elementos se inicia un ciclo do. En ese ciclo se obtienen los índices de los elementos seleccionados con la función GetNextItem() y el parámetro LVNI_SELECTED, que devuelve -1 en caso de no haber más elementos seleccionado, y por cada ítem seleccionado concateno el texto en una variable CString que luego será el título del documento.

Hemos llegado al final de este artículo que sólo pretendía presentar las características más comunes al momento de trabajar con la vista CListView.
Como siempre pueden descargar el código fuente para poder modificarlo y agregarle cosas para experimentar que es lo que aconsejo, porque probando se aprende muchísimo y además hay un link al ejecutable para verlo en funcionamiento antes de construir la aplicación.
Descargar fuente de los ejemplos: ejListView.zip (52 Kb).
Probar ejecutable: ejListView.exe (36 Kb).