Question

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.

Was it helpful?

Solution

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.

OTHER TIPS

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!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top