Skip to content

Commit

Permalink
Window class extracted into a separate lib
Browse files Browse the repository at this point in the history
  • Loading branch information
arvicco committed May 15, 2010
1 parent 32dd7bb commit 5b8b3d2
Show file tree
Hide file tree
Showing 46 changed files with 784 additions and 484 deletions.
18 changes: 18 additions & 0 deletions Gemfile
@@ -0,0 +1,18 @@
## Dependencies in this Gemfile are managed through the gemspec. Add/remove
## depenencies there, rather than editing this file
#
#require 'pathname'
#NAME = 'win_gui'
#BASE_PATH = Pathname.new(__FILE__).dirname
#GEMSPEC_PATH = BASE_PATH + "#{NAME}.gemspec"
#
#source :gemcutter
#
## Setup gemspec dependencies
#gemspec = eval(GEMSPEC_PATH.read)
#gemspec.dependencies.each do |dep|
# group = dep.type == :development ? :development : :default
# gem dep.name, dep.requirement, :group => group
#end
#gem(gemspec.name, gemspec.version, :path => BASE_PATH)

7 changes: 7 additions & 0 deletions HISTORY
@@ -0,0 +1,7 @@
== 0.0.0 / 2010-01-08

* Birthday! Initial concept: Following "Scripted Gui testing with Ruby" by Ian Dees

== 0.2.0 / 2010-05-15

* Window class extracted into a separate lib
2 changes: 1 addition & 1 deletion LICENSE
@@ -1,4 +1,4 @@
Copyright (c) 2009 arvicco
Copyright (c) 2010 Arvicco

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
Expand Down
113 changes: 38 additions & 75 deletions README.rdoc
@@ -1,92 +1,52 @@
= win_gui
by: Arvicco
url: http://github.com/arvicco/win_gui
by:: Arvicco
url:: http://github.com/arvicco/win_gui

== DESCRIPTION:

WinGui is a module that provides convenient wrapper methods for multiple Win32 API
functions (mostly from user32.dll) dealing with Windows GUI manipulation/automation.
In addition to straightforward API wrappers, it also defines more Ruby-like
abstractions and convenience methods for manipulating windows and other GUI
elements (such as WinGui::Window class and its methods).

!!! This project has been discontinued due to problems with Win32::API Callbacks and
need to support non-MRI Ruby implementations. So, new project is now based on FFI and
can be found here: http://github.com/arvicco/win (it is now available as a gem 'win').

== SUMMARY

So you want to write a simple program that makes some Win32 API function calls.
You searched MSDN high and low and you now know exactly what functions you need.
All you want is just putting those function calls into your Ruby code without too
much pain. You'd love this to be more or less natural extension of your Ruby code,
preferably not turning your code base into an ugly C++ like spaghetty
of CamelCase calls, String/Array pack/unpack gymnastics, buffer allocations,
extracting return values from [in/out] parameters and checking return codes for 0.

You can definitely use excellent 'win32-api' gem by Daniel J. Berger and Park Heesob
that allows you to define Win32 API objects for any function you can find on MSDN,
execute calls on them and even define callback objects that some of those API functions expect.

However, that gem will only take you so far. You'll still have to handle (somewhat)
gory details of argument preparation, mimicking pointers with Strings and stuff.
For example, consider the amount of code needed to complete a task as simple as
getting unicode title text for the window that you already have handle for:

api = Win32::API.new( 'GetWindowTextW', ['L', 'P', 'I'], 'L', 'user32' )
buffer = "\x00" * 1024 # I just hope it will be enough...
num_chars = api.call( window_handle, buffer, buffer.size)
title = if num_chars == 0
nil
else
buffer.force_encoding('utf-16LE').encode('utf-8').rstrip
end

Ew, ugly. What about getting information about process id for a known window?

api = Win32::API.new( 'GetWindowThreadProcessId', ['L', 'P'], 'L' , 'user32' )
process_packed = [1].pack('L')
thread = api.call(window_handle, process_packed)
process = process_packed.unpack('L').first

Wow, packing and unpacking arrays into String to get hold of a simple integer id.
Just great. Now, wouldn't it be MUCH better if you can just say something like this:
WinGui is a module that provides higher-level abstractions/wrappers around GUI-related
Win32 API functions. It uses Win gem as a basis, which in turn uses FFI.
So (in theory) it should work for any Ruby implementation supporting FFI. In practice,
it's been only tested under mingw and cygwin Ruby 1.9.1.

title = window_text( window_handle)
thread, process = window_thread_process_id( window_handle)
== SUMMARY:

