【问题标题】:Why do I have to call Repaint during OnMouseMove?为什么我必须在 OnMouseMove 期间调用 Repaint?
【发布时间】:2012-07-31 21:37:37
【问题描述】:

在我的 Delphi / C++Builder 应用程序中,我有一个 OnMouseMove 处理程序,它允许用户通过拖动绘图元素与绘图进行交互。 (我们手动实现了必要的拖放逻辑,而不是使用 VCL 的 OnDragOver 等。)

OnMouseMove 事件根据绘图的当前状态更新主窗体和几个子窗体。但是,只要我移动鼠标,主窗体和任何子窗体都不会真正重绘它们的更新状态,除非我在窗体及其每个子窗体上手动调用 Repaint。这有点脆弱,因为很容易错过需要重新绘制的子窗体。

在我停止移动鼠标的那一刻,表单按预期重新绘制,因此控件似乎按预期无效,只要 OnMouseMove 事件/WM_MOUSEMOVE 消息进入,它们就不会重新绘制。(如果我慢慢拖动非常,然后屏幕也会按预期重新绘制。)

即使在每个窗体上手动调用 Repaint 也总是不够的,因为除非我单独重绘它们,否则单个子窗体的控件可能不会重绘。 (例如,如果我调用其父 TForm 的 Repaint,TEdit 会显示其新值,但我禁用的 TRAdioButton 不会显示为禁用,除非我调用它自己的 Repaint。)

为什么需要调用 Repaint?为什么当我拖动鼠标时 Windows 不会自动重新绘制我的应用程序的窗口?有没有比手动枚举需要调用 Repaint 的窗口更好的重绘窗口的方法?

通过玩一个简短的测试应用程序,我想知道问题是否在于我的 OnMouseMove 事件太慢以至于由于应用程序忙于 WM_MOUSEMOVE 而无法发送 WM_PAINT 消息?我不确定这是否确实如此,或者如果是这样,该怎么办。

这里有一些(希望不要过于简化)代码来说明我在做什么。 GraphArea 是一个 TImage,其 Canvas 包含绘图。

void __fastcall TMachineForm::GraphAreaMouseDown(TObject *Sender,
    TMouseButton Button, TShiftState Shift, int X, int Y)
{
    if (IsNearAdjustableObject(X, Y)) {
        is_adjusting = true;
    }
}

void TMachineForm::GraphAreaMouseMove(TObject *Sender,
    TShiftState Shift, int X, int Y)
{
    if (is_adjusting) {
        AdjustObject(X, Y);

        /* Draws to the GraphArea TImage by calling GraphArea->Canvas methods */
        RedrawGraphArea();

        /* Updates several standard VCL controls on ChildForm1 and ChildForm2;
         * e.g., ChildForm1->Edit1->Text = CalculatedValue(); */
        NotifyChildForm1OfAdjustment();
        NotifyChildForm2OfAdjustment();

        /* This is where I have to manually call Repaint. I don't know why. */
        GraphArea->Repaint();
        ChildForm1->Repaint();
        ChildForm2->Repaint();
    }
}

void TMachineForm::GraphAreaMouseUp(TObject *Sender,
    TMouseButton Button, TShiftState Shift, int X, int Y)
{
    is_adjusting = false;
}

【问题讨论】:

  • 我猜这是因为拖动操作在一个特殊的消息循环中运行,而不是类似于模式消息循环。
  • @DavidHeffernan - 即使我从 OnMouseMove 处理程序手动实现自己的拖放操作,而不是调用任何 VCL 拖动操作?
  • 如果不了解您手动实现拖放的任何内容,这真的很难说。通常,您不必调用Repaint(并且不应该首先调用-父控件上的Invalidate 通常足以更新它并且它是子控件)。
  • 好吧,我假设你的意思是你打电话给Repaint,因为Invalidate 不好。如果您在窗口上进行自定义绘图,我希望您使用InvalidateRect 来确保所有受拖动影响的区域都有机会自己绘制。在不调用Repaint的代码版本中,你叫什么?显示代码意味着我们不必猜测您在做什么。请记住,我们看不到您的屏幕。
  • 仍然没有足够的代码。不清楚为什么 3 次 Repaint 调用之前的代码会导致绘制周期。此外,您不需要捕获鼠标以确保您始终收到MouseUp。很可能鼠标移动事件的处理速度比它们发布的速度慢,因此 WM_PAINT 永远不会被处理。请记住,WM_PAINT 是一个低优先级事件,因此队列必须为空才能处理。如果你慢慢拖动会发生什么?

