[ contenidos | #winprog ]

Aplicaci�n Parte 4: Interfaces con M�ltiples Documentos

Ejemplo: app_four

[images/app_four.gif]

Introducci�n a IMD

Primero un poco de repaso... Todas las ventanas tienen un Area Cliente, aqu� es donde la mayor�a de los programas dibujan im�genes, ubican los controles, etc... el Area Cliente no est� separada de la ventana, simplemente es una peque�a regi�n especializada de la ventana. A veces una ventana puede ser todo el �rea cliente, y veces nada, a veces el �rea cliente puede hacerse mas peque�a par caber en men�es, t�tulos, barras de desplazamiento, etc...

En t�rminos de IMD (en ingl�s, MDI - Multiple Document Interface), nuestra ventana principal es llamada Frame y �sta es probablemente la �nica ventana que podr�amos tener en un programa IUD (Interface con un Unico Documento). En IMD hay una ventana adicional llamada Ventana Cliente IMD, la cual es hija de nuestro Frame y a diferencia del Area Cliente, es una ventana completa y separada de las dem�s que tiene un �rea cliente y probablemente algunos pixeles para un borde. Nunca procesaremos mensajes directamente del Cliente IMD, esto es hecho por la clase ventana pre-definida "MDI_CLIENT". Podemos comunicarnos y manipular el �rea cliente IMD, como as� tambi�n las ventanas que �sta contiene, atrav�s de mensajes.

Cuando entramos a la ventana que muestra nuestro documento, o lo que sea que muestre el programa, env�amos un mensaje al Cliente IMD para decirle que cree una nueva ventana del tipo que hemos especificado. La nueva ventana es creada como una ventana hija del Cliente IMD, no de nuestro Frame Window. Esta nueva ventana es una hija IMD (IMD Child). La hija IMD Child es hija del Cliente IMD, el cual a su vez, es hijo del Frame IMD... La Hija IMD probablemente tenga sus propias ventanas hijas, por ejemplo el control edit en el programa del ejemplo de esta secci�n.

Tenemos que escribir dos (o mas) Window Procedures. Uno, como siempre, para nuestra ventana principal (el Frame) y uno m�s para la Hija IMD. Podemos tener tambi�n mas de un tipo de hija, en cuyo caso escribiremos window procedures separados para cada tipo.

Si te he confundido hablando de Clientes IMD y todo eso, quiz�s este diagrama puede ayudarte a aclarar un poco las cosas.

[images/mdi_diagram.gif]

Ahora si, IMD

IMD requiere algunos cambios astutos a lo largo de un programa, por lo tanto lee esta secci�n cuidadosamente... si tu programa no funciona o tiene un comportamiento extra�o es porque erraste alguna de las alteraciones que vamos a hacer al programa regular.

Ventana Cliente IMD

Antes de que creemos nuestra ventana IMD necesitamos hacer un cambio al procesamiento por default que utilizamos en nuestro Window Procedure... debido a que estamos creando un Frame que residir� en un Cliente IMD, necesitamos cambiar la llamada a DefWindowProc( ) a DefFrameProc( ) la cual agrega un procesamiento especializado de mensajes para Frames.
    default:
        return DefFrameProc(hwnd, g_hMDIClient, msg, wParam, lParam);
El pr�ximo paso es crear la ventana Cliente IMD, como una hija de nuestro Frame. Esto lo hacemos en el WM_CREATE, como antes...
    CLIENTCREATESTRUCT ccs;

    ccs.hWindowMenu  = GetSubMenu(GetMenu(hwnd), 2);
    ccs.idFirstChild = ID_MDI_FIRSTCHILD;

    g_hMDIClient = CreateWindowEx(WS_EX_CLIENTEDGE, "mdiclient", NULL,
        WS_CHILD | WS_CLIPCHILDREN | WS_VSCROLL | WS_HSCROLL | WS_VISIBLE,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
        hwnd, (HMENU)IDC_MAIN_MDI, GetModuleHandle(NULL), (LPVOID)&ccs);
El menu handle es el handle al men� desplegable en el que el Cliente IMD agregar� items para representar cada ventana que es creada, permiti�ndole al usuario elegir desde el men� la ventana que quiere activar. Por lo tanto, agregaremos funcionalidad para manejar este caso. En este ejemplo, es la tercera (de �ndice 2) debido a que la he agregado al Men� despu�s de File, Edit y Window.

ccs.idFirstChild es el n�mero para usar como el primer ID para los items que el Cliente agrega a la Ventana men�... queremos que esto sea f�cilmente distinguible de nuestros identificadores de men�es para que podamos procesar los comandos del men� y pasarlos desde la ventana a DefFrameProc( ) para que los procese. En el ejemplo he especificado un identificador definido como 50000, que es lo suficientemente grande para que ninguno de los identificadores de los comandos del men� sean mayor que �ste.

Ahora para que este men� funcione apropiadamente, necesitamos agregar alg�n procesamiento especial a nuestro manejador del mensaje WM_COMMAND:

    case WM_COMMAND:
        switch(LOWORD(wParam))
        {
            case ID_FILE_EXIT:
                PostMessage(hwnd, WM_CLOSE, 0, 0);
            break;

            // ... handle other regular IDs ...

            // Handle MDI Window commands
            default:
            {
                if(LOWORD(wParam) >= ID_MDI_FIRSTCHILD)
                {
                    DefFrameProc(hwnd, g_hMDIClient, msg, wParam, lParam);
                }
                else 
                {
                    HWND hChild = (HWND)SendMessage(g_hMDIClient, WM_MDIGETACTIVE,0,0);
                    if(hChild)
                    {
                        SendMessage(hChild, WM_COMMAND, wParam, lParam);
                    }
                }
            }
        }
    break;

He agregado en el case un caso m�s, el default:, el cual atrapar� todos los comandos que no procesemos directamente y realizar� un chequeo para ver si el valor es mayor o igual a ID_MDI_FIRSTCHILD. Si lo es, entonces el usuario ha clickeado en uno de los items del men� de la ventana y env�amos el mensaje a DefFrameProc( ) para que lo procese.

Si no es un alguno de los IDs de la ventana, entoces obtenemos el handle de la ventana hijo que est� activa y le enviamos el mensaje para que lo procese. Esto nos permite delegar la responsabilidad de realizar ciertas acciones a la ventana Hija y nos permite que diferentes ventanas Hijas procesen comandos de diferentes formas, si as� se desea. En el ejemplo, en el Frame window procedure solo proceso los comandos que son globales al programa y env�o los comandos que afectan a cierto documento o una ventana Hija hacia dicha ventana Hija para que los procese.

Tambi�n necesitamos modificar un poco nuestro Loop de Mensajes...

    while(GetMessage(&Msg, NULL, 0, 0))
    {
        if (!TranslateMDISysAccel(g_hMDIClient, &Msg))
        {
            TranslateMessage(&Msg);
            DispatchMessage(&Msg);
        }
    }
Hemos agregado un paso extra (TranslateMDISysAccel()), que chequea por el acelerador de teclas pre-definido, Ctrl+F6 cambia a la siguiente ventana, Ctrl+F4 cierra la ventana Hijo, etc... Si no quieres agregar este chequeo impedir�s a los usuarios de usar el comportamiento est�ndar que est�n acostumbrados a usar, o tendr�s que implementarlo manualmente.

Clase Ventana Hija

Adem�s de la ventana principal de un programa (el Frame), necesitamos crear una nueva clase ventana para cada tipo de ventana hija que necesitamos. Por ejemplo, podemos tener una que muestre texto y otra para mostrar gr�ficos o fotos. En este ejemplo solo crearemos un tipo de hijo, el cual ser� como el editor de los ejemplos anteriores.
BOOL SetUpMDIChildWindowClass(HINSTANCE hInstance)
{
    WNDCLASSEX wc;

    wc.cbSize        = sizeof(WNDCLASSEX);
    wc.style         = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc   = MDIChildWndProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInstance;
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_3DFACE+1);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = g_szChildClassName;
    wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);

    if(!RegisterClassEx(&wc))
    {
        MessageBox(0, "Could Not Register Child Window", "Oh Oh...",
            MB_ICONEXCLAMATION | MB_OK);
        return FALSE;
    }
    else
        return TRUE;
}

