Win32:如何自定义绘制编辑控件?

11

我需要实现EM_SETCUEBANNER的功能,使得在编辑控件内出现文本提示:

Example of cue banner in edit control

问题在于我不能使用通用控件的6版本,而这是获取微软提供的提示标语实现所必需的。

我已经研究过仅更改编辑控件的文本和字体格式。

Dark Gray Italic Text

但是它将抛出更高级组件库提供的组件包装器引发的“Change”事件,我找不到避免的方法。
所以我打算自定义绘制文本,在控件未聚焦且为空时绘制Cue Banner文本,并依靠默认绘制来实现其他操作。
编辑控件并没有像ListView、TreeView和其他控件一样漂亮地暴露出自定义绘制机制。 其他人也研究过这个问题,但似乎是一个几乎不可能的任务:
从目前的情况看,我需要处理以下消息:
  • WM_ERASEBKGND、WM_PAINT(出于明显的原因)
  • WM_SETFOCUS、WM_KILLFOCUS(防止白色条显示 - 如上所述)
  • WM_CHAR(以处理和更新控件中的文本)
另外,我还需要找到一种方法在控件中显示插入符号,因为我还没有找到一种方法让Windows为我进行此操作而不必绘制上述的白色条。
这将会很有趣。:roll_eyes:

考虑到Windows编辑控件从未被设计为自定义绘制:有没有人知道如何自定义绘制Windows编辑控件?


注意:我也会接受解决我的问题的答案,而不是回答我的问题。但是,任何想要自定义绘制编辑控件并遇到这个问题的人可能都希望得到答案。


你可以像我在Aero.Controls中的即时搜索框一样伪造它。http://bitbucket.org/factormystic/aero.controls - Factor Mystic
我之前尝试过你的方法。我很好奇你是如何解决“SearchBox”主题绘制问题的。但我看到你并没有解决这个问题。 - Ian Boyd
六年过去了,我只是用一些默认文本创建窗口。当用户单击控件时,您可以擦除文本。您还可以像示例中那样将字体设置为斜体,并在单击时更改它。 - kundrata
5个回答

11

自定义绘制编辑控件基本上是不可能的。有一些特殊情况下,你只需要做很少的事情就可以轻松实现,但你会冒着在下一个Windows版本(或者当有人在旧版本或通过终端服务运行您的应用程序时)出现问题的风险。

仅仅接管WM_PAINT和WM_ERASEBACKGROUND消息是不够的,因为控件有时还会在其他消息上进行绘制。

最好的方法是编写自己的编辑控件。我知道那是很大的工作量,但从长远来看,这将比试图入侵所有编辑控件的绘制代码更省力。

我记得在过去的好日子里,每个人都会子类化按钮控件以添加颜色和图形等功能。但是,有一天我坐下来编写了自己的按钮窗口类。结果,我的代码比我们源代码树中子类化并自定义绘制Windows按钮的代码还要短。


你似乎是对的。如果我想绘制提示文本,我会子类化Edit并处理WM_PAINT。有时(使用鼠标选择,在空编辑中按退格键),控件会在不调用WM_PAINT的情况下自行绘制为空。另一方面,Edit中存在大量功能,我无法为所有代码投资辩护。(剪切/复制/粘贴、撤消缓冲区、IME、RTL等) - Ian Boyd
1
无论哪种方式,都需要大量的代码。我只是想说不要自欺欺人地认为子类化会减少代码量。从长远来看,它可能并不会。 - John Knoeller
3
我最终通过子类化编辑器来实现,对其他消息做出响应并触发重绘,而不仅仅是WM_PAINT。还有一些其他的消息会导致内部重绘,而没有任何WM_PAINT发生,并且需要我在它们的重绘之上重新绘制:WM_SETFOCUSWM_KILLFOCUSWM_KEYUPWM_KEYDOWN,以及鼠标进入和离开时。 - Ian Boyd
1
@Ian:如果你最终选择了子类化控件,为什么会选择这个建议从头开始编写自己的控件的答案呢? - Adrian McCarthy
2
@Adrian McCarthy:因为它不能完全工作。你必须一层又一层地进行黑客攻击,但仍然无法使其正常工作。更糟糕的是,它非常脆弱;对编辑控件下面的任何更改都可能导致它崩溃。子类化编辑控件不能被视为解决问题的答案。最多只能称之为一种变通方法。这使得将其作为正确答案(基本上是不可能的)。 - Ian Boyd

