NovaMonoFix
Errores PHP
X
Usuario
Password
0 FPS

Tutorial WINAPI C++ 2.3 (Archivos y directorios)

14 de Mayo del 2010 por Josep Antoni Bover, 25 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++ 2.3 (Archivos y directorios)

Este tutorial se enfoca en el tratamiento de archivos y directorios. En versiones antiguas de windows los programas podian escribir archivos en cualquier parte del disco, pero a partir de windows vista esto ya no es así.

En los sistemas operativos actuales como son Windows vista y Windows 7, si tenemos que escribir en ciertas partes del disco duro requerimos privilegios de administrador. Esto es un coñazo para los programadores que venian haciendo aplicaciones de windows, ya que han tenido que adaptar multitud de codigo por esta razon.

Pero existen ciertas carpetas creadas especificamente para que los programas guarden datos sin tener que requerir privilegios de administracion, y en este tutorial veremos como acceder a ellas.

Bueno imagino que mas de uno ha tratado con temas de archivos básicos ya sea con las funciones fopen fread fwrite y tal o con los objetos std::istream std::ostream std::iostream, así que no veremos nada de eso, y nos centraremos en las APIs que tiene Windows para realizar esos trabajos. Pero antes de nada deberíamos empezar por comprender un poco como funcionan Windows vista y Windows 7 en temas de lectura y escritura de archivos y permisos.

A partir de Windows vista y Windows 7 se implanto como directiva que todos los programas debían funcionar en modo usuario por defecto, al contrario de lo que pasa con Windows xp e inferiores. Dicho sistema hace que nuestra aplicación tenga privilegios limitados de escritura, y esto se convierte en un problema en aplicaciones viejas cuando las hacemos correr en Windows 6.x

Lo que pasa es muy simple… cualquier aplicación que necesite escribir datos en su mismo directorio donde se encuentra el ejecutable no podrá si no tiene privilegios de administrador, o se modifican los privilegios de la carpeta para ello. Modificar los privilegios de la carpeta es inviable desde la misma aplicación ya que seguimos necesitando privilegios de administrador. Así que la mejor solución es utilizar el directorio AppData / ProgramData o incluso he visto algunos programas que utilizan la carpeta MisDocumentos directamente que también es una buena opción. Las rutas de esos directorios pueden variar bastante asi que lo mejor es usar el API de windows para saber donde estan. Por ejemplo en Windows 7 tengo el directorio AppData en "C:\ProgramData\", pero en windows XP lo tengo en "H:\Documents and Settings\All Users\Datos de programa" asi que es poco recomendable ingeniarse algún otro sistema.

Por un lado el tema de los privilegios está muy bien porque ya de por si evita muchos programas maliciosos, virus, etc.. pero por otro lado el que tenga que empezar a aprender C / C++ y quiera tocar el tema de archivos y directorios se puede volver idiota.

Al caso, el objetivo es hacer aplicaciones compatibles tanto con Windows xp como con Windows 6.x o superiores (aunque lo de superiores… siempre nos pueden volver a sorprender). Tampoco sería algo gravísimo si hacemos la aplicación compatible pensando solo en Windows XP, ya que en Windows Vista y Windows 7 si ejecutamos esa aplicación con privilegios de administrador funcionara perfectamente. Pero personalmente ya que nos ponemos a hacer las cosas… vamos a hacerlas bien :).

Lo primero que vamos a hacer es un objeto que nos devolverá la ruta para el directorio AppData en el cual podremos escribir lo que nos de la gana. Debido a que es algo problemático obtener directorios en ciertos sistemas operativos he hecho esta clase para que resulte de lo mas fácil. Veamos la declaración de ObjetoDirectoriosWindows :

