win32-autogui 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|