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
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
Thanks!
-Bill
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
As far as I know, you won't be able to use that DLL to any keystroke
automation.
Marco
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
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
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.
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?
It was here:
http://www.autoitscript.com/forum/index.php?showtopic=56612&st=0&p=429364&entry429364
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).
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
#######