Apunte de Visual C++
Por: Demian Panello demianpanello@yahoo.com.ar
Capítulo XII
Indice rápido del capítulo 12:
En este capítulo veremos los conceptos básicos de la manipulación de archivos en VC++.
Existen 2 importantes grupos de archivos:
- Archivos de texto.
- Archivos binarios.
Un archivo de texto es aquel cuyo contenido son caracteres, (de la tabla ASCII), que además representan por si solos información. Por ejemplo, archivos de texto son los archivos .cpp que uno crea cuando programa en C++, también son de texto los archivos .txt y en general cualquier archivo, (no importa la extensión), cuyo contenido sean caracteres.
Los archivos binarios son bien diferentes a los de texto y la diferencia radica en el formato. El contenido de un archivo binario es absurdo y carece de sentido si lo editamos con algún editor de texto, (ejemplo Notepad o el EDIT del DOS), y esto es así en principio por dos cosas: por un lado, el conjunto de caracteres que están en el archivo no representan alguna información lógica al momento de visualizarlos como simples caracteres y fundamentalmente porque la creación del archivo no sigue la pauta de la de un simple texto, esto es, se creó el archivo para que guarde información con un formato determinado.
Por ejemplo guardar en un archivo el contenido de una estructura o de un arreglo de estructuras no sigue la regla simple de los archivos de texto de un caracter detrás del otro, saltos de línea, etc; y si bien la estructura almacena caracteres estos ya están subordinados a campos lo que cambia el formato del manejo de los datos. Ya no es simplemente texto, sino, información de bytes, (seguramente muchos de esos bytes son los caracteres almacenados en el campo APELLIDO de la estructura pero muchos otros no).
La MFC ofrece una clase que sirve como "envoltorio" de las funciones para manipular archivos del SDK de Windows. Esta clase se llama CFile.
Vamos a hacer una aplicación que nos permitirá crear y editar un archivo de texto; siempre leerá y escribirá el mismo archivo, "pp.txt", en el directorio actual.
Cree un proyecto MFC llamado "Archivos1", basado en diálogos y acepte todas las opciones por defecto, (si quiere puede quitar la creación del diálogo Acerca de).
Diseñe el diálogo como muestra la figura:

