win32-autogui 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitattributes +1 -0
- data/.gitignore +10 -0
- data/.yardopts +6 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +60 -0
- data/HISTORY.markdown +11 -0
- data/LICENSE +20 -0
- data/README.markdown +265 -0
- data/Rakefile +55 -0
- data/TODO.markdown +9 -0
- data/VERSION +1 -0
- data/config/cucumber.yml +7 -0
- data/examples/quicknote/.gitignore +8 -0
- data/examples/quicknote/FormAboutU.dfm +44 -0
- data/examples/quicknote/FormAboutU.pas +36 -0
- data/examples/quicknote/FormMainU.dfm +110 -0
- data/examples/quicknote/FormMainU.pas +268 -0
- data/examples/quicknote/FormSplashU.dfm +32 -0
- data/examples/quicknote/FormSplashU.pas +52 -0
- data/examples/quicknote/LICENSE +20 -0
- data/examples/quicknote/README.markdown +28 -0
- data/examples/quicknote/Rakefile +12 -0
- data/examples/quicknote/TODO.markdown +15 -0
- data/examples/quicknote/dcu/.gitignore +1 -0
- data/examples/quicknote/exe/.gitignore +0 -0
- data/examples/quicknote/exe/quicknote.exe +0 -0
- data/examples/quicknote/lib/quicknote.rb +140 -0
- data/examples/quicknote/quicknote.cfg +37 -0
- data/examples/quicknote/quicknote.dof +158 -0
- data/examples/quicknote/quicknote.dpr +16 -0
- data/examples/quicknote/quicknote.res +0 -0
- data/examples/quicknote/spec/quicknote/form_about_spec.rb +50 -0
- data/examples/quicknote/spec/quicknote/form_main_spec.rb +274 -0
- data/examples/quicknote/spec/quicknote/form_splash_spec.rb +44 -0
- data/examples/quicknote/spec/spec.opts +2 -0
- data/examples/quicknote/spec/spec_helper.rb +34 -0
- data/examples/quicknote/spec/watchr.rb +143 -0
- data/examples/skeleton/.gitignore +8 -0
- data/examples/skeleton/LICENSE +20 -0
- data/examples/skeleton/README.markdown +62 -0
- data/examples/skeleton/Rakefile +21 -0
- data/examples/skeleton/TODO.markdown +9 -0
- data/examples/skeleton/config/cucumber.yml +7 -0
- data/examples/skeleton/dcu/.gitignore +1 -0
- data/examples/skeleton/exe/.gitignore +1 -0
- data/examples/skeleton/features/basic.feature +6 -0
- data/examples/skeleton/features/step_definitions/.gitignore +0 -0
- data/examples/skeleton/features/step_definitions/application_steps.rb +43 -0
- data/examples/skeleton/features/support/env.rb +5 -0
- data/examples/skeleton/lib/myapp.rb +73 -0
- data/examples/skeleton/spec/myapp/form_about_spec.rb +50 -0
- data/examples/skeleton/spec/myapp/form_main_spec.rb +60 -0
- data/examples/skeleton/spec/spec.opts +2 -0
- data/examples/skeleton/spec/spec_helper.rb +29 -0
- data/examples/skeleton/spec/watchr.rb +143 -0
- data/features/automating_an_application.feature +11 -0
- data/features/step_definitions/.gitignore +0 -0
- data/features/step_definitions/calculator_steps.rb +37 -0
- data/features/support/env.rb +4 -0
- data/lib/win32/autogui.rb +27 -0
- data/lib/win32/autogui/application.rb +249 -0
- data/lib/win32/autogui/input.rb +238 -0
- data/lib/win32/autogui/window.rb +191 -0
- data/lib/win32/autogui/windows/window.rb +22 -0
- data/spec/applications/calculator.rb +34 -0
- data/spec/auto_gui/application_spec.rb +132 -0
- data/spec/basic_gem/basic_gem_spec.rb +13 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/watchr.rb +144 -0
- data/win32-autogui.gemspec +43 -0
- metadata +329 -0
@@ -0,0 +1,11 @@
|
|
1
|
+
Feature: Automating a GUI application
|
2
|
+
|
3
|
+
As a developer, I want to run automated tests on GUI applications
|
4
|
+
so that my specifications are testable in a repeatable manner.
|
5
|
+
|
6
|
+
Background: A running GUI application
|
7
|
+
Given a GUI application named calculator
|
8
|
+
|
9
|
+
Scenario: Simple calculation
|
10
|
+
When I type in "2+2="
|
11
|
+
Then the edit window text should match /4/
|
File without changes
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '../../../spec/applications/calculator')
|
2
|
+
|
3
|
+
include Autogui::Input
|
4
|
+
|
5
|
+
After('@calculator') do
|
6
|
+
if @calculator
|
7
|
+
@calculator.close(:wait_for_close => true) if @calculator.running?
|
8
|
+
@calculator.should_not be_running
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
Given /^a GUI application named calculator$/ do
|
13
|
+
@calculator = Calculator.new
|
14
|
+
@calculator.should be_running
|
15
|
+
end
|
16
|
+
|
17
|
+
When /^I type in "([^"]*)"$/ do |string|
|
18
|
+
@calculator.set_focus
|
19
|
+
type_in(string)
|
20
|
+
end
|
21
|
+
|
22
|
+
# "the window text should match" allows regex in the partial_output, if
|
23
|
+
# you don't need regex, use "the output should contain" instead since
|
24
|
+
# that way, you don't have to escape regex characters that
|
25
|
+
# appear naturally in the output
|
26
|
+
Then /^the edit window text should match \/([^\/]*)\/$/ do |partial_output|
|
27
|
+
@calculator.edit_window.text.should =~ /#{partial_output}/
|
28
|
+
end
|
29
|
+
|
30
|
+
Then /^the edit window text should contain exactly "([^"]*)"$/ do |exact_output|
|
31
|
+
@calculator.edit_window.text.should == unescape(exact_output)
|
32
|
+
end
|
33
|
+
|
34
|
+
Then /^the edit window text should contain exactly:$/ do |exact_output|
|
35
|
+
@calculator.edit_window.text.should == exact_output
|
36
|
+
end
|
37
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# require all files here
|
2
|
+
require 'win32/autogui/input'
|
3
|
+
require 'win32/autogui/window'
|
4
|
+
require 'win32/autogui/application'
|
5
|
+
|
6
|
+
# Master namespace
|
7
|
+
module Autogui
|
8
|
+
|
9
|
+
# Contents of the VERSION file
|
10
|
+
#
|
11
|
+
# Example format: 0.0.1
|
12
|
+
#
|
13
|
+
# @return [String] the contents of the version file in #.#.# format
|
14
|
+
def self.version
|
15
|
+
version_info_file = File.join(File.dirname(__FILE__), *%w[.. .. VERSION])
|
16
|
+
File.open(version_info_file, "r") do |f|
|
17
|
+
f.read.strip
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return False (nil) or True (Integer)
|
22
|
+
def self.win32?
|
23
|
+
RUBY_PLATFORM =~ /mingw|mswin|cygwin/i
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,249 @@
|
|
1
|
+
require 'windows/process'
|
2
|
+
require 'windows/synchronize'
|
3
|
+
require 'windows/handle'
|
4
|
+
require "win32/process"
|
5
|
+
require "win32/clipboard"
|
6
|
+
|
7
|
+
module Autogui
|
8
|
+
|
9
|
+
# Wrapper class for text portion of the RubyGem win32/clipboard
|
10
|
+
# @see http://github.com/djberg96/win32-clipboard
|
11
|
+
class Clipboard
|
12
|
+
|
13
|
+
# Clipboard text getter
|
14
|
+
#
|
15
|
+
# @return [String] clipboard data
|
16
|
+
#
|
17
|
+
def text
|
18
|
+
Win32::Clipboard.data
|
19
|
+
end
|
20
|
+
|
21
|
+
# Clipboard text setter
|
22
|
+
#
|
23
|
+
# @param [String] str text to load onto the clipboard
|
24
|
+
#
|
25
|
+
def text=(str)
|
26
|
+
Win32::Clipboard.set_data(str)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
# The Application class wraps a binary application so
|
32
|
+
# that it can be started and controlled via Ruby. This
|
33
|
+
# class is meant to be subclassed.
|
34
|
+
#
|
35
|
+
# @example
|
36
|
+
#
|
37
|
+
# class Calculator < Autogui::Application
|
38
|
+
#
|
39
|
+
# def initialize(options = {})
|
40
|
+
# defaults = {
|
41
|
+
# :name => "calc",
|
42
|
+
# :title => "Calculator"
|
43
|
+
# }
|
44
|
+
# super defaults.merge(options)
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# def edit_window
|
48
|
+
# main_window.children.find {|w| w.window_class == 'Edit'}
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# def dialog_about
|
52
|
+
# Autogui::EnumerateDesktopWindows.new.find do |w|
|
53
|
+
# w.title.match(/About Calculator/) && (w.pid == pid)
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# def clear_entry
|
58
|
+
# set_focus
|
59
|
+
# keystroke(VK_DELETE)
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
class Application
|
64
|
+
include Windows::Process
|
65
|
+
include Windows::Synchronize
|
66
|
+
include Windows::Handle
|
67
|
+
|
68
|
+
# @return [String] the executable name of the application
|
69
|
+
attr_accessor :name
|
70
|
+
|
71
|
+
# @return [String] the executable application parameters
|
72
|
+
attr_accessor :parameters
|
73
|
+
|
74
|
+
# @return [String] window title of the application
|
75
|
+
attr_accessor :title
|
76
|
+
|
77
|
+
# @return [Number] the process identifier (PID) returned by Process.create
|
78
|
+
attr_reader :pid
|
79
|
+
|
80
|
+
# @return [Number] the process thread id returned by Process.create
|
81
|
+
attr_reader :thread_id
|
82
|
+
|
83
|
+
# @return [Number] the main_window wait timeout in seconds
|
84
|
+
attr_accessor :main_window_timeout
|
85
|
+
|
86
|
+
# @return [Number] the wait timeout in seconds used by Process.create
|
87
|
+
attr_accessor :create_process_timeout
|
88
|
+
|
89
|
+
# @example initialize an application on the path
|
90
|
+
#
|
91
|
+
# Application.new :name => "calc"
|
92
|
+
#
|
93
|
+
# @example initialize with full DOS path
|
94
|
+
#
|
95
|
+
# Application.new :name => "\\windows\\system32\\calc.exe"
|
96
|
+
#
|
97
|
+
# @param [Hash] options initialize options
|
98
|
+
# @option options [String] :name a valid win32 exe name with optional path
|
99
|
+
# @option options [String] :title the application window title, used along with the pid to locate the application main window, defaults to :name
|
100
|
+
# @option options [Number] :parameters command line parameters used by Process.create
|
101
|
+
# @option options [Number] :create_process_timeout (10) timeout in seconds to wait for the create_process to return
|
102
|
+
# @option options [Number] :main_window_timeout (10) timeout in seconds to wait for main_window to appear
|
103
|
+
#
|
104
|
+
def initialize(options = {})
|
105
|
+
|
106
|
+
unless options.kind_of?(Hash)
|
107
|
+
raise ArgumentError, 'Initialize expecting options to be a Hash'
|
108
|
+
end
|
109
|
+
|
110
|
+
@name = options[:name] || name
|
111
|
+
@title = options[:title] || name
|
112
|
+
@main_window_timeout = options[:main_window_timeout] || 10
|
113
|
+
@create_process_timeout = options[:create_process_timeout] || 10
|
114
|
+
@parameters = options[:parameters]
|
115
|
+
|
116
|
+
# sanity checks
|
117
|
+
raise 'Application name not set' unless name
|
118
|
+
|
119
|
+
start
|
120
|
+
end
|
121
|
+
|
122
|
+
# Start up the binary application via Process.create and
|
123
|
+
# set the window focus to the main_window
|
124
|
+
#
|
125
|
+
# @raise [Exception] if create_process_timeout exceeded
|
126
|
+
# @raise [Exception] if start failed for any reason other than create_process_timeout
|
127
|
+
#
|
128
|
+
# @return [Number] the pid
|
129
|
+
#
|
130
|
+
def start
|
131
|
+
|
132
|
+
command_line = name
|
133
|
+
command_line = name + ' ' + parameters if parameters
|
134
|
+
|
135
|
+
# returns a struct, raises an error if fails
|
136
|
+
process_info = Process.create(
|
137
|
+
:command_line => command_line,
|
138
|
+
:close_handles => false,
|
139
|
+
:creation_flags => Process::DETACHED_PROCESS
|
140
|
+
)
|
141
|
+
@pid = process_info.process_id
|
142
|
+
@thread_id = process_info.thread_id
|
143
|
+
process_handle = process_info.process_handle
|
144
|
+
thread_handle = process_info.thread_handle
|
145
|
+
|
146
|
+
# wait for process
|
147
|
+
ret = WaitForInputIdle(process_handle, (create_process_timeout * 1000))
|
148
|
+
|
149
|
+
# done with the handles
|
150
|
+
CloseHandle(process_handle)
|
151
|
+
CloseHandle(thread_handle)
|
152
|
+
|
153
|
+
raise "Start command failed on create_process_timeout" if ret == WAIT_TIMEOUT
|
154
|
+
raise "Start command failed while waiting for idle input, reason unknown" unless (ret == 0)
|
155
|
+
@pid
|
156
|
+
end
|
157
|
+
|
158
|
+
# The application main window found by enumerating windows
|
159
|
+
# by title and application pid. This method will keep looking
|
160
|
+
# unit main_window_timeout (default: 10s) is exceeded.
|
161
|
+
#
|
162
|
+
# @raise [Exception] if the main window cannot be found
|
163
|
+
#
|
164
|
+
# @return [Autogui::Window]
|
165
|
+
# @see initialize for options
|
166
|
+
#
|
167
|
+
def main_window
|
168
|
+
return @main_window if @main_window
|
169
|
+
|
170
|
+
timeout(main_window_timeout) do
|
171
|
+
begin
|
172
|
+
# There may be multiple instances, use title and pid to id our main window
|
173
|
+
@main_window = Autogui::EnumerateDesktopWindows.new.find do |w|
|
174
|
+
w.title.match(title) && w.pid == pid
|
175
|
+
end
|
176
|
+
sleep 0.1
|
177
|
+
end until @main_window
|
178
|
+
end
|
179
|
+
|
180
|
+
# sanity checks
|
181
|
+
raise "cannot find main_window, check application title" unless @main_window
|
182
|
+
|
183
|
+
@main_window
|
184
|
+
end
|
185
|
+
|
186
|
+
# Call the main_window's close method
|
187
|
+
#
|
188
|
+
# PostMessage SC_CLOSE and optionally wait for the window to close
|
189
|
+
#
|
190
|
+
# @param [Hash] options
|
191
|
+
# @option options [Boolean] :wait_for_close (true) sleep while waiting for timeout or close
|
192
|
+
# @option options [Boolean] :timeout (5) wait_for_close timeout in seconds
|
193
|
+
#
|
194
|
+
def close(options={})
|
195
|
+
main_window.close(options)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Send SIGKILL to force the application to die
|
199
|
+
def kill
|
200
|
+
Process::kill(9, pid)
|
201
|
+
end
|
202
|
+
|
203
|
+
# @return [Boolean] if the application is currently running
|
204
|
+
def running?
|
205
|
+
main_window && (main_window.is_window?)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Set the application input focus to the main_window
|
209
|
+
#
|
210
|
+
# @return [Number] nonzero number if sucess, nil or zero if failed
|
211
|
+
#
|
212
|
+
def set_focus
|
213
|
+
main_window.set_focus if running?
|
214
|
+
end
|
215
|
+
|
216
|
+
# The main_window text including all child windows
|
217
|
+
# joined together with newlines. Faciliates matching text.
|
218
|
+
#
|
219
|
+
# @example partial match of the Window's calulator's about dialog copywrite text
|
220
|
+
#
|
221
|
+
# dialog_about = @calculator.dialog_about
|
222
|
+
# dialog_about.title.should == "About Calculator"
|
223
|
+
# dialog_about.combined_text.should match(/Microsoft . Calculator/)
|
224
|
+
#
|
225
|
+
# @return [String] with newlines
|
226
|
+
#
|
227
|
+
def combined_text
|
228
|
+
main_window.combined_text if running?
|
229
|
+
end
|
230
|
+
|
231
|
+
# @example set the clipboard text and paste it with Control-V
|
232
|
+
#
|
233
|
+
# @calculator.edit_window.set_focus
|
234
|
+
# @calculator.clipboard.text = "12345"
|
235
|
+
# @calculator.edit_window.text.strip.should == "0."
|
236
|
+
# keystroke(VK_CONTROL, VK_V)
|
237
|
+
# @calculator.edit_window.text.strip.should == "12,345."
|
238
|
+
#
|
239
|
+
# @return [Clipboard]
|
240
|
+
#
|
241
|
+
def clipboard
|
242
|
+
@clipboard || Autogui::Clipboard.new
|
243
|
+
end
|
244
|
+
|
245
|
+
private
|
246
|
+
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
# The algorithms in this module are presented in the book
|
2
|
+
# Scripted GUI testing with Ruby by Ian Dees
|
3
|
+
# @see http://pragprog.com/titles/idgtr/scripted-gui-testing-with-ruby
|
4
|
+
|
5
|
+
require 'windows/api'
|
6
|
+
|
7
|
+
# methods for simulating user input
|
8
|
+
module Autogui
|
9
|
+
module Input
|
10
|
+
|
11
|
+
# MSDN virtual key codes
|
12
|
+
VK_LBUTTON = 0x01
|
13
|
+
VK_RBUTTON = 0x02
|
14
|
+
|
15
|
+
VK_CANCEL = 0x03
|
16
|
+
VK_BACK = 0x08
|
17
|
+
VK_TAB = 0x09
|
18
|
+
VK_CLEAR = 0x0c
|
19
|
+
VK_RETURN = 0x0d
|
20
|
+
VK_SHIFT = 0x10
|
21
|
+
VK_CONTROL = 0x11
|
22
|
+
VK_MENU = 0x12
|
23
|
+
VK_PAUSE = 0x13
|
24
|
+
VK_ESCAPE = 0x1b
|
25
|
+
VK_SPACE = 0x20
|
26
|
+
VK_PRIOR = 0x21
|
27
|
+
VK_NEXT = 0x22
|
28
|
+
VK_END = 0x23
|
29
|
+
VK_HOME = 0x24
|
30
|
+
VK_LEFT = 0x25
|
31
|
+
VK_UP = 0x26
|
32
|
+
VK_RIGHT = 0x27
|
33
|
+
VK_DOWN = 0x28
|
34
|
+
VK_SELECT = 0x29
|
35
|
+
VK_EXECUTE = 0x2b
|
36
|
+
VK_SNAPSHOT = 0x2c
|
37
|
+
VK_INSERT = 0x2d
|
38
|
+
VK_DELETE = 0x2e
|
39
|
+
VK_HELP = 0x2f
|
40
|
+
|
41
|
+
VK_0 = 0x30
|
42
|
+
VK_1 = 0x31
|
43
|
+
VK_2 = 0x32
|
44
|
+
VK_3 = 0x33
|
45
|
+
VK_4 = 0x34
|
46
|
+
VK_5 = 0x35
|
47
|
+
VK_6 = 0x36
|
48
|
+
VK_7 = 0x37
|
49
|
+
VK_8 = 0x38
|
50
|
+
VK_9 = 0x39
|
51
|
+
VK_A = 0x41
|
52
|
+
VK_B = 0x42
|
53
|
+
VK_C = 0x43
|
54
|
+
VK_D = 0x44
|
55
|
+
VK_E = 0x45
|
56
|
+
VK_F = 0x46
|
57
|
+
VK_G = 0x47
|
58
|
+
VK_H = 0x48
|
59
|
+
VK_I = 0x49
|
60
|
+
VK_J = 0x4a
|
61
|
+
VK_K = 0x4b
|
62
|
+
VK_L = 0x4c
|
63
|
+
VK_M = 0x4d
|
64
|
+
VK_N = 0x4e
|
65
|
+
VK_O = 0x4f
|
66
|
+
VK_P = 0x50
|
67
|
+
VK_Q = 0x51
|
68
|
+
VK_R = 0x52
|
69
|
+
VK_S = 0x53
|
70
|
+
VK_T = 0x54
|
71
|
+
VK_U = 0x55
|
72
|
+
VK_V = 0x56
|
73
|
+
VK_W = 0x57
|
74
|
+
VK_X = 0x58
|
75
|
+
VK_Y = 0x59
|
76
|
+
VK_Z = 0x5a
|
77
|
+
|
78
|
+
VK_LWIN = 0x5b
|
79
|
+
VK_RWIN = 0x5c
|
80
|
+
VK_APPS = 0x5d
|
81
|
+
|
82
|
+
VK_NUMPAD0 = 0x60
|
83
|
+
VK_NUMPAD1 = 0x61
|
84
|
+
VK_NUMPAD2 = 0x62
|
85
|
+
VK_NUMPAD3 = 0x63
|
86
|
+
VK_NUMPAD4 = 0x64
|
87
|
+
VK_NUMPAD5 = 0x65
|
88
|
+
VK_NUMPAD6 = 0x66
|
89
|
+
VK_NUMPAD7 = 0x67
|
90
|
+
VK_NUMPAD8 = 0x68
|
91
|
+
VK_NUMPAD9 = 0x69
|
92
|
+
VK_MULTIPLY = 0x6a
|
93
|
+
VK_ADD = 0x6b
|
94
|
+
VK_SEPARATOR = 0x6c
|
95
|
+
VK_SUBTRACT = 0x6d
|
96
|
+
VK_DECIMAL = 0x6e
|
97
|
+
VK_DIVIDE = 0x6f
|
98
|
+
|
99
|
+
VK_F1 = 0x70
|
100
|
+
VK_F2 = 0x71
|
101
|
+
VK_F3 = 0x72
|
102
|
+
VK_F4 = 0x73
|
103
|
+
VK_F5 = 0x74
|
104
|
+
VK_F6 = 0x75
|
105
|
+
VK_F7 = 0x76
|
106
|
+
VK_F8 = 0x77
|
107
|
+
VK_F9 = 0x78
|
108
|
+
VK_F10 = 0x79
|
109
|
+
VK_F11 = 0x7a
|
110
|
+
VK_F12 = 0x7b
|
111
|
+
|
112
|
+
VK_NUMLOCK = 0x90
|
113
|
+
VK_SCROLL = 0x91
|
114
|
+
VK_OEM_EQU = 0x92
|
115
|
+
VK_LSHIFT = 0xa0
|
116
|
+
VK_RSHIFT = 0xa1
|
117
|
+
VK_LCONTROL = 0xa2
|
118
|
+
VK_RCONTROL = 0xa3
|
119
|
+
VK_LMENU = 0xa4
|
120
|
+
VK_RMENU = 0xa5
|
121
|
+
|
122
|
+
VK_OEM_1 = 0xba
|
123
|
+
VK_OEM_PLUS = 0xbb
|
124
|
+
VK_OEM_COMMA = 0xbc
|
125
|
+
VK_OEM_MINUS = 0xbd
|
126
|
+
VK_OEM_PERIOD = 0xbe
|
127
|
+
VK_OEM_2 = 0xbf
|
128
|
+
VK_OEM_3 = 0xc0 # US '~' key
|
129
|
+
VK_OEM_4 = 0xdb
|
130
|
+
VK_OEM_5 = 0xdc # US '\' key
|
131
|
+
VK_OEM_6 = 0xdd
|
132
|
+
VK_OEM_7 = 0xde # US quotes key
|
133
|
+
VK_OEM_8 = 0xdf
|
134
|
+
|
135
|
+
# delay in seconds between keystrokes
|
136
|
+
KEYBD_KEYDELAY = 0.050
|
137
|
+
|
138
|
+
# keybd_event
|
139
|
+
KEYBD_EVENT_KEYUP = 2
|
140
|
+
KEYBD_EVENT_KEYDOWN = 0
|
141
|
+
|
142
|
+
Windows::API.auto_namespace = 'Autogui::Input'
|
143
|
+
Windows::API.auto_constant = true
|
144
|
+
Windows::API.auto_method = true
|
145
|
+
Windows::API.auto_unicode = false
|
146
|
+
|
147
|
+
Windows::API.new('keybd_event', 'IILL', 'V', 'user32')
|
148
|
+
Windows::API.new('mouse_event', 'LLLLL', 'V', 'user32')
|
149
|
+
|
150
|
+
# Send keystroke to the focused window, keystrokes are virtual keycodes
|
151
|
+
#
|
152
|
+
# @example send 2+2<CR>
|
153
|
+
#
|
154
|
+
# keystroke(VK_2, VK_ADD, VK_2, VK_RETURN)
|
155
|
+
#
|
156
|
+
def keystroke(*keys)
|
157
|
+
return if keys.empty?
|
158
|
+
|
159
|
+
keybd_event keys.first, 0, KEYBD_EVENT_KEYDOWN, 0
|
160
|
+
sleep KEYBD_KEYDELAY
|
161
|
+
keystroke *keys[1..-1]
|
162
|
+
sleep KEYBD_KEYDELAY
|
163
|
+
keybd_event keys.first, 0, KEYBD_EVENT_KEYUP, 0
|
164
|
+
end
|
165
|
+
|
166
|
+
# String together keystrokes, simulates the user typing.
|
167
|
+
#
|
168
|
+
# Note: This method can be slow for large strings. Consider using
|
169
|
+
# the clipboard instead.
|
170
|
+
#
|
171
|
+
# @see Clipboard
|
172
|
+
#
|
173
|
+
# @example send 2+2<CR>
|
174
|
+
#
|
175
|
+
# type_in("2+2\n")
|
176
|
+
#
|
177
|
+
# @param [String] string of characters to simulate typing
|
178
|
+
def type_in(string)
|
179
|
+
string.each_char do |char|
|
180
|
+
keystroke(*char_to_virtual_keycode(char))
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
# convert a single character to a virtual keycode
|
187
|
+
#
|
188
|
+
# @param [Char] char is the character to convert
|
189
|
+
#
|
190
|
+
# @return [Array] of virtual keycodes
|
191
|
+
def char_to_virtual_keycode(char)
|
192
|
+
|
193
|
+
unless char.size == 1
|
194
|
+
raise "virtual keycode conversion is for single characters only"
|
195
|
+
end
|
196
|
+
|
197
|
+
code = char.unpack('U')[0]
|
198
|
+
|
199
|
+
case char
|
200
|
+
when '0'..'9'
|
201
|
+
[code - ?0 + 0x30]
|
202
|
+
when 'A'..'Z'
|
203
|
+
[VK_SHIFT, code]
|
204
|
+
when 'a'..'z'
|
205
|
+
[code - ?a + ?A]
|
206
|
+
when ' '
|
207
|
+
[code]
|
208
|
+
when '+'
|
209
|
+
[VK_ADD]
|
210
|
+
when '='
|
211
|
+
[VK_OEM_PLUS]
|
212
|
+
when ','
|
213
|
+
[VK_OEM_COMMA]
|
214
|
+
when '.'
|
215
|
+
[VK_OEM_PERIOD]
|
216
|
+
when '-'
|
217
|
+
[VK_OEM_MINUS]
|
218
|
+
when '_'
|
219
|
+
[VK_SHIFT, VK_OEM_MINUS]
|
220
|
+
when ':'
|
221
|
+
[VK_SHIFT, VK_OEM_1]
|
222
|
+
when ';'
|
223
|
+
[VK_OEM_1]
|
224
|
+
when '\''
|
225
|
+
[VK_OEM_7]
|
226
|
+
when '\"'
|
227
|
+
[VK_SHIFT, VK_OEM_7]
|
228
|
+
when "\\"
|
229
|
+
[VK_OEM_5]
|
230
|
+
when "\n"
|
231
|
+
[VK_RETURN]
|
232
|
+
else
|
233
|
+
raise "No conversion exists for character #{char}"
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
end
|