Archivo : ObjetoDirectoriosWindows.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
// Clase que se usa para obtener directorios especiales de windows
class ObjetoDirectoriosWindows {
public : //////////////////////////// Miembros publicos
// -Constructor
ObjetoDirectoriosWindows(void);
// -Destructor
~ObjetoDirectoriosWindows(void);
// -Función para obtener el directorio Archivos de Programa 32 bits
// NOTA : StrOut debe ser un TCHAR de tamaño MAX_PATH como minimo
BOOL ArchivosDeProgramaX86(TCHAR *StrOut);
// -Función para obtener el directorio AppData general
// NOTA : StrOut debe ser un TCHAR de tamaño MAX_PATH como minimo
BOOL AppData(TCHAR *StrOut);
// -Función para obtener el directorio AppData del usuario actual
// NOTA : StrOut debe ser un TCHAR de tamaño MAX_PATH como minimo
BOOL AppData_UsuarioActual(TCHAR *StrOut);
// -Función para obtener el directorio del Escritorio general
// NOTA : StrOut debe ser un TCHAR de tamaño MAX_PATH como minimo
BOOL Escritorio(TCHAR *StrOut);
// -Función para obtener el directorio del Escritorio para el usuario actual
// NOTA : StrOut debe ser un TCHAR de tamaño MAX_PATH como minimo
BOOL Escritorio_UsuarioActual(TCHAR *StrOut);
protected : ///////////////////////// Miembros protegidos
// -Función que llamara a SHGetFolderPath si es posible
// NOTA : StrOut debe ser un TCHAR de tamaño MAX_PATH como minimo
BOOL _ObtenerDirectorioV5(int CSLID, TCHAR *StrOut);
// -Función que llamara a SHGetKnownFolderPath si es posible
// NOTA : StrOut debe ser un TCHAR de tamaño MAX_PATH como minimo
BOOL _ObtenerDirectorioV6(const GUID& rfid, TCHAR *StrOut);
// -Modulo shell32.
HMODULE _Shell32;
// -Función SHGetKnownFolderPath.
TpSHGetKnownFolderPath _SHGetKnownFolderPath;
};

En esencia hay 4 funciones públicas a las que les pasamos un TCHAR[MAX_PATH] y nos devuelven la ruta especificada, y luego tenemos el barullo, me explico… En Windows XP se usa la API SHGetFolderPath para obtener ese tipo de rutas, esa api no funciona en Windows 95 y no sabría decir si en Windows 98 existía (pero con VC6 se tiene que cargar dinamicamente por lo que lo veo poco probable), así que ya perdemos compatibilidad con esos dos sistemas operativos, pero ahora viene lo mejor.. En Windows Vista y Windows 7 se ha implementado una nueva API para esa tarea SHGetKnownFolderPath, y como no… no es compatible con WindowsXP, es mas CUALQUIER PROGRAMA QUE ENLACE ESTÁTICAMENTE A ESA API NO FUNCIONARA EN WINDOWS XP, podéis aplaudir.

Yo personalmente me niego crear mis aplicaciones para que solo funcionen en Windows vista y Windows 7, con lo cual lo mas fácil seria tirar de SHGetFolderPath, pero claro no me da la gana el no ofrecer las nuevas opciones de SHGetKnownFolderPath, asi que esta clase se ha diseñado para cargar SHGetKnownFolderPath dinámicamente si existe y en ese caso utilizarla. Que no existe? Es que estamos en Windows XP y utilizaremos SHGetFolderPath.

Para las aplicaciones del tutorial esto será prácticamente inútil ya que no necesitamos ningún directorio de los nuevos de SHGetKnownFolderPath, pero seguro que mas de uno se ha encontrado o se encontrara con este problema, así que os doy una solución inicial a este.

Obviamente si estamos en Windows xp y preguntamos por un directorio extendido de Windows 6.x vamos a tener que retornar de alguna forma que ese recurso no existe en Windows xp, pero eso dependerá mas de la aplicación que de mi implementación para solucionar el problema. Y recordemos que el problema es grave ya que directamente nos saldrá un mensaje de error en Windows XP que no se encuentra la función SHGetKnownFolderPath y se cerrara nuestra aplicación sin mas.

Bueno vamos al meollo, veamos la función _ObtenerDirectorioV5 :

Archivo : ObjetoDirectoriosWindows.cpp
1
2
3
4
5
6
// -Función que llamara a SHGetFolderPath si es posible
// NOTA : StrOut debe ser un TCHAR de tamaño MAX_PATH como minimo
BOOL ObjetoDirectoriosWindows::_ObtenerDirectorioV5(int CSLID, TCHAR *StrOut) {
if (SUCCEEDED(SHGetFolderPath(NULL, CSLID, NULL, 0, StrOut))) return TRUE;
return FALSE;
}

Como veis se llama a la API SHGetFolderPath la cual nos retornara la ruta donde se encuentra el directorio AppData. Si la operación sale bien se retorna TRUE, en caso contrario FALSE.

Ahora veamos la funcion _ ObtenerDirectorioV6 :

