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
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
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
¿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