Esto es b�sicamente id�ntico a registrar nuestro Frame, no hay flags particularmente especiales para usar con IMD. Debemos poner nuestro men� en NULL y el window procedure apuntando al window procedure de la ventana hija que escribiremos a continuaci�n.

IMD Procedure Hijo

El window procedure para una IMD Hija es parecido a culaquier otro pero con algunas excepciones. Primero de todo, los mensajes por default, en lugar de ser pasados a DefWindowProc( ), son pasados a DefMDIChildProc( ).

En este caso particular, tambi�n queremos deshabilitar los men�es Edit y Window cuando no se necesitan (s�lo porque es bueno hacerlo), por lo tanto procesando el mensaje WM_MDIACTIVATE los habilitamos o deshabilitamos dependiendo si la ventana est� activa o no. Si tenemos m�ltiples tipos de ventana hijo, aqu� es donde podemos poner el c�digo para cambiar completamente el men�, la barra de herramientas o hacer alteraciones a otros aspectos del programa para reflejar las acciones y comandos que son espec�ficos del tipo de ventana que est� siendo activada.

Para ser a�n mas completos, podemos deshabilitar los items Close y Open del men�, debido a que no ser�n de utilidad cuando las ventanas no est�n activas. He deshabilitado todos estos items por default en el recurso, por lo tanto no necesitamos agregar c�digo extra para hacer esto cuando la aplicaci�n se ejecuta por primera vez.