6
创建一个自己的窗口类,看起来像一个空的编辑控件,它绘制提示文本并显示插入符号,并拥有焦点。同时创建编辑控件,但将其放在窗口后面(或隐藏)。
当您收到第一个WM_CHAR消息(或WM_KEYDOWN?)时,将您的窗口放在编辑控件后面,将焦点给予编辑控件,然后将WM_CHAR消息传递下去。从那时起,编辑控件将接管。
如果需要在编辑控件为空时重新显示提示文本,则可以监听来自编辑控件的EN_CHANGE通知。但我认为只有在编辑失去焦点且为空时才回到提示文本会更好。

有趣的方法,而不是仅仅子类化。它需要两个单独的窗口,并处理覆盖控件和父窗口上的代码,但它会起作用。我想知道您使用哪些函数来复制EDIT控件所需的框架和样式,包括任何活动主题(ThemeDrawText?)。也许更容易的方法是创建第二个编辑控件以匹配外观,然后再创建另一个实际用于保存内容的编辑控件,在父窗口中同时注意两个编辑控件的EN_CHANGE事件。 - Dwayne Robinson

6
继承EDIT控件对我很有帮助——需要在编辑对象属性时向用户显示一些格式信息(某些属性可能是多行)。像Adrian在他的答案中所说的那样,重要的是在自己的绘图之前调用EDIT控件的过程。在之后调用或发出自己的BeginPaint/EndPaint(返回0或DefWindowProc)会导致问题,从根本上不显示文本,到仅在调整大小后而非编辑后显示,再到留下剩余插入符的屏幕垃圾。因此,无论EDIT控件的其他重绘时间如何,我都没有遇到任何问题。
一些设置:
SetWindowSubclass(attributeValuesEdit, &AttributeValueEditProcedure, 0, reinterpret_cast<DWORD_PTR>(this));

// Not only do multiline edit controls fail to display the cue banner text,
// but they also ignore the Edit_SetCueBannerText call, meaning we can't
// just call GetCueBannerText in the subclassed function. So store it as
// a window property instead.
SetProp(attributeValuesEdit, L"CueBannerText", L"<attribute value>");

