Apunte de Visual C++

 

Por: Demian Panello        demianpanello@yahoo.com.ar

      

Capítulo XI

Indice rápido del capítulo 11:

 

APLICACIONES SDI, (SINGLE DOCUMENT INTERFACE):

Como habrá visto mientras utiliza algunos programas de Windows, existen notables diferencias entre las aplicaciones que venimos haciendo y esos programas. Ejemplo: el Wordpad es bien diferente, por lo menos en aspecto, a los ejemplos que he presentado hasta ahora, y más diferente es aún Excel.

Básicamente existen 3 tipos de aplicaciones Windows:

 

- Basadas en diálogos: que es lo que hemos hecho hasta ahora. Simplemente ventanas.

- SDI, (Single Document Interface): Aplicaciones de interfaz de un único documento. Dentro de esta categoría está por ejemplo el Wordpad y el Paint.

- MDI, (Múltiple Document Interface): Aplicaciones de interfaz de múltiples documentos. Excel, FrontPage, etc.

 

En una aplicación SDI hasta que no se guarde el trabajo no se puede abrir otro documento. Por ejemplo en Worpad hasta que no se guarde, o se cancele, lo que se está escribiendo, no se puede abrir uno nuevo o uno existente.

 

Arquitectura Documento - Vista:

 

En una aplicación SDI uno tiene la información, (documento), y diferentes formas de presentarla, (vista). Todo esto contenido de alguna forma, (marco).

Bien, cuando uno crea con el ApplicationWizard una aplicación SDI se generan 3 clase muy importantes, además de la clase para la aplicación y para el diálogo Acerca de.

Estas 3 clases son derivadas de CDocument, CView y CFrameWnd.

 

CDocument: La clase CDocument provee la funcionalidad básica para clases documentos definidas por el usuario. Un documento representa la información que por lo general el usuario manipula cuando abre o guarda un archivo utilizando el menú Archivo.

 

CView: La clase CView provee la funcionalidad básica para las clases vistas definidas por el usuario. Una vista es la intermediaria entre el documento y el usuario. La vista se encarga de representar la información contenida en el documento.

Existen varias vistas derivadas de CView:

 

- CListView.

- CCtrlView.

- CDaoRecordView.

- CEditView.

- CRecordView.

- CFormView.

- CRichEditView.

- CScrollView.

- CTreeView.

 

(para más información sobre las vistas vea la ayuda de Visual C++).

 

CFrameWnd: Esta clase es la esencia de una aplicación SDI. Es la ventana contenedora de la vista y el documento.

 

Bueno, ahora vamos a crear una aplicación SDI. La aplicación tendrá como objetivo algo parecido a lo que hicimos en el capítulo anterior. Se dibujarán líneas con sólo pulsar el botón izquierdo del mouse y con el derecho se limpia la pantalla.

 

Ingrese a Visual C++, vaya al menú FILE - NEW y en la solapa PROJECTS seleccione MFC AppWizard (exe). En Project Name escriba "lineas2", pulse OK.

En el siguiente paso seleccione SINGLE DOCUMENT.

 

 

 

Pulse NEXT. Deje como están por defecto las opciones de las siguientes dos pantallas, hasta que llegue al paso 4, allí pulse el botón ADVANCED:

 

 

En esta ventana le decimos a AppWizard que extensión tendrán los archivos que manipule nuestra aplicación, (cuando ésta pueda grabar y abrir). Entonces en File extension escriba "lin", esta será la extensión de nuestros archivos. Pulse Close, la ventana del paso 4 indica que tendrá por defecto nuestra aplicación SDI, como por ejemplo "barra de estado", "barra de herramientas", etc. Esa opciones déjelas como están por defecto y pulse FINISH.

 

AppWizard creará el armazón de nuestra primer aplicación SDI, que es totalmente funcional, puede si lo desea probar el programa pulsando CTRL-F5 y verá que funciona, aunque aún no pasa nada si se pulsa algún elemento del menú.

 

 

 

