t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

辞は達するのみ by 孔子

論語に「辞は達するのみ」という言葉がある。
これは「言葉はその意味が相手に伝わることが大事だ。」という意味だ。

孔子が何を伝えたかったかという話は、以下2通りの解釈がある。

  • だから分かりやすく慎重に言葉を選択するべき
  • だからいたずらに飾り立てず、些細なことに拘らず、意味の方を大事にすべき

(達してないやん。。と思ってしまうがそれは置いといて)

さて、言葉はさながら生き物のように、時代と共にその意味や解釈が移り変わる。
ところが最近SNSの発達のおかげで原義の方がまた勢力を盛り返すようになって面白い。

言葉の本来の意味を知っている人、知らない人、気に掛けるひと、掛けない人が入り混じってコミュニケーションに多少の支障を生む。SNSでは日本語警察とか、マサカリと呼ばれるアレ。

しかし、孔子が言ったように言葉は意味を伝える道具。所詮道具なので、誤用に目くじらを立てる暇があったら相手が何を言いたかったのか、言葉尻ではなく総合的に判断してきちんと「意味」を受け取るようにしたい。

人間、全ての言葉を辞書どおりに正確に話すことができるか?まぁ、無理だろう。
多少なり間違って覚えているものである。

伝える側は相手に合わせて最大限に言葉を選ぶ、受け取る側は相手の意図を理解するべく努力する。
それでうまくコミュニケーションが取れるんじゃないかと。

円滑なコミュニケーションの為には、言葉は所詮道具であることを改めて心にとめておきたい。
「伝える」「理解する」という本来の目的を忘れないよう自戒を込めてここに記す。

VBA IfやForの終了構文を自動補完するマクロ

※タイトルにつられた皆さん。多分イメージしてるものと違いますのでガッカリされないようご注意ください。

Twitterで、IfやForの終了構文を自動補完することができないのかなという疑問が投げかけられた。
VBEには文字入力を検知してマクロを走らせるような機能は無いので、たとえば If ~ Thenと書いてEnterを押すと自動でEnd Ifが入るような仕組みは作れない。
どうしてもやろうと思ったら外部のスニペットソフトを作ってグローバルキーフックという技術もあるけど、大掛かりになるし安定動作するかも分からない。

そこで方向性を変えて、Pythonっぽく終了構文なしでインデントで表されたコードを、正規のVBAコードに変換するようなマクロを書いてみた。
ただし思考実験みたいなものなので、考慮漏れしてるケースも多数あるだろうしまともにテストしていない。
それに、既に終了構文があっても追記してしまうという重大な欠陥がある。

従って、これはただのアイデア紹介のサンプルコードであり、間違ってもこのまま実務で使おうなどと考えないこと。

実行イメージ

マクロ実行前のコードがこちらだとすると、

Sub fnFizzBuzz()
    Dim ret, i
    For i = 1 To 100
        Select Case 0
            Case i Mod 15
                ret = "FizzBuzz"
            Case i Mod 3
                ret = "Fizz"
            Case i Mod 5
                ret = "Buzz"
            Case Else
                ret = CStr(i)

    fnFizzBuzz = ret
End Sub

マクロによりEnd SelectとNextが補完されたのがこちら。

Sub fnFizzBuzz()
    Dim ret, i
    For i = 1 To 100
        Select Case 0
            Case i Mod 15
                ret = "FizzBuzz"
            Case i Mod 3
                ret = "Fizz"
            Case i Mod 5
                ret = "Buzz"
            Case Else
                ret = CStr(i)
        End Select
    Next

    fnFizzBuzz = ret
End Sub

マクロでVBプロジェクトに直接アクセスする手もあるが、今回は単純にクリップボードにコードをコピーした状態でマクロを実行するとイミディエイトウィンドウに出力される仕様とした。

まず必要なのは、このブログではおなじみのStackクラス。
クラスモジュールを挿入してオブジェクト名をStackとし、以下のコードを貼り付け。

Private Items() As Variant

Property Get Count() As Integer
    Count = UBound(Items)
End Property

Property Get Top() As Variant
    Top = Items(UBound(Items))
End Property

