t-hom’s diary

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

VBA 過去直近のX曜日を求めるワンライナーコード

今回はさくっと短めの記事。

まず過去直近の日曜(当日含む)の日付を求めるコードがこちら。

?date-weekday(date,vbSunday)+1

クエスチョンマークは、イミディエイトウインドウで実行することを想定して書いている。
そして当日を含まない過去直近の日曜の日付を求めるコードはこちら。

?date - weekday(date-1,vbSunday)

どちらも曜日定数を代えると直近のその曜日が取れる。

結論は以上。

ここからは余談

当日を含まない方で、まず私が考えたのがIf文で当日の場合だけ分岐させる方法。

?iif(date = date-weekday(date,vbSunday)+1, date -7, date-weekday(date,vbSunday)+1)

もうちょっと楽にならんかなとTwitterでぼやいたところ、はけた氏より以下のアドバイスをいただいた。

weekdayの中で「date+1」とか「date-1」とか入れたら、簡単になりませんか?

なるほど、賢い。

当日を含めたくないなら、単純に前日起算してやれば良いのだ。
ということで両方のdateから1を引く。

?date-1-weekday(date-1,vbSunday)+1

 -1と+1を相殺して、こう。

?date - weekday(date-1,vbSunday)

完成。

VBA Excelがフリーズするほど大量のデータを特定列の値で分類して別シートに分ける処理

※注意 今回の記事はアイデアを記したものであり、コードの全体は掲載していません。ヒントを求めている方向けです。答えを求めてる方はごめんなさい。

Excelシートの特定列の値でレコードを分類し、個別のシートに転記する処理を作りたい場合がある。
いつもなら、レコードを1件ずつ読み取りながら転記していく。

ただ、IT運用業務ではサーバーのアクセスログなどの大量データを扱うことがあり、この方法ではどうやってもフリーズしてしまう事態に遭遇した。今回は16万件のレコード。このような大量データを扱う場合、セルに一つずつアクセスする普通のコーディングではExcelが長時間フリーズしてしまう。(大抵、会社のPCというのは普通の事務処理ができれば十分というスペックなので、家のPCよりも酷いことになる。)

ここまでデータがスケールしてしまうと、レコードを1件ずつ読み取る方式では厳しいのだ。

そこで、高速化テクの1つである、動的配列への転記を使うことにした。
thom.hateblo.jp

以下のようにシートから動的配列に転記し、それを分類ごとの動的配列に分け、各シートに転記する方法である。分類ごとの動的配列は分類名(特定列の値)をキーにして辞書型データに持たせることにする。
f:id:t-hom:20190227032305p:plain

ただ、二次元配列はそのまま扱うとやや面倒くさい。
私は普段からレコードをクラスモジュールに入れ、シートに作成したWriteLineメソッドで転記している。

たとえば、Sheet1モジュールに次のようなコードを挿入しておく。

Private Cursor As Long
Sub Init()
    Cursor = 2
End Sub
Sub WriteLine(ParamArray arr())
    For i = LBound(arr) To UBound(arr)
        Cells(Cursor, i + 1).Value = arr(i)
    Next
    Cursor = Cursor + 1
End Sub

すると、標準モジュールからは単にInitしてからWriteLineを実行するだけでデータを順次書き込むことができる。

Sub hoge()
    Sheet1.Init
    For i = Asc("A") To Asc("Z")
        Sheet1.WriteLine i, Chr(i)
    Next
End Sub

書き込み位置を指定するCursorはWriteLineメソッドの内部でインクリメントされるため、書き込みを指示するメインモジュールでは特に書き込み位置を意識しなくて良い。これは楽。

動的配列でもこれと同じ仕組みを使いたい。
そこで、WriteLineを実装したVirtualSheetというクラスを作って動的配列を格納することにした。
f:id:t-hom:20190227034114p:plain

今回、配列の動的拡張も考えていたのだが、二次元配列なので以下の制約があって諦めた。
thom.hateblo.jp

