Esempio: app_four

MDI Panoramica

Prima un pò di basi… Ogni finestra ha una Client Area, la parte dove molti programmi disegnano immagini, inseriscono controlli etc… la Client Area non è separata dalla finestra, è solo una regione specializzata di essa. Alcune volte una finestra può essere tutta Client Area, e altre volte la Client Area è piccola per lasciare spazio ai menù ai titoli alle scrollbars, etc…

Utilizzando i termini MDI, La finestra principale viene chiamata Frame Window, questa rappresenta probabilmente l’unica finestra a disposizione quando una applicazione è SDI (Single Document Interface). Nelle applicazioni MDI c’è una ulteriore finestra chiamata MDI Client Window che è figlia della Frame Window. Diversamente dalla Client Area essa è una finestra completamente separata ed ha una propria Client Area e probabilmente pochi pixels per il bordo. Non andremo mai a processare i messaggi per la finestra MDI Client, l’operazione viene svolta dalla classe predefinita di windows chiamata "MDICLIENT"; è possibile comunicare con questa classe e manipolare le finestre MDI Client tramite messaggi.

Inviando un messaggio alla MDI Client viene creata una nuova finestra del tipo specificato. La nuova finestra viene create come figlia di MDI Client, non del Frame e viene chiamata MDI Child. Ricapitolando quindi, MDI Child è figlia di MDI Client che a sua volta è figlia di MDI Frame (Non disperate). Per complicare ulteriormente le cose MDI Child può contenere altri controlli propri, per esempio il controllo textbox nei programmi di esempio di questa sezione.

Bisogna quindi scrivere due (o più) procedure per le finestre. La prima, come sempre per la finestra principale (Frame), ed un’altra per l’MDI Child. Se abbiamo più di un tipo di MDI Child dobbiamo scrivere ovviamente una procedura per ogni tipo.

Se i concetti appena presentati vi hanno confuso un attimino, date uno sguardo al diagramma seguente e può darsi che vi si chiariscano alcuni concetti.

mdi_diagram.gif

Iniziare ad usare MDI

MDI richiede alcuni cambiamenti all’interno del programma, quindi per favore leggete attentamente questa sezione… altrimenti può capitare che il vostro programma non funzioni oppure si comporti in modo strano.

MDI Client Window

Prima di creare la nostra finestra MDI, abbiamo bisogno di cambiare la funzione predefinita per processare i messaggi all’interno della nostra Window Procedure… Siccome stiamo creando una Frame Window che ospiterà la MDI Client Window, dobbiamo cambiare DefWindowProc() in DefFrameProc() Che aggiunge messaggi specializzati per le Frame Windows.

    default:
        return DefFrameProc(hwnd, g_hMDIClient, msg, wParam, lParam);

Il passo successivo consiste nel creare la finestra MDI Client, come figlia della nostra Frame Window. Lo facciamo con WM_CREATE come al solito…

    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);

Il menu handle passato è proprio l’handle del menu di popup del quale l’MDI client aggiungerà le voci e sarà contestuale ad ogni nuova finestra creata, permettendo all’utente di selezionare la finestra che vuole e agendo quindi sulla finestra attiva. Aggiungeremo funzionalità a breve per supportare questo caso. In questo esempio è il terzo popup (indice 2) siccome abbiamo aggiunto l’Edit e la Finestra dopo il menù File.

ccs.idFirstChild è un numero da usare come primo ID per le voci che il Client aggiunge al menù Window… Facciamo in modo che queste voci siano facilmente distinguibili in modo da poter processare facilmente i comandi del menù e passare i del menù Window a DefFrameProc() per essere processati. Nell’esempio è stato specificato un identificatore definito come 50000, abbastanza alto da renderci sicuri che nessun ID dei nostri menù sarà superiore ad esso.

A questo punto per far funzionare il tutto, c’è bisogno di aggiungere del codice specializzato alla nostra procedura per WM_COMMAND:

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

            // ... processa gli altri ID regolari ...

            // Processa i comandi della Finestra MDI
            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;

Egrave; stato aggiunto il caso default: che intercetterà tutti i comandi non processati ed effettuerà un controllo per vedere se il valore è maggiore o uguale a ID_MDI_FIRSTCHILD nel cui caso l’utente ha clickato su una delle voci del menù Window e quindi inviamo il messaggio a DefFrameProc() che provvederà a processarlo.

Nel caso non è nessuno degli ID del menù della finestra, ci ricaviamo l’handle della Child Window attiva e gli inoltriamo il messaggio per processarlo. Questo ci consente di delegare la responsabilità di alcune azioni alle Child Windows e permette a diverse Child Windows di effettuare azioni diverse processando i comandi come meglio desiderano. Nell’esempio vengono processati solamente i comandi che sono globali al programma nella procedura della Frame Window, e tutti i comandi relativi ad alcuni documenti o Child Windows vengono passati alla Child Window stessa per essere processati.

Siccome l’abbiamo fatto nell’esempio precedente, il codice per dimensionare l’MDI Client è lo stesso del codice per ridimensionare il controllo textbox. In questo modo teniamo conto della grandezza e della posizione della toolbar e della status bar in modo che non si sovrappongano alla finestra MDI Client.

Abbiamo bisogno di modificare leggermente anche il nostro loop di messaggi…

    while(GetMessage(&Msg, NULL, 0, 0))
    {
        if (!TranslateMDISysAccel(g_hMDIClient, &Msg))
        {
            TranslateMessage(&Msg);
            DispatchMessage(&Msg);
        }
    }