Public Function Pop() As Variant
    If UBound(Items) > 0 Then
        Pop = Items(UBound(Items))
        ReDim Preserve Items(UBound(Items) - 1)
    Else
        Pop = Empty
    End If
End Function

Public Sub Push(ByRef x As Variant)
    ReDim Preserve Items(UBound(Items) + 1)
    Items(UBound(Items)) = x
End Sub

Private Sub Class_Initialize()
    ReDim Items(0)
End Sub

そして以下の2点を参照設定
Microsoft VBScript Regular Expressions 5.5
Microsoft Forms 2.0 Object Library

Formsの方はユーザーフォームを挿入してから削除すると参照設定されるのでそのやり方がオススメ。

そしてメインの標準モジュール(名前は任意)を挿入し、以下のコードを張り付け。

'必要な参照設定
'  Microsoft VBScript Regular Expressions 5.5
'
'  Microsoft Forms 2.0 Object Library
'  (こちらはフォームモジュール挿入して削除すれば参照設定される)
Sub FromIndentStyleToCorrectVBAStyle()
    With New DataObject
        .GetFromClipboard
        Dim arr: arr = Split(.GetText, vbNewLine)
    End With
    
    Dim StatementStack As Stack: Set StatementStack = New Stack
    Dim IndentStack As Stack: Set IndentStack = New Stack
    StatementStack.Push "Dummy"
    IndentStack.Push -1
    
    Dim i
    For i = LBound(arr) To UBound(arr)
        indents = GetIndentNumber(arr(i))
        Do While indents <= IndentStack.Top
            indents = IndentStack.Top - 4
            Debug.Print Space(IndentStack.Pop) & StatementStack.Pop
        Loop
        Debug.Print arr(i)
        es = GetEndStatement(arr(i))
        If es <> "" Then
            StatementStack.Push es
            IndentStack.Push indents
        End If
    Next
End Sub

Function GetIndentNumber(codeLine)
    Dim n: n = 1
    Do While Mid(codeLine, n, 1) = " "
        n = n + 1
    Loop
    n = n - 1
    GetIndentNumber = n
End Function

Function GetEndStatement(ByVal begin)
    Dim ret As String
    
    Dim re As RegExp: Set re = New RegExp
    begin = Trim(begin)
    
    re.Pattern = "^For .*"
    If re.Test(begin) Then
        ret = "Next": GoTo Fin
    End If
    
    re.Pattern = "^If .* Then$"
    If re.Test(begin) Then
        ret = "End If": GoTo Fin
    End If
    
    re.Pattern = "^Select Case "
    If re.Test(begin) Then
        ret = "End Select": GoTo Fin
    End If
    
    re.Pattern = "^Do"
    If re.Test(begin) Then
        ret = "Loop": GoTo Fin
    End If
    
Fin:
    GetEndStatement = ret
End Function

後はPythonみたいにインデントのみで書かれたコードをコピーして、FromIndentStyleToCorrectVBAStyleを実行すると、終了構文が補完されたコードがイミディエイトウィンドウに出力される。

一応作ってみたものの、私自身は毎日VBAに触ってるのであまりタイピングが面倒という感覚がなくなってきている。
ツール作成で一番時間を食うのは試行錯誤やデバッグなので、タイピングのロスは誤差の範囲。
だからまぁ、これを自分で使うことは無いだろうなぁ。

追記(言い訳)

今回はとりあえずアイデアを形にするためのやっつけコーディングなので、より良い方法があっても突っ込まないで欲しい。
たとえばスタックを2個使ってるけど、本当は関連項目だからユーザー定義型かクラスにしてデータをまとめて1つのスタックに積むか、スタック自体を改変して複数個を1領域にPushできる仕様にすべきだったと思う。
※Popが多値になるのでそれはそれで扱い注意だけど。

それと、冒頭で述べた既に終了構文があっても追記してしまうという重大な欠陥について、修正できなかった訳じゃないけど、このアイデアにそれほど思い入れが無いので放置することにした。もしこのアイデアを気に入った人がいたら直して使って欲しい。

部屋を綺麗に保つために大事な「運用」という観点

