2

In Delphi, by using the Skype API, I can send a message to a contact fairly easy. However, what I am trying to do, is enter the message in the Chat Box of the currently focused Contact, without sending the message.

By using Winspector, I found that the Classname of the Chatbox is TChatRichEdit, which is placed on a TChatEntryControl, which is placed on a TConversationForm, and finally, which is placed on the tSkMainForm. (Obviously the Skype Client is coded in Delphi ;) )

By using the Win API, how can I find the correct tSkMainForm>TConversationForm>TChatEntryControl>TChatRichEdit, and then enter a message into it?

What would be the best way to go about this?

Also, the TConversationForm contains the name of the contact aswell, so I guess that makes it a bit easier?

EDIT: Here is a screenshot of Windspector Spy, showing the TChatRichEdit:

Winspector Spy

Here is my current code:

function GetConversationWindow(Wnd: HWnd; P: LParam): Bool; stdcall;
var
  Param: PGetConversationParam;
  ProcID: DWord;
  // WndClass docs say maximum class-name length is 256.
  ClassName: array[0..256] of Char;
  WindowTitle: array[0..256] of Char;
begin
  Result := True; // assume it doesn't match; keep searching
  Param := PGetConversationParam(P);

  GetWindowThreadProcessID(Wnd, @ProcID);
  if ProcID <> Param.ProcID then
    Exit;

  if GetClassName(Wnd, ClassName, Length(ClassName)) = 0 then
    Exit;
  if StrComp(ClassName, 'TConversationForm') <> 0 then
    Exit;

  if SendMessage(Wnd, wm_GetText, Length(WindowTitle), LParam(@WindowTitle[0])) = 0 then
    Exit;
  if Param.ContactName = WindowTitle then begin
    Param.Result := Wnd;
    Result := False;
  end;
end;



procedure TForm1.Button1Click(Sender: TObject);
var
  Param: TGetConversationParam;
  RichEditWnd, ControlWnd : HWND;
  ParentWnd : HWND;
begin
  //Param.ProcID := GetSkypeProcessID;
  Param.ContactName := 'xSky Admin';
  ParentWnd := FindWindowEx(0,0,'tSkMainForm',nil);

  if EnumChildWindows(ParentWnd,@GetConversationWindow, LParam(@Param)) then
    Abort; // Didn't find it.

  // Param.Result holds the conversation window's handle. Now walk its children.
  ControlWnd := FindWindowEx(Param.Result, 0, 'TChatEntryControl', nil);
  if ControlWnd = 0 then
    Abort; // Conversation doesn't have an entry control

  RichEditWnd := FindWindowEx(ControlWnd, 0, 'TChatRichEdit', nil);
  if RichEditWnd = 0 then
    Abort;

  ShowMessage('Got it!');
end;

I never reach the ShowMessage.

Here is a screenshot of my IDE in Debug Mode:

IDE in Debug Mode

I added a breakpoint at the Abort Line.

Any ideas?

Jeff
  • 12,085
  • 12
  • 82
  • 152

2 Answers2

4

Something like this:

var
  aHandle   : cardinal;
begin
   aHandle := FindWindow(PWideChar('TChatRichEdit'), nil);
   result  := aHandle <> 0;
   if result then
      PostMessage(aHandle, WM_...); 

Then you have a handle of that window. You can use WM_SETTEXT or something to input text. But Skype uses WM_COPYDATA to communicate with other programs, and vice versa. You should search StackOverflow for that.

Mihaela
  • 2,482
  • 3
  • 21
  • 27
1

I guess TConversationForm is a top-level window. Use EnumWindows to find that. (Don't bother with FindWindow yet; it always returns the first window it finds, so if there are multiple conversations active, you have no control over which you'll get.)

type
  PGetConversationParam = ^TGetConversationParam;
  TGetConversationParam = record
    ProcID: DWord;
    ContactName: string;
    Result: HWnd;
  end;

function GetConversationWindow(Wnd: HWnd; P: LParam): Bool; stdcall;
var
  Param: PGetConversationParam;
  ProcID: DWord;
  // WndClass docs say maximum class-name length is 256.
  ClassName: array[0..256] of Char;
  WindowTitle: array[0..256] of Char;
begin
  Result := True; // assume it doesn't match; keep searching
  Param := PGetConversationParam(P);

  GetWindowThreadProcessID(Wnd, @ProcID);
  if ProcID <> Param.ProcID then
    Exit;

  if GetClassName(Wnd, ClassName, Length(ClassName)) = 0 then
    Exit;
  if StrComp(ClassName, 'TConversationForm') <> 0 then
    Exit;

  if SendMessage(Wnd, wm_GetText, Length(WindowTitle), LParam(@WindowTitle[0])) = 0 then
    Exit;
  if Param.ContactName = WindowTitle then begin
    Param.Result := Wnd;
    Result := False;
  end;
