el Guille, la Web del Visual Basic, C#, .NET y más...

Documento/Vista, ventanas partidas y cuadros de diálogo

 

Autor: RFOG (MVP de Visual C++)
Publicado: 03/Jul/2008
Actualizado: 03/Jul/2008

Cómo aprovechar la arquitectura documento/vista de MFC sin tener que pelearnos con sus complejidades, y de paso cómo construir un cuadro de diálogo que pueda cambiar su tamaño de forma dinámica y reorganice sus controles automáticamente.




 

Introducción:

El modelo de objetos en MFC casi exige utilizar la arquitectura Documento/Vista excepto para los cuadros de diálogo cliente como ventana principal. Realmente no estás forzado a utilizarlo, pero si lo haces las cosas se simplifican (aunque se compliquen por otro lado) o al menos permiten hacer cosas que sin Doc/Vista tendrías que utilizar MFC como si fuera Win32, es decir, tendrías que hacértelo tu todo a mano, o casi.

Las batallitas

Esta entrada tiene su historia, y como a Guille le gustan las historias, vamos a contar una. Cuando en mi trabajo se decide hacer algún tipo de aplicación, mi jefe solo dice: quiero un programa que haga esto o aquello, y lo demás corre de mi cuenta. Pues bien, en mi último desarrollo decidí darle caña de una vez a MFC (porque tenía tiempo para estudiar dentro de los plazos de entrega), por lo que le dije a Guille: pronto tendrás una entrada sobre la Ribbon en mi sección.

Pero como Murphy anda por todos lados, nada más empezar me di un buen frentazo: cinco días para saber cómo hacer una aplicación SDI con una ventana partida y que encima no funcionaba bien del todo, o mejor dicho: no funcionaba nada bien.

Puesto en contacto con las altas esferas, nadie pudo decirme nada coherente, pero como buenos profesionales que son, lo que me dijeron me fue sirviendo para delimitar el problema, pero cinco días son muchos días para haberlos perdido en una tarea así, por lo que me decidí a hacer la aplicación en C# y .NET. Al menos no tendría problemas con la UI (tendría otros relacionados con el puñetero Interop).

Finalmente, el otro día, haciendo una búsqueda para otros menesteres, encontré de refilón la solución, pero como no quiero adelantar la cosa, vamos por partes (y habrán supuesto bien, se trata de un bug de uno de los asistentes de MFC). Y la entrada para la Ribbon y el Feature Pack la dejaremos para más adelante.

Doc/Vista o no Doc/Vista

La primera decisión importante es decidir si nos vamos a basar en este modelo o no. Si lo hacemos la ventaja es que tenemos ya mucho código preconstruido dentro de MFC. El inconveniente es que el tema es bastante complejillo de entender… Por otro lado, si no lo usamos tendremos que hacérnoslo nosotros todo a mano casi como si estuviéramos en Win32.

Pero entonces viene el truco del almendruco, que no es mío, sino de David Ching y de Tom Serface y con el cual se mostraron de acuerdo otros MVP que entraron en el tema: usa el modelo pero no lo uses. La solución es una de esas cosas evidentes por sí mismas que una vez que te las han dicho hacen que te parezcas tonto a ti mismo, no digamos ya a los demás...

A lo que me refiero es que, en el asistente, elijas usar la arquitectura pero luego te centres por completo en la parte de la Vista e ignores la parte del Documento. Sencillo, genial, evidente por sí mismo, pero yo estaba cegado porque pensaba que “Doc” tenía que ver con “documento de disco” –aunque intelectualmente supiera que no es más que un modelo arquitectónico.

Otra idea, esta de Tom Serface fue la de que usara el modelo MDI en lugar del SDI, que sólo permitiera una ventana y que la maximizara en su creación, así seguro que cuando el jefe dijera “¿no se podrían abrir más de estas?” tu, en lugar de acordarte de ciertos parientes, sonrieras y dijeras: en treinta segundos los tienes. Pero no vamos a usar este acercamiento aquí.

Lo que hace la experiencia.

Splitter o no splitter