どのみちデータ量が多いので動的配列の拡張を繰り返すのは望ましくない。
そこで、VirtualSheetにInitメソッドを実装し、引数としてあらかじめレコード数を与えて配列サイズを確定させることにした。
分類ごとのレコード数はあらかじめ分類列だけを配列転記し、Dictionaryを使ってカウントしておく。
thom.hateblo.jp


更に、VirtualSheetにWriteToSheetメソッドを実行し、そこに引数で指定したワークシートにデータを書き込む処理を実装した。
f:id:t-hom:20190227035856p:plain

VirtualSheetのコードは以下のとおり。

Private arr()
Private cursor As Long
Const COLUMN_SIZE = 2
Enum Col
    列1 = 12
End Enum

Sub Init(row_size As Long)
    ReDim arr(1 To row_size, 1 To COLUMN_SIZE)
    cursor = LBound(arr, 1)
End Sub

Sub WriteLine(rc As Record)
    arr(cursor, Col.1) = rc.1
    arr(cursor, Col.2) = rc.2
    cursor = cursor + 1
End Sub

Sub WriteToSheet(ws As Worksheet)
    ws.Range(ws.Cells(1, 1), ws.Cells(UBound(arr, 1), UBound(arr, 2))).Value = arr
End Sub

今回は汎用性は犠牲にして、カラムサイズを定数で直接VirtualSheetに持たせた。WriteLineも汎用ではなく、Recordクラス型のオブジェクトを受け取って配列に格納するようにした。

Recordクラスは以下のとおり。

Public1 As Long
Public2 As String
Public Property Get Self() As Object
    Set Self = Me
End Property

最終的なデータ変換のプロセスは以下のようになった。
f:id:t-hom:20190227042753p:plain

これで20万件くらいのデータならなんとか待てるレベル。

今回私が作ったものは全体データは動的配列のまま扱ったが、実際にはSheetモジュールに動的配列を持たせ、GetNextで動的配列から1つずつ、Recordオブジェクトとして取り出す処理をしてたので、実質クラスを使ったのと同じようなことをしている。

いつもはシートからGetNextで取り出しつつ、別のシートにWriteLine。
今回はシートからGetNextで取り出しつつ、VirtualSheetにWriteLine。最後にWriteToSheet。

データ量がどれだけスケールするかによってデータ構造は使い分ける必要がある。
ただし、基本的にデータがスケール際に発生する複雑さをクラスで上手くラップしてやれば、メインロジックはだいたいいつも通りになる。

Excel 2013ではセル範囲に名前を付けるとズームアウトしたときに範囲名が表示される。

表題の件、もう何年も使ってるのに今まで知らなくて、偶然見つけてとても驚いたのでここに記しておく。
私の手元の環境でしか検証してないので他の環境は不明。

たとえばこんな風に名前が定義されていたとする。
f:id:t-hom:20190224140015p:plain

それぞれ開始セルと終了セルには私が予め文字列を書いておいた。
f:id:t-hom:20190224135804p:plain

これを25%までズームアウトすると。。。

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


ええええええええええええええええっ!!!!!!!

ひょっとしてExcel慣れしている方からすると常識なのかもしれないけど、私は何年もExcel触ってたのに知らなかった。

で、これが何の役に立つんだって?

知らん。

VBA タイムスタンプからユニークキーを生成する。

Excelでレコードごとにユニークキーが欲しくなることがある。

ユニークキーとは、一意にアイテムを特定でき、変わることのないコードのことで、数桁の数値や文字列で表現される。
実用上は単純な連番でも全く問題ない。

ただ以下のようなシチュエーションで連番が崩れたときに、何となく気持ち悪い。

  • 項目の削除で欠番が発生したとき
  • 後からアイテムを挿入した時

人間心理としては、改めて1から番号を連番で振り直したくなるものである。
そして実際に振り直してしまう人が多いが、実はこれ、やってはいけないアンチパターンである。

もう一度いう。ユニークキーとは、一意にアイテムを特定でき、変わることのないコードである。
あるアイテムをX番だとして覚えておく。これでアイテムの名称変更があってもX番を参照すれば良い。
それさえ管理すれば安心安定のキー。絶対的に信頼できる唯一のフィールド。
それが、変わる。。だと!?もはや何も信じられない。

