Custom control with non-client area – doesn't calculate at first

  • A+

I'm writing a custom control which is simply a container with a non-client area. Within that non-client area, there's one small area which is a button, and the rest of it is transparent. The drawing isn't an exact rectangle.

So far, I have it half-way working. The problem is that it doesn't calculate the non-client area up front, unless I make a minor tweak, such as re-sizing it.

I've followed many resources describing how to accomplish this. My implementation of handling WM_NCCALCSIZE is more or less identical to "working" examples I've found. But when the control is first created, it does not calculate this at all. When I drop a breakpoint inside the message handler of mine (WMNCCalcSize), based on the examples I've found, I'm supposed to first check Msg.CalcValidRects, and only do my calculation if it's True. But when debugging run-time, it's False, thus the calculation isn't done.

In design-time, if I re-size the control, THEN it decides to calculate properly. It's still not perfect (this code is still in the works), but it doesn't seem to set the non-client area until after I tweak it. Further, in run-time, if I tweak the size in the code, it still doesn't calculate.

Custom control with non-client area - doesn't calculate at first

The image on the top is when the form is initially created/shown. The second one is after I re-size it a little bit. Notice the test button, which is aligned alLeft. So initially, it consumes the area which is supposed to be non-client.

If I comment out the check if Msg.CalcValidRects then begin, then it calculates properly. But I see every example doing this check, and I'm pretty sure it's needed.

What am I doing wrong and how to make it calculate the non-client area at all times?

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. 

... I'm supposed to first check Msg.CalcValidRects, and only do my calculation if it's True.

You've got that wrong. The message has a somewhat complicated mechanism and the documentation might be slightly confusing trying to explain two distinct mode the message operates (wParam true or false). The part that relates to your case is the second paragraph of lParam:

If wParam is FALSE, lParam points to a RECT structure. On entry, the structure contains the proposed window rectangle for the window. On exit, the structure should contain the screen coordinates of the corresponding window client area.

You'll find numerous usage examples of this simple form in the VCL where wParam is not checked at all, like in TToolWindow.WMNCCalcSize, TCustomCategoryPanel.WMNCCalcSize etc..

(Note that NCCALCSIZE_PARAMS.rgrc is not even a rectangle array when wParam is false, but since you're operating on the supposed first rectangle, you're fine.)


:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: