Setting multiple labels to transparent across 1.000 forms? - delphi

I skinned my software with Devexpress and I found that the labels were non-transparent causing them to have grey background.
There's just endless forms, so I was wondering whether there was a way to do this task (of setting labels to transparent) automatically.
I did a similar thing earlier, the Devexpress controls on the form had LookAndFeel.NativeStyle = True, I used Grep Search to replace it to False on all dfm forms. In the label's case however, the transparent property is not present.
Thank you.

The global Screen variable keeps track of all forms:
procedure MakeLabelsTransparent(AParent: TWinControl);
var
I: Integer;
begin
with AParent do
for I := 0 to ControlCount - 1 do
if Controls[I] is TLabel then
TLabel(Controls[I]).Transparent := True
else if Controls[I] is TWinControl then
MakeLabelsTransparent(TWinControl(Controls[I]));
end;
procedure TMainForm.ActiveFormChange(Sender: TObject);
begin
with Screen do
if (ActiveCustomForm <> nil) and (ActiveCustomForm.Tag = 0) then
begin
MakeLabelsTransparent(ActiveCustomForm);
ActiveCustomForm.Tag := 1;
end;
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
Screen.OnActiveFormChange := ActiveFormChange;
end;
And if you have to use the Tag property for a particular form, then omit this check: it wouldn't really get that much slower.

For this type of task, GExperts contains the Set Component Properties tool:
This tool waits in the background
until you compile a project. It then
scans the current project's forms to
check for components with certain
properties and changes those
properties to a defined value. This
tool is useful to deactivate datasets
or database connections before you
compile your applications, but it can
be used for any similar situations as
well. To activate the scanning,
enable the checkbox next to this
expert in the GExperts Configuration
screen.
It can be used to set a property which is not yet in the DFM as well, and only requires one additional entry in the GExpert configuration, and a recompile.
I have just tested it and it works as expected.