Mi idea, ya que estaba en el ajo, era la de empezar a construir de alguna forma un sistema similar a WindowsForms pero en C++ y MFC. Es decir, poder alojar controles dentro de una ventana normal y que estos se anclaran y funcionaran de forma similar a WindowsForms, heredando una nueva generación de clases MFC con esta funcionalidad, pero al final decidí ser algo menos ambicioso y resolví hacerme una ventana partida con un cuadro de diálogo a la derecha y un control de lista a la izquierda, aunque la idea me sigue rondando por mi cabezón y ciertamente sería factible hacerla.

Pero una ventana con un separador deslizable necesita que el cuadro de diálogo de la derecha pueda redimensionarse tal y como lo hace una ficha de Windows Forms, así que también hemos implementado algo así.

Ahora, al tajo.

El modelo Doc/Vista explicado

Una aplicación MFC estándar cuenta con un objeto global derivado de CWinApp o CWinAppEx que representa a la aplicación en sí misma. Dentro del WinMain oculto que tiene MFC se instancia nuestra clase derivada, y los ficheros que definen dicha clase tienen el nombre del proyecto que hayamos creado.

Esta Aplicación contiene un objeto Marco derivado de CFrameWnd o CFrameWndEx que para entendernos es lo que da consistencia, hueco y soporte para las ventanas de la aplicación, ya sea la principal, ya sean las barras de herramientas, etc. La implementación se encuentra en MainFrm.cpp/.h en cualquier proyecto generado con el asistente. Dentro de esta clase es de donde parten las demás ventanas hijas.

El Documento se controla a nivel de aplicación, es decir, es la aplicación la que contiene al marco y al documento. El asistente siempre nos añadirá “Doc” al nombre del fichero que contiene su implementación a partir del nombre del proyecto.

Luego, para cada Vista, es decir, para cada “ficha” que sea una vista del documento se crea alguna clase derivada de CView o de sus hijos. Las fichas son manejadas por el marco y los objetos que la representan están contenidos en ella, y las vistas obtienen sus datos del documento a través del método GetDocument(), pero como nosotros no vamos a usar la parte Doc, ignoraremos este comportamiento.

Las vistas se crean a nivel de marco, es decir, es el marco el que va a crearlas, y encima la forma predeterminada es que ni siquiera se haga con código nuestro, sino que serán las tripillas de MFC las que, mediante RTTI, obtengan la vista adecuada y la creen.

Clases Vista

Hemos dicho que una clase vista hereda de CView, pero hacerlo desde esta clase no añade absolutamente ninguna funcionalidad. Las clases hijas permiten que nuestra ventana sea un árbol (como el del explorador de Windows), un control de lista, un control de edición (ya sea de texto como RTF) e incluso un cuadro de diálogo; también hay una clase de la cual nosotros podemos heredar nuevo comportamiento totalmente personalizado.

Pero nosotros queremos tener a la izquierda una ventana que sea un control de lista y a la derecha un cuadro de diálogo.

Tenemos que heredar, pues, dos nuevas clases. Una de ellas será hija de CListView y la otra de CFormView. La mejor forma es hacerlo con el asistente de Visual Studio. Lo hagamos como lo hagamos, debemos tener en cuenta que nuestras clases han de implementar RTTI para que luego el gestor de doc/view sea capaz de crearlas dinámicamente. En MFC esto se consigue de forma automática si nuestras clases incluyen la macro DECLARE_DYNCREATE() en la definición de la clase e IMPLEMENT_DYNCREATE() en algún lugar del código fuente.

Crear una clase que herede de CListView es trivial. Crear una clase heredada CFormView requiere que creemos un cuadro de diálogo cuyo ID vendrá asociado en el constructor de nuestra clase. La figura siguiente recoge el nuestro de demo, que no es más que un listbox y un botón normal y corriente.

Figura 1
Figura 1

Creando la ventana partida

Una vez que tenemos las dos clases que van a formar parte de la ventana principal, tenemos que crearlas a mano pero de forma indirecta, a través de un objeto del tipo CSplitterWnd o CSplitterWndEx. Como el controlador de las ventanas hijas, ya lo hemos dicho, es el objeto marco, es ahí donde se deben crear, pero no donde queramos, sino sobrescribiendo el método OnCreateClient() de CMainFrame.

Y aquí es donde viene el problema, ya que si creamos dicho método con el asistente, nos lo generará así:

BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
    // TODO: Add your specialized code here and/or call the base class

    return CFrameWndEx::OnCreateClient(lpcs, pContext);
}

