Introduction
I required a simple WTL control to display a text in an arbitrary color, with the option to enable and disable editing the content. The disabled edit control always displays the text grayed, hence the only alternative is to set the read-only
attribute of the edit control. However, such control can receive the focus and the user can move the caret and make text selection. This can be useful when content of the control needs to be copied, but is often confusing for the end user. Therefore, I created a simple edit control that:
- allows text color customization
- can be easily switched to read-only state (and vice versa) and
- can be switched to disabled state (and vice versa) still displaying the text in the selected color
This article does not reveal any new feature that has not been described elsewhere. For example, basic principles have been described in articles “Using colors in CEdit and CStatic” and “Changing the background color of a read-only edit control”. Similarly, “Fancy controls” include a highly customizable edit control. What this article offers is a compilation of those principles, providing as simple as possible control with a consistent behavior.
Implementation
The following sections briefly describe the implementation of the CColoredReadOnlyEdit
class that has been derived from CEdit
control to modify its appearance and behavior:
class CColoredReadOnlyEdit : public CWindowImpl<CColoredReadOnlyEdit, CEdit>
{
};
Text Color
To customize the display of the edit control content, the handler for reflected WM_CTRLCOLOREDIT
message must be implemented:
class CColoredReadOnlyEdit : public CWindowImpl<CColoredReadOnlyEdit, CEdit>
{
public:
BEGIN_MSG_MAP(CColoredReadOnlyEdit)
MSG_OCM_CTLCOLOREDIT(OnCtlColor)
END_MSG_MAP()
HBRUSH OnCtlColor(CDCHandle dc, CEdit edit)
{
dc.SetTextColor(m_textColor);
int backColorIndex = IsReadOnly() ? COLOR_3DFACE : COLOR_WINDOW;
dc.SetBkColor(::GetSysColor(backColorIndex));
return ::GetSysColorBrush(backColorIndex);
}
};
The message handler sets the text and background color and must return the brush used to fill the edit control area as the result. Only text color is adjustable in this implementation and its value is saved as a class member COLORREF m_textColor
. This value is set to the default color corresponding to the COLOR_WINDOWTEXT
constant in the control constructor but can be changed by calling the SetTextColor()
method.
Since the control handles WM_CTRLCOLOREDIT
message reflected from the parent dialog, it is of utmost importance to include the REFLECT_NOTIFICATIONS
macro into parent dialog message map:
class CMainDlg : public CDialogImpl<CMainDlg>, public CWinDataExchange<CMainDlg>
{
public:
BEGIN_MSG_MAP(CMainDlg)
REFLECT_NOTIFICATIONS()
END_MSG_MAP()
};
If this is omitted, the message would not be returned to the control and the handler does not have any effect!
Note that when the edit control is set to read-only mode (e.g., by invoking the SetReadOnly()
method or by setting the Read Only property in resource designer), the WM_CTRLCOLORSTATIC
message is reflected instead of WM_CTRLCOLOREDIT
. Therefore, we must extend the message map for this message too. Handlers for both messages are the same, so the existing implementation is sufficient:
BEGIN_MSG_MAP(CColoredReadOnlyEdit)
MSG_OCM_CTLCOLOREDIT(OnCtlColor)
MSG_OCM_CTLCOLORSTATIC(OnCtlColor)
END_MSG_MAP()
Preventing Undo Operation
If the text is changed and then the control switched to read-only mode, the user can still undo the last text change, either through the context menu or by a keyboard shortcut (usually ALT + Back or CTRL + Z) as illustrated by the screenshot below. Obviously, edit control was not meant to be switched from/to read-only state dynamically.
To avoid this unexpected behavior, we must add a handler for the WM_UNDO
message which will check if the control is in read-only mode. In such an instance, the method will simply return; otherwise, it will pass the message to the base class to handle it:
BEGIN_MSG_MAP(CColoredReadOnlyEdit)
MSG_WM_UNDO(OnUndo)
END_MSG_MAP()
void OnUndo()
{
if (IsReadOnly())
return;
SetMsgHandled(FALSE);
}
IsReadOnly()
is a helper method:
BOOL IsReadOnly() const
{
return (GetWindowLong(GWL_STYLE) & ES_READONLY) == ES_READONLY;
}
Now the undo functionality is actually disabled for read-only
state, but the context menu still displays the corresponding item as enabled. Modifying items in the context menu is a bit trickier, however. At first sight, the WM_CONTEXTMENU
message handler seems to be the appropriate place. But, when this handler is invoked, the context menu doesn’t exist yet and thus cannot be modified. Menu handling must be postponed to the moment when the context menu has been already popped-up, e.g. when context menu raises the WM_ENTERIDLE
message:
BEGIN_MSG_MAP(CColoredReadOnlyEdit)
MSG_WM_ENTERIDLE(OnEnterIdle)
END_MSG_MAP()
void OnEnterIdle(UINT nWhy, HWND hWho)
{
if (nWhy == MSGF_MENU && IsReadOnly())
{
MENUBARINFO mbi = {0};
mbi.cbSize = sizeof(MENUBARINFO);
::GetMenuBarInfo(hWho, OBJID_CLIENT, 0, &mbi);
HMENU hMenu = mbi.hMenu;
::EnableMenuItem(hMenu, WM_UNDO, MF_BYCOMMAND | MF_GRAYED);
::EnableMenuItem(hMenu, 0x8013, MF_BYCOMMAND | MF_GRAYED);
}
SetMsgHandled(FALSE);
}
The above handler also disables “Insert Unicode control character” menu (with ID equal to 0x8013
), which is normally not disabled.
To prevent menu operations inside the message handler to be executed repeatedly all the time, the context menu is displayed, a control flag m_contextMenuShownFirstTime
has been added to the class. This flag is initially set inside the WM_CONTEXTMENU
handler and reset when the OnEnterIdle()
handler is invoked for the first time. The corresponding code is not provided for the sake of brevity, but the reader can take a look at the attached source code.
Disabling the Control
As mentioned in the introduction, one of the goals was to create a control that can be completely disabled so that it cannot receive the focus, nevertheless, still displaying the text in the selected color. To achieve this, message handlers for WM_SETFOCUS
, WM_LBUTTONDOWN
and WM_LBUTTONDBLCLK
messages must be added. To distinguish this state from the classic disabled state (which displays the content grayed), I named it “Cannot receive focus” state. A distinct class member flag m_cannotReceiveFocus
is used to store the state and it can be changed through a publicly available SetCannotReceiveFocus()
method.
When the control is disabled, the WM_SETFOCUS
message handler must pass the focus to the next control if the control receives focus while the user is tabbing through the dialog. The user can tab to the control either from the preceding control (by TAB key) or from the succeeding one (by SHIFT + TAB key) in the tab order. In the former case, the control must pass the focus to the next control in the tab order and in the latter, it must pass the focus to the preceding control:
BEGIN_MSG_MAP(CColoredReadOnlyEdit)
MSG_WM_SETFOCUS(OnSetFocus)
END_MSG_MAP()
void OnSetFocus(HWND hwndOldFocus)
{
if (m_cannotReceiveFocus)
{
CWindow parent = GetParent();
CWindow nextDlgTabItem = parent.GetNextDlgTabItem(m_hWnd);
if (nextDlgTabItem != hwndOldFocus)
nextDlgTabItem.SetFocus();
else
parent.GetNextDlgTabItem(m_hWnd, TRUE).SetFocus();
return;
}
SetMsgHandled(FALSE);
}
Handlers for WM_LBUTTONDOWN
and WM_LBUTTONDBLCLK
messages must simply override the base class implementation to prevent setting the focus and selecting the entire text respectively:
BEGIN_MSG_MAP(CColoredReadOnlyEdit)
MSG_WM_LBUTTONDOWN(OnLeftButton)
MSG_WM_LBUTTONDBLCLK(OnLeftButton)
END_MSG_MAP()
void OnLeftButton(UINT nFlags, CPoint point)
{
if (!m_cannotReceiveFocus)
SetMsgHandled(FALSE);
}
Handling ES_NOHIDESEL Style
Edit control with ES_NOHIDESEL
style shows the selection even when it does not have the focus. Although this style is used very rarely, it should be handled too: when the control is disabled, it should not show any selection. Consequently, when the control is being switched to the “Cannot receive focus” state, the current selection is stored into the class member m_selection
and the selection removed. When the control is enabled later, the selection is restored:
void SetCanReceiveFocus(BOOL canReceiveFocus = TRUE)
{
if (cannotReceiveFocus == m_cannotReceiveFocus)
return;
if (cannotReceiveFocus)
{
m_selection = GetSel();
if (GetNoHideSelection())
SetSelNone(TRUE);
}
else if (GetNoHideSelection())
SetSel(m_selection, TRUE);
m_cannotReceiveFocus = cannotReceiveFocus;
}
This functionality is demonstrated by the lower edit control in the demo application which has the ES_NOHIDESEL
style set.
Still, the above method is not complete. The disabled (i.e., “Cannot receive focus”) state is closely related to the read-only
state: if the control is disabled, it is in the read-only
state. If the read-only
state is reset, the control must be enabled, too. Therefore, setter methods for both states must handle the other state appropriately:
void SetReadOnly(BOOL readOnly = TRUE)
{
if (readOnly == IsReadOnly())
return;
if (readOnly == FALSE)
SetCanReceiveFocus();
CEdit::SetReadOnly(readOnly);
}
void SetCanReceiveFocus(BOOL canReceiveFocus = TRUE)
{
if (canReceiveFocus == m_canReceiveFocus)
return;
if (!canReceiveFocus)
{
SetReadOnly();
m_selection = GetSel();
if (GetNoHideSelection())
SetSelNone(TRUE);
}
else if (GetNoHideSelection())
SetSel(m_selection, TRUE);
m_canReceiveFocus = canReceiveFocus;
}
Using the Code
Only four steps are required to use and activate the control:
- Include ColoredReadOnlyEdit.h file into your project.
- Add
CColoredReadOnlyEdit
control instances to a dialog, for example:
CColoredReadOnlyEdit m_ctlEdit;
CColoredReadOnlyEdit m_ctlEditWNoHideSel
- Subclass the edit control you want to replace with this control. This can be achieved using the
DDX_CONTROL
macro:
BEGIN_DDX_MAP(CMainDlg)
DDX_CONTROL(IDC_EDIT, m_ctlEdit)
END_DDX_MAP()
or manually subclassing it in the WM_INITDIALOG
message handler of the parent dialog:
m_ctlEditWNoHideSel.SubclassWindow(GetDlgItem(IDC_EDIT_NOHIDESEL));
- Do not forget to append the
REFLECT_NOTIFICATIONS
macro to the dialog message map:
BEGIN_MSG_MAP(CMainDlg)
REFLECT_NOTIFICATIONS()
END_MSG_MAP()
Besides the standard interface provided by the base CEdit
class, there are two additional methods through which you can customize the control:
void SetTextColor(COLORREF newColor);
void SetCannotReceiveFocus(BOOL cannotReceiveFocus = TRUE);
History
- 5th August, 2013 - Initial version