Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations strongm on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

Windows Messages 1

Status
Not open for further replies.

ors

Programmer
Sep 10, 2005
18
MT
I am creating a small utility to manage the windows of another application.

I set a timer to enumerate the windows. I must do this a number of times because a number of steps are involved. Let us call this other applicaton GM. Here's the pseudo code:

Check if the main window of GM is loaded.
If it is, check whether its child window DD is loaded.
If DD is loaded resize and reposition its window.
Also send key strokes to DD in order to have it open 4 more windows.
Check again to see whether these new windows are open. If they are, resize and reposition each of them.

My problem is that not all windows are being opened, and I am afraid this is a timing problem with the Windows messages. I am enumerating windows for each check. If you know of a better way, I am open to suggestions.


Now here are the main code blocks:

[TIMER EVENT START]
begin
if gOkToRun then
begin
gOkToRun := false;
// enumerate windows and check for GMisLoaded and terminate as necessary
gGMisLoaded := False;
EnumWindows(@GMCheck,LongInt(Self));
if (not gGMisLoaded) then
mnuExitClick(Self)
else
begin
// enumerate windows and check for Debug session activity
gDebugLoaded := false;
EnumWindows(@DebugCheck,LongInt(Self));
sleep(1000);
if gDebugLoaded and gDebugSet then
begin
if not gWindowsSet then
begin
// enumerate windows and set up Debug windows (one time per debug session)
EnumWindows(@ResetWindows,LongInt(Self));
gWindowsSet := true;
end;
end
else
begin
gDebugSet := false; //loaded watch and debug info widows
gWindowsSet:= false; //resized and repositioned debug info widows
GMDAMain.ListBox1.Clear;

end;
end; // end else if (not gGMisLoaded)
end;
gOkToRun := true;
[TIMER EVENT END]

[OPEN CHILD WINDOWS]
var i : Integer;
begin
for i := 0 to gMaxWindows-1 do
begin
// messagedlg(IntToStr(i),mtInformation,[mbOK],0);
if (wRecord.open = 1) then
begin
if (wRecord.name = 'Messages') then
begin
OpenMessages(Wnd);
sleep(250);
end;
if (wRecord.name = 'Global Variables') then
begin
OpenGlobal(Wnd);
sleep(250);
end;
if (wRecord.name = 'All Instances') then
begin
OpenInstances(Wnd);
sleep(250);
end;
end;
end;
end;
[OPEN CHILD WINDOWS END]



This is a sample of the code which sends key strokes. I have one of these for each window I need to open.


[CODE START]
procedure TGMDAMain.OpenMessages(Wnd: Hwnd);
//----------------------------------------------------------------------]]
begin
if (Wnd <> 0) then
begin
SetForegroundWindow(Wnd);
Sleep( 250 );
// Alt-&Watch
keybd_event( VK_MENU, Mapvirtualkey( VK_MENU, 0 ), 0, 0 );
keybd_event( Ord('T'), MapVirtualKey( Ord('T'), 0), 0, 0 );
keybd_event( Ord('T'), MapVirtualKey( Ord('T'), 0), KEYEVENTF_KEYUP, 0 );
keybd_event( VK_MENU, Mapvirtualkey( VK_MENU, 0 ), KEYEVENTF_KEYUP, 0 );

// &Load
keybd_event( Ord('M'), MapVirtualKey( Ord('M'), 0), 0, 0 );
keybd_event( Ord('M'), MapVirtualKey( Ord('M'), 0), KEYEVENTF_KEYUP, 0 );
End;
End;
[CODE END]

 
In your openmessage routine you should be aware that SetForegroundWindow may not succeed (for a number of reasons). You need to check that because if the foreground window is not set then some if not all of your keyboard events will go elsewhere.

so you should check
Code:
if SetForegroundWindow(Wnd)<>0 then 
begin
    //Set foreground succedded

    //.. your keybd_event messages
end
else
begin
    //Display error message 
    //or use SendMessage function instead

end;

[\code]

I would ecommend the use of SendMessage instead of keybd_event to send a message to other windows. For the simple reason that SendMessage takes as first parameter the window handle of where you want your message to go. In other words you dont even need to set the desired window as foreground to be sure of key messages to it.

e.g.
sendmessage(Wnd,WM_CHAR,ORD('T'),0);

I hope it helps


