3

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

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

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

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

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

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

这是一些(希望不会过于简化)代码来说明我在做什么。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;
}
4

3 回答 3

4

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

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

于 2012-07-31T23:48:10.803 回答
2

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

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

于 2012-07-31T22:42:49.370 回答
2

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

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

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

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