FindComponent Not Finding Components Created at Runtime - delphi

I use Delphi 7 with a number of third party components. My main stub application loads a number of DLLs, which are various modules like creditors, debtors, purchase orders, and so on.
I have an issue with FindComponent(). 99% of the time, it works how it should. But not for the code below.
I was trying to create a form reports, where I keep all the details of the reports selection criteria in a table, and then create the criteria on the fly. In theory, it should work perfectly, but for some reason after creating the components, FindComponent() cannot find them.
try
for i := gbSelectionCriteria.ComponentCount - 1 downto 0 do begin
ShowMessage(gbSelectionCriteria.Components[i].Name);
gbSelectionCriteria.Components[i].Free;
end;
// The above loop to remove the components from the groupbox works fine
// Creating the components works
fSysData.tbSelectionCriteria.First;
while not fSysData.tbSelectionCriteria.EOF do begin
case fSysData.tbSelectionCriteriaComponentType.AsInteger of
1 : begin // TMyAdvEdit
with TMyAdvEdit.Create(gbSelectionCriteria) do begin
Visible := False;
Parent := gbSelectionCriteria;
Name := fSysData.tbSelectionCriteriaName.AsString;
Left := fSysData.tbSelectionCriteriaLeft.AsInteger;
Top := fSysData.tbSelectionCriteriaTop.AsInteger;
Width := fSysData.tbSelectionCriteriaWidth.AsInteger;
LabelCaption := fSysData.tbSelectionCriteriaCaption.AsString;
LabelPosition := AdvEdit.lpLeftCenter;
LabelAlwaysEnabled := True;
LabelTransparent := True;
EditType := MyEditType[fSysData.tbSelectionCriteriaDataType.AsInteger];
Text := '';
OnClick := GetClickEvent(fSysData.tbSelectionCriteriaOnClickEvent.AsString);
OnDblClick := GetClickEvent(fSysData.tbSelectionCriteriaOnDblClickEvent.AsString);
OnKeyPress := GetKeyPressEvent(fSysData.tbSelectionCriteriaOnKeyPressEvent.AsString);
Visible := True;
// at this point findComponent finds nothing
if FindComponent(Name) <> nil then
ShowMessage(Name+' Created');
end;
edEdit.OnClick := GetClickEvent(fSysData.tbSelectionCriteriaOnClickEvent.AsString);
edEdit.OnDblClick := GetClickEvent(fSysData.tbSelectionCriteriaOnDblClickEvent.AsString);
edEdit.OnKeyPress := GetKeyPressEvent(fSysData.tbSelectionCriteriaOnKeyPressEvent.AsString);
edEdit.Visible := True;
if FindComponent(edEdit.Name) <> nil then
ShowMessage(edEdit.Name+' Created');
end;
2 : begin
end;
3 : begin
end;
4 : begin
end;
5 : begin
end;
6 : begin
end;
7 : begin
end;
8 : begin
end;
end;
fSysData.tbSelectionCriteria.Next;
end;
if fSysData.tbSysReports.Locate('ReportID', TAdvOfficeRadioButton(Sender).Tag, []) then begin
ReportData.ReportID := TAdvOfficeRadioButton(Sender).Tag;
ReportData.RepName := fSysData.tbSysReportsReportName.AsString;
ReportData.RepTitle := fSysData.tbSysReportsReportTitle.AsString;
ReportData.RepModule := fSysData.tbSysReportsModule.AsString;
ReportData.RepOrientation := fSysData.tbSysReportsReportOrientaton.AsString;
ReportData.RepPageIndex := fSysData.tbSysReportsCriteriaPageIndex.AsInteger;
end;
finally
end;
The Process of the reports is:
User clicks a button
Radio buttons are created from the button click
User clicks a radio button
Report criteria is created from the radio button click
User enters data or DblClicks to select data from a list.
User Clicks Preview button to view Report - this is where FindComponent fails and returns nil..
All the code worked before when I had created all the criteria at design time, then added the code above.
The code below is part of what needs to be added to the query to retrieve the data for the report:
if Length(TMyAdvEdit(FindComponent('edQuoteReference')).Text) > 0 then
qryTempTable.SQL.Add(' and q.UserReference = "' + TMyAdvEdit(FindComponent('edQuoteReference')).Text + '"');
This is the first time FindComponent() fails and goes no further.
I have tried various ways to create the components, but each of them results in an Access Violation because the component is nil.
I have looked everywhere, and tried everything I can think of, for a solution to this problem.

