Accessing StringGrid from another Form - delphi

Edit the body for more details.
I have a Form called ENP, defined in the EnpView unit. The ENP form, is created and shown from the handler event of a click a toolbar item in the Main Form (TPrincipal).
procedure TPrincipal.ENP1Click(Sender: TObject);
begin
TENP.Create(self).Show();
end;
Enp form knows (in public declaration) clearGrid() message.
ENP Form have a TStringGrid called StringGrid. And a button called "Add". When click "Add" button other Form is created and shown: AddEnp form, defined in EnpViewAdd unit.
procedure TENP.opAgregarClick(Sender: TObject);
begin
TAddEnp.Create(self).Show();
end;
The AddEnp form, have any TEdits. The values of the inputs must be adding in the EnpView.StringGrid.
I try this:
implementation
uses
EnpView, Main;
procedure TAddEnp.AgregarClick(Sender: TObject);
begin
{ Agrego el nodo xml }
Globals.Xml.agregarMuestra(muestra.Text);
Globals.Xml.insertEnp(muestra.Text,golpes.Text,metros.Text);
{ Send messages to EnpView Form }
ENP.clearGrid();
ENP.populateGrid();
end;
ClearGrid messages fails in line 1, with access violation:
procedure TENP.clearGrid();
begin
Self.StringGrid.RowCount := 2;
Self.StringGrid.Rows[1].Clear();
end;
The clearGrid methods works if is send within the class. Any ideas ?.

Create a property named, for instance, ENPForm in TAddENP and assign the ENP form just after creating it. Declare it as follows:
TAddENP = class(TForm)
private
FENPForm: TENP;
// all of your already existing declarations
public
property ENPForm: TENP read FENPForm write FENPForm;
end;
Now that you have a possible reference to ENP form, you can use it as you like.
During the creation of TAddENP form, do as follows:
procedure TENP.opAgregarClick(Sender: TObject);
var
addForm: TAddENP;
begin
addForm := TAddEnp.Create(Self);
addForm.EnpForm := Self;
addForm.Show;
end;
Now you created the second form and gave it a secure reference to the first one. They can now talk to each other safely.
I advise you to avoid having one form operating other oneĀ“s components, because this increses the dependency between them. Instead, declare public methods to do that, so the forms will depend on their interfaces, not their implementations.
I hope this helps.

From your question (I added code-style to make it more clear):
I have a Form called ENP, defined in the EnpView unit. The ENP form,
is created and shown from the handler event of a click a toolbar item
in the Main Form (TPrincipal).
procedure TPrincipal.ENP1Click(Sender: TObject);
begin
TENP.Create(self).Show();
end;
This does nothing with your ENP form variable.
You create an instance of the TENP form class and show it using Show, but the ENP variable is not assigned.
You cannot assign the instance to the ENP variable, as each button click creates a new instance (so you have multiple instances of TENP) around.
Then you create a convoluted depedency of a TAddEnp instance and the (never assinged ENP variable).
You do this by creating a TAddEnp instance (why TAddEnp here, and not TAddENP?) show it using Show (giving the users the opportunity to go back to the TENP instance and again click on the opAgregar button to create more instances of TAddEnp):
procedure TENP.opAgregarClick(Sender: TObject);
begin
TAddEnp.Create(self).Show();
end;
Followed by having the TAddEnp depend on the ENP variable:
procedure TAddEnp.AgregarClick(Sender: TObject);
begin
//...
ENP.clearGrid();
ENP.populateGrid();
end;
This will indeed fail:
ClearGrid messages fails in line 1, with access violation:
procedure TENP.clearGrid();
begin
Self.StringGrid.RowCount := 2;
Self.StringGrid.Rows[1].Clear();
end;
The reason is that ENP is not assigned (by default it will be nil), so inside clearGrid, the Self will also be nil.
Solutions you could implement
Keep a single instance of TENP and TAddEnp around, using ShowModal to force modality to prevent the user from clicking on the same buttons multiple times.
Keep your existing Show behaviour, but binding each TAddEnp instance to the TENP instance it was created from.
For the first solution, your code will become this:
procedure TPrincipal.ENP1Click(Sender: TObject);
begin
ENP := TENP.Create(Application);
ENP.ShowModal();
ENP.Release();
ENP := nil;
end;
and
procedure TENP.opAgregarClick(Sender: TObject);
begin
AddEnp := TAddEnp.Create(Application);
AddEnp.ShowModal();
AddEnp.Release();
AddEnp := nil;
end;
The second will take more effort, as you need to prevent the use of the existing variables.
Delete the ENP variable.
Delete the AddENP variable.
Fix the self -> Self in the methods
procedure TPrincipal.ENP1Click(Sender: TObject);
begin
TENP.Create(Self).Show();
end;
and
procedure TENP.opAgregarClick(Sender: TObject);
begin
TAddEnp.Create(Self).Show();
end;
Here add the dependency:
procedure TAddEnp.AgregarClick(Sender: TObject);
var
ENP: TENP;
begin
//...
ENP := Owner as TENP; // Owner will be of type TENP because of the Create(Self) in the ENP1Click method
ENP.clearGrid();
ENP.populateGrid();
end;

