NovaMonoFix
Errores PHP
X
Usuario
Password
0 FPS

Tutorial WinAPI C++ 3.8 (Creación del ObjetoTreeView)

06 de Noviembre del 2010 por Josep Antoni Bover, 0 visitas, 0 comentarios, 0 votos
Categorías : Windows, Programación, C y C++.
La web está en segundo plano, animación en pausa.
Cargando animación...
Tutorial WinAPI C++ 3.8 (Creación del ObjetoTreeView)

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 :

Archivo : ObjetoTreeView.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Clase que hereda ObjetoControlEstandar y se centra en las funciones del TreeView
class ObjetoTreeView : public ObjetoControlEstandar {
public : //////// Miembros publicos
// -Constructor
ObjetoTreeView(void);
// -Destructor
~ObjetoTreeView(void);
// -Función para crear el TreeVuew
void 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 nodos
HTREEITEM AgregarNodo(HTREEITEM nPadre, TCHAR *nTexto, const UINT nIDIcono);
// -Función para borrar un nodo y sus hijos
void BorrarNodo(HTREEITEM bNodo);
// -Función para borrar todos los nodos
void BorrarTodo(void);
// -Función que retorna el total de nodos del treeview
UINT TotalNodos(void);
// -Función para obtener el primer nodo root
HTREEITEM NodoRoot(void);
// -Función para obtener el nodo marcado
HTREEITEM NodoMarcado(void);
// -Función para obtener el nodo padre del nodo especificado
HTREEITEM NodoPadre(HTREEITEM pNodo);
// -Función para obtener el primer nodo hijo del nodo especificado
HTREEITEM NodoHijo(HTREEITEM pNodo);
// -Función para obtener el anterior nodo del nodo especificado
HTREEITEM NodoAnterior(HTREEITEM pNodo);
// -Función para obtener el siguiente nodo del nodo especificado
HTREEITEM NodoSiguiente(HTREEITEM pNodo);
// -Función para obtener el texto de un nodo
void ObtenerTexto(HTREEITEM pNodo, TCHAR *nTexto, UINT nTamTexto);
// -Función para asignar el texto de un nodo
void AsignarTexto(HTREEITEM pNodo, TCHAR *nTexto);
// -Función para expandir / contraer nodos
void ExpandirNodo(HTREEITEM pNodo, const bool nExpandir = true);
protected : ///// Miembros protegidos
HFONT _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 :

Archivo : ObjetoTreeView.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Función para crear el TreeView
void 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 CommonControls
ObjetoIniciarCommonControls::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 TreeView
SendMessage(_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 :

Archivo : ObjetoTreeView.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// -Función para agregar nodos
HTREEITEM 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.

Para crear nodos que no tengan padre hay que especificar NULL en el parámetro HTREEITEM nPadre.

Visto el tema de creación de nodos, veamos como podemos borrarlos :

Archivo : ObjetoTreeView.cpp
1
2
3
4
5
6
7
8
9
// -Función para borrar un nodo y sus hijos
void ObjetoTreeView::BorrarNodo(HTREEITEM bNodo) {
TreeView_DeleteItem(_hWnd, bNodo);
};
// -Función para borrar todos los nodos
void 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 :

Archivo : ObjetoTreeView.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// -Función para obtener el primer nodo root
HTREEITEM ObjetoTreeView::NodoRoot(void) {
return TreeView_GetRoot(_hWnd);
};
// -Función para obtener el nodo padre del nodo especificado
HTREEITEM ObjetoTreeView::NodoPadre(HTREEITEM pNodo) {
return TreeView_GetParent(_hWnd, pNodo);
};
// -Función para obtener el primer nodo hijo del nodo especificado
HTREEITEM ObjetoTreeView::NodoHijo(HTREEITEM pNodo) {
return TreeView_GetChild(_hWnd, pNodo);
};
// -Función para obtener el anterior nodo del nodo especificado
HTREEITEM ObjetoTreeView::NodoAnterior(HTREEITEM pNodo) {
return TreeView_GetNextItem(_hWnd, pNodo, TVGN_PREVIOUS);
};
// -Función para obtener el siguiente nodo del nodo especificado
HTREEITEM ObjetoTreeView::NodoSiguiente(HTREEITEM pNodo) {
return TreeView_GetNextItem(_hWnd, pNodo, TVGN_NEXT);
};
// -Función para obtener el nodo marcado
HTREEITEM ObjetoTreeView::NodoMarcado(void) {
return TreeView_GetSelection(_hWnd);
};

Para terminar con esta clase veamos las funciones ObtenerTexto y AsignarTexto :

Archivo : ObjetoTreeView.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// -Función para obtener el texto de un nodo
void 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 nodo
void 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 :

Archivo : 3.8 Tutorial crear ObjetoTreeView.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void MiVentana::Crear(void) {
// Creamos un Imagelist de 16 * 16, para 3 iconos
ImageList = ImageList_Create(16, 16, ILC_COLORDDB | ILC_MASK, 3, 3);
// Creamos los iconos en memoria y los agregamos al ImageList
Icon1 = 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 ventana
CrearVentana(NULL, TEXT("MiVentana"), WS_OVERLAPPEDWINDOW | WS_VISIBLE, TEXT("Ejemplo tutorial 3.8"), 100, 100, 400, 280, NULL );
// Creamos el TreeView
TreeView.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 normales
TreeView_SetImageList(TreeView.hWnd(), ImageList, TVSIL_NORMAL);
// Creamos 20 nodos dentro del TreeView
HTREEITEM Padre = NULL;
UINT Icono = 0;
for (size_t i = 0; i < 20; i++) {
if (Icono > 2) Icono = 0;
// Creamos el nodo padre
Padre = TreeView.AgregarNodo(NULL, TEXT("Nodo"), Icono++);
// Creamos 20 nodos dentro de este nodo
for (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 :

Archivo : 3.8 Tutorial crear ObjetoTreeView.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 LPNMTVCUSTOMDRAW
LPNMTVCUSTOMDRAW DatosTree = (LPNMTVCUSTOMDRAW)Datos;
// Miramos en que estado de pintado se encuentra
switch(DatosTree->nmcd.dwDrawStage) {
// Pre-pintado general
case CDDS_PREPAINT :
// Retornamos CDRF_NOTIFYITEMDRAW para que nos diga cuando se pintara cada nodo
return CDRF_NOTIFYITEMDRAW;
// Pre-pintado de un nodo
case CDDS_ITEMPREPAINT :
// Si no es el nodo marcado
if ((HTREEITEM)DatosTree->nmcd.dwItemSpec != TreeView.NodoMarcado()) {
// Cambiamos el color del fondo por rojo
DatosTree->clrTextBk = RGB(255,0,0);
}
else {
// Cambiamos el color del fondo por verde
DatosTree->clrTextBk = RGB(0,100,0);
}
// Retornamos CDRF_NEWFONT para que actualize nuestros datos
return 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.