断捨離とかミニマリストとかがもてはやされていた頃、なんとなく感じた違和感。
それは私がシステムの運用に携わっているためかもしれない。

一度開発されたシステムは、その後何年にもわたって運用されていく。
作って終わりではないのだ。

私は、お部屋の片付け・掃除は片付いた状態を作る「開発」作業だと思っている。
重要なのはその片付いた状態をどうやって維持、つまり「運用」していくか。

どれだけ綺麗な部屋を完成させても、日々ゴミは出るしホコリもたまる。
片付けられない男女に必要なのは、「開発」ではなくて「運用」の方なんじゃないか。

これが断捨離やミニマリストに対して感じていた違和感の正体だった。

まぁ、以下のようなタイプには効くと思うし、不要なものを捨てることは大事だと思う。
f:id:t-hom:20180902090813p:plain

ただ、一発綺麗にしたいだけならお金を払って業者になんとかしてもらうという手もある。

片付けられない人の悩みって、以下のタイプが多いんじゃないだろうか。
※これは極端な例だけど、波が激しいという意味では当てはまる人多いと思う。
f:id:t-hom:20180902090851p:plain

まぁ一発奮起して片付けする人ってこんな状態↓を目指すんだろうけど、そんなこと出来る人ってそうそう居ないし、運用考える前に開発始めちゃう感じなので、数日で元の木阿弥になるのは目に見えている。
f:id:t-hom:20180902091325p:plain

それよりは、こんな感じ↓で「そこそこ綺麗」を通年運用していく方が快適に暮らせるだろうし、まずは運用から考えた方が良いなと最近思うようになった。
f:id:t-hom:20180902091540p:plain

なんかボヤっとした話になってしまったので一つだけ改善した事例を挙げる。
以前私は、飲み終わったペットボトルを台所に置いてて、たまってからラベル剥いて洗って捨てるってことをしてた。私は全く料理をしないので、台所は完全にペットボトル置き場と化していた。

ところが溜まったボトルを全部ラベルを剥いて洗うのって面倒くさいのでつい後回しになる。結果、空ペットボトルが台所以外にも浸食。ペットボトルってフタを閉めておけば臭いもしないし、中身が多少残ってても汚れないので、男の一人暮らしで散乱しやすいものダントツ一位だと思われる。

これはさすがにまずいってことで、以前ポタペットというアイテムを買って、飲んだら洗ってすぐ干すようにした。

ポタペット グリーン

ポタペット グリーン

翌朝まずまず乾いてるので多少水滴あってもそのままこちらにポイ。

サンコープラスチック ゴミ箱 2段分別スリム 47L ライトベージュ

サンコープラスチック ゴミ箱 2段分別スリム 47L ライトベージュ

ゴミ箱のデザインは今一つ気に入らないんだけど、まぁ取り合えず運用重視。
捨てたときにゴミ箱に水が垂れるようなことがなければ良いので、完全に乾いてなくてもOKとした。

結果、台所はもちろん部屋にペットボトルが転がっているという事態はなくなった。

極端なミニマリストの考え方だと、まずポタペットって無くても良いので捨てるor買わない。専用のごみ箱もゴミ袋があればいいよねって話になるので買わないor捨てるという風になるんじゃないかと。でもそこには運用の観点が抜け落ちてる。

経験上、運用を考慮していないところから汚れていくので、運用を支えるアイテムは迷わず買うべきだと思うし、部屋を片付ける際も一番に考えるべきはどうやって維持していくかということだと思う。


以下、お気に入りの片づけ本。

片づけられない女のためのこんどこそ!片づける技術

片づけられない女のためのこんどこそ!片づける技術

これは捨てる系の本だけど、基地を作るという考え方が役に立った。一か所綺麗だと他の汚れが気になってくるもの。だからとりあえず机だけは死守しよう。この考え方を取り入れることでとりあえず部屋全体がそこそこ綺麗な状態を維持できるようになった。

同じ著者で、今度は整頓系の本。

必要なものがスグに! とり出せる整理術!

必要なものがスグに! とり出せる整理術!

