在调整窗口大小时绘制内容会留下未涂色的边框。

3
我遇到的问题似乎很琐碎,但我找不到解决方法。我的窗口里有一些图形。
为了简单起见,我们假设它是一个填满整个客户区域的实心绿色矩形。我想让这个矩形在窗口改变大小时重新绘制并填满整个窗口。最初我所做的是从WM_SIZE处理程序发布WM_PAINT消息。
它能够工作,但如果我快速移动鼠标,我会看到绿色矩形周围有一些未绘制(白色)区域(实际上只有一两个边靠近鼠标的位置)。我对问题的理解是,处理用户输入(鼠标)的系统线程比我处理WM_PAINT消息的线程更快。这意味着在我开始绘制更新的矩形(其大小取自WM_SIZE)时,鼠标实际上已经移动了一点,系统会绘制一个与我试图用绿色填充的不同的新窗口框架。这会在调整大小期间创建未填充的边框旁边的区域。
当我停止调整大小时,绿色最终填满整个窗口,但在调整大小期间,边框附近会出现闪烁,这很烦人。为了解决这个问题,我尝试了以下方法。
bool finishedPainting;
RECT windowRect;

case WM_PAINT :
    // .....  painting here

  finishedPainting = TRUE;
  break;

case WM_SIZE :
    // .... some actions

    // posting WM_PAINT
  InvalidateRect(hWnd, NULL, FALSE);
  PostMessage(hWnd, WM_PAINT, 0, 0);
  break;

case WM_SIZING :
    // this supposedly should prevent the system from passing
    // new window size to WM_SIZE
  if (!finishedPainting) memcpy((void*)lParam, &windowRect, sizeof(windowRect));
  else {
      // remember current window size for later use
    memcpy(&windowRect, (void*)lParam, sizeof(windowRect));
    finishedPainting = FALSE;
  }
  return TRUE;

它不起作用。稍微变化一下,我也尝试了这个。

bool  finishedPainting;
POINT cursorPos;

case WM_PAINT :
    // .....  painting here

  finishedPainting = TRUE;
  break;

case WM_SIZE :
  if (!finishedPainting) SetCursorPos(cursorPos.x, cursorPos.y);
  else {
    finishedPainting = FALSE;
    GetCursorPos(&cursorPos);

      // .... some actions

    InvalidateRect(hWnd, NULL, FALSE);
    PostMessage(hWnd, WM_PAINT, 0, 0);
  }
  break;

这也行不通。就我所理解的,解决问题的方法在于以某种方式减缓鼠标的速度,使得它仅在绘画完成后(拖动窗口的角或边缘)才移动到屏幕的下一个位置。
有什么想法可以实现这一点吗?或者我对问题和解决方案的看法存在根本性错误?
// ====================================================
更新
我做了几个实验,以下是我的发现:
1)调整大小时,消息序列为WM_SIZING - WM_NCPAINT - WM_SIZE - WM_PAINT。对我来说,这看起来有点奇怪。我期望WM_SIZE在WM_SIZING之后紧随其后,而不会被WM_NCPAINT中断。
2)在每个消息处理程序中,我都在调整大小时检查窗口的宽度(为简单起见,我只改变宽度)。令人惊讶的是,在WM_SIZE中测量的宽度与在WM_SIZING中测量的宽度不同,但与在WM_NCPAINT和WM_PAINT中测量的宽度相同。这本身并不是问题,只是一个奇怪的事实。
3)我得出结论,导致在窗口边框附近出现闪烁的两个主要原因。第一个原因是WM_NCPAINT先于WM_PAINT。想象一下你正在拉伸窗口。新的框架将首先出现(WM_NCPAINT先出现),然后WM_PAINT填充客户区域。人眼会捕捉到新框架已经出现但是为空的短暂时间。即使您指定不希望在重新绘制之前删除窗口背景,新添加的区域仍为空,您可以看到它持续了几秒钟。当您快速向右移动右侧的窗口边缘时,闪烁的原因最好演示为此。闪烁效果的另一个原因不太明显,并且在将左侧窗口边缘抓住并向左移动时最容易看到。在此移动期间,您将看到沿右侧边缘未填充的区域。就我所理解的,这种效果是由此引起的。当用户调整大小时,Windows执行以下操作:A)发送WM_NCPAINT以绘制新框架,B)将旧客户端区域的内容复制到新的左上角窗口中(在我们的情况下,它移动到左侧),C)发送WM_PAINT以填充新的客户端区域。但是在阶段B期间,由于某种原因,Windows会在右侧边缘产生这些未填充的区域,尽管它似乎不应该,因为旧内容应该保持在原地,直到在WM_PAINT期间被重新绘制。
好了,问题仍然存在-如何在调整大小时摆脱这些伪影。就我所看到的,使用标准技术和函数似乎无法实现这一点,因为它们是由Windows在调整大小期间执行的步骤序列引起的。交换WM_NCPAINT和WM_PAINT可能会有所帮助,但这似乎超出了我们的控制范围(除非有一种简单的方法可以做到这一点,我只是不知道)。

