【发布时间】:2015-02-11 13:00:22
【问题描述】:
所以,我偶然发现了一个有趣的 Windows API 错误,我想知道是否有人对如何解决它有一些见解。似乎连谷歌都在为此苦苦挣扎。应该注意的是,虽然我将在 Qt 源代码中解决这个问题,但问题在于 Windows 默认消息处理,而不是 Qt。我将提到的所有文件都可以在网上找到,因为它们都是开源库。下面是一个复杂的问题,我将尝试提供尽可能多的上下文。我自己花了很多时间和精力来解决这个问题,但由于我才做了大约 8 个月的工程师,所以我仍然很缺乏经验,很可能会遗漏一些明显的东西。
上下文:
我编写了一个程序,它使用 Qt 为我的窗口添加自定义外观。这些皮肤覆盖了系统默认的非客户端 UI 皮肤。换句话说,我使用自定义绘制的框架(由 Qt 支持)。自 Qt5 以来,当我的程序在 任何 Windows Aero 操作系统之前的操作系统 上运行时,我一直遇到问题(小于 XP 并且大于禁用 Windows Aero 的 Vista)。不幸的是,Qt 开发人员几乎已经确认他们不再真正支持 XP,所以我不会依赖他们来修复这个错误。
错误:
在运行具有 composition 已禁用(Windows Aero 已禁用或不存在)的机器时单击非客户端区域中的任意位置将导致 Windows 在我的自定义之上重新绘制其系统默认非客户端 UI皮肤。
我的研究
经过一些调试和调查,我找到了 qwindowscontext.cpp 中的 qWindowsProc。我能够确定在我的窗口皮肤被绘制之前要处理的最后一个窗口消息是 WM_NCLBUTTONDOWN。这看起来很奇怪,所以我上网。
果然,我找到了一个名为 hwnd_message_Handler.cc 的文件,它来自 Google 的 Chromium Embedded Framework (CEF)。在该文件中有许多关于各种 Windows 消息如何由于某些疯狂的原因导致在自定义框架上重绘系统默认非客户端框架的 cmet。以下是这样的评论。
// 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 ;-)
该文件中还存在几个自定义消息处理程序以防止发生此错误。例如,我发现导致此错误的另一条消息是 WM_SETCURSOR。果然,他们有一个处理程序,当移植到我的程序时,它工作得很好。
他们处理这些消息的常用方法之一是使用 ScopedRedrawLock。本质上,这只是在恶意消息的默认处理(通过 DefWindowProc)开始时锁定重绘,并在调用期间保持锁定,当超出范围时自行解锁(因此,ScopedRedrawLock) .对于 WM_NCLBUTTONDOWN,这将不起作用,原因如下:
在 WM_NCLBUTTONDOWN 的默认处理过程中单步执行 qWindowsWndProc,我看到 WM_SYSCOMMAND 在 WM_NCLBUTTONDOWN 之后直接在同一个调用堆栈中处理。此特定 WM_SYSCOMMAND 的 wParam 是 0xf012 - 另一个官方未记录的值**。幸运的是,在 MSDN WM_SYSCOMMAND 页面的备注部分,有人对此发表了评论。原来是SC_DRAGMOVE代码。
出于似乎显而易见的原因,我们不能简单地锁定重绘来处理 WM_NCLBUTTONDOWN,因为如果用户单击非客户区(在本例中为 HTCAPTION),Windows 会自动假定用户正在尝试拖动窗口。在此处锁定将导致窗口在拖动期间永远不会重绘 - 直到 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;
}
这使他们看起来好像遇到了同样的问题,但并没有完全解决它。
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;
}
}
虽然该处理程序解决了类似的问题,但并不完全相同。
问题
所以在这一点上我被困住了。我不太确定在哪里看。 也许我读错了代码? 也许 CEF 中有答案,而我只是忽略了它? 似乎 CEF 工程师遇到了这个问题,但还没有给出解决方案,给出 TODO: 评论。 有人知道我还能做什么吗? 我该往哪里走? 不解决这个错误是不可行的。我愿意深入挖掘,但此时我正在考虑自己实际处理 Windows 拖动事件,而不是让 DefWindowProc 处理它。但是,在用户实际拖动窗口的情况下,这可能仍然会导致错误。
链接
我列出了我在研究中一直使用的链接列表。就个人而言,我自己下载了 CEF 源代码,以便更好地浏览代码。如果您真的有兴趣解决这个问题,您可能也需要这样做。
相切
只是为了给 CEF 的代码带来验证,如果你查看 hwnd_message_handler 的标头,你还会注意到有两个值 0xAE 和 0xAF 的未记录的 windows 消息。在导致问题的 WM_SETICON 的默认处理过程中,我看到了 0xAE,这段代码有助于确认我所看到的确实是真实的。
【问题讨论】:
-
但是你的问题是什么?
-
我的建议是不要担心。这些天您不应该支持 XP,更不用说更早的任何事情了。极少数用户会在禁用 Aero 的情况下运行 Vista。
-
作为开发人员,我们有责任鼓励人们从 XP 中继续前进,不支持它是这样做的好方法 :)
-
如果它不起作用,用户不会这样看,他们只会把你放在头上
-
Even Qt no longer supports XP,所以你真的很无助。如果您使用的是 Qt 5,除非您解决所有在 Qt 5 中不起作用的问题,否则您不能真正说您支持 XP,但这可能是一项艰巨的任务,每次 Qt 更新都会变得越来越糟。
标签: c++ windows qt winapi user-interface