Menüs mit Icons

Das im Folgenden beschriebene Verfahren zur Darstellung vom Menüs mit Symbolen funktioniert ab WINDOWS95 bzw. WINDOWS NT4.0. Ab WINDOWS98 bzw. WINDOWS2000 gibt es eine weitere Möglichkeit, Symbole innerhalb eines Menüs darzustellen. Da aber die mit dem VC++ 6.0 ausgelieferten Header-Dateien WINDOWS98 noch nicht erkennen, wird an dieser Stelle auf die neue Möglichkeit nicht näher eingegangen. Weitere Informationen hierzu erhalten Sie unter dem Stichwort MENUITEMINFO in der Online-Hilfe.

Bei vielen kommerziellen Programmen enthalten Menüeinträge nicht nur Text sondern oft zusätzlich ein kleines Icon. Unsere bisherigen Menüeinträge bestanden nur aus dem Menü-Text, da die MFC bis jetzt keine direkte Unterstützung für Menüeinträge mit Icons anbietet.

Um einen Menüeintrag mit einem Icon zu versehen, muss er durch die Anwendung selbst gezeichnet werden. Jawohl, Sie haben richtig gelesen. Sie müssen den Menüeintrag selbst 'zeichnen'. Damit Sie aber nicht alles von Hand erledigen müssen, erstellen Sie zunächst wie gewohnt das Menü und die dazugehörigen Nachrichtenbearbeiter. Wenn alles so funktioniert, wie Sie es sich vorgestellt haben, können Sie ans Modifizieren der Menüeinträge gehen. Im Programm wird dazu zunächst der Stil des Menüeintrags modifiziert, indem ihm der Stil MFT_OWNERDRAW hinzugefügt wird. Selbstverständlich könnten Sie auch das komplette Menü, d.h. das Fenstermenü wie auch die einzelnen Popup-Menüs, im Programm entsprechend aufbauen. Dadurch verlieren Sie aber die Möglichkeit, per Klassen-Assistent den einzelnen Menüeinträge Methoden zuzuweisen. Um die Menüeinträge zu modifizieren, überschreiben Sie in der Regel die Methode OnCreate(...) des Rahmenfensters.

Denken Sie immer daran, dass Menüs zum Rahmenfenster gehören und nicht zum Ansichtsobjekt!

In der OnCreate(...) Methode werden dann die Menüeinträge angepasst. Mehr zur OnCreate(...) Methode selbst weiter unten.

Um an die Informationen zu einem bestehenden Menüeintrag zu kommen, rufen Sie die API-Funktion GetMenuItem(...) auf. Diese Funktion erhält u.a. als letzten Parameter einen Zeiger auf eine Struktur vom Typ MENUITEMINFO, in der die Informationen über den Menüeintrag abgelegt werden. MENUITEMINFO hat folgenden Aufbau:

struct tagMENUITEMINFO
{
    UINT cbSize;
    UINT fMask;
    UINT fType;
    UINT fState;
    UINT wID;
    HMENU hSubMenu;
    HBITMAP hbmpChecked;
    HBITMAP hbmpUnchecked;
    DWORD dwItemData;
    LPWSTR dwTypeData;
    UINT cch;
    #if (_WIN32_WINNT >= 0x0500)
        HBITMAP hbmpItem;
    #endif
}
Das Strukturelement hbmpItem ist erst ab WINDOWS2000 gültig und in der aktuellen Version des VC++ eventl. verfügbar!