Related

Cannot access a VCL component in a procedure called from FormCreate on a modal form (i.e. directly after opening the form)

Running into a runtime error when I open a modal form (Form2) that, on create, calls another procedure that does somehting with a VCL component. The following program demonstrates the issue.
This is the code on the modal form:
procedure DoLabel;
begin
form2.Label1.Caption := 'A';
end;
procedure TForm2.FormCreate(Sender: TObject);
begin
DoLabel;
end;
This compiles well. However, the program crashes on opening the modal form. The debugger says: access violation at address xxxx. It is probably a basic error, but what am I doing wrong? And how to solve this?
You are using the global Form2 variable, which has not been assigned yet (or at all) while the TForm2 object is still being constructed.
You need to change your code to not rely on that variable during construction (and preferably remove it entirely, unless TForm2 is auto-created at startup, which it does not sound like it is).
For instance, pass the Form's Self pointer, or even its TLabel pointer, as an input parameter to DoLabel(), eg:
procedure DoLabel(ALabel: TLabel);
begin
ALabel.Caption := 'A';
end;
procedure DoFormStuff(AForm: TForm2);
begin
// use AForm as needed...
end;
procedure TForm2.FormCreate(Sender: TObject);
begin
DoLabel(Label1);
DoFormStuff(Self);
end;
Though, in this case, it would make more sense to have DoFormStuff(), and possible DoLabel() too, be a member of the TForm2 class instead of free functions:
procedure TForm2.FormCreate(Sender: TObject);
begin
DoLabel;
DoFormStuff;
end;
procedure TForm2.DoLabel;
begin
Label1.Caption := 'A';
end;
procedure TForm2.DoFormStuff;
begin
// use Self as needed...
end;

How to invoke a procedure created inside the New Component during the implementation

I have created and implemented a New Component and inside this created component, there is a procedure InitCombo that needs to be invoked in the implementation.
How will I do that?
Here's the procedure InitCombo inside the New Component:
procedure TNewComponent.InitCombo; //TComboBox ancestor
begin
FStoredItems.OnChange := nil;
StoredItems.Assign(Items);
AutoComplete := False;
FStoredItems.OnChange := StoredItemsChange;
doFilter := True;
StoredItemIndex := -1;
end;
Here's my attempt to invoke but returns an error message:
procedure TfrmMain.FormActivate(Sender: TObject);
begin
TNewComponent.InitCombo;
end;
Error Messages
[dcc32 Error] makeasale_u_v1.pas(84): E2076 This form of method call only allowed for class methods or constructor
Please note that compiling, building, the installation went well and it is working. Except only on how to invoke the procedure inside the component?
Based only on the following portion of the first paragraph of your question
there is a procedure InitCombo that needs to be invoked in the implementation.
It appears you're confused about at least a couple of things about writing components.
First, you initialize the component properties either in its constructor to initialize things, or in an overridden Loaded method, which is called after the component is streamed in from the .dfm file where the component was used. Note that Loaded changes should not touch properties or events that the user can set in the Object Inspector, because doing so will prevent the user's settings from being used.
constructor TNewComponent.Create(AOwner: TComponent);
begin
inherited;
// Do your stuff here
end;
procedure TNewComponent.Loaded;
begin
// Do your stuff here
end;
Second, events that are published (that can be seen in the Events tab of the Object Inspector) belong to the programmer that is using the component, not the component author. Never do anything to those event handlers. Your component should never touch those events except to call them if the end user has assigned a handler. So the code below is absolutely incorrect, because the OnChange event belongs to the user of your component, not your component code:
procedure TNewComponent.InitCombo; //TComboBox ancestor
begin
FStoredItems.OnChange := nil;
...
FStoredItems.OnChange := StoredItemsChange;
end;
If you absolutely must do this, you need to do it properly by saving any event handler the end user has assigned, and restore it afterward:
procedure TNewComponent.InitCombo;
var
OldOnChange: TNotifyEvent;
begin
OldOnChange := Self.OnChange;
// Do your stuff here
Self.OnChange := OldOnChange;
end;
Third, unless you're using a class procedure or class function, you cannot call a method on a class itself (in other words, you cannot use TNewComponent.DoSomething). You call methods or access properties on an instance of the component itself. In your component code, that would be done by using Self, which refers to the current implemented component, as in Self.DoSomething.

get method as text

