出现拼写检查下划线时 Rich Edit 控件发送 EN_CHANGE
Rich edit control sends EN_CHANGE when spellcheck underline appears
假设您刚刚在启用了拼写检查的 Rich Edit 控件中设置了一些文本,并且该文本存在一些拼写错误。一秒钟过去了,拼写检查开始了,然后拼写错误的文本将被加下划线。但是你猜怎么着:rich edit 控件实际上只会为下划线事件发送一个 EN_CHANGE
通知(假设你已经通过 SendMessage(hwnd, EM_SETEVENTMASK, 0, (LPARAM)ENM_CHANGE)
注册了通知)。
是否有避免此类行为的解决方法?我有一个对话框,其中包含一些支持拼写检查的丰富编辑控件。而且我还想知道编辑事件何时发生,所以我知道何时启用“保存”按钮。因此,仅针对拼写检查下划线事件获得 EN_CHANGE
通知是一个问题。
我考虑过的一个选项是完全禁用 EN_CHANGE
通知,然后在子类丰富的编辑控件中自行触发它们。例如,当有 WM_CHAR
时,它会显式发送 EN_CHANGE
通知等。但这似乎是个问题,因为有许多类型的事件应该触发更改,例如删除,copy/pastes,等等,我可能无法正确捕捉所有这些。
我考虑过的另一个选项是动态启用和禁用 EN_CHANGE
通知。例如,仅在有焦点时启用它们,并在焦点消失时禁用它们。但这似乎也有问题,因为丰富的编辑可能在设置文本时已经获得焦点。然后会出现拼写检查下划线,并且会发送不需要的 EN_CHANGE
通知。
我想也可以使用计时器,但我认为这很容易出错。
还有人有其他想法吗?
这是一个可重现的例子。只需 运行 它,它就会说发生了一些变化:
#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;
}
此外,使用 EM_SETMODIFY
和 EM_GETMODIFY
似乎没有帮助。我猜拼写检查下划线会导致 EM_SETMODIFY
,因此在处理程序中检查该标志无济于事。
使用EM_CANUNDO
(也可能是EM_CANREDO
)来验证内容是否已更改。我希望拼写检查器不要添加任何撤消信息。
因为关于 CHANGENOTIFY
的文档( 必须包含与 EN_CHANGE 通知代码关联的信息,但不是..)是错误的 - 仅研究存在。
在我的测试中,我认为 EN_CHANGE
与拼写检查相关,仅当丰富的编辑处理 WM_TIMER
消息时才收到。所以解决方案是下一个 - subclass richedit 并记住(保存在 class 成员变量中) - 当它在 WM_TIMER
中时。比,当我们处理 EN_CHANGE
- 检查是 richedit 在 WM_TIMER
.
部分 POC 代码。我特别展示了更复杂的情况 - 如果框架或对话框 winndow
中存在多个(不止一个)child richedit`s
#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);
}
};
我最近尝试在没有子类化的情况下解决这个问题,但只取得了一定的成功。
我的替代解决方法是将整个文档标记为 CFE_PROTECTED
。在 EN_PROTECTED
处理程序中,来自拼写检查器的 ENPROTECTED::msg
是 WM_NULL
,您可以设置一个标志告诉自己忽略下一个 EN_CHANGE
。要允许从上下文菜单中进行实际的拼写更正,您还需要跟踪菜单。
这感觉相当脆弱,但跟踪也是如此 WM_TIMER
。
假设您刚刚在启用了拼写检查的 Rich Edit 控件中设置了一些文本,并且该文本存在一些拼写错误。一秒钟过去了,拼写检查开始了,然后拼写错误的文本将被加下划线。但是你猜怎么着:rich edit 控件实际上只会为下划线事件发送一个 EN_CHANGE
通知(假设你已经通过 SendMessage(hwnd, EM_SETEVENTMASK, 0, (LPARAM)ENM_CHANGE)
注册了通知)。
是否有避免此类行为的解决方法?我有一个对话框,其中包含一些支持拼写检查的丰富编辑控件。而且我还想知道编辑事件何时发生,所以我知道何时启用“保存”按钮。因此,仅针对拼写检查下划线事件获得 EN_CHANGE
通知是一个问题。
我考虑过的一个选项是完全禁用 EN_CHANGE
通知,然后在子类丰富的编辑控件中自行触发它们。例如,当有 WM_CHAR
时,它会显式发送 EN_CHANGE
通知等。但这似乎是个问题,因为有许多类型的事件应该触发更改,例如删除,copy/pastes,等等,我可能无法正确捕捉所有这些。
我考虑过的另一个选项是动态启用和禁用 EN_CHANGE
通知。例如,仅在有焦点时启用它们,并在焦点消失时禁用它们。但这似乎也有问题,因为丰富的编辑可能在设置文本时已经获得焦点。然后会出现拼写检查下划线,并且会发送不需要的 EN_CHANGE
通知。
我想也可以使用计时器,但我认为这很容易出错。
还有人有其他想法吗?
这是一个可重现的例子。只需 运行 它,它就会说发生了一些变化:
#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;
}
此外,使用 EM_SETMODIFY
和 EM_GETMODIFY
似乎没有帮助。我猜拼写检查下划线会导致 EM_SETMODIFY
,因此在处理程序中检查该标志无济于事。
使用EM_CANUNDO
(也可能是EM_CANREDO
)来验证内容是否已更改。我希望拼写检查器不要添加任何撤消信息。
因为关于 CHANGENOTIFY
的文档( 必须包含与 EN_CHANGE 通知代码关联的信息,但不是..)是错误的 - 仅研究存在。
在我的测试中,我认为 EN_CHANGE
与拼写检查相关,仅当丰富的编辑处理 WM_TIMER
消息时才收到。所以解决方案是下一个 - subclass richedit 并记住(保存在 class 成员变量中) - 当它在 WM_TIMER
中时。比,当我们处理 EN_CHANGE
- 检查是 richedit 在 WM_TIMER
.
部分 POC 代码。我特别展示了更复杂的情况 - 如果框架或对话框 winndow
中存在多个(不止一个)child richedit`s#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);
}
};
我最近尝试在没有子类化的情况下解决这个问题,但只取得了一定的成功。
我的替代解决方法是将整个文档标记为 CFE_PROTECTED
。在 EN_PROTECTED
处理程序中,来自拼写检查器的 ENPROTECTED::msg
是 WM_NULL
,您可以设置一个标志告诉自己忽略下一个 EN_CHANGE
。要允许从上下文菜单中进行实际的拼写更正,您还需要跟踪菜单。
这感觉相当脆弱,但跟踪也是如此 WM_TIMER
。