在Delphi窗体中覆盖控件

4

我可以在表单画布上绘制一些东西并覆盖表单上的控件吗?

我尝试了以下方法:

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文档,这是预期的行为)。

我的问题是如何在控件上方绘制图形?


1
如果您在 TMemo 上放置一个 TButton,您是否期望备忘录能够在按钮表面上绘制?如果您将 Microsoft WordPad 窗口放在 Microsoft Paint 窗口上方,您是否期望 Paint 窗口能够在 WordPad 窗口内部绘制? - Andreas Rejbrand
@Marjan:它们是具有不规则形状的窗口。就像Microsoft WordPad可以在Microsoft Paint之上,一个不规则形状的窗口(例如一只狗)也可以在Microsoft Paint之上。 - Andreas Rejbrand
1
其实,Andreas Rejbrand,这并不是真的。在桌面窗口上绘图很容易,但擦除刚刚绘制的东西却是一个问题。 - Free Consulting
@Andreas Rejbrand,这是打破特定窗口剪辑区域和Z顺序的方法。顺便说一句,上面未经格式化的代码在绘制后立即失效 :-) - Free Consulting
2
@Andreas - 那为什么不告诉他使用另一个DC,而不是说这是不可能的呢? - Sertac Akyuz
显示剩余2条评论
5个回答

10
不要在绘图处理程序中进行“无效”操作。 Invalidating 会导致发送一个 WM_PAINT,这当然会重新开始绘图处理。即使您不移动鼠标,您发布的代码示例也会一遍又一遍地运行 'OnPaint' 事件。由于您的绘图依赖于光标位置,因此您将使用 'OnMouseMove' 事件完成此操作。但是,您还需要拦截其他窗口控件的鼠标消息。以下示例使用 'ApplicationEvents' 组件,出于这个原因。如果您的应用程序将有多个窗体,则需要设计一种机制来区分您正在绘制的窗体。

另请参见文档,VCL 的 Invalidate 使整个窗口失效。您不需要这样做,您只是绘制一个小矩形,而且您知道您要绘制什么和已经绘制了哪些部分。只需使您要绘制和已绘制的位置无效。

至于在控件上绘图,实际上画图部分很容易,但是您不能使用提供的画布来完成。表单具有 WS_CLIPCHILDREN 样式,子窗口的表面将被从更新区域中排除,因此您必须使用 GetDCExGetWindowDC。正如评论中所提到的 '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 可以分配给窗体的 Canvas.Handle,如果您更喜欢使用画布进行绘制。但是,剪辑需要调用 API。 - Sertac Akyuz
这正是我正在寻找的。感谢您提供详细的示例,并花时间理解我的问题。 - iamjoosy
@iamjoosy - 不用谢!:) 不要担心时间,这是一个有趣的问题。 - Sertac Akyuz
你好!代码写得不错... 你能给我展示一下如何在桌面窗口上绘制圆形动画并在用户点击时进行吗? - Beto Neto

1

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

您的应用程序只能在允许应用程序这样做的控件上进行绘制。许多常见控件为应用程序提供了灵活性,以通过控件自定义绘制技术来自定义控件外观。


1

你不能这样做。

控件是绘制在它们的父窗口之上的。无论你在父窗口上绘制什么,都会被控件遮盖。不清楚为什么你需要进行这样的绘制;但是,也许你可以在表单内创建一个透明的控件并将其置于前端,然后在它的画布上绘制。这样你的绘图看起来就在表单和其他控件的顶部,但用户不能与表单上的其他控件交互,因为它们在透明控件的后面。


1

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

  1. 复制带有控件的窗体的位图,并将此位图用作此新控件的背景图像,或者

  2. 使此新窗口具有不规则形状,以便在某些不规则形状的区域外是透明的。


没错,这就是我过去所做的。我只是认为可能会有更简单的解决方案。我思考的缺陷在于我忽略了这样一个事实:父窗口在子控件绘制之前已经被绘制,因此父窗口的画布也被遮盖了。 - iamjoosy

-1

我做了一些涉及在我的表单上绘制组件周围手柄的事情,这是我所做的。

首先创建一个像这样的消息:

Const
PM_AfterPaint = WM_App + 1;

编写一个处理消息的过程:
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 不需要重绘你的窗体。你的绘画将导致窗体的一部分被 "invalidate"。ValidateRect 告诉 Windows 一切都是 "validate"。

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

Procedure Paint; Override;

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

每次需要重绘表单(WM_Paint)时,它都会调用祖先的绘制方法并将一个AfterPaint消息添加到消息队列中。当该消息被处理时,AfterPaint方法会被调用并绘制您的内容,并告诉Windows一切正常,从而防止再次调用绘制方法。
希望这可以帮助您。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接