Especifique las siguientes propiedades de los controles:
Control de edición:
- ID: IDC_EDIT1
- Tildar los estilos: Multiline, Horizontal Scroll, Auto HScroll, Vertical Scroll, Want Return y Border.
Botones: IDC_GUARDA, (Guardar "pp.txt") e IDC_ABRIR, (Cargar "pp.txt").
Con el Class Wizard cree una variable de tipo CString llamada m_strTexto para el control de edición, (recuerde Class Wizard -> Member Variables -> Seleccione IDC_EDIT1 y pulse Add Variable).
Ahora hay que escribir en el mensaje WM_LBUTTONDOWN del botón IDC_GUARDA el código que crea el archivo "pp.txt" y guarda en él el contenido del cuadro de edición.
void CArchivos1Dlg::OnGuarda()
{
CFile Archi; (1)
Archi.Open ("pp.txt", CFile::modeCreate | CFile::modeWrite); (2)
//Abro el archivo
UpdateData(TRUE); (3)
Archi.Write ( m_strTexto, m_strTexto.GetLength ()); (4)
Archi.Close (); (5)
m_strTexto=""; (6)
UpdateData(FALSE); (7)
AfxMessageBox("Archivo guardado con éxito"); (8)
}
En la línea (1) se crea un objeto CFile llamado Archi y con él en la línea (2) abrimos el archivo por medio de la función miembro Open(). Esta función recibe 2 parámetros, el primero es una cadena que indica el archivo que se va abrir, (obviamente se puede incluir el path) y el segundo parámetro especifica el modo de apertura que se pueden combinar con las siguientes constantes:
En nuestro caso abrimos el archivo para crearlo y escribir.
Open() retorna distinto de 0 en caso que la apertura tenga éxito y 0 en caso que falle por algún motivo, (por ejemplo el disco esta protegido contra escritura). Deberíamos colocar esta sentencia evaluada en un IF para que sea más prolijo el código, pero bueno, por ahora confiemos que se podrá abrir sin problemas.
Una vez abierto el archivo transfiero el contenido del cuadro de edición a la variable miembro m_strTexto con la conocida función UpdateData(TRUE). Y en (4) escribimos en el archivo el contenido de m_strTexto con la función Write. Dos parámetros necesita: la variable de tipo CString a escribir y la longitud de la misma.
Luego se cierra el archivo con Close(), se "limpia" la variable m_strTexto y se vuelve a llamar a UpdateData() esta vez con el parámetro FALSE para que pase el contenido de la variable al cuadro de edición, lo que da a lugar que el cuadro de edición quede vacío.
Puede probar la aplicación. Escriba algo en el cuadro de edición y luego pulse el botón "Guardar pp.txt". Para verificar si realmente escribió en el archivo vaya a la carpeta donde se encuentre el proyecto y edite "pp.txt".
Escribamos ahora el código que me permite editar el archivo en el cuadro de edición. Para esto agregue el mensaje OnAbrir() para el botón IDC_ABRIR.
Para este caso hay que tener en cuenta que quizás el archivo a editar sea muy grande como para intentar leerlo de una vez, por eso vamos a leer, (mientras haya texto que leer), de a pedazos de 512 bytes, (o caracteres, es un estándar este número en lo que respecta a lectura en bloques). El procedimiento sería:
- Abrir el archivo para sólo lectura.
- Si se pudo abrir, entonces, leer 512 bytes.
- Mientras la cantidad de bytes leídos es mayor a 0, se "acumula" lo leído en la variable miembro m_strTexto y se vuelve a leer 512 bytes.
- Cuando no haya nada que leer se actualiza el cuadro de edición con el contenido de m_strTexto y se cierra el archivo.
Más o menos debería se así, veamos entonces lo que hay que escribir en OnAbrir().
void CArchivos1Dlg::OnAbrir()
{
CFile Archi;
char strLeo[512]; (1)
UINT bLeidos; (2)
Archi.Open ("pp.txt", CFile::modeRead); //Abro el archivo
if (Archi) (3)
//si retorna distinto de NULL es porque se pudo abrir
{
m_strTexto="";
bLeidos=Archi.Read (strLeo,sizeof(strLeo)-1); (4) //leo
511 bytes
while (bLeidos)
(5)
{
strLeo[bLeidos]=NULL; (6)
//Agrego terminación nula al final
m_strTexto +=
CString(strLeo); (7)
//lo acumulo en la variable
bLeidos=Archi.Read
(strLeo,sizeof(strLeo)-1); (8) //leo otra vez
}
Archi.Close (); //cierro el archivo
}
UpdateData(FALSE); (9)
}
En (1) se declara una cadena de 512 caracteres, en realidad 511 ya que el último caracter de una cadena en C es '\0', el caracter nulo, (NULL), que indica donde termina la cadena, (en muy importante tener presente ésto). En (2) se declara un entero largo, (UINT), donde se almacenará la cantidad de bytes leído en cada intento.
Se intenta abrir el archivo para sólo lectura y en (3) se verifica que Archi no sea NULL porque sino implicaría que no se pudo abrir el archivo. En caso de no haber problemas, en (4) se hace una lectura del archivo con la función Read().
Dos parámetros son necesarios para una simple lectura, primero la variable de tipo cadena, (puede ser CString o como en nuestro caso un arreglo de char) y luego el tamaño de la misma que en caso de usar una variable char se obtiene con el operador sizeof(), si fuera CString se utilizaría GetLength() como cuando escribimos en el archivo. Se preguntará por qué se hace sizeof(strLeo)-1 y no simplemente sizeof(strLeo); bien, el tema es que como usamos una cadena de tipo char tendremos que "poner a mano" nosotros el caracter nulo en la última posición de la cadena, entonces en vez de leer 512 bytes leemos 511 y nos sobra una posición para poder colocar el caracter de fin de cadena.
Read() retorna la cantidad bytes leidos, entonces en (5) se verifica si algo se leyó, o sea si bLeidos es distinto de 0 porque de mientras sea así se continuará leyendo, (es una forma de detectar el fin de archivo).
Dentro del mientras, en (6) se le coloca a la cedena la terminación nula, NULL, y se acumula su contenido en la variable miembro m_strTexto, línea (7). En (8) se vuelve a leer.
Cuando no haya más bytes que leer el flujo sale del ciclo mientras, se cierra el archivo con Close() y se transfiere todo el contenido de m_strTexto, (que es justamente el contenido del archivo), a el cuadro de edición por medio de UpdateData(FALSE).
Resumiendo:
Un archivo se abre usando la función Open() de la clase CFile. En los parámetros se especifica el archivo a abrir y el modo de apertura, ejemplo: CFile::modeCreate.
Para escribir en un archivo hay que usar la función miembro de CFile, Write(), cuyos parámetros permiten especificar el buffer, (la variable que contiene lo que se quiere escribir) y el tamaño de la misma.
La lectura con Read() sigue lo mismo preceptos que Write(), salvo que el buffer ahora recibirá los datos leídos desde el archivo. Es importante el retorno de Read() ya que el mismo devuelve la cantidad de bytes leídos que en caso de alcanzar el fin del archivo es menor que la cantidad leída.
Descargar archivos fuentes del ejemplo: archivos1.zip.
Dentro del concepto de archivo binario, (archivos BMP, EXE, etc.), exiten los archivos de estructuras. Estos archivos contienen información en registros, donde cada registro obedece a una estructura determinada. Por ejemplo, tenemos la estructura Personas:
struct Personas
{
char Apellido[20];
char Nombre[20];
int edad;
};
Se puede crear un archivo que almacene registros de tipo Personas, o sea que se escribe y se lee información de tipo Personas.
Vamos a escribir un ejemplo de aplicación que manipule un archivo de estructuras, en particular de tipo Personas.
Con el Application Wizard cree una aplicación MFC basada en diálogos, (quite la opción About Box), y diseñe el diálogo más o menos como muestra la figura:

