Como
sabemos, Windows
es el entorno más popular de interfaz gráfico de usuario (GUI). Desde este
punto de vista, Windows
es un entorno multitarea basado en ventanas, que representan programas,
y que permite ejecución concurrente.
Para
desarrollar programas,
Windows provee una librería de rutinas y funciones
(SDK - Kit de desarrollo
de software)
que permiten gestionar componentes como menús, diálogos, ventanas, etc.
Visual
C++ es un entorno integrado de desarrollo
que permite la programación orientada
a objetos (POO) conjuntamente con el sistema
de desarrollo SDK (también denominado API) de Windows. Al ser un entorno
integrado Visual C++ incluye, entre otras, las siguientes herramientas
de desarrollo:
·
Editor de texto
·
Compilador/Enlazador
·
Depurador
·
Visor
de datos
y dependencias (Browser)
Pero
si desde el punto de vista del usuario Windows es un sistema
amigable, desde el punto de vista del desarrollador observaremos todo lo
contrario. El SDK de Windows no es mas que un complejo conjunto de funciones
que añade además numerosas definiciones de tipos de datos
nuevos para cualquier programador de C/C++ para DOS. Para solucionar este
problema, Visual C++ incluye la librería de clases MFC (Microsoft
Foundation Classes) que permite crear y gestionar de manera intuitiva
componentes típicos de Windows. Esto es, la MFC es una implementación que
utiliza el API encapsulando todas las estructuras
y llamadas a funciones en objetos fáciles de utilizar. Basándose en la potencia
de la MFC, Visual C++ se convierte en un generador de programas C++ para
Windows.
El objetivo
del presente curso es conocer el modelo
de programación
para Windows basado en la librería de clases MFC. En este documento se
destacarán ideas, conceptos y tratamientos generales, en ningún momento
pretende ser un manual
completo de programación
con MFC.
1. CONCEPTOS PRELIMINARES
1. ¿ Que es C ++ ?
Como todos
sabemos, "C" es un lenguaje
de alto nivel, basado en funciones, que permite desarrollos estructurados.
Entre otras muchas características
contempla la definición de estructuras
de datos,
recursividad o indirecciones a datos o código (punteros).
"C ++",
por su parte, es un superconjunto de "C", al que recubre con una capa
de soporte a la POO. Permite por tanto la definición, creación y manipulación
de objetos.
La
POO es una nueva filosofía de programación que se basa en la utilización
de objetos. El objetivo
de la POO no es sino la meta
de cualquier modelo
de programación estructurada convencional: "imponer" una serie de normas
de desarrollo que aseguren y faciliten la mantenibilidad y reusabilidad del
código.
Los
mecanismos básicos de la POO son: objetos, mensajes, métodos
y clases.
·
Objetos. Un objeto es una entidad que tiene unos atributos
particulares (datos) y unas formas de operar sobre ellos (los métodos
o funciones miembro). Es decir, un objeto incluye, por una parte una serie de operaciones
que definen su comportamiento,
y una serie de variables
manipuladas por esas funciones que definen su estado.
Por ejemplo, una ventana Windows contendrá operaciones
como "maximizar" y variables
como "ancho" y "alto" de la ventana.
·
Mensajes. En C++, un mensaje se corresponde
con el nombre de uno de los métodos de un objeto. Cuando se pasa un mensaje a
un objeto, este responde ejecutando el código de la función asociada.
·
Método. Un método
(función miembro) se implementa dentro de un objeto y determina como tiene que
actuar el objeto cuando se produce el mensaje asociado. En C++ un método
se corresponde con la definición de la función miembro del objeto. La estructura
más interna de un objeto está oculta, de tal manera que la única conexión con
el exterior son los mensajes
·
Clases. Una clase es la definición de un
tipo de objetos. De esta manera, una clase "Empleado" representaría
todos los empleados de una empresa,
mientras que un objeto de esa clase (también denominado instancia)
representaría a uno de esos empleados en particular.
Las
principales características de la POO son: abstracción, encapsulamiento,
herencia
y polimorfismo:
·
Abstracción. Es el mecanismo de diseño
en la POO. Nos permite extraer de un conjunto de entidades datos y
comportamientos comunes para almacenarlos en clases.
·
Encapsulamiento. Mediante esta técnica
conseguiremos que cada clase sea una caja negra, de tal manera que los objetos
de esa clase se puedan manipular como unidades básicas. Los detalles de la
implementación se encuentran dentro de la clase, mientras que desde el
exterior, un objeto será simplemente una entidad que responde a una serie de
mensajes públicos (también denominados interfaz de la clase).
·
Herencia. Es el mecanismo que nos permite
crear clases derivadas
(especialización) a partir de clases bases (generalización). Es decir,
podríamos tener la clase "Empleado" (clase base) y la clase
"Vendedor" derivando de la anterior. Una librería de clases
(como la MFC) no es más que un conjunto de definiciones de clases
interconectadas por múltiples relaciones de herencia.
·
Polimorfismo. Esta característica
nos permite disponer de múltiples implementaciones de un mismo método de clase,
dependiendo de la clase en la que se realice. Es decir, podemos acceder a una
variedad de métodos distintos (con el mismo nombre) mediante el mismo mecanismo
de acceso. En C++ el polimorfismo se consigue mediante la definición de clases derivadas,
funciones virtuales y el uso de punteros a objetos.
Otros
dos conceptos muy importantes en la POO son relativos a la creación y
destrucción de objetos. En lenguajes estructurados convencionales, cuando se
define una variable se le reserva espacio en memoria
y, si no se inicializa expresamente, se hace por defecto (por ejemplo, en C una
variable global siempre se inicializa a 0, pero una automática no, por lo que
si no se inicializa expresamente su contenido inicial será basura);
por otra parte, cuando se destruye una variable (por que se abandona el ámbito
de su definición - scope -) se libera la memoria
que estaba ocupando. Si ahora hacemos el paralelismo obligado entre variables y
objetos para los lenguajes POO nos daremos cuenta de que deben existir procedimientos
especiales de construcción
y destrucción de objetos. En concreto,
cada clase tiene dos funciones miembro especiales denominadas constructor
y destructor.
·
Constructor -> Función miembro que es automáticamente
invocada cada vez que se define un objeto, su objetivo es la inicialización del
mismo. Toma el mismo nombre que la clase, puede recibir parámetros y podemos
tener varios constructores definidos.
·
Destructor -> Función miembro invocada
automáticamente cada vez que se destruye un objeto. Su objetivo es realizar
operaciones como liberación de memoria,
cerrar ficheros abiertos, etc. Toma el mismo nombre de la clase comenzado
primero por el carácter "~", no toma parámetros y no admite la sobrecarga
(sólo puede existir uno en cada clase).
En
muchos casos, para las clases mas sencillas, podemos encontrar clases que no
tiene constructor o destructor, ó ninguno de los dos. En C++, siempre existen
constructores y destructores por defecto que realizan una
inicialización/liberación estándar.
1. El modelo de programación Windows
El
modelo de programación propuesto por Windows es totalmente diferente al modelo
de ejecución secuencial de DOS. Al ser Windows un entorno multitarea los
programas tienen que estar preparados para compartir los recursos
de la maquina (procesador,
memoria, teclado,
ratón …). Esto supone que Windows ha de disponer de métodos que permitan
suspender tareas para activar otras en función de las circunstancias del
momento (por ejemplo, por acción del usuario).
Pero
por parte de las aplicaciones, este hecho supone que han de cooperar en la
compartición de esos recursos.
Las aplicaciones Windows se limitan a "esperar" mensajes procedentes
del sistema, procesarlos y volver al estado
de espera. Este modelo de programación se conoce como "orientado al
evento".
·
Mensaje. Es una notificación a la aplicación de que ha
ocurrido algo de interés
y que por lo tanto debe de realizarse alguna acción específica. El origen del
mensaje puede ser el usuario (haciendo click con el ratón dentro e una
ventana), la propia aplicación (mandándose un mensaje a si misma) o Windows
(pidiendo, por ejemplo, que se repinte la ventana tras ocultarse otra que
tuviese delante). Dado que la unidad mínima de ejecución en Windows es una
ventana, los mensajes van realmente dirigidos a ellas.
·
Ventana
y procedimiento
de ventana. En
Windows, una aplicación se representa físicamente por su ventana principal
(aunque después pueda desplegar diversas ventanas hijas). Cada una de esas
ventanas dispone de una serie de propiedades y un código asociado (lo que
concuerda con el principio de la POO, en el concepto
de objeto). Al código asociado a cada ventana se le denomina procedimiento
de ventana. Es una función que recibe los mensajes, los procesa y devuelve
el control
a Windows para quedar en espera.
Otra
de las características específicas de Windows frente a DOS es el uso de
recursos por parte de las aplicaciones, como son iconos, menús, mapas
de bits, cursores, plantillas de diálogos, etc. Las aplicaciones Windows
disponen por tanto de recursos (gráficos generalmente) propios almacenados en
lo que se llama el fichero de recursos). El proceso
de construcción
de programas en Windows incorpora una fase adicional al compilado y enlazado de
los módulos objeto y las librerías. Hay un proceso
final de compilación y de enlazado (bind) del fichero de recursos.
3. EL ENTORNO DE DESARROLLO - INTRODUCCIÓN.
El
entorno de desarrollo viene representado por el icono "Developer
Studio". En él se integran entre otras las siguientes herramientas:
·
Editor orientado a la
codificación C/C++ (resaltando palabras claves …)
·
Compilador/Enlazador
incremental, que acelera el proceso de construcción de los programas.
·
Depurador
visual, que permite visualizar y modificar el contenido de variables y áreas de
memoria.
·
Visor
de datos (browser) que permite fácilmente controlar dependencias y referencias
a funciones, datos, clases, etc. Además permite visualizar la jerarquía de las
clases utilizadas en los programas.
·
Herramientas
complementarias como un analizador de ventanas (Spy ++) o un trazador de
funciones MFC.
1. El concepto
de proyecto
(workspace)
En Visual C++ la
construcción de cualquier tipo de programa
se inscribe dentro del concepto de proyecto
(workspace). Un proyecto define los pasos a seguir para alcanzar la
construcción de un objetivo (un programa,
una DLL, etc.), en realidad es un concepto análogo a lo que se conoce como
"makefile" en otros entornos típicos de desarrollo en C. En realidad,
Visual C++ genera para cada proyecto dos ficheros que lo definen, el fichero de
workspace (con extensión wsp) y un makefile (con extensión mak) estándar que
permitiría la utilización del mismo proyecto en otro entorno distinto.
Desde el punto de
vista funcional, el proyecto contiene referencias a cada uno de los ficheros fuentes
(C/C++, con extensiones c y cpp respectivamente), objetos, librerías o ficheros
de recursos (extensión rc) que se deben utilizar para construir el objetivo
final del proyecto.
En definitiva,
para crear cualquier programa con Visual C++ debemos comenzar creando un
proyecto para él, codificando y añadiendo los módulos necesarios a dicho
proyecto, y definiendo los recursos asociados.
Cuando
se crea un nuevo proyecto (desde la opción "Nuevo" del menú
"Fichero" aparece un diálogo que nos permite especificar que se cree
un nuevo workspace), lo primero que solicita el sistema es determinar el tipo
de objetivo que se persigue con este proyecto. Destacar las siguientes
posibilidades:
·
Aplicación (.EXE)
basada en la MFC (MFC AppWizard)
·
Librería
de enlace dinámico (.DLL) basada en la MFC. (MFC AppWizard)
·
Aplicación
(.EXE) estándar para Windows (basada en el SDK)
·
Librería
de enlace dinámico (.DLL) (basada en el SDK)
·
Aplicación
(.EXE) modelo DOS (Console application)
·
Librería
estática
(.LIB)
Como
ya hemos destacado anteriormente, el objetivo del presente curso es el manejo
de la librería MFC, por lo que nos centraremos básicamente en el primer punto
(la construcción de una DLL con MFC es totalmente similar).
1. EL GENERADOR DE APLICACIONES.
Ya
sabemos que Visual C++, apoyado en la potencia
de la MFC, es capaz de convertirse en un generador de aplicaciones. Para ello
dispone de dos herramientas integradas complementarias:
·
AppWizard, que es el generador de aplicaciones propiamente
dicho. Con él podremos generar esqueletos de programas para Windows basados en
la MFC.
·
ClassWizard, herramienta de mantenimiento
de los programas generados con la anterior. Permite añadir o eliminar clases,
modificar los comportamientos de las mismas, etc.
Pero
el código generado mediante este método presenta una complejidad añadida a la
natural de cualquier programa; junto con el código C/C++ y el de la MFC
aparecen líneas (generalmente entre comentarios) que son totalmente necesarias
para el funcionamiento de las dos herramientas anteriores, modificar cualquiera
de esas líneas de código dará muchos problemas
a la hora de utilizar ClassWizard para modificarlo. De todas maneras, este
"defecto" es bastante popular entre los usuarios de cualquier
generador de código, para cualquier lenguaje.
El
formato general de los proyectos
generados con estas herramientas suele tener las siguientes características:
·
Cada clase de nuestro
programa dispondrá de dos ficheros: Un fichero de cabecera (extensiones .h o
.hpp) y un fichero de implementación (.cpp). El fichero de cabecera contiene la
definición de la clase (definiciones de sus miembros datos y funciones - los
mensajes -), mientras que el fichero fuente contiene la implementación de esas
funciones miembro (los métodos de la clase).
·
Un
fichero de recursos (extensión .rc), aunque éste es opcional.
·
Módulos
objetos (.obj) y librerías estáticas (.lib) necesarias para crear nuestro
programa.
1. CONSTRUCCIÓN DE UNA APLICACIÓN BÁSICA
Seguiremos
los siguientes pasos:
1. Crear un nuevo proyecto. Desde el menú "Fichero", en la opción
"Nuevo".
En
estos momentos aparecerá la secuencia de diálogos del generador ClassWizard.
Veamos cuales serían los pasos a seguir para crear una aplicación sencilla:
1. Paso 1.
Permite identificar el modelo de ventana principal de nuestra aplicación: SDI,
MDI o basada en diálogo. Nosotros elegiremos SDI.
A
partir de este momento da comienzo la generación del código definido antes.
Como se habrá observado, el nombre por defecto de las clases generadas tiene
mucho que ver con el nombre que le hayamos dado al proyecto. De esta manera, si
hubiésemos llamado "curso1" al proyecto tendríamos la siguiente
situación:
·
Clase CCurso1App
(módulos curso1.h y curso1.cpp) que representa una aplicación Windows.
·
Clase CMainFrame
(ficheros mainfrm.h y mainfrm.cpp) que representan la ventana principal de la
aplicación.
·
Clases
CCurso1Doc y CCurso1View (ficheros curso1doc.h/curso1doc.cpp y
curso1view.h/curso1view.cpp respectivamente), representantes de lo que se
conoce en el mundo Windows como interfaz "Documento/Vista" y que
trataremos en adelante.
·
Clase CAboutDlg
que representa el típico diálogo de "Acerca de …" y que ha sido
generado automáticamente por AppWizard, esta clase (rompiendo la norma habitual
de la MFC) aparece definida e implementada dentro los mismos ficheros que la
clase aplicación (módulos curso1.h y curso1.cpp). En el futuro evitaremos este
tipo de construcciones.
Cada
una de estas clases se revisará con detalles en capítulos sucesivos.
El Visor
de Proyectos es una ventana a través de la que podemos visualizar el
contenido de los proyectos,
accedemos a la misma desde el menú "Ver" y la opción "Proyecto".
Está formada por 3 carpetas que nos permiten visualizarlos desde distintos
puntos de vista:
·
ClassView, o Visor de clases, representa el proyecto
con las clases que lo componen. Desde aquí podremos añadir miembros datos
o funciones
fácilmente.
·
ResourceView, o Visor de recursos, que
permite añadir/modificar/eliminar recursos
de la aplicación.
·
FileView, o Visor de ficheros, que
representa el proyecto mediante la lista de ficheros fuente que lo componen.
Por
último aparece también la carpeta InfoView que permite un rápido acceso
a la ayuda en línea.
Cada
una de estas funcionalidades se utilizarán en función de la tarea a realizar.
La gestión
y tratamiento de los recursos
sólo se puede hacer mediante ResourceView, mientras que ClassView
y FileView son casi similares en cuanto a su objetivo
último: el mantenimiento
del código del programa.
1. LA LIBRERÍA DE CLASES MFC - INTRODUCCIÓN.
A
continuación, y siguiendo en todo momento el código de la aplicación básica que
acabamos de generar, procederemos a presentar las clases mas utilizadas de la
MFC. Esta librería está compuesta por cientos de clases distintas, que desde el
punto de vista funcional, las podemos dividir en 4 grupos:
1. Clases orientadas al interfaz de usuario, representan
ventanas, menús, diálogos, etc.
1. Filosofía de trabajo con la MFC.
El
hecho de utilizar una librería de clases como la MFC añade complejidad al desarrollo
e impone una serie de normas
de programación
a las que regirse. La complejidad añadida deriva de la necesidad de que el
programador ahora no sólo debe controlar C/C++, sino que además debe conocer
las clases de la MFC para poder
utilizar su potencia.
Entran
aquí en juego
algunos conceptos que ya conocemos:
·
Herencia, Un buen número de las clases de la MFC son "no
instanciables", es decir, no podemos crear objetos de esa clase con lo
que no podremos utilizarla directamente. Esto significa que el programador
deberá en muchos casos derivar sus propias clases de alguna de la MFC
(estas clases "prohibidas" suelen ser abstracciones que dan
pie a polimorfismos). Además debemos saber que clase es la mas óptima
para que nos sirva de clase base; por ejemplo: se debe saber que la clase MFC CWinApp
es la que da soporte a las aplicaciones Windows,
y que toda aplicación debe derivar su propia versión de la clase para poder
funcionar (en realidad, un programa
MFC comienza con la creación de un objeto de ese tipo y finaliza con la
destrucción del mismo). También deberemos saber que si lo que queremos es
obtener una ventana de diálogo, deberemos derivar nuestra propia clase desde CDialog
(que es una clase especializada para ello), en vez de desde CWnd (que
representa a una clase mas genérica y presentaría muchos problemas),
por ejemplo, CDialog soporta que su plantilla haya sido dibujada en los
recursos de la aplicación (al estilo de un formulario de Visual Basic,
…) mientras que CWnd no, lo que nos obligaría a pintar los botones con código
duro (programar su tamaño, posición en pixeles dentro de la ventana padre …),
aunque no sería imposible. En definitiva, a la hora de derivar clases de la MFC
debemos tener en cuenta el concepto
de especialización para obtener una programación
más sencilla.
·
Polimorfismo. Pero la mayor potencia
de la MFC reside en las funciones
polimórficas (virtuales). El mecanismo de herencia
no sería útil si no se pudiese redefinir el comportamiento
por defecto de las clases de la MFC. Por ejemplo, supongamos que hemos creado
un diálogo (derivando una clase desde CDialog) con nuestro propio diseño,
supongamos que tiene unos campos que deseamos inicializar con los datos de un
empleado para que aparezcan en pantalla, en este caso debemos ser capaces de
definir nuestro propio método
de inicialización. Esto es posible gracias a que este método
(en concreto
denominado OnInitDialog) es definido como virtual dentro de la
clase base CDialog, lo que da lugar a una función polimórfica. Así, si en
nuestra clase derivada de CDialog redefinimos ese método OnInitDialog,
nos aseguraremos que el procedimiento
de inicialización que se ejecutará será el nuestro. En definitiva, las dos
clases (CDialog como clase base y la nuestra como derivada) responden al mismo
mensaje (OnInitDialog) pero de distinta manera (por que no comparten el
método).
Esta
es la filosofía de trabajo que debemos seguir: derivar nuestras clases de la
clase MFC mas especializada en función de lo que queramos, y utilizar las
funciones virtuales que tenga para conseguir el comportamiento
deseado.
1. Las clases básicas para interfaz de usuario.
Para conseguir
una mejor visión del funcionamiento de la MFC, nos basaremos en el código
generado en el punto 5.
1. La clase CWinApp
Representa
una aplicación Windows.
Cada programa debe tener un objeto global o estático (es decir, que exista
durante todo el programa) de una clase propia derivada de CWinApp. De hecho,
otra característica
de la programación basada en MFC es que el código carece de programa principal
(punto inicial de ejecución como la función "main" de C/C++ para DOS,
UNIX,
etc.), la ejecución comienza con la creación del objeto CWinApp.
En
realidad, es la MFC la que provee una función principal estándar WinMain (este
es el nombre que toma para las aplicaciones Windows) que utilizarán todos los programas.
Los pasos básicos que sigue son tres: inicialización del programa, entrada en
el bucle de mensajes y finalización del programa. Veámoslo más en detalle.
·
Inicialización del
programa. Se obtiene en dos fases
distintas: inicialización de la aplicación e inicialización de la
instancia de la aplicación. Para entender esto tenemos que tener en cuenta
que los programas
Windows pueden ser "multi-instanciables", podemos tener varios
ejemplares del mismo programa ejecutándose a la misma vez. Los ejemplos mas
claros son aplicaciones como el bloc de notas (notepad) o cualquier utilidad
de conexión remota (telnet).
En definitiva, si queremos obtener programas multi-instanciables tenemos que
distinguir operaciones
de inicialización que servirían a todos los ejemplares y que por tanto se
deberían realizar una sola vez, de aquellas que son específicas a cada ejemplar
(como crear cada ejemplar su ventana principal).
·
Bucle
de mensajes. Es la
parte central de la ejecución, no finaliza hasta que no se cierra la ventana
principal.
·
Finalización
de la instancia.
Permitirá liberar recursos, cerrar ficheros de trabajo, confirmar operaciones,
etc.
Los
detalles mas importantes de la clase CWinApp son los siguientes:
1. Miembros datos públicos -> Destacar:
o
m_pszAppName, contiene el nombre de la aplicación.
o
m_pMainWnd, contiene un puntero (referencia) a
la ventana principal de la aplicación.
o
m_hInstance, identificador de la instancia del
programa que se está ejecutando.
o
m_hPrevInstance, identificador de la instancia previa
del programa (0 si no hay instancia previa). Sirve para saber si se deben
realizar tareas de "inicialización de aplicación" (ver arriba).
o
m_lpCmdLine, línea de comando con la que se
llamó al programa (argumentos).
1. Constructor CWinApp -> Recibe un sólo parámetro con el nombre de la aplicación (por
defecto llevará NULL, nulo).
o
InitApplication -> Invocada por el sistema
automáticamente cuando se ejecuta la primera instancia de un programa (cuando
no existe instancia previa).
o
InitInstance -> Invocada por el sistema
para cualquier instancia del programa. Se suelen realizar operaciones como la
de creación de la ventana principal, etc.
o
Run -> Que realiza el bucle de
mensajes por defecto. En la mayoría de los casos este bucle es suficiente, con
lo que no tendremos por que redefinirlo.
o
ExitInstance -> Invocada por el sistema
después de cerrarse su ventana principal.
Comparando
el código generado para nuestra primera aplicación con lo expuesto en este
punto podemos comenzar a entender como funcionan las cosas en el mundo MFC.
2. 2 El interfaz Documento/Vista
El
estándar Documento/Vista es una manera de enfocar el desarrollo de aplicaciones
para Windows. Se basa en los siguientes conceptos:
1. Documento, cualquier tipo de información
que se presenta sobre una ventana, podría ser tanto una hoja de cálculo,
como una base de datos
o algún tipo de gráfico.
Figura
1.- Interfaz documento/vista
De
esta manera, cualquier tipo de información
se puede interpretar como un documento, que se presenta al usuario mediante una
vista, que se inscribe dentro de una ventana marco. Mientras la vista se ocupa
solamente de representar el documento, la ventana marco se puede ocupar del
tratamiento del menú.
AppWizard
genera todo su código basándose en este estándar. Una aplicación SDI sólo podrá
tener abierto un documento (la ventana principal es también la ventana marco),
mientras que una MDI podrá tener varios abiertos simultáneamente (aquí la
ventana principal puede contener varias marcos con vistas). Sin embargo este
enfoque no es el mas idóneo para un buen número de aplicaciones Windows SDI
donde la ventana principal no sirve sino como contenedor del menú, y la
representación o el manejo de los datos de la aplicación se suele realizar a
través de diálogos. Si es sin embargo mas útil en el caso de las aplicaciones
MDI donde si que las ventanas marcos se utilizan como formularios
usuales, es decir, como ventanas para presentación de datos.
Figura
2.- Detalle de aplicación MDI
Observando
la figura basada en el entorno de desarrollo de Visual C++,
vemos que la ventana principal contiene dos ventanas marcos, la primera
representa en su vista el contenido del workspace que tenemos abierto, mientras
la segunda está representando un fichero de código fuente CPP.
Las
clases MFC en las que se apoya el interfaz Documento/Vista son las siguientes:
1. Clase CDocument-> funcionalidad básica para documentos
de usuario. Un documento es un objeto que almacena cualquier tipo de datos. Es
una clase abstracta de la que tendremos que derivar nuestros propios documentos.
o
CScrollView, es una vista con soporte a las barras de scroll.
o
CFormView, vista formulario, con controles al
estilo de los diálogos.
o
CEditView, vista orientada a la edición de
textos.
o
CRecordView, vista orientada a la visualización
de bases de datos.
1. Clase CDocTemplate -> Clase abstracta que aporta la funcionalidad básica para
plantillas SDI o MDI. Al ser una clase abstracta no la podremos utilizar
directamente, tendremos que utilizar sus clases derivadas:
CSingleDocTemplate y CMultiDocTemplate.
Toda
aplicación que se base en el Documento/Vista debe registrar en el sistema las
plantillas de documento que soporta antes de poder utilizarlas, lo que se
realiza a través de la función CWinApp::AddDocTemplate. Así, una
aplicación SDI, que debe soportar una única plantilla de documento, debe crear
un objeto CSingleDocTemplate (donde se define la relación entre
documento, vista y ventana marco) y registrarlo mediante el método anterior.
Una vez realizado esto bastará con invocar la función CDocTemplate::OpenDocumentFile()
del objeto recién creado para conseguir que se visualice la ventana principal
de la aplicación, con su vista presentando el documento especificado (el
enfoque en aplicaciones MDI es algo distinto y se revisará posteriormente).
Estas operaciones se introducirán dentro del InitInstance de la
aplicación.
De
todas maneras, en contra de lo que hemos dicho antes, no es difícil adaptar
cualquier aplicación al protocolo
requerido por el interfaz documento vista, sin mucho impacto en la misma,
gracias a que la mayoría de las funcionalidades están predefinidas por defecto.
7. 2. 3. La ventana básica CWnd
Es una de las clases mas
importantes de la MFC ya que contiene las funcionalidades básicas de todas las
clases de ventanas Windows. Como siempre, al ser una abstracción, no está muy
orientada a la utilización directa, mas bien siempre tenderemos a utilizar
alguna de sus clases derivadas, mas especializadas, como CFrameWindow
(ventana principal de aplicación) o CDialog (para ventanas de diálogo).
Pero su principal interés
reside en que es la clase responsable del funcionamiento del mecanismo de paso
de mensajes de Windows. Éste queda oculto por lo que se llama en la MFC mapa
de mensajes. El mapa de mensajes de una clase de ventana recoge la
asociación entre el mensaje recibido del sistema y la función que se ejecutará
como respuesta. Todas estas funciones de respuesta (métodos
de clase) son funciones virtuales que por tanto se pueden redefinir en todas
las clases derivadas.
Supongamos que estamos
construyendo una aplicación la cual queremos que antes de finalizar descargue
cierto contenido de memoria
en un fichero. Para ello deberíamos ser capaces de contestar adecuadamente al
mensaje WM_CLOSE que el sistema manda a la ventana principal cuando se tiene
que cerrar. El mapa de mensajes de la clase CWnd especifica que la función de
respuesta será CWnd::OnClose(). Bastará entonces con que en nuestra
clase ventana principal redefinamos una nueva versión de esa función heredada.
No hace falta que conozcamos de memoria
todo el mapa de mensajes por defecto de CWnd (nombres de las funciones de
respuesta, etc.), la utilización de AppWizard automatiza todo este
complejo tratamiento.
Los miembros datos y
miembros funciones de esta clase son muy numerosos y variados, los iremos
comentando según los vayamos utilizando.
7.
2. 4. Una ventana principal - CFrameWnd
Derivando
de CWnd, proporciona las funcionalidades básicas de cualquier ventana principal
de aplicación. Incluye soporte de menús, barras de estado
y barra de herramientas.
Las aplicaciones SDI derivarán de esta clase sus ventanas principales. Entre
sus características
incluye título, menú de sistema y borde redimensionable.
De
sus miembros funciones destacan:
·
Create -> Función que permite crear una ventana física y asociarla con el
objeto recién creado.
·
LoadFrame -> Función análoga a la
anterior, per de mas alto nivel (exige menos parámetros) que adopta muchos
comportamientos por defecto.
Derivando
de CFrameWnd aparecen otras clases de ventanas mas especializadas, son las
siguientes:
·
CMDIFrameWnd-> Ventana principal para aplicaciones MDI.
·
CMDIChildWnd -> Ventanas MDI hijas (tienen la
mayoría de las propiedades de cualquier ventana principal).
7. 2. 5. Las clases Vista
Como
ya hemos comentado, la clase CView proporciona la funcionalidad básica
para ventanas que se asocian a plantillas de documentos y que realizan tareas
de intermediarias entre el documento y el usuario. Es la responsable de
presentar la imagen
del documento y de interpretar las acciones
del usuario sobre el mismo (modificaciones, etc.). Entre estas funcionalidades
cabe destacar:
·
CView::OnInitialUpdate, función miembro que permite la inicialización de la
vista con los datos del documento.
·
CView::OnUpdate, invocada ante cualquier
modificación del documento, permite actualizar la vista antes de que se
repinte.
·
CView::OnDraw, función de pintado de la vista
(utilizada casi siempre para "documentos gráficos").
·
CView::GetDocument, que permite obtener una referencia
al documento que se esta representando.
Las
vistas mas especializadas se encuentran entre las siguientes clases derivadas:
·
CEditView, donde la vista es una ventana de edición de texto.
·
CListView, donde la vista es una lista.
·
CFormView, donde la vista utiliza una
plantilla de diálogo de los recursos de la aplicación (al estilo de los formularios
tradicionales).
·
CRecordView, derivando de CFormView implementa
además soporte para bases de datos, permitiendo asociar estáticamente campos de
tablas con controles de la plantilla de diálogo que utiliza.
7. 3 Clases básicas de propósito general.
Presentaremos a
continuación algunas de las clases de propósito general mas importantes y
utilizadas de la MFC.
7. 3. 1 La clase CString
La
clase CString representa un string de caracteres. Cualquier programador de
C/C++ sabe lo tedioso que puede ser el manejo de este tipo de dato ya que no
está reconocido como tal en el núcleo de C. Mientras que en otros lenguajes de
alto nivel el string de caracteres es un tipo de dato autónomo que dispone de
sus propios operadores (asignación, concatenación, comparación, etc.), en C/C++
el string se representa como una colección (array) de caracteres (que el tipo
de dato básico), esto supone que cualquier operación sobre ellos se debe
realizar mediante funciones de la librería estándar de C (strcpy, strcat,
strcmp, etc.).
La
principal virtud de CString es precisamente esa, facilita el manejo de strings
de caracteres automatizando todo el tratamiento óptimo de la memoria.
Destacan las siguientes funcionalidades:
·
Función CString::GetLength,
que permite conocer el número de caracteres que contiene.
·
Funciones
CString::GetAt/CString::SetAt, que permiten acceder y modificar respectivamente
el carácter del string que aparece en una posición dada.
·
Funciones
CString::MakeUpper/CString::MakeLower, que permiten convertir a
mayúsculas/minúsculas respectivamente.
·
Función
CString::Find, que permite conocer si un determinado string o carácter
aparece dentro de otro string.
Pero
además, la clase CString tiene sobrecargados algunos de los operadores de C/C++
lo que permite un manejo intuitivo de esta clase, son los siguientes: asignación
(mediante =), concatenación (mediante + y +=) y comparación
(operadores ==, <, <=, etc.).
7. 3. 2. La clase CFile
Esta
clase presenta las funcionalidades básicas para el tratamiento de ficheros,
aportando funciones de alto nivel.
·
Funciones CFile::Open/CFile::Close
para abrir y cerrar ficheros.
·
Funciones
CFile::Read/CFile::Write, para lectura
y escritura
·
Funciones
CFile::Seek/CFile::SeekToBegin/CFile::SeekToEnd que permiten posicionamiento
directo dentro del fichero.
·
Funciones
CFile::GetLenght/CFile::SetLenght que permiten obtener y cambiar el
tamaño del fichero.
·
Función
CFile::Rename/CFile::Remove que permiten renombrar (mover) y eliminar
ficheros respectivamente.
7. 3. 3. La clase CTime
CTime
presenta las funcionalidades básicas de tratamiento de fechas y horas. Al
estilo de como se hace en C/C++ mediante el tipo de dato "time_t",
representa un determinado instante de tiempo
(con fecha, horas, minutos y segundos). Destacan:
·
Función CTime::GetCurrentTime,
que obtiene el instante actual.
·
Funciones
de extracción que permiten acceder a cualquiera de los datos significativos que
componen un CTime: el día del mes ó el día de la semana, el mes, el año, la
hora, etc.
·
Función
CTime::Format, que permite construir un string formateado a partir de un
CTime.
·
Operadores
de asignación, suma, resta y comparación. Como complementaria a esta clase
aparece CTimeSpan que representa un intervalo de tiempo.
Las operaciones de suma se realizan con esta clase, y una resta de dos fechas
dadas siempre da como resultado un CTimeSpan.
7. 3. 4. La clase CRect
CRect
automatiza el tratamiento de rectángulos. En Windows este concepto
se utiliza continuamente ya que las posiciones o el tamaño de las ventanas se
representan mediante los rectángulos que ocupan en la pantalla.
Un
rectángulo viene representado físicamente por las coordenadas de su esquina
superior izquierda y la de su esquina inferior derecha. Estos datos se almacenan
en sus variables
miembro públicas: top, left, bottom y right.
Dispone
de funciones como:
·
CRect::Width/CRect::Height, que permiten saber el ancho/alto de un rectángulo.
·
CRect::PtInRect¸que permite saber si un determinado
punto está contenido dentro de un rectángulo.
7. TRATAMIENTO DEL MENU.
Una
de las principales vías de interacción con el usuario, además de los diálogos,
son los menús. De manera general, cada ventana principal de aplicación contiene
un menú. El método que suele asociar la ventana con el menú es la función CFrameWnd::LoadFrame,
esta recibe como primer parámetro el identificador de los recursos de la
ventana principal. Serían el menú, el icono y el título de la ventana. En suma,
si creamos un menú, un icono y un string todos con el mismo identificador, y
utilizamos éste en la llamada a LoadFrame, la ventana principal aparecerá con
las características deseadas (este es el método que se suele seguir en la MFC).
De
todas maneras, la MFC proporciona otro método alternativo que permite asociar o
cambiar el menú de una ventana. Mediante la función CWnd::SetMenu
podemos realizar también esta operación.
Aunque
en la mayoría de los casos el menú de una ventana es constante durante toda la
duración de la misma, puede que en algún momento necesitemos modificarlo
dinámicamente, para ello se proporciona la clase CMenu que aporta las
funcionalidades básicas para tratamiento de menús y la función de ventana CWnd::GetMenu
que devuelve una referencia al objeto CMenu asociado con el menú de la ventana.
Destacan:
·
CMenu::LoadMenu, que permite asociar un objeto CMenu con un menú
diseñado previamente en los recursos de la aplicación.
·
CMenu::DeleteMenu, que permite eliminar elementos
(items o submenús desplegables - popup -) de un determinado menú.
·
CMenu::AppendMenu, que permite añadir items o
submenús a un menú.
·
CMenu::EnableMenuItem, que permite habilitar e
inhabilitar items de un menú.
·
CMenu::CheckMenuItem, que permite "marcar"
como seleccionados elementos de un menú.
·
CMenu::GetMenuState, que permite saber si un
determinado ítem está habilitado o seleccionado.
Pero
aunque hemos repasado hasta aquí los detalles de como asociar un menú a una
ventana (CFrameWnd::LoadFrame o CWnd::SetMenu) no hemos hecho ningún comentario
de lo mas importante. ¿ Como podemos capturar las selecciones del usuario
dentro del menú y asociarles operaciones concretas ?. Este aspecto lo veremos a
continuación.
7. 1. Gestión de mensajes de comandos
del menú.
Las selecciones
que el usuario realice dentro de un menú se notifican a la ventana propietaria
mediante mensajes de comando (WM_COMMAND). Cada uno de estos mensajes, cuando
son recibidos por una ventana, va acompañado del identificador del ítem de menú
seleccionado de manera que la aplicación pueda reconocer las operaciones a
ejecutar. La forma en que se puede asociar una función de respuesta a un ítem
de menú es bastante gráfica y sencilla gracias al uso de ClassWizard.
Para ello,
bastará con abrir la ventana de ClassWizard (CTRL+W o en el menú
"Ver" con la opción "ClassWizard"), seleccionar la clase
que queremos que controle el mensaje en cuestión, seleccionar el ítem de menú
deseado y especificar el nombre de la función miembro que funcionará como
respuesta a ese mensaje (tras presionar el botón de "Añadir
función"). Desde ese momento, ClassWizard añadirá los ficheros de cabecera
e implementación de la clase seleccionada los datos específicos de la nueva
función definida; el programador será el responsable de completar la
implementación de la función con el tratamiento adecuado.
Una
característica propia de todas las aplicaciones generadas con AppWizard es que
los items de menú que no estén asociados con ninguna función de respuesta
aparecerán inhabilitados.
8. 2. Objetos capaces de gestionar mensajes de comando
de menú.
En la MFC tanto
el objeto aplicación CWinApp como cualquier ventana (objetos CWnd y derivados)
es capaz de recibir mensajes de comando procedentes del menú de la aplicación.
Aunque de alguna forma lo mas "real" es que la ventana sea la
encargada de gestionarlos (ya que es la propietaria del menú y además en la
programación SDK de Windows, la aplicación no tiene entidad como tal y son
solamente las ventanas los elementos capaces de recibir mensajes del sistema,
mediante su ya conocido procedimiento de ventana) si que es verdad que
la elección siempre depende del programador. Todo esto quiere decir que
mediante ClassWizard podemos asociar un comando del menú con una función
miembro de un objeto aplicación.
De cualquier
manera lo que siempre es una práctica aconsejable es centralizar todas las
funciones de respuesta (también conocidas como manejadores de mensajes)
dentro de una misma clase.
Las
ventanas de diálogos son uno de los elementos mas utilizados para la
interacción con el usuario. De manera general un diálogo es una ventana
especial que contiene en su interior ventanas hijas que son controles (botones,
listas, cajas de edición, listas desplegables, …) identificados por un número
único, además, cuando un diálogo se despliega, se convierte en una ventana
exclusiva en el sentido de que deja inhabilitada cualquier operación con el
resto de la aplicación (este efecto se conoce realmente con el nombre de
diálogos modales, aunque existe la posibilidad de crear también diálogos no
modales). Las plantillas de estos diálogos pueden diseñarse mediante herramientas
integradas en el entorno de desarrollo como es el ya conocido ResourceView
(también conocido como taller de recursos).
Es
la clase CDialog, derivada de CWnd, la responsable de proporcionar las
funcionalidades básicas de esta ventana especializada.
·
El constructor de la
clase CDialog::CDialog, requiere dos parámetros: el nombre de la
plantilla de diálogo que utilizará (la que hayamos diseñado en los recursos) y
una referencia a la ventana padre del diálogo (casi siempre es la ventana que
pide que se visualice). También existe un constructor por defecto (sin
parámetros) orientado a la creación de diálogos no modales.
·
La
función CDialog::DoModal, aplicada sobre un objeto construido previamente
permite visualizarlo en modo modal. Esta función es síncrona en el sentido de
que no devuelve el control
a la función llamante hasta que no se cierre el diálogo. Devuelve un valor
que podemos utilizar para indicar distintas opciones de cierre del diálogo (por
ejemplo podemos devolver TRUE para indicar que se presionó
OK o FALSE para Cancelar).
·
La
función CDialog::EndDialog permite finalizar un diálogo modal, recibe un
parámetro que se corresponderá con el valor
que devuelva DoModal.
·
La
función CDialog::Create, aplicada sobre un objeto construido
previamente, permite crear diálogos no modales. Los diálogos no modales no se
finalizan con EndDialog sino que utilizaremos la función estándar heredada de
CWnd DestroyWindow.
·
Función
CDialog::OnInitDialog, para inicializar el diálogo.
·
Funciones
CDialog::OnOk/CDialog::OnCancel que permiten realizar las operaciones de
confirmación o cancelación del objetivo
del diálogo.
Pero
CDialog es una clase abstracta, por lo que no la podremos utilizar
directamente (no se pueden crear objetos CDialog). El programador deberá
derivar sus propias clases desde CDialog para crear sus propios diálogos.
1. Creación de diálogos
Los
pasos para crear una ventana de diálogo son los siguientes:
1. Crear la plantilla de diálogo que se va a utilizar en
el fichero de recursos. Para ello utilizaremos ResourceView y las
herramientas de diseño
de diálogos. El diálogo se podrá completar con los controles necesarios (cajas
de edición, listas, botones, etc.). Cada uno de los controles deberá llevar un
identificador único.
El
sistema genera entonces el esqueleto de la nueva clase, en los ficheros
especificados, añadiéndolos al proyecto. Bastará con completar el código
generado e introducir una llamada al diálogo en algún punto del programa
(mediante la función CDialog::DoModal por ejemplo).
1. Tratamiento de los controles de diálogos.
La
librería MFC aporta también clases representativas de los controles típicos de
Windows. Esto nos permite crearlos de manera dinámica
(mediante código) o manejarlos adecuadamente. De entre estas clases, destacan:
·
CButton, representa cualquier tipo de botones (pushbuttons, radio
buttons o check boxes).
·
CComboBox, representa listas desplegables.
·
CListBox, representa listas.
·
CEdit, representando cajas de edición.
·
CStatic, representando texto
estáticos.
Cada
una de estas clases deriva de CWnd (no en vano son todas ventanas
especializadas) e incorpora funciones específicas que facilitan su tratamiento
(así por ejemplo las listas aportan funciones que permiten saber el elemento
que tiene seleccionado en cada momento).
Aunque
un control puede aparecer dentro de cualquier ventana (se dice que el control
es una ventana hija) es en los diálogos donde mas se utilizan. De manera
general, la ventana padre debe ser capaz de manejar los controles que la forman
(rellenando una lista con las opciones disponibles, presentando el DNI del
empleado seleccionado en la caja de edición al efecto, etc.). Para conseguir
esto disponemos de dos métodos:
1. Asociación estática
de controles. Este método consiste
en la creación de objetos miembros de la clase de la ventana padre (el diálogo)
y su asociación con cada uno de los controles. Es decir, existe una variable
miembro dentro del diálogo que representa cada uno de los controles. De manera
general no se "mapearán" todos los controles a variables
sino sólo aquellos que necesitemos (aquellos que vayamos a manejar en el
programa). Este trabajo se realiza mediante ClassWizard dentro de su
"subcarpeta" marcada como "Variables". Así un botón se
mapea a un objeto CButton, una lista a un objeto CListBox, etc.
1. Mensajes de comando de controles de diálogos.
De manera análoga
a lo que comentamos con los items de menú, los controles informan a las
ventanas padres de las acciones
que el usuario realiza sobre ellos. Así los botones informan de que se hizo
"click" sobre ellos o las cajas de edición lo hacen de que se
modificó su contenido. El mecanismo utilizado es también el mensaje de comando
WM_COMMAND aunque aquí tiene un enfoque adicional.
Mientras los
elementos de menú sólo informan de que han sido seleccionados los controles
pueden informar de distintos eventos
(dependiendo del tipo de control), así siempre es distinto realizar un click
sobre una lista que un doble click. En definitiva, los mensajes de comando de
los controles de diálogos incorporan una nueva información que se conoce como notificación.
Las notificaciones recogidas por cada tipo de control son distintas,
dependiendo de su naturaleza.
La forma de
añadir funciones manejadoras de mensajes de controles a los diálogos es análoga
a la que se describió para los comandos
de menú. Lo realizaremos mediante ClassWizard, pero además de seleccionar el
identificador del control tendremos que seleccionar también la notificación a
la que queremos añadir el nuevo manejador.
Una vez que hemos
visto como obtener un vínculo con los controles que forman un diálogo hemos de
conocer como inicializarlo para que presente al usuario el aspecto
seleccionado. El momento de inicialización del diálogo viene marcado por la
llegada del mensaje WM_INITDIALOG, cuyo manejador virtual es CDialog::OnInitDialog.
En nuestra clase de ventana de diálogo redefiniremos esta función virtual para
definir nuestra propia inicialización.
La
clase CPropertySheet presenta las funcionalidades básicas de los diálogos
basados en pestañas o subcarpetas. En la imagen
se presenta un detalle del diálogo ClassWizard que es de este estilo.
Figura
3.- Detalle de diálogo con subcarpetas.
Estos
diálogos están compuestos por una serie de páginas (subcarpetas), que vienen
representadas en la MFC mediante la clase CPropertySheet. Cada una de estas
páginas es básicamente un diálogo de estilos especiales, pero cuentan con su
plantilla correspondiente en el fichero de recursos del proyecto.
Para
construir diálogos de este tipo seguiremos los siguientes pasos:
1. Generar las páginas necesarias. Para ello necesitamos
diseñar las plantillas de cada una de ellas en los recursos, y añadir una clase
derivada de CPropertyPage que las represente (de manera análoga a como se vio
en el punto 9.1)
7. EL INTERFAZ MDI
El
interfaz de documento múltiple es sensiblemente distinto al ya conocido de
documento simple (SDI) en el que nos hemos basada en las exposiciones previas.
Las aplicaciones MDI pueden tener abiertas simultáneamente varias ventanas
marco, conteniendo vistas que visualizan documentos. Las diferencias
principales son las siguientes:
·
La ventana principal de
la aplicación derivará de CMDIFrameWnd, y su objetivo principal no es
contener una vista como ventana cliente
(según el modelo
SDI) sino controlar y gestionar las ventanas marcos existentes.
·
Como
ventana cliente de la ventana principal aparece la clase de ventana MDICLIENT.
Esta es la verdadera ventana padre de las sucesivas ventanas marco que se vayan
abriendo. La MFC no contempla ninguna clase que represente este tipo de
ventanas ya que su comportamiento por defecto es totalmente suficiente y
estándar. De hecho, a primera vista puede quedar oculta la existencia de esta
ventana cliente ya que en ningún momento necesitamos crearla (la crean las
funciones por defecto).
·
Las
ventanas marcos derivarán de CMDIChildWnd y son "hijas" de la
ventana MDICLIENT de la ventana principal de la aplicación (recordar que en el modelo
SDI la ventana principal era a su vez ventana marco de documento).
De
esta manera, para crear la ventana principal de la aplicación la función InitInstance
invocará a la función CFrameWnd::LoadFrame(), mientras para crear cualquier
ventana marco con su vista y documento tendremos que utilizar las funciones
aportadas por los objetos CMultiDocTemplate (como CDocTemplate::OpenDocumentFile)
que hayamos definido como plantillas soportadas por la aplicación mediante la
función CWinApp::AddDocTemplate (es decir, el mismo tratamiento descrito
arriba para aplicaciones SDI).
Obviamente,
las ventanas principales de este tipo de aplicaciones, derivadas de
CMDIFrameWnd contienen funcionalidades especializadas en la gestión
de las ventanas MDI hijas, destacan las siguientes:
·
CMDIFrameWnd::MDIGetActive, que permite obtener una referencia a la ventana MDI
hija (ventana marco con vista y documento) que está activa.
·
Funciones
como CMDIFrameWnd::MDITile que organiza en mosaico las ventanas MDI
hijas existentes, CMDIFrameWnd::MDICascade que lo hace en cascada o CMDIFrameWnd::MDIIconArrange
que alinea en el área cliente de la ventana principal los iconos de las
ventanas MDI hijas que estén minimizadas.
La
utilización del modelo SDI o MDI dependerá siempre de los requisitos de nuestra
aplicación, de todas maneras el funcionamiento de ambos modelos
es muy similar en su concepción y tratamiento gracias a las abstracciones
prestadas por la MFC.
7. CLASES ORIENTADAS A ACCESO A BASES DE DATOS
La MFC proporciona
clases que proveen un interfaz de alto nivel para el tratamiento de bases de
datos. Para ello se basa en dos mecanismos distintos pero de funcionamiento y
tratamiento muy similar:
1. Tratamiento mediante ODBC
El término ODBC
(Open Data Base Conectivity) indica que su objetivo es conseguir un tratamiento
estándar para distintos sistemas
gestores de bases de datos (SGBD). Su funcionamiento se basa en el uso de
drivers. Estos drivers proveen un interfaz estándar de programación, ocultando
los detalles internos de funcionamiento de cada SGBD. El estándar ODBC está muy
extendido, proveen una librería de funciones C (el API o SDK de ODBC) que se
basa en un SQL
estándar como lenguaje
de consulta utilizado, de manera que podemos acceder con el mismo método a
tablas dBase, Oracle
o Informix (…) sin mas que disponer del driver ODBC apropiado para cada SGBD.
Para utilizar una
base de datos
mediante ODBC entra en juego
lo que se denomina Origen de datos (DSN - Data source name). Un origen
de datos es una entidad identificada por su nombre que especifica una base de
datos concreta, mediante ese DSN podremos acceder a cualquier tabla de dicha
base de datos. La definición de los DSN se consigue gracias al Administrador
ODBC que se suele encontrar dentro del Panel de Control
de Windows (puede que encontremos instalaciones en las que no aparezca
referencias al ODBC ya que es una extensión de Windows no estándar, aunque en
la actualidad es instalado por la mayoría de los SGBD mas comerciales).
El modelo DAO
(Data Access
Object) es una extensión del ODBC que está especializada y optimizada para el
acceso a bases de datos de Microsoft Access.
DAO ha sido incorporado a la MFC en la última versión comercializada, ya que
antes el tratamiento de bases de datos Access
estaba integrado dentro del propio ODBC.
Dado que el funcionamiento
es totalmente análogo (aunque las clases DAO tienen tratamientos y capacidades
específicas) pero siendo ODBC un método mucho mas general, nos centraremos en
el uso de este último para el desarrollo de este capítulo, realizando el
paralelismo oportuno con el modelo DAO cuando sea necesario.
Dos son los
conceptos que se utilizan en el tratamiento de bases de datos mediante MFC: la
base de datos (conjunto de tablas) y el recordset (conjunto de registros
de una tabla). Son representados mediante las clases CDatabase y CRecordset.
1. La clase CDatabase
Presta
las funcionalidades básicas para el tratamiento de bases de datos. Encapsula la
conexión con un DSN definido dentro del administrador
ODBC que nos permitirá operar sobre la base de datos asociada.
·
Función CDatabase::Connect
que permite conectar un objeto CDatabase con un DSN ODBC. Las función CDatabase::Close
permite liberar esa conexión.
·
Funciones
de soporte a transacciones (encapsulamiento de varias operaciones como una
operación atómica) como CDatabase::BeginTrans (marca
comienzo de una transacción), CDatabase::CommitTrans (que cierra y
confirma las operaciones realizadas) o CDatabase::Rollback (que cierra y
anula las operaciones). Obviamente, para que este enfoque funcione el SGBD
asociado al DSN debe ser transaccional, lo que podemos averiguar mediante la
función CDatabase::CanTransact.
·
La
función CDatabase::ExecuteSQL que ejecuta la sentencia SQL
que recibe como parámetro. Con esta función sólo se pueden realizar tareas de mantenimiento
de la base de datos como añadir tablas (CREATE TABLE …), modificarlas (ALTER
TABLE …), eliminarlas (DROP TABLE …), etc. Es decir, al nivel de CDatabase el
SQL se puede utilizar como lenguaje
de mantenimiento de datos, pero no como lenguaje de consulta (sentencias SELECT
…) que se reserva para el objeto CRecordset.
En
la mayoría de los casos el trabajo
se realizará sin la necesidad de trabajar al nivel de CDatabase, ya que la
mayoría de las aplicaciones se limitan a consultar y modificar registros de
tablas, operaciones que se realizan al nivel de CRecordset. Sólo en el caso de
que nuestra aplicación tenga requisitos de mantenimiento de la base de datos
(añadir/modificar/borrar tablas, gestión de permisos, usuarios …) necesitaremos
trabajar al nivel de CDatabase, en cuyo caso bastará con crear un objeto de esa
clase, abrirlo, operar sobre él y cerrarlo; en ningún caso necesitaremos
derivar de CDatabase nuestras propias clases para bases de datos.
El
análogo de CDatabase en el mundo DAO es CDaoDatabase.
1. La clase CRecordset
Representa
un conjunto de registros de una determinada tabla resultado de realizar una
consulta sobre ella (un SELECT del SQL), algo análogo al concepto de cursor, es
decir, siempre se asocia a una tabla de una base de datos. Presenta
restricciones como la imposibilidad de realizar consultas compuestas (registros
de mas de una tabla) o las clásicas uniones de las bases de datos relacionales.
La
clase CRecordset es abstracta de manera que tendremos que derivar
nuestros propios recordsets de ella. Entre sus miembros destacan:
1. Su constructor recibe como parámetro una referencia
al objeto CDatabase al cual pertenece la tabla asociada. Por defecto lleva un
valor nulo lo que significa que el proceso
por defecto construye su propio CDatabase cuando es necesario.
La
clase análoga a CRecordset en el modelo DAO se denomina CDaoRecordset
La
creación de los recordsets necesarios se realizará mediante ClassWizard. Para
ello procederemos a añadir una nueva clase al proyecto, en el momento de
especificar la clase base seleccionaremos CRecordset. Tras confirmar la
operación el sistema nos pedirá que seleccionemos el DSN del origen de datos
ODBC (para lo que debe haber sido definido previamente en el administrador
de ODBC). Una vez realizado esto aparecerá una lista con el nombre de todas las
tablas que forman la base de datos asociada al DSN seleccionado. Escogeremos
una de ellas que se asociará al nuevo objeto recordset. El sistema como siempre
crea un esqueleto de la nueva clase que incluye, entre otras cosas, un
constructor por defecto y la definición de la conexión a la tabla (en la
función CRecordset::GetDefaultConnect). Además, esta clase incorpora un
miembro dato por cada uno de los campos de la tabla asociada, datos que
serviremos para almacenar los valores
del registro actual en cada momento. Estas variables se mapean a cada campo,
relación que se define en el cuerpo de la función CRecordset::DoFieldExchange.
Esta función es implementada por ClassWizard y solo en algunos casos tendremos
que modificarla (los veremos a continuación). Podemos destacar dos tipos de
recordsets distintos:
·
Tipo Snapshot,
donde el recordset es una vista estática del contenido de la tabla en el
momento de su apertura. Cualquier modificación sobre la misma no se reflejaría
en el recordset hasta que no volviese a abrirse.
·
Tipo Dynaset,
donde se representa una visión dinámica de las tablas. Es mas utilizado
aplicaciones que trabajan sobre bases de datos multiusuario o compartidas,
donde otras aplicaciones pueden realizar modificaciones.
Para
utilizar un determinado recordset bastará con crear un objeto de esa clase,
rellenar la información de la consulta en m_strFilter (si no va relleno
se recuperarían todos los registros de la tabla), la información de ordenación
en m_strSort (opcional) y llamar a la función Open que realiza la
consulta apropiada. Cuando el recordset haya dejado de utilizarse invocaremos
la función Close que libera los recursos de la conexión ODBC.
11.3.3 Los recordsets
parametrizados
Uno
de los mayores problemas
que se le han achacado al ODBC ha sido el excesivo tiempo de respuesta (factor
por el que vio la luz
el modelo DAO). Aunque esto siempre ha dependido de la calidad
del driver y del propio SGBD lo cierto es que operaciones de apertura y cerrado
sobre algunas tablas son realmente lentas; esto, como no, se agrava en el caso
de acceso a bases de datos remotas (soportadas por múltiples SGBD’s como SQL Server,
Informix net, etc.) ó cuando se recuperan muchos registros.
Para
solucionar, en alguna medida, este problema aparecen los recordsets
parametrizados. Éstos nos permiten modificar los criterios de selección de
registros de manera dinámica sin necesidad de cerrar y volver a abrir el
recordset. En su lugar se utiliza simplemente la función CRecordset::Requery
que no libera la conexión con el DSN asociado por lo que es sensiblemente mas
rápida.
Para
parametrizar un determinado recordset seguiremos como sigue:
1. Incluir miembros datos dentro de la clase que nos
servirán como parámetros en las consultas. La inicialización de estos nuevos
miembros la realizaremos en el constructor de la clase, así como también
inicializaremos el miembro CRecordset::m_nParams con el número de
parámetros que hayamos definido.
Una
vez parametrizado, podremos modificar los criterios de búsqueda del recordset
de manera dinámica sin mas que modificar el valor de los parámetros y refrescar
el contenido del cursor mediante Requery.
La
utilización de un recordset parametrizado es totalmente análoga a la de uno
estándar. Simplemente basta con rellenar el valor de los parámetros antes de
realizar la apertura de la tabla.
1. Manejo de excepciones mediante CDBException
Cuando
trabajamos con bases de datos se pueden producir situaciones anómalas que
provocan errores de ejecución de las aplicaciones. Por ejemplo, el intento de
abrir una determinada tabla que ha sido abierta previamente por otro usuario de
modo exclusivo, o invocar la función de pasar al siguiente registro cuando ya
se ha alcanzado el último del recordset, son casos que provocarán una
finalización anómala del programa.
El
concepto de excepción permite capturar estas situaciones especiales antes de
que se produzca el error fatal, avisar al usuario y continuar con la ejecución
del programa dentro de lo posible.
De
manera general, una excepción se puede producir dentro del ámbito de cualquier
función sin necesidad de que se estén manejando en ella recordsets o bases de
datos. La clase abstracta CException, representa este concepto. De entre
sus clases derivadas destacan:
·
CMemoryException, para gestión de excepciones producidas por
operaciones directas sobre memoria (new, delete, …).
·
CFileException, para operaciones sobre ficheros
(fichero no existente o de sólo lectura,
etc.)
·
CDBException y CDaoException para
operaciones sobre bases de datos ODBC y DAO respectivamente.
Cada
una de estas clases incorpora funcionalidades propias, pero su utilización es
muy similar, por lo que nos centraremos sobre CDBException que nos ocupa ahora.
La
forma de utilizar las excepciones es como sigue: tenemos que encapsular la
operación que pueda producir la excepción dentro de un bloque TRY/CATCH, que de
manera general tiene el siguiente formato.
TRY
{
Operación que puede
producir excepción …
}
CATCH(Clase de
excepción a capturar, objeto asociado)
{
Operaciones de
recuperación de la excepción …
}
La
rama donde se encuentran las operaciones de recuperación de la excepción sólo
se ejecutarán en el caso de que se produzca una excepción de la clase
especificada en la sentencia CATCH, entonces el objeto que le acompaña tomará
los valores
que identifican dicha excepción. Mas en concreto,
para el caso de CDBException, el ejemplo sería algo así:
CEmpleadosSet e;
e.m_strFilter =
"DEPARTAMENTO = ‘VENTAS’";
TRY
{
e.Open();
}
CATCH(CDBException, e)
{
…
}
END_CATCH
Autor: Fco. Javier Rivilla Lizano
Level data, S.A. - Septiembre 1.997
Trabajo enviado por:
Coria David Marcelo
coriadavid@yahoo.com