So implementieren Sie Menü-Handler für Editierfelder

Veröffentlicht: 15. Dez 2001 | Aktualisiert: 18. Jun 2004

Von Paul DiLascia

Eingabefelder verfügen über ein vorgegebenes Kontextmenü. Um dieses zu erweitern, muss man schon etwas in die Trickkiste greifen.

* * *

Nachhilfe für Windows

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

Bild03

Warum wird eigentlich keine WM_INITMENUPOPUP-Nachricht generiert, wenn man ein Eingabefeld (edit control) mit der rechten Maustaste anklickt? Nun, ich kann Ihnen zwar nicht sagen, warum das nicht geschieht, aber ich kann Ihnen verraten, wie man sich behelfen kann. Ich werde selbst hin und wieder an diesen Umstand erinnert. Meistens ergibt es sich, wenn man von einem Edit-Steuerelement eine Unterklasse ableiten möchte und einfach ein Kontextmenü braucht (Bild B1). Oder wenn man den Zustand von vorgegebenen Edit-Elementen ändern möchte.

Bild01
Manchmal braucht man ein Kontextmenü.

Normalerweise würden Sie einen entsprechenden WM_INITMENUPOPUP-Handler schreiben, die gewünschten Einträge vornehmen und sich zufrieden zurücklehnen. Aber wie Sie festgestellt haben, schicken Edit-Steuerelemente leider kein WM_INITMENUPOPUP. Das Edit-Element muss TrackPopupMenu mit einem Nullhandle (für das HWND) aufrufen und/oder TPM_NONOTIFY. Dadurch wird das Menü angewiesen, keine Hinweisnachrichten zu schicken. Es ist möglich - und hier rate ich wieder nur - dass die Autoren versucht haben, durch eine Reduktion des Nachrichtenverkehrs die Leistung zu verbessern. Man kann es sich kaum noch vorstellen, aber Windows 1.0 und 2.0 liefen auf Maschinen mit 640 KByte RAM und einem 8-MHz-Prozessor! (Hatten Eingabefelder damals schon irgendwelche Kontextmenüs? Erinnert sich jemand?)

Wie auch immer - nehmen wir an, Sie möchten Ihre eigenen Menüpunkte ins Kontextmenü des Edit-Elements einbauen. Wie geht das? Nun, Sie haben eigentlich keine andere Wahl als das Rad neu zu erfinden. Aber sehen Sie's von der positiven Seite. Es ist nämlich ziemlich einfach. Ich habe eine kleine Klasse namens CEditMenuHandler geschrieben, die sich die ganze Arbeit auflädt. Sie brauchen diese Klasse nur noch zu benutzen. Damit Sie auch genau wissen, wie, habe ich das Eingabefeld aus dem FileType-Programm vom vorigen Beispiel etwas abgeändert und drei Dateitypen fest ins Kontextmenü "eingelötet" (Bild B2).

Bild02

So sieht das Kontextmenü mit zusätzlichen Menüpunkten aus.

Um CEditMenuHandler zum Leben zu erwecken, müssen Sie drei Dinge tun (Nun, vielleicht vier, wenn man den Entwurf des Menüs mitzählt.). Zuerst legen Sie eine Instanz der Klasse an
.

class CMyEdit : public CEdit { 
protected: 
  CEditMenuHandler m_editMenuHandler; 
  virtual void PreSubclassWindow(); 
};

Anschließend hängen Sie die Instanz in den Nachrichtenfluss ein. Das können Sie in OnCreate tun. Aber wenn das Eingabefeld in einem Dialog lebt, dann ist PreSubclassWindow der Ort der Wahl, denn normalerweise legen Sie keine Instanzen der Steuerelemente an, sondern leiten Unterklassen von ihnen ab. Wenn Sie das Objekt in den Nachrichtenfluss einhängen, geben Sie ihm die ID Ihres Kontextmenüs:

void CMyEdit::PreSubclassWindow() 
{ 
  //IDR EDITMENU ist mein Kontextmenü. 
  m_editMenuHandler.Install(this, IDR_EDITMENU); 
}

So weit, so gut. Nun ist das Objekt installiert und Sie können die einzigen beiden Funktionen aufrufen, die Sie brauchen. OnUpdateEditCommand aktualisiert die Menüpunkte und OnEditCommand führt die Befehle aus.