Archivo : ObjetoDirectoriosWindows.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// -Función que llamara a SHGetKnownFolderPath si es posible
// NOTA : StrOut debe ser un TCHAR de tamaño MAX_PATH como minimo
BOOL ObjetoDirectoriosWindows::_ObtenerDirectorioV6(const GUID& rfid, TCHAR *StrOut) {
#if _MSC_VER > 1300
if (_SHGetKnownFolderPath != NULL) { // Vista o superior
PWSTR Tmp = NULL;
if (S_OK == _SHGetKnownFolderPath(FOLDERID_ProgramData, NULL, 0, &Tmp)) {
wcscpy_s(StrOut, MAX_PATH, Tmp);
CoTaskMemFree(Tmp);
return TRUE;
}
}
#endif
return FALSE;
}

Ahora echemos un vistazo al constructor, y asi veremos como cargamos dinámicamente la API SHGetKnownFolderPath :

Archivo : ObjetoDirectoriosWindows.cpp
1
2
3
4
5
6
7
// -Constructor
ObjetoDirectoriosWindows::ObjetoDirectoriosWindows(void) {
_Shell32 = LoadLibrary(TEXT("shell32.dll"));
_SHGetKnownFolderPath = NULL;
if (_Shell32 != NULL)
_SHGetKnownFolderPath = reinterpret_cast<TpSHGetKnownFolderPath>(GetProcAddress(_Shell32, "SHGetKnownFolderPath"));
}

Lo primero que hacemos es usar LoadLibrary con la shell32.dll que es la librería que tiene la API SHGetKnownFolderPath. Si el resultado de LoadLibrary es un handle valido, utilizamos la API GetProcAddress para obtener la dirección de la API SHGetKnownFolderPath que introduciremos en la variable _ SHGetKnownFolderPath.

Ahora veamos el destructor :

Archivo : ObjetoDirectoriosWindows.cpp
1
2
3
4
5
// -Destructor
ObjetoDirectoriosWindows::~ObjetoDirectoriosWindows(void) {
if (_Shell32 != NULL) FreeLibrary(_Shell32);
_Shell32 = NULL;
}

Lo único que se hace es usar la API FreeLibrary con el handle de la Shell32.dll.

Por último veamos la función AppData :

Archivo : ObjetoDirectoriosWindows.cpp
1
2
3
4
5
6
7
8
// -Función para obtener el directorio AppData general
// NOTA : StrOut debe ser un TCHAR de tamaño MAX_PATH como minimo
BOOL ObjetoDirectoriosWindows::AppData(TCHAR *StrOut) {
#if _MSC_VER > 1300
if (_ObtenerDirectorioV6(FOLDERID_ProgramData, StrOut) == FALSE) return _ObtenerDirectorioV5(CSIDL_COMMON_APPDATA, StrOut);
#endif
return _ObtenerDirectorioV5(CSIDL_COMMON_APPDATA, StrOut);
};

Primero usamos _ObtenerDirectorioV6, en el caso de que retorne FALSE, usamos _ObtenerDirectorioV5.

En el caso de que necesitemos una ruta distinta podemos crearnos nuestra función haciendo exactamente lo mismo que en la función AppData, con solo cambiar el FOLDERID y el CSLID por el adecuado para la ruta que queramos obtener.

Bueno con esto ya tenemos un objeto que utiliza a poder ser la API SHGetKnownFolderPath, y si no es posible utiliza la API SHGetFolderPath. A partir de aquí ya somos capaces de ubicar una ruta donde podamos crear nuestros archivos. Ahora pasemos a la fase de creación de archivos, para ello crearemos un ObjetoArchivo que nos permitirá leer y escribir archivos de una forma fácil. Veamos la declaración de ObjetoArchivo :