标签: delphi c++builder repaint


【解决方案1】:

有一次,我在使用 VCL 拖放的应用程序中发现了相同的行为。不知何故,由发布自己或调用 Invalidate 产生的 WM_PAINT 消息不会到达消息队列的顶部。

我建议不要使用Repaint,而是使用Update,这样可以更好地处理儿童重绘。

【讨论】:

    【解决方案2】:

    Repaint() 立即重新绘制调用它的控件。很有可能您的调整逻辑正在对 GraphicArea 和 ChildForms 进行更改,以要求它们使用新值重新绘制自己,但它们实际上并不知道需要重新绘制它们,因此它们不会这样做。这可以解释为什么除非您手动触发重绘,否则您看不到任何更改。

    我建议使用Invalidate() 而不是Repaint()Invalidate() 向操作系统发出信号,表示控件需要重新绘制,但实际上还没有执行绘制。这让操作系统可以在自己的时间管理绘画,并且控件将正常接收来自操作系统的绘画请求,而不是直接从您那里接收。

    【讨论】:

    • Invalidate() 没有帮助,显然是因为鼠标消息进入得太快,操作系统无法传递 WM_PAINT 请求。
    • 您可以调用每个控件的Update() 方法来一次处理一个挂起的绘制请求,或者您可以单独调用Form 的Update() 方法来处理Form 和子项的所有挂起的绘制请求一次。
    • 我试图避免手动列出需要更新的每个表单和控件;这似乎很难维护。正如我试图在我的问题中解释的那样,调用表单的Update 会重新绘制一些但不是所有的孩子。 (RedrawWindow 可以根据需要正确重绘所有子级。)
    【解决方案3】:

    Raymond Chen explains WM_PAINT 消息如何工作。使窗口无效(无论是通过设置导致窗口无效的 VCL 方法或属性,还是通过调用 Invalidate 手动)有效地导致设置一个标志,表示下次应该传递 WM_PAINT 消息 @987654325 @ 被调用,但没有可用的消息。

    据我所知,如果消息的生成速度足够快(例如,WM_MOUSEMOVE 消息)并且处理这些消息需要足够长的时间,那么消息队列可能永远不会为空,所以 WM_PAINT 消息永远不会交付。

    解决方案是手动调用Update(其性能应该比Repaint 好一点)或类似的。其他注意事项:

    • 重绘所有拥有的表单:我还没有在这里找到一个干净的解决方案,所以我可能会继续手动跟踪拥有的表单并在它们上手动调用 Repaint。 (如果需要,我可以遍历 TForm 的组件以查找 TForm,但这会增加可衡量的开销。)
    • 处理子控件(例如在我的示例中不重绘自身为禁用状态的 TRAdioButton):不要调用 RepaintUpdate,而是使用 RedrawWindow,它可以指示子窗口也重绘自身. RedrawWindow(ChildForm1->Handle, NULL, NULL, RDW_UPDATENOW | RDW_ALLCHILDREN);

    【讨论】:

    • 如果您只是使窗口的一部分无效(而不是整个表面,如 Invalidate),则更新只会比重绘好一点。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-21
    • 1970-01-01
    • 2013-04-15
    • 2019-11-05
    • 2016-06-09
    • 2017-01-16
    相关资源
    最近更新 更多