What about API functions expecting callbacks? Well, something like this may be nice:
Win gem provides Rubyesque wrappers around Win32 API functions, but it is not enough to
to make Win32 API calls feel like more or less natural extension of Ruby code.
The reason for this, straightforward API wrappers are not object-oriented enough.

enum_child_windows( parent_handle, message ){|child_handle, message| puts child_handle }
For example, here is how you deal with typical GUI-related tasks using Win:

If you think about it, callbacks are not much more than code blocks, so let's not be afraid
to treat them as such. It would be also good if test functions return true/false instead of
zero/nonzero, find functions return nil if nothing was found etc...
require 'win/gui'
include Win::Gui::Window

So this is an idea behind WinGui library - make Win32 API functions more fun to use
and feel more natural inside Ruby code. Following the principle of least surprise, we
define methods with Rubyesque names (minimized? instead of IsMinimized, etc), minimum
arguments with sensible defaults, explicit return values and generous use of attached blocks.
window_handle = find_window('WinClass', nil)
title = window_text(window_handle )
thread, process = window_thread_process_id(window_handle)
puts window_handle, title, thread, process

Well, we even keep a backup solution for those diehard Win32 API longtimers who would rather
allocate their buffer strings by hand and mess with obscure return codes. If you use original
CamelCase method name instead of Rubyesque snake_case one, it will expect those standard
parameters you know and love from MSDN, return your zeroes instead of nils and support no
other enhancements.
enum_child_windows(window_handle, message) do |child_handle, message|
title = window_text(child_handle )
thread, process = window_thread_process_id(child_handle)
puts child_handle, title, thread, process
end
close_window(window_handle)

And if you do not see your favorite Windows API function amoung those already defined, it is
quite easy to define new one with def_api class method that does a lot of heavy lifting for
you and can be customized with options and code blocks to give you reusable API wrapper method
with the exact behavior you need.
Ideally, there should be thin wrapper class around window handle, and the code above should be more like this:
require 'win_gui'
include WinGui

