tags:

views:

201

answers:

4

I have written an MDI based application, in which the child forms are of different types. I have now come across a situation where I need one child form to send a message to another child form, telling it to update itself. The first child form is unaware of whether the second child form is being displayed at the moment.

I had thought of having the first child form (form A) send a message to the main MDI form (form 0), which could then check the list of MDI child forms currently being displayed on the screen. If the desired form (form B) is being displayed, then the main form could send a second message to this form (form B).

Unfortunately, I haven't been able to write successfully the code which would enable the first child form to signal the main form. How can a child form send a message to its owner?

TIA, No'am

+3  A: 

The owner of a form isn't necessarily another form. The Owner property is just TComponent, which could be anything, including nil. But if the owner is a form, you can send it a message like this:

if Owner is TForm then
  SendMessage(TForm(Owner).Handle, am_Foo, 0, 0);

You might not need to know the owner, though. The MDI parent form is always the main form of the project, and the main form is always designated by Application.MainForm. Send a message to that form's handle.

SendMessage(Application.MainForm.Handle, am_Foo, 0, 0);

The list of MDI children will be in Application.MainForm.MDIChildren. Your child forms can check that list for themselves rather than have the MDI parent do it. Here's a function either of your forms can use to find instances of any MDI child class. (If the forms that want to communicate aren't MDI children, you can still use this technique, but instead of Application.MainForm.MDIChildren, search the Screen.Forms list.)

function FindMDIChild(ChildClass: TFormClass): TForm;
var
  i: Integer;
begin
  for i := 0 to Pred(Application.MainForm.MDIChildCount) do begin
    if Application.MainForm.MDIChild[i].InheritsFrom(ChildClass) then begin
      Result := Application.MainForm.MDIChildren[i];
      exit;
    end;
  end;
  Result := nil;
end;

Your first child class could use it like this:

var
  SecondChild: TForm;
begin
  SecondChild := FindMDIChild(TSecondChild);
  if Assigned(SecondChild) then begin
    SendMessage(SecondChild.Handle, am_Foo, 0, 0);
  end;
end;

When window messages are sent to windows in the same thread as the sender (which is always the case for any two VCL forms), their handlers are called immediately while the sender waits for a response. That's just like an ordinary function call, so you might wish to skip the messages and make regular functions in your form classes. Then you could use code like this:

var
  SecondForm: TSecondForm;
begin
  SecondForm := TSecondForm(FindMDIChild(TSecondForm));
  if Assigned(SecondForm) then begin
    SecondForm.Foo(0, 0);
  end;
end;
Rob Kennedy
This sounds very good, but unfortunately the forms that have to send the message don't recognise the TSecondForm type (obviously in my program it's something else). I used what I consider to be a variant of this technique: a procedure in a common module is called which iterates over the mdi child windows and sends each one the message. Only one mdi child has the message handler code so there's no possibility of having the wrong child handle the message. I'm also using a wparam value, thus allowing for the possibility that in the future there will be more messages. Thanks.
No'am Newman
I found a better solution that the one I proposed above. The forms sending the message make a simple procedure call to TMainForm(Application.MainForm).RefreshDockets. This procedure then iterates through the MDI child forms, looking for a certain type of form and then calls one of the form's methods. for i:= 0 to MDIChildCount - 1 do if MDIChildren[i] is TDoDockets then TDoDockets (MDIChildren[i]).RefreshDockets;(I apologise for not formatting correctly).
No'am Newman
If you sending code doesn't "recognize" the type, then it's because you have to **use** the second form's unit in the first unit: `uses SecondFormUnit;` (Put that in the *implementation* section to avoid "circular unit reference" errors.)
Rob Kennedy
A: 

Why not just send the message to Application.Mainform.Handle, then in the Main form loop from 0 to MDIChildcount and resend the message to each one. Then, repond to the specific message only in the form class you want. I hope this serves your needs.

José Romero
I was missing the 'Application.Mainform.Handle' part - I had tried 'Application.Handle', but the message wasn't being handled.
No'am Newman
+1  A: 

Another approach that is worth using here is to use interfaces rather than messages. The advantage is that interfaces are more specific... messages can easily be accidentally re-used, unless your very specific on where your message constants are located.

To use this model, first create a global unit (or add to an existing) the following interface declarations:

type
  ISpecificSignal = interface
    {type CTRL-SHIFT-G here to generate a new guid}
    procedure PerformSignal(Handle:Integer);
  end;

Then modify your MAIN form interface, adding following:

TYPE
  TMainForm = Class(TForm,ISpecificSignal)
    :
  private
    Procedure PerformSignal(Handle:Integer);
  end;

and in the implementation of the PerformSignal procedure looks like the following:

Procedure TMainForm.PerformSignal(Handle:Integer);
var
  i: Integer;
  Intf : ISpecificSignal;
begin
  for i := 0 to Pred(Application.MainForm.MDIChildCount) do begin      
    if Supports(Application.MainForm.MDIChild[i],ISpecificSignal,Intf) and
      (Application.MainForm.MDIChild[i].Handle <> Handle) then 
      Intf.PerformSignal(Handle);
end;

In your child form which ultimately must handle the signal, perform the same steps as you did for the main form, but change the PerformSignal to invoke the code you desire. Repeat as needed.

In the form which needs to actually start the process add the following code:

procedure DoSomething;
var
  Intf : ISpecificSignal;
begin
  if Supports(Application.MainForm,ISpecificSignal,Intf) then
    Intf.PerformSignal(Handle);
end; 

The largest advantage to this approach is your not limited to what parameters are passed (the interface can have any number of parameters, or no parameters), and it works without having to exercise the message pump.

EDIT Added Handle to avoid a situation where the existing form also needs the same notifications from other forms.

skamradt
A: 

I don't know that specific of your problem, so this might not apply to your situation.

I guess your situation is FormA edit some value that affects FormB "rendering". The way I usually deal with this kind of situation is by making the value change to trigger an event. If more than 1 component in the system needs to be modified, I use a "multicasting" event.

A simple multicaster mechanism looks like this.

TMultiCastNotifyEventReceiver = class(TComponent)
private
  FEvent : TNotifyEvent
public
  property Event : TNotifyEvent read FEvent write FEvent; 
end;

TMultiCastNotifyEvent = class(TComponent)
private
  //TList or TObjectList to keep a list of Listener.
  //Housekeeping code to make sure you don't keep reference to dangling pointers (I derived from TComponent to have access to the FreeNotification mechanis
public
  procedure DoEvent(Sender : Tobject); //Same parameters as TNotifyEvent
  function AddListener(NotifyEvent : TNotifyEvent) : TMultiCastNotifyEventReceiver
end;

That way, your formA doesn't need to know it's parent... Doesn't need to know FormB. FormB doesn't need to know FormA. Requirement for this to work though is that the FormA AND FormB must know the Value, and Value needs to know it needs to send a notification when it's modified. Usually results in better modularisation and encapsulation.

Then again, I put a lot of assumption about the nature of the problem you were trying to fix. I hope this helps anyway.

Ken Bourassa