|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
SummaryThe 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. IntroductionSo 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:
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. BackgroundAs 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 MenuOur 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
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
In the class, declare the menu items. // Declare the MenuItems to insert under the Edit menu
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()
{
// Create
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();
// Initialize
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. // The main entry point for this module. Used for initializing
// the Edit menu of the Windows Forms application.
public void ConnectMenus(MenuItem miEdit)
{
// Subsequent calls are ignored.
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 UtilitiesWhen 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 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;
/// Windows API SendMessage functions
[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);
// Return the handle of the window that has the focus.
[DllImport("user32.dll")]
public static extern IntPtr GetFocus();
/// Windows API GetParent function
[DllImport("user32", SetLastError = true)]
public extern static IntPtr GetParent(IntPtr hwnd);
// The constructor. Not used since all methods are static.
public Win32API(){}
Add a method to obtain the framework control that is associated with the currently focused control. // Return the Framework control associated with the specified handle.
public static Control GetFrameworkControl(IntPtr hControl)
{
Control rv = null;
if( hControl.ToInt32() != 0 )
{
rv = Control.FromHandle(hControl);
// Try the parent, since with a ComboBox, we get a
// handle to an inner control.
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 // Edit commands for the inner textbox in the ComboBox control.
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;
}
// Determine whether the clipboard contains any format
// that can be pasted into a rich text box.
public static bool CanPasteAnyFormat(IntPtr hRichText)
{
return SendMessage(hRichText, EM_CANPASTE, 0,0) != 0;
}
// Determine the length of a control's Text. Required since using the
// RichTextBox .Length property wipes out the Undo/Redo buffer.
public static int GetTextLength(IntPtr hControl)
{
GETTEXTLENGTHEX lpGTL = new GETTEXTLENGTHEX();
lpGTL.uiFlags = 0;
lpGTL.uiCodePage = 1200; // Unicode
return SendMessage(hControl, EM_GETTEXTLENGTHEX,
ref lpGTL, IntPtr.Zero);
}
Responding to the Edit Menu Popup EventWe now have everything we need to setup the menu when the user clicks on the
edit menu item. We use the Enable or Disable Menu ItemsNow 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 // Declare an enumeration to represent the
// visible/invisible enabled/disabled
// state of the menu.
[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 MenusNow, write the Popup event handler. We'll start with // When the user clicks on the Edit menu, determine the focused control,
// call a method to get state flags, then setup the edit menu based
// on the flags.
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);
// Show or hide and enable or disable menu
// controls according to eEditState.
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 // Obtain EditState flags for TextBox and RichTextBox controls.
private EditState GetTextBoxEditState(TextBoxBase textbox)
{
// Set Booleans defining the textbox state.
bool bWritable = (textbox.ReadOnly == false && textbox.Enabled == true);
bool bTextSelected = (textbox.SelectionLength > 0);
// Cannot use textbox.TextLength, because that
// wipes out Undo/Redo buffer in RichTextBox
bool bHasText = Win32API.GetTextLength(textbox.Handle) > 0;
bool bIsRichText = (textbox is RichTextBox);
// Use the Booleans to set the EditState flags.
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 // TextBox
{
if( Clipboard.GetDataObject().GetDataPresent(DataFormats.Text) )
eState |= EditState.PasteEnabled;
}
}
return eState;
}
The method looks at various aspects of the ComboBox (DropDown style) ControlAnother common control that we'd like the edit menu to operate against is the
...
else if( ctlFocus is ComboBox &&
((ComboBox)ctlFocus).DropDownStyle == ComboBoxStyle.DropDown )
eEditState = GetComboBoxEditState(hFocus, (ComboBox)ctlFocus);
...
And write the code to get the // Obtain EditState flags for ComboBox controls.
private EditState GetComboBoxEditState(IntPtr hEdit, ComboBox combobox)
{
// Set Booleans defining the ComboBox state.
bool bWritable = combobox.Enabled;
bool bClipboardText =
Clipboard.GetDataObject().GetDataPresent(DataFormats.Text);
bool bTextSelected = combobox.SelectionLength > 0;
bool bHasText = combobox.Text.Length > 0;
// Use the Booleans to set the EditState flags.
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 Custom ControlsWhile 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 // Define the public interface for user defined editable controls.
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 else
{
// If this is not a simple control, search up the parent chain for
// a custom editable control.
ISupportsEdit ctlEdit = GetISupportsEditControl(ctlFocus);
if( ctlEdit != null )
eEditState = GetISupportsEditState(ctlEdit);
}
The // Takes a control and traverses the parent chain until it finds a control
// that supports the ISupportsEdit interface. Returns that control, or null
// if none is found.
private ISupportsEdit GetISupportsEditControl(Control ctlFocus)
{
while( !(ctlFocus is ISupportsEdit) && ctlFocus != null )
ctlFocus = ctlFocus.Parent;
return (ISupportsEdit)ctlFocus;
}
// Set EditState flags for ISupportsEdit controls.
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 CommandWe'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. // Click handler for all edit menus. Determine the focused window
// and framework control, then call a method to take the appropriate
// action, based on the type of the control.
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 // Perform the command associated with MenuIndex on the specified TextBoxBase.
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
The methods for // Perform the command associated with MenuIndex
// on the specified ComboBox.
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;
}
}
// Perform the command associated with MenuIndex on
// the specified ISupportsEdit control.
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 ProjectI'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. ConclusionWell, 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 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
| ||||||||||||||||||||