Dynamically access a property in a Delphi component - delphi

I'm using Delphi 5, and we have a method to dynamically create certain controls based on the contents of a database table (we create TButtons mostly) and take action when those are clicked. This allows us to add simple controls to a form without having to recompile the application.
I was wondering if it was possible to set a component's property based on a property name contained in a string so we could set further options.
Pseudo-code:
Comp := TButton.Create(Self);
// Something like this:
Comp.GetProperty('Left').AsInteger := 100;
// Or this:
Comp.SetProperty('Left', 100);
Is this possible at all?

You have to use the Run-Time Type Information features of Delphi to do this:
This blog describes exactly what you are trying to do: Run-Time Type Information In Delphi - Can It Do Anything For You?
Basically you have to get the property information, using GetPropInfo and then use SetOrdProp to set the value.
uses TypInfo;
var
PropInfo: PPropInfo;
begin
PropInfo := GetPropInfo(Comp.ClassInfo, 'Left');
if Assigned(PropInfo) then
SetOrdProp(Comp, PropInfo, 100);
end;
This is not as concise as your pseudo-code, but it still does the job. Also it gets more complicated with other stuff, like array properties.

From one of my working units (in Delphi 7 though)
var
c : TComponent;
for i := 0 to pgcProjectEdits.Pages[iPage].ControlCount - 1 do
begin
c := pgcProjectEdits.Pages[iPage].Controls[i];
if c is TWinControl
then begin
if IsPublishedProp(c,'color')
then
SetPropValue(c,'color',clr);
if IsPublishedProp(c,'readonly')
then
SetPropValue(c,'readonly', bReadOnly );
...
end;
...
You have to include TypInfo in the uses statement.
Don't know if this works under Delphi 5.

Just as an added example. Here is how to set sub-properties, I'm setting the Margins on this Button component:
uses TypInfo;
...
procedure TForm1.Button1Click(Sender: TObject);
begin
var PropInfo := GetPropInfo(Button1.ClassInfo, 'Margins');
if Assigned(PropInfo) then
begin
var Margins := TMargins.Create(self);
try
Margins.Left := 100;
Margins.Top := 100;
Margins.Right := 100;
Margins.Bottom := 100;
SetObjectProp(Button1, PropInfo, Margins);
finally
Margins.Free;
end;
end;
end;
This works on Delphi 10.3 Rio and later due to the inline variables.

Related

How to create a non visual component without any icon on the form?

