Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Win32 API's in Powershell - FindWindow and SendMessage

4,323 views
Skip to first unread message

Bill V.

unread,
Nov 28, 2007, 4:41:02 PM11/28/07
to
Hello everyone.

I'm fairly new to Powershell (as is nearly everyone, I imagine) and I'm just
starting to get my feet wet. And, what better way to learn a new language
than to attempt to tackle an insanely complicated process! ;)

I'm a PC Tech at a college who is trying to make my life easier by
automating software installs (rather than keeping several ghost images for
each computer type, my ultimate goal is to keep clean images of basic
computer models, and use automation to install the software required on the
computers. I also want to schedule the script to perform regular updates as
well, but that's some ways off yet - I'd be happy with it running through
once). Quite a bit of the educational software out there is, to be blunt,
poorly written - at least regarding their installers. Typically, there are
no command line options for installed software, just keystrokes and mouse
clicks in the gui provided. I'm trying to automate this, without using
sendkeys - Ideally the script should be able to run whether someone is logged
on or not - an option I don't have with sendkeys. (Not to mention the basic
problems of applications loosing focus even if someone is logged in.)

However, all is not lost - it looks like FindWindow and SendMessage could be
the answer to my prayers, if I could only get them to work. :) (from what I
gather, I can send messages to the window, even if no one is currently logged
on - exactly what I'm looking for) Actually, I have FindWindow working, but I
can't get SendMessage to run properly. Right now, I can find the window I'm
looking for (Window type '#32770' - an Install Shield window, with a name
of"Welcome"). All, I'm trying to do currently is hit Alt+N to move on to the
next window. Once I get that working, it should be downhill from there. The
code will likely look familiar to people out there - it's been the result of
my searching the Internet for just how to do this.

I'll follow with my code and error message below.
#####
Code
#####

$domain = [AppDomain]::CurrentDomain

$name = New-Object Reflection.AssemblyName 'PInvokeFindWindowAssembly'
$assembly = $domain.DefineDynamicAssembly($name, 'Run')
$FindWindowmodule = $assembly.DefineDynamicModule('PInvokeFindWindowModule')
$FindWindowtype = $FindWindowmodule.DefineType('PInvokeType')
[Type[]]$FindWindowparameterTypes = [string], [string]
$FindWindowmethod = $FindWindowtype.DefineMethod('FindWindow',
'Public,Static,PinvokeImpl', [int], $FindWindowparameterTypes)
$Constructor =
[Runtime.InteropServices.DllImportAttribute].GetConstructor([string])
$FindWindowattr = New-Object Reflection.Emit.CustomAttributeBuilder
$Constructor, 'user32'
$FindWindowmethod.SetCustomAttribute($FindWindowattr)
$realFindWindowType = $FindWindowtype.CreateType()

[object[]]$FindWindowargs = [string] '#32770', [string] "Welcome"


[IntPtr]$FoundWindow = $realFindWindowType.InvokeMember('FindWindow',
'Public,Static,InvokeMethod', $null, $null, $FindWindowargs)


$name = New-Object Reflection.AssemblyName 'PInvokeSendMessageAssembly'
$assembly = $domain.DefineDynamicAssembly($name, 'Run')
$SendMessagemodule = $assembly.DefineDynamicModule('PInvokeSendMsgeModule')
$SendMessagetype = $SendMessagemodule.DefineType('PInvokeType')
[Type[]]$SendMessageparameterType = [IntPtr], [Int32], [Int32], [Int32]
$SendMessagemethod = $SendMessagetype.DefineMethod("SendMessage",
'Public,Static,PinvokeImpl', [int], $SendMessageparameterTypes)
$Constructor =
[Runtime.InteropServices.DllImportAttribute].GetConstructor([string])
$SendMessageattr = New-Object Reflection.Emit.CustomAttributeBuilder
$Constructor, 'user32'
$SendMessagemethod.SetCustomAttribute($SendMessageattr)
$realSendMessageType = $SendMessagetype.CreateType()

[object[]]$SendMessageargs = [IntPtr]$FoundWindow, [Int32]100,
[Int32]0x00000012, [Int32]0x20380001

$realSendMessageType.InvokeMember('SendMessage',
'Public,Static,InvokeMethod', $null, $null, $SendMessageargs)

[object[]]$SendMessageargs = [IntPtr]$FoundWindow, [Int32]104,
[Int32]0x00000009, [Int32]0xa00f0001

$realSendMessageType.InvokeMember('SendMessage',
'Public,Static,InvokeMethod', $null, $null, $SendMessageargs)


#####
Errors
#####
PS F:\PowerShell Scripts\Automated Installs - Install Pearson CourseCompass>
.\
endMessage.ps1
Exception calling "InvokeMember" with "5" argument(s): "Method
'PInvokeSendMess
ageType.SendMessage' not found."
At F:\PowerShell Scripts\Automated Installs - Install Pearson
CourseCompass\Sen
dMessage.ps1:38 char:34
+ $realSendMessageType.InvokeMember( <<<< 'SendMessage',
'Public,Static,InvokeM
ethod', $null, $null, $SendMessageargs)
Exception calling "InvokeMember" with "5" argument(s): "Method
'PInvokeSendMess
ageType.SendMessage' not found."
At F:\PowerShell Scripts\Automated Installs - Install Pearson
CourseCompass\Sen
dMessage.ps1:42 char:34
+ $realSendMessageType.InvokeMember( <<<< 'SendMessage',
'Public,Static,InvokeM
ethod', $null, $null, $SendMessageargs)


#####
Parting Message
#####

I'm more of a scripter than a programmer (I can't claim to know .Net by any
means) so I'm hoping the answer is failry obvious. Hopefully a second pair
of eyes is all it will take.

Thanks!

Bill

ebgreen

unread,
Nov 28, 2007, 4:57:01 PM11/28/07
to
I would suggest reading this article in the latest issue of MSDN magazine:
http://msdn.microsoft.com/msdnmag/issues/07/12/TestRun/

Shay Levi

unread,
Nov 28, 2007, 4:55:31 PM11/28/07
to
IMO, AutoIt is your friend. Writing this kind of automations in PowerShell
can be quiet a challenge.
Few days ago I blogged on a new set of Cmdlets, byDr. James McCaffrey, designed
to perform Windows UI automation tasks
http://msdn.microsoft.com/msdnmag/issues/07/12/TestRun/default.aspx

Anyway, for now, AutoIt can perform a wide range of GUI automations, and
based on my own expreince, its the
best GUI automation tool I've know. You can get more info on http://www.autoitscript.com/autoit3.


-----
Shay Levi
$cript Fanatic
http://scriptolog.blogspot.com
Hebrew weblog: http://blogs.microsoft.co.il/blogs/scriptfanatic

Bill V.

unread,
Nov 28, 2007, 5:50:00 PM11/28/07
to
I'll have to look into System.Management.Automation.dll and see if it has
what I need. Have you used it any? Can it send keystrokes even to out of
focus windows?

Thanks!

-Bill

Bill V.

unread,
Nov 28, 2007, 6:00:02 PM11/28/07
to
You're the second person to mention that article (in nearly as many minutes).
I'm planning on looking the cmd-lets over (when I get home) and I'll see if
it helps me any. It certainly would be a cleaner way to get things working.

Ironically, I checked out AutoIt earlier this month (I've been working on
this problem off an on for a while - I've just finally gotten the chance to
pick it back up again). It looked like it suffered from the same problem I
had with sendkeys - you had to be logged in for things to function properly.
I asked around in the genral forum if it could send messages to out of focus
apps, but I never got a response (and i got back to work and hadn't gotten
back to things since then).


Thanks
-Bill

Marco Shaw [MVP]

unread,
Nov 28, 2007, 9:48:28 PM11/28/07
to
Bill V. wrote:
> I'll have to look into System.Management.Automation.dll and see if it has
> what I need. Have you used it any? Can it send keystrokes even to out of
> focus windows?

As far as I know, you won't be able to use that DLL to any keystroke
automation.

Marco

Marco Shaw [MVP]

unread,
Nov 28, 2007, 9:57:21 PM11/28/07
to

> Ironically, I checked out AutoIt earlier this month (I've been working on
> this problem off an on for a while - I've just finally gotten the chance to
> pick it back up again). It looked like it suffered from the same problem I
> had with sendkeys - you had to be logged in for things to function properly.
> I asked around in the genral forum if it could send messages to out of focus
> apps, but I never got a response (and i got back to work and hadn't gotten
> back to things since then).

I don't see a way that AutoItX can send keys to an out-of-focus window,
but can't you just bring that window into focus first before sending
keystrokes to it?

"WinActivate
------------------------
Activates (gives focus to) a window.

WinActivate "title" [, "text"]

Parameters
title The title of the window to activate.
text [optional] The text of the window to activate. "

Marco


--
Microsoft MVP - Windows PowerShell
http://www.microsoft.com/mvp

PowerGadgets MVP
http://www.powergadgets.com/mvp

Blog:
http://marcoshaw.blogspot.com

Shay Levi

unread,
Nov 29, 2007, 4:09:32 AM11/29/07
to
From my exprience, clicking on unfocused window control shouldn't be a problem,
use the ControlClick function

ControlClick ( "title", "text", controlID [, button] [, clicks]] )


Here's a test:

Open a few explorer windows, then execute this in AutoIt:

Run("control desk.cpl")
WinWait("Display Properties")
Sleep(3000)
; now manually, click another window to make it the active window
ControlClick ( "Display Properties", "", "Button1")
; you should see the "Save As" dialog


HTH

Bill V.

unread,
Nov 29, 2007, 8:11:04 AM11/29/07
to
Part of the problem is that I can't bring a window into focus. One of the
goals is to be able to run the script regardless of if someone is logged in
or not. To have a window in focus, I have to have someone logged in. Since
I ultimately want to have the script as a weekly scheduled task (it will
ultimately be able to perform weekly scheduled tasks, or updates if they come
along), it would be much cleaner to have it be able to run completely in the
background than to have to have the computers logged in as someone.

That's why I've been so stubborn :). I actually already have vbscripts that
will perform the install (at least for this particular application - part of
why I was messing with this one early on in the process. It was the messiest
install I commonly make. But using sendkeys is always messy, and doesn't
always work well, even if I try to continually bring the window back into
focus). But, if I can send messages directly to the window, then it shoudn't
even matter if I can never bring the window into focus.