你在这里自掘坟墓。通过双缓冲或编写一个什么也不做的WM_ERASEBKGND消息处理程序来抑制闪烁。 - Hans Passant
2
汉斯,我使用双缓冲技术,处理WM_ERASEBKGND消息并采用其他技巧来减少闪烁。问题在于,在这种情况下,闪烁并不是由于绘图技术不当引起的。事实上,在正常操作期间,我根本没有闪烁。唯一无法解决的是调整大小时的绘图。而且,即使在这种情况下,我的东西也不会闪烁。闪烁的是窗口的新增区域,因为我无法控制何时添加它。似乎问题出现在更低的一两个级别上——当Windows调整窗口大小时所做的事情。 - wladp
你不应该需要进行WM_SIZE / SIZING处理 - 如果你想要在大小更改时重新绘制整个窗口,只需在WNDCLASS中指定CS_HREDRAW和CS_VREDRAW类样式位,Windows会为你处理。此外,在边框附近存在一定程度的闪烁似乎是不可避免的(根据下面马克的答案),你可能希望与Explorer、Chrome、IE等应用程序进行比较,以查看基准,并检查你至少不比这些应用程序更差。 - BrendanMcK
我处理WM_SIZE的原因是,当窗口被调整大小时,我需要浏览我的数据并重新计算一些参数,而WM_SIZE似乎只是一个方便的位置。至于在边缘附近不可避免的闪烁,我同意,这似乎是系统输入(鼠标)处理线程以比应用程序线程更高的优先级(更快)运行的副作用。 - wladp
4个回答

4

您不应该自己发布或发送WM_PAINT消息。相反,使用::InvalidateRect来使您的窗口的部分无效,并让Windows决定何时发送WM_PAINT消息。


从 WM_SIZE 处理程序中删除 PostMessage(hWnd,WM_PAINT,0,0)不会改变任何东西(仍然在边界附近闪烁)。 - wladp
你应该删除所有已完成的绘画代码。不要试图猜测系统。当收到 WM_SIZE 消息时,调用 InvalidateRect 函数;当 Windows 请求绘制时,调用 WM_PAINT 函数。 - Paul Mitchell
对于与闪烁有关的一般问题,可以在这里查看优秀的答案:https://dev59.com/sHVC5IYBdhLWcg3wvT1a - Paul Mitchell
谢谢Paul,我已经阅读了。我几乎使用了答案中的所有技术。它们主要致力于在窗口不调整大小时如何正确绘制。但是当它调整大小时,似乎更难控制Windows做什么以及何时做什么。 - wladp

3

Windows系统是有意这样设计的。响应用户(即鼠标)通常被认为比拥有完全更新绘制的窗口更加重要。

如果你总是在WM_PAINT处理程序中绘制整个窗口,通过重写WM_ERASEBKGND处理程序并返回不做任何操作,可以消除大量闪烁。

如果你真的坚持优先更新窗口而不是鼠标响应性能,可以使用RDW_UPDATENOW标志替换InvalidateRect调用,使用RedrawWindow调用。

编辑:你的观察结果很有道理。WM_SIZING在窗口调整大小之前被触发,让你有机会修改大小和/或位置。你可以尝试在WM_NCPAINT处理程序中绘制客户区,甚至在边框绘制之前。在绘制完成后,你可以验证客户区以防止重新绘制。


