Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Edit Control with Arbitrary Text Color (Even When Disabled)

0.00/5 (No votes)
5 Aug 2013 1  
A simple WTL edit control that can display content in arbitrary color even when it is disabled

Demo application screenshot

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:

  1. allows text color customization
  2. can be easily switched to read-only state (and vice versa) and
  3. 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)
        // more mappings to come here...
    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.

Undo available even when CEdit is in read-only mode

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);
        // or if you prefer to delete the item:
        //::DeleteMenu(hMenu, 0x8013, MF_BYCOMMAND);
    }
    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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here