Showing posts with label shell. Show all posts
Showing posts with label shell. Show all posts

Wednesday, May 30, 2007

Automating Applications with Ruby & The Windows Script Host

We've talked at length about automating Windows applications through COM/OLE, using the win32ole library. But not all applications expose themselves (so to speak) to such automation. The Windows Script Host can automate the activation of windows, and the sending of keystrokes. This may sometimes be all that you need to get the job done.

The Windows Script Host (WSH) has been part of the Windows operating system since Windows 98. You can use WSH's Shell object (via COM/OLE) to send keystokes to windows.

First, require the win32ole library:


require 'win32ole'

Now we'll create an instance of the Wscript Shell object:

wsh = WIN32OLE.new('Wscript.Shell')

To send keystrokes to a window, you must first activate the window, bringing it to the forefront. This can be done with the Wscript Shell's AppActivate method, which returns true if the window was successfully activated, and false otherwise. The AppActivate method takes the window title text as it's argument:

wsh.AppActivate('Title')

The string passed to the AppActivate method can be a partial, but must be the start or ending of the window title. The method is not case sensitive, and does not accept regular expressions. To quote Microsoft: "In determining which application to activate, the specified title is compared to the title string of each running application. If no exact match exists, any application whose title string begins with title is activated. If an application still cannot be found, any application whose title string ends with title is activated. If more than one instance of the application named by title exists, one instance is arbitrarily activated."

Once you have the window activated, you may use the Wscript Shell's SendKeys method to send keystrokes to the window. The SendKeys method takes a string in quotes. Special keys (ie, ENTER, TAB, PGDN, PGUP, Function Keys) may be embedded in the string, if surrounded by braces:

wsh.SendKeys('Ruby{TAB}on{TAB}Windows{ENTER}')

The SHIFT key is represented by '+', the ALT key is represented by '%', and the CTRL key is represented by '^', so to quit an application by sending ALT-F4:

wsh.SendKeys('%{F4}')

Further details on the syntax of the SendKeys method can be found
here.

Timing is important using these methods, so you may need to insert a sleep method here and there, to get the optimal performance. For example, a 1-second (or less) wait between activating a window and sending keystrokes, or vice-versa.

So, putting it all together, here's a brief example that activates Notepad (which must be running first), inserts text, saves the file to a specific name, and quits Notepad:

# Require the win32ole library:
require 'win32ole'
# Create an instance of the Wscript Shell:
wsh = WIN32OLE.new('Wscript.Shell')
# Try to activate the Notepad window:
if wsh.AppActivate('Notepad')
sleep(1)
# Enter text into Notepad:
wsh.SendKeys('Ruby{TAB}on{TAB}Windows{ENTER}')
# ALT-F to pull down File menu, then A to select Save As...:
wsh.SendKeys('%F')
wsh.SendKeys('A')
sleep(1)
if wsh.AppActivate('Save As')
wsh.SendKeys('c:\temp\filename.txt{ENTER}')
sleep(1)
# If prompted to overwrite existing file:
if wsh.AppActivate('Save As')
# Enter 'Y':
wsh.SendKeys('Y')
end
end
# Quit Notepad with ALT-F4:
wsh.SendKeys('%{F4}')
end

The above code snippet can be improved upon, and I encourage you to do so. But it, hopefully, demonstrates what can be done.

Mimicing keystrokes is certainly not the ultimate in program automation, but it may sometimes be all that you need to get the job done. For example, back in the days before pop-up blockers, I had written a script that would simply run in the background, look for pop-up ads (based on a list of title strings), and close them. Simple, yet effective.

I should probably also mention AutoIt, "a freeware Windows automation language. It can be used to script most simple Windows-based tasks." I've not used it myself, but I believe that the Watir library leverages it.

That's all for now. As always, let me know if you have questions, comments, or requests for future topics.

Thanks for stopping by!


Digg my article

Thursday, May 24, 2007

Launching Apps and Printing Docs with the Windows Shell

A reader recently asked how to launch an application from within a Ruby script. A quick answer is to use the system or exec methods. But you can also leverage the Windows Shell to launch applications, and have control over the window state. You can also use the shell to print documents. Let's get right down to it, shall we?...

Require the win32ole library...


require 'win32ole'

Create an instance of the Windows Shell object...

shell = WIN32OLE.new('Shell.Application')

The shell object's ShellExecute method performs a specified operation on a specified file. The syntax is...

shell.ShellExecute(FILE, ARGUMENTS, DIRECTORY, OPERATION, SHOW)

FILE: Required. String that contains the name of the file on which ShellExecute will perform the action specified by OPERATION.

ARGUMENTS: Optional. The parameter values for the operation.

DIRECTORY: Optional. The fully qualified path of the directory that contains the file specified by FILE. If this parameter is not specified, the current working directory is used.

OPERATION: Specifies the operation to be performed. It should be set to one of the verb strings that is supported by the file (Examples: 'open', 'edit', or 'print'). If this parameter is not specified, the default operation is performed.