Cuando debería haberlo hecho así:

BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
    // TODO: Add your specialized code here and/or call the base class

    return TRUE;
}

Porque si nosotros vamos a crear las ventanas hijas, el antecesor de OnCreateClient predefinido no debería hacerlo. Al menos podrían haber puesto en el comentario que se quitara la llamada al padre si creamos nosotros las hijas. Y esto no está en la documentación por ningún lado.

Finalmente, nuestro código quedaría así:

BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
    CRect rect;
    GetClientRect(&rect);
    rect.NormalizeRect();

    if(!m_wndSplitter.CreateStatic(this,1,2))
    {
        TRACE0("No puedo crear el splitter principal");
        return FALSE;
    }

    if(!m_wndSplitter.CreateView(0,0,RUNTIME_CLASS(CListView),CSize(0,0),pContext))
    {
        TRACE0("No puedo crear la vista de la derecha");
        return FALSE;
    }

    if(!m_wndSplitter.CreateView(0,1,RUNTIME_CLASS(CLogView),CSize(0,0),pContext))
    {
        TRACE0("No puedo crear la vista de la izquierda");
        return FALSE;
    }

    //m_wndSplitter.SetColumnInfo(0,rect.Width()-100,100);
    m_wndSplitter.SetColumnInfo(1,100,100);

    return true;
    //return CFrameWndEx::OnCreateClient(lpcs, pContext);
}

Primero miramos el tamaño del área cliente y la normalizamos. MFC no implementa la técnica RAII, por lo que primero se crea el objeto que va a representar la ventana, y luego la propia ventana. En nuestro caso la clase se crea en la pila al colocar

CSplitterWndEx m_wndSplitter;

En la definición de la misma.

Luego, la llamada a CreateStatic nos creará lo que quiera que se necesario dentro de Windows para mantener esto.

Finalmente, las dos llamadas a CreateView nos crearán, de forma dinámica, cada una de las vistas. Observemos que llamamos a la clase, no a un objeto, mediante la macro RUNTIME_CLASS().

Lo último que hacemos es ponerle a una vista el tamaño que queramos. La otra se ajustará automáticamente.

Si ahora ejecutamos la aplicación, veremos algo parecido a la imagen:

Figura 2
Figura 2

Permitir el redimensionado del cuadro de diálogo

Pero así queda feo, muy feo, feísimo. El cuadro de diálogo no se redimensiona como una ficha normal… Así que sólo nos queda implementar el truco del almendruco: crearnos un método que capture el mensaje WM_SIZE de la clase vista que representa al cuadro de diálogo. Con el asistente es un segundo. El código que hemos puesto es este, aunque hay muchas otras formas más de hacerlo:

void CLogView::OnSize(UINT nType, int cx, int cy)
{
    CFormView::OnSize(nType, cx, cy);

    if(cx>0 && cy>0)
    {
        if(m_btnClearLog.m_hWnd!=NULL)
            m_btnClearLog.MoveWindow(0,cy-23,cx,23);
        if(m_lbLog.m_hWnd!=NULL)
            m_lbLog.MoveWindow(0,0,cx,cy-23);
    }
}

M_btnClearLog es una variable DDX que representa al botón, y m_lbLog la que representa al listbox.

El resultado queda bastante más profesional:

Figura 3
Figura 3

¿El siguiente paso?

Pese a lo bonito, esta técnica adolece de varias carencias; la primera es que cuando arranca y muestra la ventana por primera vez, el cuadro no se redimensiona. La solución es muy sencilla y se deja para que el lector se caliente los cascos.

La segunda es que todo el código de control del movimiento hay que hacerlo a mano y puede resultar muy, pero que muy tedioso. La solución pasa por subclasificar todos los controles MFC y capturar muchos mensajes de Windows y hacer como hace WindowsForms: definir una serie de enumeraciones que representen docking, y otros limitadores y actuar de forma adecuada en esos mensajes capturados, pero el ejercicio queda para un futuro lejano...

 


Ir al índice de los artículos de RFOG en el sitio del Guille




La fecha/hora en el servidor es: 24/11/2024 9:14:14

La fecha actual GMT (UTC) es: 

©Guillermo 'guille' Som, 1996-2024