Archivo : ObjetoArchivo.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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// Clase que encapsula las funciones necesarias para abrir-cerrar y leer-escribir archivos
class ObjetoArchivo {
public : //////////////////////////// Miembros publicos
// - Constructor
ObjetoArchivo(void);
// - Destructor
~ObjetoArchivo(void);
// - Función para abrir un archivo (lectura / escritura)
BOOL AbrirArchivo(const TCHAR *Path, const bool Abrir_si_no_existe = false);
// - Función para abrir un archivo
BOOL AbrirArchivoLectura(const TCHAR *Path);
// - Función para abrir un archivo
BOOL AbrirArchivoEscritura(const TCHAR *Path, const bool Abrir_si_no_existe = true);
// - Función para cerrar el archivo
BOOL CerrarArchivo(void);
// - Función para leer datos del archivo
BOOL Leer(LPVOID, const size_t NumBytes);
// - Función para guardar datos en el archivo
BOOL Guardar(LPCVOID Buffer, const size_t NumBytes);
// - Función que lee un string previamente guardado con GuardarString
// Hay que pasarle un puntero NULL en el que se creara el string.
// Una vez no necesites mas el string deberas borrarlo con delete.
BOOL LeerString(PTCHAR &nStr);
// - Función que guarda un string terminado con '\0'
BOOL GuardarString(const TCHAR *Str);
// - Función que lee un UINT
inline BOOL LeerUINT(UINT &Valor) {
return Leer(&Valor, sizeof(UINT));
};
// - Función que guarda un UINT
inline BOOL GuardarUINT(const UINT Valor) {
return Guardar(&Valor, sizeof(UINT));
};
// - Función que lee un UINT
inline BOOL LeerDWORD(DWORD &Valor) {
return Leer(&Valor, sizeof(DWORD));
};
// - Función que guarda un DWORD
inline BOOL GuardarDWORD(const DWORD Valor) {
return Guardar(&Valor, sizeof(DWORD));
};
// - Función para saber si el archivo esta abierto
bool EstaAbierto(void);
// -Función para obtener la longitud del archivo (limitada a valores de 32 bits)
// Si el archivo es muy grande puedes usar ValorAlto para almacenar el HIDWORD,
// el valor de retorno siempre sera el LODWORD
DWORD ObtenerLongitud32(DWORD *ValorAlto = NULL);
// - Función para obtener la longitud del archivo en formato 64 bits
LARGE_INTEGER ObtenerLongitud64(void);
// - Función que nos dice si se ha llegado al final del archivo con la función Leer
inline bool FinalDelArchivo(void) const {
return _FinalDelArchivo;
}
// - Función para posicionar el cursor dentro del archivo
inline ULONG Posicion(const long Pos, const bool Desde_El_Final = false) {
if (Desde_El_Final == false) return SetFilePointer(_Archivo, Pos, NULL, FILE_BEGIN);
else return SetFilePointer(_Archivo, Pos, NULL, FILE_END);
};
protected : ///////////////////////// Miembros protegidos
// - Miembro que contiene la ID del archivo.
HANDLE _Archivo;
// - Miembro que indica si se ha llegado al final del archivo con la función Leer
bool _FinalDelArchivo;
};

Esta clase tiene varias funciones para abrir archivos, una para lectura, una para escritura, y una para lectura y escritura. Además tiene 2 funciones base para leer y escribir datos, y otras 4 funciones que usan las dos primeras para leer / escribir tipos de datos mas específicos como son una cadena de caracteres y un UINT. Por último también tenemos dos funciones para obtener la longitud del archivo, y una función para saber si el archivo está abierto por nosotros. Ahora veamos la función AbrirArchivo :

Archivo : ObjetoArchivo.cpp
1
2
3
4
5
6
7
8
9
10
11
// -Función para abrir un archivo lectura/escritura
// Devuelve FALSE en caso de error
BOOL ObjetoArchivo::AbrirArchivo(const TCHAR *Path, const bool Abrir_si_no_existe) {
CerrarArchivo();
_Archivo = CreateFile( Path, FILE_READ_DATA | FILE_WRITE_DATA, 0, 0,
(Abrir_si_no_existe) ? OPEN_ALWAYS : OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, 0 );
_FinalDelArchivo = false;
if (_Archivo != INVALID_HANDLE_VALUE) return TRUE;
else return FALSE;
}

Lo primero que hacemos es usar la función CerrarArchivo por si ya había alguno abierto no dejarlo colgado, luego usamos la API CreateFile para obtener el handle del archivo, y retornamos TRUE si la operación ha tenido éxito, FALSE en caso contrario.

La API CreateFile se usa tanto para crear archivos como para abrir archivos, podemos especificar FILE_READ_DATA , FILE_WRITE_DATA , o los dos a la vez según nos convenga, también podemos especificar si creamos un archivo en el caso de que no exista con el parámetro OPEN_ALWAYS, o podemos especificar el parámetro OPEN_EXISTING si solo queremos abrir un archivo ya existente.

Ahora veamos la función CerrarArchivo :

Archivo : ObjetoArchivo.cpp
1
2
3
4
5
6
7
// -Función para cerrar el archivo
BOOL ObjetoArchivo::CerrarArchivo(void) {
BOOL Ret = FALSE;
if (_Archivo != INVALID_HANDLE_VALUE) Ret = CloseHandle(_Archivo);
_Archivo = INVALID_HANDLE_VALUE;
return Ret;
};

