为什么我必须在OnMouseMove期间调用Repaint?

时间:2012-07-31 21:37:37

标签: delphi c++builder repaint

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

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

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

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

为什么有必要打电话给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;
}

3 个答案:

答案 0 :(得分:4)

有一次,我注意到在我的应用程序中使用VCL拖放的相同行为。以某种方式,发布自己或调用WM_PAINT导致的Invalidate消息无法到达消息队列的顶部。

而不是Repaint,我建议使用Update来更好地处理儿童重绘。

答案 1 :(得分:2)

Repaint()立即重新调用它所调用的控件。有可能你的调整逻辑正在改变GraphicArea和ChildForms的方式,要求他们用新值重绘自己,但他们实际上并不知道需要重绘它们,所以他们不这样做。这可以解释为什么除非你手动触发重新绘制,否则你没有看到任何变化。

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

答案 2 :(得分:2)

Raymond Chen explains WM_PAINT消息的工作原理。使窗口无效(无论是通过设置导致窗口无效的VCL方法或属性,还是通过调用Invalidate手动设置)都会导致设置一个标志,说明应该传递WM_PAINT消息下次调用GetMessage并且没有消息可用。

据我所知,如果消息生成得足够快(例如,WM_MOUSEMOVE消息)并处理这些消息需要足够长的时间,那么消息队列可能永远不会为空,所以{{ 1}}消息永远不会传递。

解决方案是手动调用WM_PAINT(其执行情况应优于Update)或类似情况。其他注意事项:

  • 重新绘制所有拥有的表单:我没有在这里找到一个干净的解决方案,所以我可能会继续手动跟踪所拥有的表单并手动调用它们上的重绘。 (如果我想,我可以迭代TForm的组件寻找TForms,但这会增加可衡量的开销。)
  • 处理子控件(例如我的示例中未将自身重绘为已禁用的TRadioButton):使用RedrawWindow,而不是调用RepaintRepaint,这可以指示孩子窗户也重绘自己。
    RedrawWindow(ChildForm1->Handle, NULL, NULL,
        RDW_UPDATENOW | RDW_ALLCHILDREN);