自定义带有非客户区的控件 - 第一次不计算

6
我正在编写一个自定义控件,它只是一个带有非客户区的容器。在那个非客户区内,有一个小区域是按钮,其余部分是透明的。绘图不是一个精确的矩形。
到目前为止,我已经完成了一半。问题是,除非我进行微小的调整,例如重新调整大小,否则它不会预先计算非客户区域。
我遵循了许多描述如何完成此操作的资源。我的处理WM_NCCALCSIZE的实现与我找到的“工作”的示例或多或少相同。但是当控件首次创建时,它根本不计算这个。当我在我的消息处理程序(WMNCCalcSize)中放置断点时,根据我找到的示例,我应该首先检查Msg.CalcValidRects,仅在其为True时执行计算。但是在运行时调试时,它是False,因此未执行计算。
在设计时,如果我重新调整控件大小,那么它才会正确计算。虽然还不完美(这段代码还在修改中),但似乎直到我微调它之后才设置非客户区域。此外,在运行时,如果我在代码中微调大小,它仍然不会计算。

Before and After Resize

顶部的图像是表单最初创建/显示时的情况。第二个图像是我稍微调整大小后的情况。请注意测试按钮,它对齐方式为alLeft。因此,最初它占用了应该是非客户端区域的区域。
如果我注释掉检查if Msg.CalcValidRects then begin,那么它会正确计算。但我看到每个示例都进行了这个检查,我很确定它是必需的。
我做错了什么,如何使其始终计算非客户端区域?
unit FloatBar;

interface

uses
  System.Classes, System.SysUtils, System.Types,
  Vcl.Controls, Vcl.Graphics, Vcl.Forms,
  Winapi.Windows, Winapi.Messages;

type
  TFloatBar = class(TCustomControl)
  private
    FCollapsed: Boolean;
    FBtnHeight: Integer;
    FBtnWidth: Integer;
    procedure RepaintBorder;
    procedure PaintBorder;
    procedure SetCollapsed(const Value: Boolean);
    function BtnRect: TRect;
    procedure SetBtnHeight(const Value: Integer);
    procedure SetBtnWidth(const Value: Integer);
    function TransRect: TRect;
  protected
    procedure CreateParams(var Params: TCreateParams); override;
    procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
    procedure WMNCPaint(var Message: TWMNCPaint); message WM_NCPAINT;
    procedure WMNCHitTest(var Message: TWMNCHitTest); message WM_NCHITTEST;
    procedure WMNCCalcSize(var Msg: TWMNCCalcSize); message WM_NCCALCSIZE;
    procedure Paint; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Repaint; override;
    procedure Invalidate; override;
  published
    property BtnWidth: Integer read FBtnWidth write SetBtnWidth;
    property BtnHeight: Integer read FBtnHeight write SetBtnHeight;
    property Collapsed: Boolean read FCollapsed write SetCollapsed;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Float Bar', [TFloatBar]);
end;

{ TFloatBar }

constructor TFloatBar.Create(AOwner: TComponent);
begin
  inherited;
  ControlStyle:= [csAcceptsControls,
    csCaptureMouse,
    csDesignInteractive,
    csClickEvents,
    csReplicatable,
    csNoStdEvents
    ];
  Width:= 400;
  Height:= 60;
  FBtnWidth:= 50;
  FBtnHeight:= 20;
  FCollapsed:= False;
end;

procedure TFloatBar.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  with Params.WindowClass do
    Style := Style and not (CS_HREDRAW or CS_VREDRAW);
end;

destructor TFloatBar.Destroy;
begin

  inherited;
end;

procedure TFloatBar.Invalidate;
begin
  inherited;
  RepaintBorder;
end;

procedure TFloatBar.Repaint;
begin
  inherited Repaint;
  RepaintBorder;
end;

procedure TFloatBar.RepaintBorder;
begin
  if Visible and HandleAllocated then
    Perform(WM_NCPAINT, 0, 0);
end;

procedure TFloatBar.SetBtnHeight(const Value: Integer);
begin
  FBtnHeight := Value;
  Invalidate;
end;

procedure TFloatBar.SetBtnWidth(const Value: Integer);
begin
  FBtnWidth := Value;
  Invalidate;
end;

procedure TFloatBar.SetCollapsed(const Value: Boolean);
begin
  FCollapsed := Value;
  Invalidate;
end;

procedure TFloatBar.WMNCPaint(var Message: TWMNCPaint);
begin
  inherited;
  PaintBorder;
end;

procedure TFloatBar.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
  Message.Result := 1;
end;

procedure TFloatBar.WMNCCalcSize(var Msg: TWMNCCalcSize);
var
  lpncsp: PNCCalcSizeParams;
begin
  if Msg.CalcValidRects then begin            //<------ HERE --------
    lpncsp := Msg.CalcSize_Params;
    if lpncsp = nil then Exit;
    lpncsp.rgrc[0].Bottom:= lpncsp.rgrc[0].Bottom-FBtnHeight;
    Msg.Result := 0;
  end;
  inherited;
end;

function TFloatBar.BtnRect: TRect;
begin
  //Return a rect where the non-client collapse button is to be...
  Result:= Rect(ClientWidth-FBtnWidth, ClientHeight, ClientWidth, ClientHeight+FBtnHeight);
end;