具体的なアイデアよりは、考え方が参考になった。要は「取り出しにくい・しまいにくい」収納が、元の位置に戻さなくなる元凶。
使いやすさ≒運用を重視して設計することですぐ片付けられる部屋になる。

あと細々やりすぎないってところもミソ。私の場合は以下のチェストに文房具・薬/サプリ・衛生/身だしなみ・工具類ってくくりでゴチャーっと入れてる。

ちょっとチープでダサいかなぁと思ったけど、片付けが苦手な人はオシャレでシックなアイテムよりも、とにかく運用重視で揃えると良い。
どんなオシャレなアイテムも、汚部屋では台無しなので、片付け初心者を脱するまでは何を買っても一緒。
きちんと運用が定まってから高級感のあるものにアップグレードするのは良いと思う。

VBA 複雑な罫線をVBAで描き分ける

Twitterでお題が流れてきたので乗っかってみた。

これの本人返信ツイートでマクロは禁止ですと書いてあったんだけど気にせずにマクロ記述時間込みでトライアル。

結果、マクロ記述と実行を合われて4分15秒で完成。記述込みだとマクロ使って1分は逆に無理だな。。

書いたマクロはこちら。

Sub hoge()
    Dim r As Range
    For Each r In Selection
        If r.Interior.Color = vbYellow Then
            r.Borders.LineStyle = XlLineStyle.xlContinuous
            If r.Offset(1, 0).Interior.Color = vbYellow Then r.Borders(xlEdgeBottom).LineStyle = XlLineStyle.xlDash
            If r.Offset(0, 1).Interior.Color = vbYellow Then r.Borders(xlEdgeRight).LineStyle = XlLineStyle.xlDash
            If r.Offset(-1, 0).Interior.Color = vbYellow Then r.Borders(xlEdgeTop).LineStyle = XlLineStyle.xlDash
            If r.Offset(0, -1).Interior.Color = vbYellow Then r.Borders(xlEdgeLeft).LineStyle = XlLineStyle.xlDash
        End If
    Next
End Sub

使い方

まず手動で描きだしたいセルをCtrl+ドラッグやCtrl+クリックで選択する。
f:id:t-hom:20180831211202p:plain

それを黄色く塗りつぶす。
f:id:t-hom:20180831211301p:plain

あとは塗りつぶした範囲が入るように適当に範囲選択して、
f:id:t-hom:20180831211350p:plain

マクロを実行すると、こうなる。
f:id:t-hom:20180831211430p:plain

最後に全選択して塗りつぶしをクリアし、表示メニューから枠線を消すとできあがり。

f:id:t-hom:20180831211559p:plain

お題と形違うけど。。まぁ趣旨は合ってるからいいや。

以上

追記

元記事がはてなブログだったようなので引用
www.waenavi.com

VBA オートシェイプを使った泡のアニメーション

TwitterJavascriptを使った綺麗なアニメーションが流れてきたので、VBAでも真似してみた。
ics.media

本家の躍動感まではコピーできなかったけど、それなりに見栄えのするアニメーションができたので紹介。

f:id:t-hom:20180830223613g:plain

作り方

まずSheet1のオブジェクト名をプロパティウィンドウからScreenに変更する。
f:id:t-hom:20180830223828p:plain

そしてシートモジュールScreenのコードに初期化用のClearプロシージャを用意しておく。

Sub Clear()
    Me.Cells.Interior.Color = vbBlack
    Dim sh As Shape
    For Each sh In Me.Shapes
        sh.Delete
    Next
End Sub


次にクラスモジュールを挿入し、オブジェクト名をBubbleとする。

クラスのコードはこちら。

Option Explicit
Private bubbleShape As Shape
Private speed As Double
Private scales As Double

Public Function Float() As Boolean
    If bubbleShape.Top - speed > 0 Then
        bubbleShape.IncrementTop -speed
        speed = speed * 1.1
        scales = scales * 0.99
        bubbleShape.ScaleHeight scales, msoFalse, msoScaleFromMiddle
        bubbleShape.ScaleWidth scales, msoFalse, msoScaleFromMiddle
        Dim c: c = Int((255 - 100 + 1) * Rnd + 100)
        bubbleShape.Fill.ForeColor.RGB = RGB(c, c, c)
        Float = True
    Else
        Float = False
    End If