Shay Levi

unread,
Nov 29, 2007, 8:31:19 AM11/29/07
to
Bill,

I can't tell for sure what happens when no one is logged on. What I do know
is that you can send any mouse click
or keystrock to any windows whether it's active (in focus) or not (using
AutoIt).

Can you point me to your post in AutoIt forums?

Bill V.

unread,
Nov 29, 2007, 9:19:03 AM11/29/07
to
Actually, I was a bit misleading. I had gotten a response, just nothing
positive :) (I'd actually forgotten I had any responses, I just moved on).

It was here:

http://www.autoitscript.com/forum/index.php?showtopic=56612&st=0&p=429364&entry429364

Marco Shaw [MVP]

unread,
Dec 3, 2007, 2:39:12 PM12/3/07
to

Now, I came accross this following something else. This may have to do
with the threading model used by PowerShell and how those 2 APIs work.

I'm going to try to research this, and post back in a few days
(hopefully I can find the time).

Bill V.

unread,
Dec 10, 2007, 4:14:06 PM12/10/07
to
I hadn't been able to get things to work exactly as I wanted, but it looks
like I did have some success. Actually, I might have been doing better than
I thought, and I just hadn't been restarting powershell to notice it. I
ended up switching to C# for using sendmessage and findwindow, simply because
I could find more examples to follow (and blatently steal from). Ultimately,
I was making C# more complicated than it needed to be, simply because I never
unloaded what I had loaded - namely I was having a terrible time because
Powershell doesn't seem to know what a HandleRef is (and I wasn't prepared to
tell Powershell what it had been missing out on), and since I never unloaded
my C# code, (instead I thought reloading it would accomplish this) it would
never accept using an IntPtr, even though I had updated the code to do so. I
finally discovered my mistake by taking the code home - starting from scratch
on a new computer. (I tend to leave my work computer running for weeks at a
time.)