とはいえ、綺麗に1番からカウントされてる以上、オーダーが崩れたら気になるというのはとてもよく分かる。

どうすれば良いか。結構長いこと悩んでいたのだが、最近画期的なアイデアがひらめいた。

要するに綺麗な連番を採用するから気になるのであって、最初から意味不明なコードをユニークキーに使えば順番がバラバラだろうが何だろうが気にならなくなる!

そこで今回は、パッと見て意味が分からないユニークキーをVBAで生成してみようと思う。

今回生成するユニークキーの外観

たとえば、2019/02/19 0:59:40に生成したひとつ目のユニークキーは「XKR_2RG#0」である。
この英数字+記号の9文字で、年月日時分秒+重複に備えた連番を表すことができ、その気になれば元の日付にデコードもできる。
※今回デコード用のコードは用意してません。

前置きが長くなったけど、ここから実行結果とコードと解説

VBAコード

※コードのうち、奇数変換部分は以下の記事のものを流用させていただきました。ありがとうございます。
※流用の際、一部識別子名を変えてます。
hex309.hatenablog.com

Sub TestGetUniqueKey()
    Debug.Print Now
    Debug.Print GetUniqueKey
    Debug.Print GetUniqueKey
    Debug.Print GetUniqueKey
    
    '連番部分がリセットされることを確認するために1秒待つ
    Application.Wait Now() + TimeValue("00:00:01")
    Debug.Print Now
    Debug.Print GetUniqueKey
    Debug.Print GetUniqueKey
    Debug.Print GetUniqueKey
End Sub

Function GetUniqueKey() As String
    Static n As Long
    Static storedStamp As String
    Dim timeStamp As String
    timeStamp = _
        RadixConversion(CLng(Date), 36) & "_" & _
        RadixConversion(Hour(Time) * 60 ^ 2 + Minute(Time) * 60 + Second(Time), 36)

    If storedStamp = timeStamp Then
        n = n + 1
    Else
        n = 0
    End If
    
    storedStamp = timeStamp
    GetUniqueKey = timeStamp & "#" & n
End Function

Public Function RadixConversion(ByVal num As Long, ByVal Radix As Long) As String
    Dim Quotient As Long
    Dim Remainder As Long
    Dim Answer As String
    Quotient = num
    Do
        Remainder = Quotient Mod Radix
        Quotient = Quotient \ Radix
        Answer = GetNumChar(Remainder) & Answer
    Loop Until Quotient = 0
    RadixConversion = Answer
End Function

Private Function GetNumChar(ByVal num As Long) As String
    Dim temp As Variant
    temp = Split("0 1 2 3 4 5 6 7 8 9 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z")
    GetNumChar = temp(num)
End Function

実行方法

TestGetUniqueKeyを実行するとイミディエイトウインドウにキーが3つ出力され、1秒後に続いてキーが更に3つ出力される。

例)

2019/02/19 1:24:42 
XKR_3X6#0
XKR_3X6#1
XKR_3X6#2
2019/02/19 1:24:43 
XKR_3X7#0
XKR_3X7#1
XKR_3X7#2

解説

まず日付は桁数を減らすためにCLng関数でシリアル値に変換している。

前提として、VBAでは日時データをシリアル値というDouble型の内部データで管理している。
試しにイミディエイトウインドウに「?now;vbnewline;cdbl(now)」と入力してみると、現在日時とそれをダブル型で表した数値が表示される。

2019/02/19 1:07:54 
 43515.0471527778 

このとき整数部分が日付を表す。(上の例でいうと43515が日付を表し、1899/12/31からの経過日数である。)
秒まででよければ少数部は逆に、010754と6桁で表した方が桁が少ないので、ここでは日付部分だけシリアル値を採用する。

そして日付シリアル値を1~Zまでの英数字を使って36進数で表すと2019年現在、3桁に収まる。
たとえば本日2019/2/19は、XKRである。