LRESULT CALLBACK MDIChildWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
        case WM_CREATE:
        {
            HFONT hfDefault;
            HWND hEdit;

            // Creamos el control Edit

            hEdit = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", "", 
                WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL, 
                0, 0, 100, 100, hwnd, (HMENU)IDC_CHILD_EDIT, GetModuleHandle(NULL), NULL);
            if(hEdit == NULL)
                MessageBox(hwnd, "Could not create edit box.", "Error", MB_OK | MB_ICONERROR);

            hfDefault = GetStockObject(DEFAULT_GUI_FONT);
            SendMessage(hEdit, WM_SETFONT, (WPARAM)hfDefault, MAKELPARAM(FALSE, 0));
        }
        break;
        case WM_MDIACTIVATE:
        {
            HMENU hMenu, hFileMenu;
            UINT EnableFlag;

            hMenu = GetMenu(g_hMainWindow);
            if(hwnd == (HWND)lParam)
            {      //si est� activa, activamos los menu�s
                EnableFlag = MF_ENABLED;
            }
            else
            {                          //cuando se desactiva, desactivamos los menu�s
                EnableFlag = MF_GRAYED;
            }

            EnableMenuItem(hMenu, 1, MF_BYPOSITION | EnableFlag);
            EnableMenuItem(hMenu, 2, MF_BYPOSITION | EnableFlag);

            hFileMenu = GetSubMenu(hMenu, 0);
            EnableMenuItem(hFileMenu, ID_FILE_SAVEAS, MF_BYCOMMAND | EnableFlag);

            EnableMenuItem(hFileMenu, ID_FILE_CLOSE, MF_BYCOMMAND | EnableFlag);
            EnableMenuItem(hFileMenu, ID_FILE_CLOSEALL, MF_BYCOMMAND | EnableFlag);

            DrawMenuBar(g_hMainWindow);
        }
        break;
        case WM_COMMAND:
            switch(LOWORD(wParam))
            {
                case ID_FILE_OPEN:
                    DoFileOpen(hwnd);
                break;
                case ID_FILE_SAVEAS:
                    DoFileSave(hwnd);
                break;
                case ID_EDIT_CUT:
                    SendDlgItemMessage(hwnd, IDC_CHILD_EDIT, WM_CUT, 0, 0);
                break;
                case ID_EDIT_COPY:
                    SendDlgItemMessage(hwnd, IDC_CHILD_EDIT, WM_COPY, 0, 0);
                break;
                case ID_EDIT_PASTE:
                    SendDlgItemMessage(hwnd, IDC_CHILD_EDIT, WM_PASTE, 0, 0);
                break;
            }
        break;
        case WM_SIZE:
        {
            HWND hEdit;
            RECT rcClient;


            GetClientRect(hwnd, &rcClient);

            hEdit = GetDlgItem(hwnd, IDC_CHILD_EDIT);
            SetWindowPos(hEdit, NULL, 0, 0, rcClient.right, rcClient.bottom, SWP_NOZORDER);
        }
        return DefMDIChildProc(hwnd, msg, wParam, lParam);
        default:
            return DefMDIChildProc(hwnd, msg, wParam, lParam);
    
    }
    return 0;
}

He implementado como comandos File Open y Save, DoFileOpen( ) y DoFileSave( ) son igual que en los ejemplos anteriores con el ID del control edit cambiado y adicionalmente he puesto como t�tulo de la Ventana Hija IMD el nombre del archivo.

Los comandos edit son f�ciles, debido a que el control edit est� desarrollado para soportarlos, s�lo le decimos que hacer.