Wir wollen hier nicht auf alle Details der einzelnen Strukturelemente eingehen sondern nur die für unseren Zweck relevanten Einträge betrachten. Um Informationen über einen bestimmten Menüeintrag auszulesen, ist zunächst das Element cbSize mit der Strukturgröße zu initialisieren. Anschließend ist das Element fMask mit den Konstanten MIIM_DATA, MIIM_TYPE und MIIM_ID zu belegen. Über fMask wird der Funktion GetMenuItemInfo(...) mitgeteilt, welche Informationen ausgelesen werden sollen. Wird das Flag MIIM_TYPE gesetzt, so wird der Typ des Menüeintrags im Element fType zurückgeliefert. Sie benötigen diese Information in der Regel immer dann, wenn auch Trennlinien im Popup-Menü enthalten sind; diese sollen ja nachher nicht mit einem Icon versehen werden. Die Konstante MIIM_ID teilt der Funktion mit, dass zusätzlich noch die ID des Menüeintrags ausgelesen werden soll. Über diese ID bestimmen wir nachher das Icon, das dem Menüeintrag hinzugefügt wird. Außerdem wird über diese Konstante der Menü-Text ausgelesen. Dazu müssen Sie im Element dwTypeData einen Zeiger auf einen Puffer ablegen, in den der Menü-Text kopiert werden soll. Das Struktur-Element cch enthält in diesem Fall noch die Größe des Puffers für den Menü-Text. Zur letzten Konstante MIIM_DATA kommen wir gleich noch.

Nach dem Sie die Struktur entsprechend initialisiert und die Funktion GetMenuItemInfo(...) aufgerufen haben, können Sie die zurückgelieferten Daten auswerten. Im Beispiel unten wird je nach ID des Menüeintrags das entsprechend zugeordnete Icon geladen. Anschließend wird der Menü-Text und das Icon-Handle in einer dynamisch angelegten Struktur abgelegt. Da diese Daten später wieder beim Zeichnen des Menüeintrags benötigen werden, müssen wir noch den Zeiger auf diese Struktur irgendwo ablegen. Glücklicherweise enthält jeder Menüeintrag einen zusätzlichen 32-Bit Eintrag dwItemData, in dem wir den Zeiger ablegen können.

Jetzt haben wir alle Zusatzinformationen zusammen, um den Menüeintrag selbst zeichnen zu können. Was uns nun 'nur' noch bleibt, ist die so neu erstellte Zusatzinformation mit dem Menüeintrag zu verknüpfen und den Stil des Eintrags auf MFT_OWNERDRAW umzusetzen. Hierfür wird die API-Funktion SetMenuItemInfo(...) aufgerufen. Die Bedeutung der Parameter entspricht denen der Funktion GetMenuItemInfo(...). Das Element fMask der MENUITEMINFO-Struktur muss nun auf MIIM_DATA und MIIM_TYPE gesetzt werden:

void CMainFrame::ChangeMenuItem(CMenu * pMenu, int nIndex)
{
    HICON        hMenuIcon;
    strMENUDATA  *pstrMenuData;
    MENUITEMINFO MenuInfo;
    char         acMenuText[80];

    // Lese Info ueber aktuellen Menue-Eintrag aus
    MenuInfo.cbSize = sizeof(MenuInfo);
    MenuInfo.fMask = MIIM_DATA|MIIM_TYPE|MIIM_ID;
    MenuInfo.dwTypeData = acMenuText;
    MenuInfo.cch = sizeof(acMenuText);
    ::GetMenuItemInfo(*pMenu,nIndex,TRUE,&MenuInfo);

    // Falls Trennline, dann fertig!
    if (MenuInfo.fType == MFT_SEPARATOR)
        return;

    // Werte nun Menue-ID aus und lade das entsprechende Icon
    switch (MenuInfo.wID)
    {
    case ID_FILE_NEW:
        hMenuIcon = (HICON)LoadImage(AfxGetInstanceHandle(),MAKEINTRESOURCE(IDI_FILENEW),
                                    IMAGE_ICON,16,16,LR_DEFAULTCOLOR);
        break;
    case ID_FILE_OPEN:
        ....
// Fuer restliche Menue-Eintraege Icons entsprechend laden
        break;
    default:
        // Menu-Eintrag benoetigt kein Icon
        // -> kein OWNERDRAW erforderlich -> fertig
        return;
    }
    // Menu-Daten in eigener Struktur sichern
    pstrMenuData = new strMENUDATA;
    // Menue-Text sichern
    pstrMenuData->CMenuText = (LPCSTR)MenuInfo.dwTypeData;
    // Icon-Handle des Menue-Eintrags sichern
    pstrMenuData->hIconSelected = hMenuIcon;
    // Menue-Eintrag jetzt auf OWNERDRAW umsetzen
    MenuInfo.fType = MFT_OWNERDRAW;
    // Zeiger auf Sicherungsstruktur im Menue-Eintrag ablegen
    MenuInfo.fMask = MIIM_DATA|MIIM_TYPE;
    MenuInfo.dwItemData = (DWORD)pstrMenuData;
    MenuInfo.dwTypeData = (LPTSTR)pstrMenuData;
    // Menue-Eintrag modifizieren
    ::SetMenuItemInfo(*pMenu,nIndex,TRUE,&MenuInfo);
}

