I created a test component
unit Control1;
interface
uses
System.SysUtils, System.Classes, FMX.Types, FMX.Controls;
type
TTestComp = class(TControl)
private
i: integer;
protected
procedure Paint; override;
public
constructor Create(AOwner: TComponent); override;
published
property Width;
property Height;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Test', [TTestComp]);
end;
{ TTestComp }
constructor TTestComp.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
i := 0;
end;
procedure TTestComp.Paint;
begin
inherited;
inc(i);
canvas.BeginScene;
canvas.Fill.Color := $FF000000;
canvas.FillRect(localrect, 0, 0, [], 1);
canvas.Fill.Color := $FFFFFFFF;
canvas.FillText(localrect, IntToStr(i), false, 1, [], TTextAlign.Center);
canvas.EndScene;
end;
end.
Here is the problem:
Component is drawing in top-left corner
Too much paint method calls when resizing form.
Just resized form.
I have a lot of components, built according to this principle. And when I change the size of the form, they begin to lag (Low FPS).
Standard components (TButton and etc.) work fine
1.Component is drawing in top-left corner
Your component is drawing exactly where it is placed (Position property). If you don't assign any values to Position.X and Position.Y the default values 0 is used for both.
2.Too much paint method calls when resizing form.
When you resize the form all components are re-painted, also f.ex. buttons. In a test with 81 of your controls I did not realize any lagging (but I assume your actual controls do some more painting than this example control).
Related
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.
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.)
In my component I need to adjust some variables every time the Width or Height is changed but before the component is painted. I try to override the Resize method and update the variables there but it does not always work. See the code below. If I create the component at run time, everythin it's ok. But if I drop the component on the Form at design time, change its size and run the program, my component is painted at default size because the new size is not updated as it should in Resize method. This is also happens when I save the project, close it and reopen it.
unit OwnGauge;
interface
uses
Windows, SysUtils, Classes, Graphics, OwnGraphics, Controls, StdCtrls;
type
TOwnGauge = class(TGraphicControl)
private
PaintBmp: TBitmap;
protected
procedure Paint; override;
procedure Resize; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('OwnMisc', [TOwnGauge]);
end;
constructor TOwnGauge.Create(AOwner: TComponent);
begin
PaintBmp:= nil;
inherited Create(AOwner);
PaintBmp:= TBitmap.Create;
PaintBmp.PixelFormat:= pf24bit;
Width:= 200;
Height:= 24;
end;
destructor TOwnGauge.Destroy;
begin
inherited Destroy;
PaintBmp.Free;
end;
procedure TOwnGauge.Paint;
begin
with PaintBmp do begin
Canvas.Brush.Color:= clRed;
Canvas.Brush.Style:= bsSolid;
Canvas.FillRect(ClientRect);
end;
BitBlt(Canvas.Handle, 0, 0, Width, Height, PaintBmp.Canvas.Handle, 0, 0, SRCCOPY);
end;
procedure TOwnGauge.Resize;
begin
PaintBmp.SetSize(Width,Height);
inherited;
end;
end.
Edit:
I've done further research and I found that in TWinControl.WMSize handler of WM_SIZE message is the following code:
if not (csLoading in ComponentState) then Resize;
So now it's clear that Resize is not triggered when the values from the designer are loaded.
I found the solution !
Instead overriding Resize I must override SetBounds, because Resize is called from SetBounds, but not when the properties of the component are loaded.
procedure TOwnGauge.SetBounds(ALeft, ATop, AWidth, AHeight: Integer);
begin
inherited;
PaintBmp.SetSize(Width,Height);
end;
I have a composite component that consists of a TEdit and a TButton (yes, I know about TButtonedEdit) that inherits from TCustomControl. The edit and button are created in its constructor and placed on itself.
At designtime the selection box is not drawn properly - my guess is that the edit and button are hiding it because its been drawn for the custom control and then overdrawn by them.
Here the comparison:
I have also seen this for other 3rd party components (like the TcxGrid also only draws the outer part of the selection indicator)
Question: how can I change that?
Most simple case for reproducing:
unit SearchEdit;
interface
uses
Classes, Controls, StdCtrls;
type
TSearchEdit = class(TCustomControl)
private
fEdit: TEdit;
public
constructor Create(AOwner: TComponent); override;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Custom', [TSearchEdit]);
end;
{ TSearchEdit }
constructor TSearchEdit.Create(AOwner: TComponent);
begin
inherited;
fEdit := TEdit.Create(Self);
fEdit.Parent := Self;
fEdit.Align := alClient;
end;
end.
As I said in the comments, the easiest thing I can think of is to paint the controls in the parent and "hide" them from the designer at design time. You can do this by calling SetDesignVisible(False) on each of the child controls. Then you use PaintTo to do the painting on the parent.
Using your example we get:
type
TSearchEdit = class(TCustomControl)
...
protected
procedure Paint; override;
...
end;
constructor TSearchEdit.Create(AOwner: TComponent);
begin
inherited;
fEdit := TEdit.Create(Self);
fEdit.Parent := Self;
fEdit.Align := alClient;
fEdit.SetDesignVisible(False);
end;
procedure TSearchEdit.Paint;
begin
Inherited;
if (csDesigning in ComponentState) then
fEdit.PaintTo(Self.Canvas, FEdit.Left, FEdit.Top);
end;
I am trying to create a set of custom components like TEdit, TDBEdit, TComboBox with a new kind of border (rounded corner) and I have created this code:
unit RoundRectControls;
interface
uses
SysUtils, Classes, Controls, StdCtrls, Windows, Messages, Forms;
type
TRoundRectEdit = class(TEdit)
private
{ Private declarations }
protected
{ Protected declarations }
public
constructor Create(AOwner: TComponent); override;
{ Public declarations }
published
property BorderStyle default bsNone;
property Ctl3D default False;
{ Published declarations }
end;
procedure Register;
procedure DrawRoundedRect(Control: TWinControl);
implementation
constructor TRoundRectEdit.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
DrawRoundedRect(Self);
end;
procedure Register;
begin
RegisterComponents('Eduardo', [TRoundRectEdit]);
end;
procedure DrawRoundedRect(Control: TWinControl);
var
r: TRect;
Rgn: HRGN;
begin
with Control do
begin
r := ClientRect;
rgn := CreateRoundRectRgn(r.Left, r.Top, r.Right, r.Bottom, 30, 30) ;
Perform(EM_GETRECT, 0, lParam(#r)) ;
InflateRect(r, - 4, - 4) ;
Perform(EM_SETRECTNP, 0, lParam(#r)) ;
SetWindowRgn(Handle, rgn, True) ;
Invalidate;
end;
end;
end.
But after I tried to put the component in the Form, this message came:
So, how to I fix that? I am new to construct components and I need a good tutorial on the web. Something tells me that I need to make that DrawRoundedRect outside the Constructor... But where?
Edit 1 - 2012-07-27 14:50
Sertac Akyuz's Answer was great and resolved the problem, but the result was kind of ugly. I don't know what I am doing wrong. The text of the EditBox is too close to the top-left. Does anyone know how do I fix it?
You are requesting 'ClientRect' but the edit control window has not been created yet (no window, no rectangle). You can move your region modifying code to some place after it is created. Example:
type
TRoundRectEdit = class(TEdit)
private
{ Private declarations }
protected
procedure CreateWnd; override;
{ Protected declarations }
public
constructor Create(AOwner: TComponent); override;
...
constructor TRoundRectEdit.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
// DrawRoundedRect(Self);
end;
procedure TRoundRectEdit.CreateWnd;
begin
inherited;
DrawRoundedRect(Self);
end;
The error message itself reflects the effort of the VCL to create the window once its handle has been requested. It can't do so because it cannot resolve in what window the control is to be placed.
Creating a new region in SetBounds() should be fine. Just be sure to call inherited first, and then use the updated Width/Height to create the new region. CreateWnd() should still create the initial region using the current Width/Height. SetBounds() should recreate the region only if HandleAllocated() is True.
I am willing to designed one Application in Delphi XE2 Borderlessly and Captionlessly by using the following code :
BorderIcons = []
BorderStyle = bsNone
But the problem is that there is no Menu on Right Click on the Application on Taskbar just like in the above image. Then I have tried the following codes on FormShow event, but there is also another problem. One Border is created on Left side and Left-Botton side. The codes are :
procedure TForm1.FormShow(Sender: TObject);
var
r: TRect;
begin
r := ClientRect;
OffsetRect(r, 0, GetSystemMetrics(SM_CYCAPTION));
OffsetRect(r, GetSystemMetrics(SM_CXFRAME), GetSystemMetrics(SM_CYFRAME));
SetWindowRgn(Handle,
CreateRectRgn(
r.Left, r.Top,
ClientWidth + r.Left, ClientHeight + r.Top), True);
end;
Please help me.
The simple solution is not to remove the system menu in the first place. Note that the system menu is the official name for the menu that is missing in your app.
Make your .dfm file look like this:
BorderIcons = [biSystemMenu]
BorderStyle = bsNone
Get rid of that FormShow codeāit's not needed.
OK, it looks like a stray bit of code from my experimentation was confounding me. Here's what works.
Do exactly what you originally did in your .dfm form:
BorderIcons = []
BorderStyle = bsNone
Then add back the system menu using CreateParams:
TForm1 = class(TForm)
protected
procedure CreateParams(var Params: TCreateParams); override;
end;
...
procedure TForm1.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.Style := Params.Style or WS_SYSMENU;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
BorderStyle := bsNone;
SetWindowLong(Handle, GWL_STYLE,
WS_POPUP or WS_CLIPSIBLINGS or WS_CLIPCHILDREN or WS_SYSMENU);
SetWindowLong(Handle, GWL_EXSTYLE, WS_EX_CONTROLPARENT or WS_EX_APPWINDOW);
end;
You don't need the code in the OnShow handler with this solution.
The above code can be called any time (not just in OnCreate), it can be used to alter the behavior of a running form for instance (just include WS_VISIBLE to window styles if the form is already visible).
If you want the behavior to be in effect for the life time of the form, it's better to set the flags in an overriden CreateParams (where form styles are applied by VCL). This will also take possible recreation of the form into account. Don't set any form property from the OI for this solution, all of the flags are explicitly set in the code:
type
TForm1 = class(TForm)
..
protected
procedure CreateParams(var Params: TCreateParams); override;
..
procedure TForm1.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.Style := WS_POPUP or WS_CLIPSIBLINGS or WS_CLIPCHILDREN or WS_SYSMENU;
Params.ExStyle := WS_EX_CONTROLPARENT or WS_EX_APPWINDOW;
end;
You can have a window that appears not to have a caption bar, or a standard caption, by simply taking over the painting of the entire window:
Create a new empty application. Use this code for your form:
unit ncUnit1;
interface
// XE2 uses clause
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs;
// If you're not using XE2 take out the prefixes (WinApi, Vcl, System, etc)
type
TForm1 = class(TForm)
private
{ Private declarations }
public
{ Public declarations }
protected
procedure WMNCPaint(var Message: TWMNCPaint); message WM_NCPAINT;
procedure SolidColorNcPaint(solidColor,frameColor:TColor);
procedure Resizing(State: TWindowState); override;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
{ TForm1 }
procedure TForm1.WMNCPaint(var Message: TWMNCPaint);
begin
SolidColorNcPaint(clBtnFace,clBtnFace);
end;
procedure TForm1.Resizing(State: TWindowState);
begin
inherited;
PostMessage(Self.Handle,WM_NCPAINT,0,0); {force initial paint}
end;
procedure TForm1.SolidColorNcPaint(solidColor,frameColor:TColor);
var
aBorder:Integer;
ahdc : HDC;
begin
aBorder := GetSystemMetrics(SM_CYSIZEFRAME);
canvas.Lock;
ahdc := GetWindowDC(Handle);
canvas.Handle := ahdc;
ExcludeClipRect(canvas.Handle, aBorder, 0, Width-aBorder, Height - aBorder) ;
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := frameColor;
Canvas.Pen.Color := solidColor;
Canvas.Rectangle( 0,0, Width,Height);
ReleaseDC(Self.Handle, ahdc);
canvas.Handle := 0;
canvas.Unlock;
end;
end.
What you see above is only enough code to redraw a solid color over the non-client area of the window, not to remove it completely. Depending on the style of custom window you want, you should render whatever you want on the form. If you don't want a Close button then remove the close button, and if you do not want the resizing behaviour, remove the resizing behaviour. If you set the FormStyle=fsDialog plus the above code, you would get a window that has a complete custom drawn title area (which you can put whatever you want into). If you actually don't want the title area to exist at all, you can modify the above code to achieve that too.
You could do what David says and/or also take a look at:
SetWindowRgn API.
If you use just the SetWindowRgn you don't have to remove the TForm's border, just make a rectangle that starts below it.