// In der Nachrichtentabelle von CMyEdit 
ON_COMMAND_RANGE(ID_EDIT_FIRST, ID_EDIT_LAST, OnEditCommand) 
ON_UPDATE_COMMAND_UI_RANGE(ID_EDIT_FIRST,ID_EDIT_LAST,  
                           OnUpdateEditCommand) 
void CMyEdit::OnUpdateEditCommand(CCmdUI* pCmdUI) 
{ 
  m_editMenuHandler.OnUpdateEditCommand(pCmdUI)); 
} 
void CMyEdit::OnEditCommand(UINT nID) 
{ 
  m_editMenuHandler.OnEditCommand(nID); 
}

CEditMenuHandler erledigt die ganze anfallende Arbeit. Sie kümmert sich um das Ausschneiden, Kopieren, Einfügen und andere Aktionen. Außerdem sperrt sie die Menüpunkte entsprechend und schaltet sie auch nach Bedarf frei, je nachdem, ob Text markiert ist, ob es etwas zum Einfügen gibt und so weiter. Die zuständige Funktion gibt TRUE zurück, wenn sie den Befehl ausgeführt hat. Andernfalls gibt sie Ihnen mit FALSE die Gelegenheit, sich selbst um die Befehle zu kümmern, sofern Sie es möchten. In meinem Beispiel hat CMyEdit für die Befehle TXT, BMP und JPG jeweils eine eigene Funktion.

// in der Nachrichtentabelle von CMyEdit 
ON_COMMAND(ID_FILETYPE_TXT, OnFiletypeTXT) 
void CMyEdit::OnFiletypeTXT() 
{ 
  SetWindowText(_T("txt")); 
  SetSel(0,-1); 
}

CMyEdit gibt die Edierbefehle an CEditMenuHandler weiter und erledigt den Rest selbst. Dasselbe gilt für die Aktualisierung der Menüpunkte. Es läuft alles butterweich.

Natürlich ist einiges an Code erforderlich, damit alles so reibungslos funktioniert. Eigentlich ist der Aufwand sogar schon etwas zu hoch, wenn man sich vergegenwärtigt, dass CEditMenuHandler im Prinzip nur Windows neu erfindet. Aber so ist das Leben. Aber CEditMenuHandler bedient sich bereits vorhandenen Codes, nämlich einer Klasse namens CPopupMenuInitHandler, die ich in meinem Artikel "Die COMToys, Teil II", System Journal 04/2000, S. 113 beschrieben habe. CPopupMenuInitHandler hat per se nichts mit Edit-Menüs zu tun. Diese Klasse macht den Aktualisierungsmechanismus mit CCmdUI, den die MFC für Menüs anbietet, in den Kontextmenüs von beliebigen Fenstern verfügbar. Dieser wunderschöne Aktualisierungsmechanismus der MFC wird nämlich vollständig innerhalb von CFrameWnd implementiert und steht daher auch nur Rahmenfenstern zur Verfügung. In anderen Fensterarten wie zum Beispiel Edit-Steuerelementen kümmert sich die MFC nicht um WM_INITMENUPOPUP und den CCmdUI-Mechanismus. Was für eine Schande! Aber CPopupMenuInitHandler funktioniert bei jedem CWnd-Objekt. Sie ist von einer anderen Klasse namens CSubclassWnd abhängig, mit deren Hilfe man von jedem CWnd-Objekt eine Unterklasse ableiten kann.

CPopupMenuInitHandler fängt für Ihr Fenster WM_INITMENUPOPUP ab und kümmert sich dann um die Menüaktualisierung nach MFC-Art. Sie brauchen nur ein CPopupMenuInitHandler-Objekt anzulegen und zu installieren. Dann können Sie ON_UPDATE_COMMAND_UI-Funktionen für die Aktualisierung des Kontextmenüs implementieren, wie Sie es von den Rahmenfenstern her kennen. Nicht schlecht, finde ich. Weitere Einzelheiten finden Sie im erwähnten COMToys-Artikel oder im Quelltext selbst.

