Introduction
The CEdit control provides only basic editing functionality, like cut/copy/paste and single level undo. It is interesting to note that undo for
the single line CEdit control reverts all consecutive text changes, while in multiline controls, only the last character change is undone. This inconsistency may be pretty
annoying for the end user particularly if a dialog contains both single line and multiline edit controls.
I decided to improve undo/redo functionality of the edit control and to allow multiple levels of undo/redo. In addition, I chose to include some editing options
like deleting text from the current cursor position up to the next/previous character block or to the end/start of text. Finally, the control needs to support standard keyboard
shortcuts like Ctrl + A, Ctrl + Z, and Ctrl + Y. Advanced features like syntax highlighting and auto-completion has been left out since they are handled in the derived classes.
Background
To implement the above requirements, the class CEditEx derived from the CEdit control has been defined. CEditEx
intercepts editing actions in order to create the corresponding undo/redo operations. After the undo/redo operation is created and pushed to the undo stack,
editing command is passed to the base class for further processing. This way it is guaranteed that derived classes (e.g., the class responsible for auto-completion)
receive the same messages as if they were derived directly from the CEdit class.
Implementation
Follows a brief implementation description with the most important points of interest.
Multilevel Undo and Redo
Multilevel undo/redo is implemented using the Command design pattern. All commands are derived from the abstract CEditExCommand that has pure
virtual methods DoUndo and DoExecute. These methods are implemented in the derived classes. Commands are stored in the CCommandHistory
class that consists of undo and redo stacks.

Note that all the above classes are defined as local in the CEditEx class, making the entire code compact and easy to reuse.
As already mentioned, editing actions must be intercepted to create undo/redo commands. It is worthwhile to identify which messages must be handled:
WM_CHAR in order to track characters typed;
WM_PASTE in order to capture text that will be pasted from clipboard;
WM_CUT and WM_CLEAR (which are generated by the Delete command on the context menu) in order to capture text that will be removed;
WM_UNDO in order to call undo operation from CCommandHistory. This message must not be passed to the base CEdit class since
it would handle it in its own way.
The above messages are handled in the overridden WindowProc() method:
LRESULT CEditEx::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_CHAR:
{
wchar_t wChar = static_cast<wchar_t>(wParam);
int nCount = lParam & 0xFF;
if (iswprint(wChar)) {
CString newText(wChar, nCount);
CreateInsertTextCommand(newText);
}
}
break;
case WM_PASTE:
CreatePasteCommand();
break;
case WM_CUT:
case WM_CLEAR: if (!IsSelectionEmpty())
m_commandHistory.AddCommand(new CDeleteSelectionCommand(this,
CDeleteSelectionCommand::Selection));
break;
case WM_UNDO:
Undo();
return TRUE; }
return CEdit::WindowProc(message, wParam, lParam);
}
CreateInsertTextCommand() and CreatePasteCommand() are private methods that create commands for the corresponding editing operations.
Undo() is the overridden implementation which pops a command from the undo stack and executes it.
Unicode Control Characters
On recent versions of Windows edit controls, the context menu contains additional items like ones to enter specific Unicode control characters. Most of these
Unicode control characters are important when combining left-to-right and right-to-left writing.