End Function

Private Sub Class_Initialize()
    Randomize
    speed = 5
    scales = 1
    
    Dim x: x = Int((Application.Width - 0 + 1) * Rnd + 0)
    Dim y: y = Int((Application.Height - 200 + 1) * Rnd + 200)
    Dim size: size = Int((50 - 30 + 1) * Rnd + 30)
    If Int((1 - 0 + 1) * Rnd + 0) = 1 Then
        Set bubbleShape = Screen.Shapes.AddShape(msoShapeDonut, x, y, size, size)
        bubbleShape.Adjustments.Item(1) = 0.1
    Else
        Set bubbleShape = Screen.Shapes.AddShape(msoShapeOval, x, y, size, size)
    End If
    bubbleShape.Line.Visible = msoFalse
    bubbleShape.Fill.ForeColor.RGB = vbWhite
    bubbleShape.Fill.Transparency = 0.1
End Sub

Public Property Get Self() As Object
    Set Self = Me
End Property

Private Sub Class_Terminate()
    bubbleShape.Delete
End Sub


最後にメインプロシージャ用に標準モジュールを挿入する。
このモジュールのオブジェクト名は任意。

標準モジュールに書くコードはこちら。

Option Explicit
#If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal ms As LongPtr)
#Else
Private Declare Sub Sleep Lib "kernel32" (ByVal ms As Long)
#End If
Sub BubbleAnimation()
    Const BUBBLE_LEVEL = 5 ' 1 to 10
    Screen.Clear
    Dim bubbles As Collection: Set bubbles = New Collection
    Do
        Application.ScreenUpdating = False
        Dim i
        For i = 1 To Int((BUBBLE_LEVEL - 0 + 1) * Rnd + 0)
            bubbles.Add New Bubble
        Next
        
        Dim j
        For j = bubbles.Count To 1 Step -1
            If Not bubbles(j).Float Then
                bubbles.Remove j
            End If
        Next
        Application.ScreenUpdating = True
        Sleep 10
        DoEvents
    Loop
End Sub

あとはBubbleAnimationを実行すればアニメーションが始まる。
終了方法は用意してないのでVBEの停止ボタンで止めている。

※今回のコードはお遊びなので割とやっつけコーディング。
 そのためマジックナンバーを多用してるが、良い子は真似しないように。

VBA 業務フローチャートをマクロで簡単に作成するツールの更新と公開

以前、フローチャートを作成するツールの作り方を紹介した。
thom.hateblo.jp

こちらの反響が良かったのと、ちょうどお盆休みをいただいてて良い機会なのでもうすこしブラッシュアップし、GitHubで公開してみた。

公開場所とダウンロード方法

以下のリンクにてxlsmファイルを公開中。
BreadChart/BreadChart.xlsm at master · thom-jp/BreadChart · GitHub

リンク先に飛んだら右下あたりのDownloadボタンでダウンロード可能。
f:id:t-hom:20180813222810p:plain

ソースコードだけ閲覧したい場合は以下BreadChartのトップから src/BreadChart.xlsm を開き、各ファイルを閲覧できる。
github.com

使い方

基本的に使い方は以前のバージョンと同じなので以下のGIFアニメ参照。
f:id:t-hom:20160731010231g:plain

以前のバージョンの課題と改善したポイント

以前はセルのマス目とプロセス(フローチャートのひとつひとつの四角)の位置が連動しておらず、プロセスが表示される場所に合わせてセルの幅をうまく調整する必要があった。
f:id:t-hom:20160731024442p:plain

今回は1セルに1プロセスが収まり、セル幅、高さを変えてもひな形作成時にプロセスがセルの中央に配置されるようになった。
f:id:t-hom:20180813223433p:plain

ひな形を作成する範囲は名前の管理で"BreadRange"を編集することで変更可能になった。
f:id:t-hom:20180813232447p:plain

また、以前のバージョンではコネクタは最短距離で結ばれる仕様だったので、以下のように混線することがあった。
f:id:t-hom:20180813223718p:plain

