避免 EN_CHANGE 通知






3.89/5 (12投票s)
当编辑控件涉及复杂的控件交互时,如果应用程序实际更改控件值而生成EN_CHANGE通知,则会导致问题。为了避免不得不处理来自CEdit和CRichEdit的EN_CHANGE通知,本文将展示如何派生
引言
Windows固有的一个问题是,普通的编辑控件,MFC类CEdit
,在文本发生变化时会生成EN_CHANGE
通知,即使文本是由应用程序以编程方式更改的。
这可能导致尴尬的局面;例如,如果两个编辑控件之间存在某个必须维持的不变量,响应其中一个的EN_CHANGE
事件可能需要更改另一个的文本;但是,由于另一个的更改需要第一个的更改,设置该文本会触发另一个EN_CHANGE
,在此期间,第二个控件的句柄更改了第一个控件的内容,这会触发一个EN_CHANGE
,导致应用程序通过更改第二个控件来响应,依此类推,直到由于持续的无限递归导致堆栈溢出。
在本文中,我将展示如何在普通的CEdit
控件中避免EN_CHANGE
通知,以及在Rich Edit控件中实现相同功能要简单得多。
在 CEdit 中避免它
要避免CEdit
控件中的EN_CHANGE
通知,您必须创建控件的子类。在我的例子中,它被命名为CNoNotifyEdit
。
为此子类,以及EN_CHANGE
的“反射事件处理程序”,由=EN_CHANGE
通知指示
接下来,手动编辑生成的头文件和实现文件。
在消息映射中,更改显示以下内容的行
ON_CONTROL_REFLECT(EN_CHANGE, OnEnChange)
修改为:
ON_CONTROL_REFLECT_EX(EN_CHANGE, OnEnChange)
这需要更改方法的原型,所以请更改
public: afx_msg void OnEnChange();
to
protected:
afx_msg BOOL OnEnChange();
请注意,我不仅将返回类型从void
更改为BOOL
,还将作用域更改为protected
。在MFC的整个历史中,从来没有一个合理的理由使这些方法成为public
,因为任何试图从类外部调用它们的方法都将代表对合理面向对象方法论的严重侵犯,没有人会这样做。我总是将我的处理程序更改为protected
。(这代表了Visual Studio .NET众多失误之一,它更多的是在破坏IDE的可用性,而不是修复可笑的bug和设计缺陷。)
然后,我添加了一个新方法
public: void SetWindowTextNoNotify(LPCTSTR s);
并添加了一个变量
protected:
BOOL notify;
在构造函数中初始化notify
变量
CNoNotifyEdit::CNoNotifyEdit() { notify = TRUE; }
SetWindowTextNoNotify
的实现如下:
void CNoNotifyEdit::SetWindowTextNoNotify(LPCTSTR s) { CString old; CEdit::GetWindowText(old); if(old == s) return; // do nothing, already set BOOL previous = notify; notify = FALSE; CEdit::SetWindowText(s); notify = previous; }
作为一种优化,我简单地检查现有字符串是否与我要设置的字符串相同,如果相同,则不做任何操作。如果您有包含大量文本的编辑控件,您可能会选择消除此步骤,因为它会影响内存碎片。
最后,反射的EN_CHANGE
处理程序的实现非常简单
BOOL CNoNotifyEdit::OnChange()
{
return !notify;
}
反射处理程序的工作方式是,如果处理程序返回TRUE
,则表示处理程序已完成处理事件所需的一切,并且该事件不会被发送到父窗口。因此,在这种情况下,如果notify
为FALSE
(不需要通知),则处理程序返回TRUE
。但是,如果notify
为TRUE
,则处理程序返回FALSE
,MFC将其解释为“请像平常一样将此事件传递给父窗口”。
在 CRichEditCtrl 中避免它
Rich Edit控件的问题略有不同。首先,Rich Edit控件通常不生成EN_CHANGE
通知。每当您在Rich Edit控件中子类化EN_CHANGE
处理程序时,您都会看到这样的注释:
// TODO: If this is a RICHEDIT control, the control will not // send this notification unless you override the CEdit::OnInitDialog() // function and call CRichEditCtrl().SetEventMask() // with the ENM_CHANGE flag ORed into the mask.
现在,这条消息有点奇怪。例如,CEdit
类根本不会参与其中。它将是CRichEditCtrl
类。而且,这两个类都没有OnInitDialog
处理程序,因为这只是CDialog
类的一个成员。您不会调用CRichEditCtrl().SetEventMask()
,因为将SetEventMask
调用应用于构造函数没有意义!对于CEdit
控件,完全不清楚为什么会收到这个信息,因为ClassWizard知道控件的类,因此不需要在除了派生自CRichEdit
的类之外添加这些注释。它也没有解释“ORed into the mask”是什么意思,因为要将某物OR进去,需要有一个初始值来OR进去。所以,除了注释每行大约有一个深度和根本性的bug之外,它完全有意义。不。
这条注释实际上是什么意思?
好吧,如果控件位于CDialog
派生的类中,包括对话框栏和属性页,您通常会在OnInitDialog
处理程序中启用这些事件。如果它是CFormView
派生的类的一部分,您通常会在OnInitialUpdate
处理程序中启用这些事件。
但是,如果您始终想要获取Rich Edit控件的这些事件,并且不想为添加的每个Rich Edit控件都这样做,该怎么办?
在这种情况下,您可以在CRichEditCtrl
-派生子类的PreSubclassWindow
处理程序中处理它。PreSubclassWindow
是执行许多有用且有趣的“默认初始化”的好地方,这些初始化需要一个实际存在的HWND
对象(与构造函数相对,构造函数可能在与控件关联的HWND
存在之前很久就执行)。我在一篇单独的文章中对此进行了讨论。
对于CDialog
、CPropertyPage
、CFormView
等,您启用EN_CHANGE
事件的操作顺序如下,假设c_MyEdit
是一个绑定到HWND
的控件变量。
c_MyEdit.SetEventMask(ENM_CHANGE | c_MyEdit.GetEventMask());
顺便说一句,您是否觉得CRichEditCtrl::GetEventMask
返回long
而CRichEditCtrl::SetEventMask
需要DWORD
有点奇怪?您是否怀疑过有人在发布这些规范之前真正看过它们?
现在,我们可以向我们的CRichEditCtrl
派生类添加一个SetWindowTextNoNotify
方法。
void CMyRichEditCtrl::SetWindowTextNoNotify(LPCTSTR s)
{
DWORD oldmask = CRichEditCtrl::GetEventMask();
DWORD newmask = oldmask & ~ENM_CHANGE;
CRichEditCtrl::SetEventMask(newmask);
CRichEditCtrl::SetWindowText(s);
CRichEditCtrl::SetEventMask(oldmask);
}
因为我们能够避免生成任何EN_CHANGE
通知,所以不需要一个导致它们被忽略的反射处理程序。它们根本不会发生。
在一个案例中,我通过添加一个新方法来重写了SetWindowText
方法。
void SetWindowText(LPCTSTR s, BOOL notify = TRUE);
并将其实现为
void CMyRichEditCtrl::SetWindowTAext(LPCTSTR s, BOOL notify /* = TRUE */) { DWORD oldmask; DWORD newmask; if(!notify) { oldmask = CRichEditCtrl::GetEventMask(); newmask = oldmask &~ENM_CHANGE; CRichEditCtrl::SetEventMask(newmask); } CRichEditCtrl::SetWindowText(s); if(!notify) CRichEditCtrl::SetEventMask(oldmask); }
我稍微倾向于后一种方法,留给读者作为练习,在普通的CEdit
控件中实现相应的函数。