No borre el botón Cancelar, ya que de paso ahora, antes de empezar, veremos como hacer para que el diálogo no se cierre si se pulsa ESC.
Cómo hacer para que al pulsar ESC no se cierre un diálogo:
Cuando se genera una aplicación Dialog Based, ésta presenta ya un diálogo predefinido con los botones Aceptar y Cancelar, estos botones generan, al pulsarlos, los mensaje predefinidos OnOk() y OnCancel(). En los dos casos el resultado es el mismo, se cierra el diálogo. Para evitar que al pulsar la tecla ENTER se pulse Aceptar se puede optar por quitar el botón o mejor quitar el tilde a la propiedad Default Button en la solapa Style.
Pero en el caso del botón Cancelar, aunque se lo borre del diálogo igual el efecto de pulsar la tecla ESC genera el mensaje OnCancel() y esto se puede evitar de la siguiente forma.
Antes de diseñar lo que respecta al ejemplo y teniendo el diálogo en pantalla, pulse dos veces sobre el botón Cancelar para poder acceder al mensaje OnCancel() y alli comente con // la línea CDialog::OnCancel();.
Ahora puede usar el botón Cancelar para que cumpla la función de alguno necesario para la aplicación que vamos a hacer, (por ejemplo puede ser el botón Ver datos), cambiandole el ID por defecto IDCANCEL por el que reemplace.
Regresemos a nuestro ejemplo. Las propiedades de los controles y sus variables miembros asociadas, (crear con el Class Wizard), son:
Control |
ID |
Propiedades modificadas | Variable miembro asociada | Tipo |
|---|---|---|---|---|
| Static Text | El que trae por defecto | Caption: Apellido (20 caracteres). | ninguna | - |
| Static Text | El que trae por defecto | Caption: Nombre (20 caracteres). | ninguna | - |
| Static Text | El que trae por defecto | Caption: Edad. | ninguna | - |
| Edit Box | IDC_APELLIDO | Las propiedades por defecto. | m_strApellido | CString |
| Edit Box | IDC_NOMBRE | Las propiedades por defecto. | m_strNombre | CString |
| Edit Box | IDC_EDAD | Tildar: Number y Right aligned text | m_Edad | int |
| Button | IDC_GUARDAR | Caption: Guardar datos y tildar la propiedad Default button | ninguna | - |
| Button | IDC_VER | Caption: Ver datos >> | ninguna | - |
| Button | IDOK | Caption: Aceptar y quitar el tilde a la propiedad Default button. | ninguna | - |
| Button | IDC_LIMPIAR | Caption: Limpiar | ninguna | - |
Recuerde que para poder crear las variables asociadas con el Class Wizard, debe especificar primero todos los ID de los controles.
Lo que vamos a hacer es lo siguiente:
En los cuadros de edición el usuario va a escribir el apellido, el nombre y la edad, luego al pulsar el botón Guardar datos, (que como le asignamos la propiedad Default button esto ocurre con sólo pulsar ENTER), se pasan las variables miembros asociadas a los controles a los campos de la estructura y se escribe en el archivo "datos.dat", (la extensión puede ser cualquiera). Pero atención, si se ingresan nuevos datos el archivo no se vuelve a crear borrando el contenido existente sino que agregaremos los nuevos.
Si se pulsa "Ver datos >>" podremos ir visualizando en los cuadros de edición los campos de cada registro en cada pulsación de este botón, hasta que se llegue al fin del archivo, cuando mostramos un mensaje aclaratorio y se visualizan nuevamente los datos del primer registro. El botón "limpiar" borra los contenidos de los cuadro de edición en caso que se esté visualizando un registro y se desee ingresar uno nuevo.
Además de estos controles vamos a necesitar definir en algún lugar la estructura Personas. Esto se podría hacer en el código del botón Guardar datos y en el de Ver datos, pero no es el lugar más prolijo, sino que se debe definir la estructura en la declaración de la clase del diálogo. A esta altura debe ya acostumbrarse a pensar qué cosas son útiles de definir en una clase y por lo general esto está asociado a la protección de la información y al ámbito.
Para definir la estructura en la clase del diálogo pulse dos veces en el Class View la entrada de la clase del diálogo:

