Tutorial WinAPI C++ 3.8 (Creación del ObjetoTreeView)
Categorías : Windows, Programación, C y C++.

En este tutorial veremos el funcionamiento del control TREEVIEW de windows. El TreeView es un control que nos permite visualizar una serie de datos en forma de árbol. El TreeView suele utilizarse para mostrar listas de directorios o listas de bases de datos.
Vamos a seguir como en los últimos controles creando un nuevo objeto al que llamaremos ObjetoListTree que heredara de ObjetoControlEstandar (La clase ObjetoControlEstandar se describe en el tutorial 3.3 Creación del ObjetoEditBox).
Veamos su declaración :
// Clase que hereda ObjetoControlEstandar y se centra en las funciones del TreeViewclass ObjetoTreeView : public ObjetoControlEstandar {public : //////// Miembros publicos// -ConstructorObjetoTreeView(void);// -Destructor~ObjetoTreeView(void);// -Función para crear el TreeVuewvoid CrearTreeView( HWND hWndParent, const UINT nEstilos, const UINT nID,const int cX, const int cY,const int cAncho, const int cAlto );// -Función para agregar nodosHTREEITEM AgregarNodo(HTREEITEM nPadre, TCHAR *nTexto, const UINT nIDIcono);// -Función para borrar un nodo y sus hijosvoid BorrarNodo(HTREEITEM bNodo);// -Función para borrar todos los nodosvoid BorrarTodo(void);// -Función que retorna el total de nodos del treeviewUINT TotalNodos(void);// -Función para obtener el primer nodo rootHTREEITEM NodoRoot(void);// -Función para obtener el nodo marcadoHTREEITEM NodoMarcado(void);// -Función para obtener el nodo padre del nodo especificadoHTREEITEM NodoPadre(HTREEITEM pNodo);// -Función para obtener el primer nodo hijo del nodo especificadoHTREEITEM NodoHijo(HTREEITEM pNodo);// -Función para obtener el anterior nodo del nodo especificadoHTREEITEM NodoAnterior(HTREEITEM pNodo);// -Función para obtener el siguiente nodo del nodo especificadoHTREEITEM NodoSiguiente(HTREEITEM pNodo);// -Función para obtener el texto de un nodovoid ObtenerTexto(HTREEITEM pNodo, TCHAR *nTexto, UINT nTamTexto);// -Función para asignar el texto de un nodovoid AsignarTexto(HTREEITEM pNodo, TCHAR *nTexto);// -Función para expandir / contraer nodosvoid ExpandirNodo(HTREEITEM pNodo, const bool nExpandir = true);protected : ///// Miembros protegidosHFONT _Fuente;};
Esta clase tiene un miembro para crear el TreeView, varios miembros para agregar y eliminar nodos, y varios miembros para poder trabajar con los nodos.
Lo que tiene que quedar claro es que un TreeView no es una lista normal como seria el ListView. En un listview tenemos un vector de items a los cuales accedemos especificando una posición, pero en un treeview no hay tal vector, y los nodos están enlazados entre ellos con punteros que apuntan a : NodoPadre, NodoHijo, NodoAnterior y NodoSiguiente.
Si por ejemplo queremos recorrer todos los nodos, vamos a tener que hacer una búsqueda recursiva que empiece por NodoRoot, y que salte al NodoSiguiente siempre que el nodo actual no tenga hijos. En caso de tener hijos, debemos realizar otra búsqueda recursiva en el hijo. Podemos ver un ejemplo de busqueda recursiva por los nodos en el tutorial 3.11 Terminando Ensamblador
Visto por encima el funcionamiento del TreeView pasemos a ver la función CrearTreeView :
// Función para crear el TreeViewvoid ObjetoTreeView::CrearTreeView( HWND hWndParent, const UINT nEstilos, const UINT nID,const int cX, const int cY, const int cAncho, const int cAlto ) {// Si el control ya se ha creado, salimos.if (_hWnd != NULL) return;// Iniciamos los CommonControlsObjetoIniciarCommonControls::Iniciar();// Creamos el control TREEVIEW_hWnd = CreateWindowEx( NULL, WC_TREEVIEW, NULL, nEstilos, cX, cY, cAncho, cAlto, hWndParent,reinterpret_cast<HMENU>(IntToPtr(nID)), GetModuleHandle(NULL), NULL );// Enlazamos el TreeView con esta clase_ConectarControl();// Creamos la fuente para el TreeView_Fuente = CreateFont( 13, 0, 0, 0, FW_NORMAL, false, false, false, DEFAULT_CHARSET,OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, TEXT("Tahoma") );// Asignamos la fuente creada al TreeViewSendMessage(_hWnd, WM_SETFONT, (WPARAM)_Fuente , 0);}
Lo primero que se hace es llamar a la función ObjetoIniciarCommonControls::Iniciar. Esta función llama a la API InitCommonControlsEx para iniciar los controles estándar de windows para nuestra aplicación. Antiguamente en VC6 esto era necesario si queríamos trabajar con controles estándar, pero ahora mismo a decir verdad ya no se si es necesario, porque en mi maquina me funcionan igual los ejemplos tanto si llamo a InitCommonControlsEx como si no… y en la MSDN no dice nada.. Por lo que prefiero mantener este código por si las moscas.
Lo segundo que se hace es llamar a la API CreateWindowEx con el segundo parámetro "WC_TREEVIEW" (que es una macro para identificar la clase). Esto hace que el control creado se convierta en un TREEVIEW.
Lo siguiente es llamar a la función _ConectarControl, que re-emplazara el WindowProcedure del TreeView por el nuestro.
Y por último creamos una fuente con la API CreateFont para el listview, y le mandamos el mensaje WM_SETFONT al treeview con la API SendMessage, cosa que le dirá al treeview que debe usar esa fuente.
Ahora veamos la función AgregarNodo :
// -Función para agregar nodosHTREEITEM ObjetoTreeView::AgregarNodo(HTREEITEM nPadre, TCHAR *nTexto, const UINT nIDIcono) {TVINSERTSTRUCT ITS;TVITEM Item;Item.pszText = nTexto;Item.cchTextMax = static_cast<int>(wcslen(nTexto));Item.mask = TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_CHILDREN;Item.state = TVIS_SELECTED;Item.stateMask = TVIS_SELECTED;Item.iImage = nIDIcono;Item.iSelectedImage = nIDIcono;Item.cChildren = 0;ITS.hInsertAfter = NULL;ITS.item = Item;ITS.hParent = nPadre;HTREEITEM Ret = TreeView_InsertItem(_hWnd, &ITS);// IMPORTANTE Decirle al padre que tiene hijos...Item.mask = TVIF_HANDLE | TVIF_CHILDREN;Item.cChildren = 1;Item.hItem = nPadre;TreeView_SetItem(_hWnd, &Item);return Ret;}
Esta función recibe en los parámetros un HTREEITEM que apunta al nodo padre del que queremos crear, un String con el nombre del nuevo nodo y un UINT con la ID del icono dentro del ImageList.
En esencia un HTREEITEM es un HANDLE que nos permite acceder a los datos del nodo, y lo necesitaremos siempre que se quiera acceder a ellos.
Lo primero que se hace dentro de esta función es rellenar la estructura TVITEM para asignarle el texto y el icono. Fijaros que en el miembro mask le especificamos las constantes : TVIF_TEXT para especificar el texto, TVIF_IMAGE para especificar el icono, TVIF_SELECTEDIMAGE para especificar el icono seleccionado, y TVIF_CHILDREN para especificar si este nodo tiene hijos.
Una vez tenemos el TVITEM correctamente rellenado, lo asignamos a la estructura TVINSERTSTRUCT, y se lo pasamos a la macro TreeView_InsertItem.
Por último volvemos a rellenar el TVITEM, pero esta vez para editar el padre de este nodo, y indicarle que tiene hijos. Para ello establecemos el miembro cChildren a 1 y llamamos a la macro TreeView_SetItem.
Visto el tema de creación de nodos, veamos como podemos borrarlos :
// -Función para borrar un nodo y sus hijosvoid ObjetoTreeView::BorrarNodo(HTREEITEM bNodo) {TreeView_DeleteItem(_hWnd, bNodo);};// -Función para borrar todos los nodosvoid ObjetoTreeView::BorrarTodo(void) {TreeView_DeleteAllItems(_hWnd);};
En esencia tenemos la macro TreeView_DeleteItem que nos permite borrar un nodo sabiendo su HTREEITEM, y la macro TreeView_DeleteAllItems que nos permite eliminar todos los nodos.
Ahora veamos las funciones NodoRoot, NodoPadre, NodoHijo, NodoSiguiente, NodoAnterior, y NodoMarcado :
// -Función para obtener el primer nodo rootHTREEITEM ObjetoTreeView::NodoRoot(void) {return TreeView_GetRoot(_hWnd);};// -Función para obtener el nodo padre del nodo especificadoHTREEITEM ObjetoTreeView::NodoPadre(HTREEITEM pNodo) {return TreeView_GetParent(_hWnd, pNodo);};// -Función para obtener el primer nodo hijo del nodo especificadoHTREEITEM ObjetoTreeView::NodoHijo(HTREEITEM pNodo) {return TreeView_GetChild(_hWnd, pNodo);};// -Función para obtener el anterior nodo del nodo especificadoHTREEITEM ObjetoTreeView::NodoAnterior(HTREEITEM pNodo) {return TreeView_GetNextItem(_hWnd, pNodo, TVGN_PREVIOUS);};// -Función para obtener el siguiente nodo del nodo especificadoHTREEITEM ObjetoTreeView::NodoSiguiente(HTREEITEM pNodo) {return TreeView_GetNextItem(_hWnd, pNodo, TVGN_NEXT);};// -Función para obtener el nodo marcadoHTREEITEM ObjetoTreeView::NodoMarcado(void) {return TreeView_GetSelection(_hWnd);};
- La función NodoRoot utiliza la macro TreeView_GetRoot para devolver el nodo que hace de nodo principal dentro del TreeView. (Este nodo es el primer nodo insertado al principio)
- La función NodoPadre utiliza la macro TreeView_GetParent para devolver el padre del nodo especificado.Si devuelve NULL es que el nodo padre es el nodo Root.
- La función NodoHijo utiliza la macro TreeView_GetChild para devolver el primer hijo que tenga este nodo. Si devuelve NULL es que no tiene hijos.
- La función NodoSiguiente utiliza la macro TreeView_GetNextItem especificando la constante TVGN_NEXT, para devolver el siguiente nodo de su jerarquia. Si devuelve NULL es que no hay nodo siguiente.
- La función NodoAnterior utiliza la macro TreeView_GetNextItem especificando la constante TVGN_PREVIOUS, para devolver el anterior nodo de su jerarquía. Si devuelve NULL es que no hay nodo anterior.
- Y por último la función NodoMarcado utiliza la macro TreeView_GetSelection que nos devuelve el nodo que tiene el foco del teclado y está enmarcado. Puede devolver NULL.
Para terminar con esta clase veamos las funciones ObtenerTexto y AsignarTexto :
// -Función para obtener el texto de un nodovoid ObjetoTreeView::ObtenerTexto(HTREEITEM pNodo, TCHAR *nTexto, UINT nTamTexto) {TVITEM Item;Item.pszText = nTexto;Item.cchTextMax = nTamTexto;Item.mask = TVIF_TEXT | TVIF_HANDLE;Item.hItem = pNodo;TreeView_GetItem(_hWnd, &Item);}// -Función para asignar el texto de un nodovoid ObjetoTreeView::AsignarTexto(HTREEITEM pNodo, TCHAR *nTexto) {TVITEM Item;Item.pszText = nTexto;Item.cchTextMax = wcslen(nTexto);Item.mask = TVIF_TEXT | TVIF_HANDLE;Item.hItem = pNodo;TreeView_SetItem(_hWnd, &Item);}
En las dos funciones esencialmente rellenamos la estructura TVITEM para indicar que queremos asignar/obtener el texto, y utilizamos la macros TreeView_GetItem para obtener el texto, y TreeView_SetItem para asignarlo. Si por ejemplo necesitáramos modificar el icono de un nodo, podríamos hacernos las funciones AsignarIcono y ObtenerIcono, en las cuales asignaríamos en el miembro mask las constantes TVIF_HANDLE, TVIF_IMAGE, y TVIF_SELECTEDIMAGE con lo que estaríamos diciéndole que asigne / obtenga el icono.
Ahora construiremos una aplicación sencilla utilizando esta clase, veamos la función Crear de MiVentana :
void MiVentana::Crear(void) {// Creamos un Imagelist de 16 * 16, para 3 iconosImageList = ImageList_Create(16, 16, ILC_COLORDDB | ILC_MASK, 3, 3);// Creamos los iconos en memoria y los agregamos al ImageListIcon1 = LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_ICON1));ImageList_AddIcon(ImageList, Icon1);Icon2 = LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_ICON2));ImageList_AddIcon(ImageList, Icon2);Icon3 = LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_ICON3));ImageList_AddIcon(ImageList, Icon3);// Creamos la ventanaCrearVentana(NULL, TEXT("MiVentana"), WS_OVERLAPPEDWINDOW | WS_VISIBLE, TEXT("Ejemplo tutorial 3.8"), 100, 100, 400, 280, NULL );// Creamos el TreeViewTreeView.CrearTreeView( _hWnd, WS_CHILD | WS_VISIBLE | WS_BORDER | TVS_HASBUTTONS | TVS_LINESATROOT | TVS_HASLINES,ID_TREEVIEW, 10, 10, 365, 220 );// Enlazamos el TreeView con el imagelist para iconos normalesTreeView_SetImageList(TreeView.hWnd(), ImageList, TVSIL_NORMAL);// Creamos 20 nodos dentro del TreeViewHTREEITEM Padre = NULL;UINT Icono = 0;for (size_t i = 0; i < 20; i++) {if (Icono > 2) Icono = 0;// Creamos el nodo padrePadre = TreeView.AgregarNodo(NULL, TEXT("Nodo"), Icono++);// Creamos 20 nodos dentro de este nodofor (size_t y = 0; y < 20; y++) {if (Icono > 2) Icono = 0;TreeView.AgregarNodo(Padre, TEXT("SubNodo"), Icono++);}}};
El imagelist para que os hagáis una idea es una lista de iconos cargada en memoria que el treeview utilizara para pintar los iconos de sus nodos. Además los imagelist pueden ser compartidos por varios controles a la vez, con lo que conseguimos no tener que cargar iconos independientemente para cada control, lo que nos ahorra código y memoria.
Para empezar creamos un ImageList con la macro ImageList_Create de 16*16 con capacidad para 3 iconos. En segundo lugar cargamos en memoria los 3 iconos de los recursos con la API LoadIcon, y luego los añadimos al ImageList con la macro ImageList_AddIcon. En tercer lugar llamamos a la funcion CrearVentana para crear la ventana principal, y luego creamos el TreeView. En cuarto lugar asignamos el imagelist al TreeView con la macro TreeView_SetImageList. Y por ultimo creamos 20 nodos, y en cada uno de esos nodos creamos otros 20 nodos.
Hasta aquí tenemos hecho un treeview con varios nodos que mostraran un icono.
Como los demás controles estándar que se han mostrado anteriormente el TreeView tambien envía notificaciones para ciertos eventos, pero en vez de enviarlas por el mensaje WM_COMMAND, las manda por el mensaje WM_NOTIFY. En el tutorial del listview vimos como responder al evento doble click, en este veremos como responder a la notificación NM_CUSTOMDRAW de forma que podamos cambiar los colores de los nodos :
LRESULT MiVentana::Evento_Notificacion(const UINT cID, LPNMHDR Datos) {if (Datos->code == NM_CUSTOMDRAW && cID == ID_TREEVIEW) {// En este punto sabemos que es una notificación del treeview, convertimos la estructura LPNMHDR a LPNMTVCUSTOMDRAWLPNMTVCUSTOMDRAW DatosTree = (LPNMTVCUSTOMDRAW)Datos;// Miramos en que estado de pintado se encuentraswitch(DatosTree->nmcd.dwDrawStage) {// Pre-pintado generalcase CDDS_PREPAINT :// Retornamos CDRF_NOTIFYITEMDRAW para que nos diga cuando se pintara cada nodoreturn CDRF_NOTIFYITEMDRAW;// Pre-pintado de un nodocase CDDS_ITEMPREPAINT :// Si no es el nodo marcadoif ((HTREEITEM)DatosTree->nmcd.dwItemSpec != TreeView.NodoMarcado()) {// Cambiamos el color del fondo por rojoDatosTree->clrTextBk = RGB(255,0,0);}else {// Cambiamos el color del fondo por verdeDatosTree->clrTextBk = RGB(0,100,0);}// Retornamos CDRF_NEWFONT para que actualize nuestros datosreturn CDRF_NEWFONT;}}return 0;}
Una vez sabemos que la notificación es para nuestro TreeView, lo primero que hacemos es convertir la estructura LPNMHDR a LPNMTVCUSTOMDRAW para así poder acceder a los datos del "CustomDraw". Cuando tengamos convertida la estructura miraremos el estado de pintado en el que se encuentra, los estados que nos interesan son CDDS_PREPAINT, y CDDS_ITEMPREPAINT.
En el CDDS_PREPAINT retornaremos CDRF_NOTIFYITEMDRAW para decirle al TreeView que queremos que nos notifique cuando va a pintar cada nodo.
En el CDDS_ITEMPREPAINT miraremos si el nodo esta marcado o no para asignar un color de fondo o otro, y por ultimo retornaremos CDRF_NEWFONT para decirle al treeview que actualice los datos.
Para mas información referente al control TreeView consulta el siguiente enlace de la MSDN : TreeView Control.
Siguiente tutorial : 3.9 Creación del ObjetoZLIB.