今回のバージョンでは横並び以外は原則上から下へ接続されるように変更した。
f:id:t-hom:20180813223916p:plain

また、以前のバージョンではプロセス入力モードで既存のプロセスをクリックした際に内容を1から入れ直しだったが、今回は既存の入力内容をあらかじめテキストボックスに表示させる仕様にした。これで文言を微修正したいときに最初から入れ直す必要がなくなった。
f:id:t-hom:20180813224117p:plain

以前のバージョンでは判断入力はあまり文字が入らなかった。
以下は、"判断入力はあまり文字が入らない"と入力した結果。
f:id:t-hom:20180813224700p:plain
3文字しか表示されてない。ひどい。。

改良したものがこちら。
f:id:t-hom:20180813232017p:plain

文字がはみ出しても枠線が邪魔にならないように枠線を廃止して、更に以下の設定を行っている。
f:id:t-hom:20180813224926p:plain

これに合わせてプロセスの文字入力もセンター揃えになった。

あとは地味な改良であるが、ひな形作成ボタン押下時に編集内容が消えてしまう旨と、チャートの完成ボタン押下時に一旦完成させると編集モードに戻せない旨の警告表示を追加した。

以上

VBA For Eachが順番を保証しない理由を自作のコレクションで説明

今回はVBAのFor Eachステートメントが仕様上、出力順を保証しない理由についてC#で作った自作のコレクションを使って説明してみようと思う。

さて、普段ならコードから紹介して後で説明に入るスタイルを取るが、今回はちょっとややこしいのでいきなり本題の説明から入ろうと思う。

前提知識

Excel VBAは、VBA言語によってExcelオブジェクトを操作することをいう。初心者のうちはVBAの命令もExcelオブジェクトのメソッドも一緒くたに考えてしまうけれど、VBAVBAExcelExcelなので、この2つが頭の中できちんと分離できていることが前提となる。

説明

VBAのFor Eachはその仕様上、出力順を保証していない。

とはいっても、たとえば以下のような選択範囲に対し、
f:id:t-hom:20180811185033p:plain

次のコードを実行してみると、

Sub hoge()
    For Each x In Selection
        Debug.Print x
    Next
End Sub

常に、1,2,3,4,5,6,7,8,9の順に出力される。

にも関わらず、For Eachが出力順を保証しないとはどういうことか。

For Each「が」保証していないというところがミソ。

VBAのFor Each文はオブジェクトに対して、「次のアイテムちょうだい」っていう命令を出す。
このとき、何を次のアイテムとして差し出すかはオブジェクトの実装によるということだ。
つまり順序よく綺麗に出力されるのはVBAがやってるんじゃなくて、ExcelのRangeオブジェクトがそういう風にアイテムを返しているからだ。
じゃあExcel側が保証してるかというと、それはそれで特にドキュメントが無いのでやはり将来その仕様が絶対に変更されないとは言えない。

さて、説明はここまで。
ここからはこの説明を裏付ける証拠として、登録したのとは逆順にFor Eachで出力されるコレクションを作ってみようと思う。

実演

※注意:今回は手探りの実験になったので、読者の皆さんの環境において動作を保証するものではありません。もし真似される場合はくれぐれも自己責任でお願いします。

Visual Studioの操作

今回はVisual Studio Community 2017を使用した。
起動するとスタートページが開くので新しいプロジェクトの作成をクリック。
f:id:t-hom:20180811190224p:plain

Visual C#のクラスライブラリ(.NET Standard)を選択して、名前はそのままでOK。
f:id:t-hom:20180811190456p:plain

ソースコード「Class1.cs」の編集画面になるので全部テキストを削除する。
f:id:t-hom:20180811194636p:plain