I modified some old Monad code to do what I wanted, though I still had
trouble getting things to work when the window was out of focus (I could see
the messages going to the button, but I couldn't activate the button unless
it was in focus). There may still be a way to directly do what I was
attempting, but I finally went around things by sending the bm_clicked
message directly to the parent window. Fortunately, I could use the
sendmessage and findwindow commands I had been working on so hard earlier -
so I didn't completely waste my efforts. (See, I almost know what I'm
talking about now! ;) ). Fortunately, this does seem to work while out of
focus.

Anyway, this is the code that I ultimately got working - I'll clean it up a
bit further at some time, and make it look more like mine (and maybe even
document things a bit). Again, if you look a bit, you'll likely recognize
the original Monad code (just search for the comments). Basically, I use
findwindow, to look for the new window that comes up with each new screen.
Then, findwindowex finds the button that I need to activate. Then I tell the
parent window, via sendmessage, that the button referenced by findwindowex
has been triggered.


#####################################################
# This is a general purpose routine that I put into a file called
# LibraryCodeGen.msh and then dot-source when I need it.
#####################################################
function Compile-Csharp ([string] $code, $FrameworkVersion="v2.0.50727",
[Array]$References)
{
#
# Get an instance of the CSharp code provider
#
$cp = new-object Microsoft.CSharp.CSharpCodeProvider


#
# Build up a compiler params object...
#

"${framework}\System.Data.dll,${framework}\System.dll,${framework}\system.xm­l.dll"


$framework = join-Path $env:windir
"Microsoft.NET\Framework\$FrameWorkVersion"
$refs = new-object Collections.ArrayList
$refs.AddRange( @("${framework}\System.dll",
"${framework}\system.windows.forms.dll",
"${framework}\System.data.dll",
"${framework}\System.Drawing.dll",
"${framework}\System.Xml.dll"))
if ($references.Count -ge 1)
{
$refs.AddRange($References)
}

$cpar = New-Object System.CodeDom.Compiler.CompilerParameters
$cpar.GenerateInMemory = $true
$cpar.GenerateExecutable = $false
$cpar.OutputAssembly = "custom"
$cpar.ReferencedAssemblies.AddRange($refs)
$cr = $cp.CompileAssemblyFromSource($cpar, $code)


if ( $cr.Errors.Count)
{
$codeLines = $code.Split("`n");
foreach ($ce in $cr.Errors)
{
write-host "Error: $($codeLines[$($ce.Line - 1)])"
$ce | out-host
}
}

}

