Introduction
One of the problems that is intrinsic to Windows is the fact that an ordinary Edit control, MFC class CEdit
, will generate EN_CHANGE
notifications any time the text changes, even if the text is changed programmatically by the application.
This can lead to awkward situations; for example, if two edit controls have some invariant which must be maintained, responding to an EN_CHANGE
event in one may necessitate changing the text of the other; but, since a change in the other requires a change in the first, setting that text triggers another EN_CHANGE
, during which the handler for the second control changes the contents of the first control, which triggers an EN_CHANGE
which causes the application to then respond by changing the second, and so on, until there is a stack overflow due to the infinite recursion that goes on.
In this article, I will show how to avoid EN_CHANGE
notifications in ordinary CEdit
controls, and also how much simpler it is to implement the same functionality in a Rich Edit Control.
Avoiding it in CEdit
To avoid EN_CHANGE
notifications in a CEdit
control, you must create a subclass of the control. In my case, it was called CNoNotifyEdit
.
To this subclass, and a "Reflected event handler" for EN_CHANGE
, which is indicated by the =EN_CHANGE
notification:
Next, hand-edit the resulting header file and implementation file.
In the Message Map, change the line that says:
ON_CONTROL_REFLECT(EN_CHANGE, OnEnChange)
to read:
ON_CONTROL_REFLECT_EX(EN_CHANGE, OnEnChange)
This requires a change in the prototype for the method, so change:
public:
afx_msg void OnEnChange();
to:
protected:
afx_msg BOOL OnEnChange();
Note that I not only change the result type from void
to BOOL
, but I also change the scope to be protected
. There is not now, and never has been in the entire history of MFC, a sensible reason to make these methods public
, since any program that ever attempted to call them from outside the class would represent such an egregious breach of sensible OO methodology that no one would ever do it. I always change my handlers to be protected
. (This represents one of the numerous blunders of Visual Studio .NET, which has been more of an exercise in destroying the usability of the IDE than of fixing ridiculous bugs and design flaws.)
Then, I add a new method:
public:
void SetWindowTextNoNotify(LPCTSTR s);
and add a variable:
protected:
BOOL notify;
Initialize the notify
variable in the constructor:
CNoNotifyEdit::CNoNotifyEdit()
{
notify = TRUE;
}
The implementation of SetWindowTextNoNotify
is:
void CNoNotifyEdit::SetWindowTextNoNotify(LPCTSTR s)
{
CString old;
CEdit::GetWindowText(old);
if(old == s)
return;
BOOL previous = notify;
notify = FALSE;
CEdit::SetWindowText(s);
notify = previous;
}
As an optimization, I simply check to see if the existing string is the same as the string I'm about to set, and if they are, I do nothing. If you had edit controls with massive amounts of text, you might choose to eliminate this step because of its impact on memory fragmentation.
Finally, the implementation of the reflected EN_CHANGE
handler is quite simple:
BOOL CNoNotifyEdit::OnChange()
{
return !notify;
}
The way a reflected handler works is that if the handler returns TRUE
, it means the handler has done everything necessary to handle the event and it will not be sent to the parent window. So, in this case, if notify
is FALSE
(no notification desired), the handler returns TRUE
. But, if notify
is TRUE
, then the handler returns FALSE
, which MFC interprets as "Please pass this event to the parent window as usual".
Avoiding it in CRichEditCtrl
The problem is a little different in a Rich Edit control. First, Rich Edit controls do not normally generate EN_CHANGE
notifications. Whenever you subclass an EN_CHANGE
handler in a rich edit control, you get comments that say:
Now, this message is more than a little strange. For example, the CEdit
class would not be involved at all. It would be the CRichEditCtrl
class. And, neither of these have an OnInitDialog
handler, because that is only a member of the CDialog
class. You would not call CRichEditCtrl().SetEventMask()
because it would make no sense to apply the SetEventMask
call to a constructor! It is not clear why you would get this for a CEdit
control at all, since the ClassWizard knows the class of the control, and therefore would not need to put these comments in except for classes derived from CRichEdit
. It also does not explain what is meant by "ORed into the mask", since what is required to OR something in is to have a value in the first place into which to OR it. So, other than the fact that the comment has approximately one deep and fundamental bug per line, it makes perfect sense. Not.
What does this comment really mean?
Well, if the control is in a CDialog
-derived class, including dialog bars and property pages, you would typically enable the events as part of the OnInitDialog
handler. If it is part of a CFormView
-derived class, you would typically enable the events as part of the OnInitialUpdate
handler.
But, what if you always want to get these events for a rich edit control, and don't want to be bothered doing this for every rich edit control you add?
In that case, what you do is handle it in the PreSubclassWindow
handler of your CRichEditCtrl
-derived subclass. PreSubclassWindow
is a good place to do lots of useful and interesting "default initializations" that require an actual HWND
object exist (as opposed to a constructor, which can be executed far, far before there is an HWND
associated with the control). I discuss this in a separate article.
For a CDialog
, CPropertyPage
, CFormView
, etc., your enabling of EN_CHANGE
events would be done by the following sequence of operations, assuming that c_MyEdit
is a control variable that is bound to the HWND
:
c_MyEdit.SetEventMask(ENM_CHANGE | c_MyEdit.GetEventMask());
By the way, has it struck you as a little odd that CRichEditCtrl::GetEventMask
returns a long
while CRichEditCtrl::SetEventMask
requires a DWORD
? Do you wonder if anyone ever actually looked at these specifications before publishing them?
Now, we can add a SetWindowTextNoNotify
method to our CRichEditCtrl
-derived class:
void CMyRichEditCtrl::SetWindowTextNoNotify(LPCTSTR s)
{
DWORD oldmask = CRichEditCtrl::GetEventMask();
DWORD newmask = oldmask & ~ENM_CHANGE;
CRichEditCtrl::SetEventMask(newmask);
CRichEditCtrl::SetWindowText(s);
CRichEditCtrl::SetEventMask(oldmask);
}
Because we are able to avoid even generating any EN_CHANGE
notification, there is no need to have a reflected handler that will cause them to be ignored. They simply won't happen at all.
In one case, I overrode the SetWindowText
method by adding a new method:
void SetWindowText(LPCTSTR s, BOOL notify = TRUE);
and implemented it as:
void CMyRichEditCtrl::SetWindowTAext(LPCTSTR s, BOOL notify )
{
DWORD oldmask;
DWORD newmask;
if(!notify)
{
oldmask = CRichEditCtrl::GetEventMask();
newmask = oldmask &~ENM_CHANGE;
CRichEditCtrl::SetEventMask(newmask);
}
CRichEditCtrl::SetWindowText(s);
if(!notify)
CRichEditCtrl::SetEventMask(oldmask);
}
I have a slight preference for this latter approach, and it is left as an Exercise For The Reader to implement the corresponding function in the ordinary CEdit
control.