Ahora analizaremos un poco que es lo que AppWizard nos generó automáticamente.

Mire algo curioso; vaya a la solapa ResourceView de la ventana Workspace y expanda la carpeta Dialog. ¿Donde está el diseño de la aplicación?, esa ventana con menú que aparece cuando ejecutó la aplicación, (imagen sobre estas líneas). En la carpeta Dialog solamente encontrará el diseño del diálogo Acerca de, y solamente eso.

Lo que ocurre es que la ventana con el menú, la barra de herramientas y la barra de estado que UD ve cuando ejecuta el programa es la ventana marco creada por la clase CFrameWnd, (siga leyendo que más abajo se aclara todo). La región en blanco es la vista donde se mostrará la información del documento, en nuestro caso esa información serán líneas.

 

Ahora seleccione la solapa ClassView de la ventana Workspace allí verá las clases que forman nuestra aplicación:

 

 

 

CAboutDlg: Es la clase del diálogo Acerca de.

CLineas2App: Es la clase aplicación presente en todos los programas, sean Basados en diálogos, SDI o MDI.

CLineas2Doc: Es la clase documento, la cual se encarga de manejar los datos, (la información), del programa.

CLineas2View: Es la clase vista que se encargará de mostrar la información en pantalla o en impresora.

CMainFrame: Es la ventana - marco, contenedora de la vista.

 

Nuestros datos, o información, en el documento serán una lista de puntos, (pares x, y), de acuerdo a las pulsaciones del botón izquierdo del mouse en diferentes lugares de la vista. Por ejemplo: si el usuario pulsa 3 veces el botón izquierdo del mouse en diferentes lugares de la vista se obtendría dos líneas uniendo esos puntos, lo que implica que son 3 pares x,y, y esos pares son nuestra información y los guardamos en una lista dinámica, (CList), como dato público perteneciente a nuestra clase documento.

El dibujo de las líneas estará a cargo de la vista, o sea, el código que dibuja las líneas y va guardando los pares x, y, se escribirá en el mensaje WM_LBUTTONDOWN de la vista.

 

Entonces lo primero que vamos a hacer es agregar ese dato miembro de tipo CList en la definición de la clase CLineas2Doc.

Pulse dos veces donde dice CLineas2Doc en ClassView, se editará la definición de la clase: (a continuación se transcribe el principio de la clase).

 

class CLineas2Doc : public CDocument
{
protected: // create from serialization only
CLineas2Doc();
DECLARE_DYNCREATE(CLineas2Doc)

// Attributes
public:
        CList<CPoint, CPoint> m_ListaPuntos;        //Esta línea declara una variable de tipo CList.
// Operations
public:

// Overrides
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CLineas2Doc)
public:

....

 

}

 

Marcada en amarillo se encuentra la declaración de la variable CList.

La clase CList permite crear objetos que se comportan como las listas doblemente enlazadas de C. Para decirlo de forma sencilla, son listas dinámicas en las cuales uno puede ir agregando elementos sin estar limitados por una cantidad fija a almacenar. Imagine que en lugar de una variable CList que permita almacenar variables tipo CPoint hubiéramos declarado una array, (vector), de por ejemplo 50 posiciones de tipo CPoint. Estaríamos limitados a guardar sólo 50 puntos. Esta, obviamente no es la idea, por eso se usa la clase CList cuya definición se encuentra en el archivos "afxtempl.h" el que tendremos que incluir en el archivo de cabecera StdAfx.h. Entonces en la solapa FILES de la ventana Workspace expanda la carpeta Header Files y pulse dos veces sobre StdAfx.h.

Agregue luego de la última sentencia #include la siguiente línea: #include "afxtempl.h".

La definición de CList es la siguiente:

 

template< class TYPE, class ARG_TYPE >
class CList : public CObject

 

TYPE: Es el tipo de dato, (clase), que guardará la lista. En nuestro ejemplo es CPoint.

