[ 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