【问题标题】:Draw over controls in Delphi form以 Delphi 形式绘制控件
【发布时间】:2010-12-18 15:26:16
【问题描述】:

如何在 Forms 画布上和 Form 上的控件上绘制一些东西?

我尝试以下方法:

procedure TForm1.FormPaint(Sender: TObject);
var x,y: Integer;
begin
  x := Mouse.CursorPos.X - 10;
  y := Mouse.CursorPos.Y - 10;
  x := ScreentoClient(point(x,y)).X - 10;
  y := ScreenToClient(point(x,y)).Y - 10;
  Canvas.Brush.Color := clRed;
  Canvas.FillRect(rect(x, y, x + 10, y + 10));
  Invalidate;
end;

矩形是在绘制其他控件之前绘制的,因此它隐藏在控件后面(根据 Delphi 文档,这是预期的行为)。

我的问题是如何绘制控件?

【问题讨论】:

  • 如果您将TButton 放在TMemo 上,您是否希望备忘录能够在按钮表面绘制?如果您将 Microsoft WordPad 窗口放在 Microsoft Paint 窗口之上,您是否希望 Paint 窗口能够在 WordPad 窗口内绘图?
  • @Andreas:那么像小狗这样的“玩具”如何在你的屏幕上跑来跑去,或者你可以用锤子工具来“破解”你的屏幕曾经由 Mark Miller 在“Fun with Delphi”中演示过会议,做吗?
  • @Marjan:它们是形状不规则的窗户。就像 Microsoft WordPad 可以位于 Microsoft Paint 之上一样,形状不规则的窗口(例如狗)也可以位于 Microsoft Paint 之上。
  • 实际上,这不是真的,Andreas Rejbrand。它很容易在桌面窗口上绘制,但是擦除刚刚绘制的东西会造成问题。
  • @Andreas - 那么为什么不告诉他使用不同的 DC 而不是说它无法完成。

标签: delphi drawing custom-draw


【解决方案1】:

不要在油漆处理程序中“无效”。 Invalidating 会导致发送WM_PAINT,这当然会开始整个油漆处理。即使您不移动鼠标,您发布的代码示例也会导致“OnPaint”事件一次又一次地运行。由于您的绘图取决于光标的位置,因此您将为此使用“OnMouseMove”事件。但是您还需要拦截其他窗口控件的鼠标消息。出于这个原因,下面的示例使用了“ApplicationEvents”组件。如果您的应用程序将有多个表单,您需要设计一种机制来区分您在哪个表单上绘图。

另请参阅文档,VCL 的 Invalidate 使整个窗口无效。您不需要这样做,您正在绘制一个小矩形,并且您确切知道要在哪里绘制。只是使您将绘制的位置和绘制的位置无效。

至于在控件上绘图,实际上绘图部分很容易,但是您不能使用提供的画布来做到这一点。窗体具有WS_CLIPCHILDREN 样式,子窗口的表面将被排除在更新区域之外,因此您必须使用GetDCExGetWindowDC。正如 cmets 中提到的“user205376”,擦除您绘制的内容有点棘手,因为您实际上可以在多个控件上绘制一个矩形。但是这个 api 也有一个快捷方式,正如您将在代码中看到的那样。

我尝试对能够遵循的代码进行一些注释,但跳过了错误处理。实际的绘制可能在“OnPaint”事件处理程序中,但不是从“TWinControl”下降的控件在处理程序之后被绘制。所以它在 WM_PAINT 处理程序中。

type
  TForm1 = class(TForm)
    [..]
    ApplicationEvents1: TApplicationEvents;
    procedure FormCreate(Sender: TObject);
    procedure ApplicationEvents1Message(var Msg: tagMSG; var Handled: Boolean);
  private
    FMousePt, FOldPt: TPoint;
    procedure WM_PAINT(var Msg: TWmPaint); message WM_PAINT;
  public
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  // no rectangle drawn at form creation
  FOldPt := Point(-1, -1);
end;

procedure TForm1.ApplicationEvents1Message(var Msg: tagMSG;
  var Handled: Boolean);
var
  R: TRect;
  Pt: TPoint;
begin
  if Msg.message = WM_MOUSEMOVE then begin

    // assume no drawing (will test later against the point).
    // also, below RedrawWindow will cause an immediate WM_PAINT, this will
    // provide a hint to the paint handler to not to draw anything yet.
    FMousePt := Point(-1, -1);


    // first, if there's already a previous rectangle, invalidate it to clear
    if (FOldPt.X > 0) and (FOldPt.Y > 0) then begin
      R := Rect(FOldPt.X - 10, FOldPt.Y - 10, FOldPt.X, FOldPt.Y);
      InvalidateRect(Handle, @R, True);

      // invalidate childs
      // the pointer could be on one window yet parts of the rectangle could be
      // on a child or/and a parent, better let Windows handle it all
      RedrawWindow(Handle, @R, 0,
                     RDW_INVALIDATE or RDW_UPDATENOW or RDW_ALLCHILDREN);
    end;


    // is the message window our form?
    if Msg.hwnd = Handle then
      // then save the bottom-right coordinates
      FMousePt := SmallPointToPoint(TSmallPoint(Msg.lParam))
    else begin
      // is the message window one of our child windows?
      if GetAncestor(Msg.hwnd, GA_ROOT) = Handle then begin
        // then convert to form's client coordinates
        Pt := SmallPointToPoint(TSmallPoint(Msg.lParam));
        windows.ClientToScreen(Msg.hwnd, Pt);
        FMousePt := ScreenToClient(Pt);
      end;
    end;

    // will we draw?  (test against the point)
    if PtInRect(ClientRect, FMousePt) then begin
      R := Rect(FMousePt.X - 10, FMousePt.Y - 10, FMousePt.X, FMousePt.Y);
      InvalidateRect(Handle, @R, False);
    end;
  end;
