win_gui 0.2.17 → 0.2.18
Sign up to get free protection for your applications and to get access to all the features.
- 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
|