Kehren wir jetzt wieder zur OnCreate(...) Methode zurück. Nachdem alle Menüeinträge durch den Aufruf der Methode ChangeMenuItem(...) modifiziert wurden, fehlt uns noch ein wichtiger Parameter. Da wir, wie bereits schon mehrfach erwähnt, den Menüeintrag selbst zeichnen müssen, benötigen wir noch den Font der für die Menü-Texte normalerweise verwendet wird. Schließlich sollen die geänderten Menüeinträge ja mit der gleichen Schrift wie die original Menüeinträge ausgegeben werden. Um diesen Font zu ermitteln, wird die API-Funktion SystemParametersInfo(...) mit der Kennung SPI_GETNONCLIENTMETRICS aufgerufen.

Sehen Sie sich in der Online-Hilfe auch ruhig einmal die Beschreibung der Funktion SystemParametersInfo(...) an. Diese an und für sich relativ unbekannte Funktionen liefert Ihnen fast alle Informationen über Ihr System.

SystemParametersInfo(...) liefert u.a. eine LOGFONT-Struktur mit den Font-Daten zurück. Nun kann über CreateFontIndirekt(...) der für die Menü-Texte zu verwendende Font erstellt werden. Somit sieht unsere komplette OnCreate(...) Methode jetzt wie folgt aus:

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
        return -1;
   
    // TODO: Speziellen Erstellungscode hier einfügen
    // Zeiger auf CMenu-Objekt fuer Fenstermenue
    CMenu* pFrameMenu = GetMenu() ;
    // Durchlaufe nun alle Popup-Menues um diese eventl.
    // mit einem Icon zu versehen
    for (UINT iMenuIndex=0; iMenuIndex<pFrameMenu->GetMenuItemCount(); iMenuIndex++)
    {
        // Zeiger auf CMenu-Objekt fuer Popup-Menue
        CMenu *pSubMenu = pFrameMenu->GetSubMenu(iMenuIndex);
        // Alle Menue-Eintraege durchsuchen
        for (UINT iSubMenuIndex=0; iSubMenuIndex<pSubMenu->GetMenuItemCount();
             iSubMenuIndex++)
            // Menue-Eintrage anpassen
            ChangeMenuItem(pSubMenu,iSubMenuIndex);
    }
    // Lade Icon fuer gesperrten Menue-Eintrag
    m_hIconDisabled = (HICON)LoadImage(AfxGetInstanceHandle(),MAKEINTRESOURCE(IDI_DISABLED),
                                        IMAGE_ICON,16,16,LR_DEFAULTCOLOR);
    // Kopie des Menue-Fonts erstellen
    NONCLIENTMETRICS info;
    info.cbSize = sizeof(info);
    ::SystemParametersInfo(SPI_GETNONCLIENTMETRICS,sizeof(info), &info, 0);
    m_pCMenuFont = new CFont;
    m_pCMenuFont->CreateFontIndirect(&info.lfMenuFont);

    return 0;
}