1
RedrawWindow()是一个好主意。我想知道我怎么会忘记这个函数。然而,当我尝试使用它时,结果仍然是一样的——在绘制完成之前,在窗口(正在调整大小的窗口)中添加了某种新区域。这与该函数所应该完成的工作相矛盾(在返回之前完成绘制),并导致沿边界的撕裂效果。 - wladp
更新:我错了,RedrawWindow()的工作方式与描述的完全相同。但是RDW_UPDATENOW标志不影响主窗口,它仅影响其子窗口,使它们在函数返回之前接收WM_PAINT(并希望被重绘)。在我的情况下,我没有子窗口,这导致RedrawWindow()无效,即使用RedrawWindow()或InvalidateRect()没有任何区别。 - wladp
@wladp,我认为你误解了RedrawWindow。它总是更新您传递给它的窗口句柄,并根据标志包括或排除子级。请参阅文档:http://msdn.microsoft.com/en-us/library/dd162911.aspx 您可以尝试包括RDW_INVALIDATE标志。 - Mark Ransom
你是对的,Mark。我做了些实验并确认RedrawWindow()会导致窗口重绘,并且它在窗口被重绘之前不会返回。我也进行了一些调查,请看我在顶部的发现(我编辑了我的原始问题)。非常感谢您的意见。 - wladp
我按照你的建议在WM_NCPAINT中尝试绘制客户区域。它可以正常工作,但是仍然存在闪烁效果。似乎闪烁现象减轻了一些,整个窗口更新速度也变慢了一点,但主要问题是这种效果并没有消失。我想我必须在这个点上停下来,至少暂时停下来。虽然彻底消除这种效果是可取的,但如果它仍然存在,那么这绝对不是关键问题。我已经花了足够的时间在这上面,还有更重要的事情要做。无论如何,感谢你的帮助和线索,马克! - wladp

1

手动发布WM_PAINTWM_SIZE是个坏主意。但你可以使用一个奇怪的hacky crazy方法,在WM_SIZE中记录新调整大小的坐标,使用MoveWindow将窗口的大小更改回先前的大小,然后在绘制完成后WM_PAINT消息中使用MoveWindow手动重新调整大小。

另一个可能的解决方案是不断地填充窗口颜色,无论屏幕是否被调整大小。即

// in the WinMain function
if (GetMessage(&msg,NULL,NULL,0))
{
    TranslateMessage(&msg,NULL,NULL);
    DispatchMessage(&msg,NULL,NULL);
}
else
{
     // fill your window with the color in here
}

当然,你也可以直接将窗口的背景颜色设置为绿色,而不是自己进行所有的绘制:

// Before RegisterClass or RegisterClassEx
// wincl is a WNDCLASS or WNDCLASSEX
wincl.hbrBackground = CreateSolidBrush(RGB(50, 238, 50));

这不是关于填充窗口颜色或绘制矩形的问题。它们只是用来说明问题的简单事物。实际上,我有一个显示实时数据的图表。虽然在调整大小时出现闪烁并不是很重要,但肯定很烦人。至于你的第一个想法(在WN_SIZE中使用MoveWindow())实际上是我尝试做的另一种变化。但它也不起作用。 - wladp
据我所知,原因是一样的——在记录了 WM_SIZE 中的窗口大小之后,当 WM_PAINT 来临时,鼠标已经处于略微不同的位置。鼠标拖动窗口的角落或边缘到那个位置,结果是新的窗口框架与我即将绘制的矩形不同。这反过来又导致了沿边界未填充的区域。 - wladp

0

我曾经遇到过同样的问题。虽然解决方案有很多词,但总结起来就是将窗口类样式更改为CS_VREDRAW | CS_HREDRAW:

void WindowsGUI::initialise(){
    gWindowClass.cbSize = sizeof(gWindowClass);
    gWindowClass.lpfnWndProc = windowProcess;
    gWindowClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    gWindowClass.hCursor = LoadCursor(NULL, IDC_ARROW);
    gWindowClass.hbrBackground = (HBRUSH) GetStockObject (DKGRAY_BRUSH + 1);
    gWindowClass.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
    gWindowClass.style = **CS_VREDRAW** | **CS_HREDRAW**;
    glGUI = this;
}

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