为什么在OnMouseMove中必须调用Repaint?

问题描述:

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

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

即时我停止移动鼠标,表单重新绘制按预期的方式,所以它看起来控件正在作为预期失效,只要OnMouseMove事件/ WM_MOUSEMOVE消息进来,它们就不会重新绘制。(If我很慢地拖动非常,然后屏幕也会按预期重新绘制。) (例如,如果我调用其父母TForm的重画,则TEdit会显示其新值,但我禁用的TRadioButton不会显示为禁用,除非我将其称为它自己的重绘。)

为什么有必要根本打电话给Repaint ?为什么当我拖动鼠标时,Windows不会自动重新绘制应用程序的窗口?有没有更好的方法来重绘窗口比尝试手动枚举哪些窗口需要重绘调用?

从一个简短的测试应用程序,我想知道是否问题是我的OnMouseMove事件足够慢,WM_PAINT消息不会得到调度,因为应用程序是忙于WM_MOUSEMOVE?我不确定这是否确实如此,或者如果是这样的话,该怎么办。

下面是一些(希望不是过于简化)代码来说明我在做什么。 GraphArea是Canvas包含图的TImage。

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; 
} 
+0

我想这是因为拖动操作在一个特殊的消息循环内运行,而不是类似于模态消息循环。 – 2012-07-31 21:46:10

+0

@DavidHeffernan - 即使我是从OnMouseMove处理程序手动实现自己的拖放操作,而不是调用任何VCL拖动操作? – 2012-07-31 21:47:39

+2

如果你不了解你的手动拖放操作,那真的很难说。通常情况下,您不必调用Repaint(并且不应该放在首位 - 父控件上的“Invalidate”通常足以更新它并且它是子控件)。 – 2012-07-31 21:48:12

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

据我所知,如果消息生成速度足够快(例如,WM_MOUSEMOVE消息)并且处理这些消息花费足够长的时间,那么消息队列可能永远不会为空,因此WM_PAINT消息不会被传递。

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

  • 重绘拥有的所有形式:我还没有找到一个干净的解决方案在这里,所以我可能会继续人工跟踪资的形式和他们手动调用重绘。 (如果需要,我可以迭代TForm的组件来寻找TForms,但是这会增加可测量的开销。)
  • 处理子控件(例如在我的示例中不会将自身重绘为禁用的TRadioButton):而不是调用RepaintUpdate,使用RedrawWindow,它可以指示子窗口重绘自己。
     
    RedrawWindow(ChildForm1->Handle, NULL, NULL, 
        RDW_UPDATENOW | RDW_ALLCHILDREN); 
    
+0

如果只是使窗口的某些部分无效(而不是整个表面与无效),则更新只会比重新绘制好一点。 – 2012-08-01 15:41:50

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

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

+0

'Invalidate()'不起作用,显然是因为鼠标消息太快,操作系统无法发送'WM_PAINT'请求。 – 2012-08-01 14:34:23

+0

您可以调用每个控件的Update()方法来一次处理一个待处理的绘画请求,或者您可以单独调用Form的Update()方法来一次处理Form和Children的所有待处理的绘制请求。 – 2012-08-01 17:21:58

+0

我试图避免必须手动列出每个需要更新的表单和控件;这似乎很难保持。正如我试图在我的问题中解释的,调用窗体的“更新”重新绘制了一些但不是所有的孩子。 (如果需要,'RedrawWindow'可以正确重新绘制所有的子项。) – 2012-08-01 17:34:34

有一次,我注意到我的应用程序中使用VCL拖放的相同行为。不知怎的,WM_PAINT因发布自己或致电Invalidate而产生的消息没有达到消息队列的顶部。

而不是Repaint,我建议使用Update应该更好地处理儿童重新粉刷。