win_gui 0.2.17 → 0.2.18
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/HISTORY +4 -0
- data/README.rdoc +11 -12
- data/VERSION +1 -1
- data/lib/win_gui/window.rb +44 -19
- data/spec/win_gui/window_spec.rb +148 -114
- metadata +4 -4
data/HISTORY
CHANGED
data/README.rdoc
CHANGED
@@ -7,7 +7,8 @@ url:: http://github.com/arvicco/win_gui
|
|
7
7
|
WinGui is a module that provides higher-level abstractions/wrappers around GUI-related
|
8
8
|
Win32 API functions. It uses Win gem as a basis, which in turn uses FFI.
|
9
9
|
So (in theory) it should work for any Ruby implementation supporting FFI. In practice,
|
10
|
-
it's only been tested under mingw and cygwin flavors of Ruby 1.9.
|
10
|
+
it's only been tested under mingw and cygwin flavors of Ruby 1.9. Porting to JRuby is
|
11
|
+
under way, waiting for its 1.9 compatibility to mature.
|
11
12
|
|
12
13
|
== SUMMARY:
|
13
14
|
|
@@ -32,26 +33,24 @@ For example, here is how you deal with typical GUI-related tasks using Win:
|
|
32
33
|
end
|
33
34
|
close_window(window_handle)
|
34
35
|
|
35
|
-
|
36
|
+
This works fine, but such functional style is does not feel like object-oriented Ruby.
|
37
|
+
Ideally, there should be a thin wrapper class around window handle, and the code above
|
38
|
+
should be more like this:
|
39
|
+
|
36
40
|
require 'win_gui'
|
37
41
|
include WinGui
|
38
42
|
|
39
|
-
window = Window.find(:
|
43
|
+
window = Window.find(:title => /PartTitle/)
|
40
44
|
puts window.handle, window.title, window.thread, window.process
|
41
|
-
window.
|
45
|
+
window.children.each {|child| puts child.handle, child.title, child.thread, child.process }
|
42
46
|
window.close
|
43
47
|
|
44
|
-
|
45
|
-
Windows GUI-related code much more fun than it is right now.
|
48
|
+
WinGui library strives to provide such wrappers and convenience methods that will make
|
49
|
+
working with Windows GUI-related code much more fun than it is right now.
|
46
50
|
|
47
51
|
== REQUIREMENTS:
|
48
52
|
|
49
|
-
Only works with Ruby 1.9
|
50
|
-
|
51
|
-
== FEATURES/PROBLEMS:
|
52
|
-
|
53
|
-
This project is quite new, so it may be not suitable for production-quality systems yet.
|
54
|
-
Contributors always welcome!
|
53
|
+
Only works with Ruby 1.9 compatible implementations since Win gem uses some of latest Ruby features.
|
55
54
|
|
56
55
|
== INSTALL:
|
57
56
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.2.
|
1
|
+
0.2.18
|
data/lib/win_gui/window.rb
CHANGED
@@ -14,7 +14,7 @@ module WinGui
|
|
14
14
|
# Returns either Window instance (for a found handle) or nil if nothing found.
|
15
15
|
# Private method to dry up other window lookup methods
|
16
16
|
#
|
17
|
-
def lookup_window
|
17
|
+
def lookup_window opts # :yields: index, position
|
18
18
|
# Need this to avoid handle considered local var in begin..end block
|
19
19
|
handle = yield
|
20
20
|
if opts[:timeout]
|
@@ -30,40 +30,63 @@ module WinGui
|
|
30
30
|
Window.new(handle) if handle
|
31
31
|
end
|
32
32
|
|
33
|
+
def lookup_window_in_collection opts, &collection_proc
|
34
|
+
class_name = opts[:class]
|
35
|
+
title = opts[:title]
|
36
|
+
id = opts[:id]
|
37
|
+
class_regexp = class_name.is_a? Regexp
|
38
|
+
title_regexp = title.is_a? Regexp
|
39
|
+
|
40
|
+
lookup_window(opts) do
|
41
|
+
collection_proc.call.each do |handle|
|
42
|
+
win = Window.new handle
|
43
|
+
|
44
|
+
id_match = !id || win.id == id
|
45
|
+
title_match = !title || win.title == title ||
|
46
|
+
title_regexp && win.title =~ title
|
47
|
+
class_match = !class_name || win.class_name == class_name ||
|
48
|
+
class_regexp && win.class_name =~ class_name
|
49
|
+
return win if class_match && title_match && id_match
|
50
|
+
end
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
33
55
|
# Finds top level window by title/class, returns wrapped Window object or nil (raises exception if asked to).
|
34
56
|
# If timeout option given, waits for window to appear within timeout, returns nil if it didn't.
|
35
57
|
# Options:
|
36
|
-
# :title:: window title
|
37
|
-
# :class:: window class
|
58
|
+
# :title:: window title (String or Regexp)
|
59
|
+
# :class:: window class (String or Regexp)
|
38
60
|
# :timeout:: timeout (seconds)
|
39
61
|
# :raise:: raise this exception instead of returning nil if nothing found
|
40
62
|
#
|
41
|
-
def top_level
|
42
|
-
|
63
|
+
def top_level opts={}
|
64
|
+
if opts[:class].is_a?(Regexp) or opts[:title].is_a?(Regexp)
|
65
|
+
lookup_window_in_collection(opts) { WinGui.enum_windows }
|
66
|
+
else
|
67
|
+
lookup_window(opts) { WinGui.find_window opts[:class], opts[:title] }
|
68
|
+
end
|
43
69
|
end
|
44
|
-
|
70
|
+
|
71
|
+
alias find top_level
|
45
72
|
end
|
46
73
|
|
47
74
|
# Finds child window (control) by either control ID or window class/title.
|
48
75
|
# By default, only direct children are searched.
|
49
76
|
# Options:
|
50
77
|
# :id:: integer control id (such as IDOK, IDCANCEL, etc)
|
51
|
-
# :title:: window title
|
52
|
-
# :class:: window class
|
78
|
+
# :title:: window title (String or Regexp)
|
79
|
+
# :class:: window class (String or Regexp)
|
53
80
|
# :indirect:: search all descendants, not only direct children
|
54
81
|
# :timeout:: timeout (seconds)
|
55
82
|
# :raise:: raise this exception instead of returning nil if nothing found
|
56
|
-
# TODO: add the ability to nail indirect children as well
|
57
83
|
#
|
58
84
|
def child(opts={})
|
59
85
|
if opts[:indirect]
|
60
|
-
self.class.
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
(opts[:title] ? child.title == opts[:title] : true)
|
65
|
-
end
|
66
|
-
found.handle if found
|
86
|
+
self.class.lookup_window_in_collection(opts) { enum_child_windows }
|
87
|
+
elsif opts[:class].is_a?(Regexp) or opts[:title].is_a?(Regexp)
|
88
|
+
self.class.lookup_window_in_collection(opts) do
|
89
|
+
enum_child_windows.select { |handle| child? handle }
|
67
90
|
end
|
68
91
|
else
|
69
92
|
self.class.lookup_window opts do
|
@@ -106,9 +129,9 @@ module WinGui
|
|
106
129
|
WinGui.set_cursor_pos *point
|
107
130
|
|
108
131
|
button = opts[:mouse_button] || opts[:mouse] || opts[:which]
|
109
|
-
down, up =
|
110
|
-
|
111
|
-
|
132
|
+
down, up = (button == :right) ?
|
133
|
+
[WinGui::MOUSEEVENTF_RIGHTDOWN, WinGui::MOUSEEVENTF_RIGHTUP] :
|
134
|
+
[WinGui::MOUSEEVENTF_LEFTDOWN, WinGui::MOUSEEVENTF_LEFTUP]
|
112
135
|
|
113
136
|
WinGui.mouse_event down, 0, 0, 0, 0
|
114
137
|
WinGui.mouse_event up, 0, 0, 0, 0
|
@@ -150,6 +173,8 @@ module WinGui
|
|
150
173
|
get_window_thread_process_id.last
|
151
174
|
end
|
152
175
|
|
176
|
+
alias pid process
|
177
|
+
|
153
178
|
# Control ID associated with the window (only makes sense for controls)
|
154
179
|
def id
|
155
180
|
get_dlg_ctrl_id
|
data/spec/win_gui/window_spec.rb
CHANGED
@@ -1,5 +1,104 @@
|
|
1
1
|
require "spec_helper.rb"
|
2
2
|
|
3
|
+
shared_examples_for 'normal window finder' do
|
4
|
+
it 'finds top-level window by title and wraps it in a Window object' do
|
5
|
+
window = Window.top_level(:title => title, timeout: 1)
|
6
|
+
window.handle.should == @win.handle
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'finds top-level window by class and wraps it in a Window object' do
|
10
|
+
window = Window.top_level(:class => class_name, timeout: 1)
|
11
|
+
window.handle.should == @win.handle
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'finds top-level window by both class and title' do
|
15
|
+
window = Window.top_level(:class => class_name, :title => title)
|
16
|
+
window.handle.should == @win.handle
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'finds top-level window by both class and title' do
|
20
|
+
window = Window.top_level(:class => class_name, :title => title)
|
21
|
+
window.handle.should == @win.handle
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'returns nil if either window title or class do not match' do
|
25
|
+
Window.top_level(:class => class_name, :title => impossible).should == nil
|
26
|
+
Window.top_level(:class => impossible, :title => title).should == nil
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'returns nil immediately if top-level window not found' do
|
30
|
+
start = Time.now
|
31
|
+
Window.top_level(:title => impossible).should == nil
|
32
|
+
(Time.now - start).should be_within(0.05).of 0
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'returns nil after timeout if top-level window not found' do
|
36
|
+
start = Time.now
|
37
|
+
Window.top_level(:title => impossible, :timeout => 0.3).should == nil
|
38
|
+
(Time.now - start).should be_within(0.05).of 0.3
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'raises exception if asked to' do
|
42
|
+
expect { Window.top_level(:title => impossible, :raise => "Horror!") }.to raise_error "Horror!"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
shared_examples_for 'child window finder' do
|
47
|
+
|
48
|
+
it 'finds ANY child window without specific args' do
|
49
|
+
use { @child = @win.child(opts) }
|
50
|
+
@child.should_not == nil
|
51
|
+
@win.child?(@child.handle).should == true
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'returns nil immediately if specific child not found' do
|
55
|
+
start = Time.now
|
56
|
+
@win.child(opts.merge(:title => impossible)).should == nil
|
57
|
+
(Time.now - start).should be_within(0.05).of 0
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'returns nil after timeout if specific child not found' do
|
61
|
+
start = Time.now
|
62
|
+
@win.child(opts.merge(:title => impossible, :timeout => 0.5)).should == nil
|
63
|
+
(Time.now - start).should be_within(0.05).of 0.5
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'finds child window by class and returns it as a Window object (no timeout)' do
|
67
|
+
child = @win.child opts.merge(:class => class_name)
|
68
|
+
child.should_not == nil
|
69
|
+
@win.child?(child.handle).should == true
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'finds child window by class and returns it as a Window object (with timeout)' do
|
73
|
+
child = @win.child opts.merge(:class => class_name, :timeout => 0.5)
|
74
|
+
child.should_not == nil
|
75
|
+
@win.child?(child.handle).should == true
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'finds child with specific text and returns it as a Window object' do
|
79
|
+
with_dialog(:save) do |dialog|
|
80
|
+
child = dialog.child opts.merge(:title => cancel)
|
81
|
+
child.should_not == nil
|
82
|
+
dialog.child?(child.handle).should == true
|
83
|
+
child.get_dlg_ctrl_id.should == IDCANCEL
|
84
|
+
|
85
|
+
child = dialog.child opts.merge(:title => "&Save")
|
86
|
+
child.should_not == nil
|
87
|
+
dialog.child?(child.handle).should == true
|
88
|
+
child.get_dlg_ctrl_id.should == IDOK
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'finds child control with a given ID and returns it as a Window object' do
|
93
|
+
with_dialog(:save) do |dialog|
|
94
|
+
child = dialog.child opts.merge(:id => IDCANCEL)
|
95
|
+
child.should_not == nil
|
96
|
+
dialog.child?(child.handle).should == true
|
97
|
+
child.text.should == "Cancel"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
3
102
|
describe WinGui::Window do
|
4
103
|
before(:each) { @win = launch_test_app.main_window }
|
5
104
|
after(:each) { close_test_app }
|
@@ -51,18 +150,17 @@ describe WinGui::Window do
|
|
51
150
|
|
52
151
|
it 'has class_name and text/title properties (derived from WinGui function calls)' do
|
53
152
|
@win.class_name.should == WIN_CLASS
|
54
|
-
# text
|
153
|
+
# text property accessed by sending WM_GETTEXT directly to window (convenience method in WinGui)
|
55
154
|
@win.text.should == WIN_TITLE
|
56
|
-
# window_text
|
155
|
+
# window_text property accessed via GetWindowText
|
57
156
|
@win.window_text.should == WIN_TITLE
|
58
157
|
# title property is just an alias for window_text
|
59
158
|
@win.title.should == WIN_TITLE
|
60
159
|
end
|
61
160
|
|
62
|
-
it 'has thread and process properties derived from get_window_thread_process_id' do
|
63
|
-
thread
|
64
|
-
|
65
|
-
[thread, process].should == get_window_thread_process_id(@win.handle)
|
161
|
+
it 'has thread and process(pid) properties derived from get_window_thread_process_id' do
|
162
|
+
[@win.thread, @win.process].should == get_window_thread_process_id(@win.handle)
|
163
|
+
@win.pid.should == @win.process
|
66
164
|
end
|
67
165
|
|
68
166
|
it 'has id property that only makes sense for controls' do
|
@@ -71,143 +169,79 @@ describe WinGui::Window do
|
|
71
169
|
end
|
72
170
|
|
73
171
|
describe '::top_level' do
|
74
|
-
it 'finds top-level window by title and wraps it in a Window object' do
|
75
|
-
window = Window.top_level(title: WIN_TITLE, timeout: 1)
|
76
|
-
window.handle.should == @win.handle
|
77
|
-
end
|
78
|
-
|
79
|
-
it 'finds top-level window by class and wraps it in a Window object' do
|
80
|
-
window = Window.top_level(class: WIN_CLASS, timeout: 1)
|
81
|
-
window.handle.should == @win.handle
|
82
|
-
end
|
83
|
-
|
84
172
|
it 'finds ANY top-level window without args and wraps it in a Window object' do
|
85
173
|
use { @window = Window.top_level() }
|
86
174
|
@window.should be_a Window
|
87
175
|
end
|
88
176
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
(
|
93
|
-
end
|
177
|
+
context 'with String arguments' do
|
178
|
+
let(:title) { WIN_TITLE }
|
179
|
+
let(:class_name) { WIN_CLASS }
|
180
|
+
let(:impossible) { IMPOSSIBLE }
|
94
181
|
|
95
|
-
|
96
|
-
start = Time.now
|
97
|
-
Window.top_level(title: IMPOSSIBLE, timeout: 0.3).should == nil
|
98
|
-
(Time.now - start).should be_within(0.03).of 0.3
|
182
|
+
it_should_behave_like 'normal window finder'
|
99
183
|
end
|
100
184
|
|
101
|
-
|
102
|
-
|
103
|
-
|
185
|
+
context 'with Regexp arguments' do
|
186
|
+
let(:title) { Regexp.new WIN_TITLE[-6..-1] }
|
187
|
+
let(:class_name) { Regexp.new WIN_CLASS[-6..-1] }
|
188
|
+
let(:impossible) { Regexp.new IMPOSSIBLE }
|
104
189
|
|
105
|
-
|
106
|
-
use { @window = Window.find() }
|
107
|
-
@window.should be_a Window
|
190
|
+
it_should_behave_like 'normal window finder'
|
108
191
|
end
|
109
|
-
end # describe .top_level
|
110
192
|
|
111
|
-
|
112
|
-
|
193
|
+
context 'with mixed arguments' do
|
194
|
+
let(:title) { Regexp.new WIN_TITLE[-6..-1] }
|
195
|
+
let(:class_name) { WIN_CLASS }
|
196
|
+
let(:impossible) { IMPOSSIBLE }
|
113
197
|
|
114
|
-
|
115
|
-
start = Time.now
|
116
|
-
@win.child(title: IMPOSSIBLE).should == nil
|
117
|
-
(Time.now - start).should be_within(0.03).of 0
|
118
|
-
end
|
119
|
-
|
120
|
-
it 'returns nil after timeout if specific child not found' do
|
121
|
-
start = Time.now
|
122
|
-
@win.child(title: IMPOSSIBLE, timeout: 0.5).should == nil
|
123
|
-
(Time.now - start).should be_within(0.03).of 0.5
|
198
|
+
it_should_behave_like 'normal window finder'
|
124
199
|
end
|
125
200
|
|
126
|
-
|
127
|
-
use { @child = @win.child() }
|
128
|
-
@child.should_not == nil
|
129
|
-
@win.child?(@child.handle).should == true
|
130
|
-
end
|
201
|
+
end # describe .top_level
|
131
202
|
|
132
|
-
|
133
|
-
|
134
|
-
child.should_not == nil
|
135
|
-
@win.child?(child.handle).should == true
|
136
|
-
end
|
203
|
+
describe '#child' do
|
204
|
+
spec { use { @child = @win.child(title: "Title", class: "Class", id: 0) } }
|
137
205
|
|
138
|
-
|
139
|
-
|
140
|
-
child.should_not == nil
|
206
|
+
context 'direct children only' do
|
207
|
+
let(:opts) { {} }
|
141
208
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
end
|
209
|
+
context 'with String arguments' do
|
210
|
+
let(:cancel) { 'Cancel' }
|
211
|
+
let(:class_name) { TEXTAREA_CLASS }
|
212
|
+
let(:impossible) { IMPOSSIBLE }
|
147
213
|
|
148
|
-
|
149
|
-
with_dialog(:save) do |dialog|
|
150
|
-
child = dialog.child(title: "Cancel")
|
151
|
-
child.should_not == nil
|
152
|
-
dialog.child?(child.handle).should == true
|
153
|
-
child.get_dlg_ctrl_id.should == IDCANCEL
|
154
|
-
|
155
|
-
child = dialog.child(title: "&Save")
|
156
|
-
child.should_not == nil
|
157
|
-
dialog.child?(child.handle).should == true
|
158
|
-
child.get_dlg_ctrl_id.should == IDOK
|
214
|
+
it_should_behave_like 'child window finder'
|
159
215
|
end
|
160
|
-
end
|
161
216
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
child
|
217
|
+
context 'with Regexp arguments' do
|
218
|
+
let(:cancel) { /Cancel/ }
|
219
|
+
let(:class_name) { Regexp.new TEXTAREA_CLASS[-6..-1] }
|
220
|
+
let(:impossible) { Regexp.new IMPOSSIBLE }
|
221
|
+
|
222
|
+
it_should_behave_like 'child window finder'
|
168
223
|
end
|
169
224
|
end
|
170
225
|
|
171
|
-
context 'indirect
|
172
|
-
|
173
|
-
@win.child(title: IMPOSSIBLE, indirect: true).should == nil
|
174
|
-
end
|
226
|
+
context 'indirect children' do
|
227
|
+
let(:opts) { {:indirect => true} }
|
175
228
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
end
|
229
|
+
context 'with String arguments' do
|
230
|
+
let(:cancel) { 'Cancel' }
|
231
|
+
let(:class_name) { TEXTAREA_CLASS }
|
232
|
+
let(:impossible) { IMPOSSIBLE }
|
181
233
|
|
182
|
-
|
183
|
-
child = @win.child(class: TEXTAREA_CLASS, indirect: true)
|
184
|
-
child.should_not == nil
|
185
|
-
@win.child?(child.handle).should == true
|
234
|
+
it_should_behave_like 'child window finder'
|
186
235
|
end
|
187
236
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
dialog.child?(child.handle).should == true
|
193
|
-
child.id.should == IDCANCEL
|
194
|
-
|
195
|
-
child = dialog.child(title: "&Save", indirect: true)
|
196
|
-
child.should_not == nil
|
197
|
-
dialog.child?(child.handle).should == true
|
198
|
-
child.id.should == IDOK
|
199
|
-
end
|
200
|
-
end
|
237
|
+
context 'with Regexp arguments' do
|
238
|
+
let(:cancel) { /Cancel/ }
|
239
|
+
let(:class_name) { Regexp.new TEXTAREA_CLASS[-6..-1] }
|
240
|
+
let(:impossible) { Regexp.new IMPOSSIBLE }
|
201
241
|
|
202
|
-
|
203
|
-
with_dialog(:save) do |dialog|
|
204
|
-
child = dialog.child(id: IDCANCEL, indirect: true)
|
205
|
-
child.should_not == nil
|
206
|
-
dialog.child?(child.handle).should == true
|
207
|
-
child.text.should == "Cancel"
|
208
|
-
end
|
242
|
+
it_should_behave_like 'child window finder'
|
209
243
|
end
|
210
|
-
end
|
244
|
+
end
|
211
245
|
end # describe child
|
212
246
|
|
213
247
|
describe '#children' do
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: win_gui
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.2.
|
5
|
+
version: 0.2.18
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- arvicco
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-02-
|
13
|
+
date: 2011-02-17 00:00:00 +03:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -46,7 +46,7 @@ dependencies:
|
|
46
46
|
version: 0.3.1
|
47
47
|
type: :runtime
|
48
48
|
version_requirements: *id003
|
49
|
-
description: Abstractions/wrappers around GUI-related Win32 API functions
|
49
|
+
description: Work with Windows GUI in an object-oriented way. Abstractions/wrappers around GUI-related Win32 API functions
|
50
50
|
email: arvitallian@gmail.com
|
51
51
|
executables: []
|
52
52
|
|
@@ -115,7 +115,7 @@ rubyforge_project:
|
|
115
115
|
rubygems_version: 1.5.0
|
116
116
|
signing_key:
|
117
117
|
specification_version: 3
|
118
|
-
summary:
|
118
|
+
summary: Work with Windows GUI in an object-oriented way
|
119
119
|
test_files:
|
120
120
|
- spec/extension_spec.rb
|
121
121
|
- spec/spec_helper.rb
|