5

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.

  • 1
    Can you just keep a 'private' copy of the text in the edit control and then, when you get the `EN_CHANGE` notification, check to see if the text has *actually* changed before you enable your "Save" button? A `GetWindowtext()` call would let you know what the current content is. – Adrian Mole May 09 '21 at 18:55
  • @AdrianMole Do you mean for the parent dialog to keep a private copy? I suppose that's possible, but of course then it has to keep a copy for every rich edit control and compare accordingly. –  May 09 '21 at 20:27
  • @AdrianMole That's actually harder than you might think. Some or all of the text on a rich edit control can be emboldened, for example and that 'boldness' forms part of the control's state. Maybe that can be resolved by streaming the contents of the rich edit control to a memory buffer as RTF though, see: https://learn.microsoft.com/en-us/windows/win32/controls/em-streamout. – Paul Sanders May 09 '21 at 23:58
  • you got pointer to [`CHANGENOTIFY`](https://learn.microsoft.com/en-us/windows/win32/api/textserv/ns-textserv-changenotify) structure in *lParam*. so you simply need check *dwChangeType* member and do action based on it. all is very simply – RbMm May 10 '21 at 00:33
  • 2
    @RbMm Contrary to the documentation, the rich edit actually sends a `WM_COMMAND`, not `WM_NOTIFY`. So that `CHANGENOTIFY` structure is not available. –  May 10 '21 at 01:54
  • Can [EN_STARTCOMPOSITION](https://learn.microsoft.com/en-us/windows/win32/controls/en-startcomposition) help you? Such a save button don't need to be too precise. or as @Adrian Mole said, you can keep a 'private' copy and then compare by yourself. – YangXiaoPo-MSFT May 10 '21 at 09:59
  • @RemyLebeau Well it actually sends a `WM_COMMMAND` notification, not `WM_NOTIFY`. I think `WM_NOTIFY` is sent by "windowless" rich edit controls. https://learn.microsoft.com/en-us/windows/win32/controls/en-change--rich-edit-control- –  May 10 '21 at 18:19
  • and what is bad for you in solution with tracking `WM_TIMER` in richedit ? – RbMm May 10 '21 at 18:28
  • Well according to these docs, both the plain edit and the rich edit send the `EN_CHANGE` as part of `WM_COMMAND`: https://learn.microsoft.com/en-us/windows/win32/controls/en-change –  May 10 '21 at 18:51

3 Answers3

1

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);
    }
};
RbMm
  • 31,280
  • 3
  • 35
  • 56
  • But it seems that WM_TIMER is used a lot for a rich edit control, not just the spellchecker. Isn't that going to be a problem? –  May 10 '21 at 14:17
  • @user15025873 - and how and why this can be problem ? are you test idea ? – RbMm May 10 '21 at 15:14
  • If a `WM_TIMER` message is generated for more than one reason, it's hard to justify that observing a `WM_TIMER` message corresponds to a single reason. – IInspectable May 10 '21 at 16:25
  • @IInspectable - were you view that i say that `WM_TIMER` only for single reason ? i at all not assume this. but by fact spellchecker sent `EN_CHANGE` only from `WM_TIMER`. another `EN_CHANGE` send for instance from `WM_CHAR`. by fact this is good solution for view difference between different `EN_CHANGE` – RbMm May 10 '21 at 16:29
  • visa versa. i not assume that WM_TIMER used only for check spelling, but i assume that check spelling use only WM_TIMER – RbMm May 10 '21 at 16:39
  • @user15025873 - *drawback is that it requires the parent and child to have access to the same flag.* ?! what you mean under this ? no any drawback here at all. think my solution the best from possible – RbMm May 10 '21 at 19:04
  • @user15025873 *I mean it seems to require the child subclass procedure to know information about the parent data/procedure.* - and ? so what ? we have this information as you can see. your alternative how minimum much more bad if working at all – RbMm May 10 '21 at 19:08
  • @user15025873 - unclear what sense in subclass proc, send some messages, change properties, revert it back, when enough simply remember that now `WM_TIMER` processed – RbMm May 10 '21 at 19:14
  • @user15025873 - but i special write code for this case ! look better/ in what problem ?! simply change `bool _bInTimer[10] = {};` - 2 to 10. – RbMm May 10 '21 at 19:18
  • @user15025873 - not difficult easy - my code **already** do this. - every rich edit have own separate flag. even better use 1 bit instead bool (1 byte). i and _bittestandset, _bittestandreset, _bittest. i can change for this, simply not sure are exist sense – RbMm May 10 '21 at 19:20
  • @user15025873 - in what problem ?! what you at all mean under *translate* – RbMm May 10 '21 at 19:23
  • @user15025873 - https://pastebin.com/zRS1zUdJ if use 1 bit instead 1 byte for flag. but nothing basically not changed – RbMm May 10 '21 at 19:33
  • Yes, this seems to work OK. WM_TIMER is also used for scrolling etc. but so far I have not found any other EN_CHANGE inside a WM_TIMER handler. Nice solution. – Gerrit Beuze Aug 16 '23 at 12:59
0

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

Daniel Sęk
  • 2,504
  • 1
  • 8
  • 17
  • This was promising, but after experimenting, there are times when it'll still fail. You can make some changes in an edit control, which will populate the undo queue. Then if an underline suddenly appears at some point (and with the rich edit spellchecker, they appear at unexpected points in time), then the condition check will register true and give you a false positive. –  May 10 '21 at 14:03
  • There is also `EM_GETMODIFY`. – Daniel Sęk May 10 '21 at 14:53
  • The spellchecker underline actually sets the modify flag, so that doesn't work either. –  May 10 '21 at 14:55
0

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.

Anders
  • 97,548
  • 12
  • 110
  • 164