Hat man CPopupMenuInitHandler erst einmal zur Verfügung, ist CEditMenuHandler fast ein Kinderspiel. Listing L1 zeigt den Code. Um das Leben zu erleichtern habe ich schon eine Funktion für WM_CONTEXTMENU eingebaut. Sie brauchen also nur noch die Menü-ID anzugeben und CEditMenuHandler zeigt das Menü an, sobald der Anwender die rechte Maustaste drückt (Bild B2).

L1 Die Klasse CEditMenuHandler

EditMenu.h 
// System Journal 2/2001 
// Sofern dieser Code funktioniert, stammt er von Paul DiLascia. 
// Lässt sich mit Visual C++ 6.0 kompilieren und läuft unter  
// Windows 98. Sollte eigentlich auch unter Windows NT laufen. 
#include "menuinit.h" 
// CEditMenuHandler baut das Kontextmenü des Edit-Steuerelements 
// in einer erweiterbaren Form nach. 
// 
// Benutzen wie folgt: 
// - Rufen Sie Install auf, wenn das Edit-Steuerelement angelegt 
//   und die Unterklasse abgeleitet wird. 
// - Bearbeiten Sie mit OnEditCommand alle Befehle im Bereich von 
//   ID_EDIT_FIRST bis ID_EDIT_LAST. 
// - Kümmern Sie sich mit OnUpdateEditCommand um UPDATE_COMMAND_UI  
//   im Bereich von ID_EDIT_FIRST bis ID_EDIT_LAST. 
// 
class CEditMenuHandler : public CPopupMenuInitHandler { 
public: 
   CEditMenuHandler(); 
   ~CEditMenuHandler(); 
   BOOL Install(CWnd* pWnd, UINT nIDMenu); 
   void Remove() { Unhook(); } 
   // Diese Funktionen sind für's Ausschneiden, Kopieren und so weiter 
   // zuständig. 
   virtual BOOL OnEditCommand(UINT nID); 
   virtual BOOL OnUpdateEditCommand(CCmdUI* pCmdUI); 
protected: 
   UINT m_nIDMenu; 
   virtual LRESULT WindowProc(UINT msg, WPARAM wp, LPARAM lp); 
   virtual BOOL OnContextMenu(CPoint pt); 
}; 
#define ID_EDIT_FIRST ID_EDIT_CLEAR 
#define ID_EDIT_LAST  ID_EDIT_REDO 
EditMenu.cpp 
// System Journal 2/2001 
// Sofern dieser Code funktioniert, stammt er von Paul DiLascia. 
// Lässt sich mit Visual C++ 6.0 kompilieren und läuft unter  
// Windows 98. Sollte eigentlich auch unter Windows NT laufen. 
#include "StdAfx.h" 
#include "EditMenu.h" 
#ifdef _DEBUG 
#define new DEBUG_NEW 
#undef THIS_FILE 
static char THIS_FILE[] = __FILE__; 
#endif 
CEditMenuHandler::CEditMenuHandler() 
{ 
} 
CEditMenuHandler::~CEditMenuHandler() 
{ 
} 
// Installiert die Bearbeitungsfunktionen für das Edit-Menü. Sie müssen 
// die Menü-ID eines Menüs angeben, in dem es diese Befehle gibt. 
// 
BOOL CEditMenuHandler::Install(CWnd* pWnd, UINT nIDMenu) 
{ 
   m_nIDMenu = nIDMenu; 
   return !HookWindow(pWnd); 
} 
// Allgemeine Aktualisierung der Menüpunkte: Freischalten/Sperren von 
// Standard-Menüpunkten aufgrund des Vorhandenseins von markiertem  
// Text, einfügbarem Text und so weiter. Diese Funktion rufen Sie zur 
// Aktualisierung der Menübefehle auf, wenn eine ON_UPDATE_COMMAND_UI- 
// Hinweisnachricht für einen Edit-Befehl eintrifft. 
// 
BOOL CEditMenuHandler::OnUpdateEditCommand(CCmdUI* pCmdUI) 
{ 
   CEdit* pEditWnd = DYNAMIC_DOWNCAST(CEdit,CWnd::FromHandle(m_hWnd)); 
   if (pEditWnd) { 
      UINT nID = pCmdUI->m_nID; 
      switch (nID) { 
      case ID_EDIT_CUT: 
      case ID_EDIT_COPY: 
      case ID_EDIT_CLEAR: 
         int beg,end; 
         pEditWnd->GetSel(beg,end); 
         pCmdUI->Enable(beg<end); 
         return TRUE; 
      case ID_EDIT_UNDO: 
         pCmdUI->Enable(pEditWnd->CanUndo()); 
         return TRUE; 
      case ID_EDIT_PASTE: 
         pCmdUI->Enable(::IsClipboardFormatAvailable(CF_TEXT)); 
         return TRUE; 
      case ID_EDIT_SELECT_ALL: 
         pCmdUI->Enable(pEditWnd->GetWindowTextLength()>0); 
         return TRUE; 
      } 
   } 
   return FALSE; // nicht ausgeführt 
} 
// Erledige die üblichen Edit-Befehle: Text ausschneiden, kopieren, 
// einfügen und so weiter. Sie rufen diese Funktion auf, wenn ein 
// Edit-Befehl eintrifft (ON_COMMAND-Nachricht). 
// 
BOOL CEditMenuHandler::OnEditCommand(UINT nID) 
{ 
   CEdit* pEditWnd = DYNAMIC_DOWNCAST(CEdit,CWnd::FromHandle(m_hWnd)); 
   if (pEditWnd) { 
      switch (nID) { 
      case ID_EDIT_UNDO: 
         pEditWnd->Undo(); 
         return TRUE; 
      case ID_EDIT_CUT: 
         pEditWnd->Cut(); 
         return TRUE; 
      case ID_EDIT_COPY: 
         pEditWnd->Copy(); 
         return TRUE; 
      case ID_EDIT_PASTE: 
         pEditWnd->Paste(); 
         return TRUE; 
      case ID_EDIT_CLEAR: 
         pEditWnd->Clear(); 
         return TRUE; 
      case ID_EDIT_SELECT_ALL: 
         pEditWnd->SetSel(0,-1); 
         return TRUE; 
      } 
   } 
   return FALSE; // nicht ausgeführt 
} 
// Eine interne WindowProc fängt WM_CONTEXTMENU ab. 
// 
LRESULT CEditMenuHandler::WindowProc(UINT msg, WPARAM wp, LPARAM lp) 
{ 
   if (msg==WM_CONTEXTMENU) { 
      return OnContextMenu((CPoint)lp); 
   } 
   return CPopupMenuInitHandler::WindowProc(msg, wp, lp); 
} 
////////////////// 
// Zeige Kontextmenü an. Bei Bedarf überschreiben. 
// 
BOOL CEditMenuHandler::OnContextMenu(CPoint pt) 
{ 
   CMenu editMenu; 
   VERIFY(editMenu.LoadMenu(m_nIDMenu)); 
   CMenu* pSubMenu = editMenu.GetSubMenu(0); 
   pSubMenu->TrackPopupMenu(0,pt.x,pt.y,CWnd::FromHandle(m_hWnd)); 
   return TRUE; // ausgeführt 
} 
Der Rest ist ziemlich einfach. OnUpdateEditCommand aktualisiert die Menüpunkte entsprechend.  
Hier ein Beispiel: 
// in CEditMenuHandler::OnUpdateEditCommand 
switch (nID) { 
case ID_EDIT_PASTE: 
    pCmdUI->Enable(::IsClipboardFormatAvailable(CF_TEXT)); 
...

CEditMenuHandler schaltet also den Paste-Befehl frei, wenn es irgendeinen Text gibt, der sich einfügen ließe. CEditMenuHandler geht davon aus, dass Sie in Ihrem Menü die üblichen MFC-IDs benutzen, also ID_EDIT_CUT, ID_EDIT_COPY und so weiter. Zur Ausführung der Befehle ruft CEditMenuHandler die Methoden CEdit::Cut, CEdit::Copy usw. auf, die dem Edit-Steuerelement einfach nur die Nachricht WM_CUT, WM_COPY und so weiter schicken. CEdit::Cut, Copy und Paste gehören eigentlich nach CWnd, weil jedes Fenster sie implementieren kann. In der Praxis sind aber Edit-Steuerelemente und Kombinationsfelder, die ein Edit-Steuerelement enthalten können, die einzigen Steuerelemente, die diese Befehle implementieren.