#########################################################
$WinAPIcode = '
using System;
using System.Runtime.InteropServices;

namespace Win32APIStuff
{
public class CShWinAPI
{

[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string
lpWindowName);
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr
hWndChild, string lpClassName, string lpWindowName);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr
wParam, IntPtr lParam);


public static IntPtr RunFindWindow(string ClassNm, string WindowNm)
{
IntPtr FindWindowVar;
FindWindowVar = FindWindow(ClassNm, WindowNm);
return FindWindowVar;
}

public static IntPtr RunFindWindowEx(IntPtr hWdParent, string
ClassNm, string WindowNm)
{
IntPtr FindWindowVar;
FindWindowVar = FindWindowEx(hWdParent, System.IntPtr.Zero,
ClassNm, WindowNm);
return FindWindowVar;
}

public static IntPtr RunSendMessage(IntPtr Window, uint Message,
IntPtr wPar, IntPtr lPar)
{
IntPtr Sent;
Sent = SendMessage(Window, Message, wPar, lPar);
return Sent;
}


}


}
'

########################################################
# So now we compile the code and use .NET object access to run it.
########################################################
compile-CSharp $WinAPIcode

start-sleep -s 15

#######
#First Screen
#######

$FoundParent = [Win32APIStuff.CShWinAPI]::RunFindWindow('#32770', 'Welcome')
write-host $FoundParent
$FoundWindow = [Win32APIStuff.CShWinAPI]::RunFindWindowEx($FoundParent,
'Button', '&Next >')
write-host $FoundWindow

