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

En este tutorial vamos a empezar a jutar las piezas para formar nuestro instalador. En primer lugar usaremos los controles que hemos creado desde cero para el interface grafico del instalador, y luego le añadiremos el ObjetoZLIB para poder descomprimir datos.
Tambien usaremos una parte de los common dialogs que tocamos antes, en concreto vamos a usar el ObjetoDlgDirectorios que nos servira para seleccionar el directorio destino de la instalacion.
Hay que remarcar que este punto es algo complicado dependiendo de las capacidades deseadas. Para empezar todo ejecutable creado con VC requiere unas dlls mínimas para funcionar, si el sistema operativo no tiene esas dlls, el ejecutable no arrancara.
Esto es un problema, ya que si compilamos el instalador con VisualStudio 2010 este requerirá msvcp100.dll y msvcr100.dll. Esas dlls no vienen por defecto en ningún sistema operativo hasta la fecha (Puede que en instalaciones de windows 7 SP1 ya vengan dichas dlls, aunque aun no he tenido oportunidad de comprobarlo, pero en windows 7 a pelo sin service pack no vienen) por lo que si queremos que el instalador se ejecute, esas dlls deberán estar en el mismo directorio que se vaya a ejecutar el instalador.. Con lo que al final la única solución es distribuir un zip con el instalar.exe y las 2 dlls dentro.
Si deseamos que el Instalar.exe funcione solo sin necesitar runtimes extras, necesitaremos compilar el instalar.exe con algún runtime que sepamos que ya está instalado en la maquina destino. Por ejemplo si queremos hacer un instalador para windows 2000/ME o superior… lo ideal sería hacerlo en VC6.
Por supuesto estoy hablando de casos extremos en los que no deseamos utilizar el instalador de visual studio y nos queremos hacer uno nosotros por lo que sea. Por ejemplo el instalador de BubaTronik que tiene un estilo como el reproductor, se ha hecho con VC6 para mantener compatibilidad con sistemas operativos antiguos.
Dicho esto ya podemos empezar a ver código, lo mejor será empezar por el principio así que echaremos un vistazo al WinMain :
// WinMainint APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow) {// Comprobamos si el usuario es administrador// NO ES ADMINISTRADORif (IsUserAnAdmin() == FALSE) {int TotalArgs = 0;TCHAR **Args = CommandLineToArgvW(GetCommandLine(), &TotalArgs);// Ejecutamos el instalador de forma que pida privilegios de administradorShellExecute(NULL, TEXT("RunAs"), Args[0], NULL, NULL, SW_SHOWNORMAL);LocalFree(Args);}// ES ADMINISTRADORelse {// El programa se ha arrancado con privilegios de administrador,// mostramos la ventana de instalaciónVentanaInstalador Ventana;MSG Mensaje;while (TRUE == GetMessage(&Mensaje, NULL, 0, 0)) {TranslateMessage(&Mensaje);DispatchMessage(&Mensaje);}}return 0;}

