I am subclassing TGridPanel to my control TMyGridPanel.
I do this because i want to add 4 default buttons in the GridPanel.
So i override the constructor and create the buttons like:
constructor TMyGridPanel.Create(AOwner: TComponent);
var
i: Integer;
btn: TButton;
begin
inherited Create(AOwner);
for i := 0 to 3 do
begin
btn := TButton.Create(Self);
btn.Parent := Self;
btn.Align := alClient;
btn.Caption := 'Hello World';
btn.Visible := True;
end;
end;
This is working fine.
The ControlCollection Items property shows 4 Buttons as CollectionItems .
Now i want to copy and paste (duplicate) my control because i want to have 2 of it.
However when i do it the buttons don't show up in the control.
The ControlCollection Items property shows 4 Collection Items but they don't have a name (empty).
When i close the form and reopen it the buttons appear.
I am trying to fix this problem for some days now but can't figure it out.
Problem:
When you copy your panel component to clipboard, all its published properties are streamed into text (paste it in notepad to see how it looks).
Pasting to the form reconstructs the component back from this text.
And as ControlCollection property is defined in Vcl.ExtCtrls.TGridPanel as published, buttons within it are included in this text. Here is an excerpt:
object MyGridPanel1: TMyGridPanel
Left = 64
...
ControlCollection = <
item
Column = 0
Control = Button9
Row = 0
end
item
Column = 1
Control = Button10
Row = 0
end
...
object Button9: TButton
Left = 1
...
end
object Button10: TButton
Left = 92
...
end
...
end
When pasting, the IDE designer first creates a new object of class TMyGridPanel. During this step the constructor of TMyGridPanel creates a new set of buttons.
After that all published properties get reconstructed from the text, including the ControlCollection and Buttons within it, and this is where problem lies.
Possible solution:
A possible solution in this situation is to change parent class of TMyGridPanel to TCustomGridPanel
TMyGridPanel2 = class(TCustomGridPanel)
...
TCustomGridPanel (similar to other TCustom... components) does not publish any of its properties, so they won't get streamed into clipboard.
Actually inheriting from TCustom... variants of controls, and not from the one registered in Component Pallet, is the right way to subclass components.
If we now copy this variant of TMyGridPanel2 to clipboard and paste it in notepad, we can see that there no additional properties:
object MyGridPanel21: TMyGridPanel2
Left = 184
Top = 200
Width = 185
Height = 41
end
Drawbacks:
This approach works, but have several cons that has to be noted:
You cannot access custom properties introduced by TGridPanel in Object Inspector (but you can access them at runtime).
A workaround to bring a property back in Object Inspector, is to add it in published section of your component:
TMyGridPanel2 = class(TCustomGridPanel)
public
...
published
property BorderStyle;
property ColumnCollection;
property RowCollection;
...
end;
You cannot change properties of the four buttons via Object Inspector, nor attach events to them. You have to do that in code.
Actually this is good behavior. When you create a composite component that has child controls, it is good practice to have all functionality contained within the component itself.
Full code sample:
unit MyGridPanel2;
interface
uses
Classes, Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.Controls;
type
TMyGridPanel2 = class(TCustomGridPanel)
private
public
constructor Create(AOwner: TComponent); override;
published
end;
procedure Register;
implementation
{ TMyGridPanel2 }
constructor TMyGridPanel2.Create(AOwner: TComponent);
var
i: Integer;
btn: TButton;
begin
inherited Create(AOwner);
for i := 0 to 3 do
begin
btn := TButton.Create(Self);
btn.Parent := Self;
btn.Align := alClient;
btn.Caption := 'Hello World';
btn.Visible := True;
end;
end;
procedure Register;
begin
RegisterComponents('Custom', [TMyGridPanel2]);
end;
end.
Try this in test project first, not in production.
Related
I'm using a TGridPanel to hold some panels. At design time, I've set the grid panel to have 1 row and 5 columns.
I can add a panel to the grid using this code, which works well:
procedure TForm6.AddPanelToGrid(const ACaption: string);
var
pnl: TPanel;
begin
pnl := TPanel.Create(gpOne);
pnl.Caption := ACaption;
pnl.Parent := gpOne;
pnl.Name := 'pnlName' + ACaption;
pnl.OnClick := gpOne.OnClick;
pnl.ParentBackground := false;
pnl.ParentColor := false;
pnl.Color := clLime;
pnl.Font.Size := 14;
gpOne.ControlCollection.AddControl(pnl);
pnl.Height := pnl.Width;
end;
What I want to do is remove a TPanel from the grid when I click on it (which is why I have set the on click handler to that of the grid panel in the above code).
In that click handler I do this, which almost works:
procedure TForm6.gpOneClick(Sender: TObject);
begin
if not (sender is TPanel) then exit;
gpOne.ControlCollection.RemoveControl(Sender as TPanel);
(Sender as TPanel).Free;
gpOne.UpdateControlsColumn( 0 ); <<<-------
gpOne.UpdateControlsRow(0);
gpOne.Refresh();
end;
Using a parameter for UpdateControlColumn() causes the order of the panels in the grid to change - the first and second swap places.
I can get around this by adding the column idex to the panel's tag property, then pass that to UpdateControlColumn(). This then works, but once a panel has been removed the higher tag numbers are no longer valid - the panels have moved column.
So, how can I get the column that a panel is in from within the OnClick handler?
I'm using Delphi 10.1 Berlin - if that makes any difference.
To test this, I started a new project, added a TGridPanel, set it to have 1 row and 5 equally widthed columns. I added 6 TButton controls and created an OnClick handler for each with the following code:
AddPanelToGrid('One'); // changing the string for each button.
Click a few buttons to add some panels, then click the panels to remove them.
TCustomGridPanel has a pair of useful functions, CellIndexToCell() and CellToCellIndex, but they are not public and thus not directly accessible from a TGridPanel.
To make them available declare TGridPanel anew as below:
type
TGridPanel = class(Vcl.ExtCtrls.TGridPanel) // add this
end; // -"-
TForm27 = class(TForm)
Button1: TButton;
gpOne: TGridPanel;
...
end;
Then add rand c variables for row and col, add the call to CellIndexToCell() and use c as argument for UpdateControlsColumn:
procedure TForm27.gpOneClick(Sender: TObject);
var
r, c: integer;
begin
if not (sender is TPanel) then exit;
gpOne.CellIndexToCell(gpOne.ControlCollection.IndexOf(Sender as TPanel), c, r); // add this
gpOne.ControlCollection.RemoveControl(Sender as TPanel);
(Sender as TPanel).Free;
gpOne.UpdateControlsColumn( c ); // <<<-------
gpOne.UpdateControlsRow(0);
gpOne.Refresh();
end;
And follow advise of Remy Lebeau, regarding freeing the panel. ( I just noticed his comment).
If you haven't already, you may also want to take a look at TFlowPanel and its FlowStyle property. TflowPanel reordering after deletion is more predictable if you use more than one row, but depends of course on what you need.
I've got a TCategoryPanel descendant and would like to add several controls to its header region, like a TComboBox and a TButton for instance.
I took a look at the TCustomCategoryPanel codes and from what I understood it moves controls added to it into its internal FPanelSurface container using a message handler TCustomCategoryPanel.CMControlListChanging for CM_CONTROLLISTCHANGING.
I created a similar message handler in my descendant:
interface
// ...
TElementsCategoryPanel = class(TCategoryPanel)
// ...
private
FObservationTypeSelector: TComboBox;
procedure CMControlListChanging(var Message: TCMControlListChanging);
message CM_CONTROLLISTCHANGING;
// ...
end;
implementation
// ...
constructor TElementsCategoryPanel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
// ...
FObservationTypeSelector := TComboBox.Create(Self);
FObservationTypeSelector.Name := 'ObservationTypeSelector';
FObservationTypeSelector.Parent := Self; // *
// ...
end;
procedure TElementsCategoryPanel.CMControlListChanging(
var Message: TCMControlListChanging);
begin
if not (Message.ControlListItem^.Control = FObservationTypeSelector) then // *
inherited;
end;
Using breakpoints * to step through, the logic works. When FObservationTypeSelector.Parent := Self is set in the constructor, my CMControlListChanging is being called:
If Message.ControlListItem^.Control is not my FObservationTypeSelector combobox, it will call its parent function via inherited.
If Message.ControlListItem^.Control IS my FObservationTypeSelector combobox, it will do nothing, meaning it will keep its assignment from the constructor.
But the code does not seem to have an effect. Afterwards when positioning my combobox after the panel is shown, setting it to FObservationTypeSelector.Top := 0 it remains bound inside the FPanelSurface container, being placed under the header, instead inside of it, as expected.
Current state:
Desired state:
What am I doing wrong here?
I have a application which load plugins that are in the form of bpls. Each plugin contains a form to be embedded into another package called CoreInf (form name ManageF) which is just the base gui of the application that contains a TTabcontrol known as MTabcontrol1. Here basically what happens
A list of plugins are dynamically loaded at runtime in sequential order to layout the interface. It loads the plugins based on the interface IPluginFrame(Shared.bpl) . If it contains the interface it then tries to create a new tab in MTabcontrol1 and embedded the form. What im trying to do is make a dynamically created speedbutton onClick focus a TEdit box on a specific ebedded form but it keeps coming up as access violation errors.
Shared.bpl that contains the interface
unit PluginIntf;
interface
uses
FMX.Types, FMX.Controls;
type
IPluginFrame = interface
['{3B4943DB-951B-411B-8726-03BF1688542F}']
function GetBaseControl: TControl;
end;
implementation
end.
Form and Button that is to be ebedded into the interface
InventoryInf.pas has the form to be embeeded
var
InventoryF: TInventoryF;
implementation
{$R *.fmx}
function TInventoryF.GetBaseControl: TControl;
begin
Result := InvenLayout; //<---- This is a a TLayout which align
//to client this is the parent of every item on form
end;
//Below is the on click event for the TSpeedButton invBtn
//Which looks for the embedded form that is embedded
//in MTabcontrol1 as a TTabitem that has the name InventoryF
procedure TInventoryF.Look4Tabitem(Sender: TObject);
var
o : TTabitem;
s :TEdit;
begin
o := TTabitem(ManageF.MTabcontrol1.FindComponent('InventoryF'));
ManageF.MTabcontrol1.ActiveTab := o;
s := TEdit(ManageF.MTabcontrol1.FindComponent('VendorF').FindComponent('SearchEdit1')); <-- Dont think it actually found the TEdit
s.Setfocus; <------------------ Not focusing giving access violtaion error
with DataConModule1.InventoryQuery do
...
end;
TSpeedButton invBtn that is injected into Panel to the side
unit InjectedInvBtn;
interface
implementation
uses
FMX.Types, InventoryInf, FMX.Controls, FMX.Forms, InjectedControlsHelper, FMX.StdCtrls,
ManageInf;
var
SysBtn: TSpeedButton;
initialization
SysBtn := TInjectedControl<TSpeedButton>.Create;
SysBtn.Align := TAlignLayout.Top;
SysBtn.Name := 'invBtn';
SysBtn.Text := 'INVENTORY';
ManageF.MenuPanel.AddObject(SysBtn);
SysBtn.OnClick := InventoryF.Look4Tabitem;
end.
**ManageF Showing how it loads the forms into tabs MTabControl1 **
uses Shared;
function IsPluginFrameClass(AType: TRttiType; var AFormClass: TCustomFormClass): Boolean;
var
LClass: TClass;
begin
if not (AType is TRttiInstanceType) then Exit(False);
LClass := TRttiInstanceType(AType).MetaclassType;
Result := LClass.InheritsFrom(TCustomForm) and Supports(LClass, IPluginFrame);
if Result then
AFormClass := TCustomFormClass(LClass);
end;
function TSettingsF.LoadPluginTabs(const AFileName: string): HMODULE;
var
Context: TRttiContext;
Frame: TCustomForm;
FrameClass: TCustomFormClass;
LType: TRttiType;
Package: TRttiPackage;
Tab: TTabItem;
// Statusbar: TTabItem;
//Butz: TButton;
begin
Result := LoadPlugin(AFileName);
try
{ Cycle through the RTTI system's packages list to find the one we've just loaded. }
for Package in Context.GetPackages do
if Package.Handle = Result then
begin
{ Cycle through the package's types looking for implementors of the
IPluginFrameinterface defined in the shared package. }
for LType in Package.GetTypes do
if IsPluginFrameClass(LType, FrameClass) then
begin
{ For each frame, create a new tab to host its contents. In the case of
a VCL application, we could require an actual TFrame object, or failing
that, embed the form directly. FireMonkey has neither frames proper nor
supports embedded forms, so instead we ask the implementing form to
nominate a base control that will get embedded. }
Tab := TTabItem.Create(ManageF.MTabcontrol1);
Frame := FrameClass.Create(Tab);
Tab.Text := ' ' + Frame.Caption;
Tab.Name := Frame.Name;
MyTablist.Add(Tab);
(Frame as IPluginFrame).GetBaseControl.Parent := Tab;
{ Associate the tab with the plugin - since it owns the 'frame' form,
and that form owns its own components, freeing the tab will have the
effect of freeing all the actual plugin objects too. }
RegisterPluginComponent(Result, Tab);
ManageF.MTabcontrol1.AddObject(Tab);
Tab.Width := Tab.Canvas.TextWidth(Tab.Text + 'w');
end;
if IsStatusFrameClass(LType, FrameClass) then
begin
....
{ Associate the tab with the plugin - since it owns the 'frame' form,
and that form owns its own components, freeing the tab will have the
effect of freeing all the` actual plugin objects too. }
// RegisterPluginComponent(Result, Statusbar);
// ManageF.StatusMenuPanel1.AddObject(Statusbar);
//Statusbar.Width := Statusbar.Canvas.TextWidth(Statusbar.Name + 'w');
end;
Break;
end;
except
UnloadPlugin(Result);
raise;
end;
end;
Hope i illustrated the problem properly. Please Help =(
Found a work around by looping through the components of the Tcustomform that is embedded. Here is what i did.
instead of TEdit i used a tms edit box TTMSFMXSearchEdit
procedure TVendorF.focuscheck;
var
i: integer;
j: integer;
Fieldname: string;
o : TTabitem;
e : TTMSFMXSearchEdit;
begin
if ManageF.MTabcontrol1.FindComponent('VendorF').Name = 'VendorF' then
begin
o := TTabitem(ManageF.MTabcontrol1.FindComponent('VendorF'));
//ShowMessage(IntToStr(o.ComponentCount)) ;
// ShowMessage((o.Components[0].tostring));
for i := 0 to ManageF.MTabcontrol1.ActiveTab.ComponentCount - 1 do
if (ManageF.MTabcontrol1.ActiveTab.Components[i]) is TCustomForm then
begin
// ShowMessage('TCustomForm Recognized gonna look for child components now');
// ShowMessage(IntToStr(ManageF.MTabcontrol1.ActiveTab.Components[i].ComponentCount));
for j := 0 to ManageF.MTabcontrol1.ActiveTab.Components[i].ComponentCount - 1 do
if (ManageF.MTabcontrol1.ActiveTab.Components[i].Components[j]) is TTMSFMXSearchEdit then
begin
// ShowMessage('Edit box found =)')
if (ManageF.MTabcontrol1.ActiveTab.Components[i].Components[j].Name = 'VenSearchEdit1') then
begin
//ShowMessage('Edit1 box found =)');
//ShowMessage('See if we can focus it');
e := TTMSFMXSearchEdit(ManageF.MTabcontrol1.ActiveTab.Components[i].Components[j]) ;
e.SetFocus;
end;
end;
end;
end;
end;
Its a little sloppy but it works =). If anyone else got a better way let me know
I have added a TImage to the style of TListBoxItem.
If I add to a TListBox, it works. If I add to a TComboBox, it doesn't works. I can't even change the height if the item in a TComboBox.
Here my sample code:
procedure TMainForm.FormCreate(Sender: TObject);
const
BitmapFile : String = 'F:\testimage.png';
var
ItemText : TText;
ItemImage : TImage;
ListBoxItem : TListBoxItem;
button : TButton;
begin
ListBoxItem := TListBoxItem.Create(nil);
ListBoxItem.Parent := CBoxHeadMenuLanguage;
ListBoxItem.StyleLookup := 'ListBoxItemIconStyle';
ListBoxItem.Height := 50; //just for test
ItemText := ListBoxItem.FindStyleResource('text') as TText;
if Assigned(ItemText) then ItemText.Text := 'Hello World!';
ItemImage := ListBoxItem.FindStyleResource('image') as TImage;
if Assigned(ItemImage) then If FileExists(BitmapFile) Then ItemImage.Bitmap.LoadFromFile(BitmapFile);
end;
You really shouldn't be doing styling stuff in FormCreate since styles are applied on an as-needed basis and can be removed and reapplied at any time.
Instead you'll need to use either OnApplyStyleLookup event or the ApplyStyle method. I recommend subclassing TListBox and using the latter and add a property to store the bitmap.
An outline class declaration would be:
type TBitmapLBItem = class(TListBoxItem)
private
FBitmap: TBitmap;
protected
procedure ApplyStyle;override;
public
property Bitmap: TBitmap read FBitmap write SetBitmap;
end;
Use FindStyleResource etc both in ApplyStyle and SetBitmap (or create a shared method to do it).
And in FormCreate create items of your new class and set the Bitmap properties as appropriate.
As for the height problem, try setting the ItemHeight property of the combo box. If you want a variety of heights within the list you're probably out of luck.
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...