65.9K
CodeProject 正在变化。 阅读更多。
Home

带有任意文本颜色的编辑控件(即使禁用时)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (4投票s)

2013年8月5日

Zlib

6分钟阅读

viewsIcon

25508

downloadIcon

1044

一个简单的 WTL 编辑控件,即使在禁用时也能以任意颜色显示内容

Demo application screenshot

引言

我需要一个简单的 WTL 控件来以任意颜色显示文本,并可以选择启用和禁用编辑内容。禁用的编辑控件始终显示灰色文本,因此唯一的替代方法是设置编辑控件的 只读 属性。但是, 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. 允许自定义文本颜色
  2. 可以轻松切换到只读状态(反之亦然),并且
  3. 可以切换到禁用状态(反之亦然),仍然显示选定的颜色文本

本文没有揭示任何未在其他地方描述过的新功能。例如,基本原理已在文章“在 CEdit 和 CStatic 中使用颜色”和“更改只读编辑控件的背景颜色”中进行了描述。类似地,“花式控件”包含一个高度可定制的编辑控件。本文提供的是这些原理的汇编,提供一个尽可能简单的控件,具有一致的行为。

实现

以下各节简要描述了 CColoredReadOnlyEdit 类的实现,该类已从 CEdit 控件派生,以修改其外观和行为。

class CColoredReadOnlyEdit : public CWindowImpl<CColoredReadOnlyEdit, CEdit>
{
    // ...
};

文本颜色

要自定义编辑控件内容的显示,必须实现反射的 WM_CTRLCOLOREDIT 消息的处理程序。

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);
    }
}; 

消息处理程序设置文本和背景颜色,并必须返回用作结果的用于填充编辑控件区域的画笔。在此实现中,只有文本颜色是可调的,其值保存在类成员 COLORREF m_textColor 中。此值在控件构造函数中设置为对应于 COLOR_WINDOWTEXT 常量的默认颜色,但可以通过调用 SetTextColor() 方法进行更改。

由于控件处理从父对话框反射的 WM_CTRLCOLOREDIT 消息,因此将 REFLECT_NOTIFICATIONS 宏包含在父对话框消息映射中至关重要。

class CMainDlg : public CDialogImpl<CMainDlg>, public CWinDataExchange<CMainDlg>
{
public:
    BEGIN_MSG_MAP(CMainDlg)
        // ...
        REFLECT_NOTIFICATIONS()
    END_MSG_MAP()
};

如果忽略此项,消息将不会返回到控件,并且处理程序无效!

请注意,当编辑控件设置为只读模式时(例如,通过调用 SetReadOnly() 方法或在资源设计器中设置只读属性),会反射 WM_CTRLCOLORSTATIC 消息而不是 WM_CTRLCOLOREDIT。因此,我们也必须扩展此消息的消息映射。两个消息的处理程序相同,因此现有实现就足够了。

BEGIN_MSG_MAP(CColoredReadOnlyEdit)
    MSG_OCM_CTLCOLOREDIT(OnCtlColor)
    MSG_OCM_CTLCOLORSTATIC(OnCtlColor)
    // ...
END_MSG_MAP()

防止撤销操作

如果文本已更改然后控件切换到只读模式,用户仍然可以撤销上次的文本更改,通过上下文菜单或键盘快捷键(通常是 ALT + Back 或 CTRL + Z),如下图所示。显然,编辑控件并非旨在动态地从/切换到只读状态。

Undo available even when CEdit is in read-only mode

为了避免这种意外行为,我们必须添加一个 WM_UNDO 消息的处理程序,该处理程序将检查控件是否处于只读模式。在这种情况下,该方法将简单地返回;否则,它会将消息传递给基类进行处理。

BEGIN_MSG_MAP(CColoredReadOnlyEdit)
    // ...
    MSG_WM_UNDO(OnUndo)
    // ...
END_MSG_MAP()

void OnUndo()
{
    if (IsReadOnly())
        return;
    SetMsgHandled(FALSE);
}

IsReadOnly() 是一个辅助方法。

BOOL IsReadOnly() const
{
    return (GetWindowLong(GWL_STYLE) & ES_READONLY) == ES_READONLY;
}