So, nun geht's langsam ans Zeichnen der Menüeinträge. Damit WINDOWS das Popup-Menü richtig aufbauen kann, sendet es für jeden Eintrag der den Stil MFT_OWNERDRAW besitzt, eine WM_MEASUREITEM Nachricht an die Anwendung. Innerhalb der MFC wird dadurch die CWnd-Methode OnMeasureItem(....) aufgerufen. Für unseren Fall ist nur der letzte Parameter der aufgerufenen Methode von Interesse. Er zeigt auf eine MEASUREITEMSTRUCT Struktur. Über diese Struktur muss die Anwendung WINDOWS mitteilen, wie viel Platz für den aktuellen Menüeintrag benötigt wird. Wie Sie sicher noch wissen, haben wir in der Methode ChangeMenuItem(...) dem Menüeintrag einen Zeiger auf eine dynamisch angelegte Struktur hinzugefügt. In dieser Struktur wurde unter anderem der Menü-Text abgespeichert. Und diesen Struktur-Zeiger können wir in der OnMeasureItem(...) Methode auslesen und darüber den vom Menü-Text belegten Platz berechnen. Zu diesem Platz wird dann noch der vom Icon benötigte Platz sowie ein Zwischenraum zwischen dem Icon und dem Text hinzuaddiert. Der letztendlich benötigt Platz wird dann über die Strukturelemente itemWidth und itemHeight der MEASUREITEMSTRUCT Struktur an WINDOWS zurückgegeben.

void CMainFrame::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
    // TODO: Code für die Behandlungsroutine für Nachrichten hier einfügen...
    // Falls Menue-Eintrag OWNERDRAW Attribut besitzt
    if (lpMeasureItemStruct->CtlType == ODT_MENU)
    {
        int    nTextWidth;

        // Hilfs-DC erstellen
        CWindowDC     CTestDC(NULL);
        // Zeiger auf eigene Menue-Daten holen
        strMENUDATA     *pMenuData = (strMENUDATA*)lpMeasureItemStruct->itemData;
        // Vom Menue-Text benoetigten Platz berechnen
        // ACHTUNG! DrawText(...) gibt hier nichts aus sondern berechnet
        // nur den vom Menue-Text benoetigten Platz
        CRect CFontSpace(0,0,0,0);
        CFont *pOldFont = CTestDC.SelectObject(m_pCMenuFont);
        CTestDC.DrawText(pMenuData->CMenuText,CFontSpace,DT_CALCRECT|DT_LEFT|DT_VCENTER);
        CTestDC.SelectObject(pOldFont);
        nTextWidth = CFontSpace.Width();
        // Fuer den Menue-Eintrag benoetigten Platz nun um die Icon-Breite
        // sowie den Leerraum zwischen dem Icon und Text korrigieren
        nTextWidth += nICONWIDTH;
        nTextWidth += nICONSPACE;
        // Platz fuer Checkmark noch abziehen
        nTextWidth -= GetSystemMetrics(SM_CXMENUCHECK);
        // Benoetigten Platz nun zurueckmelden
        lpMeasureItemStruct->itemWidth = nTextWidth;
        lpMeasureItemStruct->itemHeight = max(nICONHEIGHT,GetSystemMetrics(SM_CYMENU));
    }
    else
        // kein OWNERDRAW Menue, normale Behandlung der Nachricht
        CFrameWnd::OnMeasureItem(nIDCtl, lpMeasureItemStruct);
}

Das eigentliche Zeichnen der Menüeinträge erfolgt in der Methode OnDrawItem(...). Diese Methode erhält u.a. einen Zeiger auf eine Struktur vom Typ DRAWITEMSTRUCT. In dieser Struktur sind u.a. die ID des Menüeintrags abgelegt sowie der DC, der zum Zeichnen des Menüeintrags verwendet wird. Ferner enthält das Element itemData in unserem Fall den Zeiger auf die in ChangeMenuItem(...) angelegt dynamische Struktur mit dem Menü-Text und dem Icon-Handle. Wie der Menü-Eintrag letztendlich zu zeichnen ist, wird durch das Element itemAction bestimmt. itemAction kann folgende Werte annehmen:

itemAction

Bedeutung

ODA_DRAWENTIRE Der gesamte Menüeintrag muss neu gezeichnet werden.
ODA_FOCUS Der Menüeintrag hat den Focus erhalten bzw. abgegeben. Welcher Zustand hier gilt wird über das Bit ODS_FOCUS im Strukturelement itemState festgelegt.
ODA_SELECT Der Menüeintrag wurde selektiert bzw. deselektiert. Welcher Zustand hier gilt wird über das Bit ODS_SELECT im Strukturelement itemState festgelegt.