FindComponent searches for components owned by the subject of the method call. You call FindComponent on the form, and so look for the component amongst those components owned by the form. But the control you search for is not owned by the form, it is owned by gbSelectionCriteria, which is what you passed to the control's constructor as the Owner argument.
If you wish to use FindComponent in the way you do you therefore need to make the form be the owner of the controls that you create. Then when you call FindComponent on the form, it can find the control because it is the owner. Pass Self to the control's constructor to make this come to pass:
TMyAdvEdit.Create(Self)
I'm having to make some reasonably large guesses here. Perhaps this code actually resides in a data module rather than a form. But the essential principle will be as I say.

Firstly I do apologize if this is in the wrong spot..
Thanks for the response and the answer, I have been doing this for a lot of years and I can't believe I missed something so small.
this,
if FindComponent(Name) <> nil then
should have been this,
if gbSelectionCriteria.FindComponent(Name) <> nil then
I don't normally use with, it was just one way to test create the component.
I set the components visibility to false before and then to true after it is created to stop flicker as it creates.
Thanks again..

Related

Delphi DBCombobox in Delphi XE app, only shows values if in .Items and not the actual nonconforming Datafield value

I've encountered a very strange DBComboBox problem in a master/detail app using Access via ADO. If you have a DBComboBox (.Style=csDropDown) containing a list of items and you enter some text that doesn't exist in the list, the value in the table's DBComboBox field won't appear when navigating back to that record. I've used the DBNavigator.OnClick code below to attempt to resolve this problem but it only works if the first record in the table contains a value not in the list. When you change the value of the DBComboBox in the first record to one that is in the list, no nonconforming items will appear in the DBComboBox text. Has anyone found a solution to this?
procedure TForm1.DBNavigator1Click(Sender: TObject; Button: TNavigateBtn);
var
SavePlace : TBookmark;
begin
if (DBComboBox1.Text='') then begin
SavePlace := TADODataSet(DBNavigator1.DataSource.DataSet).GetBookmark;
TADODataSet(DBNavigator1.DataSource.DataSet).Requery;
TADODataSet(DBNavigator1.DataSource.DataSet).GotoBookMark(SavePlace);
TADODataSet(DBNavigator1.DataSource.DataSet).FreeBookMark(SavePlace);
end;
end;
Unfortunately I don't have XE installed, but I have made a sample project which
reproduces your problem in D7 and Seattle. The code is shown below and I think
you will find that if you follow the exact steps below, it shows that there is something
rather strange going on. Update See the bottom of the answer for a possible work-around, which I think is preferable to the code you quote in your q.
As you'll see, except for Form1 itself, all the components are created at runtime
entirely in code. This is to remove any doubt whether the behaviour is caused
by some obscure property setting (it isn't) and in case you wish to submit it
to EMBA as a bug report. For a similar reason I've used a TClientDataSet so that
the app does not depend on any external data.
Steps (please follow steps 4-7 exactly the first time you try them)
Restart the IDE and create a new project and edit the .Pas file for the main form as shown below. The reason for restarting the IDE is that I discovered that if it has been running for a long time (two days in my case) the details of the misbehaviour
of the app change slightly).
Compile and run.
The app will start with the first from in the DBGrid selected.
Type anything (an 'X' will do) into the DBComboBox, then click the Save toolbutton
on the DBNavigator.
Click the Next (>) toolbutton on the DBNavigator once only. The DBComboBox now displays
'Two'.
Click the Prior (<) toolbutton on the DBNavigator once only. The DBComboBox is now empty.
Click the Prior (<) toolbutton on the DBNavigator once only. The DBComboBox now displays
what you typed in step 4.
Close the app. Most likely the IDE debugger will catch a fault and open the CPU window.
This fault occurs on the line
DestroyWindow(FHandle);
in TApplication.Destroy. I am no Windows internals expert but I think it's likely that this is because of some corruption being caused by whatever causes the blank result in step 6. The fact that
step 7 causes the DBComboBox to correctly display what you typed makes me suspect that cause is actually
in the way the DBComboBox interacts with its FieldDataLink which connects it to the dataset.
Btw, the fact that the fault does not occur if you call DBComboBox1.Free in TForm1's FormDestroy
seems to me to confirm that the fault is related to whatever is causing your problem.
All this, and the fact that it has apparently passed unnoticed in the 25 years of Delphi, seems very strange
to me. This demo app can show up another quirk that's been lurking in the DBGrid for a similar length ot time. To see
it:
Comment out all the references to the DBComboBox and reinstate dgMultiSelect amongst the grid options in the
line that sets them. Compile and run the app.
Click in the cell in the Name column for the first row, type something and save it.
Click the Next toolbutton once. The first row does not de-select itself as it should.
AFAICT (by displaying the DBGrid's count of Bookmarks on the form's caption) this is not
because it has saved a bookmark on the first row.
While I've been writing this, a possible work-around has occurred to me, which I'll updated
this to include if I can get it to work.
Code
type
TForm1 = class(TForm)
procedure FormCreate(Sender : TObject);
private
procedure SetUpDataSet;
procedure SetUpGUI;
protected
public
ClientDataSet1 : TClientDataSet;
DBGrid1: TDBGrid;
DataSource1: TDataSource;
DBNavigator1: TDBNavigator;
DBComboBox1: TDBComboBox;
end;
[...]
procedure TForm1.SetUpGUI;
begin
ClientDataset1 := TClientDataSet.Create(Self);
DataSource1 := TDataSource.Create(Self);
DataSource1.DataSet := ClientDataSet1;
DBGrid1 := TDBGrid.Create(Self);
DBGrid1.Top := 8;
DBGrid1.Left := 8;
DBGrid1.Width := 425;
DBGrid1.Options := [dgEditing, dgTitles, dgColumnResize, dgColLines, dgRowLines, dgTabs, dgConfirmDelete, dgCancelOnExit{, dgMultiSelect}];
DBGrid1.DataSource := DataSource1;
DBGrid1.Parent := Self;
DBNavigator1 := TDBNavigator.Create(Self);
DBNavigator1.DataSource := DataSource1;
DBNavigator1.Top := 144;
DBNavigator1.Left := 16;
DBNavigator1.Parent := Self;
DBComboBox1 := TDBComboBox.Create(Self);
DBComboBox1.DataField := 'Name';
DBComboBox1.DataSource := DataSource1;
DBComboBox1.Top := 240;
DBComboBox1.Left := 16;
DBComboBox1.Parent := Self;
end;
procedure TForm1.SetUpDataSet;
var
Field : TField;
begin
// Create 2 fields in the CDS
Field := TIntegerField.Create(Self);
Field.FieldName := 'ID';
Field.FieldKind := fkData;
Field.DataSet := ClientDataSet1;
Field := TStringField.Create(Self);
Field.FieldName := 'Name';
Field.Size := 40;
Field.FieldKind := fkData;
Field.DataSet := ClientDataSet1;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
SetUpGUI;
SetUpDataSet;
// Set up DBComboBox
DBComboBox1.Style := csDropDown;
DBComboBox1.Items.Add('One');
DBComboBox1.Items.Add('Two');
DBComboBox1.Items.Add('Three');
// Next, set up the CDS
ClientDataSet1.CreateDataSet;
ClientDataSet1.InsertRecord([1, '']);
ClientDataSet1.InsertRecord([2, 'Two']);
ClientDataSet1.InsertRecord([3, '']);
ClientDataSet1.First;
end;
Possible work-around Add the following method to Form1:
procedure TForm1.ClientDataSet1AfterScroll(DataSet: TDataSet);
var
S : String;
begin
S := DataSet.FieldByName('Name').AsString;
if S <> DbComboBox1.Text then
DbComboBox1.Text := S;
Caption := IntToStr(DBGrid1.SelectedRows.Count);
end;
Then, in the SetUpGUI method, add the following immediately after the line where ClientDataSet1 is created:
ClientDataset1.AfterScroll := ClientDataSet1AfterScroll;
I have not tested this thoroughly, but it seems to work in the test conditions of the steps I've described above.