次に0時からの経過秒数を求め、これも36進数に変換する。
試しにイミディエイトウインドウに「?time;vbnewline;Hour(Time) * 60 ^ 2 + Minute(Time) * 60 + Second(Time)」と入力してみると、現在時刻と0時からの経過秒数が表示される。

1:17:06 
 4626 

一日は24時間なので最大値は「?24*60^2」で求まり、86400である。235959と比べて1桁少なく済む。
さて、4626を36進数に直すと、「?RadixConversion(4626,36)」で3KIと出た。

これをつなげてXKR_3KIとする。
さらに秒まで重複したときのために、末尾に#と連番を付加する。

具体的には、ユニークキーを得る関数GetUniqueKey内部で、Static変数storedStampにタイムスタンプを記録しておき、次の呼び出し時に変化が無ければStatic変数nを加算する。タイムスタンプが変化したらnをリセットすることで連番nを制御している。

おわりに

実質連番なのである程度の規則性は出てしまうけど、綺麗に1~並んでるよりは、欠番とかソート順とかが気にならなくなる。
これなら振り直したり、しないよね?

ちなみに今回のマクロは基本的におひとり様もしくは排他制御下で使うことが前提。
複数名が同時に振り出すようなシチュエーションではキーの重複もあり得るので、そのような場面には適用しないように注意。

以上

VBA オートシェイプを使って般若心経をスクロールするアニメーション

今回の記事はオートシェイプを左から右へスクロールさせながらメッセージを表示させるマクロ。
ありがたいお経を題材にしてみた。
f:id:t-hom:20190217162212g:plain

コード

クラスモジュール

クラス名は「CharBox」として、以下のコードを張り付ける。

Private sh As Shape
Private limit As Double
Private parent As Collection
Private message As String
Private nextCharBoxCreated As Boolean
Private Sub Class_Initialize()
    Set sh = Screen.Shapes.AddShape(msoShapeRectangle, 0, 0, 0, 0)
    sh.Visible = msoFalse
    sh.Line.Visible = msoFalse
    sh.Width = 50
    sh.Height = 50
    sh.Top = START_Y
    sh.Left = START_X
    sh.Fill.ForeColor.RGB = rgbWhite
    With sh.TextFrame2.TextRange.Font
        .Size = 36
        .NameComplexScript = "HGS行書体"
        .NameFarEast = "HGS行書体"
        .Name = "HGS行書体"
        .Fill.ForeColor.RGB = rgbWhite
    End With
    sh.TextFrame2.TextRange.ParagraphFormat.Alignment = msoAlignCenter
    sh.TextFrame2.VerticalAnchor = msoAnchorMiddle
    sh.Visible = msoTrue
    DoEvents
End Sub

Sub Init(c As Collection, limit_ As Double, message_ As String)
    limit = limit_
    message = message_
    Set parent = c
    c.Add Me, CStr(ObjPtr(Me))
    sh.TextFrame2.TextRange.Text = Left(message, 1)
End Sub

Function GetPercentage() As Double
    GetPercentage = (sh.Left - START_X) / (END_X - START_X)
End Function

Sub Move(amount)
    If IsMovable Then
        If GetPercentage < 0.5 Then
            n = 255 - CInt(255 * GetPercentage * 2)
        Else
            n = CInt(255 * (GetPercentage - 0.5) * 2)
        End If
        sh.Left = sh.Left + amount
        sh.TextFrame2.TextRange.Font.Fill.ForeColor.RGB = RGB(n, n, n)
    Else
        parent.Remove CStr(ObjPtr(Me))
    End If
    
    If Not nextCharBoxCreated Then
        If sh.Left > START_X + sh.Width Then
            nextCharBoxCreated = True
            If Len(message) > 1 Then
                With New CharBox
                    .Init parent, limit, Mid(message, 2)
                End With
            End If
        End If
    End If
End Sub

Function IsMovable() As Boolean
    IsMovable = sh.Left < limit
End Function

Private Sub Class_Terminate()
    sh.Delete
End Sub

標準モジュール

モジュール名は任意。以下のコードを張り付ける。

Public Const START_X As Double = 20
Public Const START_Y As Double = 20
Public Const END_X As Double = 500

