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.
|