user15025873
user15025873

Reputation:

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.

Upvotes: 5

Views: 759

Answers (3)

Anders
Anders

Reputation: 101616

I recently tried to work around this without subclassing but it was only somewhat successful.

My alternative workaround consists of marking the entire document as CFE_PROTECTED. In the EN_PROTECTED handler ENPROTECTED::msg is WM_NULL when coming from the spell checker and you can set a flag telling yourself to ignore the next EN_CHANGE. To allow actual spelling corrections from the context menu you also need to keep track of menus.

This feels rather fragile but so does tracking WM_TIMER.

Upvotes: 0

Daniel Sęk
Daniel Sęk

Reputation: 2769

Use EM_CANUNDO (maybe also EM_CANREDO) to verify that contents has changed. I hope that spellchecker does't add any undo information.

Upvotes: 0

RbMm
RbMm

Reputation: 33706

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

Upvotes: 1

Related Questions