以下のコードを張り付け。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace ThomSample
{
    [ComVisible(true)]
    [Guid("A4496B44-8A89-4ABB-A6F0-91B81D56A1C9")]
    [InterfaceType(ComInterfaceType.InterfaceIsDual)]
    public interface IContraryCollection : IEnumerable
    {
        new IEnumerator GetEnumerator();
        void Add(string str);
    }

    [ComVisible(true)]
    [Guid("338178CD-EAFB-4AD6-B8AF-27AB8E50CB24")]
    [ProgId("ThomSample.ContraryCollection")]
    [ClassInterface(ClassInterfaceType.None)]
    public class ContraryCollection : IContraryCollection
    {
        public List<String> list;

        public ContraryCollection()
        {
            list = new List<string>();
        }

        public IEnumerator GetEnumerator()
        {
            for (int i = list.Count-1; i >= 0; i--)
            {
                yield return list[i];
            }
        }

        public void Add(string str)
        {
            list.Add(str);
        }
    }
}

ファイルメニューからすべて保存。
f:id:t-hom:20180811190827p:plain

ビルドメニューからClassLibrary1のビルドを実行。
f:id:t-hom:20180811190932p:plain

左下のステータスバーにビルド正常終了とでたらOK。

ソリューションエクスプローラーの余白で右クリックしてメニューからエクスプローラーで開くを選択。
f:id:t-hom:20180811191146p:plain

\bin\Debug\netstandard2.0の順に開くとClassLibrary1.dllが入っているので、デスクトップ等の分かりやすい場所に配置する。
f:id:t-hom:20180811191343p:plain

dllのレジストリ登録

作成したdllはCOMとして登録する必要があるが、これにはVisual Studioについてくるregasmというコマンドツールを使う。

このコマンドツールを使うには開発者コマンドプロンプトを使用する必要があり、さらに管理者モードで起動する必要がある。
まず、Windows10の場合は以下のパスに開発者コマンドプロンプトへのリンクがあるのでパスを開く。
C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio 2017\Visual Studio Tools

右クリックして管理者として実行する。
f:id:t-hom:20180811192123p:plain

cdコマンドでClassLibrary1.dllを置いた場所に移動する。
f:id:t-hom:20180811192251p:plain

コマンドで regasm /codebase ClassLibrary1.dll を実行する。
うだうだ文句を言われるが、ちゃんと管理者で実行できてれば一応成功する。
f:id:t-hom:20180811192438p:plain

これでレジストリ登録もOK。
ちなみにregasm /u ClassLibrary1.dll で登録解除できる。

VBAからの利用

ここからようやくVBA
適当にブックを作成して標準モジュールに次のコードを張り付けて実行する。

Sub hoge()
    Set c = CreateObject("ThomSample.ContraryCollection")
    c.Add "a"
    c.Add "b"
    c.Add "c"
    c.Add "d"
    c.Add "e"
    For Each x In c
        Debug.Print x
    Next
End Sub

すると、For Each文で、登録したのとは逆順に、e, d, c, b, aと出力される。

ちょっとC#の知識がないとややこしいのでうまく説明できないけれど、逆順に返している部分はこちら。

        public IEnumerator GetEnumerator()
        {
            for (int i = list.Count-1; i >= 0; i--)
            {
                yield return list[i];
            }
        }

yield returnはEnumeratorによってアイテムが要求される度に値を返す性質があるようだ。
for文とyield returnによってリターン用に値がストックされてるイメージかな。
それでFor Eachが実行されると値が要求するたびに出力される。このとき逆順なのは、for文を回す際にiをマイナスしてるから。
私自身、あんましC#に詳しくないので上手く説明できないのが残念だけど雰囲気だけでも伝わればと思う。

検証が終わったらregasm /u ClassLibrary1.dll で登録解除も忘れずに。

まとめ

これで出力順をコントロールしてるのはFor Eachではないことが明白になった。
繰り返しになるが、For Eachで次に何のアイテムが出力されるかは、オブジェクト次第ということだ。

参考

C#でGetEnumeratorを実装したクラスをCOMとして公開する方法について参考にさせていいただいた記事。
Exposing an Enumerator from Managed Code to COM. - limbioliong
上記ではList型が持つEnumeratorを返している。


以下はC#でGetEnumeratorを自分で実装する方法
ledsun.hatenablog.com
既存型のEnumeratorではなくて自分で実装したかったのでこちらを参考にさせていただいた。


以下はGUID(UUID)の生成に使ったツール
GUID生成ツール


GUID(UUID)とは何ぞやというのは以下
e-words.jp

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。