I need to save and load some properties to a database and I am stuck with this.
I have a form with several methods and a button. button.onclick event is assigned to one of the form's methods.
I need to get the name of the assigned method as string (just like Object inspector "form1.proc1") and save it to the database. Later I need to get the method name from the database and assign button.onclick to the corresponding form's method.
Is this possible at all?
Form1 = class(TForm)
...
procedure proc1(Sender: TObject);
procedure proc2(Sender: TObject);
procedure proc3(Sender: TObject);
Button1.OnClick = readMethodNameFromDatabase;
...
saveMethodToDatabase(Button1.OnClick);
You can obtain a method, given its name, like this:
function TForm1.MethodFromName(const Name: string): TNotifyEvent;
begin
TMethod(Result).Data := Self;
TMethod(Result).Code := MethodAddress(Name);
if TMethod(Result).Code=nil then
raise Exception.CreateFmt('Count not find method named %s', [Name]);
end;
This is the mechanism that the RTL uses when reading your .dfm files. It relies on the method being published.
You can call it like this:
Button1.OnClick := TNotifyEvent(MethodFromName('Button1Click'));
Naturally you'd substitute a database read in the final code.
As for the second part of your question, you can get the name of an event handler with this code:
MethodName(#Button1.OnClick);

How to keep Modal Dialog on top of Dynamic Created Form? (CreateParams - overridden)

I am dynamically creating a Form that overrides the CreateParams so that I can have it displayed on the TaskBar. From the dynamically created Form, I am calling a TColorDialog but once it is displayed my Form will go under the MainForm with the ColorDialog on top of that.
After the ColorDialog is closed the dynamic Form will return back over the MainForm.
I see that on the ColorDialog Execute method there is a Handle that can be passed but I am not sure if I am on the right track with that?
If I click under the Dialog on the MainForm it will flash but how can I have the dynamically created Form "own" this Dialog with the MainForm at the back?
I create the Form like this:
procedure TMain.Button1Click(Sender: TObject);
var
SEMArcF: TWriteSEMArcFrm;
begin
SEMArcF := TWRiteSEMArcFrm.Create(nil);
SEMArcF.Show;
end;
and it is freed on the OnClose Event:
procedure TWriteSEMArcFrm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action := caFree;
end;
I am overriding the CreateParams like this:
procedure TWriteSEMArcFrm.CreateParams(var Params: TCreateParams);
begin
inherited;
if (FormStyle = fsNormal) then begin
Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW;
Params.WndParent := GetDesktopWindow;
end;
end;
and to show the ColorDialog I either create it or just have a TColorDialog Component on the Form, either way will result in the same. I want it to be owned by the dynamic Form.
EDIT
I now add:
Application.ModalPopupMode := pmAuto;
The full code:
procedure TWriteSEMArcFrm.btnBackColourClick(Sender: TObject);
var
ColorDlg: TColorDialog;
begin
Application.ModalPopupMode := pmAuto;
ColorDlg := TColorDialog.Create(nil);
try
if ColorDlg.Execute then
re.Color := ColorDlg.Color;
finally
ColorDlg.Free;
end;
end;
This works fine but could there be any unusual behaviour by setting this?
Thank you
Chris
TColorDialog derives from TCommonDialog, which has two overloaded versions of Execute() available - the legacy parameterless version that has existed for years, and a newer overload that takes a parent HWND as an input parameter. You are likely calling the former. That overload uses the Handle property of the currently active TForm (only if the TApplication.ModalPopupMode property is not set to pmNone), falling back to the Handle of the MainForm if needed. If you want more control, you should call the other overload directly instead, then you can pass the dynamic form's Handle property as the parameter value.

How to check if a child window exists?

I have a main form (MainForm) and a MDI child window (TFormChild).
I want to create multiple TFormChild forms, but the first one must behave in a certain way so I need to detect if a TFormChild window already exists.
I use this code but it is not working:
function FindChildWindowByClass(CONST aParent: HWnd; CONST aClass: string): THandle;
begin
Result:= FindWindowEx(aParent, 0, PChar(aClass), NIL);
end;
I call it like this:
Found:= FindChildWindowByClass(MainForm.Handle, 'TFormChild')> 0;
In a form, you can refer to the MDIChildCount and MDIChildren properties.
for example :
var
i: integer;
begin
for i:= 0 to MainForm.MDIChildCount-1 do
begin
if MainForm.MDIChildren[i] is TFormChild then
...
end;
...
end;
Call it like
Found:= FindChildWindowByClass(MainForm.ClientHandle, 'TFormChild')> 0;
MDI child windows are children of the 'MDICLIENT', ClientHandle property of TCustomFrom holds the handle.
The best way to accomplish this is to have the form you want to open actually check to see if it already exists. To do so your, form must declare a class procedure. Declared as a class procedure, the proc can be called regardless of whether the form exists or not.
Add to your form's public section
class procedure OpenCheck;
then the procedure looks like this
Class procedure TForm1.OpenCheck;
var
f: TForm1;
N: Integer;
begin
F := Nil;
With Application.MainForm do
begin
For N := 0 to MDIChildCount - 1 do
begin
If MDIChildren[N] is TForm1 then
F := MDIChildren[N] as TForm1;
end;
end;
if F = Nil then //we know the form doesn't exist
//open the form as the 1st instance/add a new constructor to open as 1st
else
//open form as subsequent instance/add new constructor to open as subsqt instance
end;
Add Form1's unit to your mdiframe's uses clause.
To open the form, call your class procedure, which in turn will call the form's constructor.
TForm1.OpenCheck;
One word of warning using class procedures, do not access any of the components/properties of the form. Since the form does not actually have to be instantiated, accessing them would produce an access violation/that is until you know F is not nil. Then you can use F. to access form components/properties.

Resources