富编辑控件在出现拼写检查下划线时发送 EN_CHANGE
Posted
技术标签:
【中文标题】富编辑控件在出现拼写检查下划线时发送 EN_CHANGE【英文标题】:Rich edit control sends EN_CHANGE when spellcheck underline appears 【发布时间】:2021-07-31 06:57:46 【问题描述】:假设您刚刚在启用拼写检查的富编辑控件中设置了一些文本,并且该文本有一些拼写错误。一瞬间,拼写检查将启动,然后拼写错误的文本将加下划线。但是你猜怎么着:富编辑控件实际上会为下划线事件发送一个EN_CHANGE
通知(这是假设你已经通过SendMessage(hwnd, EM_SETEVENTMASK, 0, (LPARAM)ENM_CHANGE)
注册了通知)。
是否有解决方法可以避免出现此类行为?我有一个带有一些启用拼写检查的富编辑控件的对话框。而且我还想知道编辑事件何时发生,所以我知道何时启用“保存”按钮。因此,仅针对拼写检查下划线事件获取EN_CHANGE
通知是一个问题。
我考虑过的一个选项是完全禁用EN_CHANGE
通知,然后在子类富编辑控件中自行触发它们。例如,当有WM_CHAR
时,它会显式发送EN_CHANGE
通知等。但这似乎是个问题,因为应该触发更改的事件类型很多,例如删除、复制/粘贴等。 ,而且我可能无法正确捕获所有这些。
我考虑过的另一个选项是动态启用和禁用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
,因此在处理程序中检查该标志是无用的。
【问题讨论】:
可以发布一个最小的工作示例吗? (***.com/help/minimal-reproducible-example)。因此,这对我来说非常不清楚。 不熟悉 Windows API 的人不必清楚特定于 Windows API 的问题。正如所写,很清楚发生了什么,以及需要解决哪个问题。 您能否在编辑控件中保留文本的“私有”副本,然后在收到EN_CHANGE
通知时检查文本是否实际上 在启用“保存”按钮之前更改了吗? GetWindowtext()
电话会让您知道当前的内容是什么。
@AdrianMole 你的意思是让父对话框保留一个私人副本吗?我想这是可能的,但当然它必须为每个富编辑控件保留一份副本并进行相应的比较。
@RbMm 与文档相反,富编辑实际上发送了WM_COMMAND
,而不是WM_NOTIFY
。所以CHANGENOTIFY
结构不可用。
【参考方案1】:
因为有关 CHANGENOTIFY
的文档(必须包含与 EN_CHANGE 通知代码相关的信息,但不是..)是错误的 - 仅存在研究。
在我的测试中,我认为与拼写检查相关的EN_CHANGE
仅在富编辑处理WM_TIMER
消息时收到。所以解决方案是下一个-子类richedit并记住(保存在类成员变量中)-当它在WM_TIMER
中时。比,当我们处理EN_CHANGE
- 检查是在WM_TIMER
里面的richedit。
部分 POC 代码。我特别展示了更复杂的情况 - 如果框架或对话框窗口中存在多个(多个)子 Richedit
#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);
;
【讨论】:
但似乎 WM_TIMER 被大量用于丰富的编辑控件,而不仅仅是拼写检查器。这不会是个问题吗? @user15025873 - 以及如何以及为什么会出现问题?你在测试想法吗? 如果生成WM_TIMER
消息的原因不止一个,则很难证明观察WM_TIMER
消息对应于一个原因。
@IInspectable - 你认为我说WM_TIMER
只是为了一个原因吗?我根本不认为这一点。但事实上拼写检查器仅从WM_TIMER
发送EN_CHANGE
。另一个EN_CHANGE
例如从WM_CHAR
发送。事实上,这是解决不同EN_CHANGE
之间视图差异的好方法
反之亦然。我不假设 WM_TIMER 仅用于检查拼写,但我假设检查拼写仅使用 WM_TIMER【参考方案2】:
使用EM_CANUNDO
(也可能是EM_CANREDO
)验证内容是否已更改。我希望拼写检查器不会添加任何撤消信息。
【讨论】:
这是有希望的,但经过试验,有时它仍然会失败。您可以在编辑控件中进行一些更改,这将填充撤消队列。然后,如果在某个点突然出现下划线(并且使用富编辑拼写检查器,它们会出现在意想不到的时间点),那么条件检查将注册为真并给您一个误报。 还有EM_GETMODIFY
。
拼写检查下划线实际上设置了修改标志,所以这也不起作用。【参考方案3】:
我最近尝试在不使用子类的情况下解决这个问题,但它只是有点成功。
我的替代解决方法是将整个文档标记为CFE_PROTECTED
。在EN_PROTECTED
处理程序中,ENPROTECTED::msg
是WM_NULL
,当来自拼写检查器时,您可以设置一个标志,告诉自己忽略下一个EN_CHANGE
。要允许从上下文菜单进行实际的拼写更正,您还需要跟踪菜单。
这感觉相当脆弱,但跟踪 WM_TIMER
也是如此。
【讨论】:
以上是关于富编辑控件在出现拼写检查下划线时发送 EN_CHANGE的主要内容,如果未能解决你的问题,请参考以下文章