At design time, you can just parse all .dfm then add the
Transparent = True
line just after any
object MyLabel : TLabel
line.
At runtime, you may override the TCustomForm.DoCreate and TCustomFrame.Create methods, as such:
type
THookedForm = class(TCustomForm)
procedure HookedDoCreate;
end;
THookedFrame = class(TCustomFrame)
constructor Create(AOwner: TComponent); override;
end;
var
PatchForm, OriginalForm: TPatchEvent;
PatchPositionForm: PPatchEvent = nil;
PatchFrame, OriginalFrame: TPatchEvent;
PatchPositionFrame: PPatchEvent = nil;
procedure PatchCreate;
var ov: cardinal;
begin
// hook TForm:
PatchPositionForm := PPatchEvent(#THookedForm.DoCreate);
OriginalForm := PatchPositionForm^;
PatchForm.Jump := $E9; // Jmp opcode
PatchForm.Offset := PtrInt(#THookedForm.HookedDoCreate)-PtrInt(PatchPositionForm)-5;
if not VirtualProtect(PatchPositionForm, 5, PAGE_EXECUTE_READWRITE, #ov) then
RaiseLastOSError;
PatchPositionForm^ := PatchForm; // enable Hook
// hook TFrame:
PatchPositionFrame := PPatchEvent(#TCustomFrame.Create);
OriginalFrame := PatchPositionFrame^;
PatchFrame.Jump := $E9; // Jmp opcode
PatchFrame.Offset := PtrInt(#THookedFrame.Create)-PtrInt(PatchPositionFrame)-5;
if not VirtualProtect(PatchPositionFrame, 5, PAGE_EXECUTE_READWRITE, #ov) then
RaiseLastOSError;
PatchPositionFrame^ := PatchFrame; // enable Hook
end;
{ THookedForm }
procedure THookedForm.HookedDoCreate;
var i: integer;
begin
// enumerate all labels, then set Transparent := true
for i := 0 to Components.Count-1 do
if Components[i] is TLabel then
TLabel(Components[i]).Transparent := true;
DoCreate; // call initial code
end;
{ THookedFrame }
constructor THookedFrame.Create(AOwner: TComponent);
var i: integer;
begin
// enumerate all labels, then set Transparent := true
for i := 0 to Components.Count-1 do
if Components[i] is TLabel then
TLabel(Components[i]).Transparent := true;
inherited Create(AOwner); // call normal constructor
end;
....
initialization
PatchCreate;

A related tip (I always forget to make use of this handy feature):
Configure the label the way you want to have it;
Select it on the form;
Go to Component/Create component template;
You can then a name for the template:
From then on, the template appears as a new component type in your tool palette, with the settings that you prefer.
(Yeah, I know this doesn't change current labels)

You can set the BackColor property to Color.Transparent.

The following should work: the transparent-property is present in the DFM-file only if the value is not the default. So you can us a Grep-Search to insert the "Transparent=TRUE" just in the next line after the "=TLabel". I have not tried this myself, but it is easy to try...

Related

Delphi 7 AlphaBlend not working for dynamic form

I have created dynamic Form as the next :
procedure TForm1.Button1Click(Sender: TObject);
var
Frm:TForm2;
begin
frm:=TForm2.Create(nil);
Frm.Left:=5;
Frm.Top:=5;
Frm.Parent:=Self;
Frm.OnCreate:=OncreateFrm;
Frm.Show;
end;
and when am trying to change the AlphaBlend property, the transparency wouldn't change..
procedure TForm1.OncreateFrm(Sender: TObject);
begin
AlphaBlend:=True;
AlphaBlendValue:=200;
end;
Also overriding the constructor it gave the same result ..
Thanks.
Your approach
Frm := TForm2.Create(nil);
Frm.Left := 5;
Frm.Top := 5;
Frm.Parent := Self;
Frm.OnCreate := OncreateFrm;
Frm.Show;
cannot possibly work because you set the OnCreate handler on line 5, which is after the form has been created on line 1; consequently, at the time the form is created (line 1), it sees that OnCreate is nil and so does nothing. Your instruction on line 5 has no effect.
This is like telling your fiend "Please buy some milk on your way home from work" after your friend has already come home from work.
Solutions
1: Set the properties at design time
Of course, you can use the Object Inspector to set the AlphaBlend and AlphaBlendValue properties of TForm2 at design time. But I suspect you want to do it dynamically, because you ask this question.
2: Use the OnCreate handler on TForm2
Just open TForm2 in the form editor and double click it to give it its own OnCreate handler:
// in Unit2.pas
procedure TForm2.FormCreate(Sender: TObject);
begin
AlphaBlend := True;
AlphaBlendValue := 128;
end;
3: Override TForm2's constructor
// in Unit2.pas
constructor TForm2.Create(AOwner: TComponent);
begin
inherited;
AlphaBlend := True;
AlphaBlendValue := 128;
end;
4: Set the properties when you create the object
// in Unit1.pas
procedure TForm1.Button1Click(Sender: TObject);
var
Frm: TForm2;
begin
Frm := TForm2.Create(nil);
Frm.Left := 5;
Frm.Top := 5;
Frm.AlphaBlend := True;
Frm.AlphaBlendValue := 128;
Frm.Show;
end;
Unlike the previous three approaches, this one affects only this instance of TForm2 -- it doesn't affect the class itself.
All these approaches work.
There is a "but"
Your line
Frm.Parent := Self
means that you make this form into a control instead of a top-level window.
And layered windows (the Win32 feature on which the VCL's AlphaBlend feature is based) are only supported as child windows in Windows 8 and later.
Therefore, if you are using Windows 7 or earlier, you cannot use AlphaBlend in this case.

How can I refer to a control whose name is determined at runtime?

As a kind of self-study exercise, I've made a form which contains six panels in a 2x3 rectangle and I want them to switch between visible and invisible one after another. I'm trying to do so by using a for loop of some kind. I could of course write something like:
Panel1.Visible := true;
Panel1.Visible := false;
Panel2.Visible := true;
Panel2.Visible := false;
Panel3.Visible := true;
etc. etc.
But this takes quite a lot of typing and is pretty inefficient when I decide I want it to wait for 100ms between each step. For example, I'd then have to edit all the six steps to wait. This is doable for six steps, but maybe another time I want to do it a hundred times! So I'm thinking there must also be a way to use a for loop for this, where a variable varies from 1 to 6 and is used in the object identifier. So it would something like this:
for variable := 1 to 6 do begin
Panel + variable.Visible := true;
Panel + variable.Visible := false;
end;
Now, this obviously doesn't work, but I hope somebody here can tell me if this is in fact possible and if yes, how. Maybe I can use a string as the identifier? My explanation is probably pretty bad because I don't know all the technical terms but I hope the code explains something.
You can loop through the panel's Owner's Components array.
var
i: Integer;
TmpPanel: TPanel;
begin
{ This example loops through all of the components on the form, and toggles the
Visible property of each panel to the value that is opposite of what it has (IOW,
if it's True it's switched to False, if it's False it's switched to True). }
for i := 0 to ComponentCount - 1 do
if Components[i] is TPanel then
begin
TmpPanel := TPanel(Components[i]);
TmpPanel.Visible := not TmpPanel.Visible; // Toggles between true and false
end;
end;
You can also use the FindComponent method, if you want a very specific type of component by name. For instance, if you have the 6 panels, and their names are Panel1, Panel2, and so forth:
var
i: Integer;
TmpPanel: TPanel;
begin
for i := 1 to 6 do
begin
TmpPanel := FindComponent('Panel' + IntToStr(i)) as TPanel;
if TmpPanel <> nil then // We found it
TmpPanel.Visible := not TmpPanel.Visible;
end;
end;
This is a situation where you want to create the controls dynamically at runtime rather than at designtime. Trying to grapple with 6 different variables is just going to be a world of pain. And when you need the grid to be 3x4 rather than 2x3, you'll regret that decision even more.
So, start with a completely blank form. And add, in the code, a two dimensional array of panels:
private
FPanels: array of array of TPanel;
Then, in the form's constructor, or an OnCreate event handler, you can initialise the array by calling a function like this:
procedure TMyForm.InitialisePanels(RowCount, ColCount: Integer);
var
Row, Col: Integer;
aLeft, aTop, aWidth, aHeight: Integer;
Panel: TPanel;
begin
SetLength(FPanels, RowCount, ColCount);
aTop := 0;
for Row := 0 to RowCount-1 do begin
aLeft := 0;
aHeight := (ClientHeight-aTop) div (RowCount-Row);
for Col := 0 to ColCount-1 do begin
Panel := TPanel.Create(Self);
FPanels[Row, Col] := Panel;
Panel.Parent := Self;
aWidth := (ClientWidth-aLeft) div (ColCount-Col);
Panel.SetBounds(aLeft, aTop, aWidth, aHeight);
inc(aLeft, aWidth);
end;
inc(aTop, aHeight);
end;
end;
And now you can refer to your panels using cartesian coordinates rather than a flat one dimensional array. Of course, you can easily enough declare a flat one dimensional array as well if you want.
The key idea is that when you are creating large numbers of control in a structured layout, you are best abandoning the designer and using code (loops and arrays).
Use FindComponent method of TComponent:
for variable := 1 to 6 do begin
pnl := FindComponent('Panel' + IntToStr(variable));
if pnl is TPanel then
begin
TPanel(pnl).Visible := true;
TPanel(pnl).Visible := false;
end;
end;
As others have answered, FindComponent is the way to go.
But if you just want to modify generic properties for the component, such as visible, position etc, it's not necessary to compare to the type.
This will work just as fine:
for i := 1 to 16 do
begin
(FindComponent( 'P' + inttostr(i) ) as TControl).Visible := false;
end;
(NOTE: this is for Delphi 6/ 7, modern versions probably do this in other ways)
Actually my answer
If you use a name convention to name your component like
"Mycomponent" + inttostr(global_int)
you can use it to find it very easily :
function getMyComponent(id:integer) : TComponent;
begin
result := {Owner.}FindConponent('MyComponent'+inttostr(id));
end;
You also can make your generated components to interact each other by using (sender as TComponent).name to know which other component are related to him.
Exemple
Following is an example of what you can do with this :
Imagine a pagecontrol where tabs are an interface you want to have multiple time
(for ex, to describe columns in a file with 1 tab = 1 col, and you want to dynamically add tabs).
For our example, we are naming button and edit this way :
Button : "C_(column_number)_btn"
Edit : "C_(column_number)_edi"
You can actually refer directly to the edit with a buttonclick, linked at runtime by calling findcomponent :
procedure TForm1.ColBtnClick(Sender:TObject);
var nr : string; Edit : TEdit;
begin
// Name of the TButton. C(col)_btn
nr := (Sender as TButton).Name;
// Name of the TEdit C_(column)_edi
nr := copy(nr,1,length(nr)-3)+'edi';
// Get the edit component.
edit := (Form1.Findcomponent(nr) as TEdit);
//play with it
Edit.Enabled := Not Edit.Enabled ;
showmessage(Edit.Text);
Edit.hint := 'this hint have been set by clicking on the button';
//...
end;
Of course, you link this procedure to every generated buttons.
If anyone wants to practice with it, you may want to know how to generate the tabsheet and components, here you go :
procedure Form1.addCol(idcol:integer, owner : TComponent); // Form1 is a great owner imo
var
pan : TPanel; // Will be align client with the new tabsheet
c: TComponent; //used to create components on the pannel
tab : TTabSheet;
begin
try
pan := TPanel.create(owner);
pan.name := format('Panel_%d',[idcol]);
pan.caption := '';
// dynamically create that button
c := TButton.create(Owner);
with c as TButton do
begin
Name := format('C%d_btn',[idcol]);
Parent := pan;
//Top := foo;
//Left := bar;
caption := 'press me';
OnClick := Form1.ColBtnClick; // <<<<<<< link procedure to event
end;
//create a Tedit the same way
c := TEdit.create(Owner);
with c as TEdit do
Name := format('C%d_edi',[idcol]);
Parent := pan;
// other properties
// create the tabsheet and put the panel in
finally
tab := TTabSheet.Create(Parent);
tab.caption := 'Column %d';
tab.PageControl := Pagecontrol1;
pan.Parent := tab;
pan.Align := alClient;
end;
end;
Generating names to get the component is actually a very good way to have a clean code.
Scrolling through parent - child components in order to find the one you want is actually inefficient and becomes hell if there is many component (in my example, if there is 3, 10 or unknown number of TEdit looping child (brother) components will be ugly.
Maybe this example is useless but It may helps someone, someday.

Shift in the right of last item of the menu

Delphi Xe2U4. Main menu items: File, Option, Help (name: HelpMenuItem). 2 buttons. Use StyleManager Xe2 (in project option enabled xe2 themes, and default set 'Metro Blue').
Procedure TForm1.RightMenu; // Shift in the right of last item of the menu
var mii: TMenuItemInfo;MainMenu: hMenu; Buffer: array[0..79] of Char;
begin
MainMenu := Self.Menu.Handle;
mii.cbSize := SizeOf(mii) ;
mii.fMask := MIIM_TYPE;
mii.dwTypeData := Buffer;
mii.cch := SizeOf(Buffer) ;
GetMenuItemInfo(MainMenu, HelpMenuItem.Command, false, mii) ;
mii.fType := mii.fType or MFT_RIGHTJUSTIFY;
SetMenuItemInfo(MainMenu, HelpMenuItem.Command, false, mii) ;
end;
procedure TForm1.Metro1Click(Sender: TObject); // Not Work
begin
TStyleManager.TrySetStyle('Metro Blue'); // or any other
RightMenu;
end;
procedure TForm1.Windows1Click(Sender: TObject); // Work
begin
TStyleManager.TrySetStyle('Windows'); // standart theme
RightMenu;
end;
Why does not work at use theme?
Whether or there is a normal way to shift last point of the menu in the right, whether is not dependent schemes are applied or not?
Unfortunally the vcl style hook of the TMainMenu doesn't implement the code to draw a particular menu item aligned to the right. Also this vcl style hook (TMainMenuBarStyleHook) is embedded in the TFormStyleHook (the vcl style hook for the forms) as a strict private member, so there is not much room for modifications here. Fix this issue will require which you rewrite the a new vcl style hook for the TForms and the TMainMenus. So If you want do this you must copy the TFormStyleHook class from the Vcl.Forms unit to a new unit and then fix the implementation of the TFormStyleHook.TMainMenuBarStyleHook.DrawItem and the TFormStyleHook.TMainMenuBarStyleHook.Paint methods.
Procedure TForm1.RightMenu; // Shift in the right of last item of the menu
var mii: TMenuItemInfo;MainMenu: hMenu; Buffer: array[0..79] of Char;
begin
MainMenu := Self.Menu.Handle;
mii.cbSize := SizeOf(mii) ;
mii.fMask := MIIM_TYPE;
mii.dwTypeData := Buffer;
mii.cch := SizeOf(Buffer) ;
GetMenuItemInfo(MainMenu, HelpMenuItem.Command, false, mii) ;
mii.fType := mii.fType or MFT_RIGHTJUSTIFY;
if SetMenuItemInfo(MainMenu, HelpMenuItem.Command, false, mii) then DrawMenuBar(self.Menu.WindowHandle);
end;

Can you override MessageDlg calls to a Custom TForm/Dialog?

I have been using code similar to this
MessageDlg('', mtWarning, [mbOK], 0);
throughout my project, (thanks to the GExperts Message Dialog tool :) ) and i was wondering if anyone knows of a way do override the call and show my own custom Form.
The only way i can think to do it its make a New Form with something like
function MessageDlg(const Msg: string; DlgType: TMsgDlgType;
Buttons: TMsgDlgButtons; HelpCtx: Longint): Integer;
begin
//show my own code here
end;
and put it each of my uses lists before the Dialogs unit but is there a guaranteed way to make sure it uses my code not the Dialogs unit Code.
I don't like the idea of copying the dialogs unit to a local dir and making changes to it.
Or is this all to much work and should i just use my own function call and replace all the MessageDlg with my own. (which would not be fun, ive prob used MessageDlg too much)
BTW, you want to add it after the Dialogs unit in your uses clause.
You have three choices in my opinion:
Add your own unit after the Dialogs unit that has a method called MessageDlg and has the same signature to create your own form.
Or create a whole new method, or set of methods, that creates specific dialogs using your own form.
Do a global Search & Replace for MessageDlg with DarkAxi0mMessageDlg and then add your DarkAxi0mDialogs unit to your uses clause.
The first one is problematic because you might miss a unit and still get the old MessageDlg. The second one takes a lot more use, but provides better flexibility in the long run. The third one is probably the easiest and with the least downsides. Make sure you backup before doing the replace, and then use a diff tool (like Beyond Compare) to check your changes.
I would recommend you to encapsulate the MessageDlg inside of you own procedures, this way if you change your procedures all your Message dialogs will be changed and you keep a standard.
Example: Create some procedures like, Alert(), Error(), Warning(), etc. If you ever need to change your error message looks, you need to do it only in one place.
Someday you might want to add a picture to your error messages, alerts... whatever, who knows?
You can use a tool like TextPad to search/replace all instances of a string across folders and subfolders. So, I would suggest that you replace "MessageDlg(" with "MyMessageDlg(" so that you can customize it at will. Should take all of 5 minutes.
I think it would cause you problems to create a replacement and leave it named as it is currently in conflict with the VCL.
You can hijack the MessageDlg function and make it point to your own MyMessageDlg function (with same signature) but I think it would the least safe of all the solutions.
A bad hack in lieu of clean code IMO.
Save the original opcodes of MessageDlg (asm generated by the compiler)
Put a hard jump to your MyMessageDlg code
...then any call to MessageDlg will actually execute YOUR code ...
Restore the original code to MessageDlg
MessageDlg now behaves as usual
It works but should be reserved for desperate situations...
i made a MessageDlgEx function based on MessageDlg and dropped it into one of my "library" files so all my apps can use it. my function allows you to specify default & cancel buttons, give button texts, etc. it'd be a bad practice to modify/replace the built-in function. i still use the built-in function but keep this function on hand for situations where a little more is needed.
FYI--the function returns the number of the button pressed. the first button is 1. pressing Close causes a return value of 0. the buttons have no glyphs.
i have been using this for about 5 years & it's served me well.
function MessageDlgEx(Caption, Msg: string; AType: TMsgDlgType;
AButtons: array of string;
DefBtn, CanBtn: Integer; iWidth:integer=450;bCourier:boolean=false): Word;
const
icMin=50;
icButtonHeight=25;
icInterspace=10;
icButtonResultStart=100;
icFirstButtonReturnValue=1;
var
I, iButtonWidth, iAllButtonsWidth,
iIconWidth,iIconHeight:Integer;
LabelText:String;
Frm: TForm;
Lbl: TLabel;
Btn: TBitBtn;
Glyph: TImage;
FIcon: TIcon;
Rect:TRect;
Caption_ca:Array[0..2000] of Char;
begin
{ Create the form.}
Frm := TForm.Create(Application);
Frm.BorderStyle := bsDialog;
Frm.BorderIcons := [biSystemMenu];
Frm.FormStyle := fsStayOnTop;
Frm.Height := 185;
Frm.Width := iWidth;
Frm.Position := poScreenCenter;
Frm.Caption := Caption;
Frm.Font.Name:='MS Sans Serif';
Frm.Font.Style:=[];
Frm.Scaled:=false;
if ResIDs[AType] <> nil then
begin
Glyph := TImage.Create(Frm);
Glyph.Name := 'Image';
Glyph.Parent := Frm;
FIcon := TIcon.Create;
try
FIcon.Handle := LoadIcon(HInstance, ResIDs[AType]);
iIconWidth:=FIcon.Width;
iIconHeight:=FIcon.Height;
Glyph.Picture.Graphic := FIcon;
Glyph.BoundsRect := Bounds(icInterspace, icInterspace, FIcon.Width, FIcon.Height);
finally
FIcon.Free;
end;
end
else
begin
iIconWidth:=0;
iIconHeight:=0;
end;
{ Loop through buttons to determine the longest caption. }
iButtonWidth := 0;
for I := 0 to High(AButtons) do
iButtonWidth := Max(iButtonWidth, frm.Canvas.TextWidth(AButtons[I]));
{ Add padding for the button's caption}
iButtonWidth := iButtonWidth + 18;
{assert a minimum button width}
If iButtonWidth<icMin Then
iButtonWidth:=icMin;
{ Determine space required for all buttons}
iAllButtonsWidth := iButtonWidth * (High(AButtons) + 1);
{ Each button has padding on each side}
iAllButtonsWidth := iAllButtonsWidth +icInterspace*High(AButtons);
{ The form has to be at least as wide as the buttons with space on each side}
if iAllButtonsWidth+icInterspace*2 > Frm.Width then
Frm.Width := iAllButtonsWidth+icInterspace*2;
if Length(Msg)>sizeof(Caption_ca) then
SetLength(Msg,sizeof(Caption_ca));
{ Create the message control}
Lbl := TLabel.Create(Frm);
Lbl.AutoSize := False;
Lbl.Left := icInterspace*2+iIconWidth;
Lbl.Top := icInterspace;
Lbl.Height := 200;
Lbl.Width := Frm.ClientWidth - icInterspace*3-iIconWidth;
Lbl.WordWrap := True;
Lbl.Caption := Msg;
Lbl.Parent := Frm;
if bCourier then
lbl.Font.Name:='Courier New';
Rect := Lbl.ClientRect;
LabelText:=Lbl.Caption;
StrPCopy(Caption_ca, LabelText);
Lbl.Height:=DrawText(Lbl.Canvas.Handle,
Caption_ca,
Length(LabelText),
Rect,
DT_CalcRect or DT_ExpandTabs or DT_WordBreak Or DT_Left);
If Lbl.Height<iIconHeight Then
Lbl.Height:=iIconHeight;
{ Adjust the form's height accomodating the message, padding and the buttons}
Frm.ClientHeight := Lbl.Height + 3*icInterspace + icButtonHeight;
{ Create the pusbuttons}
for I := 0 to High(AButtons) do
begin
Btn := TBitBtn.Create(Frm);
Btn.Height := icButtonHeight;
Btn.Width := iButtonWidth;
Btn.Left:=((Frm.Width-iAllButtonsWidth) Div 2)+I*(iButtonWidth+icInterspace);
Btn.Top := Frm.ClientHeight - Btn.height-icInterspace;
Btn.Caption := AButtons[I];
Btn.ModalResult := I + icButtonResultStart + icFirstButtonReturnValue;
Btn.Parent := Frm;
If I=DefBtn-1 Then
Begin
Frm.ActiveControl:=Btn;
Btn.Default:=True;
End
Else
Btn.Default:=False;
If I=CanBtn-1 Then
Btn.Cancel:=True
Else
Btn.Cancel:=False;
end;
Application.BringToFront;
Result := Frm.ShowModal;
{trap and convert user Close into mrNone}
If Result=mrCancel Then
Result:=mrNone
Else
If Result>icButtonResultStart Then
Result:=Result - icButtonResultStart
Else
Exception.Create('Unknown MessageDlgEx result');
Frm.Free;
end;

Remove and Replace a visual component at runtime

Is it possible to, for instance, replace and free a TEdit with a subclassed component instantiated (conditionally) at runtime? If so, how and when it should be done? I've tried to set the parent to nil and to call free() in the form constructor and AfterConstruction methods but in both cases I got a runtime error.
Being more specific, I got an Access violation error (EAccessViolation). It seems François is right when he says that freeing components at frame costruction messes with Form controls housekeeping.
This more generic routine works either with a Form or Frame (updated to use a subclass for the new control):
function ReplaceControlEx(AControl: TControl; const AControlClass: TControlClass; const ANewName: string; const IsFreed : Boolean = True): TControl;
begin
if AControl = nil then
begin
Result := nil;
Exit;
end;
Result := AControlClass.Create(AControl.Owner);
CloneProperties(AControl, Result);// copy all properties to new control
// Result.Left := AControl.Left; // or copy some properties manually...
// Result.Top := AControl.Top;
Result.Name := ANewName;
Result.Parent := AControl.Parent; // needed for the InsertControl & RemoveControl magic
if IsFreed then
FreeAndNil(AControl);
end;
function ReplaceControl(AControl: TControl; const ANewName: string; const IsFreed : Boolean = True): TControl;
begin
if AControl = nil then
Result := nil
else
Result := ReplaceControlEx(AControl, TControlClass(AControl.ClassType), ANewName, IsFreed);
end;
using this routine to pass the properties to the new control
procedure CloneProperties(const Source: TControl; const Dest: TControl);
var
ms: TMemoryStream;
OldName: string;
begin
OldName := Source.Name;
Source.Name := ''; // needed to avoid Name collision
try
ms := TMemoryStream.Create;
try
ms.WriteComponent(Source);
ms.Position := 0;
ms.ReadComponent(Dest);
finally
ms.Free;
end;
finally
Source.Name := OldName;
end;
end;
use it like:
procedure TFrame1.AfterConstruction;
var
I: Integer;
NewEdit: TMyEdit;
begin
inherited;
NewEdit := ReplaceControlEx(Edit1, TMyEdit, 'Edit2') as TMyEdit;
if Assigned(NewEdit) then
begin
NewEdit.Text := 'My Brand New Edit';
NewEdit.Author := 'Myself';
end;
for I:=0 to ControlCount-1 do
begin
ShowMessage(Controls[I].Name);
end;
end;
CAUTION: If you are doing this inside the AfterConstruction of the Frame, beware that the hosting Form construction is not finished yet.
Freeing Controls there, might cause a lot of problems as you're messing up with Form controls housekeeping.
See what you get if you try to read the new Edit Caption to display in the ShowMessage...
In that case you would want to use
...ReplaceControl(Edit1, 'Edit2', False)
and then do a
...FreeAndNil(Edit1)
later.
You have to call RemoveControl of the TEdit's parent to remove the control. Use InsertControl to add the new control.
var Edit2: TEdit;
begin
Edit2 := TEdit.Create(self);
Edit2.Left := Edit1.Left;
Edit2.Top := Edit2.Top;
Edit1.Parent.Insertcontrol(Edit2);
TWinControl(Edit1.parent).RemoveControl(Edit1);
Edit1.Free;
end;
Replace TEdit.Create to the class you want to use, and copy all properties you need like I did with Left and Top.
You can actually use RTTI (look in the TypInfo unit) to clone all the matching properties. I wrote code for this a while back, but I can't find it now. I'll keep looking.

Resources