[ 初めての方へ | 一覧(最新更新順) | 全文検索 | 過去ログ ]
『配列に入れたオブジェクトをWithでまとめるとEmptyになる』(傘)
ユーザーフォーム上の複数のリストオブジェクトに同じ操作をするため
Array関数で配列にしたものをForNextでループするときに
配列にしたオブジェクトをWithでまとめようとすると次の行で配列内のリストオブジェクトがEmptyに変わってしまいます。
Withを使わずに全部書いてやると問題なく動作するんですが、これは何が悪いんでしょうか?
以下はその部分の抜粋です。
Dim lstAry: lstAry = Array(Me.lst_A, Me.lst_B, Me.lst_C, Me.lst_D) Dim i as Long, j as Long Dim dic as Object Set dic = CreateObject("Scripting.Dictionary") For i = 0 To Ubound(lstAry) With lstAry(i) For j = 0 To .ListCount - 1 '★この行に移動したとき配列の中身が消える If .Selected(j) Then dic(.List(j)) = i + 1 End If Next j End With Next i
< 使用 Excel:Office365、使用 OS:Windows10 >
>リストオブジェクト たぶんListBoxのことですよね?
Dim v As Variant For Each v In lstAry なら、vは[Variant/Object/ListBox]でlstAryの各要素と同じ型を維持してる様です。 (いずれにせよ[Variant/Object/ListBox]のまま使うのは気持ち悪いかもですね^^;)
>何が悪いんでしょうか? 悪いとかではないと思うんですが、 強いて言うなら「これでもイケる」と勝手に解釈した事?
For...Next ステートメントにせよ For Each...Next ステートメントにせよ ユーザーから見えない裏っ側でいろいろやってる部分がある筈ですし、 それは、当てはめられたlstAry側も同じく裏っ側でいろいろある事でしょう。
その裏っ側でざっくり言えば「要素がObjectだったらSetで返す」なんて気の利いた構文になってないんでしょうね。 たぶんですけど...
(白茶) 2022/07/11(月) 19:01
Dim lstAry: lstAry = Array("A", "B", "C", "D") For i = 0 To Ubound(lstAry) With Me.Controls("lst_" & lstAry(i)) ・・・
という書き方をしてたんですが、これならコントロール自体を配列にしても同じだなと気づいて
最初のような書き方にしたらEmptyになるようになってしまったんです。
今までオブジェクトを配列にする書き方をしたことがなく今回が初めてだったので
自分が知らないだけで、オブジェクト変数への代入はSetが必要のようなルールがあるんだったら教えてほしいです。
For i = 0 To Ubound(lstAry) For j = 0 To lstAry(i).ListCount - 1 If lstAry(i).Selected(j) Then dic(lstAry(i).List(j)) = i + 1 End If Next j Next i
なら問題ないのに
For i = 0 To Ubound(lstAry) With lstAry(i) For j = 0 To .ListCount - 1 If .Selected(j) Then dic(.List(j)) = i + 1 End If Next j End With Next i
とWithを使ったとたんおかしくなるのがよくわからなくて…
(傘) 2022/07/11(月) 20:03
わー、マジだった...orz
With lstAry(i) For j = 0 To lstAry(i).ListCount - 1 'この行に移動したとき【には既に】配列の中身が消えてますね '~~~~~~~~~あえて If lstAry(i).Selected(j) Then '~~~~~~~~~あえて End If Next j End With
インテリセンスを犠牲にする様なWithの書き方ってあんまやった事ないから分かんないすけど Withと[Variant/Object/ListBox]の組み合わせで発生するのかな...?
どのみち私個人の見解としては、さして変わらないです。 他の方の書き込みでも待ってみましょう。
(白茶) 2022/07/11(月) 20:43
ちょっと実験してみました・・
まず、一旦 Set ListDummy=lstAry(i) の形にしてから、
With ListDummy でやってみた。 ・・旨く行った。
その後、オリジナルのコードに戻して、再実行したら問題が発生しなくなっちゃった。
この現象が、誰でも再現するなら、 MicroSoftの人じゃないと説明出来ないのでは?
(半平太) 2022/07/11(月) 20:56
(γ) 2022/07/11(月) 21:15
私も実験してみました。 私の場合、最初から
With lstAry(i)
でなんのエラーもなく実行できました。 Array関数を要素をVariant型として格納するので、何として解釈するかがその時によって変わる可能性があるのかも。 型が未確定になるVariant型はなるべく使いたくないです。
コントロールを配列のように扱いたい場合は、自分なら、Collection を使いますね。
Public Sub Test1() Dim LstCol As New Collection LstCol.Add Me.lst_A LstCol.Add Me.lst_B LstCol.Add Me.lst_C
Dim dic As Object Set dic = CreateObject("Scripting.Dictionary")
Dim Lst As MSForms.ListBox, j As Long For Each Lst In LstCol With Lst For j = 0 To .ListCount - 1 If .Selected(j) Then dic(.List(j)) = dic.Count + 1 End If Next j End With Next End Sub
(hatena) 2022/07/11(月) 22:15
そういうコードは自分では書かないので、ほうっなるほどって言う感じなんですが、 理由はMSの中の人に聞かないとわかりませんが、大丈夫なコードはいろいろ出来るような気が。
hatenaさんの指摘のとり、Variatnではなくて、型が決まってないとだめな感じはしますね
以下3パターンはどれもOKでした
Private Sub CommandButton1_Click() ' ちゃんと明示的にSet する
Dim lstAry(0 To 2) As MSForms.ListBox Set lstAry(0) = Me.ListBox1 Set lstAry(1) = Me.ListBox2 Set lstAry(2) = Me.ListBox3
Dim i As Long, j As Long Dim dic As Object Set dic = CreateObject("Scripting.Dictionary") For i = 0 To UBound(lstAry) With lstAry(i) For j = 0 To .ListCount - 1 '★この行に移動したとき配列の中身が消える If .Selected(j) Then dic(.List(j)) = i + 1 End If Next j End With Next
End Sub
Private Sub CommandButton2_Click() ' 名前で指定する Dim lb As MSForms.ListBox Dim dic As Object Set dic = CreateObject("Scripting.Dictionary") For Each l In Array(Me.ListBox1.Name, Me.ListBox2.Name, Me.ListBox3.Name) With Me.Controls(l) For j = 0 To .ListCount - 1 '★この行に移動したとき配列の中身が消える If .Selected(j) Then dic(.List(j)) = i + 1 End If Next j End With Next End Sub
Private Sub CommandButton3_Click() ' コレクションで hatenaさんと同じ Dim controlcollection As New collection Dim c As MSForms.ListBox Dim dic As Object Set dic = CreateObject("Scripting.Dictionary") controlcollection.Add Me.ListBox1 controlcollection.Add Me.ListBox2 controlcollection.Add Me.ListBox3 For Each c In controlcollection With c For j = 0 To .ListCount - 1 '★この行に移動したとき配列の中身が消える If .Selected(j) Then dic(.List(j)) = i + 1 End If Next j
End With Next
End Sub (´・ω・`) 2022/07/11(月) 23:44
ぅへぇ〜〜、気持ち悪ぃ〜よ〜 orz
For i = 0 To UBound(lstAry) Debug.Print lstAry(i).Name 'イケる With lstAry(i) 'ローカルウインドウによればココの通過でEmptyに変わる Debug.Print .Name 'でもイケる Debug.Print lstAry(i).Name '実行時エラー424 Debug.Print .Name 'まだイケる (.Parent.Nameも取れるよ) End With Next
(白茶) 2022/07/12(火) 00:03
確かに気持ち悪いですね。 ただ、個人的には、どちらからといえば Dim lstAry: lstAry = Array(Me.lst_A, Me.lst_B, Me.lst_C, Me.lst_D) これができちゃうほうが気持ち悪いです ここでエラーになってほしいです
Set ステートメント使ってないのに、配列要素にObject型が代入されるのはなぜ? lstAryの型が、Variant/Object/ListBox なのはなぜ? ListBoxのデフォルトプロパティではないの? と いろいろ不思議ではあります
どうせ考えてもわからないので、(MSの中の人ではないので) どうやったら意図した動作をするのかを考える方が建設的では? (´・ω・`) 2022/07/12(火) 01:09
というよりArray関数でコントロールの配列を作るというのが、あまりいい方法ではないということです。Array関数で作成される配列の要素はVariant型になります。Variant型はなんでも代入できて便利なんですが、型が不確定なので想定外の動作になる可能性があるのでできれば使わない方がいいです。
(´・ω・`)さんの回答にもあるように型指定した配列に格納したり、Collectionに格納したりして、まとめて操作できるようにするということは一般的な使い方です。
(hatena) 2022/07/12(火) 10:59
私に勘違いがありましたので、上のレスは無視願います。m(__)m
今回の質疑の流れが私にはしっくりこないです。
> >何が悪いんでしょうか? > 悪いとかではないと思うんですが、 > 強いて言うなら「これでもイケる」と勝手に解釈した事?
私もイケると思っちゃいそう。 ダメになるのでは? と嗅覚を働かせるポイントが分からないです。
> ぅへぇ〜〜、気持ち悪ぃ〜よ〜 orz との事ですが、よくよく考えると、With句の中は、With句を設定した時の参照で確定するので、 その後は変数LstAryがNotingになろうとも、それには影響されないとの解釈でいいんじゃないでしょうか?
> Dim lstAry: lstAry = Array(Me.lst_A, Me.lst_B, Me.lst_C, Me.lst_D) > これができちゃうほうが気持ち悪いです
との事ですが、私は普通に For Each r in Array(Rgane("A1"),Range("Z100")) 等とするので、 そんなに違和感が湧かないです。むしろ、これぞVariant型の真骨頂なんて思います。
それにしても、これには困ったもんです。Withのヘルプに何かヒントでも書いておいてくれても良さそうな気がしますが。 ↓ >配列にしたオブジェクトをWithでまとめようとすると次の行で配列内のリストオブジェクトがEmptyに変わってしまいます。
(半平太) 2022/07/12(火) 22:16
>ダメになるのでは? と嗅覚を働かせる いやいや^^; 私もそこまで積極的に疑う場面ではないとは思ってますよ。 (MSさんに言わせたら「勝手に解釈した」って事なんでない? って言いたかっただけ)
>With句を設定した時の参照で確定するので ByVal的アレですよね。(←なんて語彙力... orz)
With句の持つポインタはオブジェクトのアドレスを保持したまま、 同時にlstAry(i)のポインタからオブジェクトのアドレスが失われ...って感じですか。 ↑これがマジ意味不明ですけど。
なんか、冒頭の事例に反する気がして「気持ち悪い」んですよね... Withの方がダメだったんじゃなかったっけか? って。 やっぱForとの絡みもあるのかなぁ...
(白茶) 2022/07/13(水) 01:53
結論から書くと配列を格納する変数をlstAryではなくlstAry()と宣言することでEmptyになる現象は起きなくなりました。
Sub test1() Dim ary(): ary = Array(Range("A1"), Range("A2"), Range("A3")) 'Emptyにならない 'Dim ary: ary = Array(Range("A1"), Range("A2"), Range("A3")) 'Emptyになる 'Dim ary(2) 'Emptyにならない 'Set ary(0) = Range("A1") 'Set ary(1) = Range("A2") 'Set ary(2) = Range("A3") Dim i as Long For i = 0 To 2 With ary(i) .Value = 1 'Emptyになってもシート上に値は書き込まれる End With Next i End Sub
以下のページを読んでいて偶然これに気づきました。 【VBA】Split関数とArray関数の変数宣言時の括弧 https://teratail.com/questions/337233
今までも配列を格納する変数にVariant型を使うときに、括弧ありで宣言するのと無しで宣言する違いが正直よくわかっていなかったのですが OfficeVBAリファレンスのArray関数のページにこのようにあります。 「配列として宣言されていない Variant には、配列を含めることができます。 〜 配列が含まれている Variant は 要素の型が Variant の配列とは概念的に異なりますが、配列要素には同じ方法でアクセスされます。」 https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/array-function
つまり「Variant型の配列=Variant型 "ではない"」ということです。 そして偶然かはわかりませんがこれはWithステートメントにも関係してきます。
Sub test2() Dim ary1 Debug.Print TypeName(ary1) 'Empty ローカルウィンドウ上ではVariant/Empty With ary1 End With Dim ary2() Debug.Print TypeName(ary2) 'Variant() With ary2 '★ End With End Sub
test2を実行すると★でコンパイルエラーになり 「Withの対象は、ユーザー定義型、オブジェクト型、またはバリアント型のいずれかでなければなりません。」 と表示され、TypeNameでも両者のデータ型は違う結果になります。 つまりary2はVariant型ではないということです。 普段配列というのは「●●型の配列」として呼ぶでしょうが、実際には「●●配列型」というのが正確なんじゃないでしょうか。 上で載せたSplit関数とArray関数のページにある、 Dim a a = Split("a-i-u-e-o", "-") Dim b b = Array("a", "i", "u", "e", "o") ではエラーにならずに Dim c() c() = Split("a-i-u-e-o", "-") Dim d() As String d() = Array("a", "i", "u", "e", "o") だとエラーになるというのは Split関数が「String型の配列」ではなく「String配列型」を返し 同じくArray関数は「Variant型の配列」ではなく「Variant配列型」を返すために aとbでは「Variant型の変数に●●配列型のデータを代入している」状態なのでエラーにならず cとdは変数と返り値のデータ型が違うためエラーになる、ということなんだと思います。 なので > Dim lstAry: lstAry = Array(Me.lst_A, Me.lst_B, Me.lst_C, Me.lst_D) > これができちゃうほうが気持ち悪いです というのは > Set ステートメント使ってないのに、配列要素にObject型が代入される というより、Array関数が「引数を要素に格納したVariant配列型のデータを返している」から こういう書き方ができちゃうんでしょう。
そしてWithステートメントで配列の中がEmptyになる理由ですが Variant型変数にRangeオブジェクトをSet無しで代入しようとしたときの挙動と似ていると思います。
Sub test3() Dim var 'Set var = Range("A1") 'SetするとRangeオブジェクトが代入される var = Range("A1") '★ End Sub
ご存じのとおり★の行では変数varにはRangeオブジェクトは代入されません。 では何が代入されるかというと、「その時A1に書き込まれているデータ」が代入されるようです。 A1が1なら1が、A1が"a"なら"a"が代入され、そして空欄なら「Emptyが代入」されます。 この「Variant変数にSet無しで空欄のRangeオブジェクトを代入しようとするとEmptyが代入される」という挙動は 「Variant型の変数に代入したオブジェクト配列型の要素がEmptyになる」という現象と 上で述べた「Withの対象はオブジェクト型またはバリアント型である必要がある」という制約が関係している気がします。
ここからは全くの推測ですが、Withステートメントの処理というのは対象になるオブジェクトを 内部的に何かの変数に保持させることでその記述を省略できるようにしていて どんなオブジェクトを保持するかわからないためVariant/Object型のような変数が使われていて 基本的にそのオブジェクトの下位のプロパティを操作するためにその変数はByRefの状態で そこに配列型を代入したVariant型の変数を入れてやろうとすると、"ガワ"がVariant型なので データ型の扱いが中身のオブジェクトとしてではなく空欄の"何か"("何処か"のほうが正しいか?)が代入されて それがByRefで元データ側に反映されて…ということが起こっているんじゃないでしょうか。 そして"要素の型がVariantの配列 と 配列が含まれているVariant は概念的に異なる"そうなので Withの対象にする変数をVariant配列型にしてやるとそういうことが起きなくなる、と。
リストボックスのEmptyも何か空欄のままになっているプロパティの値とかを引っ張ってきてるのかもしれません。 まあ、それならそれでtest1のEmptyになってもシート上に値は書き込まれる話とか 白茶さんが書いてくれたWithの参照ではエラーにならないのに元オブジェクトの参照ではエラーになる話はよく分かりませんが… (ByRefで元オブジェクトにはEmptyが反映されるけどWithで保持している物は最初に参照された状態を保つとか?)
長々と書きまくってしまいましたが、だいたいこんなような話なんじゃないかと。
自分にはあまりにも難しい話だったので無茶苦茶な内容になっているかもしれませんが
Emptyになる件の回避策も含め、いい勉強になったと思います。
(傘) 2022/07/13(水) 20:25
何かしっくりこないですねぇ・・
> 「Withの対象は、ユーザー定義型、オブジェクト型、 > またはバリアント型のいずれかでなければなりません。」
それは、ドット演算子が使える型かどうか、と言うことなんじゃないですか?
バリアント型が含まれているのは、ドット演算子が使えるかどうかは、 コンパイル時点ではまだ不明なのでエラーにはしないでくれているに過ぎない。
> Dim ary2() > Debug.Print TypeName(ary2) 'Variant() > With ary2 '★ > End With
つまり、上のary2は配列なので、ドット演算子が使えないのが形式的に明らかなので、 コンパイル時点でエラーとなる。
(半平太) 2022/07/14(木) 15:12
うーむ、必ずしもそうとも言えないなぁ。
これだと、実行時でもWithまでは通りますねぇ・・不思議 ↓ Dim ary1 ' Debug.Print TypeName(ary1) 'Empty ローカルウィンドウ上ではVariant/Empty ary1 = Array(Range("a1"), 2, 3) Debug.Print IsArray(ary1) With ary1
End With
(半平太) 2022/07/14(木) 15:30
今回、問題になっているところの「With」に指定するのは配列の要素の方なので、 それに限定したテストコードを書いてみましたが、 Variant型が何かの形で影響を及ぼしているようには見えません。
途中経過に差異が見当たらないです。(ForNextも関係なく現象が起きます)
Sub test() Dim ary1, ary2(), o1, o2
Range("A1").Value = "TEST"
ary1 = Array(Range("A1"), 2, 3) ary2() = Array(Range("A1"), 2, 3)
Debug.Print TypeName(ary1(0)) 'Range Debug.Print TypeName(ary2(0)) 'Range
Set o1 = ary1(0) 'Variant/Object/Range Set o2 = ary2(0) 'Variant/Object/Range
With ary1(0) Debug.Print .Value 'TEST (ary1(0)はEmptyになる) End With
With ary2(0) Debug.Print .Value 'TEST (ary2(0)はEmptyにならない) End With End Sub
(半平太) 2022/07/14(木) 17:18
あれぃ? ひょっとして冒頭のコード、
> With lstAry(i) > For j = 0 To .ListCount - 1 '★この行に移動したとき配列の中身が消える > If .Selected(j) Then
別に↑ここで「エラーで止まった」とは傘さん仰ってないのか。 単に配列の中身が消える現象を確認しただけに過ぎない。
であれば、私の >> 冒頭の事例に反する気がして「気持ち悪い」 は無かった事になりますね。
何か勝手に勘違いしてたな... さすがにそんな一貫性のない現象起こらないか。 (結局は↓この現象で一貫してるのね。我ながら気付くの遅せwww)
Sub test() Dim v, o v = Array(UserForm1.ListBox1) Set o = v(0) PrintPtr v(0) PrintPtr o With v(0) Debug.Print .Name End With PrintPtr v(0) PrintPtr o End Sub Private Sub PrintPtr(a) Dim p As LongPtr If IsObject(a) Then p = ObjPtr(a) Debug.Print VarType(a), VarPtr(a), p End Sub
↓実行結果 8 200621032 199583144 8 1570100 199583144 ListBox1 0 200621032 0 8 1570100 199583144
これはまた失礼しました...^^;
(白茶) 2022/07/15(金) 19:59
[ 一覧(最新更新順) ]
YukiWiki 1.6.7 Copyright (C) 2000,2001 by Hiroshi Yuki.
Modified by kazu.