Sub HeartSutra()
    For Each sh In Screen.Shapes
        sh.Delete
    Next
    
    Dim c As Collection
    Set c = New Collection
    With New CharBox
        .Init c, END_X, Screen.Range("A1").Value
    End With
    
    Dim cb As CharBox
    Do While c.Count <> 0
        Application.ScreenUpdating = False
        For Each cb In c
            cb.Move 2
        Next
        Application.ScreenUpdating = True
        DoEvents
    Loop
    Debug.Print "End"
End Sub

実行方法

準備

  • Excelの表示タブで枠線を消しておく
  • Sheet1のモジュール名をScreenとしておく。
  • A1セルに般若心境を入力し、白文字にして見えなくしておく。

入力するのはこちら。

仏説摩訶般若波羅蜜多心経     観自在菩薩行深般若波羅蜜多時照見五蘊皆空度一切苦厄舎利子色不異空空不異色色即是空空即是色受想行識亦復如是舎利子是諸法空相不生不滅不垢不浄不増不減是故空中無色無受想行識無眼耳鼻舌身意無色声香味触法無眼界乃至無意識界無無明亦無無明尽乃至無老死亦無老死尽無苦集滅道無智亦無得以無所得故菩提薩埵依般若波羅蜜多故心無罣礙無罣礙故無有恐怖遠離一切顛倒夢想究竟涅槃三世諸仏依般若波羅蜜多故得阿耨多羅三藐三菩提故知般若波羅蜜多是大神呪是大明呪是無上呪是無等等呪能除一切苦真実不虚故説般若波羅蜜多呪即説呪曰羯諦羯諦波羅羯諦波羅僧羯諦菩提薩婆訶般若心経

※ちなみに埵と罣はShift-JISに無くて(多分日本の文字じゃない)、行書体が無いためその2文字だけ角ばったゴシック体になってしまう。

実行

  • HeartSutraを実行するだけ

解説

今回のコードはオートシェイプ1つに対して1つのオブジェクト(CharBox型)を対応付けて管理している。

  1. CharBoxはコンストラクタでシェイプを作成する。
  2. シェイプはSTART_Xに生成され、Move命令ごとに、END_Xに向かって移動する。
  3. シェイプは移動距離が50%に達するまで、移動の度にフォントの色を徐々に濃くし、その後は100%に達するまで徐々に薄くする。
  4. CharBoxはコンストラクタとは別のInitプロシージャでコレクションを受け取り、自身の格納アドレスをキーに自身をコレクションに登録する。一方でメインマクロはコレクションのみを保持しており、CharBoxは保持していない。したがってCharBoxはコレクションから消えると参照を失って消滅する仕組み。
  5. CharBoxは、開始位置から自身の幅だけ進むと、次のCharBoxを生成する。ただし生成されたCharBoxが保持されるのはやはりコレクションのみで、CharBox同士に親子参照関係は無い。
  6. CharBoxはEND_Xに達すると自身の格納アドレスをキーに自身をコレクションから抹消する。
  7. 参照を失ったCharBoxは消滅する。消滅の直前、デストラクタによりシェイプは削除される。

オブジェクト指向でつまづく7つのポイントと処方箋

こちらの記事で伊藤先生も書かれているように、この数年でVBAのクラスモジュールに関する情報が充実してきている。

これからクラスモジュールについて学習しようと考えている方も沢山(期待を込めて)いらっしゃることと思う。

そこで今回は、オブジェクト指向を学習するにあたってつまづきやすいポイントを紹介し、その処方箋としてアドバイスを書いてみようと思う。

私がオブジェクト指向でつまづきやすいポイントは以下の7つだと考える。

  1. オブジェクト指向の目的が理解できない
  2. オブジェクト指向の悪評に惑わされる
  3. オブジェクト指向の用語がややこしすぎる
  4. オブジェクト指向の前に覚えるべきことを覚えていない
  5. オブジェクト指向の一部の特長を過大に受け止めてしまう
  6. オブジェクト指向での具体的な実装方法が分からない
  7. オブジェクト指向の活用方法が分からない

