c++winapiatlricheditrichedit-control

Rich edit control sends EN_CHANGE when spellcheck underline appears


Let's say you've just set some text in a spellcheck-enabled rich edit control, and the text has some spelling errors. A split second will go by, spellcheck will kick in, and then the misspelled text will get underlined. But guess what: the rich edit control will actually send an EN_CHANGE notification just for the underlining event (this is assuming you've registered for notifications by doing SendMessage(hwnd, EM_SETEVENTMASK, 0, (LPARAM)ENM_CHANGE)).

Is there a workaround to not get this type of behavior? I've got a dialog with some spellcheck-enabled rich edit controls. And I also want to know when an edit event has taken place, so I know when to enable the "Save" button. Getting an EN_CHANGE notification merely for the spellcheck underlining event is thus a problem.

One option I've considered is disabling EN_CHANGE notifications entirely, and then triggering them on my own in a subclassed rich edit control. For example, when there's a WM_CHAR, it would send the EN_CHANGE notification explicitly, etc. But that seems like a problem, because there are many types of events that should trigger changes, like deletes, copy/pastes, etc., and I'd probably not capture all of them correctly.

Another option I've considered is enabling and disabling EN_CHANGE notifications dynamically. For example, enabling them only when there's focus, and disabling when focus is killed. But that also seems problematic, because a rich edit might already have focus when its text is set. Then the spellcheck underline would occur, and the undesirable EN_CHANGE notification would be sent.

I suppose a timer could be used, too, but I think that would be highly error-prone.

Does anybody have any other ideas?

Here's a reproducible example. Simply run it, and it'll say something changed:

#include <Windows.h>
#include <atlbase.h>
#include <atlwin.h>
#include <atltypes.h>
#include <Richedit.h>

class CMyWindow :
    public CWindowImpl<CMyWindow, CWindow, CWinTraits<WS_VISIBLE>>
{
public:
    CMyWindow()
    {
    }

BEGIN_MSG_MAP(CMyWindow)
    MESSAGE_HANDLER(WM_CREATE, OnCreate)
    COMMAND_CODE_HANDLER(EN_CHANGE, OnChange)
END_MSG_MAP()

private:
    LRESULT OnCreate(UINT, WPARAM, LPARAM, BOOL& bHandled)
    {
        bHandled = FALSE;

        LoadLibrary(L"Msftedit.dll");

        CRect rc;
        GetClientRect(&rc);
        m_wndRichEdit.Create(MSFTEDIT_CLASS, m_hWnd, &rc,
            NULL, WS_VISIBLE | WS_CHILD | WS_BORDER);

        INT iLangOpts = m_wndRichEdit.SendMessage(EM_GETLANGOPTIONS, NULL, NULL);
        iLangOpts |= IMF_SPELLCHECKING;
        m_wndRichEdit.SendMessage(EM_SETLANGOPTIONS, NULL, (LPARAM)iLangOpts);

        m_wndRichEdit.SetWindowText(L"sdflajlf adlfjldsfklj dfsl");
       
        m_wndRichEdit.SendMessage(EM_SETEVENTMASK, 0, (LPARAM)ENM_CHANGE);
      
        return 0;
    }

    LRESULT OnChange(WORD, WORD, HWND, BOOL&)
    {
        MessageBox(L"changed", NULL, NULL);
        return 0;
    }

private:
    CWindow m_wndRichEdit;
};


int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    CMyWindow wnd;
    CRect rc(0, 0, 200, 200);
    wnd.Create(NULL, &rc);

    MSG msg;
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

Also, it appears that using EM_SETMODIFY and EM_GETMODIFY don't help. I guess the spellcheck underlining results in a EM_SETMODIFY, so checking that flag in the handler is of no avail.


Solution

  • because documentation about CHANGENOTIFY ( must contains information that is associated with an EN_CHANGE notification code, but not..) is wrong - only research exist.

    in my test i view that EN_CHANGE related to Spellcheck received only when rich edit handle WM_TIMER message. so solution is next - subclass richedit and remember (save in class member variable) - when it inside WM_TIMER. than, when we handle EN_CHANGE - check are richedit inside WM_TIMER.

    partial POC code. i special show more complex case - if several (more than one) child richedit`s exist in frame or dialog winndow

    #include <richedit.h>
    
    class RichFrame : public ZFrameMultiWnd
    {
        enum { richIdBase = 0x1234 };
        bool _bInTimer[2] = {};
    
    public:
    protected:
    private:
        static LRESULT WINAPI SubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
        {
            if ((uIdSubclass -= richIdBase) >= _countof(_bInTimer))
            {
                __debugbreak();
            }
    
            bool bTimerMessage = uMsg == WM_TIMER;
            
            if (bTimerMessage)
            {
                reinterpret_cast<RichFrame*>(dwRefData)->_bInTimer[uIdSubclass] = TRUE;
            }
            
            lParam = DefSubclassProc(hWnd, uMsg, wParam, lParam);
    
            if (bTimerMessage)
            {
                reinterpret_cast<RichFrame*>(dwRefData)->_bInTimer[uIdSubclass] = false;
            }
    
            return lParam;
        }
    
        virtual BOOL CreateClient(HWND hWndParent, int nWidth, int nHeight, PVOID /*lpCreateParams*/)
        {
            UINT cy = nHeight / _countof(_bInTimer), y = 0;
    
            UINT id = richIdBase;
            ULONG n = _countof(_bInTimer);
    
            do 
            {
                if (HWND hwnd = CreateWindowExW(0, MSFTEDIT_CLASS, 0, WS_CHILD|ES_MULTILINE|WS_VISIBLE|WS_BORDER, 
                    0, y, nWidth, cy, hWndParent, (HMENU)id, 0, 0))
                {
                    SendMessage(hwnd, EM_SETLANGOPTIONS, 0, 
                        SendMessage(hwnd, EM_GETLANGOPTIONS, 0, 0) | IMF_SPELLCHECKING);
    
                    SetWindowText(hwnd, L"sdflajlf adlfjldsfklj d");
                    SendMessage(hwnd, EM_SETEVENTMASK, 0, ENM_CHANGE);
    
                    if (SetWindowSubclass(hwnd, SubclassProc, id, reinterpret_cast<ULONG_PTR>(this)))
                    {
                        continue;
                    }
                }
    
                return FALSE;
    
            } while (y += cy, id++, --n);
            
            return TRUE;
        }
    
        virtual LRESULT WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
        {
            switch (uMsg)
            {
            case WM_COMMAND:
                if (EN_CHANGE == HIWORD(wParam))
                {
                    if ((wParam = LOWORD(wParam) - richIdBase) >= _countof(_bInTimer))
                    {
                        __debugbreak();
                    }
                    
                    DbgPrint("EN_CHANGE<%x> = %x\n", wParam, _bInTimer[wParam]);
                }
                break;
    
            case WM_DESTROY:
                {
                    UINT id = richIdBase;
                    ULONG n = _countof(_bInTimer);
                    do 
                    {
                        RemoveWindowSubclass(GetDlgItem(hwnd, id), SubclassProc, id);
                    } while (id++, --n);
                }
                break;
    
            case WM_NCDESTROY:
                PostQuitMessage(0);
                break;
            }
            return __super::WindowProc(hwnd, uMsg, wParam, lParam);
        }
    };