[Win32APIStuff.CShWinAPI]::RunSendMessage($FoundParent, 0x111, 1,
$FoundWindow)
start-sleep -s 15

#######
#Next Screen
#######

$FoundParent = [Win32APIStuff.CShWinAPI]::RunFindWindow('#32770', 'Software
License Agreement')
write-host $FoundParent
$FoundWindow = [Win32APIStuff.CShWinAPI]::RunFindWindowEx($FoundParent,
'Button', '&Yes')
write-host $FoundWindow

[Win32APIStuff.CShWinAPI]::RunSendMessage($FoundParent, 0x111, 1,
$FoundWindow)
start-sleep -s 15

#######
#Next Screen
#######

$FoundParent = [Win32APIStuff.CShWinAPI]::RunFindWindow('#32770',
'Information')
write-host $FoundParent
$FoundWindow = [Win32APIStuff.CShWinAPI]::RunFindWindowEx($FoundParent,
'Button', '&Next >')
write-host $FoundWindow

[Win32APIStuff.CShWinAPI]::RunSendMessage($FoundParent, 0x111, 1,
$FoundWindow)
start-sleep -s 15

#######
#Next Screen
#######

$FoundParent = [Win32APIStuff.CShWinAPI]::RunFindWindow('#32770', 'Choose
Destination Location')
write-host $FoundParent
$FoundWindow = [Win32APIStuff.CShWinAPI]::RunFindWindowEx($FoundParent,
'Button', '&Next >')
write-host $FoundWindow

[Win32APIStuff.CShWinAPI]::RunSendMessage($FoundParent, 0x111, 1,
$FoundWindow)
start-sleep -s 15

#######
#Next Screen
#######

$FoundParent = [Win32APIStuff.CShWinAPI]::RunFindWindow('#32770', 'Select
Components')
write-host $FoundParent
$FoundWindow = [Win32APIStuff.CShWinAPI]::RunFindWindowEx($FoundParent,
'Button', '&Next >')
write-host $FoundWindow

[Win32APIStuff.CShWinAPI]::RunSendMessage($FoundParent, 0x111, 1,
$FoundWindow)
start-sleep -s 15

#######
#Next Screen
#######

$FoundParent = [Win32APIStuff.CShWinAPI]::RunFindWindow('#32770', 'Choose
Destination Location')
write-host $FoundParent
$FoundWindow = [Win32APIStuff.CShWinAPI]::RunFindWindowEx($FoundParent,
'Button', '&Next >')
write-host $FoundWindow

[Win32APIStuff.CShWinAPI]::RunSendMessage($FoundParent, 0x111, 1,
$FoundWindow)
start-sleep -s 360

#######
#Next Screen
#######

$FoundParent = [Win32APIStuff.CShWinAPI]::RunFindWindow('#32770', 'Setup
Complete')
write-host $FoundParent
$FoundWindow = [Win32APIStuff.CShWinAPI]::RunFindWindowEx($FoundParent,
'Button', 'Finish')
write-host $FoundWindow

[Win32APIStuff.CShWinAPI]::RunSendMessage($FoundParent, 0x111, 1,
$FoundWindow)
start-sleep -s 15

#######
#Next Screen
#######

0 new messages