これらは私自身がつまづいたポイントでもある。こうしてみると結構ある。
では、具体的にどういうことなのか一つずつ説明していく。

オブジェクト指向の目的が理解できない

症状

初心者向けの解説でオブジェクト指向を「たい焼き」とか「動物」に例えて説明されているケースがある。
喩えを持ち出すことは悪いことではないけれど、私自身、それが業務のプログラムにどう繋がるのかサッパリ理解できなかった。
Animalクラスから継承させたDogやCatにワンニャンさせてみたところで、それが何かの役に立つとは思えないのだ。

処方箋

まずオブジェクト指向の最大の恩恵は、データや命令をセットにして扱えることであり、それによりコードをスッキリと分かりやすく記述できることである。
オブジェクト指向はコードを整理整頓する技術であり、それが根本の目的と言っても過言ではない。
何かすごいことをする技術だと思って手を出したけど今一つメリットが分からない、今までの知識でも事足りると感じているとしたら、それはある意味正しい。
散らかった部屋でも人は生きていけるように、コードは汚くても動く。
オブジェクト指向の恩恵は、コードを読んで理解し、メンテナンスする際に初めて実感できるだろう。

オブジェクト指向の悪評に惑わされる

症状

私が専門学校でJavaやVB.Netを習っている頃は、まだまだオブジェクト指向をDisる現役プログラマが沢山いた。オブジェクト指向は高度な考え方であり、そんなものを現場に持ち込んだら破綻する、と。当時の開発現場では使用する言語はJavaでも、中身はCOBOLさながらのゴリゴリの手続き型ロジックで書かれていることが多いと書かれた記事を読んだことを覚えている。
人間、一度身に着けた知識はなるべく長く使い回したいもので、ある程度年齢を重ねてから新しい手法を受け入れるのはとても勇気がいることだ。

処方箋

これは今の若手には関係ない話だと思う。VBA界隈を別としてオブジェクト指向は当たり前のように使われており、その有用性を否定する声は聞かなくなった。むしろオブジェクト指向はもう古い、これからは関数型だなどという意見も耳にするようになったけれど、これを真に受けてオブジェクト指向はもはや用済みだと考えるのは間違い。「オブジェクト指向はもう普遍化しており、今更取り立てて騒ぐようなものではない(つまりプロの開発者なら常識的に身に着けていること)、これから大きく取り上げるべきは関数型だ。」という解釈が正しいと思う。

オブジェクト指向の用語がややこしすぎる

症状

オブジェクト指向の言葉はややこしい。まず「オブジェクト」と「指向」さえもハッキリと分からないという方もいると思う。そのうえフィールドだのメソッドだのインスタンスだのと横文字が並んでいてますます混乱を増長しているようだ。さらに継承・委譲・多態性などという耳慣れない日本語まで登場する。

処方箋

これはオブジェクト指向について詳しく調べている際に陥る罠である。調べれば調べるほど、芋づる式に新しい言葉が出てきてますます混乱する。
しかし、実はVBAに限定すれば、覚えるべき言葉は限られる。例えば「継承」はVBAには存在しないので急いで覚える必要はない。
ここにVBA使いがオブジェクト指向に手を出すにあたって必ず覚えて欲しい言葉と、任意で覚えると良い言葉、覚えなくても何とかなる言葉、むしろ覚えない方が良い言葉の4つに分類しておく。

必ず覚えてほしい言葉 オブジェクト・クラス・インスタンス・プロパティ・メソッド
任意で覚えると良い言葉 メンバー・フィールド・隠蔽・アクセサ・カプセル化・コンストラクタ・デストラクタ
覚えなくてもVBAでは何とかなる言葉 継承・多態性・指向・インターフェース・デザインパターン
VBAではむしろ覚えない方が良い言葉 メッセージ

覚えない方が良い言葉というのは少々言い過ぎかもしれないが、VBAにおけるオブジェクト指向において、メッセージという言葉は不要な混乱を招く。
実はオブジェクト指向には2系統の思想があり、メッセージを扱うのはVBAとは別のオブジェクト指向体系である。従ってメッセージという言葉が登場する解説はVBA使いにはオススメしない。