Lo primero que hacemos es utilizar la API IsUserAnAdmin para determinar si el programa se esta ejecutando con privilegios de administrador.
- En el caso de no ser administrador utilizaremos las API's CommandLineToArgvW y GetCommandLine para obtener la ruta completa de nuestro ejecutable, con esa ruta ejecutaremos nuestro ejecutable de nuevo de forma que pida privilegios de administrador (utilizando la API ShellExecute especificando en el parametro lpOperation "RunAs"). Al especificar RunAs en el parametro lpOperation le estamos diciendo al Windows que nuestra aplicacion necesita privilegios de administrador.
Luego borraremos la memoria que crea CommandLineToArgvW con la API LocalFree.
- En el caso de ser administrador continuaremos la ejecución normal mostrando la ventana del instalador.
Bueno con esto ya hemos visto como podemos hacer que nuestra aplicación pida privilegios de administrador, ahora pasemos a ver el objeto VentanaInstalador :
// Objeto final : ObjetoHWND -> PlantillaEventos -> ObjetoVentana -> VentanaInstaladorclass VentanaInstalador : public ObjetoVentana {public : ///////////////// Miembros publicos// - ConstructorVentanaInstalador(void);protected : ////////////// Miembros portegidos// - Función que crea la ventana del instaladorvoid Crear(void);// - Función que determina si el instalador está lleno o vacio, y en el caso de estar lleno// lee los valores de la instalación y los asigna a sus correspondientes controles.BOOL LeerDatos(void);// - Función que enlaza con el mensaje WM_CLOSELRESULT Evento_Cerrar(void);// - Función que enlaza con el mensaje WM_PAINTLRESULT Evento_Pintar(HDC hDC, PAINTSTRUCT &PS);// - Función que recibe cuando se hace click en un ObjetoBotonLRESULT Evento_ObjetoBoton_Click(ObjetoBoton *BotonPresionado, const UINT nBoton);// - Función para mostrar al usuario un dialogo para seleccionar un directoriovoid SeleccionarDirectorio(void);// - Función que valida los datos y empieza la instalacionvoid InstalarContenido(void);// - Barra de progresoObjetoBarraProgreso Barra;// - EditBox que indica la ruta destinoObjetoEditBox PathDestino;// - Botón para instalarObjetoBoton Instalar;// - Botón para salirObjetoBoton Salir;// - Botón para buscar el directorioObjetoBoton BuscarDirectorio;// - Archivo que contiene toda la instalacionObjetoArchivo InstalarExe;// - Función que comprueba si la ruta especificada es validaBOOL _PathValido(const TCHAR *nText);// - Función para mostrar el ultimo error con FormatMessage añadiendo un texto personalizadovoid _MostrarUltimoError(const TCHAR *Mensaje);};
No he dejado ningún miembro público de forma que cuando se inicie el constructor se cree directamente la ventana y los controles necesarios.
Las funciones mas interesantes por ver son : LeerDatos, InstalarContenido y _MostrarUltimoError. Las demás funciones mas o menos se han tratado con anterioridad.
Empezemos por la función LeerDatos, esta función se encargara de determinar si hay datos dentro del instalar.exe o es un instalador vacio, y en el caso de haber datos extraerá los primeros que necesitamos.
// - Función que determina si el instalador está lleno o vacio, y en el caso de estar lleno// lee los valores de la instalación y los asigna a sus correspondientes controles.BOOL VentanaInstalador::LeerDatos(void) {LPWSTR Path = GetCommandLine();TCHAR PathInstalarExe[MAX_PATH + 1];// 1 - Parseamos el path por si viene con comillasUINT TamPath = wcslen(Path) + 1;UINT Contador = 0;for (UINT i = 0; i < TamPath; i++) {if (Path[i] != TEXT('"')) {PathInstalarExe[Contador] = Path[i];Contador ++;}}// 2 - Abrimos el instalador para lecturaif (InstalarExe.AbrirArchivoLectura(PathInstalarExe) == FALSE) {_MostrarUltimoError(TEXT("Error abriendo el instalador..."));return FALSE;}// 3 - Leemos los datos del instalar.exeTCHAR Tmp[512];// Nos desplazamos al final del archivo menos ((sizeof(TCHAR) * 32) + sizeof(UINT)// La cabecera son 32 caracteres del tipo TCHAR y ademas le sumamos el tamaño de un UINT.// El UINT será el tamaño del ejecutable limpioUINT TamInstalarExeReal = 0;InstalarExe.Posicion(-static_cast<long>(((sizeof(TCHAR) * 32) + sizeof(UINT))), true);InstalarExe.LeerUINT(TamInstalarExeReal);// - Leemos la cabecera para determinar si el instalador esta lleno o vacioInstalarExe.Leer(Tmp, sizeof(TCHAR) * 32);TCHAR Cabecera[32] = TEXT("Instalador 1.0 devildrey33 ");if (wcscmp(Tmp, Cabecera) != 0) {// Instalador vacioInstalar.Activado(false);TamInstalarExeReal = 0;return FALSE;}// 4 - Nos movemos a la posición donde termina el instalar.exe y empiezan los datosInstalarExe.Posicion(TamInstalarExeReal, false);// 5 - Obtenemos el Path por defecto de la instalaciónUINT NumPathDefecto = 0;InstalarExe.LeerUINT(NumPathDefecto);TCHAR *PathDefecto = NULL;TCHAR PathDefecto2[MAX_PATH];InstalarExe.LeerString(PathDefecto);ObjetoDirectoriosWindows Directorios;TCHAR PathDestinoInicial[MAX_PATH];wcscpy_s(PathDestinoInicial, MAX_PATH, PathDefecto);switch (NumPathDefecto) {case 0 : // C:wcscpy_s(PathDefecto2, 4, TEXT("C:\\"));break;case 1 : // Archivos de programa x86Directorios.ArchivosDeProgramaX86(PathDefecto2);break;case 2 : // Archivos de programa x64// Directorios.ArchivosDeProgramaX64(PathDefecto);break;}// 6 - Asignamos el path por defecto de la instalación al editboxswprintf_s(PathDestinoInicial, TEXT("%s\\%s"), PathDefecto2, PathDefecto);PathDestino.AsignarTexto(PathDestinoInicial);delete [] PathDefecto;return TRUE;}
- Lo primero es parsear el path por si viene con comillas, en ese caso las quitaremos.
- Abrimos nuestro ejecutable de la instalación para lectura, y en caso de fallar mostramos un error.
- Leemos el final del archivo para determinar ver si hay la cabecera de 32 TCHAR TEXT("Instalador 1.0 devildrey33 "). Si la comparación de la cabecera con el texto da positivo, es que el instalador contiene archivos. Si la coprobacion sale negativa saldremos de la función ya que no hay datos por leer.
- Movemos el puntero a la posición donde empiezan los datos.
- Obtenemos el path por defecto de la instalación, para ello leemos un UINT del archivo, y luego un string. El UINT determina el directorio por defecto : “c:\” “c:\Archivos de programa (x86)” y “c:\archivos de programa”. El string contiene el nombre de la aplicación a instalar “Ejemplo 3.5” (Todo esto se decide en el ensamblador)
- Una vez tenemos el directorio lo asignamos al EditBox, y salimos de la función.
Ahora veamos la función InstalarContenido que será la que se ejecutara al pulsar el botón instalar :
void VentanaInstalador::InstalarContenido(void) {// 1 - Comprobamos que el directorio de instalacion sea validoTCHAR InstalarDir[MAX_PATH];TCHAR TmpSubDirectorio[MAX_PATH];TCHAR TmpTxt[512];PathDestino.ObtenerTexto(InstalarDir, MAX_PATH);if (_PathValido(InstalarDir) == FALSE) {MessageBox(_hWnd, TEXT("La ruta especificada no es válida.\n\La ruta no puede tener los siguientes caracteres / : * ? \" < > |"),TEXT("Error"), MB_OK);return;}// 2 - Si el directorio tiene una antibarra al final la quitamosUINT TamTmp = wcslen(InstalarDir);if (InstalarDir[TamTmp] == TEXT('\\')) {InstalarDir[TamTmp] = TEXT('\0');}// 3 - Creo el directorio de la instalaciónBOOL RC = CreateDirectory(InstalarDir, NULL);if (RC == FALSE && GetLastError() != 183) { // Error 183 "Error el directorio ya existe y no puede ser creado"swprintf_s(TmpTxt, 512, TEXT("Error creando el directorio '%s' (%d). Instalacion abortada.\n"), InstalarDir, GetLastError());_MostrarUltimoError(TmpTxt);return;}// 4 - Leemos y creamos los subdirectoriosUINT TotalDirectorios = 0;UINT i = 0;PTCHAR TmpDir;InstalarExe.LeerUINT(TotalDirectorios);for (i = 0; i < TotalDirectorios; i++) {InstalarExe.LeerString(TmpDir);swprintf_s(TmpSubDirectorio, MAX_PATH, TEXT("%s%s"), InstalarDir, TmpDir);MessageBox(_hWnd, TmpSubDirectorio, TEXT("a"), MB_OK);delete [] TmpDir;RC = CreateDirectory(TmpSubDirectorio, NULL);if (RC == FALSE && GetLastError() != 183) { // Error 183 "Error el directorio ya existe y no puede ser creado"swprintf_s( TmpTxt, 512, TEXT("Error creando el directorio : '%s' (%d). Instalacion abortada.\n"),TmpSubDirectorio, GetLastError());_MostrarUltimoError(TmpTxt);return;}}// 5 - Descomprimimos y creamos los archivos finalesUINT TotalArchivos = 0;UINT TamArchivo = 0;MSG msg;char *Datos = NULL;ObjetoZLIB ZLIB;ContenedorBinario ArchivoComprimido;InstalarExe.LeerUINT(TotalArchivos);Barra.Maximo(static_cast<float>(TotalArchivos));for (i = 0; i < TotalArchivos; i++) {InstalarExe.LeerString(TmpDir);swprintf_s(TmpSubDirectorio, MAX_PATH, TEXT("%s%s"), InstalarDir, TmpDir);delete [] TmpDir;InstalarExe.LeerUINT(TamArchivo);Datos = new char[TamArchivo];InstalarExe.Leer(Datos, TamArchivo * sizeof(char));ArchivoComprimido.Borrar();ArchivoComprimido.AgregarParte(Datos, TamArchivo);if (ZLIB.Descomprimir(ArchivoComprimido, TmpSubDirectorio) == FALSE) {// error creando o descomprimiendo archivoswprintf_s(TmpTxt, 512, TEXT("Error creando el archivo : '%s'.\n"), TmpSubDirectorio);MessageBox(_hWnd, TmpTxt, TEXT("Error"), MB_OK);}Barra.Valor(static_cast<float>(i));// esperar eventosif (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {TranslateMessage(&msg);DispatchMessage(&msg);}}MessageBox(NULL, TEXT("Instalación completada!"), TEXT("Instalación completada"), MB_OK);}
- Miramos si el directorio de instalación es válido, ya que no puede contener caracteres que no acepte el sistema de archivos de windows.
- Comprobamos la ruta, si el directorio tiene una antibarra al final la quitamos.
- Intentamos crear el directorio de la instalación, en caso de no poder mostramos el error y salimos de la función.
- Leemos y creamos los subdirectorios necesarios para los archivos. Lo primero que encontraremos en el instalador después de haber utilizado la función LeerDatos será el total de subdirectorios a crear en forma de UINT. Luego habrá que leer tantos strings como el total de subdirectorios, y mientras los leemos los iremos creando. En caso de no poder crear algún subdirectorio mostraremos un error y saldremos de la función.
- Descomprimimos y creamos los archivos finales. Una vez leídos correctamente todos los subdirectorios lo siguiente que nos encontraremos será un UINT con el total de archivos que contiene el instalador. Después tenemos que leer un string que será el nombre del archivo, un UINT que nos dirá el tamaño de los datos comprimidos para el archivo actual, y por ultimo leeremos tantos bytes como diga el UINT del tamaño para leer el archivo, descomprimirlo, y guardarlo en su sitio.
Tabla que nos muestra un instalador por dentro : | ||||||||||||
|
Ya solo nos falta ver la función _MostrarUltimoError :
// Función para mostrar el ultimo error con FormatMessage añadiendo un texto personalizadovoid VentanaInstalador::_MostrarUltimoError(const TCHAR *Mensaje) {DWORD ErrNum = GetLastError();LPVOID lpMsgBuf;if (ErrNum == 0) return;FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,NULL, ErrNum, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMsgBuf, 0, NULL );if (Mensaje == NULL) {MessageBox(NULL, (LPCTSTR)lpMsgBuf, TEXT("Error!"), MB_OK | MB_ICONINFORMATION);}else {TCHAR Buffer[2048];wsprintf(Buffer, TEXT("%s\n%s"), Mensaje, (LPCTSTR)lpMsgBuf);MessageBox(NULL, Buffer, TEXT("Error"), MB_OK | MB_ICONINFORMATION);}LocalFree(lpMsgBuf);}
Esta función tiene por objetivo mostrar un mensaje de error con la información que nos da la API GetLastError. Con el número del error podemos utilizar FormatMessage para obtener un mensaje de error en formato de texto, que es bastante mas descriptivo que un numero. Por último mostramos el error con la API MessageBox.
Y con esto hemos terminado con el instalador. Ya solo nos queda el último paso : 3.11 Tutorial terminando el Ensamblador.