When the user selects one of these control characters from the menu, the WM_CHAR message is sent to the CEdit control. But contrary
to WM_CHAR messages generated by keyboard input, this one has repeat count (contained in the lParam parameter) always equal to 0.
Thereafter, this message is handled separately inside the WindowProc() method:
LRESULT CEditEx::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_CHAR:
{
wchar_t wChar = static_cast<wchar_t>(wParam);
int nCount = lParam & 0xFF;
if (iswprint(wChar)) {
CString newText(wChar, nCount);
CreateInsertTextCommand(newText);
}
else if (nCount == 0) {
CString newText(wChar);
CreateInsertTextCommand(newText);
}
}
break;
}
return CEdit::WindowProc(message, wParam, lParam);
}
Additional Block Deletion Operations
Additional block deletion operations include:
- delete text from current cursor position up to the end of block (keyboard shortcut Ctrl + Del);
- delete text from current cursor position to the end of text (Ctrl + Shift + Del);
- delete text from the beginning of text block to the current cursor position (Ctrl + Back);
- delete text from the beginning of text to current cursor position (Ctrl + Shift + Back).
Text block delimiters are identified by character type changes between space character, punctuation, or alphanumeric character, like in the Visual Studio source
code editor. Keyboard shortcuts are captured in the overridden PreTranslateMessage() method where the WM_KEYDOWN message is handled:
BOOL CEditEx::PreTranslateMessage(MSG* pMsg) {
switch (pMsg->message) {
case WM_KEYDOWN:
if (PreTranslateKeyDownMessage(pMsg->wParam) == TRUE)
return TRUE;
break;
}
return CEdit::PreTranslateMessage(pMsg);
}
BOOL CEditEx::PreTranslateKeyDownMessage(WPARAM wParam) {
switch (wParam) {
case VK_DELETE:
return DoDelete();
case VK_BACK:
return DoBackspace();
}
return FALSE;
}
BOOL CEditEx::DoDelete() {
if ((GetKeyState(VK_CONTROL) & 0x8000) != 0) {
if ((GetKeyState(VK_SHIFT) & 0x8000) != 0)
DeleteToTheEnd();
else
DeleteToTheBeginningOfNextWord();
return TRUE;
}
if (IsSelectionEmpty() == false)
m_commandHistory.AddCommand(new CDeleteSelectionCommand(this,
CDeleteSelectionCommand::Selection));
else {
if (GetCursorPosition() < GetWindowTextLength())
m_commandHistory.AddCommand(new CDeleteCharacterCommand(this, false));
}
return FALSE;
}
BOOL CEditEx::DoBackspace() {
if ((GetKeyState(VK_CONTROL) & 0x8000) != 0) {
if ((GetKeyState(VK_SHIFT) & 0x8000) != 0)
DeleteFromTheBeginning();
else
DeleteFromTheBeginningOfWord();
return TRUE;
}
if (IsSelectionEmpty() == false)
m_commandHistory.AddCommand(new CDeleteSelectionCommand(this,
CDeleteSelectionCommand::Selection));
else {
if (GetCursorPosition() > 0)
m_commandHistory.AddCommand(new CDeleteCharacterCommand(this, true));
}
return FALSE;
}
DeleteToTheEnd(), DeleteToTheBeginningOfNextWord(), DeleteFromTheBeginning(), and DeleteFromTheBeginningOfWord()
are methods that simply extend the current selection and delete the selection applying the corresponding command.
Keyboard shortcuts
This is the easiest task: the PreTranslateMessage() method has to handle the WM_KEYDOWN method for the following shortcuts:
- Ctrl + A to select entire text;
- Ctrl + Z to undo an action;
- Ctrl + Y to redo an action
and call the corresponding operations:
BOOL CEditEx::PreTranslateKeyDownMessage(WPARAM wParam) {
switch (wParam) {
case _T('A'):
if ((GetKeyState(VK_CONTROL) & 0x8000) != 0) {
SetSel(0, -1);
return TRUE;
}
break;
case _T('Z'):
if ((GetKeyState(VK_CONTROL) & 0x8000) != 0) {
Undo();
return TRUE;
}
break;
case _T('Y'):
if ((GetKeyState(VK_CONTROL) & 0x8000) != 0) {
Redo();
return TRUE;
}
break;
}
Redo() is, like the Undo() method, an overridden implementation that gets a command from the redo stack and executes it.
For single line edit controls, Alt + Back generates the WM_UNDO message by default, so no additional code is required. However, in multiline mode,
CEdit simply ignores this keyboard shortcut! Therefore, it is necessary to capture the Alt + Back shortcut through the generated
WM_SYSCHAR message in PreTranslateMessage() and to call the Undo() method:
BOOL CEditEx::PreTranslateMessage(MSG* pMsg) {
switch (pMsg->message) {
case WM_SYSCHAR:
if (pMsg->wParam == VK_BACK) {
if (IsMultiLine())
Undo();
return TRUE;
}
break;
}
return CEdit::PreTranslateMessage(pMsg);
}
Updating the Context Menu
The original context menu contains Undo (WM_UNDO), Cut (WM_CUT), Copy (WM_COPY), Paste (WM_PASTE), Delete (WM_CLEAR),
and Select All (EM_SETSEL) entries. If the default context menu is used, then the only problem is how to correctly enable and disable the Undo menu entry.
One simple way is to implement the WM_ENTERIDLE message handler and check if it has been called because the menu has been displayed. Then the handle to the menu is obtained
and the WM_UNDO command is enabled or disabled checking if the undo command history is empty:
BEGIN_MESSAGE_MAP(CEditEx, CEdit)
ON_WM_CONTEXTMENU()
ON_WM_ENTERIDLE()
END_MESSAGE_MAP()
void CEditEx::OnContextMenu(CWnd* pWnd, CPoint point) {
m_contextMenuShownFirstTime = true;
CEdit::OnContextMenu(pWnd, point);
}
void CEditEx::OnEnterIdle(UINT nWhy, CWnd* pWho) {
CEdit::OnEnterIdle(nWhy, pWho);
if (nWhy == MSGF_MENU) {
if (m_contextMenuShownFirstTime) {
m_contextMenuShownFirstTime = false;
UpdateContextMenuItems(pWho);
}
}
}
void CEditEx::UpdateContextMenuItems(CWnd* pWnd) {
MENUBARINFO mbi = {0};
mbi.cbSize = sizeof(MENUBARINFO);
::GetMenuBarInfo(pWnd->m_hWnd, OBJID_CLIENT, 0, &mbi);
HMENU hMenu = mbi.hMenu;
if (m_commandHistory.CanUndo())
::EnableMenuItem(hMenu, WM_UNDO, MF_BYCOMMAND | MF_ENABLED);
else
::EnableMenuItem(hMenu, WM_UNDO, MF_BYCOMMAND | MF_GRAYED);
}
During code development, I was tempted to modify the default context menu. Before discussing that option, it should be outlined that the built-in context menu is localization
aware and will display the entries in the localized language even if the rest of the application is not localized, as visible from the screenshot below.

There are two approaches for context menu modification:
- replace it by a completely new custom menu or
- modify default menu on the fly.
The second approach requires less effort: it is enough to modify the menu inside the WM_ENTERIDLE message handler. For example, to insert the Redo menu entry,
the above code of UpdateContextMenuItems() should include a call to the InsertMenu() function. However, besides additional flickering caused by the menu being
rearranged while already shown, this approach would create inconsistencies if translation for the inserted menu entry is not provided since
the original menu entries are displayed in the localized language.
Replacing the built-in context menu with a completely new one requires more effort, especially if we want to mimic the default context menu with Unicode support
entries and other specific stuff. On the other hand, through controlled localization, this menu would become consistent with the rest of our application.
Since both approaches have some drawback, I just left the context menu as is by default.
How to Use the Code
Simply include the EditEx.h and EditEx.cpp files into your project and replace all CEdit class instances with the CEditEx class.
Dedication
In memory of my dad Bojan Šribar (1931 - 2011).