オブジェクト指向の前に覚えるべきことを覚えていない

症状

なんとなくクラスモジュールに手を出してみたものの、プロシージャ?モジュールレベル変数?引数?戻り値?何それ状態。

処方箋

クラスモジュールを使用するにはプロシージャの分割やモジュールレベル変数、プロシージャ間の値の引き渡し等の前提知識が必要となる。それらの知識が無い場合はまずそこから学習が必要である。VBAも最近は良書が揃っているので自分が理解できるレベルの本で少しずつレベルアップすると良いと思う。
VBAと比べると他の言語は文法の解説書が充実しているので、VBAの書籍で今一つ理解できない場合は、プログラミングの基礎を身に着けるために一旦他の言語に手を出してみるというのも良い経験になると思う。

オブジェクト指向の一部の特長を過大に受け止めてしまう

症状

手続き型しか知らないプログラマがオブジェクト指向を獲得すると、まるで人類の英知に触れたかのような気持ちになり、オブジェクト指向のすばらしさについて語り歩くようになる。特に、オブジェクト指向の3大要素「カプセル化、継承、ポリモーフィズム」と、「いくらでもインスタンスが作れる」というオブジェクト指向の特長が強力なメリットとして強く語られる。また、カプセル化 = 隠蔽という認識が広まっており、オブジェクト指向本来のメリットである「データや命令をセットで扱う」という特長(本来のカプセル化)が霞んでしまっているのはとても残念だ。
結果的にオブジェクト指向は「難しくて大層なモノ」と誤解され、初心者から敬遠されているように思える。

処方箋

オブジェクト指向は単にデータや命令をセットにしてひと塊で扱うための整理整頓の技術である。だからその目的で気軽に使って良い。
なんならデータだけしか使わずにユーザー定義型と同じような使い方だってアリだし、インスタンスを一つしか使わなくても構わない。現にインスタンスが一つであることを保証するテクニック(シングルトン)まで登場しているくらいだ。
私は長らく、「たい焼き」のせいで、「複製しなければオブジェクト指向を採用する意味はない」と誤解していた。オブジェクト指向はもっと気軽に使って良い。

参考記事

thom.hateblo.jp

オブジェクト指向での具体的な実装方法が分からない

症状

私がVBAでオブジェクト指向を採用しようと思ったとき、クラスモジュールの使い方に関する説明は殆ど無かった。よって実際にクラスを作成する段階での記述方法について、思考錯誤しながらとても苦労した覚えがある。

処方箋

今は良書が揃っていてWebサイトでの解説も色々出そろっているのでそちらを参照すると良い。

参考記事

thom.hateblo.jp

オブジェクト指向の活用方法が分からない

症状

クラスモジュールの作り方は分かったが、それを活用する具体的なアイデアが思い浮かばない。

処方箋

これについては当ブログでも色々と記事を書いたので参照していただけると幸いである。
代表的なものは以下。
thom.hateblo.jp

それと、技術が無いとアイデアはなかなか湧いてこない。活用方法の前に、そもそもクラスモジュールの使い方をマスターしているかもう一度学習してみるのも良いと思う。
これは道具が先か、それを使うシチュエーションが先かという問題である。先に道具を知っているからこそ、適切なシチュエーションに遭遇した際に、「ここで使うと良いんじゃないか」と分かる。道具を知らないと、非効率なシチュエーションに何度出くわしても、それが普通のことだと思って見過ごしてしまう。
だからまずは道具であるクラスモジュールについてきちんと学習しておくことが大事。
使いどころは、強引にでも使っているうちに、だんだんわかってくる。

まとめ

オブジェクト指向は分かってしまえばそんな大層な技術ではないのだが、いかんせん学習における罠が多い。オブジェクト指向の学習の成否はいかにワナにハマらないかにかかっていると言っても過言ではない。
これから学習される方は、この7つの罠を注意深く避けて学習を進めるようにしていただけるとスムーズにオブジェクト指向に入門できると思う。

VBA 環状矢印を複数個つなげてリング状にするマクロ

