Tutorial WinAPI C++ 3.11 (Terminando el Ensamblador)
Categorías : Windows, Programación, C y C++.

Esta es la ultima parte del tercer tutorial, donde juntaremos todos los controles estandar que hemos visto anteriormente y formaremos una aplicacion encargada de hacer una lista de archivos.
Con esa lista de archivos la aplicacion podra comprimirlos todos para luego insertarlos en el ejecutable del Instalador.
Y tambien le indicaremos la ruta predeterminada de la instalación.
Ahora que tenemos todos los controles necesarios, además del ejecutable de la instalación solo nos queda hacer el programa para ensamblar la instalación con los archivos.
La idea es hacer un programa que nos permita seleccionar los archivos que queremos meter dentro del instalador, y nos pida datos, como el directorio por defecto y el archivo que se usara como instalador.
Además le añadiremos posibilidad de cargar y guardar proyectos de instalador.
Por último en vez de utilizar una ventana como siempre utilizaremos un dialogo y veremos en que se diferencian.
Vamos al lio, lo primero será crear un ObjetoDialogo para contener diálogos de windows :
// Clase que hereda PlantillaEventos y se especializa en dialogosclass ObjetoDialogo : public PlantillaEventos<BOOL, 0> {public : //////////////// Miembros publicos// -ConstructorObjetoDialogo(void);// -Destructor~ObjetoDialogo(void);// Función para crear el dialogoHWND CrearDialogo(UINT ID_Dialogo);// Evento que nos dice cuando arranca el dialogovirtual BOOL Evento_IniciarDialogo(HWND hWndNuevoFoco) { return TRUE; };protected : ///////////// Miembros protegidos// -Gestor de mensajes estatico inicialstatic BOOL CALLBACK _GestorMensajes(HWND nWnd, UINT uMsg, WPARAM wP, LPARAM lP);};
Aunque parece prácticamente igual que un ObjetoVentana hay que destacar que el _GestorMensajes retorna BOOL mientras que el el ObjetoVentana retorna LRESULT. Esto no es porque ami me guste mas… y a decir verdad no se la razón de ello, simplemente si vamos a usar un dialogo necesitamos utilizar WindowProcedures que retornen BOOL. Y aquí está la razón de porque utilizamos la clase plantilla PlantillaEventos a la hora de basar todas nuestras ventanas / diálogos / controles.
Por último la función CrearDialogo solo recibe un parámetro, que es la ID que tiene el dialogo en los recursos. Para especificar los estilos del dialogo hay que hacerlo mediante el editor de recursos.
Veamos la declaración de un dialogo pequeño que necesitaremos para poner nombre al nuevo proyecto DialogoNuevoProyecto :
// Clase que hereda ObjetoControlEstandar y se centra en las funciones del listboxclass DialogoNuevoProyecto : protected ObjetoDialogo {public : //////////////////// Miembros publicos// -ConstructorDialogoNuevoProyecto(void);// -Destructor~DialogoNuevoProyecto(void);// -Función para crear el dialogo para introducir el nombre del nuevo proyecto// Devuelve el nombre del proyecto o NULL si se ha cancelado.const TCHAR *Mostrar(HWND hWndParent);// -Re-emplazamos el evento cerrar y añadimos la API EndDialogBOOL Evento_Cerrar(void) {EndDialog(_hWnd, 0);return ObjetoDialogo::Evento_Cerrar();};// -Función que se llama al crear el dialogoBOOL Evento_IniciarDialogo(HWND hWndNuevoFoco);// -Re-emplazamos el evento commando para obtener que button se ha pulsadoBOOL Evento_Comando(const UINT nID, const UINT nNotificacion, HWND hWndControl);protected :ObjetoEditBox Edit_Nombre;ObjetoButton Boton_Ok;ObjetoButton Boton_Cancelar;TCHAR *_NombreProyecto;};
En esencia es muy parecido a una ventana, pero con la diferencia de que todos los eventos devuelven BOOL, y que en el Evento_Cerrar para cerrar el dialogo utilizamos la API EndDialog en vez de la API DestroyWindow.
Veamos la función Mostrar :
// -Función para crear el dialogo MODAL para introducir el nombre del nuevo proyectoconst TCHAR *DialogoNuevoProyecto::Mostrar(HWND hWndParent) {if (_NombreProyecto != NULL) delete [] _NombreProyecto;_NombreProyecto = NULL;DialogBoxParam(GetModuleHandle(NULL), MAKEINTRESOURCE(IDD_NUEVO_PROYECTO), hWndParent, (DLGPROC)_GestorMensajes, (LPARAM)this);return _NombreProyecto;}
Para crear un dialogo y enlazarlo a nuestro ObjetoDialogo utilizamos la API DialogBoxParam pasándole la ID del dialogo, y el _GestorMensajes que debe usar. El _GestorMensajes que se ha usado es el de ObjetoDialogo::_GestorMensajes, y este cuando recibe el mensaje WM_INITDIALOG enlaza la clase con el dialogo utilizando la API SetWindowLongPtr con la constante GWLP_USERDATA en el que le especificamos un puntero a nuestro ObjetoDialogo.
Una vez hecho esto el gestor de mensajes estático puede pasar los mensajes a las funciones virtuales de los eventos.
Ahora veamos la declaración del dialogo principal de la aplicación DialogoEnsamblador :
// Objeto final : ObjetoHWND -> PlantillaEventos -> ObjetoDialogo -> DialogoEnsambladorclass DialogoEnsamblador : public ObjetoDialogo {public : //////////////////// Miembros publicos// ConstructorDialogoEnsamblador(void);// Función que recibe el mensjae WM_CLOSEBOOL Evento_Cerrar(void);// Función que recibe cuando se pulsa un Button de windowsBOOL Evento_Comando(const UINT nID, const UINT nNotificacion, HWND hWndControl);// Función que crea el dialogo para el ensambladorvoid Crear(void);// Función para mostrar un dialogo nuevo poryectovoid NuevoProyecto(void);// Función para mostrar un dialogo abrir proyectovoid CargarProyecto(void);// Función para mostrar un dialogo guardar proyectovoid GuardarProyecto(void);// Función para mostrar un dialogo de seleccion de archivosvoid AgregarArchivos(void);// Función para mostrar un dialogo de seleccion de directoriosvoid AgregarDirectorio(void);// Función que borra los archivos seleccionados de la listavoid BorrarSeleccionado(void);// Función que muestra un dialogo para buscar el Instalar.exevoid BuscarEXE(void);// Función que crea el instalar.exe con los archivos de la listavoid CrearInstalador(void);protected : ///////////////// Miembros protegidos// Función que comprueba si la ruta del path relativa tiene caracteres invalidos// Los caracteres / : * ? " < > | no son válidos para crear archivos y directoriosBOOL _PathValido(const TCHAR *nText);// Función para activar / desactivar los controles del dialogovoid _ActivarControles(const bool nActivar);// Función que nos dice si el path esta ya en la listaBOOL _BuscarPathAbsoluto(const TCHAR *nPath);// Función para agregar un directorio de forma recursivaUINT _AgregarArchivosRecursivo(const TCHAR *nPath, const UINT TamPathInicial = 0);// Función que recorre todos los nodos para construir la jerarquia de directoriosUINT _ArbolRecursivo( ObjetoArchivo &InstalarExe, HTREEITEM nNodo = NULL,TCHAR *PathActual = TEXT(""), const bool Root = true );// Función para mostrar el ultimo error con FormatMessage añadiendo un texto personalizadovoid _MostrarUltimoError(const TCHAR *Mensaje = NULL);// Función para imprimir texto en la ventana Resultadosvoid _ImprimirDebug(const TCHAR *Txt, ...);// BotonesObjetoButton Boton_NuevoProyecto;ObjetoButton Boton_AbrirProyecto;ObjetoButton Boton_GuardarProyecto;ObjetoButton Boton_AgregarArchivos;ObjetoButton Boton_AgregarDirectorios;ObjetoButton Boton_BorrarSeleccionado;ObjetoButton Boton_CrearInstalador;ObjetoButton Boton_Buscar;// Barra de progresoObjetoProgressBar Barra_Total;// Lista de archivosObjetoListView Lista;// Arbol de directorios a crearObjetoTreeView Arbol;// EditBox que indican los pathsObjetoEditBox Path_Destino;ObjetoEditBox Path_Instalar;// ComboBox que indica el path por defectoObjetoComboBox Path_Defecto;ProyectoIPRJ IPRJ;};
La mayoría de funciones de este objeto ya se han tratado en los tutoriales anteriores, lo que mas nos interesa ver de este objeto son las siguientes funciones : CrearInstalador, _AgregarArchivosRecursivo, y _ArbolRecursivo.
Empezaremos por _AgregarArchivosRecursivo, esta función tiene como objetivo agregar todos los archivos que se encuentren en el directorio especificado al ObjetoListView :
// Función para agregar un directorio de forma recursivaUINT DialogoEnsamblador::_AgregarArchivosRecursivo(const TCHAR *nPath, const UINT TamPathInicial) {WIN32_FIND_DATA FindInfoPoint;HANDLE hFind = NULL;bool Valido = false;TCHAR PathFinal[MAX_PATH +1];TCHAR PathTmp[MAX_PATH +1];TCHAR PathRelativoTmp[MAX_PATH +1];UINT TamInicial = 0;UINT Tam = wcslen(nPath);wcscpy_s(PathFinal, MAX_PATH +1, nPath);// Agrego una antibarra al final, si no la llevaif (PathFinal[Tam -1] != TEXT('\\')) {Tam ++;wcscat_s(PathFinal, TEXT("\\"));}wcscat_s(PathFinal, TEXT("*.*"));// Especifico el tamaño inicial del pathif (TamPathInicial == 0) TamInicial = Tam;else TamInicial = TamPathInicial;// Busco archivos recursivamentehFind = FindFirstFile(PathFinal, &FindInfoPoint);if (hFind == INVALID_HANDLE_VALUE) return 0;do {wcscpy_s(PathTmp, MAX_PATH +1, PathFinal);PathTmp[Tam] = 0;Valido = true;// Comprobamos la validez del archivo (basicamente por si es el directorio "." o el directorio "..")if (FindInfoPoint.cFileName[0] == TEXT('.')) {if (FindInfoPoint.cFileName[1] == 0) Valido = false;else if (FindInfoPoint.cFileName[1] == TEXT('.') && FindInfoPoint.cFileName[2] == 0) Valido = false;}if (Valido == true) {wcscat_s(PathTmp, MAX_PATH +1, FindInfoPoint.cFileName);if (FindInfoPoint.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {// Si es un directorio realizo una nueva busqueda recursiva dentro de el_AgregarArchivosRecursivo(PathTmp, TamInicial);}else {// Si es un archivo lo añado a la lista, y analizo sus directorioswcscpy_s(PathRelativoTmp, MAX_PATH +1, TEXT("\\"));wcscat_s(PathRelativoTmp, MAX_PATH +1, &PathTmp[TamInicial]);if (_BuscarPathAbsoluto(PathTmp) == FALSE) {IPRJ.AnalizarRuta(PathRelativoTmp);Lista.AgregarItem(0, PathRelativoTmp, PathTmp);}}}} while (FindNextFile(hFind, &FindInfoPoint) != 0);return 0;}
Lo primero que hace esta función es construir un path que al final tenga “*.*” para pasárselo a la API FindFirstFile. A partir de que le pasamos los parámetros de búsqueda a la API FindFirstFile, debemos ir llamando a la API FindNextFile hasta que retorne 0.
Cada vez que llamamos a FindFirstFile o FindNextFile el miembro de la estructura WIN32_FIND_DATA cFileName contendrá el nombre de un archivo, lo que hacemos es mirar si es un archivo valido, ya que nos puede devolver “.” Y “..”, y luego comprobamos si es un directorio o un archivo.
En el caso de ser un directorio hacemos otra llamada a _AgregarArchivosRecursivo para inspeccionarlo. Y en el caso de ser un archivo lo agregamos al ObjetoListView.
Con esto conseguimos enumerar todos los archivos del directorio especificado recursivamente.
Ahora veamos la función CrearInstalador :
// Función que crea el instalar.exe con los archivos de la listavoid DialogoEnsamblador::CrearInstalador(void) {TCHAR PathDestino[MAX_PATH +1];TCHAR Tmp2[512];TCHAR TmpRelativo[512];TCHAR PathInstalarExe[MAX_PATH +1];TCHAR Mensaje[512];// 1 - Comprobación de datos// 1.1 No hay archivos para la instalaciónif (Lista.TotalItems() == 0) {MessageBox(_hWnd, TEXT("No se han añadido archivos al instalador"), TEXT("Error!"), MB_OK);return;}// 1.2 La ruta destino de la instalación no es validaPath_Destino.ObtenerTexto(PathDestino, MAX_PATH + 1);if (_PathValido(PathDestino) == FALSE) {MessageBox( _hWnd,TEXT("La ruta especificada no es válida. \nLa ruta no puede tener los siguientes caracteres / : * ? \" < > |"),TEXT("Error"), MB_OK );Path_Destino.AsignarFoco();return;}// 1.3 El archivo Instalar.exe no existePath_Instalar.ObtenerTexto(PathInstalarExe, MAX_PATH + 1);HANDLE HInstalar = CreateFile(PathInstalarExe, FILE_READ_DATA, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);DWORD TamInstalar = GetFileSize(HInstalar, NULL);if (HInstalar == INVALID_HANDLE_VALUE) {// Si estas ejecutando el ejemplo de www.devildrey33.es y te sale este error, compila el ejemplo 3.10 Terminando Instaladorswprintf_s( Tmp2, 512, TEXT("No se encuentra el archivo : %s \nSin este archivo no se puede crear el instalador."),PathInstalarExe);MessageBox(_hWnd, Tmp2, TEXT("Error!"), MB_OK);Path_Instalar.AsignarFoco();return;}CloseHandle(HInstalar);HInstalar = NULL;// 2 - Creación del instalador// 2.1 Abrimos el archivo destinoObjetoArchivo InstalarExe;ObjetoArchivo ArchivoL; // NOTA hay que hacer que se valide el nombre del proyecto o podria incluir caracteres no validosswprintf_s(Tmp2, 512, TEXT("..\\Instalar %s.exe"), IPRJ.Nombre());if (InstalarExe.AbrirArchivoEscritura(Tmp2, true) == FALSE) {swprintf_s(Mensaje, 512, TEXT("Error creando el archivo %s."), Tmp2);_MostrarUltimoError(Mensaje);return;}// 2.2 Leemos el contenido del Instalar.exe y lo volcamos al Instalador final.ArchivoL.AbrirArchivoLectura(PathInstalarExe);char *Buffer = new char[TamInstalar];ArchivoL.Leer(Buffer, TamInstalar * sizeof(char));InstalarExe.Guardar(Buffer, TamInstalar * sizeof(char));delete [] Buffer;// 2.3 Guardamos el directorio destinoInstalarExe.GuardarUINT(Path_Defecto.Seleccion());InstalarExe.GuardarString(PathDestino);// 2.4 Parseamos el arbol para crear la estructura de directorios y la guardamosUINT TotalDirectorios = Arbol.TotalNodos() - 1; // Restamos 1 porque el root ya se creara por otro ladoInstalarExe.GuardarUINT(TotalDirectorios); // Guardamos el total de directorios_ArbolRecursivo(InstalarExe);// 2.5 Comprimimos y guardamos los archivosUINT TotalArchivos = Lista.TotalItems();ObjetoZLIB Compresor;ContenedorBinario ArchivoComprimido;InstalarExe.GuardarUINT(TotalArchivos); // Guardamos el total de archivosBarra_Total.AsignarRango(0, TotalArchivos);for (UINT i = 0; i < TotalArchivos; i++) {Lista.ObtenerTexto(i, 1, Tmp2, 512); // Obtenemos la ruta absoluta del archivoLista.ObtenerTexto(i, 0, TmpRelativo, 512); // Obtenemos la ruta relativa del archivoInstalarExe.GuardarString(TmpRelativo); // Guardamos la ruta relativa dentro del Instalar.exeCompresor.Comprimir(Tmp2, ArchivoComprimido); // Comprimimos el archivo en memoriaInstalarExe.GuardarUINT(ArchivoComprimido.Longitud()); // Guardamos el tamaño de los datos comprimidosInstalarExe.Guardar(ArchivoComprimido(), ArchivoComprimido.Longitud()); // Guardamos los datos comprimidos en el Instalar.exeBarra_Total.Valor(i);// esperar eventos}// 2.6 Guardamos el tamaño original del Instalar.exe al final del archivoInstalarExe.GuardarDWORD(TamInstalar);// 2.7 Guardamos la cabecera al final de todo para saber si el instalar.exe esta vacio o lleno.TCHAR Cabecera[32] = TEXT("Instalador 1.0 devildrey33 ");InstalarExe.Guardar(Cabecera, sizeof(TCHAR) * 32);MessageBox( _hWnd, TEXT("El instalador se ha creado correctamente en el directorio '3.11 Tutorial terminando Ensamblador'."),TEXT("Instalador creado"), MB_OK);}
- Lo primero es comprobar que tenemos datos suficientes para crear el Instalador. Necesitamos archivos a aladir, una ruta destino valida para instalar archivos, y el instalar.exe que usaremos.
- Creamos un nuevo archivo.exe, y le volcamos el instalar.exe, a continuación guardamos el numero de directorio predeterminado en formato UINT, y luego el nombre del directorio de la aplicación. Leemos el contenido del ObjetoTreeView (que son los subdirectorios por crear) y los volcamos al archivo.exe. Comprimimos y volcamos los archivos dentro de archivo.exe, y por ultimo añadimos al final un DWORD con el tamaño original del instalar.exe, y una cabecera de 32 TCHAR’s con el texto “Instalador 1.0 devildrey33 “
Tabla que nos muestra un instalador por dentro : | ||||||||||||
|
Y por ultimo veamos la función _ArbolRecursivo, que enumera todos los nodos dentro del TREEVIEW (que representan directorios) y los guarda dentro del Instalar.exe
// Función que recorre todos los nodos para construir la jerarquia de directoriosUINT DialogoEnsamblador::_ArbolRecursivo(ObjetoArchivo &InstalarExe, HTREEITEM nNodo, TCHAR *PathActual, const bool Root) {TCHAR nPathActual[512];TCHAR Tmp[512];HTREEITEM NodoTmp = NULL;HTREEITEM Hijo = NULL;UINT TotalDirectorios = 0;// Si el nNodo es NULL asignamos el nodo ROOTif (nNodo == NULL) NodoTmp = Arbol.NodoRoot();else NodoTmp = nNodo;// Mientras NodoTmp no sea NULLwhile (NodoTmp != NULL) {// Obtenemos el primer hijo de NodoTmp o NULL si no hay ningunoHijo = Arbol.NodoHijo(NodoTmp);// Si no es el nodo ROOTif (Root == false) {Arbol.ObtenerTexto(NodoTmp, Tmp, 512);// Añadimos PathActual al principio del nPathActual, y al final agregamos el nombre del nodo actualswprintf_s(nPathActual, 512, TEXT("%s\\%s"), PathActual, Tmp);_ImprimirDebug(TEXT("%s\n"), nPathActual);// Guardamos la ruta completa del directorioInstalarExe.GuardarString(nPathActual);TotalDirectorios ++;}else {// Si es el nodo ROOT dejamos PathActual tal y como estawcscpy_s(nPathActual, 512, PathActual);}// Si el Hijo no es NULL lo escaneamos recursivamenteif (Hijo != NULL) TotalDirectorios += _ArbolRecursivo(InstalarExe, Hijo, nPathActual, false);// Asignamos como nodo actual el siguiente nodoNodoTmp = Arbol.NodoSiguiente(NodoTmp);}// Devolvemos el total de directorios a crearreturn TotalDirectorios;}
El objetivo de esta función es enumerar todos los nodos partiendo del nodo ROOT, e ir montando el path en memoria para volcarlo al archivo instalar.exe
Hay que pensar que los nodos simplemente son strings, y que tenemos que concaternar todos los padres con sus hijos para obtener el path correctamente.
En esencia escaneamos todos los nodos partiendo del nodo ROOT utilizando la función ObjetoTreeView::NodoSiguiente, si vemos que algún nodo tiene uno o mas hijos con la función ObjetoTreeView::NodoHijo, volvemos a llamar a la función _ArbolRecursivo para añadir sus hijos. Por último agregamos las cadenas concatenadas al Instalar.exe
Y aqui termina la tercera serie de tutoriales, espero que sean de ayuda para aquellos que os iniciais en la programación con el API de windows.
Actualmente creo que los tutoriales en cuanto a ventanas y graficos tienen un nivel bastante aceptable (quizas se revise o extienda algúno pero por lo general abarcan la mayoria de cosas básicas a mi entender), por lo que los proximos tutoriales intentare enfocarlos a otros temas de interes como pueden ser : Creación de archivos dmp (para depurar aplicaciones), Creación y manejo de componentes COM, Interacción con redes/internet (por ejemplo mandar un e-mail mediante Winsock, SMTP y MIME, o hacer un chat cliente/servidor simple), etc...