I would like to create a non visual component (like TTimer for example) that I can drop on the form and that I can set up directly from the Object Inspector, but I don't want to see its icon on the form (it'd just obstruct anything). For example TFloatAnimation works like this but I don't understand how.
The GExperts library (http://www.gexperts.org/) has a plug-in which can toggle the visibility
of non-visual components on a form, and it is apparently not Delphi-version-specific but it is
not exactly trivial.
The method which does this is
procedure THideNonVisualCompsExpert.ToggleNonVisualVisible(Form: TCustomForm);
const
NonVisualClassName = 'TContainer';
var
VisibleState: Boolean;
FormHandle: THandle;
CompHandle: THandle;
WindowClass: string;
FirstCompFound: Boolean;
WinControl: TWinControl;
ChildControl: TWinControl;
i: Integer;
begin
Assert(Assigned(Form));
Assert(Form.Handle > 0);
FirstCompFound := False;
WinControl := Form;
if InheritsFromClass(WinControl.ClassType, 'TWinControlForm') then
begin
for i := WinControl.ComponentCount - 1 downto 0 do
begin
if WinControl.Controls[i] is TWinControl then
begin
ChildControl := WinControl.Controls[i] as TWinControl;
if InheritsFromClass(ChildControl.ClassType, 'TCustomFrame') then
begin
WinControl := ChildControl;
Break;
end;
end;
end;
end;
FormHandle := GetWindow(WinControl.Handle, GW_CHILD);
CompHandle := GetWindow(FormHandle, GW_HWNDLAST);
VisibleState := False;
GxOtaClearSelectionOnCurrentForm;
while (CompHandle <> 0) do
begin
WindowClass := GetWindowClassName(CompHandle);
if AnsiSameText(WindowClass, NonVisualClassName) then
begin
if not FirstCompFound then
begin
VisibleState := not IsWindowVisible(CompHandle);
FirstCompFound := True;
end;
if VisibleState then
ShowWindow(CompHandle, SW_SHOW)
else
ShowWindow(CompHandle, SW_HIDE);
end;
CompHandle := GetWindow(CompHandle, GW_HWNDPREV);
end;
end;
in the unit GX_HideNonVisualComps.Pas.
As written, it toggles the visibility of all the non-visual components on the
target form, but looking at the code of the ToggleNonVisualVisible method it looks like it
ought to be possible (but I have not tried) to adapt it to operate on a selected component class and
force instances of the class to a non-visible state. Once you have done that, you would probably
need to experiment with how and when to invoke the method at design-time; if I was doing it, I would probably start
with somewhere like the target component's Loaded method.
(I would feel more comfortable posting this "answer" as a comment but obviously it would be too long)
I have thought about this. A Non Visual Component does not do any painting, in a Windows environment (like the IDE) it has no Window, and therefore cannot influence how the IDE chooses to render it.
One approach would be to derive from TWinControl, making your component a Visual Component, and then to ensure that it is not drawn. Try setting the positioning properties to be non-published, and when you are parented, always set your position outside the parent window. This means that your control is always clipped and never painted.
I haven't tried this, but I can see no reason why it wouldn't work.
You can also use this approach to have an apparently non visual component that renders information in the IDE at designtime, but not at runtime.

Programmatically adding columns to a TdxDBGrid (Expressquantumgrid by Devexpress)

With a customer I'm stuck developing for this very old version (2.1) of ExpressQuantumGrid by DevExpress. In Delphi 4. I can't find any documentation about it.
Basically I just need to create a bunch of TdxDBGridMaskColumn and "insert" them into the grid (TdxDBGrid) at runtime. From the code completion pop-up I can't figure out how.
Thanks!
We have an old app that uses Delphi 5 and DevExpress v3, the code might not be identical but should get you started.
A function that can create a column of any type (TdxDBDateColumn for example):
function CreateColumn(const aField: string; aColClass: TdxDBTreeListColumnClass): TdxDBTreeListColumn;
var
begin
Result := dxGrid.CreateColumn(aColClass);
Result.Name := dxGrid.Name + aField;
TdxDBGridColumn(Result).DisableFilter := True;
TdxDBGridColumn(Result).DisableGrouping := True;
TdxDBGridColumn(Result).Alignment := taRightJustify;
TdxDBGridColumn(Result).FieldName := aField;
TdxDBGridColumn(Result).Caption := aField;
TdxDBGridColumn(Result).Width := 70;
end;
Then you can call this function like so:
NewColumn := CreateColumn('Username', TdxDBGridColumn);

How do I refer to components created at runtime rather than in the form designer?

i have a little problem. I'm trying to create in Delphi7 a list of components at run-time and to resize them with form's .OnResize event but no use... i can't figure out how to do it.
Here's my code:
procedure TForm1.Button1Click(Sender: TObject);
var
//ExtCtrls
panel: TPanel;
memo: TMemo;
splitter: TSplitter;
list: TListBox;
begin
panel := TPanel.Create(Self);
list := TListBox.Create(Self);
splitter := TSplitter.Create(Self);
memo := TMemo.Create(Self);
with panel do
begin
Parent := Form1;
BevelOuter := bvNone;
Top := 12;
Left := 12;
Height := Form1.Clientheight - 24;
Width := Form1.Clientwidth - 24;
end;
with list do
begin
Parent := panel;
Align := alClient;
Top := 0;
Height := panel.Height;
end;
with splitter do
begin
Parent := panel;
Top := 0;
Width := 12;
Align := alLeft;
end;
with memo do
begin
Parent := panel;
Top := 0;
Left := 0;
Width := round(panel.Width / 4) * 3;
Height := panel.Height;
Align := alLeft;
end;
end;
Do i have to somehow register their names in order to use them in form's event? Or maybe, to create a class and include them?
Any kind of help is really appreciated! Thank you in advance.
Your variables are local to the procedure where they are created so you can't refer to them using those variables when outside that procedure. The solution is to make them fields of the form class.
type
TForm1 = class(TForm)
procedure FormResize(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
FPanel: TPanel;
FMemo: TMemo;
FSplitter: TSplitter;
FList: TListBox;
end;
Then your FormResize event handler can refer to them.
procedure TForm1.FormResize(Sender: TObject);
begin
if Assigned(FPanel) then
begin
...
end;
end;
Don't forget to remove the local variables from Button1Click and use the fields instead.
procedure TForm1.Button1Click(Sender: TObject);
begin
FPanel := TPanel.Create(Self);
...
end;
Although David's answer is also very correct, I thought I would take a moment and go into some more detail. By the looks of it, you seem to be very new with Delphi. There is a very common issue with beginners, which David doesn't address in his answer, pertaining to creating and freeing these objects. Any and every time you ever call 'Create' on a class, at some point, when you're done with it, you have to also 'Free' that class. Failure to free anything will result in a memory leak, and no one wants that. Freeing is just as simple as creating - until you get into the subject of keeping a list of objects (which you don't need right now).
Let's say you wanted to create a text box (TEdit) control and place it in the center of your form. Now first of all, the Delphi IDE allows you to simply drop these controls in your form, just making sure you know. You don't necessarily need to create/free them yourself, unless there's some special scenario. But doing this is dangerous. For the sake of this example, we're assuming that this TEdit control will be there for the entire duration of your application.
First, you need to declare a variable somewhere for this control. The most reasonable place for this is inside the class where it will be used (in this case, your form which we'll call Form1). When working with variables (aka Fields) in your form, make sure you do not put anything above the private section. Everything above private is intended for auto-generated code by Delphi for anything which has been dropped (and is visual) in your form. Otherwise, any manually created things must go under either private or under public. The public area would be a good place for your control...
type
TForm1 = class(TForm)
private
public
MyEdit: TEdit;
end;
Now that it's declared, we have to create (and free) it. It's a good practice that any and every time you ever create something, that you immediately put the code to also free it before you continue working. Make an event handler for your form's OnCreate and OnDestroy events...
procedure TForm1.FormCreate(Sender: TObject);
begin
MyEdit:= TMyEdit.Create(nil);
MyEdit.Parent:= Self;
MyEdit.Left:= (ClientWidth div 2) - (Width div 2);
MyEdit.Top:= (ClientHeight div 2) - (Height div 2);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
if assigned(MyEdit) then MyEdit.Free;
end;
If this object is not created (before creation or after destruction), then you will get an "Access Violation" when trying to use it. This is because your application tries to access an area of the computer's memory which is not allocated or not matching with the type you meant to get.
Well, that's the basics to fix your scenario. However one more thing to show you. Suppose you need to just create an object for a short time, for the duration of a procedure. There's a different approach for this. In your code above, you declared your variable directly within the procedure. This example will show you when it is necessary to do this...
procedure TForm1.Button1Click(Sender: TObject);
var
MyObject: TMyObject;
begin
MyObject:= TMyObject.Create;
try
MyObject.DoSomething;
Caption:= MyObject.GetSomething;
finally
MyObject.Free;
end;
end;
You see, as long as MyObject will only be used in this one call to this procedure, then you can declare it here. But if the object is expected to stay in memory after this procedure is over and done with, then things get more complicated. Again, in your case, stick with putting this in the form's class until you're more familiar with dynamically creating objects.
A final note, as mentioned above, you do have the ability to place the TEdit control directly on your form in design-time without writing your own code. If you do this, you need to remember NOT to try to create or free these ones. This is also the case when Delphi will automatically put the code above the private section - is when there's something which you're not supposed to play with.
I don't think I am eligible to "comment", so I'm phrasing this as an "answer". If you want to resize your runtime components when their parent changes size, take a good look at the Anchors property. It can save you a lot of work.

Delphi : Globally change ADO command timeout

We have a HUGE Delphi 2005 application with LOTS of ADO components (TADODataset, TADOStoredPRoc, TADOCommand...) spread on hundreads of forms. All of them are connected to a SINGLE TADOConnection.
Most of these components have their CommandTimeout property set to the default (30s) but a few have it set to 5 minutes (300s) and some are set to never timeout (0s).
I'd like to be able to globally change this setting for all ADO components application-wide. I'd prefer to do it programmatically at runtime so that I could tweak the timeouts on a per-installation basis if I need to.
I was hoping I could find a global event on the connection when an ADO component is created/attached, where I could tweak the commandtimeout, or hack my way into injecting my code in the components themselves, but came up blank.
I don't want to create decendants because I'll have to search/replace trought all the components, and if I ever forget to use the descendants instead of the regular ADO components my timeout wont follow the rest of the application.
Anybody has an idea how we could do this ?
If all of you ADO components are placed on a form, you can iterate over all forms using the Screen.Forms and Screen.FormCount properties. For each form iterate over its ComponentCount/Components property and check for TADOCommand, TADODataSet, TADOQuery, TADOStoredProc and TADOTable. Then you can set the timeout as you wish. Of course, if you create forms dynamically you have to take this into account separately.
The following code may guide you.
procedure SetADOTimeout(ATimeout: Integer);
var
cmp: TComponent;
frm: TForm;
I: Integer;
J: Integer;
begin
for I := 0 to Screen.FormCount - 1 do begin
frm := Screen.Forms[I];
for J := 0 to frm.ComponentCount - 1 do begin
cmp := frm.Components[J];
if cmp is TADOCommand then
TADOCommand(cmp).CommandTimeout := ATimeout
else if cmp is TADODataSet then
TADODataSet(cmp).CommandTimeout := ATimeout
else if cmp is TADOQuery then
TADOQuery(cmp).CommandTimeout := ATimeout
else if cmp is TADOStoredProc then
TADOStoredProc(cmp).CommandTimeout := ATimeout
else if cmp is TADOTable then
TADOTable(cmp).CommandTimeout := ATimeout;
end;
end;
end;
Greetings to all Argentinian solutions!
Just define the OnWillExecute event handler for the TADOConnection you have and write following code:
type
TCustomADODataSetAccess = class(TCustomADODataSet);
procedure TYourDataModule.ADOConnectionWillExecute(...);
var
i: Integer;
begin
for i := 0 to ADOConnection.DataSetCount - 1 do
TCustomADODataSetAccess(ADOConnection.DataSets[i]).CommandTimeout := Connection.CommandTimeout;
end;
This will set the command timeout for any query/table/stored procedure which uses your ADO connection.
According to the documentation, you can use CommandCount and Commands to locate all the open components attached to your TADOConnection.
Your problem is likely to be dynamically created forms. You'll need to find "something" to hook when a form is created and check for ADO components on that form.
If your forms descend from a custom form class, you could do this in the form's constructor or OnCreate event.
If not, you could look at TApplicationEvents and using the TApplication's OnIdle event.
Since CommandTimeout is introduced in the TCustomADODataset class, you can iterate for each form/datamodule, find a TCustomADODataset and it's descendants (ADODataset, ADOTable, ADOQuery) then set the Property.
procedure SetADOCommandTimeOut(aTimeOut: integer);
var
i, j: integer;
begin
for i:= 0 to Screen.FormCount-1 do
begin
for j:= 0 to Forms[i].ComponentCount-1 do
if Forms[i].Components[j] is TCustomADODataset then
TCustomADODataset1(Forms[i].Components[j]).CommandTimeOut:= aTimeOut;
end;
for i:= 0 to Screen.DataModuleCount-1 do
begin
for j:= 0 to Datamodules[i].ComponentCount-1 do
if Datamodules[i].Components[j] is TCustomADODataset then
TCustomADODataset1(Datamodules[i].Components[j]).CommandTimeOut:= aTimeOut;
end;
end;
Note: TCustomADODataset1 is exactly TCustomADODataset, only it has a published CommandTimeOut property :
TCustomADODataset1 = class(TCustomADODataset)
published
property CommandTimeOut;
end;
But it's only applied to forms/datamodules which are already created. If you create your forms/datamodules dynamically, then you must apply it whenever a new form/datamodule is created.
One way to do it is by override Notification in your Mainform, checking for a new a creation of form/datamodule, but this a little bit tricky since at the the creation time, all the components are not created yet. You trick it by delay it for a while using a timer (I don't know a more elegant way - just to show the idea)
Procedure TMainForm.Notification(AComponent: TComponent; Operation: TOperation);
begin
inherited;
if (Operation = opInsert) and (
((AComponent is TForm) and not (aComponent is TMainForm)) // exclude MainForm
or (AComponent is TDataModule)
) then
begin
Timer1.Interval:= 2000; // 2 seconds ?
Timer1.Enabled:= True;
end;
end;
Procedure TMainForm.Timer1Timer(Sender: TObject);
begin
Timer1.Enabled:= False;
SetADOCommandTimeOut(MyTimeOut);
end;
You create datamodule hierarchies? If so, you can use code like Uwe's answer on your patriarch form (which all other datamodules inherit).

Adding a checkbox to cxGridDBColumn (DateEdit)

I using Delphi BDS 2006 and have a DevExpress cxGridDBColumn with properties set to DateEdit and was wondering whether it is possible to add a checkbox to the displayed date time picker popup?
I am not sure that I understand what you wish to achieve. Anyway, it is impossible without creating a custom cxEditor which supports this look&feel and desired functionality.
Here is a quick hack which should help you implement this feature. However, you should handle the checkBox yourself. I have done this for the standalone editor, however, the same approach will work with the inplace editor:
procedure TForm1.cxDateEdit1PropertiesPopup(Sender: TObject);
var
AEdit: TcxDateEdit;
ACalendar: TcxPopupCalendar;
ACheckBox: TcxCheckBox;
begin
AEdit := TcxDateEdit(Sender);
if AEdit.Tag <> 1 then
begin
AEdit.Tag := 1;
ACalendar := TcxPopupCalendar(AEdit.Properties.PopupControl);
ACheckBox := TcxCheckBox.Create(Self);
ACheckBox.Parent := ACalendar.Parent;
ACheckBox.Align := alBottom;
ACheckBox.Transparent := True;
ACalendar.Parent.Height := ACalendar.Parent.Height + ACheckBox.Height;
end;
end;

Resources