オートシェイプで矢印を綺麗な環状に並べたい場合がある。
f:id:t-hom:20190129212208p:plain

その名の通り「環状矢印」というシェイプを使うのだが、手でやるとなかなか綺麗にできない。
試しに手でやってみたのがこちら。。
f:id:t-hom:20190129212624p:plain

ひどい。

そこで今回は環状矢印を綺麗に並べるためのマクロを作成してみた。

コード

Enum CircularArrowAdjustmentItemValues
    eThickness = 1
    eStartDegree = 4
    eEndDegree = 3
    eArrowheadSize = 5
End Enum
Type Position
    x As Double
    y As Double
End Type

Sub Draw360DegreeCircularArrows()
    '固定値:さわるな危険
    Const START_DEGREE_OFFSET = -90
    Const END_DEGREE_OFFSET = -110
    
    '設定値:適宜変更よろしく
    Const RING_SIZE = 300 'ここで指定したリングサイズになる。
    Const ARROW_THICKNESS = 0.1 '矢印の太さ。0~0.2くらいまでを、少数で指定する。
    Const NUMBER_OF_ARROWS = 5 'ここで指定した数の矢印が書かれる。12くらいが限界。
    Dim pos As Position
        pos.x = 100
        pos.y = 100
    
    'メインコードここから
    Dim startDegree As Long
    Dim endDegree As Long
    Dim i As Long
    For i = 1 To NUMBER_OF_ARROWS
        Dim sh As Shape: Set sh _
            = ActiveSheet.Shapes.AddShape(msoShapeCircularArrow, pos.x, pos.y, RING_SIZE, RING_SIZE)
        With sh
            startDegree = endDegree
            endDegree = startDegree + 360 / NUMBER_OF_ARROWS
            .Fill.ForeColor.RGB = GetRandomColor
            .Line.Visible = msoFalse
            .Adjustments.Item(eThickness) = ARROW_THICKNESS
            .Adjustments.Item(eStartDegree) = startDegree + START_DEGREE_OFFSET
            .Adjustments.Item(eEndDegree) = endDegree + END_DEGREE_OFFSET
            .Adjustments.Item(eArrowheadSize) = .Adjustments.Item(eThickness)
        End With
    Next
End Sub

Function GetRandomColor()
    Dim r: r = WorksheetFunction.RandBetween(0, 255)
    Dim g: g = WorksheetFunction.RandBetween(0, 255)
    Dim b: b = WorksheetFunction.RandBetween(0, 255)
    GetRandomColor = RGB(r, g, b)
End Function

実行方法

Draw360DegreeCircularArrowsを実行すると冒頭の図のように矢印が綺麗に環状に並ぶ。
定数で指定されている設定値を変えることで違った結果になる。

実行結果サンプル

NUMBER_OF_ARROWSに12を指定したのがこちら。
f:id:t-hom:20190129214035p:plain

ARROW_THICKNESSを0.05に指定したのがこちら。
f:id:t-hom:20190129214255p:plain

NUMBER_OF_ARROWSとARROW_THICKNESSはバランスを取る必要があり、太い矢印で個数を増やしすぎるとバグる。
ARROW_THICKNESSが0.2、NUMBER_OF_ARROWSが12の結果がこちら。
f:id:t-hom:20190129214722p:plain
なぜか真ん中に円が。

ARROW_THICKNESSを0.5にしてみると。。。風車みたいになった。
f:id:t-hom:20190129214808p:plain

ARROW_THICKNESSが0.2でも、NUMBER_OF_ARROWSが少ないとサマになる。
f:id:t-hom:20190129215112p:plain

角度補正について

START_DEGREE_OFFSETがマイナス90度、END_DEGREE_OFFSETがマイナス110度となっている。
実は矢印の頭のサイズ分、.Adjustments.Item(eStartDegree)と.Adjustments.Item(eEndDegree)では角度の開始値がズレているためだ。
f:id:t-hom:20190129215847p:plain

このままではとても分かりにくいので、両方とも時計の12時を0度として時計回りの角度で指定できるようにするためにオフセット値を引いて角度を補正している。

以上。

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