Summary
The code in this article creates a fully functional Edit menu that you can add to a WinForms application by calling one function. You can add support for non-text editing features by implementing a simple interface on controls that display the non-text editing objects.
Introduction
So you've almost finished writing your new application and all that is left are a few finishing touches. You know that you need an edit menu, but what's the best way to hook it into your project? If you have a single edit surface, it's probably a no-brainer, but if your project is like mine, you have many distinct controls that are dynamically loaded as the user context changes and any of them may contain editable constituent controls. You may also have editable task panes and controls in navigation bars that are constantly changing.
If you try to track the state of all these controls, update the menus enabled and visible properties and respond appropriately to menu click events, your code can become a jumble. This article presents a UI design pattern that takes this complex situation and makes it surprisingly (well, surprising to me anyway) simple by deferring menu manipulation until the moment the menu is shown. You end up with a completely independent component that knows nothing about the application hosting the edit menu. The steps are:
- Respond to the (Edit menu expanded or clicked) event as the menu is about to be shown.
- Use the Win32 API to determine which window currently has the focus.
- Use the
Control.FromHandle()
.NET Framework method to get a reference to the .NET control associated with the focused window. - Determine the type of the control that has the focus.
- Based on the control's type and state, enable or disable and hide or show the various edit menu items.
- When the user selects one of the menu items, pass the command on to the focused control.
Of course, as always, the devil is in the details. In this article, we'll go step by step through the process of creating the magical edit menu manager and solving the various little problems encountered along the way. When we're done, you'll have a component you can easily reuse in any WinForms project.
Background
As it was with my previous article, I'm sure that readers will have plenty of criticisms, comments and suggestions for improvement. Please feel free to share them, and I'll do my best to update the article accordingly.
Creating the Menu
Our goal is to create a separate module that can easily be reused in any C# application. To this end, we'll create and manage the edit menu inside of our module. All the calling application needs to do is pass in the top level edit menu. The Edit Menu Manager populates it.
Start by creating a new Windows Application Project called MenuManagers
. Delete Form1.cs and add a new Class
called EditMenuManager
. We'll use this to define and manage the edit menu items. Add using
directives for some WinForms namespaces.
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
In the class, declare the menu items.
private MenuItem m_miEdit;
private MenuItem m_miUndo;
private MenuItem m_miRedo;
private MenuItem m_miCut;
private MenuItem m_miCopy;
private MenuItem m_miPaste;
private MenuItem m_miDelete;
private MenuItem m_miDividerRedoCut;
private MenuItem m_miDividerPasteDelete;
private MenuItem m_miSelectAll;
private MenuItem m_miDividerPropertiesSelectAll;
private MenuItem m_miProperties;
Create an enumeration corresponding to the positions of the menu items in the menu. This makes it a little bit easier to respond to menu commands.
[Flags]
private enum MenuIndex
{
Undo = 0,
Redo = 1,
Divider1 = 2,
Cut = 3,
Copy = 4,
Paste = 5,
Divider2 = 6,
Delete = 7,
SelectAll = 8,
Divider3 = 9,
Properties = 10
}
Create a function to create and initialize the menu items and call it from the constructor.
private void CreateMenus()
{
m_miUndo = new MenuItem();
m_miRedo = new MenuItem();
m_miCut = new MenuItem();
m_miCopy = new MenuItem();
m_miPaste = new MenuItem();
m_miDelete = new MenuItem();
m_miDividerRedoCut = new MenuItem();
m_miDividerPasteDelete = new MenuItem();
m_miSelectAll = new MenuItem();
m_miDividerPropertiesSelectAll = new MenuItem();
m_miProperties = new MenuItem();
m_miUndo.Index = (int)MenuIndex.Undo;
m_miUndo.Text = "&Undo";
m_miUndo.Shortcut = Shortcut.CtrlZ;
m_miRedo.Index = (int)MenuIndex.Redo;
m_miRedo.Text = "&Redo";
m_miRedo.Shortcut = Shortcut.CtrlY;
m_miDividerRedoCut.Index = (int)MenuIndex.Divider1;
m_miDividerRedoCut.Text = "-";
m_miCut.Index = (int)MenuIndex.Cut;
m_miCut.Text = "Cu&t";
m_miCut.Shortcut = Shortcut.CtrlX;
m_miCopy.Index = (int)MenuIndex.Copy;
m_miCopy.Text = "&Copy";
m_miCopy.Shortcut = Shortcut.CtrlC;
m_miPaste.Index = (int)MenuIndex.Paste;
m_miPaste.Text = "&Paste";
m_miPaste.Shortcut = Shortcut.CtrlV;
m_miDividerPasteDelete.Index = (int)MenuIndex.Divider2;
m_miDividerPasteDelete.Text = "-";
m_miDelete.Index = (int)MenuIndex.Delete;
m_miDelete.Text = "&Delete";
m_miDelete.Shortcut = Shortcut.Del;
m_miSelectAll.Index = (int)MenuIndex.SelectAll;
m_miSelectAll.Text = "Select A&ll";
m_miSelectAll.Shortcut = Shortcut.CtrlA;
m_miDividerPropertiesSelectAll.Index = (int)MenuIndex.Divider3;
m_miDividerPropertiesSelectAll.Text = "-";
m_miProperties.Index = (int)MenuIndex.Properties;
m_miProperties.Text = "Pr&operties";
m_miProperties.Shortcut = Shortcut.F4;
}
Now, add a public
method to let the calling application add these menus to its top level Edit menu. While we're at it, hook the Popup
event on the Edit menu so we can tell when the menu is being clicked, and hook the Click
events on the other menu items.
public void ConnectMenus(MenuItem miEdit)
{
if( m_miEdit == null )
{
CreateMenus();
m_miEdit = miEdit;
m_miEdit.MenuItems.AddRange(
new MenuItem[] {
m_miUndo,
m_miRedo,
m_miDividerRedoCut,
m_miCut,
m_miCopy,
m_miPaste,
m_miDividerPasteDelete,
m_miDelete,
m_miSelectAll,
m_miDividerPropertiesSelectAll,
m_miProperties});
m_miEdit.Popup += new System.EventHandler(Edit_Popup);
m_miUndo.Click += new System.EventHandler(Menu_Click);
m_miRedo.Click += new System.EventHandler(Menu_Click);
m_miCut.Click += new System.EventHandler(Menu_Click);
m_miCopy.Click += new System.EventHandler(Menu_Click);
m_miPaste.Click += new System.EventHandler(Menu_Click);
m_miDelete.Click += new System.EventHandler(Menu_Click);
m_miSelectAll.Click += new System.EventHandler(Menu_Click);
m_miProperties.Click += new System.EventHandler(Menu_Click);
}
}
This is the only method you'll need to call from your application.
Win32 API Utilities
When the user clicks on the Edit menu, the first thing we need to do is find out what control currently has the focus. To do this, we'll need to call a Win32 API method. Since we will need a number of Win32 methods, let's take a minute to create a utility class for them. Create a new class called Win32API
in a separate file. We'll put all API calls and related utility methods in it.
using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
Now add some declarations and imports to the class.
public class Win32API
{
[StructLayout(LayoutKind.Sequential)]
public struct GETTEXTLENGTHEX
{
public Int32 uiFlags;
public Int32 uiCodePage;
}
public const int WM_USER = 0x400;
public const int EM_CUT = 0x300;
public const int EM_COPY = 0x301;
public const int EM_PASTE = 0x302;
public const int EM_CLEAR = 0x303;
public const int EM_UNDO = 0x304;
public const int EM_CANUNDO = 0xC6;
public const int EM_CANPASTE = WM_USER + 50;
public const int EM_GETTEXTLENGTHEX = WM_USER + 95;
[DllImport("user32.dll", CharSet = CharSet.Auto,
SetLastError = true)]
public static extern int SendMessage(IntPtr hWnd,
int msg, int wParam, int lParam);
[DllImport("user32.dll", EntryPoint="SendMessage",
CharSet=CharSet.Auto )]
public static extern int SendMessage( IntPtr hWnd, int Msg,
ref GETTEXTLENGTHEX wParam, IntPtr lParam);
[DllImport("user32.dll")]
public static extern IntPtr GetFocus();
[DllImport("user32", SetLastError = true)]
public extern static IntPtr GetParent(IntPtr hwnd);
public Win32API(){}
Add a method to obtain the framework control that is associated with the currently focused control.
public static Control GetFrameworkControl(IntPtr hControl)
{
Control rv = null;
if( hControl.ToInt32() != 0 )
{
rv = Control.FromHandle(hControl);
if( rv == null )
rv = Control.FromHandle(GetParent(hControl));
}
return rv;
}
Finally, we add a few methods to fill in the gaps in the WinForms API. These will be used to provide Cut/Copy/Paste/Undo functionality to the ComboBox
control, and to work around a bug in the RichTextBox
control that causes the Undo/Redo buffer to be cleared when the Text
or TextLength
properties are read(!).
public static void Undo(IntPtr hEdit)
{
SendMessage(hEdit, EM_UNDO, 0, 0);
}
public static void Cut(IntPtr hEdit)
{
SendMessage(hEdit, EM_CUT, 0, 0);
}
public static void Copy(IntPtr hEdit)
{
SendMessage(hEdit, EM_COPY, 0, 0);
}
public static void Paste(IntPtr hEdit)
{
SendMessage(hEdit, EM_PASTE, 0, 0);
}
public static bool CanUndo(IntPtr hEdit)
{
return SendMessage(hEdit, EM_CANUNDO, 0, 0) != 0;
}
public static bool CanPasteAnyFormat(IntPtr hRichText)
{
return SendMessage(hRichText, EM_CANPASTE, 0,0) != 0;
}
public static int GetTextLength(IntPtr hControl)
{
GETTEXTLENGTHEX lpGTL = new GETTEXTLENGTHEX();
lpGTL.uiFlags = 0;
lpGTL.uiCodePage = 1200;
return SendMessage(hControl, EM_GETTEXTLENGTHEX,
ref lpGTL, IntPtr.Zero);
}
Responding to the Edit Menu Popup Event
We now have everything we need to setup the menu when the user clicks on the edit menu item. We use the GetFocus()
API call to obtain the handle of the currently focused control, then call the utility function GetFrameworkControl()
to find the first parent of that control that corresponds to a framework object. Usually, this is the control with the handle returned by GetFocus()
, but sometimes, like with the ComboBox
, it's the parent.
Enable or Disable Menu Items
Now that we've obtained a reference to the focused control, we can enable or disable the edit menu items based on its state. But before handling the Popup
event, let's define a flags enum
to represent the edit state of the control. This way, a single bit field variable can easily describe the condition of the menu. We use the [Flags]
attribute since the values will be combined in a bit flag.
[Flags]
private enum EditState
{
None = 0x0,
UndoVisible = 0x1,
RedoVisible = 0x2,
UndoEnabled = 0x4,
RedoEnabled = 0x8,
CutEnabled = 0x10,
CopyEnabled = 0x20,
PasteEnabled = 0x40,
SelectAllEnabled = 0x80,
DeleteEnabled = 0x100,
RenameEnabled = 0x200,
PropertiesEnabled = 0x400
}
TextBox and RichTextBox Menus
Now, write the Popup
event handler. We'll start with TextBoxBase
to handle both TextBox
and RichTextBox
controls and call the function GetTextBoxEditState()
to return the state of those classes of controls. Later, we'll add support for other types of controls. After obtaining the bit flag determining the editable state of the TextBoxBase
, we set the corresponding properties on the menu items.
private void Edit_Popup(object sender, System.EventArgs e)
{
IntPtr hFocus = Win32API.GetFocus();
Control ctlFocus = Win32API.GetFrameworkControl(hFocus);
EditState eEditState = EditState.None;
if( ctlFocus is TextBoxBase )
eEditState = GetTextBoxEditState((TextBoxBase)ctlFocus);
m_miUndo.Visible = (eEditState & EditState.UndoVisible) != 0;
m_miRedo.Visible = (eEditState & EditState.RedoVisible) != 0;
m_miDividerRedoCut.Visible = (m_miUndo.Visible == true
|| m_miRedo.Visible == true);
m_miUndo.Enabled = (eEditState & EditState.UndoEnabled) != 0;
m_miRedo.Enabled = (eEditState & EditState.RedoEnabled) != 0;
m_miCut.Enabled = (eEditState & EditState.CutEnabled) != 0;
m_miCopy.Enabled = (eEditState & EditState.CopyEnabled) != 0;
m_miPaste.Enabled = (eEditState & EditState.PasteEnabled) != 0;
m_miDelete.Enabled = (eEditState & EditState.DeleteEnabled) != 0;
m_miSelectAll.Enabled = (eEditState & EditState.SelectAllEnabled) != 0;
m_miProperties.Enabled = (eEditState & EditState.PropertiesEnabled) != 0;
}
The GetTextBoxEditState()
method does the work:
private EditState GetTextBoxEditState(TextBoxBase textbox)
{
bool bWritable = (textbox.ReadOnly == false && textbox.Enabled == true);
bool bTextSelected = (textbox.SelectionLength > 0);
bool bHasText = Win32API.GetTextLength(textbox.Handle) > 0;
bool bIsRichText = (textbox is RichTextBox);
EditState eState = EditState.UndoVisible;
if( bIsRichText )
{
eState |= EditState.RedoVisible;
if( ((RichTextBox)textbox).CanRedo )
eState |= EditState.RedoEnabled;
}
if( textbox.CanUndo )
eState |= EditState.UndoEnabled;
if( textbox.CanSelect )
eState |= EditState.SelectAllEnabled;
if( bTextSelected )
eState |= EditState.CopyEnabled;
if( bWritable )
{
if( bTextSelected )
{
eState |= EditState.CutEnabled;
eState |= EditState.DeleteEnabled;
}
if( bIsRichText )
{
if( Win32API.CanPasteAnyFormat(textbox.Handle) )
eState |= EditState.PasteEnabled;
}
else
{
if( Clipboard.GetDataObject().GetDataPresent(DataFormats.Text) )
eState |= EditState.PasteEnabled;
}
}
return eState;
}
The method looks at various aspects of the TextBox
(or RichTextBox
) state, like whether it is enabled, how much text is selected and so forth. And based on this, determines whether various Edit operations can be done on the contained text. Most of it is pretty straightforward. The one trick that's not obvious, is that you cannot use the TextBoxBase.TextLength
method to determine whether there is any text to select. The problem is that a bug in the RichTextBox
causes the reading of this property (or the RichTextBox.Text
property for that matter) to wipe out the entire Undo/Redo buffer in the RichTextBox
. In production code, you might consider deriving a control from RichTextBox
and overriding the Text
and TextLength
methods with ones that obtain the data using the API.
ComboBox (DropDown style) Control
Another common control that we'd like the edit menu to operate against is the ComboBox
when its style is DropDown
. We start by adding a new function call to the Edit_Popup
event handler.
...
else if( ctlFocus is ComboBox &&
((ComboBox)ctlFocus).DropDownStyle == ComboBoxStyle.DropDown )
eEditState = GetComboBoxEditState(hFocus, (ComboBox)ctlFocus);
...
And write the code to get the ComboBox
state.
private EditState GetComboBoxEditState(IntPtr hEdit, ComboBox combobox)
{
bool bWritable = combobox.Enabled;
bool bClipboardText =
Clipboard.GetDataObject().GetDataPresent(DataFormats.Text);
bool bTextSelected = combobox.SelectionLength > 0;
bool bHasText = combobox.Text.Length > 0;
EditState eState = EditState.UndoVisible;
if( Win32API.CanUndo(hEdit) )
eState |= EditState.UndoEnabled;
if( bWritable )
{
if( bTextSelected )
{
eState |= EditState.CutEnabled;
eState |= EditState.DeleteEnabled;
}
if( bClipboardText )
eState |= EditState.PasteEnabled;
}
if( bTextSelected )
eState |= EditState.CopyEnabled;
if( bHasText )
eState |= EditState.SelectAllEnabled;
return eState;
}
As with the TextBox
, we again look at various aspects of the ComboBox
to determine which menu actions are possible, and set the flags accordingly. Since there is no CanUndo
property on the ComboBox
, we pass the handle of the underlying TextBox
control to the Win32API.CanUndo()
method to determine if the previous action can be undone.
Custom Controls
While it's nice to have automatic support for standard text editing controls, many interesting edit behaviors are against non-text objects in controls you have defined. For example, you might be implementing a graphical editor that supports cut/copy and paste. Our menu can easily work with these controls so long as they've implemented an interface that lets us both obtain state information, and issue the corresponding commands to the control. We'll define a public
interface called ISupportsEdit
and define it at the end of the file.
public interface ISupportsEdit
{
bool UndoVisible { get;}
bool CanUndo { get; }
void Undo();
bool RedoVisible {get;}
bool CanRedo { get; }
void Redo();
bool CanCut { get;}
void Cut();
bool CanCopy { get; }
void Copy();
bool CanPaste { get; }
void Paste();
bool CanSelectAll { get; }
void SelectAll();
bool CanDelete { get; }
void Delete();
bool CanShowProperties { get; }
void ShowProperties();
}
Now, let's modify the Edit_Popup
event handler to check for controls that support this interface. After the checks for TextBoxBase
and ComboBox
, we add the following code:
else
{
ISupportsEdit ctlEdit = GetISupportsEditControl(ctlFocus);
if( ctlEdit != null )
eEditState = GetISupportsEditState(ctlEdit);
}
The GetISupportsEditControl()
method checks the control passed in to see if it implements the ISupportsEdit
interface. If it doesn't, it travels up the parent chain until it finds a control that does, or fails, returning null
. If we find one, we call the GetISupportsEditState()
method to query it for menu state.
private ISupportsEdit GetISupportsEditControl(Control ctlFocus)
{
while( !(ctlFocus is ISupportsEdit) && ctlFocus != null )
ctlFocus = ctlFocus.Parent;
return (ISupportsEdit)ctlFocus;
}
private EditState GetISupportsEditState(ISupportsEdit control)
{
EditState eState = EditState.None;
if( control.UndoVisible ) eState |= EditState.UndoVisible;
if( control.CanUndo ) eState |= EditState.UndoEnabled;
if( control.RedoVisible ) eState |= EditState.RedoVisible;
if( control.CanRedo ) eState |= EditState.RedoEnabled;
if( control.CanCut ) eState |= EditState.CutEnabled;
if( control.CanCopy ) eState |= EditState.CopyEnabled;
if( control.CanPaste ) eState |= EditState.PasteEnabled;
if( control.CanSelectAll ) eState |= EditState.SelectAllEnabled;
if( control.CanDelete ) eState |= EditState.DeleteEnabled;
if( control.CanShowProperties ) eState |= EditState.PropertiesEnabled;
return eState;
}
Issue the Edit Command
We're almost done. All that's left is to write an event handler for the menu click events and call methods for the various types of controls to do the requested edit operation. We start by writing the event handler for the menu click events.
private void Menu_Click(object sender, System.EventArgs e)
{
MenuItem miClicked = sender as MenuItem;
IntPtr hFocus = Win32API.GetFocus();
Control ctlFocus = Win32API.GetFrameworkControl(hFocus);
MenuIndex menuIndex = (MenuIndex)miClicked.Index;
if( ctlFocus is TextBoxBase )
DoTextBoxCommand((TextBoxBase)ctlFocus, menuIndex);
else if( ctlFocus is ComboBox &&
((ComboBox)ctlFocus).DropDownStyle == ComboBoxStyle.DropDown )
DoComboBoxCommand(hFocus, (ComboBox)ctlFocus, menuIndex);
else
{
ISupportsEdit ctlEdit = GetISupportsEditControl(ctlFocus);
if (ctlEdit != null )
DoISupportsEditCommand(ctlEdit, menuIndex);
}
}
This code gets the focused control, determines its type and passes the control and the menu command on to a method for performing the command. The method for Textbox
and RichTextBox
looks like this:
private void DoTextBoxCommand(TextBoxBase textbox, MenuIndex menuIndex)
{
switch(menuIndex)
{
case MenuIndex.Undo: textbox.Undo(); break;
case MenuIndex.Redo:
if( textbox is RichTextBox )
{
RichTextBox rt = (RichTextBox)textbox;
rt.Redo();
}
break;
case MenuIndex.Cut: textbox.Cut(); break;
case MenuIndex.Copy: textbox.Copy(); break;
case MenuIndex.Paste: textbox.Paste(); break;
case MenuIndex.Delete: textbox.SelectedText = ""; break;
case MenuIndex.SelectAll: textbox.SelectAll(); break;
case MenuIndex.Properties: break;
}
}
The code is fairly simple, with the only difference between the TextBox
and RichTextBox
is that RichTextBox
supports Redo()
, so that is called out separately.
The methods for ComboBox
and ISupportsEdit
are equally straightforward:
private void DoComboBoxCommand(IntPtr hEdit,
ComboBox combobox, MenuIndex menuIndex)
{
switch(menuIndex)
{
case MenuIndex.Undo: Win32API.Undo(hEdit); break;
case MenuIndex.Cut: Win32API.Cut(hEdit); break;
case MenuIndex.Copy: Win32API.Copy(hEdit); break;
case MenuIndex.Paste: Win32API.Paste(hEdit); break;
case MenuIndex.SelectAll: combobox.SelectAll(); break;
case MenuIndex.Delete: combobox.SelectedText = ""; break;
}
}
private void DoISupportsEditCommand(
ISupportsEdit control, MenuIndex menuIndex)
{
switch(menuIndex)
{
case MenuIndex.Undo: control.Undo(); break;
case MenuIndex.Redo: control.Redo(); break;
case MenuIndex.Cut: control.Cut(); break;
case MenuIndex.Copy: control.Copy(); break;
case MenuIndex.Paste: control.Paste(); break;
case MenuIndex.SelectAll: control.SelectAll(); break;
case MenuIndex.Delete: control.Delete(); break;
case MenuIndex.Properties: control.ShowProperties(); break;
}
}
The Test Project
I've included a test project in the download that illustrates how to use this code. It has a form with TextBox
, RichTextBox
, ComboBox
and custom TreeView
based controls (TestEditableUserControl
) on it. The TestEditableUserControl
supports the MenuManagers.ISupportsEdit
interface which lets you cut, copy and paste nodes between the trees. It also implements a trivial Property
dialog (a MessageBox
) that displays the node Text
as an illustration of the Properties
menu.
Conclusion
Well, believe it or not, we're done. If you're using WinForms menus, all you have to do is include this project in your solution, define a single Edit top level menu and pass it into the ConnectMenus()
method. If you have any custom controls that support editing capability, you'll want to implement the ISupportsEdit
interface on them. In my case, it was pretty easy, since the controls already had context menus with all the edit commands on them.
If you're using another menu system, you'll need to modify the code accordingly, but all the basic ideas of the article still apply. On this front, there is one caveat. If the menu system you're using takes the focus (it shouldn't) then the code in this article will not work, since the focused control will always appear to be the menu itself.
Anyway, good luck, have fun and let me know how it turns out if you do use this code. To be clear, you may use this code for any purpose whatsoever, personal or commercial.
History
- 1-30-2004 - First version
- 2-2-2004 - Second version. Minor editorial changes to the text of the article.
- 2-9-2004 - Third version. Fixed the bug pointed out by Dreamwolf (thank you). More editorial changes to the text.
I've been programming in C, C++, Visual Basic and C# for over 35 years. I've worked at Sierra Systems, ViewStar, Mosaix, Lucent, Avaya, Avinon, Apptero, Serena and now Guidewire Software in various roles over my career.