En esta primera parte vamos a ver cómo podemos añadir controles normales
dentro de una ventana como si estuviéramos en un cuadro de diálogo, pero con
la ventaja de que se trata de una ventana normal y corriente y por tanto se
puede redimensionar y usar como tal (por ejemplo como una vista dentro del
modelo Documento/Vista).
La pega es que hay que hacerlo al viejo estilo, es decir, a mano.
En detalle
Bueno, vamos allá. Lo primero de todo es tener una ventana, algo que
dicho así a bote pronto puede sonar bastante evidente, no lo es, porque
¿cómo creamos una ventana desde cero en MFC?
Pues sencillo, nos vamos al proyecto en el Explorador de Soluciones,
hacemos clic con el botón derecho y elegimos añadir nueva clase. Entonces se
nos abre el asistente (sí, los de C++ también tenemos de eso), como sale en
la imagen y seleccionamos “Clase MFC”.
Cuando la añadamos llegaremos a la siguiente pantalla, en la que
pondremos en el nombre de la clase el que queramos darle y elegiremos la
clase base, que podrá ser CWnd, CPropertyPage, CDockablePane o
cualquier otra clase que tenga funciones de ventana.
Ahora ya tenemos nuestra clase de ventana, y es cuestión de irle
añadiendo funcionalidad. Podemos usar el Class Wizard, pero lo vamos a hacer
a mano.
Usualmente en una ventana se suele sobreescribir el método virtual
PreCreateWindow () y también se debe sobreescribir el también virtual Create()
(equivalente al OnCreate() de un cuadro de diálogo). En el primero podremos
cambiar el estilo de la ventana antes de que sea creada y en el segundo
tendremos que añadir la funcionalidad extra.
Para el ejemplo que os quiero demostrar, PreCreateWindow() toma esta
forma:
BOOL MiVentana::PreCreateWindow(CREATESTRUCT& cs)
{
if (!CWnd::PreCreateWindow(cs))
return FALSE;
cs.dwExStyle |= WS_EX_CLIENTEDGE;
cs.style &= ~WS_BORDER;
cs.lpszClass = AfxRegisterWndClass(CS_HREDRAW|CS_VREDRAW|CS_DBLCLKS,
::LoadCursor(NULL, IDC_ARROW), reinterpret_cast<HBRUSH>(COLOR_WINDOW+1), NULL);
return TRUE;
}
El método Create() por defecto tiene este aspecto:
BOOL MiVentana::Create(LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle,
const RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext)
{
CWnd::Create(lpszClassName, lpszWindowName, dwStyle, rect, pParentWnd, nID, pContext);
return TRUE;
}
Como vemos estamos llamando al Create() de la clase padre (por cierto,
Visual C++ 2010 añade la palabra reservada __super para llamar al método
miembro de la clase padre).
Hasta ahora tenemos una ventana normal y corriente pero sin borde (se lo
hemos quitado en PreCreateWindow() ).
Para terminar de añadirle el esqueleto, tenemos que capturar el mensaje
WM_SIZE y sobreescribir un nuevo método miembro llamado AdjustLayout().
Este último método contendrá el código que redimensionará los controles
hijos de la ventana de forma uniforme, y lo hacemos así porque es un método
que también tiene MFC y usa a veces y porque tenemos que llamarlo en varios
sitios.
Si habéis seguido los pasos explicados, tendréis un fichero cabecera
parecido a este:
class MiVentana: public CWnd
{
DECLARE_DYNAMIC(MiVentana)
public:
MiVentana();
virtual ~ MiVentana();
virtual BOOL Create(LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle,
const RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext = NULL);
// Overrides
protected:
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
afx_msg void OnSize(UINT nType, int cx, int cy);
DECLARE_MESSAGE_MAP()
private:
CListBox m_items;
void AdjustLayout();
};
Y habremos añadido al mapa de mensajes la macro correspondiente:
BEGIN_MESSAGE_MAP(MiVentana, CWnd)
ON_WM_SIZE()
END_MESSAGE_MAP()
También tendremos dos nuevos métodos:
void MiVentana::OnSize(UINT nType, int cx, int cy)
{
CWnd::OnSize(nType, cx, cy);
AdjustLayout();
}
void MiVentana::AdjustLayout()
{
if (GetSafeHwnd() == NULL)
{
return;
}
}
Ahora ya podremos ir añadiendo el código que nos hace falta para que todo
funcione. Si os fijáis en el fichero cabecera, hemos puesto en la sección
privada un dato miembro llamado m_items que es un control de lista y que
vamos a embeber dentro de la ventana. Por lo tanto, para que el control se
cree a la vez que nuestra ventana, tenemos que modificar el método Create()
y añadir la creación del cuadro de lista. También llamaremos a AdjustLayout()
para reorganizar el tema:
BOOL MiVentana::Create(LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle,
const RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext)
{
CWnd::Create(lpszClassName, lpszWindowName, dwStyle, rect, pParentWnd, nID, pContext);
CRect rectDummy;
rectDummy.SetRectEmpty();
const DWORD dwViewStyle = WS_CHILD | WS_VISIBLE | WS_BORDER;
if(!m_items.Create(dwViewStyle,rectDummy,this,8))
{
TRACE0("Failed to create times control\n");
return -1; // fail to create
}
AdjustLayout();
return TRUE;
}
El hecho de pasar un RECT todo a cero nos ayudará a redimensionar el
Listbox de forma adecuada en AdjustLayout().
Y finalmente tenemos que implementar dicho método:
void MiVentana::AdjustLayout()
{
if (GetSafeHwnd() == NULL)
{
return;
}
CRect rectClient;
GetOwner()->GetClientRect(rectClient);
//TODO: Adjust child windows
SetWindowPos(&wndTop, rectClient.left, rectClient.top, rectClient.Width(), rectClient.Height(),
SWP_SHOWWINDOW);
if(m_items.m_hWnd!=0)
m_items.SetWindowPos(NULL, rectClient.left, rectClient.top, rectClient.Width()/2, rectClient.Height(),
SWP_NOACTIVATE | SWP_NOZORDER);
}
Si os fijáis, primero obtenemos el rectángulo cliente de la ventana padre
(que es donde en la siguiente entrada encastraremos esta ventana). Dicho
rectángulo podría ser cualquier que nos conviniera, incluso valores sacados
de un archivo de configuración que nos habría guardado el tamaño de dicha
ventana.
Primero redimensionamos la propia ventana a dicho tamaño cliente, y luego
redimensionamos el cuadro de lista a la mitad del tamaño de la ventana. De
este modo, cada vez que cambiemos el tamaño de la misma, nuestro ListBox
ocupará, siempre, su mitad izquierda.
Conclusión
De este modo podremos añadir los controles que queramos, y de hecho es la
forma que tiene el Feature Pack de hacerlo con ventanas mucho más complejas.
Para que os hagáis una idea, todo el sistema de encastración de ventanas del
propio Visual Studio funciona así, y en la entrada siguiente (o en la
posterior), veremos cómo hacerlo.