回调函数:
LRESULT CALLBACK AttributeValueEditProcedure(
    HWND hwnd,
    UINT message,
    WPARAM wParam,
    LPARAM lParam,
    UINT_PTR subclassId,
    DWORD_PTR data
    )
{

...

case WM_PRINTCLIENT:
case WM_PAINT:
    {
        auto textLength = GetWindowTextLength(hwnd);
        if (textLength == 0 && GetFocus() != hwnd)
        {
            // Get the needed DC with DCX_INTERSECTUPDATE before the EDIT
            // control's WM_PAINT handler calls BeginPaint/EndPaint, which
            // validates the update rect and would otherwise lead to drawing
            // nothing later because the region is empty. Also, grab it from
            // the cache so we don't mess with the EDIT's DC.
            HDC hdc = (message == WM_PRINTCLIENT)
                ? reinterpret_cast<HDC>(wParam)
                : GetDCEx(hwnd, nullptr, DCX_INTERSECTUPDATE|DCX_CACHE|DCX_CLIPCHILDREN | DCX_CLIPSIBLINGS);

            // Call the EDIT control so that the caret is properly handled,
            // no caret litter left on the screen after tabbing away.
            auto result = DefSubclassProc(hwnd, message, wParam, lParam);

            // Get the font and margin so the cue banner text has a
            // consistent appearance and placement with existing text.
            HFONT font = GetWindowFont(hwnd);
            RECT editRect;
            Edit_GetRect(hwnd, OUT &editRect);

            // Ideally we would call Edit_GetCueBannerText, but since that message
            // returns nothing when ES_MULTILINE, use a window property instead.
            auto* cueBannerText = reinterpret_cast<wchar_t*>(GetProp(hwnd, L"CueBannerText"));

            HFONT previousFont = SelectFont(hdc, font);
            SetTextColor(hdc, GetSysColor(COLOR_GRAYTEXT));
            SetBkMode(hdc, TRANSPARENT);
            DrawText(hdc, cueBannerText, int(wcslen(cueBannerText)), &editRect, DT_TOP|DT_LEFT|DT_NOPREFIX|DT_NOCLIP);
            SelectFont(hdc, previousFont);

            ReleaseDC(hwnd, hdc);

            // Return the EDIT's result (could probably safely just return zero here,
            // but seems safer to relay whatever value came from the edit).
            return result;
        }
    }
    break;

如果您想要实现复杂脚本的插入点导航、范围选择、IME 支持、带有复制和粘贴的上下文菜单、高对比度模式以及文本转语音等可访问性功能,那么自己编写 EDIT 控件并使其正确无误并不是一件轻松的事情。即使只使用最基本的光标支持(仅英语),编写自己的 EDIT 控件也不需要太多工作量。因此,与其他许多答案不同,我建议不要仅为了提示横幅文本而实现自己的 EDIT 控件。


4
子类化编辑控件。通过首先调用原始窗口过程,然后(如果它为空且不在焦点中),绘制提示文本来处理WM_PAINT。将其他所有消息传递给原始窗口过程。
我已经做到了 - 它有效。CodeGuru的人所遇到的问题似乎不适用于您的情况。我认为他试图做更多的外观调整。为了性能,看起来编辑控件正在WM_PAINT处理之外进行一些更新(可能是为了性能)。这将使完全控制外观几乎不可能。但是你可以绘制提示文本。

1
问题在于并不是所有的绘制都发生在/从/在WM_PAINT期间。有时,在WM_KILLFOCUS之后,EDIT控件会自行绘制 - 并且不会向控件发送WM_PAINT消息(即它不会使其无效)。这意味着您还必须处理WM_KILLFOCUS并在其后触发自己的绘制。以及WM_SETFOCUSWM_KEYUPWM_KEYDOWN和其他导致绘制而没有WM_PAINT的事件。 - Ian Boyd
1
@IanBoyd:我承认编辑控件有时会作弊并在其他时间绘制,但我已经使用这种技术实现了提示文本,并且它完全正常工作。 - Adrian McCarthy
@AdrianMcCarthy 在调用原始窗口过程后,BeginPaint 会有一个空的无效矩形区域(因为它已经被处理了)。在调用原始窗口过程之前调用 BeginPaint 是行不通的。创建自己的设备上下文会导致闪烁(特别是在右键单击事件中),而且似乎很难优化字符串绘制(因为需要将其限制在任意区域的更新上)。简而言之,我想不出一种简单的方法来绘制提示横幅而不会出现闪烁;虽然非常有趣 :) - Pooven
@Pooven:「创建自己的设备上下文」——我猜你是指 GetDC。很遗憾会出现闪烁问题。在我的实现中,我没有看到过这种情况。 - Adrian McCarthy
@AdrianMcCarthy 是的,我使用了 GetDC。在控件之间切换焦点时,闪烁并不那么严重。这主要是由于右键单击事件引起的。大多数实现在控件获得焦点时清除横幅;也许你也是这种情况?这可能解释了我们不同的经历。不过还是谢谢你的建议 :) - Pooven
@AdrianMcCarthy,是的,对我来说子类化也很好用,使用GetDCEx将重绘区域限定在必要的范围内。也许这导致了你的闪烁问题?我考虑编辑你的答案以包含代码,但由于我不确定stackoverflow所说的编辑坐在队列中的含义,我添加了自己的答案,并附上了实现的片段。 - Dwayne Robinson

0
我还需要找到一种方法来在控件中显示插入符号,因为我没有找到一种方法让Windows自动完成这个任务而不必绘制我提到的白色条。
如果你想要自己处理WM_PAINT消息而不将其转发给超类的原始窗口过程,那么你应该不要忘记调用DefWindowProc。这样插入符号就会被绘制出来。 为了避免白色条,你应该使用SetClassLongPtr函数移除类刷子。 并且保持DC的剪辑区域以裁剪Edit控件的ExtTextOut输出。 白色条可能是由于Edit控件传递给ExtTextOut的OPAQUE选项导致的。
结论:编写自己的控件。没有付出就没有收获。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接