function TFloatBar.TransRect: TRect;
begin
  //Return a rect where the non-client transparent area is to be...
  Result:= Rect(0, ClientHeight, ClientWidth, ClientHeight+FBtnHeight);
end;

procedure TFloatBar.WMNCHitTest(var Message: TWMNCHitTest);
var
  P: TPoint;
  C: TCursor;
begin
  C:= crDefault; //TODO: Find a way to change cursor elsewhere...
  P:= Point(Message.XPos, Message.YPos);
  if PtInRect(BtnRect, P) then begin
    Message.Result:= HTCLIENT;
    C:= crHandPoint;
  end else
  if PtInRect(TransRect, P) then
    Message.Result:= HTTRANSPARENT
  else
    inherited;
  Screen.Cursor:= C;
end;

procedure TFloatBar.Paint;
begin
  inherited;

  //Paint Background
  Canvas.Brush.Style:= bsSolid;
  Canvas.Pen.Style:= psClear;
  Canvas.Brush.Color:= Color;
  Canvas.FillRect(Canvas.ClipRect);

  Canvas.Pen.Style:= psSolid;
  Canvas.Pen.Width:= 3;
  Canvas.Brush.Style:= bsClear;
  Canvas.Pen.Color:= clBlue;

  Canvas.MoveTo(0, 0);
  Canvas.LineTo(ClientWidth, 0); //Top
  Canvas.LineTo(ClientWidth, ClientHeight+FBtnHeight); //Right
  Canvas.LineTo(ClientWidth-FBtnWidth, ClientHeight+FBtnHeight); //Bottom of Button
  Canvas.LineTo(ClientWidth-FBtnWidth, ClientHeight); //Left of Button
  Canvas.LineTo(0, ClientHeight); //Bottom
  Canvas.LineTo(0, 0);

end;

procedure TFloatBar.PaintBorder;
begin
  Canvas.Handle:= GetWindowDC(Handle);
  try

    //TODO: Paint "transparent" area by painting parent...


    //Paint NC button background
    Canvas.Brush.Style:= bsSolid;
    Canvas.Pen.Style:= psClear;
    Canvas.Brush.Color:= Color;
    Canvas.Rectangle(ClientWidth-FBtnWidth, ClientHeight, ClientWidth, ClientHeight+FBtnHeight);

    //Paint NC button border
    Canvas.Pen.Style:= psSolid;
    Canvas.Pen.Width:= 3;
    Canvas.Brush.Style:= bsClear;
    Canvas.Pen.Color:= clBlue;
    Canvas.MoveTo(ClientWidth, ClientHeight);
    Canvas.LineTo(ClientWidth, ClientHeight+FBtnHeight);
    Canvas.LineTo(ClientWidth-FBtnWidth, ClientHeight+FBtnHeight);
    Canvas.LineTo(ClientWidth-FBtnWidth, ClientHeight);

    //Paint NC Button Chevron      //TODO: Calculate chevron size/position
    if FCollapsed then begin
      Canvas.MoveTo(ClientWidth-30, ClientHeight+7);
      Canvas.LineTo(ClientWidth-25, ClientHeight+13);
      Canvas.LineTo(ClientWidth-20, ClientHeight+7);
    end else begin
      Canvas.MoveTo(ClientWidth-30, ClientHeight+13);
      Canvas.LineTo(ClientWidth-25, ClientHeight+7);
      Canvas.LineTo(ClientWidth-20, ClientHeight+13);
    end;
  finally
    ReleaseDC(Handle, Canvas.Handle);
  end;
end;

end.

请将您停止编写代码的位置之前的所有代码发布到您的Github存储库中以供参考。 - user10560917
@Davison,这其实和上面的代码没什么区别,而且它还有严重的问题。为了让它能够正常工作,我需要花上一两天的时间,而我现在几乎没有时间,特别是在假期期间。我甚至还没有本地代码库的副本 - 只有一个空的仓库。 - Jerry Dodge
@Davison 好的,我发布了我的代码。但是我要说,它目前存在严重问题,远不能正常工作。 - Jerry Dodge
好的,非常感谢。如果有一天您完成了这个项目,请在这里告诉我。:-) - user10560917
在你的项目中,你说:“模仿TeamViewer”,那么我认为这个项目必须尽可能接近(Team View)。我建议在你的FloatBar里只有选项卡,程序员可以插入任何其他需要的组件。 - user10560917
显示剩余6条评论
1个回答

11
我需要先检查Msg.CalcValidRects,只有当它为True时才进行计算,你的理解有误。该消息有一种有点复杂的机制,并且文档可能会稍微令人困惑地说明消息操作的两个不同模式(wParam 为 true 或 false)。与你的情况相关的部分是 lParam 的第二段:

如果 wParam 是FALSE,那么 lParam 指向一个 RECT 结构体。在进入时,结构体包含窗口的建议矩形。退出时,结构应包含相应窗口客户区域的屏幕坐标。

你会发现,在 VCL 中有许多此简单形式的用法,根本不需要检查 wParam,例如:TToolWindow.WMNCCalcSizeTCustomCategoryPanel.WMNCCalcSize 等等。
(请注意,当 wParam 为 false 时,NCCALCSIZE_PARAMS.rgrc 甚至不是一个矩形数组,但因为您正在操作所谓的“第一个”矩形,所以没关系。)

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