ARG_TYPE: Es el tipo usado para referenciar los objetos de la lista. Habitualmente este argumento es igual que el anterior.

 

Los valores de la lista se obtienen por medio de una variable de tipo POSITION.

Esta lista nos permitirá reconstruir nuestro dibujo, (simples trazados de líneas), cuando por ejemplo tenemos que redibujar la vista en caso que se minimice la aplicación u otra ventana se superponga sobre la nuestra.

 

El siguiente paso es crear una variable miembro en el documento. Esta variable será de tipo BOOL y no servirá para saber si el usuario a pulsado por primera vez el mouse, lo que implica que está comenzando a dibujar líneas. 

Esta variable le daremos el valor TRUE cada vez que se inicia un nuevo documento, puesto que el mismo está en blanco, disponible para dibujar. Cuando pulse por primera vez el botón del mouse sobre nuestra vista estableceremos su valor a FALSE para saber que ya se está dibujando.

 

Para crear la variable pulse con el botón derecho del mouse, en ClassView, sobre la clase CLineas2Doc y luego seleccione ADD MEMBER VARIABLE.

En el cuadro de diálogo que aparece escriba en Variable Type: BOOL y en Variable Name: m_Nuevo. Acepte este diálogo.

 

El valor inicial de esta variable se lo especificaremos cada vez que se crea un nuevo documento, esto es el mensaje OnNewDocument() de nuestro documento. Entonces pulse dos veces sobre OnNewDocument() y se editará la función:

 

BOOL CLineas2Doc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;

// TODO: add reinitialization code here
// (SDI documents will reuse this document)


m_Nuevo=TRUE;        //Aquí se inicializa la variable.
return TRUE;
}

 

Por ahora este es el código necesario que le incumbe a nuestro documento.

Como la vista es la encargada de mostrar los datos y sirve de intermediaria entre el usuario y los datos que se almacenan en el documento, será en ella, (la vista), donde escribiremos el resto del código de nuestra aplicación. ¿Qué nos falta programar?.

Hay que escribir el código que permita al pulsar el botón izquierdo del mouse, que se guarden en la lista los puntos donde se hizo click y además dibujar una línea. También escribiremos código para el botón derecho del mouse, ya que queremos limpiar la vista, (borrar todas las líneas).

 

Un documento puede tener varias vistas, por ejemplo: Excel muestra la información en las celdas de una rejilla o bien en un gráfico. Estas son dos vistas diferentes de un mismo documento.

Por otro lado hay que decir que para una vista existe solamente un solo documento.

En nuestro caso tenemos sólo una vista para nuestro documento, y es la vista por defecto, (la misma que usa WordPad o Paint).

 

Como se habrá percatado, será necesario acceder a los datos del documento desde la vista, para poder representarlos o trabajar con ellos.

Existe una función miembro de la vista que nos permite acceder a los datos del documento.

Expanda en ClassView la clase CLineas2View y se encontrará con una función miembro llamada GetDocument(); pulse dos veces en ella para poder ver su definición:

 

CLineas2Doc* CLineas2View::GetDocument() // non-debug version is inline
{
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CLineas2Doc)));
return (CLineas2Doc*)m_pDocument;
}

 

Esta función, mientras nuestra aplicación sea de versión DEBUG, estará fuera de la definición de la clase CLineas2View, pero si es versión RELEASE pasará a ser una función InLine, o sea estará definida dentro de la misma clase.

 

Como podrá ver en el encabezado y en la sentencia Return, la función retorna un puntero a nuestro documento, y precisamente a través de este puntero accederemos a los datos del documento.

La función es muy cortita, (por eso en la versión RELEASE será InLine, pues simplemente retorna una variable), la única línea que vale la pena explicar un poco es:

 

ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CLineas2Doc)));

 

En "cristiano" esta sentencia dice:

 

"Continúa en caso que la variable m_pDocument se una instancia de la clase CLineas2Doc".

 

