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;
Related
I'm creating simple component inherited from TControl (Firemonkey).
In constructor I wrote :
constructor TControl1.Create(AOwner: TComponent);
begin
inherited;
fTest := TLayout.Create(Self);
fTest.Parent := Self;
end;
How when I place this component to the form, Structure list shows Tlayout as subcomponent as <components1>. How can I hide it?
See screenshot.
Use
SetSubComponent(True);
Owner must be Self
constructor TControl1.Create(AOwner: TComponent);
begin
inherited;
fTest := TLayout.Create(Self);
fTest.SetSubComponent(True);
fTest.Parent := Self;
end;
Here is also similar question:
How to disable child controls at design-time?
I've been trying to boil down to an MCVE some code the author of another q sent me
to illustrate a problem with a custom component.
The component is simply a TPanel descendant which includes an embedded TDBGrid.
My version of its source, and a test project are below.
The problem is that if the embedded DBGrid has been created with persistent columns,
when its test project is re-opened in the IDE, an exception is raised
Error reading TColumn.Grid.Expanded. Property Griddoes not exist.
Executing the Stream method of the test project shows how this problem arises:
For comparison purposes, I also have a normal TDBGrid, DBGrid1, on my form. Whereas the Columns of this DBGrid1 are streamed as
Columns = <
item
Expanded = False
FieldName = 'ID'
Visible = True
end
[...]
the embedded grid's columns are streamed like this
Grid.Columns = <
item
Grid.Expanded = False
Grid.FieldName = 'ID'
Grid.Visible = True
end
[...]
It's obviously the Grid prefix of Grid.Expanded and the other column properties which is causing the problem.
I imagine that the problem is something to do with the fact that DBGridColumns
is a TCollection descendant and that the embedded grid isn't the top-level object in
the DFM.
My question is: How should the code of TMyPanel be modified so that the grid's
columns get correctly streamed?
Component source:
unit MAGridu;
interface
uses
Windows, SysUtils, Classes, Controls, ExtCtrls, DBGrids;
type
TMyPanel = class(TPanel)
private
FGrid : TDBGrid;
public
constructor Create(AOwner : TComponent); override;
published
property Grid : TDBGrid read FGrid;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Standard', [TMyPanel]);
end;
constructor TMyPanel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FGrid := TDBGrid.Create(Self);
FGrid.SetSubcomponent(True);
FGrid.Parent := Self;
end;
end.
Test project source:
type
TForm1 = class(TForm)
DBGrid1: TDBGrid;
CDS1: TClientDataSet;
DataSource1: TDataSource;
MyPanel1: TMyPanel;
Memo1: TMemo;
Button1: TButton;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
procedure Stream;
public
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
Stream;
end;
procedure TForm1.Stream;
// This method is included as an easy way of getting at the contents of the project's
// DFM. It saves the form to a stream, and loads it into a memo on the form.
var
SS : TStringStream;
MS : TMemoryStream;
Writer : TWriter;
begin
SS := TStringStream.Create('');
MS := TMemoryStream.Create;
Writer := TWriter.Create(MS, 4096);
try
Writer.Root := Self;
Writer.WriteSignature;
Writer.WriteComponent(Self);
Writer.FlushBuffer;
MS.Position := 0;
ObjectBinaryToText(MS, SS);
Memo1.Lines.Text := SS.DataString;
finally
Writer.Free;
MS.Free;
SS.Free;
end;
end;
end.
procedure TForm1.FormCreate(Sender: TObject);
var
Field : TField;
begin
Field := TIntegerField.Create(Self);
Field.FieldName := 'ID';
Field.FieldKind := fkData;
Field.DataSet := CDS1;
Field := TStringField.Create(Self);
Field.FieldName := 'Name';
Field.Size := 20;
Field.FieldKind := fkData;
Field.DataSet := CDS1;
CDS1.CreateDataSet;
CDS1.InsertRecord([1, 'One']);
end;
end.
Seems there is not much you can do about it. When you look into procedure WriteCollectionProp (local to TWriter.WriteProperties) you see that FPropPath is cleared before the call to WriteCollection.
The problem with TDBGrid, or better TCustomDBGrid, is that the collection is marked as stored false and the streaming is delegated to DefineProperties, which uses TCustomDBGrid.WriteColumns to do the work.
Inspecting that method reveals that, although it also calls WriteCollection, the content of FPropPath is not cleared before. This is somewhat expected as FPropPath is a private field.
The reason why it nonetheless works in the standard use case is that at the moment of writing FPropPath is just empty.
As even Delphi 10.1 Berlin behaves the same as Delphi 7, I suggest filing a QP report together with just this example.
The solution would involve the embedded grid not having the form that owns the panel as the streaming root, but the panel itself. This will prevent the grid's properties being qualified by 'Grid', which, in effect, will eliminate column properties being wrongly qualified by the same. That is to say, the below is a workaround for faulty behavior.
To achieve the above, remove the SetSubComponent call,
constructor TMyPanel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FGrid := TDBGrid.Create(Self);
// FGrid.SetSubcomponent(True);
FGrid.Parent := Self;
end;
The csSubComponent style being removed, now the grid is not streamed at all.
Then override GetChildren for the panel to stream the grid through the panel. GetChildren, as documented, is used to determine which child controls are saved (streamed) of a control. Since we have only one control (the grid) we don't need to make a distinction and instead can call the inherited handler modifying the root.
type
TMyPanel = class(TPanel)
private
FGrid : TDBGrid;
public
constructor Create(AOwner : TComponent); override;
procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override;
published
property Grid : TDBGrid read FGrid;
end;
...
procedure TMyPanel.GetChildren(Proc: TGetChildProc; Root: TComponent);
begin
inherited GetChildren(Proc, Self);
end;
Then remains resolving subcomponent complications. Complication here was a second grid being created sitting in front of the panel which assumes streamed properties. Very much like in this unanswered question. Note that this problem is not related to the solution provided above. The original code displays the same problem.
Having read the question mentioned above, and this one, and this one, and this one, and still not being able to resolve with the help of the code, clues, advices in them, I traced the streaming system and came up with my solution as below.
I'm not claiming it is how it is supposed to be. It is just how I could make this to work. Main modifications are, the sub-grid is now writable (which would require a setter in production code), the conditional creation of the grid, and the overriden GetChildOwner of the panel. Below is the entire unit having TMyPanel2 (TMyPanel couldn't make it... ).
unit TestPanel2;
interface
uses
Windows, SysUtils, Classes, Controls, ExtCtrls, DBGrids;
type
TMyPanel2 = class(TPanel)
private
FGrid : TDBGrid;
protected
function GetChildOwner: TComponent; override;
public
constructor Create(AOwner : TComponent); override;
destructor Destroy; override;
procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override;
published
property Grid : TDBGrid read FGrid write FGrid;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Test', [TMyPanel2]);
end;
constructor TMyPanel2.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
if not (csReading in AOwner.ComponentState) then begin
FGrid := TDBGrid.Create(Self);
FGrid.Name := 'InternalDBGrid';
FGrid.Parent := Self;
end else
RegisterClass(TDBGrid);
end;
destructor TMyPanel2.Destroy;
begin
FGrid.Free;
inherited;
end;
function TMyPanel2.GetChildOwner: TComponent;
begin
Result := Self;
end;
procedure TMyPanel2.GetChildren(Proc: TGetChildProc; Root: TComponent);
begin
Proc(Grid);
end;
end.
I want to have a custom StringGrid element.
I created a class:
type
TClassStringGrid = class(TCustomControl)
...
with
constructor TClassStringGrid.Create(AOwner: TForm);
begin
inherited Create(nil);
myGroupBox1 := TGroupBox.Create(AOwner);
myGroupBox1.Parent := AOwner;
myStringGrid1 := TStringGrid.Create(self);
myStringGrid1.Parent := myGroupBox1;
myStringGrid1.Options := myStringGrid1.Options + [goEditing];
end;
destructor TClassStringGrid.Destroy;
begin
if myStringGrid1 <> nil then begin
FreeAndNil(myStringGrid1);
end;
if myGroupBox1 <> nil then begin
DestroyComponents;
FreeAndNil(myGroupBox1);
end;
// Call the parent class destructor
inherited;
end;
I created a class in Form1 and show it. It works. But if I put some value into the StringGrid (Form1) and then try to close Form1 I get an exception "the element has no parent window" in FreeAndNil(myStringGrid1);.
What is wrong by Destroy?
I would be thankfull for any information you can provide me.
Assuming you want to show a String grid in a Group box on this control, then this is how it should look like:
type
TMyStringGrid = class(TCustomControl)
private
FGroupBox: TGroupBox;
FStringGrid: TStringGrid;
public
constructor Create(AOwner: TComponent); override;
end;
constructor TMyStringGrid.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FGroupBox := TGroupBox.Create(Self);
FGroupBox.Parent := Self;
FStringGrid := TStringGrid.Create(Self);
FStringGrid.Parent := FGroupBox;
end;
In this manner, your newly designed control is owner and parent of the sub controls. Destruction is done automatically because of that.
I have a graphical TCustomControl descendant component with a TScrollBar on it. The problem is that when I press the arrow key to move the cursor the whole canvas is painted in background color, including the region of the scroll bar, then the scroll bar is repainted and that makes scroll bar flicker. How can I solve this ?
Here is the code. There is no need install the component or to put something on the main form, just copy the code and assign TForm1.FormCreate event:
Unit1.pas
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, SuperList;
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
List: TSuperList;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
List:=TSuperList.Create(self);
List.Top:=50; List.Left:=50;
List.Visible:=true;
List.Parent:=Form1;
end;
end.
SuperList.pas
unit SuperList;
interface
uses Windows, Controls, Graphics, Classes, Messages, SysUtils, StdCtrls, Forms;
type
TSuperList = class(TCustomControl)
public
DX,DY: integer;
ScrollBar: TScrollBar;
procedure Paint; override;
constructor Create(AOwner: TComponent); override;
procedure WMKeyDown(var Message: TWMKeyDown); message WM_KEYDOWN;
procedure WMGetDlgCode(var Message: TWMGetDlgCode); message WM_GETDLGCODE;
procedure WMLButtonDown(var Message: TWMLButtonDown); message WM_LBUTTONDOWN;
published
property OnMouseMove;
property OnKeyPress;
property OnKeyDown;
property Color default clWindow;
property TabStop default true;
property Align;
property DoubleBuffered default true;
property BevelEdges;
property BevelInner;
property BevelKind default bkFlat;
property BevelOuter;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Marus', [TSuperList]);
end;
procedure TSuperList.WMGetDlgCode(var Message: TWMGetDlgCode);
begin
inherited;
Message.Result:= Message.Result or DLGC_WANTARROWS;
end;
procedure TSuperList.WMKeyDown(var Message: TWMKeyDown);
begin
if Message.CharCode=VK_LEFT then begin dec(DX,3); Invalidate; exit; end;
if Message.CharCode=VK_RIGHT then begin inc(DX,3); Invalidate; exit; end;
if Message.CharCode=VK_UP then begin dec(DY,3); Invalidate; exit; end;
if Message.CharCode=VK_DOWN then begin inc(DY,3); Invalidate; exit; end;
inherited;
end;
procedure TSuperList.WMLButtonDown(var Message: TWMLButtonDown);
begin
DX:=Message.XPos;
DY:=Message.YPos;
SetFocus;
Invalidate;
inherited;
end;
constructor TSuperList.Create(AOwner: TComponent);
begin
inherited;
DoubleBuffered:=true;
TabStop:=true;
Color:=clNone; Color:=clWindow;
BevelKind:=bkFlat;
Width:=200;
Height:=100;
DX:=5; DY:=50;
ScrollBar:=TScrollBar.Create(self);
ScrollBar.Kind:=sbVertical;
ScrollBar.TabStop:=false;
ScrollBar.Align:=alRight;
ScrollBar.Visible:=true;
ScrollBar.Parent:=self;
end;
procedure TSuperList.Paint;
begin
Canvas.Brush.Color:=Color;
Canvas.FillRect(Canvas.ClipRect);
Canvas.TextOut(10,10,'Press arrow keys !');
Canvas.Brush.Color:=clRed;
Canvas.Pen.Color:=clBlue;
Canvas.Rectangle(DX,DY,DX+30,DY+20);
end;
end.
I think the first thing that I would do is remove that scroll bar control. Windows come with ready made scroll bars. You just need to enable them.
So, start by removing ScrollBar from the component. Then add a CreateParams override:
procedure CreateParams(var Params: TCreateParams); override;
Implement it like this:
procedure TSuperList.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.Style := Params.Style or WS_VSCROLL;
end;
Yippee, your control now has a scroll bar.
Next you need to add a handler for WM_VSCROLL:
procedure WMVScroll(var Message: TWMVScroll); message WM_VSCROLL;
And that's implemented like this:
procedure TSuperList.WMVScroll(var Message: TWMVScroll);
begin
case Message.ScrollCode of
SB_LINEUP:
begin
dec(DY, 3);
Invalidate;
end;
SB_LINEDOWN:
begin
inc(DY, 3);
Invalidate;
end;
...
end;
end;
You'll need to fill out the rest of the scroll codes.
I would also suggest that you do not set DoubleBuffered in the constructor of your component. Let the user set that if they wish. There's no reason for your control to require double buffering.
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.