end;

procedure TForm1.WM_PAINT(var Msg: TWmPaint);
var
  DC: HDC;
  Rgn: HRGN;
begin
  inherited;

  if (FMousePt.X > 0) and (FMousePt.Y > 0) then begin
    // save where we draw, we'll need to erase before we draw an other one
    FOldPt := FMousePt;

    // get a dc that could draw on child windows
    DC := GetDCEx(Handle, 0, DCX_PARENTCLIP);

    // don't draw on borders & caption
    Rgn := CreateRectRgn(ClientRect.Left, ClientRect.Top,
                          ClientRect.Right, ClientRect.Bottom);
    SelectClipRgn(DC, Rgn);
    DeleteObject(Rgn);

    // draw a red rectangle
    SelectObject(DC, GetStockObject(DC_BRUSH));
    SetDCBrushColor(DC, ColorToRGB(clRed));
    FillRect(DC, Rect(FMousePt.X - 10, FMousePt.Y - 10, FMousePt.X, FMousePt.Y), 0);

    ReleaseDC(Handle, DC);
  end;
end;

【讨论】:

  • 请注意,GetDCEx 检索到的 DC 可以分配给 Form 的Canvas.Handle,如果使用画布绘制更舒服的话。但是剪辑需要 api 调用。
  • 这正是我想要的。感谢您提供详细的示例,并花时间了解我的问题。
  • @iamjoosy - 不客气! :) 别担心,这是一个有趣的问题。
  • 嗨!不错的代码......你能告诉我如何在桌面窗口上绘制并在用户点击时绘制圆形动画吗???
【解决方案2】:

应用程序主窗口不能在其他控制表面上绘制。控件定期绘制和擦除自己(基于控件“绘制周期”)

您的应用程序只能使用允许应用程序执行此操作的控件。许多通用控件通过控件自定义绘制技术为应用程序自定义控件外观提供了灵活性。

【讨论】:

    【解决方案3】:

    你不能。

    控件被绘制在它们的父窗口之上。无论您在父窗口上绘制什么,都将在该窗口上的控件后面看到。目前尚不清楚为什么需要进行这样的绘图;但是,也许您可​​以在表单内创建一个透明控件并将其设置为前面,然后在其画布上绘制。这样一来,您的绘图将位于表单及其其他控件之上,但这样用户就无法与表单上的其他控件进行交互,因为它们位于透明控件的后面。

    【讨论】:

      【解决方案4】:

      你不能这样做。您需要创建一个窗口控件(例如窗口)并将此窗口放置在您要“在”上绘制的控件之上。然后你可以

      1. 复制带有控件的表单的位图,并将该位图作为这个新控件的背景图,或者

      2. 使这个新窗口具有不规则形状,使其在某些不规则形状区域之外是透明的。

      【讨论】:

      • 是的,这就是我过去所做的。我只是认为可能有一个更简单的解决方案。我的想法的缺陷是我忽略了这样一个事实,即父窗口是在绘制子控件之前绘制的,因此,父窗口的画布被绘制了。
      【解决方案5】:

      我做了一些涉及在我的表单上的组件周围绘制句柄的事情。

      首先创建这样的消息:

      Const
      PM_AfterPaint = WM_App + 1;
      

      编写一个Procedure来处理消息:

      Procedure AfterPaint(var msg: tmsg); Message PM_AfterPaint;
      
      Procedure AfterPaint(var msg: tmsg);
      begin
        {place the drawing code here}
        ValidateRect(Handle, ClientRect);
      end;
      

      Validaterect 将告诉 Windows 不需要重新绘制表单。您的绘画将导致表格的一部分“无效”。 ValidateRect 对 windows 说一切都是“验证”。

      最后一步,您还需要覆盖绘制过程。

      Procedure Paint; Override;
      
      Procedure TForm1.paint;
      Begin
        Inherited;
        PostMessage(Handle, PM_AfterPaint, 0, 0);
      End; 
      

      所以每次你的表单需要重绘(WM_Paint)时,它都会调用祖先的paint并在消息队列中添加一个AfterPaint消息。当消息被处理时,调用 AfterPaint 并绘制你的东西并告诉 Windows 一切都很好,防止再次调用绘制。

      希望对您有所帮助。

      【讨论】:

        猜你喜欢
        • 2013-12-09
        • 1970-01-01
        • 1970-01-01
        • 2014-07-24
        • 1970-01-01
        • 2017-07-25
        • 2010-11-13
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多