La variable m_pDocument es una variable miembro protegida de la clase CView y como CLineas2View es una clase derivada de CView, por herencia, también es miembro de ella. Esta variable es un puntero a un objeto de la clase CDocument, (nuevamente por cuestiones de herencia, CLineas2Document que es derivada de CDocument, es la clase apuntada).

Esa línea existe por prudencia, para controlar cualquier error inesperado, pues ASSERT evalúa la expresión que tiene como parámetro y en caso de ser 0, o sea que m_pDocument no sea una instancia de CLineas2Doc, (eso es lo que hace la función IsKindOf()), envía un mensaje de error y salta este procedimiento.

Bien, con esta función accederemos a los datos del documentos desde la vista, (un poco más adelante ya haremos uso de la misma).

 

Ahora agregaremos un variable miembro a la vista que nos permita guardar el punto anterior donde fue pulsado el mouse, (el último punto), ya que necesitaremos 2 ptos. para poder trazar la línea.

Pulse con el botón derecho del mouse en ClassView sobre la clase CLineas2View y elija la opción Add Member Variable del menú contextual.

Aparecerá una ventana que nos pedirá el tipo de variable, allí escriba CPoint y el nombre de la variable, escriba: m_PtoAnterior.

Tilde la opción Protected, ya que esta variable será una variable protegida de la clase CLineas2View. Sólo tienen acceso a las variables protegidas de una clase las derivadas de esta, en nuestro caso solamente sirve para, por las dudas, "proteger" esta información, pero no derivaremos ninguna clase nueva.

 

Agregar mensaje WM_LBUTTONDOWN a la vista:

 

- Pulse Ctrl+W para abrir ClassWizard, vamos a agregar el mensaje WM_LBUTTONDOWN a nuestra vista.

- En ClassWizard seleccione CLineas2View en la lista de clases Class Name.

- En Object ID's verifique que se encuentre seleccionada la clase CLineas2View y en Messages seleccione WM_LBUTTONDOWN.

- Pulse Add Function y luego Edit Code.

 

Allí escriba:

 

void CLineas2View::OnLButtonDown(UINT nFlags, CPoint point)
{
    // TODO: Add your message handler code here and/or call default
    CLineas2Doc *miDoc;    (1)

    miDoc=GetDocument();  (2)         //Tomo el documento desde la vista.

    if (miDoc->m_Nuevo)    //Si es la primera vez que se pulsa en la vista
    {
        m_PtoAnterior=point;  (3)            //Guardo el punto donde se pulsó.
        miDoc->m_ListaPuntos.AddTail(point); (4)            //Lo agrego a la lista
        miDoc->SetModifiedFlag(TRUE);  (5)                    //Se está modificando.
        miDoc->m_Nuevo =FALSE;            
    }
    else
    {
        CClientDC dlgDC(this);   (6)
        CPen Lapiz;                     
        CPen *oldLapiz;
       
        Lapiz.CreatePen (PS_SOLID,2,RGB(0,0,0));    (7)
        oldLapiz=dlgDC.SelectObject(&Lapiz);               (8)
        dlgDC.MoveTo (m_PtoAnterior);                          (9)  
        dlgDC.LineTo (point);                                            (10)
        dlgDC.SelectObject (oldLapiz);                             (11)
        miDoc->m_ListaPuntos.AddTail(point);               (12)
        m_PtoAnterior=point;                                             (13)
    }

        CView::OnLButtonDown(nFlags, point);
}

 

 

Cuando se pulsa el botón izquierdo del mouse pueden ocurrir dos cosas:

 

1- se pulsó por primera vez.

2- ya se pulsó anteriormente.

 

1 - Si se pulsó el mouse por primera vez, nuestra variable m_Nuevo aún tiene el valor TRUE, entonces simplemente debemos guardar el punto donde se pulsó el mouse como PtoAnterior y en la lista de puntos, y establecer además el valor de m_Nuevo a FALSE.

Eso es lo que se hace en el bloque verdadero de la sentencia if (m_Nuevo).