"It is in our collective behaviour that we are most mysterious" Lewis Thomas
 
Your suggestion should remove the need to create intentional delays (waith n no of milliseconds) before sending more commands. Am I right?

Thanks for your help.
 
yes you are right

There is no need for intentional delays because SendMessage will not return until the destination window has received and processed the message.

"It is in our collective behaviour that we are most mysterious" Lewis Thomas
 
I replaced code as follows, however the sendkeys command is having no effect whatsoever. I am certain that the Wnd application handle is correct.

procedure OpenMessagesMenuItem(Wnd: Hwnd);
//----------------------------------------------------------------------]]
begin
if (Wnd <> 0) then
begin
// Alt-&Watch
sendmessage(Wnd,WM_CHAR,VK_MENU,0);
sendmessage(Wnd,WM_CHAR,ORD('T'),0);
// &Messages
sendmessage(Wnd,WM_CHAR,ORD('M'),0);

{ OLD CODE REMED OUT -- STARTS HERE
SetForegroundWindow(Wnd);
WaitMs(gwait);
// Alt-&Watch
keybd_event( VK_MENU, Mapvirtualkey( VK_MENU, 0 ), 0, 0 );
keybd_event( Ord('T'), MapVirtualKey( Ord('T'), 0), 0, 0 );
keybd_event( Ord('T'), MapVirtualKey( Ord('T'), 0), KEYEVENTF_KEYUP, 0 );
keybd_event( VK_MENU, Mapvirtualkey( VK_MENU, 0 ), KEYEVENTF_KEYUP, 0 );

// &Load
keybd_event( Ord('M'), MapVirtualKey( Ord('M'), 0), 0, 0 );
keybd_event( Ord('M'), MapVirtualKey( Ord('M'), 0), KEYEVENTF_KEYUP, 0 );
}
End;
End;
 
try this, I posted it some time ago in this forum:

feel free to ask some questions...
Code:
procedure SendKeysTohWnd(hWnd : LongWord; Text : string);
// send keystrokes to a specific window handle
var i         : integer;
    c         : char;
    wparam,
    lparam    : longword;
    scancode  : byte;
    oemScan   : word;
    prevchar  : word;
    Len       : word;

begin
 if Text <> '' then
  begin
   Len:=length(Text);
   i:=1;
   while i <= Len do
   begin
    c:=Text[i];
    wparam:=ord(c);
    // if character is #0 then next chars will be only keydown
    // if char is #1 then next chars will only be keyup
    if wparam < 2 then
     begin
      // #0 or #1
      Inc(i);
      if i > Len then Exit; // prevent critical error
      c:=Text[i];
      prevchar:=wparam;
      wparam:=ord(c);
     end
    else prevchar:=wparam;
    if (c > #31) and (PrevChar > 1) then // only do wm_char if keyup,keydown control chars are not used
     begin
      // normal chars, use WM_CHAR message to simulate keystroke
      oemScan:=Lobyte(vkKeyScan(c));
      scancode:=MapVirtualKey(oemScan,0);
      lparam:=1+(scancode shl 16);
      postmessage(hwnd,WM_CHAR,wparam,lparam);
     end
    else
     begin
      // system codes, use WM_KEYDOWN and WM_KEYUP to simulate keystroke
      scancode:=MapVirtualKey(wparam,0);
      if prevchar <> 1 then
       begin
        lparam:=1 or (scancode shl 16) or $40000000; // (1 shl 30);
        postmessage(hwnd,WM_KEYDOWN,wparam,lparam);
       end;
      if prevchar > 1 then
       begin
        oemScan:=Lobyte(vkKeyScan(c));
        scancode:=MapVirtualKey(oemScan,0);
        lparam:=1+(scancode shl 16);
        postmessage(hwnd,WM_CHAR,wparam,lparam);
       end;
      if prevchar <> 0 then
       begin
        lparam:=1 or (scancode shl 16) or $C0000000; // 3 shl 30
        postmessage(hwnd,WM_KEYUP,wparam,lparam);
       end;
     end;
    Inc(i);
   end;
  end;
end;


-----------------------------------------------------
What You See Is What You Get
Never underestimate tha powah of tha google!
 
Thanks for helping out.

Questions:

How do I use this to
1) hold down the ALT key
2) send a period as in filename.txt
3) and press the Return key

Your code uses the postmessage routine.