Abbiamo aggiunto un ulteriore passattio (TranslateMDISysAccel()), che controlla per le chiavi acceleratori predefinite, Ctrl+F6, passa alla finestra successiva, Ctrl+F4 chiude la Child Window e così via. Se non aggiungete questo controllo gli utenti saranno disturbati dal fatto che la vostra applicazione non fornisce un comportamento a cui sono abituati, oppure dovete implementarlo a mano.

La Classe Child Window

In aggiunta alla finestra principale del programma (la Frame Window) abbiamo bisogno di creare nuove window classes per ogni tipo di finestra desideriamo. Per esempio potremmo avere bisogno di averne una per visualizzare il testo, e una per visualizzare immagini o grafici. In questo esempio creiamo solamente un tipo di figlio che sarà semplicemente identico al programma editor degli esempi precedenti.

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;
}

Egrave; sostanzialmente identico alla registrazione della Frame Window, non ci sono flag speciali per l’utilizzo in una finestra MDI. Abbiamo impostato il menù come NULL, e la Window Procedure per puntare alla Child Window Procedure, che scriveremo in seguito.

Procedura MDI Child

La Window Procedure per una finestra MDI Child è molto simile alle altre con pochissime eccezioni. Prima di tutto, i messaggi di default sono passati a DefMDIChildProc() invece che DefWindowProc().

In questo caso particolare vogliamo anche disabilitare la TextBox e il menù della Finestra quando non sono utilizzati (solo perché è una cosa carina da fare), quindi processiamo WM_MDIACTIVATE e abilitiamo o disabilitiamo i controlli quando la finestra viene attivata oppure no. Se abbiamo diversi tipi di Child Window, possiamo anche inserire il codice per cambiare completamente il menù o la toolbar oppure fare alterazioni agli altri aspetti del programma per riflettere le azioni e i comandi che sono specifici per il tipo di finestra che viene attivata.

Per essere ulteriormente completi, possiamo disabilitare anche le voci di menù Chiudi e Salva, dato che non sarebbero molto utili quando non ci sono finestre con cui interagire. Un piccolo trucco è quello di disabilitare queste voci per default all’interno del file delle risorse e di abilitarle quando una finestra viene aperta, in modo da non dover aggiungere codice ulteriore all’interno del programma quando viene avviato.

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

            // Crea l' Edit Control

            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)
            {      //viene attivata, abilita il menu
                EnableFlag = MF_ENABLED;
            }
            else
            {                          //viene disattivata, disabilita il menu
                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;

            // Calcola la grandezza rimanente e dimensiona l'edit

            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;
}

Sono stati implementati i comandi Apri e Salva, il DoFileOpen() ed il DoFileSave() sono praticamente gli stessi degli esempi precedenti con l’unica differenza che hanno l’ID dell’edit control cambiato, e in aggiunta settano il titolo del MDI Child con il nome del file.

I comandi per la TextBox sono semplici dato che l’edit control li supporta già da sé, dobbiamo solo dirgli cosa fare.

Ricordate che vi ho detto che bisogna ricordarsi di fare alcune piccole modifiche altrimenti l’applicazione potrebbe comportarsi in modo strano? Notate che è stato chiamato DefMDIChildProc() alla fine del WM_SIZE, è molto importante altrimenti il sistema non ha possibilità di processare il messaggio. Potete visionare DefMDIChildProc() in MSDN per conoscere la lista dei messaggi che processa e passarglieli dall’interno della vostra applicazione.

Creare e Distruggere Finestre

Le finestre MDI Child non sono create direttamente, inviamo infatti un messaggio WM_MDICREATE alla finestra MDI Client indicando che tipo di finestra vogliamo settando propriamente i membri della struttura MDICREATESTRUCT. Potete visionare i vari membri di questa struttura consultando la documentazione del vostro compilatore. Il valore ritornato dal messaggio WM_MDICREATE è l’handle della nuova finestra creata.

HWND CreateNewMDIChild(HWND hMDIClient)
{
    MDICREATESTRUCT mcs;
    HWND hChild;

    mcs.szTitle = "[Senza Titolo]";
    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, "Creazione della finestra MDI Child fallita.", "Oh Oh...",
            MB_ICONEXCLAMATION | MB_OK);
    }
    return hChild;
}

Un membro di MDICREATESTRUCT che non è stato usato ma può essere abbastanza utile è il parametro lParam. Può essere usato per inviare un qualsiasi valore 32bit (tipo un puntatore ad una struttura) alla finestra MDI Child che si va a creare, in modo da passare una qualsiasi informazione personalizzata di nostra scelta. Quando viene processato il messaggio WM_CREATE della Child Window che andiamo a creare lParam punterà ad una struttura CREATESTRUCT che contiene un membro lpCreateParams che a sua volta punta alla struttura MDICREATESTRUCT da noi inviata per creare la finestra. Quindi per accedere al valore personalizzato di lParam che abbiamo settato in precedenza dobbiamo fare qualcosa del tipo…

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

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

        /*
        pMDICreateStruct ora punta alla stessa MDICREATESTRUCT che abbiamo inviato
        insieme al messaggio WM_MDICREATE e possiamo usarla per accedere al lParam
        */
    }
    break;

Se non volete disturbarvi con questi due puntatori extra potete accedere al lParam personalizzato in un unico passaggio con: ((MDICREATESTRUCT*)((CREATESTRUCT*)lParam)->lpCreateParams)->lParam

Ora possiamo implementare il comando File all’interno della procedura per la Frame Window:

    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;

Possiamo anche suppportare la disposizione delle Child Window all’interno del nostro menù Window, dato che MDI lo supporta non c’è molto lavoro da fare

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