MFC界面绘制原理

2022年 8月 11日 91点热度 0人点赞

手动调用重绘函数刷新界面 (OnPaint), Invalidate, InvalidateRect() 等类似的函数都是提供手动调用 OnPaint 的.

在刷新窗口时经常要调用重绘函数, MFC 提供了三个函数用于窗口重绘

  • InvalidateRect(&Rect)
  • Invalidate()
  • UpdateWindow()

当需要更新或者重绘窗口时, 一般系统会发出两个消息 WM_PAINT (通知客户区有变化) 和 WM_NCPAINT (通知非客户区有变化), WM_NVPAINT. 系统会自己搞定 WM_PAINT 消息对应的函数是 OnPaint(), 它是系统默认的接受 WM_PAINT 消息的函数, 但我们一般在程序中做重绘时都在 OnDraw 函数中进行的, 因为在 ONPAIN 函数中调用了 ONDRAW 函数.

///CView 默认的标准的重画函数
void CView::OnPaint() {
    CPaintDC dc(this);
    OnPreparDC(&dc);
    OnDraw(&dc); //调用了 OnDraw
}

上面讲到 InvalidateRect(&Rect), Invalidate() 两个函数形式和功能差不多, 但 Invalidate 是使得整个窗口无效, 形成无效矩形, 而 InvalidateRect(&Rect) 是使得指定的区域无效.

Invalidate() 申明无效, 等待 WM_PAINT 消息以便重绘, 队列中无其他消息时系统会自动发送, UpdateWindow() 会立即发送 WM_PAINT, 不过在它发送前, 先调用 GetUpdateRect(hWnd, NULL,TRUE) 看有无可绘制区域, 如果没有则不发送消息.

RedrawWindow() 则是具有 Invalidate()UpdateWindow() 的双特性. 声明窗口的状态为无效, 并立即更新窗口, 立即调用 WM_PAINT 消息处理.

系统为什么不在调用 Invalidate 时发送 WM_PAINT 消息呢? 又为什么非要等应用消息队列为空时才发送 WM_PAINT 消息呢? 这是因为系统把在窗口中的绘制操作当作一种低优先级的操作, 于是尽可能地推后做. 不过这样也有利于提高绘制的效率: 两个 WM_PAINT 消息之间通过 InvalidateRectInvaliateRgn 使之失效的区域就会被累加起来, 然后在一个 WM_PAINT 消息中一次得到更新, 不仅能避免多次重复地更新同一区域, 也优化了应用的更新操作. 像这种通过 InvalidateRectInvalidateRgn 来使窗口区域无效, 依赖于系统在合适的时机发送 WM_PAINT消息的机制实际上是一种异步工作方式, 也就是说, 在无效化窗口区域和发送 WM_PAINT 消息之间是有延迟的; 有时候这种延迟并不是我们希望的, 这时我们当然可以在无效化窗口区域后利用 SendMessage 发送一条 WM_PAINT 消息来强制立即重画, 但不如使用 Windows GDI 为我们提供的更方便和强大的函数: UpdateWindow 和 RedrawWindow.UpdateWindow 会检查窗口的 Update Region, 当其不为空时才发送 WM_PAINT 消息;RedrawWindow 则给我们更多的控制: 是否重画非客户区和背景, 是否总是发送 WM_PAINT 消息而不管 Update Region 是否为空等.

BeginPaint 和 WM_PAINT 消息紧密相关. 试一试在 WM_PAINT 处理函数中不写 BeginPaint 会怎样? 程序会像进入了一个死循环一样达到惊人的 CPU 占用率, 你会发现程序总在处理一个接 一个的 WM_PAINT 消息. 这是因为在通常情况下, 当应用收到 WM_PAINT 消息时, 窗口的 Update Region 都是非空的 (如果为空就不需要发送 WM_PAINT 消息了), BeginPaint 的一个作用就是把该 UpdateRegion 置为空, 这样如果不调用 BeginPaint, 窗口的 Update Region 就一直不为空, 如前所述, 系统就会一直发送 WM_PAINT 消息.

BeginPaint 和 WM_ERASEBKGND 消息也有关系. 当窗口的 Update Region 被标志为需要擦除背景时, BeginPaint 会发送 WM_ERASEBKGND 消息来重画背景, 同时在其返回信息里有一个标志表明窗口背景是否被重画过. 当我们用 InvalidateRect 和 InvalidateRgn 来把指定区域加到 Update Region 中时, 可以设置该区域是否需要被擦除背景, 这样下一个 BeginPaint 就知道是否需要 WM_ERASEBKGND 消息了.

另外要注意的一点是, BeginPaint 只能在 WM_PAINT 处理函数中使用.

如果问题解决起来不妥或者有更好的解决办法, 麻烦请告知, 帮助曾经和你一样的入门者, 谢谢.

rainbow

这个人很懒,什么都没留下

文章评论