How to find what form is calling a public procedure

I have too many Forms and I have a procedure that should be running on all form when created
procedure TDM.SetupForm(Max, DisableResize,
DisableMove: Boolean; FormWidth: Integer = 0; FormHeight: Integer = 0);
var
Form: TForm;
begin
Form := ??? // How to find the what form is running this procedure?
Form.AutoScroll := True;
if Max then
begin
Form.Width := Screen.WorkAreaWidth;
Form.Height := Screen.WorkAreaHeight;
Form.Top := 0;
Form.Left := 0;
end
else
begin
if FormWidth > 0 then
Form.Width := FormWidth;
if FormHeight > 0 then
Form.Height := FormHeight;
Form.Position := poScreenCenter;
Form.Align := alCustom;
end;
if DisableResize then
DeleteMenu(GetSystemMenu(Form.Handle, False), SC_SIZE, MF_BYCOMMAND);
if DisableMove then
DeleteMenu(GetSystemMenu(Form.Handle, False), SC_MOVE, MF_BYCOMMAND);
Form.BorderIcons := [biSystemMenu];
if Form.Height > Screen.WorkAreaHeight then
Form.Height := Screen.WorkAreaHeight;
if Form.Width > Screen.WorkAreaWidth then
Form.Width := Screen.WorkAreaWidth;
Form.ShowHint := True;
Form.OnClose := CloseFormAction;
end;
I call this Procedure on FormCreate event
How can I find what form is calling this procedure and use it inside same procedure without passing it as parameter?
I call this procedure on FormCreate event
It seems to me you don't actually need to know the "Last Created Form", but rather which form is currently being created, which you want to call this code for. If that is the case, simply add a TForm parameter to this procedure instead of declaring a variable and trying to obtain it from elsewhere...
procedure TDM.SetupForm(Form: TForm; Max, DisableResize,
DisableMove: Boolean; FormWidth: Integer = 0; FormHeight: Integer = 0);
begin
...Use the `Form` parameter...
Then you would pass Self into this whenever you call it from FormCreate...
DM.SetupForm(Self, ....
Ultimately, this sort of thing is best accomplished by creating a base form first, and then inheriting all the rest of your forms from this base. Such code would be implemented in the base form's constructor, and then you wouldn't have to explicitly call it from each and every form you wish to apply it to. However, it seems you already have many forms written and this would require modifying all of your existing code to consider the base form. Such design should be done from the beginning of development.
I must also note that putting UI code of such nature into a data module is not the right practice. A data module's purpose is to be disconnected from the UI. That's why it's not actually a visible form, but a non-visual-component-only solution. It's best to put such code in independent units for that purpose, such as MyApp.UICommon.pas.

Adding OnHint-like functionality to a third party component

Due to cosmetic reasons the app I'm maintaining uses an ancient component from DevExpress that pre-dated their current grid controls (TdxMasterView if you're interested). What I want to be able to do is to have a tooltip that displays the current cell's text, however this component does not have an OnHint event exposed.
I have been able to get the functionality that I need using the MouseMove event, however as the code requires it to translate the mouse cursor into a cell and then retrieve the contents I think this is too much code for an event that is fired so frequently (although it doesn't feel TOO laggy in operation).
The component itself is derived from TCustomControl, so has the basic Hint and ShowHint properties, however what I think I want is to be able to either expose or add an event that will fire only when the hint bubble will be shown (i.e. the OnHint event or equivalent). All I need to get the correct text is the X and Y coordinates of the mouse. This appears to be tied up in TControlAction, however I'm not entirely sure how this works as it's not immediately clear from a first glance at the code and it's not exposed by the component.
Does anyone have any example code where they have achieved something similar? I have access to the underlying source, so can modify it a bit if needed (DevExpress will never release an update to this code, so normal risks of doing this don't really apply), but I'd prefer to work by helper function, windows message or some sort of decorator if possible.
I suppose the other option is to have a timer to enable/disable the event, but that seems a bit of a sucky (if simple) solution.
Implement procedure CMHintShow(var Message: TCMHintShow); message CM_HINTSHOW; in your code.
procedure TMyComponent.CMHintShow(var Message: TCMHintShow);
var
CellIdx: Integer;
Handled: Boolean;
HintStr: string;
LHintInfo: PHintInfo;
begin
Message.Result := 1; // Don't show the hint
if Message.HintInfo.HintControl = Self then
begin
with Message.HintInfo.CursorPos do
begin
CellIdx := ImageAtPos(X, Y);
end;
Handled := False;
HintStr := '';
if Assigned(FOnGetHint) then
FOnGetHint(Self, CellIdx, HintStr, Handled);
LHintInfo := Message.HintInfo;
if (CellIdx <> -1) then
begin
if not Handled then
HintStr := Hint;
LHintInfo.CursorRect := GetCellRect(CellIdx);
Handled := True;
end;
if Handled then
begin
LHintInfo.HintStr := HintStr;
Message.Result := 0; // Show the hint
end;
end;
end;

Change Memo1.Font of all Dynamically Created Forms on Delphi

I create many forms on runtime using
Application.CreateForm(TForm2, Form2);
Form2.Show;
Now I need to change the Memo1.Font of them at once.
Form2.Memo1.Font:=newfont;
But only the latest created form's Memo1.Font changes. How can I change all?
I guess you're calling Application.CreateForm(TForm2, Form2); multiple times which reassigns the newly created form to your global Form2 variable, so later when you refer to Form2 you're referring to the last-created instance.
To access all instances of TForm2 in your application, you can use Screen.Forms property:
for I := 0 to Screen.FormCount - 1 do
if Screen.Forms[I] is TForm2 then
TForm2(Screen.Forms[I]).Memo1.Font := ...
The reason for this behaviour is, that component names should be unique,
You can not address multiple components by just one name!
In this case You will have to iterate through all components to find all TMemos.
This could look something like
var i,j: integer;
begin
// first find all Forms in Application
for i:=0 to Application.ComponentCount - 1 do
begin
if Application.Components[i] is TForm then
begin
with (Application.Components[i] as TForm) do
begin
// now find all TMemos and change the font
for j:=0 to ComponentCount-1 do
begin
if (Components[j] is TMemo) and (Components[j].Name = 'Memo1') then (Components[j] as TMemo).Font.Name := 'Arial';
end;
end;
end;
end;
This is a very general approach and You can easily adapt it to other components within Your application.
Another approach would be to memorize all created TMemos in an Objectlist when they are created the first time (then You could easily iterate the object list and change TMemos properties) but without knowing more about Your implementation it is hard to give a good advice.

Why don't a ClientDataSet descendant's fields appear at design time?

I'm trying to write a component that inherits from TClientDataset. On the create of the component in design time I want to instantiate a list of common fields that are used within my framework. The code below will execute without errors and the field will appear at run time but not design time. Can anyone help me? I'm sure its something trivial!
{ InheritedClientDataset }
constructor InheritedClientDataset.Create(AOwner: TComponent);
var
Field : TField;
begin
inherited;
Field := TField.Create(self);
Field.Name := 'ATestField';
Field.FieldName := 'Test';
Field.SetFieldType(ftInteger);
//Field.DataType := ftInteger;
Field.Size := 0;
Field.FieldKind := fkData;
self.Fields.Add(Field);
end;
Try creating your field using its fieldtype. For example, TIntegerField.
MyField := TIntegerField.Create(Self);
MyField.FieldName := 'MyFieldName';
MyField.DataSet := Self;
MyField.Name := Self.Name + '_' + MyField.FieldName;
That should work.
It will be available to controls but not the Fields Editor.
Totally untested, but you should probably be adding to FieldDefs instead:
with FieldDefs.AddFieldDef do
begin
DataType := ftInteger;
Name := 'Field1';
end;
with FieldDefs.AddFieldDef do
begin
DataType := ftString;
Size := 25;
Name := 'Field2';
end;
You may also have to add a call to CreateDataSet after the FieldDefs are added:
// After above code...
inherited CreateDataSet;
I have a feeling that in cases like this, you might be going against the design intention of the VCL component designtime. Fields are typically defined by someone who places a table object onto a data module, then set the dataset properties to a particular SQL or other table and selects the fields from that table, rather than a component with a fixed set of fields, which might be something problematic for the current architecture to support, even though you have a fix, are you sure there aren't problems with that approach?
Have you thought about an alternative approach? (Write a component with a public property that allows it to be connected to a dataset or datasource and put all your framework logic in that component). Leave the dataset class alone.
Do you really NEED to do an "IS A" relationship in OOP terms, or would your code actually be cleaner if you considered "HAS A link to a dataset" instead?

Resources