end;

That function checks several things to make sure it's looking at the desired window. It checks that the window belongs to the Skype process, that it has the expected window class, and that its title is the name of the target contact. If Skype puts additional text in the window title, you'll need to make sure it looks "close enough." Don't just call Pos to see whether the contact name appears somewhere in the title; if any contact has a name that's a substring of the a conversation window's title, you might inadvertently find a match when you shouldn't.

The process ID isn't strictly required, so you can omit that part if you don't know the process ID.

The EnumWindows function will call the above function once for each top-level window. If the window is the one you're looking for, GetConversationWindow returns False to say, "I've found what I want, so please stop asking about any more." Otherwise, it returns True: "That one wasn't it, so please give me another." If GetConversationWindow ever returns False, then EnumWindows will also return False and the Param.Result field will hold the handle of the window you were looking for. Once you have it, use FindWindowEx to navigate the rest of the window hierarchy:

var
  Param: TGetConversationParam;
begin
  Param.ProcID := GetSkypeProcessID;
  Param.ContactName := GetSkypeContactName;
  if EnumWindows(@GetConversationWindow, LParam(@Param)) then
    Abort; // Didn't find it.

  // Param.Result holds the conversation window's handle. Now walk its children.
  ControlWnd := FindWindowEx(Param.Result, 0, 'TChatEntryControl', nil);
  if ControlWnd = 0 then
    Abort; // Conversation doesn't have an entry control

  RichEditWnd := FindWindowEx(ControlWnd, 0, 'TChatRichEdit', nil);
  if RichEditWnd = 0 then
    Abort;

  // Voila!
end;
Rob Kennedy
  • 161,384
  • 21
  • 275
  • 467
  • Wow! - One question though: I cannot figure out what to input in the 2nd parameter for the GetConversationWindow - I guess the 1st one is for the Skype HWND, correct? – Jeff Apr 20 '11 at 11:32
  • No. *You* don't call that function at all. You call `EnumWindows`, and then, as I said before, "The `EnumWindows` function will call the above function once for each top-level window." See the *second* code block for the rest of the example. – Rob Kennedy Apr 20 '11 at 13:29
  • @Rob Reason I ask is because it would not compile. And I cannot see what I need to do :) – Jeff Apr 20 '11 at 18:46
  • @Rob - "Not Enough Actual Parameters" on the EnumWindows line – Jeff Apr 20 '11 at 19:40
  • @Rob - I see you edited your code - it compiles fine now, but sadly it never returns anything. I use the Skype API to find the fullname of the focused contact (which is what is shown in winspector) – Jeff Apr 21 '11 at 10:18
  • @Rob - I now see that the TConversationForms are placed on the tSkMainForm (Skypes MainForm) - will edit OP – Jeff Apr 21 '11 at 10:26
  • @Rob - So how do I go from here? :) – Jeff Apr 24 '11 at 15:04
  • Do you see the pattern in the second code block, with the repeated calls to FindWindowEx? Use EnumWindows to find the top-level window, and then use FindWindowEx to walk down the window hierarchy until you find the window that meets your criteria. I don't know what those criteria are, but I think you've already used a tool to help you figure that out. – Rob Kennedy Apr 24 '11 at 20:27
  • @Rob - Right, however using FindWindowEx wont always find the right one, right? The first window, I have to find is Skype's main form (tSkMainForm), after that, I got to find the correct TConversationForm, because there are many of them - and that is where I loose it :P – Jeff Apr 24 '11 at 20:51
  • You could use EnumChildWindows to find the conversation form instead. Or call FindWindowEx repeatedly; each time you call it, pass the handle of the previous conversation form as the second parameter. (MSDN tells you why the second parameter would be significant.) – Rob Kennedy Apr 24 '11 at 20:58
  • @Rob - I will try that :) Also, the `PConversationParam.ContactName` Does the contact name have to match 100%? – Jeff Apr 24 '11 at 21:12
  • Well, the code I wrote uses the `=` operator, so yes, if you use that, it would have to match exactly, including case. You can use whatever other comparison algorithm you want. Just be careful that you don't get false positives, or else your program might send messages to the wrong person. – Rob Kennedy Apr 24 '11 at 22:41
  • In the Spy programs, whenever a contact has some of the fancy characters, like the ™ or † © characters, they are shown as ?'s? – Jeff Apr 25 '11 at 11:00
  • @Rob - I edited the OP, there is the code I currently use, plus 2 screenshots. Hope you find them usefull. Thanks! :) – Jeff Apr 25 '11 at 11:13
  • @Rob - Fixed. Apparently the PID was needed. Thanks! Now for my next question.. :P – Jeff Apr 25 '11 at 12:14
  • Yes, Jeff, if you're going to keep the code that *checks* the PID parameter, then you're going to have to include the code that *sets* it, too. – Rob Kennedy Apr 25 '11 at 13:37