Antes, en la línea (1) se declara un puntero a nuestra clase documento y en (2) por medio de la función miembro GetDocument() de la vista, se obtiene el valor necesario que hace referencia a el documento.

En (3) se guarda el punto como punto anterior, (note que point es el parámetro pasado a el mensaje OnLButtonDown() que precisamente trae las coordenadas donde se pulsó el mouse). En (4) se lo agrega a la lista con el método de la lista AddTail().

Y como el hecho de pulsar el mouse en la vista supone posibles cambios en el documento, en (5) se establece con la función miembro del documento, SetModifiedFlag(), que se están realizando modificaciones lo que luego dará a lugar, si intentamos salir de la aplicación, que pregunte si deseamos realizar los cambios en el documento.

Se establece m_Nuevo = FALSE, porque la siguiente pulsación no será la primera.

 

2 - Si m_Nuevo no es igual a TRUE es porque ya una vez se pulsó el mouse, entonces ahora hay que dibujar la línea entre el punto anterior, m_PtoAnterior y el nuevo punto, o sea point. Además guardar como punto anterior el nuevo punto, point y agregarlo a la lista.

Para poder dibujar se crea en (6) un contexto de dispositivo para nuestro diálogo, (en realidad afecta sólo a la vista), y además se declaran objetos para el lápiz en uso y el nuevo.

En (7) se crea un lápiz de trazo sólido, ancho 2 y de color negro el cual se selecciona como activo en (8) guardando el lápiz anterior.

Para poder trazar la línea se mueve el cursor gráfico al punto anterior, (9) y desde allí se realiza el línea hasta el punto actual (10).

Se selecciona el lápiz anterior (11), se guarda el punto actual en la lista (12) y como punto anterior (13).

 

Pulse CTRL + F5 para probar la aplicación y verá que puede trazar líneas en la vista:

 

 

 

(las diferencias de las capturas de pantallas se deben a que cambié el Sistema Operativo a Windows XP).

 

 

Si minimiza la aplicación o superpone otra ventana verá que se borrarán las líneas, obviamente eso no debe ocurrir, por lo tanto deberemos escribir código en el mensaje WM_PAINT el cual es gestionado por la función OnDraw() de la vista.

Allí haremos uso de la lista de puntos pues como tenemos que reconstruir el dibujo, ya que se borraron las líneas, recorreremos toda la lista para ir trazando las líneas con los puntos de la misma.

Entonces, pulse dos veces en ClassView sobre la función OnDraw() de la vista, y escriba:

 

void CLineas2View::OnDraw(CDC* pDC)
{
    CLineas2Doc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);

    POSITION pos;    (1)
    CPen Lapiz;
    CPen *oldLapiz;

    // TODO: add draw code for native data here
    if (pDoc->m_ListaPuntos.IsEmpty ())    (2)      //si la lista está vacía retorno, pues no hay nada que dibujar.
        return;
   
    Lapiz.CreatePen(PS_SOLID,2,RGB(0,0,0));
    oldLapiz=pDC->SelectObject(&Lapiz);
    pos=pDoc->m_ListaPuntos.GetHeadPosition();  (3)   //se obtiene el 1º elemento de la lista
    pDC->MoveTo(pDoc->m_ListaPuntos.GetNext(pos));    //muevo el cursor gráfico a ese punto.
    while (pos!=NULL)                       //mientras no sea el fin de la lista
        pDC->LineTo(pDoc->m_ListaPuntos.GetNext(pos)); (4)

    pDC->SelectObject(oldLapiz);
   
}

 

Como el dibujo se borró parcialmente o en su totalidad porque se superpuso otra ventana o se minimizó la aplicación, debemos reconstruir el dibujo haciendo uso de los puntos que fuimos guardando en la lista.

Los valores almacenados en una lista se obtienen con funciones propias de clase CList junto con una variable de tipo POSITION, que es como una especie de índice, (no es un índice, es un puntero a los elementos de la lista, pero en realidad funciona de forma automática en VC++ ).

