Ruby and Microsoft Windows - Ruby in Its Setting - Programming Ruby 1.9 & 2.0: The Pragmatic Programmers’ Guide (2013)

Programming Ruby 1.9 & 2.0: The Pragmatic Programmers’ Guide (2013)

Part 2. Ruby in Its Setting

Chapter 21. Ruby and Microsoft Windows

Ruby runs in a number of environments. Some of these are Unix-based, and others are based on the various flavors of Microsoft Windows. Ruby came from people who were Unix-centric, but over the years it has developed a whole lot of useful features in the Windows world, too. In this chapter, we’ll look at these features and share some secrets that let you use Ruby effectively under Windows.

21.1 Running Ruby Under Windows

You’ll find two versions of the Ruby interpreter in the RubyInstaller distribution.

The ruby is meant to be used at a command prompt (a DOS shell), just as in the Unix version. For applications that read and write to the standard input and output, this is fine. But this also means that any time you run ruby, you’ll get a DOS shell even if you don’t want one—Windows will create a new command prompt window and display it while Ruby is running. This may not be appropriate behavior if, for example, you double-click a Ruby script that uses a graphical interface (such as Tk) or if you are running a Ruby script as a background task or from inside another program.

In these cases, you will want to use rubyw. It is the same as ruby except that it does not provide standard in, standard out, or standard error and does not launch a DOS shell when run.

You can set up file associations using the assoc and ftype commands so that Ruby will automatically run Ruby when you double-click the name of a Ruby script:

C:\> assoc .rb=RubyScript

C:\> ftype RubyScript="C:\ruby1.9\bin\ruby.exe" %1 %*

You may have to run the command prompt with elevated privileges to make this work. To do this, right-click it in the Start menu, and select Run As Administrator.

If you don’t want to have to type the rb, you can add Ruby scripts to your PATHEXT:

C:\> set PATHEXT=.rb;%PATHEXT%

21.2 Win32API

If you plan on doing Ruby programming that needs to access some Windows 32 API functions directly or that needs to use the entry points in some other DLLs, we have good news for you—the Win32API library.

As an example, here’s some code that’s part of a larger Windows application used by our book fulfillment system to download and print invoices and receipts. A web application generates a PDF file, which the Ruby script running on Windows downloads into a local file. The script then uses the print shell command under Windows to print this file.

arg = "ids=#{resp.intl_orders.join(",")}"

fname = "/temp/invoices.pdf"

site = Net::HTTP.new(HOST, PORT)

site.use_ssl = true

http_resp, = site.get2("/ship/receipt?" + arg,

'Authorization' => 'Basic ' +

["name:passwd"].pack('m').strip )

File.open(fname, "wb") {|f| f.puts(http_resp.body) }

shell = Win32API.new("shell32","ShellExecute",

['L','P','P','P','P','L'], 'L' )

shell.Call(0, "print", fname, 0,0, SW_SHOWNORMAL)

You create a Win32API object that represents a call to a particular DLL entry point by specifying the name of the function, the name of the DLL that contains the function, and the function signature (argument types and return type). In the previous example, the variable shell wraps the Windows function ShellExecute in the shell32 DLL. The second parameter is an array of characters describing the types of the parameters the method takes: n and l represent numbers, i represent integers, p represents pointers to data stored in a string, and v represents a void type (used for export parameters only). These strings are case insensitive. So, our method takes a number, four string pointers, and a number. The last parameter says that the method returns a number. The resulting object is a proxy to the underlying ShellExecute function and can be used to make the call to print the file that we downloaded.

Many of the arguments to DLL functions are binary structures of some form. Win32API handles this by using Ruby String objects to pass the binary data back and forth. You will need to pack and unpack these strings as necessary.

21.3 Windows Automation

If groveling around in the low-level Windows API doesn’t interest you, Windows Automation may—you can use Ruby as a client for Windows Automation thanks to Masaki Suketa’s Ruby extension called WIN32OLE. Win32OLE is part of the standard Ruby distribution.

Windows Automation allows an automation controller (a client) to issue commands and queries against an automation server, such as Microsoft Excel, Word, and so on.

You can execute an automation server’s method by calling a method of the same name from a WIN32OLE object. For instance, you can create a new WIN32OLE client that launches a fresh copy of Internet Explorer and commands it to visit its home page:

win32/gohome.rb

require 'win32ole'

ie = WIN32OLE.new('InternetExplorer.Application')

ie.visible = true

ie.gohome

You could also make it navigate to a particular page:

win32/navigate.rb

require 'win32ole'

ie = WIN32OLE.new('InternetExplorer.Application')

ie.visible = true

ie.navigate("http://www.pragprog.com")

Methods that aren’t known to WIN32OLE (such as visible , gohome , or navigate ) are passed on to the WIN32OLE#invoke method, which sends the proper commands to the server.

Getting and Setting Properties

An automation server’s properties are automatically set up as attributes of the WIN32OLE object. This means you can set a property by assigning to an object attribute. For example, to get and then set the Height property of Explorer, you could write this:

win32/get_set_height.rb

require 'win32ole'

ie = WIN32OLE.new('InternetExplorer.Application')

ie.visible = true

puts "Height = #{ie.Height}"

ie.Height = 300

The following example uses the automation interface built into the OpenOffice suite to create a spreadsheet and populate some cells:[97]

win32/open_office.rb

require 'win32ole'

class OOSpreadsheet

def initialize

mgr = WIN32OLE.new('com.sun.star.ServiceManager')

desktop = mgr.createInstance("com.sun.star.frame.Desktop")

@doc = desktop.LoadComponentFromUrl("private:factory/scalc", "_blank", 0, [])

@sheet = @doc.sheets[0]

end

def get_cell(row, col)