SHOW: Recommends how the window that belongs to the application that performs the operation should be displayed initially (0 = hidden, 1 = normal, 2 = minimized, 3 = maximized). The application can ignore this recommendation. If this parameter is not specified, the application uses its default value.

So, to launch Excel in a maximized window...

shell.ShellExecute('excel.exe', '', '', 'open', 3)

I suppose you could also launch your rails app with something like this...

shell.ShellExecute('ruby.exe', 'c:\my_rails_app\script\server', '', 'open', 1)

To print a document, hiding the application window...

shell.ShellExecute('C:\MyFolder\Document.txt', '', '', 'print', 0)

That's about it. As always, post a comment here or send me email if you have questions, comments, or would like to request a topic for discussion.

Thanks for stopping by!

Tuesday, May 22, 2007

The Shell Windows Collection of Internet Explorer Objects

As mentioned previously, you cannot use the WIN32OLE.connect method to connect to a running instance of Internet Explorer, as you would do, for example, with Excel or Word. I'll explain now how to do this via the Windows Shell.

First, here's the code snippet, which grabs an instance of IE that has this blog displayed...


for window in WIN32OLE.new('Shell.Application').Windows
begin
if window.Document.Title =~ /Ruby on Windows/
ie = window
end
rescue
end
end

The Windows Shell object includes a Windows method which returns a collection of all of the open windows that belong to the Shell...

shell = WIN32OLE.new('Shell.Application')
windows = shell.Windows

This is actually a collection of Internet Explorer objects, though some of these may be Internet Explorer web browser windows and others may be Windows Explorer windows.

To get a count of the number of windows, call the Count method...

windows.Count

To reference a member of this collection by index, call the Item method, passing it the (zero-based) index...

first_window = windows.Item(0)

Internet Explorer windows will normally have a Document object, which will have a Title property, so to find the IE window that you want to work with, iterate over the Windows collection and check the Document.Title value...

for window in windows
begin
if window.Document.Title =~ /Ruby on Windows/
ie = window
end
rescue
end
end

...and now you have your IE application object to work with as previously discussed.

Note that not all Shell Window objects will have a Document object, so (in the example above) wrapping the code within a begin... rescue... end block would handle the error that occurs with non-IE windows.

Make sense? Let me know if you have questions or comments, and thanks for stopping by!

Sunday, May 6, 2007

Automating the Windows Shell with Ruby

The Microsoft Windows Shell provides a set of objects and methods that allow you to automate the Windows Shell with Ruby. You can use these objects and methods to access many of the Shell's functions.

Let's start with an example involving accessing your CD-ROM drive...

As usual, we'll start by requiring the win32ole library...


require 'win32ole'

Next, we'll create an instance of the Windows Shell object...

shell = WIN32OLE.new("Shell.Application")

Now, we'll call the shell object's NameSpace method to obtain a reference to the "My Computer" folder...

my_computer = shell.NameSpace(17)

The value passed to the NameSpace method represents a special folder ("My Computer" = 17).

To obtain a reference to the drive object, we'll call the NameSpace object's ParseName method, passing it the drive letter string for the CD-ROM drive...

cdrom = my_computer.ParseName("E:\\")

Shell objects such as drives and folders have a collection of Verbs that can be called upon. We can see the list if verbs available for a drive by iterating over the drive's Verbs collection and printing out the Name value...

cdrom.Verbs.each do |verb|
puts verb.Name
end

The list of verbs may vary depending on the type of disc in the drive, but you may see something like this...

&Play
S&earch...
&Open
E&xplore
Auto&Play
Form&at
&Use with DLA
S&haring and Security...
Scan with &AVG Free
E&ject
&Copy
Create &Shortcut
P&roperties

Note the ampersand (&) in these verb names, which represent the context menu shortcut keys.

To perform an action represented by a Verb, locate the verb by Name, then call that Verb's doIt method. So, to eject your CD-ROM drive, you can do this...

cdrom.Verbs.each do |verb|
verb.doIt if verb.Name == "E&ject"
end

Putting it all together, we could whip up a little CdRom class that encapsulates such functionality...

class CdRom

attr_accessor :drive, :drive_letter, :verbs

def initialize(drive_letter)
my_computer = 17
@drive_letter = drive_letter
sh = WIN32OLE.new("Shell.Application")
@drive= sh.NameSpace(my_computer).ParseName("#{@drive_letter}")
@verbs = []
@drive.Verbs.each do |verb|
@verbs << verb.Name if verb.Name != ''
end
end

def invoke_verb(verb_name)
@drive.Verbs.each do |verb|
verb.doIt if verb.Name== verb_name
end
end

def eject
self.invoke_verb("E&ject")
end

def open
self.invoke_verb("&Open")
end

def explore
self.invoke_verb("E&xplore")
end

def play
self.invoke_verb("&Play")
end

end

...which could be used like this...

cd = CdRom.new('d:\\')
puts cd.verbs
cd.eject

A tip of the hat goes to Masaki Suketa, who informed me (via the comp.lang.ruby group) that the standard InvokeVerb method does not currently work in the win32ole library, and to use the verb.doIt method instead.

That's all for now. As always, feel free to comment here or email me if you have special requests.

Thanks for stopping by!