Por eso en (1) se declara una variable de tipo POSITION además de los lápices que necesitaremos para dibujar, (el contexto de dispositivo no hace falta crearlo pues en esta función es un parámetro).

Si la lista está vacía (2), es porque no hay nada que dibujar, por lo tanto retornamos.

Creamos y seleccionamos el lápiz a usar y en (3) obtenemos la dirección del primer elemento, (es lo que se almacena en pos) por medio de la función GetHeadPosition() e inmediatamente movemos el cursor gráfico a el valor almacenado allí, GetNext().

A partir de aquí hacemos un bucle, mientras la variable pos no sea nula pues esto ocurre cuando ya no hay más elementos en la lista. Dentro del bucle dibujamos la línea desde donde se encuentra el cursor gráfico hasta el siguiente punto almacenado(4). Cuando se dibuja la línea el cursor gráfico se mueve automáticamente al otro extremo de la misma quedando así preparado para el siguiente trazo.

 

Para terminar este primer ejemplo de aplicación SDI, nos falta escribir el código que al pulsar el botón derecho del mouse sobre la vista, permita borrar el dibujo.

Agregue entonces con ClassWizard el mensaje WM_RBUTTONDOWN y allí escriba:

 

void CLineas2View::OnRButtonDown(UINT nFlags, CPoint point)
{
    // TODO: Add your message handler code here and/or call default
    CRect miRect;
    CString Mensaje;

    Mensaje="¿Desea borrar todo el contenido del documento?.\n";
    Mensaje=Mensaje + "No podrá recuperar la información.";

    if (!GetDocument()->m_Nuevo)      (1)
        if (MessageBox(Mensaje,"Borrar",MB_YESNO+MB_ICONQUESTION)==IDYES)     (2)
        {
            GetClientRect(&miRect);    (3)
            GetDocument()->m_ListaPuntos.RemoveAll();      (4)
            RedrawWindow(miRect,NULL,RDW_INVALIDATE | RDW_UPDATENOW | RDW_ERASE); (5)
   
            GetDocument()->m_Nuevo =TRUE;    (6)
            GetDocument()->SetModifiedFlag (FALSE);  (7)
        }

    CView::OnRButtonDown(nFlags, point);
}

 

En (1) averiguamos si la variable m_Nuevo no tiene TRUE, lo que implica que ya hay algo dibujado, entonces por medio de la función MessageBox(), (2), preguntamos si realmente se desea borrar las líneas. En caso que conteste que SI, obtenemos las dimensiones de la vista y las almacenamos en el objeto miRect (3).

Borramos todo el contenido de la lista (4) y llamamos a la función RedrawWindow() pasándole como parámetro las dimensiones de la región a borrar, miRect, y además las opciones típicas RDW_INVALIDATE | RDW_UPDATENOW | RDW_ERASE (5).

En (6) le asignamos a la variable m_Nuevo del documento el valor TRUE, para así puede empezar todo de vuelta.

Y finalmente, aunque esto es optativo, se le pasa el valor FALSE a la función SetModifiedFlag() para que no pregunte si se desea guardar la información en caso de pulsar el icono de guardar o de cerrar la aplicación, porque se supone que un documento en blanco no se va a guardar.

Note que en lugar de guardar el retorno de GetDocument() directamente se accede al documento escribiendo GetDocument()-> , cualquiera de las dos formas son válidas, usando un puntero, (el retorno de GetDocument()) o la propia función.

 

Por ahora esto es todo. Esta es una aplicación SDI sencilla y lo que vió hasta ahora es como y cuando escribir código en el documento y cuando en la vista.

 

 

Resumiendo:

            - Clase aplicación.

            - Clase Marco.

            - Clase Vista.

            - Clase Documento.

   

Descargar archivos fuentes del ejemplo: lineas2.zip 

 

Capítulo siguiente (12)

 

Capítulo anterior (10)

 

Página principal