wineole 0.1.0 → 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.
- checksums.yaml +4 -4
- data/bin/wineole-vba +45 -0
- data/lib/wineole/client.rb +454 -20
- data/lib/wineole/dispatcher.rb +534 -0
- data/lib/wineole/errors.rb +6 -0
- data/lib/wineole/events.rb +353 -0
- data/lib/wineole/msoffice/address.rb +67 -0
- data/lib/wineole/msoffice/book.rb +114 -0
- data/lib/wineole/msoffice/color.rb +87 -0
- data/lib/wineole/msoffice/controls.rb +450 -0
- data/lib/wineole/msoffice/excel.rb +311 -0
- data/lib/wineole/msoffice/format.rb +362 -0
- data/lib/wineole/msoffice/forms.rb +163 -0
- data/lib/wineole/msoffice/passthrough.rb +46 -0
- data/lib/wineole/msoffice/paths.rb +62 -0
- data/lib/wineole/msoffice/range.rb +149 -0
- data/lib/wineole/msoffice/sheet.rb +101 -0
- data/lib/wineole/msoffice/vba.rb +199 -0
- data/lib/wineole/msoffice/vba_api.rb +366 -0
- data/lib/wineole/msoffice/vba_block.rb +144 -0
- data/lib/wineole/msoffice.rb +28 -0
- data/lib/wineole/proxy.rb +126 -15
- data/lib/wineole.rb +10 -1
- data/wineole-bridge-dist/aarch64-pc-windows-gnullvm/wineole-bridge.exe +0 -0
- data/wineole-bridge-dist/i686-pc-windows-gnu/wineole-bridge.exe +0 -0
- data/wineole-bridge-dist/x86_64-pc-windows-gnu/wineole-bridge.exe +0 -0
- metadata +21 -2
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
require_relative '../proxy'
|
|
2
|
+
require_relative '../errors'
|
|
3
|
+
require_relative 'passthrough'
|
|
4
|
+
require_relative 'vba_api'
|
|
5
|
+
|
|
6
|
+
module WineOLE
|
|
7
|
+
module MSOffice
|
|
8
|
+
# The tables and checks the three control collections share. Everything
|
|
9
|
+
# here either maps a Ruby-side name onto the COM name Excel wants, or
|
|
10
|
+
# refuses a call before Excel is touched -- the wrapper's job is to make
|
|
11
|
+
# the passthrough's traps unreachable, not to add features to Excel.
|
|
12
|
+
module Controls
|
|
13
|
+
# Legacy form controls: kind => the Worksheet collection whose Add
|
|
14
|
+
# makes one. Every collection takes (Left, Top, Width, Height).
|
|
15
|
+
# EditBox is missing on purpose: it exists only on dialog sheets.
|
|
16
|
+
FORM_KINDS = {
|
|
17
|
+
button: 'Buttons',
|
|
18
|
+
check_box: 'CheckBoxes',
|
|
19
|
+
option_button: 'OptionButtons',
|
|
20
|
+
list_box: 'ListBoxes',
|
|
21
|
+
drop_down: 'DropDowns',
|
|
22
|
+
spinner: 'Spinners',
|
|
23
|
+
scroll_bar: 'ScrollBars',
|
|
24
|
+
label: 'Labels',
|
|
25
|
+
group_box: 'GroupBoxes'
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# Shape.FormControlType => kind, for re-binding an existing shape.
|
|
29
|
+
# 3 (xlEditBox) is absent for the reason above, so it maps to nil.
|
|
30
|
+
FORM_CONTROL_TYPES = {
|
|
31
|
+
0 => :button,
|
|
32
|
+
1 => :check_box,
|
|
33
|
+
2 => :drop_down,
|
|
34
|
+
4 => :group_box,
|
|
35
|
+
5 => :label,
|
|
36
|
+
6 => :list_box,
|
|
37
|
+
7 => :option_button,
|
|
38
|
+
8 => :scroll_bar,
|
|
39
|
+
9 => :spinner
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
# MSForms 2.0: kind => ProgID. Shorthand only -- any String is passed
|
|
43
|
+
# to Excel verbatim as a ProgID, which is how a control outside this
|
|
44
|
+
# table gets placed.
|
|
45
|
+
MSFORMS_KINDS = {
|
|
46
|
+
command_button: 'Forms.CommandButton.1',
|
|
47
|
+
text_box: 'Forms.TextBox.1',
|
|
48
|
+
combo_box: 'Forms.ComboBox.1',
|
|
49
|
+
list_box: 'Forms.ListBox.1',
|
|
50
|
+
check_box: 'Forms.CheckBox.1',
|
|
51
|
+
option_button: 'Forms.OptionButton.1',
|
|
52
|
+
toggle_button: 'Forms.ToggleButton.1',
|
|
53
|
+
spin_button: 'Forms.SpinButton.1',
|
|
54
|
+
scroll_bar: 'Forms.ScrollBar.1',
|
|
55
|
+
label: 'Forms.Label.1',
|
|
56
|
+
image: 'Forms.Image.1'
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
59
|
+
# A worksheet ActiveX control is two objects: the OLEObject Excel
|
|
60
|
+
# wraps it in, and the MSForms control inside (OLEObject.Object).
|
|
61
|
+
# These keys belong to the host; everything else goes inside.
|
|
62
|
+
HOST_PROPS = %i[linked_cell list_fill_range visible print_object placement].freeze
|
|
63
|
+
|
|
64
|
+
# A VBA identifier. Excel accepts any string as a shape name, but a
|
|
65
|
+
# name that cannot appear in `Name_Click` is a control that can be
|
|
66
|
+
# placed and never handled.
|
|
67
|
+
VBA_NAME = /\A[A-Za-z][A-Za-z0-9_]{0,30}\z/.freeze
|
|
68
|
+
|
|
69
|
+
def self.form_collection_for(kind)
|
|
70
|
+
return FORM_KINDS[kind] if FORM_KINDS.key?(kind)
|
|
71
|
+
|
|
72
|
+
if kind.is_a?(::String)
|
|
73
|
+
raise ArgumentError,
|
|
74
|
+
"form controls have no ProgID (#{kind.inspect} given): a String kind is for " \
|
|
75
|
+
'sheet.activex and form.controls. Form control kinds are ' \
|
|
76
|
+
"#{FORM_KINDS.keys.map(&:inspect).join(', ')}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
raise ArgumentError,
|
|
80
|
+
"unknown form control kind #{kind.inspect} -- expected one of " \
|
|
81
|
+
"#{FORM_KINDS.keys.map(&:inspect).join(', ')}. For an ActiveX control " \
|
|
82
|
+
'(events reach Ruby) use sheet.activex'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.progid_for(kind)
|
|
86
|
+
return kind if kind.is_a?(::String)
|
|
87
|
+
return MSFORMS_KINDS[kind] if MSFORMS_KINDS.key?(kind)
|
|
88
|
+
|
|
89
|
+
raise ArgumentError,
|
|
90
|
+
"unknown ActiveX kind #{kind.inspect} -- expected one of " \
|
|
91
|
+
"#{MSFORMS_KINDS.keys.map(&:inspect).join(', ')}, or pass a ProgID String " \
|
|
92
|
+
'for any other registered control'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.kind_for_progid(progid)
|
|
96
|
+
MSFORMS_KINDS.key(progid) || progid
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.check_name!(name)
|
|
100
|
+
return if name.is_a?(::String) && name.match?(VBA_NAME)
|
|
101
|
+
|
|
102
|
+
raise ArgumentError,
|
|
103
|
+
"name: must be a VBA identifier -- a letter, then letters, digits or " \
|
|
104
|
+
"underscores, at most 31 characters -- because it becomes the `Name_Click` " \
|
|
105
|
+
"handler name. Got #{name.inspect}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.check_event!(event)
|
|
109
|
+
return if event.is_a?(::String) && event.match?(VBA_NAME)
|
|
110
|
+
|
|
111
|
+
raise ArgumentError,
|
|
112
|
+
"an event name is a VBA identifier such as 'Click' or 'KeyDown'. Got #{event.inspect}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Excel allows two shapes with the same name in silence, and a UserForm
|
|
116
|
+
# allows two controls with the same name in silence; after that
|
|
117
|
+
# `Name_Click` means either. A lookup that raises is the free case.
|
|
118
|
+
def self.check_free!(host, name, what)
|
|
119
|
+
host.Item(name)
|
|
120
|
+
rescue WineOLE::RemoteError
|
|
121
|
+
nil
|
|
122
|
+
else
|
|
123
|
+
raise ArgumentError,
|
|
124
|
+
"this #{what} already has a control named #{name.inspect}. Excel would add " \
|
|
125
|
+
'a second one silently and then Name_Click would be ambiguous; pick another ' \
|
|
126
|
+
'name or remove the existing control first'
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# snake_case => PascalCase. COM matches names case-insensitively, so a
|
|
130
|
+
# key already in PascalCase survives the round trip well enough.
|
|
131
|
+
def self.pascal(key)
|
|
132
|
+
key.to_s.split('_').map(&:capitalize).join
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.put(target, key, value)
|
|
136
|
+
target.public_send("#{pascal(key)}=", value)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Either `at:` (a range on `sheet`, read for its box) or all four
|
|
140
|
+
# points. Never a mix, never a partial box, never a default -- "wherever
|
|
141
|
+
# Excel puts it" stays a passthrough behaviour. `sheet: nil` is a
|
|
142
|
+
# UserForm, which has no cells for `at:` to name.
|
|
143
|
+
def self.geometry(sheet:, at:, left:, top:, width:, height:)
|
|
144
|
+
points = { left: left, top: top, width: width, height: height }
|
|
145
|
+
given = points.reject { |_, v| v.nil? }.keys
|
|
146
|
+
|
|
147
|
+
if !at.nil? && !given.empty?
|
|
148
|
+
raise ArgumentError,
|
|
149
|
+
"give either at: or left:/top:/width:/height:, not both (got at: #{at.inspect} " \
|
|
150
|
+
"and #{given.inspect})"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
unless at.nil?
|
|
154
|
+
if sheet.nil?
|
|
155
|
+
raise ArgumentError,
|
|
156
|
+
'a UserForm has no cells; give left:, top:, width: and height: in points'
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
range = sheet[at].ole
|
|
160
|
+
return [range.Left, range.Top, range.Width, range.Height]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
if given.empty?
|
|
164
|
+
raise ArgumentError,
|
|
165
|
+
'no position given: pass at: "B2:C4" (a range on this sheet) or all four of ' \
|
|
166
|
+
'left:, top:, width: and height: (points). There is no default position'
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
if given.length < 4
|
|
170
|
+
raise ArgumentError,
|
|
171
|
+
"left:, top:, width: and height: must all be given (missing #{(points.keys - given).inspect})"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
[left, top, width, height]
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# One placed control, from any of the three families.
|
|
179
|
+
#
|
|
180
|
+
# TWO OBJECTS, ONE WRAPPER. A worksheet ActiveX control is an OLEObject
|
|
181
|
+
# (Excel's host: Left, Top, Visible, LinkedCell) around an MSForms
|
|
182
|
+
# control (Caption, Value, BackColor). `ole` is the host and
|
|
183
|
+
# `ole_object` the MSForms control; unknown methods go to `ole_object`,
|
|
184
|
+
# because that is where Caption and Value live for every family. Host
|
|
185
|
+
# members are reached explicitly through `ctl.ole`. There is no lookup
|
|
186
|
+
# order that tries both: `Left` exists on neither the inner object
|
|
187
|
+
# (when hosted on a sheet) nor unambiguously on both. For the other two
|
|
188
|
+
# families `ole` and `ole_object` are the same thing.
|
|
189
|
+
#
|
|
190
|
+
# Name note: `name` covers COM `Name` with the same value (M0). The
|
|
191
|
+
# other lowercase methods here -- kind, family, ole, runtime, on, off,
|
|
192
|
+
# events, vba -- are free on Button, OLEObject and the MSForms controls
|
|
193
|
+
# (M0, measured against Excel 11). `ole_object` carries the `ole_`
|
|
194
|
+
# prefix for the reason Proxy's meta-methods do: a bare `object` would
|
|
195
|
+
# shadow COM `Object`, which on an MSForms control is a *different*
|
|
196
|
+
# thing -- the raw control under the extender, with Caption but no Name
|
|
197
|
+
# and no events (M0). `ctl.Object` still reaches that COM member.
|
|
198
|
+
class Control
|
|
199
|
+
include Passthrough
|
|
200
|
+
|
|
201
|
+
attr_reader :name, :kind, :family, :ole_object
|
|
202
|
+
|
|
203
|
+
# `vba:` is the writer a handler goes through: the BookVBA for a form
|
|
204
|
+
# control (any standard module will do, so the wrapper's own), the
|
|
205
|
+
# SheetVBA for worksheet ActiveX (Excel looks for `Name_Click` in the
|
|
206
|
+
# sheet's own module and nowhere else), the BookVBA for a UserForm
|
|
207
|
+
# control (`into:` the form module). `form:` is the Form a :userform
|
|
208
|
+
# control belongs to; it owns the runtime instance.
|
|
209
|
+
def initialize(name:, kind:, family:, ole:, ole_object:, vba:, form: nil)
|
|
210
|
+
@name = name
|
|
211
|
+
@kind = kind
|
|
212
|
+
@family = family
|
|
213
|
+
@ole = ole
|
|
214
|
+
@ole_object = ole_object
|
|
215
|
+
@writer = vba
|
|
216
|
+
@form = form
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# The Events object a block would be registered on; nil for a form
|
|
220
|
+
# control, which has none.
|
|
221
|
+
def events
|
|
222
|
+
case @family
|
|
223
|
+
when :activex then @ole_object.ole_events
|
|
224
|
+
when :userform then runtime.ole_events
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# A UserForm control's live counterpart on the form's default
|
|
229
|
+
# instance -- the object that fires events and shows changes while the
|
|
230
|
+
# form is loaded. The design-time control (`ole`) does neither.
|
|
231
|
+
def runtime
|
|
232
|
+
unless @family == :userform
|
|
233
|
+
raise ArgumentError,
|
|
234
|
+
"only a UserForm control has a runtime instance; this is a #{@family}"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
@form.runtime_control(@name)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def on(event, args: true, &block)
|
|
241
|
+
listenable.on(event, args: args, &block)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def off(name_or_subscription)
|
|
245
|
+
listenable.off(name_or_subscription)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Write one VBA handler. A form control fires only Click, so it takes
|
|
249
|
+
# the body alone and is bound through OnAction; the other two take the
|
|
250
|
+
# event name, and Excel finds the procedure by its `Name_Event` name in
|
|
251
|
+
# the right module. `params:` is the parameter list, verbatim -- the
|
|
252
|
+
# wrapper carries no signature table.
|
|
253
|
+
#
|
|
254
|
+
# The block is named `Name_Event`, so writing the same event again
|
|
255
|
+
# replaces the handler (vba.write's own rule).
|
|
256
|
+
def vba(event_or_body, body = nil, params: nil)
|
|
257
|
+
if @family == :form_control
|
|
258
|
+
unless body.nil?
|
|
259
|
+
raise ArgumentError,
|
|
260
|
+
'a form control fires only Click: call vba(body) with no event name'
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
macro = "#{@name}_Click"
|
|
264
|
+
@writer.write("Sub #{macro}()\n#{indent(event_or_body)}\nEnd Sub", name: macro)
|
|
265
|
+
@ole.OnAction = macro
|
|
266
|
+
else
|
|
267
|
+
if body.nil?
|
|
268
|
+
raise ArgumentError,
|
|
269
|
+
"vba(event, body) -- name the event, e.g. vba('Click', 'Range(\"A1\").Value = 1')"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
Controls.check_event!(event_or_body)
|
|
273
|
+
block = "#{@name}_#{event_or_body}"
|
|
274
|
+
code = "Private Sub #{block}(#{params})\n#{indent(body)}\nEnd Sub"
|
|
275
|
+
if @family == :activex
|
|
276
|
+
@writer.write(code, name: block)
|
|
277
|
+
else
|
|
278
|
+
@writer.write(code, name: block, into: @form.name)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
self
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
private
|
|
285
|
+
|
|
286
|
+
def passthrough_target
|
|
287
|
+
@ole_object
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def listenable
|
|
291
|
+
if @family == :form_control
|
|
292
|
+
raise ArgumentError,
|
|
293
|
+
'form controls have no COM events; bind a macro with vba(...) or use ' \
|
|
294
|
+
'sheet.activex for a control Ruby can listen to'
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
events
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def indent(body)
|
|
301
|
+
body.to_s.chomp.split(/\r?\n/, -1).map { |line| line.empty? ? line : " #{line}" }.join("\n")
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# `sheet.form_controls`: the Forms-toolbar controls. Cheap to place (a
|
|
306
|
+
# `Buttons.Add` plus `OnAction` measured 3 ms) and they save with the
|
|
307
|
+
# workbook, but they raise no COM events -- a handler is a macro named
|
|
308
|
+
# by OnAction, so `vba(body)` is the only way to react to one.
|
|
309
|
+
#
|
|
310
|
+
# Name note (M0): `form_controls` is free on Worksheet.
|
|
311
|
+
class FormControls
|
|
312
|
+
def initialize(sheet)
|
|
313
|
+
@sheet = sheet
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Order matters and each step exists to make a specific passthrough
|
|
317
|
+
# trap unreachable: kind and name are checked before geometry so a
|
|
318
|
+
# typo fails without a round trip; the free-name check runs against
|
|
319
|
+
# Shapes, which sees every family; the rename and the properties run
|
|
320
|
+
# after Add with the new control deleted if either fails, so a refused
|
|
321
|
+
# property does not leave an unnamed button behind.
|
|
322
|
+
def add(kind, name:, at: nil, left: nil, top: nil, width: nil, height: nil, **props)
|
|
323
|
+
collection = Controls.form_collection_for(kind)
|
|
324
|
+
Controls.check_name!(name)
|
|
325
|
+
l, t, w, h = Controls.geometry(sheet: @sheet, at: at, left: left, top: top, width: width, height: height)
|
|
326
|
+
Controls.check_free!(@sheet.ole.Shapes, name, 'sheet')
|
|
327
|
+
|
|
328
|
+
ole = @sheet.ole.public_send(collection).Add(l, t, w, h)
|
|
329
|
+
begin
|
|
330
|
+
ole.Name = name
|
|
331
|
+
props.each { |key, value| Controls.put(ole, key, value) }
|
|
332
|
+
rescue StandardError
|
|
333
|
+
ole.Delete
|
|
334
|
+
raise
|
|
335
|
+
end
|
|
336
|
+
Control.new(name: name, kind: kind, family: :form_control, ole: ole, ole_object: ole, vba: book_vba)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Re-bind a control that is already on the sheet. nil when there is no
|
|
340
|
+
# shape of that name, or the shape is not a form control (an ActiveX
|
|
341
|
+
# shape raises on FormControlType; an EditBox maps to nil).
|
|
342
|
+
def [](name)
|
|
343
|
+
kind = Controls::FORM_CONTROL_TYPES[@sheet.ole.Shapes.Item(name).FormControlType]
|
|
344
|
+
return nil if kind.nil?
|
|
345
|
+
|
|
346
|
+
ole = @sheet.ole.public_send(Controls::FORM_KINDS[kind]).Item(name)
|
|
347
|
+
Control.new(name: name, kind: kind, family: :form_control, ole: ole, ole_object: ole, vba: book_vba)
|
|
348
|
+
rescue WineOLE::RemoteError
|
|
349
|
+
nil
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
private
|
|
353
|
+
|
|
354
|
+
# A form control's macro can live in any standard module, so it goes
|
|
355
|
+
# in the wrapper's own module of the parent workbook. Only `write` is
|
|
356
|
+
# used, and paths never are, so convert_paths is moot.
|
|
357
|
+
def book_vba
|
|
358
|
+
@book_vba ||= BookVBA.new(@sheet.ole.Parent, convert_paths: false)
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# `sheet.activex`: OLEObjects hosting an MSForms control (or any other
|
|
363
|
+
# registered control, by ProgID). Each is two COM objects -- see
|
|
364
|
+
# Control's class comment -- and the properties given at placement are
|
|
365
|
+
# routed accordingly: HOST_PROPS to the OLEObject, the rest inside.
|
|
366
|
+
#
|
|
367
|
+
# The five named arguments to OLEObjects.Add are not optional on Excel
|
|
368
|
+
# 11: with only Left and Top it fails with 0x800A03EC. Geometry has
|
|
369
|
+
# already guaranteed all four points by the time Add is called.
|
|
370
|
+
#
|
|
371
|
+
# Name note (M0): `activex` is free on Worksheet.
|
|
372
|
+
class ActiveXControls
|
|
373
|
+
def initialize(sheet)
|
|
374
|
+
@sheet = sheet
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def add(kind, name:, at: nil, left: nil, top: nil, width: nil, height: nil, **props)
|
|
378
|
+
progid = Controls.progid_for(kind)
|
|
379
|
+
Controls.check_name!(name)
|
|
380
|
+
l, t, w, h = Controls.geometry(sheet: @sheet, at: at, left: left, top: top, width: width, height: height)
|
|
381
|
+
Controls.check_free!(@sheet.ole.Shapes, name, 'sheet')
|
|
382
|
+
|
|
383
|
+
ole = @sheet.ole.OLEObjects.Add(ClassType: progid, Left: l, Top: t, Width: w, Height: h)
|
|
384
|
+
begin
|
|
385
|
+
ole.Name = name
|
|
386
|
+
inner = ole.Object
|
|
387
|
+
props.each do |key, value|
|
|
388
|
+
Controls.put(Controls::HOST_PROPS.include?(key) ? ole : inner, key, value)
|
|
389
|
+
end
|
|
390
|
+
rescue StandardError
|
|
391
|
+
ole.Delete
|
|
392
|
+
raise
|
|
393
|
+
end
|
|
394
|
+
Control.new(name: name, kind: kind, family: :activex, ole: ole, ole_object: inner, vba: @sheet.vba)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def [](name)
|
|
398
|
+
ole = @sheet.ole.OLEObjects.Item(name)
|
|
399
|
+
Control.new(name: name, kind: Controls.kind_for_progid(ole.progID), family: :activex,
|
|
400
|
+
ole: ole, ole_object: ole.Object, vba: @sheet.vba)
|
|
401
|
+
rescue WineOLE::RemoteError
|
|
402
|
+
nil
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# `form.controls`: MSForms controls on a UserForm, placed on the
|
|
407
|
+
# design-time Designer. Points only -- a form has no cells for `at:`.
|
|
408
|
+
# MSForms takes the name at Add time, so there is no rename step, and
|
|
409
|
+
# the box is four property puts after it (Controls.Add has no position
|
|
410
|
+
# arguments).
|
|
411
|
+
#
|
|
412
|
+
# Name note (M0): `controls` shadows `Designer.Controls` deliberately;
|
|
413
|
+
# the COM collection is still reachable as `form.ole.Controls`.
|
|
414
|
+
class UserFormControls
|
|
415
|
+
def initialize(form, book_vba)
|
|
416
|
+
@form = form
|
|
417
|
+
@book_vba = book_vba
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def add(kind, name:, at: nil, left: nil, top: nil, width: nil, height: nil, **props)
|
|
421
|
+
progid = Controls.progid_for(kind)
|
|
422
|
+
Controls.check_name!(name)
|
|
423
|
+
l, t, w, h = Controls.geometry(sheet: nil, at: at, left: left, top: top, width: width, height: height)
|
|
424
|
+
Controls.check_free!(@form.ole.Controls, name, 'UserForm')
|
|
425
|
+
|
|
426
|
+
ole = @form.ole.Controls.Add(progid, name)
|
|
427
|
+
begin
|
|
428
|
+
ole.Left = l
|
|
429
|
+
ole.Top = t
|
|
430
|
+
ole.Width = w
|
|
431
|
+
ole.Height = h
|
|
432
|
+
props.each { |key, value| Controls.put(ole, key, value) }
|
|
433
|
+
rescue StandardError
|
|
434
|
+
@form.ole.Controls.Remove(name)
|
|
435
|
+
raise
|
|
436
|
+
end
|
|
437
|
+
Control.new(name: name, kind: kind, family: :userform, ole: ole, ole_object: ole, vba: @book_vba, form: @form)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# kind is nil: a placed MSForms control does not report the ProgID it
|
|
441
|
+
# was made from.
|
|
442
|
+
def [](name)
|
|
443
|
+
ole = @form.ole.Controls.Item(name)
|
|
444
|
+
Control.new(name: name, kind: nil, family: :userform, ole: ole, ole_object: ole, vba: @book_vba, form: @form)
|
|
445
|
+
rescue WineOLE::RemoteError
|
|
446
|
+
nil
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|