Jos*_*ers 14 c++ windows user-interface winapi qt
所以,我偶然发现了Windows API的一个有趣的错误,我想知道是否有人对如何解决它有一些了解.似乎谷歌也在努力解决这个问题.应该注意的是,虽然我将在Qt源本身修复此问题,但问题在于Windows默认消息处理,而不是Qt.我将提到的所有文件都可以在线找到,因为它们都是开源库.下面是一个复杂的问题,我会尽量给出尽可能多的上下文.我花了很多时间和精力来解决这个问题,但由于我只是一名工程师大约8个月,我仍然缺乏经验,很可能错过了一些明显的东西.
上下文:
我编写了一个程序,使用Qt来自定义皮肤上的窗口.这些外观通过系统默认的非客户端UI外观.换句话说,我使用自定义绘制的框架(由Qt支持).从Qt5开始,当我的程序在任何Windows Aero之前的操作系统上运行时,我的程序出现了问题(低于XP且超过禁用Windows Aero的Vista).不幸的是,Qt开发人员已经确认他们不再真正支持XP,所以我不会依赖它们来修复bug.
错误:
在运行禁用了合成的计算机(禁用 Windows Aero或不存在)时,单击非客户区域中的任意位置将导致Windows在我的自定义外观上重新绘制其系统默认的非客户端UI.
我的研究
一些调试和调查让我进入了qwindowscontext.cpp中的qWindowsProc.我能够确定在窗口的皮肤被涂过之前要处理的最后一条窗口消息是WM_NCLBUTTONDOWN.这看起来很奇怪,所以我接受了互联网.
果然,我发现了一个名为hwnd_message_Handler.cc的文件来自Google的Chromium Embedded Framework(CEF).在该文件中有很多关于各种Windows消息的评论,由于一些疯狂的原因,导致重新定制系统默认的非客户端帧而不是自定义帧.以下是一个这样的评论.
// A scoping class that prevents a window from being able to redraw in response
// to invalidations that may occur within it for the lifetime of the object.
//
// Why would we want such a thing? Well, it turns out Windows has some
// "unorthodox" behavior when it comes to painting its non-client areas.
// Occasionally, Windows will paint portions of the default non-client area
// right over the top of the custom frame. This is not simply fixed by handling
// WM_NCPAINT/WM_PAINT, with some investigation it turns out that this
// rendering is being done *inside* the default implementation of some message
// handlers and functions:
// . **WM_SETTEXT**
// . **WM_SETICON**
// . **WM_NCLBUTTONDOWN**
// . EnableMenuItem, called from our WM_INITMENU handler
// The solution is to handle these messages and **call DefWindowProc ourselves**,
// but prevent the window from being able to update itself for the duration of
// the call. We do this with this class, which automatically calls its
// associated Window's lock and unlock functions as it is created and destroyed.
// See documentation in those methods for the technique used.
//
// The lock only has an effect if the window was visible upon lock creation, as
// it doesn't guard against direct visiblility changes, and multiple locks may
// exist simultaneously to handle certain nested Windows messages.
//
// IMPORTANT: Do not use this scoping object for large scopes or periods of
// time! IT WILL PREVENT THE WINDOW FROM BEING REDRAWN! (duh).
//
// I would love to hear Raymond Chen's explanation for all this. And maybe a
// list of other messages that this applies to ;-)
Run Code Online (Sandbox Code Playgroud)
此文件中还存在多个自定义消息处理程序,以防止发生此错误.例如,我发现导致此错误的另一条消息是WM_SETCURSOR.果然,他们有一个处理程序,当移植到我的程序时,工作得非常好.
他们处理这些消息的常见方法之一是使用ScopedRedrawLock.从本质上讲,这只是在恶意消息的默认处理开始时锁定重绘(通过DefWindowProc)并在调用期间保持锁定状态,当它超出范围时解锁自身(因此,Scoped RedrawLock).由于以下原因,这对WM_NCLBUTTONDOWN 不起作用:
通过步进qWindowsWndProc WM_NCLBUTTONDOWN的默认处理过程中,我看到了 WM_SYSCOMMAND在WM_NCLBUTTONDOWN后直接在同一调用堆栈处理.此特定WM_SYSCOMMAND的wParam是0xf012 - 另一个正式无证值**.幸运的是,在MSDN WM_SYSCOMMAND页面的备注部分,有人评论过它.事实证明,它是SC_DRAGMOVE代码.
由于可能看似显而易见的原因,我们不能简单地锁定重绘以处理WM_NCLBUTTONDOWN,因为Windows会自动假设用户在单击非客户区域(在本例中为HTCAPTION)时尝试拖动窗口.锁定此处将导致窗口在拖动持续时间内永不重绘 - 直到Windows收到按钮消息(WM_NCLBUTTONUP或WM_LBUTTONUP).
当然,我在他们的代码中找到了这条评论,
if (!handled && message == WM_NCLBUTTONDOWN && w_param != HTSYSMENU &&
delegate_->IsUsingCustomFrame()) {
// TODO(msw): Eliminate undesired painting, or re-evaluate this workaround.
// DefWindowProc for WM_NCLBUTTONDOWN does weird non-client painting, so we
// need to call it inside a ScopedRedrawLock. This may cause other negative
// side-effects (ex/ stifling non-client mouse releases).
DefWindowProcWithRedrawLock(message, w_param, l_param);
handled = true;
}
Run Code Online (Sandbox Code Playgroud)
这使它看起来好像有同样的问题,但并没有完全解决它.
唯一的其他地方CEF处理WM_NCLBUTTONDOWN与此问题在同一范围内:
else if (message == WM_NCLBUTTONDOWN && delegate_->IsUsingCustomFrame()) {
switch (w_param) {
case HTCLOSE:
case HTMINBUTTON:
case HTMAXBUTTON: {
// When the mouse is pressed down in these specific non-client areas,
// we need to tell the RootView to send the mouse pressed event (which
// sets capture, allowing subsequent WM_LBUTTONUP (note, _not_
// WM_NCLBUTTONUP) to fire so that the appropriate WM_SYSCOMMAND can be
// sent by the applicable button's ButtonListener. We _have_ to do this
// way rather than letting Windows just send the syscommand itself (as
// would happen if we never did this dance) because for some insane
// reason DefWindowProc for WM_NCLBUTTONDOWN also renders the pressed
// window control button appearance, in the Windows classic style, over
// our view! Ick! By handling this message we prevent Windows from
// doing this undesirable thing, but that means we need to roll the
// sys-command handling ourselves.
// Combine |w_param| with common key state message flags.
w_param |= base::win::IsCtrlPressed() ? MK_CONTROL : 0;
w_param |= base::win::IsShiftPressed() ? MK_SHIFT : 0;
}
}
Run Code Online (Sandbox Code Playgroud)
虽然该处理程序解决了类似的问题,但它并不完全相同.
问题
所以在这一点上我被困住了.我不太确定在哪里看.也许我正在错误地阅读代码? 也许答案是在CEF,我只是忽略它?鉴于TODO:评论,似乎CEF工程师遇到了这个问题并且尚未提出解决方案.有谁知道我还能做什么? 我从哪里开始?不解决这个错误不是一个选择.我愿意深入挖掘,但此时我正在考虑实际处理Windows拖动事件,而不是让DefWindowProc处理它.但是,在用户实际拖动窗口的情况下,这可能仍会导致错误.
链接
我已经列出了我在研究中一直使用的链接列表.就个人而言,我自己下载了CEF源代码,以便我可以更好地浏览代码.如果您真的对解决此问题感兴趣,则可能需要执行相同的操作.
切线
只是为了验证CEF的代码,如果你查看hwnd_message_handler的标题,你还会注意到有两个未记录的窗口消息,值为0xAE和0xAF.我在WM_SETICON的默认处理过程中看到0xAE导致了问题,这段代码帮助确认我所看到的确实是真实的.
因此,实现此修复的实际方法是在 NC_LBUTTONDOWN 期间删除 WS_CAPTION 标志,并在 NC_LBUTTONUP 消息处理期间将其添加回来。但是,由于 Windows 在渲染之前计算其大小的方式,它可能会错误计算,因为它不考虑标题区域。因此,您需要在处理 WM_NCCALCSIZE 消息时对此进行补偿。
请记住,您需要偏移的像素量将根据您所在的 Windows 主题或操作系统而有所不同。即 Vista 的主题与 XP 的主题不同。因此,您需要决定一个比例因子以保持干净。