views:

194

answers:

2

I'm working on an IRC client. I've hit a majors snag which, up until not I've been able to work around. I'll show code below. What's I'm having a problem with is creating MDI child windows within the event handlers of idIRC.

For example, if I want to create a new channel form (FrmChannel), I can accomplish this easily by calling it's create procedure when I catch the '/join' command.

However, if I want to do it the right way, and wait until I've actually joined the channel, and receive confirmation of this from the server (by handling it in the onjoin event handler) then my call to my form creation procedure causes the application to hang.

The same goes for status windows. For example, if I put my status window creation procedure call on a TButton's onclick event, fine. Child form created. However, if I try the same thing when I actually receive a private message, by checking the event handler... Application hangs, no exception, and no MDI Child.

Here's the relevant code (for the sake of solving this I'll deal with the query window only).

First, the actual MDI Child creation goes like this. I have a TComponentList in here to manage a list of this class of form (in case you're wondering). There are some other things in here that keep track of the form as well, though commenting them out doesn't prevent the hang (I've tried).

procedure TFrmMain.NewQuery(const Server, MsgFrom: String);
var
Child: TFrmMessage;
TN: TTreeNode;
begin

///
/// Create form, set some data so we can reference it later.
///
///

  Child := TFrmMessage.Create(Application);
//  QueryManager.Add(Child); //TComponent List -- Used to find the Form Later On

  with Child do
  begin
   MyServer := Server; {What server this PM window is on}
   QueryWith := MsgFrom; {nickaname of the other person}
   Caption := MsgFrom; {Asthetic}
  end;

  Child.Echo('*** Conversation with ' + MsgFrom); //Herro World

  ///
  ///  The following code is working.
  ///  I'm pretty sure it's not causing the hangs.
  ///

  TN := GetNodeByText(ChanServTree, Server, True); {Find our parent node}

  with ChanServTree.Items.AddChild(TN, MsgFrom) do
  begin
   Selected := True;
   Tag := 2; {TYPE OF QUERY}
   Data := Pointer(Integer(Child)); //Pointer to Form we created
  end;

end;

Here's the event handler for my IRC component:

procedure TFrmMain.IRCPrivateMessage(ASender: TIdContext; const ANicknameFrom,
  AHost, ANicknameTo, AMessage: string);
  var
  CheckVr: String;
  aThread: TNQThread;
begin
  //DEBUG:
(StatusManager[0] as TFrmStatus).Echo('From: ' + ANickNameFrom + 'AMESSAGE: ' + '''' +AMessage + '''');

///
/// Handle Drone Version Requests!
///  This is REQUIRED on servers like irc.blessed.net - or they won't let you join
///  channels! - It's part of the Registration proccess
///

{The Drones on some server's don't follow specifications, so we need to search
hard for their presence}

CheckVr := AMessage;

StringReplace(CheckVr,' ','',[rfReplaceAll, rfIgnoreCase]);
StringReplace(CheckVr,#1,'',[rfReplaceAll, rfIgnoreCase]);
(StatusManager[0] as TFrmStatus).Echo('Message was: ' + '''' + CheckVr + '''');

if Trim(CheckVr) = 'VERSION' then
begin
 IRC.CTCPReply(ANickNameFrom,'VERSION','mIRC v6.01 Khaled Mardam-Bey');
 (StatusManager[0] as TFrmStatus).Echo('*** Sent Version Reply to ' + ANickNameFrom);

 exit; {Because if we don't, this could mess things up}
end;

  ///
  /// The Following code sends the PM to the appropriate window.
  ///  If that window does not exist, we will create one first.
  ///


  if Pos('#',Amessage) = 1 then
   begin
    //Handled Elsewhere
   end else {is PM}
   begin

     if FindQueryFrm(ANickNameTo,IRC.Host) = nil then
    begin

    NewQuery(IRC.Host, ANickNameFrom);
      exit;
     end;

   end;

//  FindChannelFrm(ANickNameTo,IRC.Host).ChannelMessage(ANicknameFrom, AMessage);

end;

I've tried commenting out various parts of the code to try to track down the cause of the hanging. The hang is caused by the Child := TFrmMessage.Create(Application); call specifically. What gives?

I've tried implementing threads to see if that might be an issue. If that's what you're thinking the problem is, I'll need help with my threading because apparently though the code is compiling, I'm still calling something wrong (because even my threaded version hangs).

Thanks in advance.

+1  A: 

It's been a while since I programmed in Delphi and battled similar problems...

In Java, socket info notifications happen on a very different thread from the one that maintains the GUI, and you're practically forbidden from making changes to the GUI from outside the GUI thread (but you're given mechanisms to legally ask the GUI thread to make the mod). In Delphi, all events are coming from the same event loop, but still... I'd get a queasy feeling asking for a major GUI update like a window open based on a socket event.

What I would try doing is getting the comm event to leave a notification on a queue or something, and getting the GUI thread to process that in the onIdle handler or something like that.

This is a stab in the dark, though. Take my recommendation with lots of salt!

Carl Smotricz
+2  A: 

As I told you in alt.comp.lang.borland-delphi earlier today, the problem is that Indy runs its event handlers in the same thread that does the blocking socket calls, which is not the same thread as your GUI. All GUI operations must take place in the same thread, but you are creating a new window in the socket thread.

To solve it, your event handler should post a notification to the main thread, which the main thread will handle asynchronously whenever it happens to next check for messages.

If you have a recent-enough Delphi version, you could try the TThread.Queue method, which works a lot like Synchronize, except the calling thread doesn't block waiting for the main thread to run the given method. They both have the same limitation regarding their method parameters, though; they only accept a zero-parameter method. That makes it cumbersome to transfer extra information for the method to use when it's eventually called. It's particularly bad for queued methods since whatever extra data you provide for them must remain intact for as long as it takes for the main thread to run it; the calling thread needs to make sure it doesn't overwrite the extra data before the queued method gets called.

A better plan is probably to just post a message to some designated window of the main thread. Application.MainForm is a tempting target, but Delphi forms are liable to be re-created without notice, so whatever window handle your other threads use might not be valid at the time they try to post a message. And reading the MainForm.Handle property on demand isn't safe, either, since if the form has no handle at the time, it will get created in the socket thread's context, which will cause all sorts of problems later. Instead, have the main thread create a new dedicated window for receiving thread messages with AllocateHWnd.

Once you have a target for messages to go to, you can arrange for threads to post and receive them. Define a message value and post them with PostMessage.

const
  am_NewQuery = wm_App + 1;

PostMessage(TargetHandle, am_NewQuery, ...);

To send the extra data the recipient will need to fully handle the event, messages have two parameters. If you only need two pieces of information, then you can pass your data directly in those parameters. If the messages need more information, though, then you'll need to define a record to hold it all. It could look something like this:

type
  PNewQuery = ^TNewQuery;
  TNewQuery = record
    Host: string;
    FromNickname: string;
  end;

Prepare and post the message like this:

procedure NewQuery(const Server, MsgFrom: string);
var
  Data: PNewQuery;
begin
  New(Data);
  Data.Host := Server;
  Data.FromNickname := MsgFrom;
  PostMessage(TargetHandle, am_NewQuery, 0, LParam(Data));
end;

Note that the caller allocates a new record pointer, but it does not free it. It will get freed by the recipient.

class procedure TSomeObject.HandleThreadMessage(var Message: TMessage);
var
  NewQueryData: PNewQuery;
begin
  case Message.Msg of
    am_NewQuery: begin
      NewQueryData := PNewQuery(Message.LParam);
      try
        Child := TFrmMessage.Create(NewQueryData.Host, NewQueryData.FromNickname);
        TN := GetNodeByText(ChanServTree, NewQueryData.Host, True); // Find parent node
        with ChanServTree.Items.AddChild(TN, NewQueryData.FromNickname) do begin
          Selected := True;
          Tag := 2; // TYPE OF QUERY
          Data := Child; // reference to form we created
        end;
      finally
        Dispose(NewQueryData);
      end;
    end;
    else
      Message.Result := DefWindowProc(TargetHandle, Message.Msg, Message.WParam, Message.LParam);
  end;
end;

I've made a couple of other changes to your code. One is that I made the child form's constructor accept the two pieces of information it needs to create itself properly. If the form wants its caption to be the nickname, then just tell it the nickname and let the form do whatever it needs to with that information.

Rob Kennedy
Hi Rob, I didn't see the newsgroup reply, thanks for answering me, twice! I'm going to take some time to try to work through these answers. I'll be back!
Commodianus
According to what I'm reading, AllocateHWnd is not thread safe. Check this out: http://17slon.com/blogs/gabr/2007/06/allocatehwnd-is-not-thread-safe.html still looking at that link and trying to reconcile both the fact that I'm in uncharted territory (personally) and your code with the code on that page.
Commodianus
Wow I'm lost. I'll sleep on this one and come back.
Commodianus
You're right. AllocateHWnd is not thread-safe. So only call it from the main thread when your program starts up, before you've created all the other threads. The function is not thread-safe, but the window handle it returns can be used from whatever threads you want. The handle is *associated* with the thread that created it.
Rob Kennedy
So to use AllocateHWnd do I just create a new Type of Form (under type) then call it from say... my main form's oncreate?
Commodianus
I don't know what you mean by "a new Type of Form." Just call the function from your main form somewhere. Its argument is a method with the signature like HandleThreadMessage as demonstrated in my answer. The method can belong to any class you want.
Rob Kennedy
I've never seen "class procedure" -- That's why I ask. For example, putting the method in my Main form... Good idea?
Commodianus
The only things causing problems for me here is that I don't know how to implement the HandleThreadMessage procedure, or what I should be changing TargetHandle to. The rest has been added to my form.
Commodianus
Class procedures are simply methods that aren't tied to an instance; check the help for details. TargetHandle is the window handle you created with AllocateHWnd. Implement the message handler much the way I demonstrated above; notice how I pretty much just took your NewQuery function and put everything into the message handler instead?
Rob Kennedy
I think I've got most of it... I'll be back.
Commodianus
I wanted to drop by again and thank you Rob, it has been a learning experience. Everything is working just the way I want.
Commodianus