I am running a timer component which triggers every second and checks for open windows. If these are not open, I send keystrokes to another application to open these windows.

I was trying to use sendmessage so that my application pauses before sending more commands. Otherwise keystrokes are resent before the windows have time to open.

Did I explain myself well enough?
 
I tried replacing sendmessage with postmessage in my previous code, but it made no difference. It still will not work.

What is the scancode in LPARAM for? In most of the examples I cam across it is set to zero.

whosrdaddy according to your code, if I am sending a keystroke mapping the 'T' key, LPARAM should be set to 1+(MapVirtualKey(Lobyte(vkKeyScan('T')),0) shl 16)

I looked up the help files but cannot make sense of it.
 
be aware that lparam for WM_KEYDOWN is splitted up into several parts, for full info look here :



small example how to send CTRL-END :
SendToKeysToHwnd(hwnd,#0#17#0#35#1#35#1#17); // send CTRL-END to input window

#0 indicates you want WM_KEYDOWN and #1 indicates you want WM_KEYUP

so pressing CTRL-END on your keyboard results in messages :

WM_KEYDOWN for CTRL
WM_KEYDOWN for END
WM_KEYUP for END and
WM_KEYUP for CTRL.

it's that easy.

to send normal characters do

SendToKeysToHwnd(hwnd,'hello world');

if it's still not working, try this:

Code:
procedure TForm1.Button1Click(Sender: TObject);

var tid, tidto : cardinal;
    hwnd : integer;
    hwnd_edbox : integer;

begin
 Tid:=GetCurrentThreadId;
 // find login window handle
 hwnd:=findwindow(nil,pchar('User Logon'));
 hwnd_edbox:=findwindowex(hwnd,0,'Edit',nil);
 TidTo:=GetWindowThreadProcessId(hwnd);
 AttachThreadInput(TidTo,Tid,True);
 SendKeysToHwnd(hwnd_edbox,'mylogin'#13);
 AttachThreadInput(TidTo,Tid,False);
end;

cheers,
Daddy

-----------------------------------------------------
What You See Is What You Get
Never underestimate tha powah of tha google!
 
MSDN says:

lParam
Specifies the repeat count, scan code, extended-key flag, context code, previous key-state flag, and transition-state flag, as shown in the following table.

0-15
Specifies the repeat count for the current message. The value is the number of times the keystroke is autorepeated as a result of the user holding down the key. If the keystroke is held long enough, multiple messages are sent. However, the repeat count is not cumulative.
16-23
Specifies the scan code. The value depends on the OEM.
24
Specifies whether the key is an extended key, such as the right-hand ALT and CTRL keys that appear on an enhanced 101- or 102-key keyboard. The value is 1 if it is an extended key; otherwise, it is 0.
25-28
Reserved; do not use.
29
Specifies the context code. The value is always 0 for a WM_KEYDOWN message.
30
Specifies the previous key state. The value is 1 if the key is down before the message is sent, or it is zero if the key is up.
31
Specifies the transition state. The value is always zero for a WM_KEYDOWN message.


Now, I am still ill equipped to understand that. What is the range 0 to 31 and how do I define specific state flags within it. It looks like a one dimensional array definition. How can I target 30 for example, and set it to 1 or 0?
 
simple, those are bits, since lparam is a 32-bit integer value, you have bits 0-31.

so if you need to set bits 2 and 29 for example, you can do this :

lparam:=(1 shl 2) or (1 shl 29);

to clear bits you can use a mask, this mask clears bit 3 :

lparam:=lparam and not(1 shl 3);
or
lparam:=lparam and $FFFFFFF7;

there are offcourse more methods to do this, like the TBits class that one can find in the JCL.

My question for you : why you would you need it? the code I posted works as I use it in several apps...

-----------------------------------------------------
What You See Is What You Get
Never underestimate tha powah of tha google!
 
Well, I need it because I want to learn and not simply use other people's code. May I outline that I appreciate your help though.

The current problem with my utility (
is that with postmessage and keybd_event the commands being sent to the target window seem to get lost or jumbled up, unless I insert a delay in milliseconds. I was hoping that sendmessage would work since I read that it waits for the message to be executed before continuing. Unfortunately as I said in my earlier post, it does not work at all for me.

In interacting with the other application window, I simply access menu items to open a number of other windows. Problem is that sometimes only a few of the menu items are accessed unless I introduce the delay I mentioned previously. BTW the other application happens to be GameMaker.

Am I right in wanting to use sendmessage? ssgaunt has suggested this but I cannot get it to work.
 
sendmessage should be fine, did you try to use the AttachThreadInput trick??

-----------------------------------------------------
What You See Is What You Get
Never underestimate tha powah of tha google!
 
If sendmessage should work, why does the following code (apparently) do nothing?

sendmessage(Wnd,WM_KEYDOWN,VK_MENU,0);
sendmessage(Wnd,WM_CHAR,ORD('T'),0);
sendmessage(Wnd,WM_KEYUP,VK_MENU,0);
// &Globals
sendmessage(Wnd,WM_CHAR,ORD('G'),0);

---

Never heared of the AttachThreadInput trick.

Looks like AttachThreadInput requires two thread ids. How do I get those?

Can you please explain how and where do I implement AttachThreadInput?

 
Looks like AttachThreadInput requires two thread ids. How do I get those?

Can you please explain how and where do I implement AttachThreadInput?
just look at the example I gave you??

repost :

Code:
procedure TForm1.Button1Click(Sender: TObject);

var tid, tidto : cardinal;
    hwnd : integer;
    hwnd_edbox : integer;

begin
 Tid:=GetCurrentThreadId;
 // find login window handle
 hwnd:=findwindow(nil,pchar('User Logon'));
 hwnd_edbox:=findwindowex(hwnd,0,'Edit',nil);
 TidTo:=GetWindowThreadProcessId(hwnd);
 AttachThreadInput(TidTo,Tid,True);
 SendKeysToHwnd(hwnd_edbox,'mylogin'#13);
 AttachThreadInput(TidTo,Tid,False);
end;

offcourse you'll need to adapt this example to fit your own needs...

-----------------------------------------------------
What You See Is What You Get
Never underestimate tha powah of tha google!
 
oops, missed that.

Ok, I adapted it to

TidTo := GetWindowThreadProcessId(Wnd, 0);

the second parameter is required. Tried to set it to Null but that is a Variant, whereas a pointer is expected.

Still can see no reason to why the following code does not work:

// Alt-&Tools menu item
sendmessage(Wnd,WM_KEYDOWN,VK_MENU,0);
sendmessage(Wnd,WM_CHAR,ORD('T'),1);
sendmessage(Wnd,WM_KEYUP,VK_MENU,1);
// &Instances menu item
sendmessage(Wnd,WM_CHAR,ORD('I'),0);


 
Your task of sending keystrokes to other windows turns out to be quite challenging. Also SendKeysTohWnd procedure posted here is very nice. I have been following the thread discussion for sometime now because it covers a very attractive task.


In relation to your question as to why SendMessage does not give you the desired results I would like to say the following:

* SendMessage definitely sends the specified message to the specified window (i have tested it) and it is certain that the message is handled.

* Why then SendMessage appears to do nothing (in your case)
The reason as to why it appears to do nothing is because you are making an assumption which i THINK its wrong. (To be honest i assumed the same because it sounded logical)

You are assuming that the two messages send as follows

SendMessage(wnd,WM_KEYDOWN,ord('t'),0);
SendMessage(wnd,WM_KEYUP,ord('t'),0);

are equivalent to an actual key press on the keyboard. What you are missing is that keyboard input is first handled by Windows Operating system. After all its the operating system which decides what messages to generate and into which window message loop to put them. As an example: Pressing down ALT key generates WM_SYSKEYDOWN rather than WM_KEYDOWN (anyone correct me if i am wrong).

* Ok, how do you achieve a key press then
Probably you had it right in first place to use keybd_event,
maybe by replacing:
SetForegroundWindow (wnd)
with
SendMessage(wnd,WM_SETFOCUS,0,0)

I say probably because i have not tested it, plus i dont know how keybd_event works.



* My alternative to what you are trying to achieve
As i understand one of your tasks is to select a menu option on a certain window. Selecting an option from top level menu (e.g. in your case "Tools") generates a WM_SYSCOMMAND message. To simulate this you would write:

SendMessage(wnd,WM_SETFOCUS,0,0);
SendMessage(wnd,WM_SYSCOMMAND,SC_KEYMENU,Ord('t'))

However this does not achieve what you wanted, because you wanted to select the instances menu option under tools menu (am i right?). Selecting menu options that are not on the top level menu generates WM_COMMAND message. Preparing a WM_COMMAND message is slightly involving because it expects as wParam the CommandID of the menu chosen. Menu CommandIDs are application specific but fortunately they can be queried.

Taking your case as an example the following code provides TriggerInstancesMenu routine:

Code:
//////////////////////////////////////////
// function: GetMenuText(i,menu)
// purpose : Helper function used to 
//           receive text of i-th option 
//           on the given menu handle. 
//////////////////////////////////////////
function GetMenuText(i:integer; menu:HMENU):string;
var
   pCh:PChar;
begin
   pCh:=StrAlloc(255);
   GetMenuString(menu,i,pCh,255,MF_BYPOSITION);
   result:=string(pCh);
   StrDispose(pch);
end;

/////////////////////////////////////////
// function: GetMenuItem(sTitle,
//                       menu,
//                       var CommandID)
// purpose : Retreive the submenu of menu
//           whose text is sTitle
// returns : zero if the submenu with
//           given title is not found
//           otherwise returns the handle
//           to the submenu
// NOTE: If sTitle option is found in menu
//       then CommandID is filled with the
//       CommandID of menu option which
//       can be used in WM_COMMAND messages
/////////////////////////////////////////
function GetMenuOption(sTitle:string;
                       menu:HMENU;
                       var CommandID:integer):HMENU;
var
  i,nOptions:integer;
  sMenuText:string;
begin
    //Assume the submenu does not exist
    result:=0;

    //How many options on top level menu?
    nOptions:=GetMenuItemCount(menu);

    //Go through all options search for sTitle
    for i:=0 to nOptions-1 do
    begin
       //Get the text of the i-th menu option
       sMenuText:=GetMenuText(i,menu);

       //Is this the tools menu
       if sMenuText=sTitle then
       begin
          //Return the handle to i-th submenu
          result:=GetSubMenu(menu,i);

          //Return also CommandID of sTitle option
          CommandID:=GetMenuItemID(menu,i);

          //break out of the loop
          Break;
       end;
    end;
end;

//////////////////////////////////////////
// procedure: TriggerInstancesMenu
// purpose  : Simulate selection of
//            Tools|Instances menu option
//////////////////////////////////////////
procedure TriggerInstancesMenu(wnd:HWND);
var
   menu,toolsMenu:HMENU;
   instancesID,nToolsID:integer;
begin

   //Get the top level menu of this window
   menu:=GetMenu(wnd);

   if menu=0 then
      ShowMessage('The selected window has no menu')
   else
   begin
       //Look for tools menu option "Tools"
       toolsMenu:=GetMenuOption('&Tools',menu,nToolsID);

       //go through all options until you find "Tools"
       if toolsMenu=0 then
          ShowMessage('The specified window has no tools menu')
       else
       begin
          //go through "Tools" options search for "Instances"
          GetMenuOption('&Instances',toolsMenu,instancesID);
          if instancesID=-1 then
            ShowMessage('Instances option not found in Tools menu')
          else
          begin
            //If you want to activate the window
            SendMessage(wnd,WM_SETFOCUS,0,0);
            SendMessage(wnd,WM_COMMAND,instancesID,0);
          end;
       end;      
   end;  
end;

I have tested the above and it works as intented. Actually if you find the above helpful I have source code for a brief delphi program that enumerates the menu of any open window into a tree structure from which you can trigger any of the available options simply by double clicking the tree structure.

NOTE: If you found the above code of interest See also GetMenu and GetSubmenu help on MSDN library.


I hope this helps





"It is in our collective behaviour that we are most mysterious" Lewis Thomas
 
* Ok, how do you achieve a key press then
Probably you had it right in first place to use keybd_event,
maybe by replacing:
SetForegroundWindow (wnd)
with
SendMessage(wnd,WM_SETFOCUS,0,0)

I say probably because i have not tested it, plus i dont know how keybd_event works.

bledazemi,

that's why I proposed the attachthreadinput trick, so you don't need the SetForegroundWindow routine. this means that you can send keys to a window that isn't focused.

I think you're on the right track concerning the menu thingie, that's what ors should do.

cheers,
Daddy

-----------------------------------------------------
What You See Is What You Get
Never underestimate tha powah of tha google!
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top