�Recuerdas que he mencionado que hay algunas cosas que necesitas recordar o tu aplicaci�n se comportar� de manera extra�a? Observa que he llamado DefMDIChildProc( ) al final del WM_SIZE, esto es importante porque de no ser as�, el sistema no tendr� la chance de hacer su propio procesamiento sobre el mensaje. Puedes buscar sobre DefMDIChildProc( ) en MSDN para encontrar una lista sobre los mensajes que procesa y siempre asegurarte de pas�rselos.

Crear y Destruir Ventanas

Las ventanas Hijas IMD nos son creadas directamente, al contrario, le enviamos a la ventana cliente el mensaje WM_MDICREATE dici�ndole que tipo de ventana queremos crear rellenando los miembros de una estructura MDICREATESTRUCT. Puedes ver los distintos miembros de �sta estructura en la documentaci�n. El valor de retorno del mensaje WM_MDICREATE es el handle a la nueva ventana.
HWND CreateNewMDIChild(HWND hMDIClient)
{ 
    MDICREATESTRUCT mcs;
    HWND hChild;

    mcs.szTitle = "[Untitled]";
    mcs.szClass = g_szChildClassName;
    mcs.hOwner  = GetModuleHandle(NULL);
    mcs.x = mcs.cx = CW_USEDEFAULT;
    mcs.y = mcs.cy = CW_USEDEFAULT;
    mcs.style = MDIS_ALLCHILDSTYLES;

    hChild = (HWND)SendMessage(hMDIClient, WM_MDICREATE, 0, (LONG)&mcs);
    if(!hChild)
    {
        MessageBox(hMDIClient, "MDI Child creation failed.", "Oh Oh...",
            MB_ICONEXCLAMATION | MB_OK);
    }
    return hChild;
}

Un miembro de la estructura MDICREATESTRUCT que no usamos pero puede ser muy �til es el lParm. Este puede ser usado para enviar cualquier valor de 32 bits (un puntero) a la ventana hija que est�s creando con el prop�sito de proveerle cualquier informaci�n. En el manejador WM_CREATE de nuestra ventana hija, el valor lParam para el mensaje WM_CREATE apuntar� a la estructura MDICREATESTRUCT. El miembro lpCreateParams de dicha estructura apuntar� a MDICREATESTRUCT que enviamos junto con WM_MDICREATE. Por lo tanto para acceder al valor lparam de la ventana hija necesitamos hacer algo como esto en el window procedure de dicha ventana...

    case WM_CREATE:
    {
        CREATESTRUCT* pCreateStruct;
        MDICREATESTRUCT* pMDICreateStruct;

        pCreateStruct = (CREATESTRUCT*)lParam;
        pMDICreateStruct = (MDICREATESTRUCT*)pCreateStruct->lpCreateParams;

        /*
        pMDICreateStruct now points to the same MDICREATESTRUCT that you
        sent along with the WM_MDICREATE message and you can use it
        to access the lParam.
        */
    }
    break;

Si no quieres complicarte con esos dos punteros extras, puedes acceder a lParam en un solo paso de la siguiente manera: ((MDICREATESTRUCT*)((CREATESTRUCT*)lParam)->lpCreateParams)->lParam

Ahora podemos implementar los comandos File del men� en nuestro Frame window procedure:

    case ID_FILE_NEW:
        CreateNewMDIChild(g_hMDIClient);
    break;
    case ID_FILE_OPEN:
    {
        HWND hChild = CreateNewMDIChild(g_hMDIClient);
        if(hChild)
        {
            DoFileOpen(hChild); 
        }
    }
    break;
    case ID_FILE_CLOSE:
    {
        HWND hChild = (HWND)SendMessage(g_hMDIClient, WM_MDIGETACTIVE,0,0);
        if(hChild)
        {
            SendMessage(hChild, WM_CLOSE, 0, 0);
        }
    }
    break;

Tambi�n podemos proveer alg�n procesamiento IMD por default al ordenamiento de la ventana para nuestro Window Men�, debido a que IMD soporta esto, no es mucho trabajo.

    case ID_WINDOW_TILE:
        SendMessage(g_hMDIClient, WM_MDITILE, 0, 0);
    break;
    case ID_WINDOW_CASCADE:
        SendMessage(g_hMDIClient, WM_MDICASCADE, 0, 0);
    break;

Copyright © 1998-2003, Brook Miles (theForger). All rights reserved.

Versi�n en Espa�ol: Federico Pizarro - 2003