现在撤销功能实际上已为 只读 状态禁用,但上下文菜单仍然将相应项显示为已启用。修改上下文菜单中的项有点棘手。乍一看,WM_CONTEXTMENU 消息处理程序似乎是合适的位置。但是,调用此处理程序时,上下文菜单尚未创建,因此无法修改。菜单处理必须推迟到上下文菜单已弹出时,例如当上下文菜单引发 WM_ENTERIDLE 消息时。

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);
}

上述处理程序还禁用了“插入 Unicode 控制字符”菜单(ID 为 0x8013),该菜单通常不禁用。

为了防止消息处理程序中的菜单操作一直重复执行,上下文菜单显示时,已在类中添加了一个控件标志 m_contextMenuShownFirstTime。此标志最初在 WM_CONTEXTMENU 处理程序中设置,并在首次调用 OnEnterIdle() 处理程序时重置。为了简洁起见,此处未提供相应代码,但读者可以查看附加的源代码。

禁用控件

如引言所述,目标之一是创建一个可以完全禁用的控件,使其无法获得焦点,但仍然以选定的颜色显示文本。为此,必须添加 WM_SETFOCUSWM_LBUTTONDOWNWM_LBUTTONDBLCLK 消息的处理程序。为了区分此状态与经典的禁用状态(显示灰色内容),我将其命名为“无法获得焦点”状态。使用一个不同的类成员标志 m_cannotReceiveFocus 来存储状态,并且可以通过公开的 SetCannotReceiveFocus() 方法进行更改。

当控件被禁用时,如果用户通过选项卡键在对话框中进行切换时控件获得焦点,WM_SETFOCUS 消息处理程序必须将焦点传递给下一个控件。用户可以通过选项卡键从前一个控件(按 TAB 键)或从后一个控件(按 SHIFT + TAB 键)到达该控件。在前一种情况下,控件必须将焦点传递给选项卡顺序中的下一个控件,在后一种情况下,它必须将焦点传递给前一个控件。

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);
}

WM_LBUTTONDOWNWM_LBUTTONDBLCLK 消息的处理程序必须简单地覆盖基类的实现,以分别防止设置焦点和选择整个文本。

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);
}

处理 ES_NOHIDESEL 样式

带有 ES_NOHIDESEL 样式的编辑控件即使在没有焦点时也会显示选择。虽然此样式使用得非常少见,但也应该处理:当控件被禁用时,它不应显示任何选择。因此,当控件切换到“无法获得焦点”状态时,当前选择会存储到类成员 m_selection 中,并移除选择。以后启用控件时,会恢复选择。

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;
}

此功能由演示应用程序中的较低编辑控件演示,该控件已设置 ES_NOHIDESEL 样式。

尽管如此,上述方法并不完整。禁用(即“无法获得焦点”)状态与 只读 状态密切相关:如果控件被禁用,它就处于 只读 状态。如果重置 只读 状态,控件也必须被启用。因此,两种状态的设置器方法都必须适当地处理另一种状态。

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

只需四个步骤即可使用和激活控件。

  • ColoredReadOnlyEdit.h 文件包含到您的项目中。
  • CColoredReadOnlyEdit 控件实例添加到对话框中,例如:
    CColoredReadOnlyEdit m_ctlEdit;
    CColoredReadOnlyEdit m_ctlEditWNoHideSel
  • 子类化您要替换为此控件的编辑控件。这可以通过 DDX_CONTROL 宏实现。
    BEGIN_DDX_MAP(CMainDlg)
        DDX_CONTROL(IDC_EDIT, m_ctlEdit)
        // ...
    END_DDX_MAP()

    或在父对话框的 WM_INITDIALOG 消息处理程序中手动子类化它。

    m_ctlEditWNoHideSel.SubclassWindow(GetDlgItem(IDC_EDIT_NOHIDESEL));
  • 不要忘记将 REFLECT_NOTIFICATIONS 宏追加到对话框消息映射中。
    BEGIN_MSG_MAP(CMainDlg)
        // ...
        REFLECT_NOTIFICATIONS()
    END_MSG_MAP()

除了基类 CEdit 类提供的标准接口外,还有两个额外的方法可用于自定义控件。

void SetTextColor(COLORREF newColor);
void SetCannotReceiveFocus(BOOL cannotReceiveFocus = TRUE);

历史

  • 2013 年 8 月 5 日 - 初始版本
© . All rights reserved.