Mit diesem 'Wissen' können die Menüeinträge nun wie folgt gezeichnet werden:

void CMainFrame::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct)
{
    // TODO: Code für die Behandlungsroutine für Nachrichten hier einfügen....
    CBrush    CBackBrush;
    // Zeiger auf eigene Menue-Daten holen
    strMENUDATA    *pMenuData = (strMENUDATA*)lpDrawItemStruct->itemData;
    // DC fuer Menue-Eintrag holen
    CDC *pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
    // Parameter fuer Menue-Eintrag holen/berechnen
    UINT wState  = lpDrawItemStruct->itemState;
    UINT wAction = lpDrawItemStruct->itemAction;
    int  nWidth  = lpDrawItemStruct->rcItem.right-lpDrawItemStruct->rcItem.left;
    int  nHeight = lpDrawItemStruct->rcItem.bottom-lpDrawItemStruct->rcItem.top;
   
    // Zeichenanforderung auswerten
    switch (lpDrawItemStruct->itemAction)
    {
    case ODA_SELECT:   
// Menue-Eintrag wurde selektiert/deselektiert
        if (wState&ODS_SELECTED)
            ::FillRect(*pDC,&lpDrawItemStruct->rcItem,GetSysColorBrush(COLOR_HIGHLIGHT));
        else
            ::FillRect(*pDC,&lpDrawItemStruct->rcItem,GetSysColorBrush(COLOR_MENU));
            // ACHTUNG! Hier steht kein break!
    case ODA_DRAWENTIRE:    // Menue-Eintrag muss neu gezeichnet werden
        // Falls Menue-Eintrag gesperrt, Sperr-Icon zeichnen sonst
        // zum Menue-Eintrag gehoeriges Icon zeichnen
        if (wState&ODS_DISABLED)
            DrawIconEx(*pDC,lpDrawItemStruct->rcItem.left,lpDrawItemStruct->rcItem.top,
                        m_hIconDisabled,0,0,0,0,DI_NORMAL);
        else
            DrawIconEx(*pDC,lpDrawItemStruct->rcItem.left,lpDrawItemStruct->rcItem.top,
                        pMenuData->hIconSelected,0,0,0,0,DI_NORMAL);
        // Startposition fuer Menue-Text berechnen
        lpDrawItemStruct->rcItem.left += nICONWIDTH+nICONSPACE;
        // Menue-Text transparent ausgeben
        pDC->SetBkMode(TRANSPARENT);
        // Textfarbe setzen
        if (wState&ODS_DISABLED)
            pDC->SetTextColor(GetSysColor(COLOR_GRAYTEXT));
        else if (wState&ODS_SELECTED)
            pDC->SetTextColor(GetSysColor(COLOR_HIGHLIGHTTEXT));
        else
            pDC->SetTextColor(GetSysColor(COLOR_MENUTEXT));
        // Menue-Text ausgeben
        pDC->DrawText(pMenuData->CMenuText,&lpDrawItemStruct->rcItem,
                      DT_EXPANDTABS|DT_VCENTER);
        break;
    }
}

Beachten Sie im obigen Listing bitte wie die Schriftfarbe für Menüeinträge gesetzt wird. Da die Schriftfarbe für die Menü-Texte von den Einstellungen in der Systemsteuerung abhängt, darf hier keine fixe Farbe verwendet werden. Vielmehr wird die aktuell zu verwendende Schriftfarbe mit Hilfe der API-Funktion GetSysColor(...) ausgelesen.

Noch ein Hinweis zum Schluss. Selbstverständlich müssen am Programmende alle belegten Ressourcen auch wieder freigeben. Im Beispiel wird die Methode OnDestroy(...) für diese Aufräumarbeiten verwendet.

Das fertige Beispiel finden Sie unter 07Ressourcen\MenuIcons.



Copyright © 2004

Senden Sie Emails mit Fragen oder Kommentaren zu dieser Website an: mailto:info@cpp-tutor.de
 Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein. Tel: +49 7129 6470