Se editará la declaración de la clase del diálogo, (el archivo archivos2Dlg.h), allí escriba lo que está marcado en amarillo:
class CArchivos2Dlg : public CDialog
{
private:
struct Persona
{
char Apellido[20];
char Nombre[20];
int edad;
};
// Construction
.....
}
Otra cosa importante a tener en cuenta es que si vamos a recorrer el archivo para visualizar cada registro necesitaremos ir guardando la posición del puntero del archivo en alguna variable. Para indicar que leer en un archivo éste utiliza un puntero que se debe desplazar tantos bytes como se desee, por eso en su momento necesitaremos una variable donde guardar esos desplazamientos.
También la declararemos en la clase del diálogo, pero seguiremos el método tradicional, esto es, pulse con el derecho sobre la entrada CArchivos2Dlg en el Class View y seleccione Add Member Variable.
Como Variable Type escriba unsigned long y como nombre de variable Pos.
Pulse 2 veces sobre el constructor de la clase del diálogo, (CArchivos2Dlg::CArchivos2Dlg(CWnd* pParent =NULL), se editará el constructor, allí le daremos un valor inicial a Pos. Como última línea, antes de la llave que cierra, escriba Pos=0;.
Con esto estamos listos para escribir el código del botón "Guardar datos", pulse dos veces sobre el botón para generar OnGuardar() y allí escriba:
void CArchivos2Dlg::OnGuardar()
{
struct Persona p; (1)
CFile f;
int cr;
CString nr;
if (MessageBox("¿Guarda estos datos?",
"Guardar", MB_YESNO|MB_ICONQUESTION)==IDYES)
{
UpdateData(TRUE);
strcpy(p.Apellido,m_strApellido); (2)
strcpy(p.Nombre, m_strNombre); (3)
p.edad =m_Edad;
(4)
f.Open
("datos.dat",CFile::modeCreate|CFile::modeNoTruncate|CFile::modeWrite); (5)
f.SeekToEnd();
(6)
f.Write (&p, sizeof(p)); (7)
m_strApellido.Empty(); (8)
m_strNombre.Empty(); (9)
m_Edad=0;
(10)
UpdateData(FALSE); (11)
AfxMessageBox("Datos guardados con éxito");
CWnd* pApe=(CWnd*)GetDlgItem(IDC_APELLIDO) ; (12)
pApe->SetFocus();
(13)
cr=f.GetLength()/sizeof(p);
(14)
nr.Format ("Cantidad de registros: %i", cr);
(15)
SetWindowText(nr); (16)
f.Close();
(17)
}
}
Al principio, en (1), se declara una variable de tipo Personas, además se contruye el objeto CFile, se declara una variable entera para almacenar la cantidad de registros, (algo extra que vamos a hacer) y una de tipo CString para poder mostrar en la barra de título del diálogo la cantidad de registros del archivo.
Se le pregunta al usuario si está seguro de querer guardar esa información y en caso afirmativo se pasan los contenidos de los cuadros de edición a las variables asociadas con nuestra conocida UpdateData(TRUE).
Una vez que las variables asociadas tienen los datos ingresados en (2), (3) y (4) se pasa esta información a los campos de la estructura p, (que es lo que en realidad vamos a grabar). En (5) se abre el archivo de manera tal que si no existe lo cree pero si ya existe que no lo trunque, o sea que no borre su contenido y que además lo abrimos para escribir.
Como vamos a agregar registros en caso que ya exista uno por lo menos, debemos mover el puntero al fin del archivo para que lo grabe a continuación del último, esto es lo que se hace con SeekToEnd() en (6).
En (7) escribimos el registro en el archivo, los parámetros son exactamente los mismos que en el ejemplo anterior, primero la variable que contiene la información a escribir, ahora se trata de una estructura y luego el tamaño de la misma. Esto implica que cada registro del archivo que estamos creando es de sizeof(p) bytes de tamaño.
En (8), (9) y (10) vaciamos las variables asociadas, (las de tipo CString con su función Empty() y la entera igualando a 0) y en (11) llamamos a UpdateData(FALSE) para transferir estos nuevos contenidos de las variables a los controles, lo que hace que se limpien quedando listos para nuevos ingresos.
Sería ideal que luego de guardar los datos el cursor quedara preparado de forma automática en el cuadro de edición del apellido para agilizar el ingreso, esto se logra "dandole el foco" a dicho control, es lo que se hace en las líneas (12) y (13). Como la función SetFocus() retorna un manejador de ventana, debería tratar al cuadro de edición como una de ellas, por eso en (12) se obtiene un puntero a CWnd con GetDlgItem() para así en (13) hacer efectivo el traslado del foco.
Y bueno, finalmente hay que obtener la cantidad de registros en el archivo para mostrar esa información en la barra de título de la ventana. Si un registro es de sizeof(p) de tamaño, haciendo tamaño del archivo/sizeof(p) se obtiene cuantos registros hay.
El tamaño del archivo se obtiene con la función miembro de CFile, GetLenght(), en (14) se hace la cuenta almacenando el resultado en un entero, en (15) se formatea una cadena con ese valor y en (16) se establece el título de la ventana.
Cerramos el archivo en (17).
Ahora escribiremos el código del botón "Ver datos >>", pero pensemos antes como debería funcionar.
Cuando se pulsa el botón "Ver..." podríamos mostrar algo si existiera algún registro o lo que equivale a decir si no estamos en el fin de archivo. Como el botón se puede pulsar de forma contínua deberíamos almacenar la posición del puntero en el archivo e ir incrementandola para poder mostrar el siguiente en la próxima pulsación. Por ejemplo si el puntero está en el primer registro su valor es 0, si quiero leer el segundo debería incrementarlo en sizeof(p) ya que el tamaño de cada registro es ese y luego de leer el segundo si quiero leer el tercero otra vez hay que incrementar el puntero en sizeof(p) y así sucesivamente hasta llegar al fin del archivo que equivale a decir, cuando los sucesivos incrementos del puntero alcanzan el tamaño del archivo.
Por cada pulsación se llama a alguna función que permita mover el puntero del archivo el desplazamiento que deseamos, se incrementa el puntero en sizeof(p) bytes para la siguiente pulsación y se procede a la lectura del registro actual.
Recuerde que declaramos una variable Pos para poder almacenar la posición del puntero en el registro.
Pulse dos veces en el botón "Ver..." para editar OnVer(), allí escriba:
void CArchivos2Dlg::OnVer()
{
struct Persona p;
CFile f;
f.Open ("datos.dat",
CFile::modeCreate|CFile::modeNoTruncate|CFile::modeRead); (1)
if (pos<f.GetLength()) (2)
//si no se llegó al fin del archivo
{
f.Seek(pos, CFile::begin); (3) //Ubico el puntero
del archivo en el siguiente registro
pos+=sizeof(p);
(4)
//incremento
pos para el próximo registro
f.Read (&p,sizeof(p));
//Leo un registro
m_strApellido=p.Apellido; //paso los contenidos a las variables miembros
m_strNombre=p.Nombre;
m_Edad=p.edad;
UpdateData(FALSE);
//Actualizo
los cuadros de edición
}
else
{ //se llegó al fin del
archivo
AfxMessageBox("Fin de archivo.\nPulse
nuevamente para ver el 1º registro");
pos=0;
//ahora
p es 0 para poder visulizar nuevamente el 1º registro
}
f.Close ();
}
Se abre el archivo para lectura (1) e inmediatamente se verifica si el contenido de Pos es menor que el tamaño del archivo, (2), de ser así es porque aún quedan registros que leer por eso en (3) ubico el puntero del archivo en el registro establecido por el contenido de Pos por medio de la función Seek(). Seek() recibe dos parámetros, el primero es la cantidad de bytes a desplazar el puntero y el segundo es desde dónde se realiza el desplazamiento, este segundo parámetro puede tomar los valores:
CFile::begin : Desde el principio del archivo, CFile::current : desde la posición actual y CFile::end : desde el fin del archivo.
Si fuera la primera vez que se pulsa el botón, Pos tiene 0, entonces Seek(Pos, CFile::begin) desplaza el puntero 0 bytes desde el comienzo, lo que implica que no lo mueve.
Luego de mover el puntero, en (4) se incrementa Pos en sizeof(p) bytes para poder en la siguiente pulsación cuando ocurra la llamada a Seek() avanzar al siguiente registro. De aqui en más se lee con Read() y se pasan los datos de la estructura a las variables asociadas a los controles.
Si en la línea (3) el contenido de Pos no era menor que el tamaño del archivo es porque se alcanzó el fin del mismo, entonces se muestra un mensaje aclaratorio y se pone Pos en 0 para poder mostrar, en caso que pulse nuevamente el botón "Ver..", el primer registro, (como que pegó toda una vuelta).
Para terminar nos queda el código del botón Limpiar, es muy fácil, sólo hay que limpiar las variables asociadas y luego llamar a UpdateData(FALSE).
Resumiendo:
Para poder ir agregando registros a un archivo de estruturas es importante llamar a Open() con los modos CFile::modeCreate y CFile::modeNoTruncate, además de que antes de escribir con Write() hay que mover el puntero del archivo al final del mismo..
La función Seek() permite mover el puntero en desplazamientos de acuerdo al tamaño del registro. Si un registro es de 50 bytes y quiero ubicar el puntero en el tercer registro tendré que especificar Seek(100, CFile::begin) ya que el primer registro se encuentra en el desplazamiento 0, el segundo a los 50 bytes y el tercero a los 100 bytes de "distancia" del principio.
Descargar archivos fuente del ejemplo: archivos2.zip.