@sheet.getCellByPosition(col, row, 0)

end

# tl: top_left, br: bottom_right

def get_cell_range(tl_row, tl_col, br_row, br_col)

@sheet.getCellRangeByPosition(tl_row, tl_col, br_row, br_col, 0)

end

end

spreadsheet = OOSpreadsheet.new

cell = spreadsheet.get_cell(1, 0)

cell.Value = 1234

cells = spreadsheet.get_cell_range(1, 2, 5, 3)

cols = cells.Columns.count

rows = cells.Rows.count

cols.times do |col_no|

rows.times do |row_no|

cell = cells.getCellByPosition(col_no, row_no)

cell.Value = (col_no + 1)*(row_no+1)

end

end

Named Arguments

Other automation client languages such as Visual Basic have the concept of named arguments. Suppose you had a Visual Basic routine with the following signature:

Song(artist, title, length): rem Visual Basic

Instead of calling it with all three arguments in the order specified, you could use named arguments:

Song title := 'Get It On': rem Visual Basic

This is equivalent to the call Song(nil, "Get It On", nil).

In Ruby, you can use this feature by passing a hash with the named arguments:

Song.new('title' => 'Get It On')

for each

Where Visual Basic has a for each statement to iterate over a collection of items in a server, a WIN32OLE object has an each method (which takes a block) to accomplish the same thing:

win32/win32each.rb

require 'win32ole'

excel = WIN32OLE.new("excel.application")

excel.Workbooks.Add

excel.Range("a1").Value = 10

excel.Range("a2").Value = 20

excel.Range("a3").Value = "=a1+a2"

excel.Range("a1:a3").each do |cell|

p cell.Value

end

Events

Your automation client written in Ruby can register itself to receive events from other programs. This is done using the WIN32OLE_EVENT class.

This example (based on code from the Win32OLE 0.1.1 distribution) shows the use of an event sink that logs the URLs that a user browses to when using Internet Explorer:

win32/record_navigation.rb

require 'win32ole'

urls_visited = []

running = true

def default_handler(event, *args)

case event

when "BeforeNavigate"

puts "Now Navigating to #{args[0]}..."

end

end

ie = WIN32OLE.new('InternetExplorer.Application')

ie.visible = TRUE

ie.gohome

ev = WIN32OLE_EVENT.new(ie, 'DWebBrowserEvents')

ev.on_event {|*args| default_handler(*args)}

ev.on_event("NavigateComplete") {|url| urls_visited << url }

ev.on_event("Quit") do |*args|

puts "IE has quit"

puts "You Navigated to the following URLs: "

urls_visited.each_with_index do |url, i|

puts "(#{i+1}) #{url}"

end

running = false

end

# hang around processing messages

WIN32OLE_EVENT.message_loop while running

Optimizing

As with most (if not all) high-level languages, it can be all too easy to churn out code that is unbearably slow, but that can be easily fixed with a little thought.

With WIN32OLE, you need to be careful with unnecessary dynamic lookups. Where possible, it is better to assign a WIN32OLE object to a variable and then reference elements from it, rather than creating a long chain of “.” expressions.

For example, instead of writing this:

workbook.Worksheets(1).Range("A1").value = 1

workbook.Worksheets(1).Range("A2").value = 2

workbook.Worksheets(1).Range("A3").value = 4

workbook.Worksheets(1).Range("A4").value = 8

we can eliminate the common subexpressions by saving the first part of the expression to a temporary variable and then make calls from that variable:

worksheet = workbook.Worksheets(1)

worksheet.Range("A1").value = 1

worksheet.Range("A2").value = 2

worksheet.Range("A3").value = 4

worksheet.Range("A4").value = 8

You can also create Ruby stubs for a particular Windows type library. These stubs wrap the OLE object in a Ruby class with one method per entry point. Internally, the stub uses the entry point’s number, not name, which speeds access.

Generate the wrapper class using the olegen.rb script, available in the Ruby source repository.[98] Give it the name of type library to reflect on:

C:\> ruby olegen.rb 'Microsoft TAPI 3.0 Type Library' >tapi.rb

The external methods and events of the type library are written as Ruby methods to the given file. You can then include it in your programs and call the methods directly.

More Help

If you need to interface Ruby to Windows NT, 2000, or XP, you may want to take a look at Daniel Berger’s Win32Utils project ( http://rubyforge.org/projects/win32utils/ ). There you’ll find modules for interfacing to the Windows clipboard, event log, scheduler, and so on.

Also, the Fiddle library (described briefly in the library section) allows Ruby programs to invoke methods in dynamically loaded shared objects. This means your Ruby code can load and invoke entry points in a Windows DLL. For example, the following code pops up a message box on a Windows machine and determines which button the user clicked.

win32/dl.rb

require 'fiddle'

user32 = DL.dlopen("user32.dll")

msgbox = Fiddle::Function.new(user32['MessageBoxA'],

[TYPE_LONG, TYPE_VOIDP, TYPE_VOIDP, TYPE_INT],

TYPE_INT)

MB_OKCANCEL = 1

msgbox.call(0, "OK?", "Please Confirm", MB_OKCANCEL)

This code wraps User32 DLL, creating a Ruby method that is a proxy to the underlying MessageBoxA method. It also specifies the return and parameter types so that Ruby can correctly marshal them between its objects and the underlying operating system types.

The wrapper object is then used to call the message box entry point in the DLL. The return values are the result (in this case, the identifier of the button pressed by the user) and an array of the parameters passed in (which we ignore).

Footnotes

[97]

See http://udk.openoffice.org/common/man/tutorial/office_automation.html for links to resources on automating OpenOffice.

[98]

http://svn.ruby-lang.org/repos/ruby/trunk/ext/win32ole/sample/olegen.rb