== DOCUMENTATION:
window = Window.find(:first, :class => 'WinClass)
puts window.handle, window.title, window.thread, window.process
window.each_child {|child| puts child.handle, child.title, child.thread, child.process }
window.close

See WinGui and WinGui::Window for documentation
This library will try to provide such wrappers and convenience methods that will make working with
Windows GUI-related code much more fun than it is right now.

== REQUIREMENTS:

Only works with Ruby 1.9.1+ since it uses some of the most recent features (block
arguments given to block, etc...)
Only works with Ruby 1.9.1+ compatible implementations since Win gem uses some of latest Ruby features.

== FEATURES/PROBLEMS:

Expand All @@ -104,11 +64,14 @@ Contributors always welcome!

More examples will follow when the code is closer to production quality...

== CREDITS:
== CREDITS/PRIOR ART:

This library started as an extension of ideas and code described in excellent book
"Scripted GUI Testing with Ruby" by Ian Dees.

Win32::GuiTest by MoonWolf is a port of eponimous Perl library to Ruby
(http://raa.ruby-lang.org/project/win32-guitest). I do not like its Perlisms though.

== LICENSE:

Copyright (c) 2009 Arvicco. See LICENSE for details
Copyright (c) 2010 Arvicco. See LICENSE for details
66 changes: 16 additions & 50 deletions Rakefile
@@ -1,58 +1,24 @@
require 'rubygems'
require 'rake'
require 'pathname'
NAME = 'win_gui'
BASE_PATH = Pathname.new(__FILE__).dirname
LIB_PATH = BASE_PATH + 'lib'
PKG_PATH = BASE_PATH + 'pkg'
DOC_PATH = BASE_PATH + 'rdoc'

begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = "win_gui"
gem.summary = %Q{Rubyesque interfaces and wrappers for Win32 API GUI functions}
gem.description = %Q{Rubyesque interfaces and wrappers for Win32 API GUI functions}
gem.email = "arvitallian@gmail.com"
gem.homepage = "http://github.com/arvicco/win_gui"
gem.authors = ["arvicco"]
gem.add_dependency "win32-api", ">= 1.4.5"
gem.add_development_dependency "rspec", ">= 1.2.9"
gem.add_development_dependency "cucumber", ">= 0"
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
end
Jeweler::GemcutterTasks.new
rescue LoadError
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
end

require 'spec/rake/spectask'
Spec::Rake::SpecTask.new(:spec) do |spec|
spec.libs << 'lib' << 'spec'
spec.spec_files = FileList['spec/**/*_spec.rb']
end
$LOAD_PATH.unshift LIB_PATH.to_s
require 'version'

Spec::Rake::SpecTask.new(:rcov) do |spec|
spec.libs << 'lib' << 'spec'
spec.pattern = 'spec/**/*_spec.rb'
spec.rcov = true
end

task :spec => :check_dependencies
CLASS_NAME = WinGui
VERSION = CLASS_NAME::VERSION

begin
require 'cucumber/rake/task'
Cucumber::Rake::Task.new(:features)

task :features => :check_dependencies
require 'rake'
rescue LoadError
task :features do
abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
end
require 'rubygems'
gem 'rake', '~> 0.8.3.1'
require 'rake'
end

task :default => :spec
# Load rakefile tasks
Dir['tasks/*.rake'].sort.each { |file| load file }

require 'rake/rdoctask'
Rake::RDocTask.new do |rdoc|
version = File.exist?('VERSION') ? File.read('VERSION') : ""

rdoc.rdoc_dir = 'rdoc'
rdoc.title = "win_gui #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end
2 changes: 1 addition & 1 deletion VERSION
@@ -1 +1 @@
0.1.6
0.2.0
5 changes: 4 additions & 1 deletion features/support/env.rb
@@ -1,4 +1,7 @@
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
require 'win_gui'

require 'spec/expectations'
require 'spec/stubs/cucumber'

require 'pathname'
BASE_PATH = Pathname.new(__FILE__).dirname + '../..'
8 changes: 8 additions & 0 deletions lib/version.rb
@@ -0,0 +1,8 @@
require 'pathname'

module WinGui

VERSION_FILE = Pathname.new(__FILE__).dirname + '../VERSION' # :nodoc:
VERSION = VERSION_FILE.exist? ? VERSION_FILE.read.strip : nil

end
28 changes: 25 additions & 3 deletions lib/win_gui.rb
@@ -1,3 +1,25 @@
win_gui_dir = File.join(File.dirname(__FILE__),"win_gui" )
$LOAD_PATH.unshift win_gui_dir unless $LOAD_PATH.include?(win_gui_dir)
require 'win_gui'
require 'version'

module WinGui

# require "bundler"
# Bundler.setup

# Requires ruby source file(s). Accepts either single filename/glob or Array of filenames/globs.
# Accepts following options:
# :*file*:: Lib(s) required relative to this file - defaults to __FILE__
# :*dir*:: Required lib(s) located under this dir name - defaults to gem name
#
def self.require_libs( libs, opts={} )
file = Pathname.new(opts[:file] || __FILE__)
[libs].flatten.each do |lib|
name = file.dirname + (opts[:dir] || file.basename('.*')) + lib.gsub(/(?<!.rb)$/, '.rb')
Pathname.glob(name.to_s).sort.each {|rb| require rb}
end
end
end # module WinGui

# Require all ruby source files located under directory lib/win_gui
# If you need files in specific order, you should specify it here before the glob
WinGui.require_libs %W[**/*]

25 changes: 17 additions & 8 deletions lib/win_gui/window.rb
@@ -1,8 +1,21 @@
require 'win/gui'

module WinGui
# Delay between key commands (events)
WG_KEY_DELAY = 0.00001
# Wait delay quant
WG_SLEEP_DELAY = 0.001
# Timeout waiting for Window to be closed
WG_CLOSE_TIMEOUT = 1

class Window
include WinGui
extend WinGui
include Win::Gui
extend Win::Gui

def initialize(handle)
@handle = handle
end

attr_reader :handle

# find top level window by title, return wrapped Window object
Expand All @@ -13,10 +26,6 @@ def self.top_level(title, seconds=3)
Window.new @handle
end

def initialize(handle)
@handle = handle
end

# find child window (control) by title, window class, or control ID:
def child(id)
result = case id
Expand All @@ -36,7 +45,7 @@ def child(id)
end

def children
enum_child_windows(@handle,'Msg').map{|child_handle| Window.new child_handle}
enum_child_windows(@handle).map{|child_handle| Window.new child_handle}
end

# emulate click of the control identified by id
Expand All @@ -52,7 +61,7 @@ def click(id)
end

def close
post_message @handle, WM_SYSCOMMAND, SC_CLOSE, 0
post_message @handle, WM_SYSCOMMAND, SC_CLOSE, nil
end

def wait_for_close
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
3 changes: 3 additions & 0 deletions old_code/lib/win_gui.rb
@@ -0,0 +1,3 @@
win_gui_dir = File.join(File.dirname(__FILE__),"win_gui" )
$LOAD_PATH.unshift win_gui_dir unless $LOAD_PATH.include?(win_gui_dir)
require 'win_gui'
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 comments on commit 5b8b3d2

Please sign in to comment.