Utilizamos la API CloseHandle para cerrar el archivo. Solo comentar que si _Archivo es igual a INVALID_HANDLE_VALUE no debemos usar CloseHandle.

Ahora veamos las funciones Leer y Guardar :

Archivo : ObjetoArchivo.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// -Función para leer datos del archivo
BOOL ObjetoArchivo::Leer(LPVOID Buffer, const size_t NumBytes) {
DWORD Leido = 0;
BOOL Ret = ReadFile(_Archivo, Buffer, NumBytes, &Leido, NULL);
if (Leido != NumBytes) _FinalDelArchivo = true;
return Leido;
};
// -Función para guardar datos en el archivo
BOOL ObjetoArchivo::Guardar(LPCVOID Buffer, const size_t NumBytes) {
DWORD Guardado = 0;
BOOL Ret = WriteFile(_Archivo, Buffer, NumBytes, &Guardado, NULL);
return Guardado;
};

Para leer datos utilizamos la API ReadFile, a la que le pasamos un buffer previamente creado con el tamaño necesario para obtener los bytes especificados. En la variable Guardado nos retorna los bytes realmente leídos que retornaremos en la función Leer.

Para guardar datos utilizamos la API WriteFile a la que le pasamos el buffer de datos a guardar, el tamaño en bytes del buffer, y una variable en la que recibiremos los bytes realmente guardados.
Ahora vamos a ver las funciones LeerString y GuardarString :

Archivo : ObjetoArchivo.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// -Función que guarda un string terminado con '\0'
BOOL ObjetoArchivo::GuardarString(const TCHAR *Str) {
UINT Tam = 0;
if (Str != NULL) Tam = wcslen(Str) + 1;
GuardarUINT(Tam);
return Guardar(Str, Tam * sizeof(TCHAR));
}
// -Función que lee un string previamente guardado con GuardarString
// Hay que pasarle un puntero NULL o sin iniciar en el que se creara el string.
// Una vez no necesites mas el string deberas borrarlo con delete.
BOOL ObjetoArchivo::LeerString(PTCHAR &nStr) {
UINT Tam = 0;
if (LeerUINT(Tam) == 0) return FALSE;
if (Tam == 0) return FALSE;
nStr = new TCHAR[Tam];
Leer(nStr, Tam * sizeof(TCHAR));
return Tam;
}

La función GuardarString primero guarda el tamaño en caracteres de la cadena, y luego guarda la cadena. De esta forma LeerString sabe cuantos caracteres deberá leer.
La función LeerString primero lee el tamaño en caracteres de la cadena, luego crea un buffer con el suficiente tamaño para albergar la cadena en el puntero por referencia nStr, y por ultimo lee el string dentro de nStr.

Y ya solo nos faltan las funciones ObtenerLongitud :

Archivo : ObjetoArchivo.cpp
1
2
3
4
5
6
7
8
9
10
11
// -Función para obtener la longitud del archivo (limitada a valores de 32 bits)
DWORD ObjetoArchivo::ObtenerLongitud32(DWORD *ValorAlto) {
return GetFileSize(_Archivo, ValorAlto);
}
// -Función para obtener la longitud del archivo en formato 64 bits
LARGE_INTEGER ObjetoArchivo::ObtenerLongitud64(void) {
LARGE_INTEGER Ret;
GetFileSizeEx(_Archivo, &Ret);
return Ret;
}

He puesto las 2 versiones ya que me parecía interesante comentar la función de 64 bits, aunque para las tareas que os voy a encomendar sobrara con la de 32 bits.

Para la función de 32 bits se usa la API GetFileSize, la cual nos retorna el DWORD bajo, y si necesitamos un valor mas alto podemos usar ValorAlto y asi obtener el DWORD Alto.

Para la función de 64 bits se usa la API GetFileSizeEx, la cual nos retorna una estructura del tipo LARGE_INTEGER. La estructura LARGE_INTEGER viene con varios valores, para 32 bits debemos usar LowPart y HighPart para obtener el resultado, pero para 64 bits solo hay que consultar el parámetro QuadPart.

En el ejemplo 2.3 se crea un archivo txt de prueba con un texto dentro, y se abre una ventana del explorador en la ruta AppData donde se ha guardado.

Ahora si que ya podemos crear la ultima ventana translucida que nos falta para el juego : 2.4 - Tutorial Creación del ObjetoEscena_Records.

Descargar tutorial WinAPI completo Snake compilada