t-hom’s diary

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

ひと足先にPythonでCOMを利用したExcel操作をやってみる

MicrosoftExcelPythonを搭載することを真剣に検討しているらしい。
www.itmedia.co.jp

まだ検討の段階なので、どんな実装になるかは明らかにされていないが、基本的にはVBAと同じくCOMの操作になるんじゃないかと思う。

今回はひと足先に、PythonでCOMを利用したExcel操作をやってみようと思う。

参考にしたサイトはこちら。
Python Win32 Extensions - MyMemoWiki

COMとは

COMとは「コンポーネント・オブジェクト・モデル」の略で、様々なプログラミング言語から呼び出すことができるソフトウェア部品の技術仕様として1997年にMicrosoftが発表したものである。
.NET登場以前はWindowsの標準的な技術であり、現在も数多くのソフトウェアがCOMに依存している。

COMではオブジェクトをメモリにどういう風に配置するか、自身が持つプロパティやメソッドを呼び出し元にどうやって伝えるかといった細々とした仕様が取り決められているので、COMの仕様に従った部品は様々な言語から利用することができる。

Excel VBAも、実はVBA言語でExcelのCOMオブジェクトを操作する行為であり、例えばRange("A1").Value = "Hello"という命令はVBAというよりも、ほぼCOMの操作に近い。

厳密にいえば、以下の特徴はCOMではなくVBAの仕様である。

  • イコールで代入する
  • オブジェクトのプロパティにはドットでアクセスする
  • 文字列はダブルクォーテーションで囲む
  • 大文字、小文字を区別しない

ただ最初の3つは多くの言語で採用されているので、Pythonで書いてもJavaで書いてもRubyで書いても、Range("A1").Value = "Hello"となる。そうでない言語もあるかもしれないが。

PythonでのCOM操作(準備編)

Pythonのインストールは多くのサイトで解説があるので本記事では省略する。
PythonでCOMを利用するためには、別途「Python Win32 Extensions」のインストールが必要である。

インストールするべきWin32 ExtensionsのバージョンはPythonのバージョンによって異なるため、まずはPythonのバージョンをチェック。
コマンドプロンプトを起動し、pythonと入力するとバージョンが表示される。
f:id:t-hom:20171224200547p:plain

そして以下リンクよりSourceForgeへアクセスし、
https://sourceforge.net/projects/pywin32/files/


pywin32のフォルダリンクを開く。
f:id:t-hom:20171224201057p:plain

するとビルドの一覧が表示される。ここは一番新しいもので良い。
f:id:t-hom:20171224201230p:plain

ここからちょっとややこしいけれど、使っているPCのCPUがAMD製かIntel製かによって変わる。
上半分のamd64と付いたものがAMD用、下半分がIntel用になっている。
f:id:t-hom:20171224202131p:plain

私の環境はIntel製CPUで、Pythonのバージョンが3.6.2なので、pywin32-221.win32-py3.6.exeをダウンロードした。

インストールは英語だけれど、「次へ、次へ、次へ、完了」と進めるだけなので特に問題ないと思う。

PythonでのCOM操作(実践編)

さて、では実際にExcelを操作してみよう。
先ほどバージョン確認のためにPythonを起動したが、これはexit()を入力して終了させて、再度pythonコマンドで起動しておく。
また、起動しているExcelがあれば終了させておいたほうが分かりやすい。

そして、入力待ちのプロンプト「>>>」が表示されたらimport win32com.clientと入力してEnter。

>>> import win32com.client

これは先ほどインストールしたPython Win32 Extentionsを読み込むための命令で、成功すれば単に次の入力プロンプト「>>>」が表示される。
他にズラズラと表示されてたらエラーなのでスペルミスがないか、インストールに成功しているか再確認する。

次に、変数xlAppにExcelを代入する。

>>> xlApp = win32com.client.Dispatch("Excel.Application")

ややこしい操作だけれど、VBScript のSet xlApp = CreateObject("Excel.Application")に相当する操作だと思っていだけると良い。
Pythonには変数宣言が存在しないので、単に変数名に値を代入するという操作になる。
また、VBAの場合はオブジェクトの代入にはSetを使うが、Pythonは数値や文字などと同じく単に代入するだけである。

さて、この段階でExcelがバックグラウンドで起動されているが、画面上はまだ表示されていない。
次に、Excelを表示させる。

>>> xlApp.Visible = -1

xlAppはExcel VBAのApplicationと同じように扱える。VBAの場合はApplication.Visible = Trueと書くが、VBAではTrueの値が-1なのに対し、PythonではTrueの値が1なので、ここでは-1を指定している。

ただVBAは0をFalse、0以外をTrueと判定するので、xlApp.Visible = Trueと書いても動作する。以降は分かりやすいようにPythonのTrue、Falseを使って書く。

さて、ここまで来たら、基本的なCOM操作はVBAと変わらない
ワークブックを追加してシート名をSampleに変え、A1セルにHelloと入力してクローズする処理をやってみる。

>>> wb = xlApp.Workbooks.Add()
>>> sh = wb.Sheets(1)
>>> sh.Name = "Sample"
>>> sh.Range("A1").Value = "Hello"
>>> xlApp.DisplayAlerts = False
>>> wb.Close()
>>> xlApp.DisplayAlerts = True
>>> xlApp.Quit()

コマンドを入力してEnterするたびに、処理が実行されるのが分かる。
ちなみに上記の処理をVBAで書くとこうなる。

    Set xlApp = Excel.Application
    Set wb = xlApp.Workbooks.Add
    Set sh = wb.Sheets(1)
    sh.Name = "Sample"
    sh.Range("A1").Value = "Hello"
    xlApp.DisplayAlerts = False
    wb.Close
    xlApp.DisplayAlerts = True
    xlApp.Quit

VBAPythonの違いを見てみると、

  • オブジェクトの代入にSetをつけるかどうか
  • 引数のないメソッド呼び出しに()を付けるかどうか

という2点を除いて、あまり変わらない。

つまりCOMである以上、オブジェクトの操作感はそれほど変わらないということになる。
ではPythonだと何が嬉しいのかというと、豊富なライブラリ群と洗練された言語仕様である。

Pythonの機能を使ってマクロを作る

折角なのでPythonのライブラリを使ってExcelマクロを作ってみよう。

サンプルなのでVBAでも簡単にできるような内容にしておく。

>>> import win32com.client
>>> import datetime
>>> xlApp = win32com.client.Dispatch("Excel.Application")
>>> xlApp.Visible = True
>>> wb = xlApp.Workbooks.Add()
>>> ws = wb.Sheets(1)
>>> now = datetime.datetime.now()
>>> ws.Range("A1").Value = now.hour

これは最初にPythonのdatetimeライブラリを読み込んでおき、変数nowに現在日時を入れて「〇時」だけを取り出すマクロ。
Pythonのdatetime型はオブジェクトなので、hourプロパティで時刻を取り出すことができる。
ブログ執筆時点では夜9時なので、A1セルには21が挿入される。

VBAならNow()関数とHour()関数を組み合わせるところだが、こちらはVBA専用の関数なのでPythonからは利用できない。
またCollectionもVBAライブラリのクラスなのでPythonからは利用できない。
VBA自体もおそらくCOMなので、PythonからVBA関数を利用する方法はあるかもしれない。

更に、配列などのデータ型や、IfやForなどの構文もVBAとは異なる。
共通しているのはあくまでCOMの操作部分だけである。

for文を例にPythonの特徴を紹介

実はPythonVBAのようなFor文は無く、VBAでいうところのFor Eachが標準のfor文にあたる。
たとえば1から10まで出力させたい場合、VBAでは次のように書く。

For i = 1 to 10
    Debug.Print i
Next

Pythonではこうなる。

>>> for i in range(1, 11):
...     print(i)
...

ここでrangeはpythonの関数なのでExcelのRangeとは関係ないことに注意。
1~11を指定するとひとつ少ない1~10が作られるという、VBA使いには馴染みのない仕様である。

この...は>>>と同じく前行から続いているという意味のプロンプトなので自分で入力する必要はない。
for文の最後は必ずコロンを入力して改行する。そしてTabキーでインデントし、print(i)と入力してEnter。
これで終わりなら、プロンプト「...」でそのままEnterを入力すると1~10まで出力される。

また、VBAではブロックを開始と終了のステートメントで表すのに対し、Pythonではインデントで表す。
コードの見た目がそのまま文法になってしまうので自由にインデントすることはできない。
これにはスタイルの違いによる読みにくさが発生しないというメリットがある。

さて、では最後のサンプルとしてPythonのforでVBAのFor Eachと同じようにセルに対して使ってみよう。
次のコードを実行するとA1からA5にそれぞれHelloと入る。

>>> for r in ws.Range("A1:A5"):
...     r.Value = "Hello"
...

これでCOMの操作は同じであるが、標準関数や文法が異なるということがお分かりいただけたかと思う。

終わりに

ExcelPythonが標準搭載される意味はとても大きいと思う。
言語を選択できるようになれば、マクロとVBAの違いなども説明しやすくなる。また、何がExcelのCOM操作で、何が言語機能なのかがよりハッキリするので、Excel VBAの構成もよりクリアに認識できるようになると思う。

現在COM以外の方法でPythonからExcelを操作するライブラリなどもあるけれど、個人的には標準搭載するならCOM方式が良いのではないかと思う。

MicrosoftはCOMの後継にあたる.NETを推進しているが、.NET言語であるVB.NetC#を外してあえてPythonの搭載を検討するあたり、Excelの.NET対応は考えていないのではないかと思う。

COMならPythonのスッキリした文法と豊富なライブラリの恩恵を受けつつ、従来のVBAユーザーもPythonに移行しやすい。

今のところPythonは完全に動的型付け言語なので、入力補完などのIDE機能は期待できない。
手入力も面倒なので、この辺りはMicrosoftが何か考えてくれるといいなと思う。

さて、今回はすべてコマンドラインからの入力だったが、もちろんファイルに書いて実行することもできる。
興味があれば書籍や他のWebサイト等で調べてみると良いと思う。

本格的にPythonを学習される場合は以下の書籍がオススメ。

Pythonスタートブック

Pythonスタートブック

レビュー記事も書いているのでよろしく。
thom.hateblo.jp

Access VBAでフォームのコントロールイベント共通化

前回紹介したExcel VBAのユーザーフォームに動的メニューを追加する方法を紹介した。
thom.hateblo.jp

ただ、実は作りたかったのはAccessで、連票フォームのフッター領域にそれを配置したかったのだ。
f:id:t-hom:20171216122925p:plain

慣れないAccessと格闘する前にまずExcelフォームでプロトタイプを作ろうと思い立ったのが前回の記事。

Accessフォームで発生した問題

さて、Accessに移植するにあたり、ひとつハマったことがある。
すごく単純なことだけど、コントロールイベントをクラスで検知しようとした際に、うまく動かなかったのだ。
原因はボタンのクリック時イベントに、[イベントプロシージャ]を選択していなかったこと。

今回はこれについて順を追って説明する。

まず以下のようなフォームを作成した。
f:id:t-hom:20171216123618p:plain

それからVBEでクラスモジュールを挿入し、オブジェクト名をButtonWrapperとした。
コードは以下のとおり。

Public WithEvents btn As CommandButton

Private Sub btn_Click()
    MsgBox btn.Caption & "がクリックされました。"
End Sub


フォーム本体のコードは次の通り。

Private Buttons As Collection
Private Sub Form_Load()
    Set Buttons = New Collection
    For i = 0 To 4
        Dim b As ButtonWrapper
        Set b = New ButtonWrapper
        Set b.btn = Me.Controls("コマンド" & i)
        Buttons.Add b
    Next
End Sub

こうすると、コマンド0からコマンド4までの5つのボタンがButtonWrapperオブジェクトに包まれてCollection(Buttons)に格納される。
ButtonWrapper内ではボタン用の変数btnがWithEventsで宣言されているので、ボタンごとにイベントを書かなくてもこれでイベントを検知できる。

これだけで動くと思っていた。
実際、Excel VBAならこれだけで動く。

ところがフォームを起動してボタンを押してもうんともすんとも言わない。

解決策

調べた結果、ボタンのプロパティでクリック時イベントが空だと、いくらイベント検知のコードがあっても動かないらしい。
f:id:t-hom:20171216124457p:plain

ということで全ボタンを選択し、クリック時プロパティのプルダウンからイベントプロシージャを選択。
f:id:t-hom:20171216124749p:plain

これで、無事にイベントを検知できるようになった。

仕様の推察

Excelの場合はボタンをクリックした際のイベントはVBAで書くと決まっているのでイベントを検知したら即、対応するコードが実行される。
Accessでは事情が異なり、クリックイベント=VBAとは限らない。

試しにボタンを配置した後、デザインモードでクリックしてイベントのビルドを選択してみる。
f:id:t-hom:20171216130354p:plain

すると、ビルダーの選択画面が出てきて、コード(VBA)以外にも、式・マクロといったAccess機能でイベントを作成できることがわかる。
f:id:t-hom:20171216130738p:plain

クリック時プロパティに明示的に[イベントプロシージャ]を設定しておくと、Accessにコードを実行するということが伝わり、対応するコードが実行される。

普通にイベントをビルドした場合は問題ないが、今回のようにコントロールイベントを共通化する際は明示的にイベントのビルドを行わないので、この設定が必要である。

VBA クラスモジュールを使ってフォームに動的なメニューを作る

今回作成するのはボタンを動的に切り替えられるメニューである。

これだけでは意味が分からないと思うので動作サンプルを紹介する。
f:id:t-hom:20171209061338g:plain

通常は1つのボタンに1つの処理なので、5つボタンがあれば5つしか処理は書けないが、このメニューは▲と▼で動的にボタンを切り替えることができる。

作り方の紹介

必要なものは、

  • クラスモジュール「SelectButton」
  • クラスモジュール「PagedButtons」
  • フォームモジュール「(任意のオブジェクト名)」

SelectButtonの作り方

クラスモジュールを挿入し、オブジェクト名を「SelectButton」に変更する。
それから以下のコードを張り付け。

Option Explicit
Public WithEvents btn As MSForms.CommandButton
Public Parent As PagedButtons
Private Sub btn_Click()
    Parent.callBack btn.Caption
End Sub

Public Property Let Enabled(e As Boolean)
    btn.Enabled = e
End Property

Public Property Let Caption(x As String)
    btn.Caption = x
End Property

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

Public Sub ReleaseObject()
    Set btn = Nothing
    Set Parent = Nothing
End Sub

PagedButtonsの作り方

クラスモジュールを挿入し、オブジェクト名を「PagedButtons」に変更する。
※複数形のsを見落とさずに。
それから以下のコードを張り付け。

Option Explicit
Private WithEvents previousButton As MSForms.CommandButton
Private WithEvents nextButton As MSForms.CommandButton
Private pageNumber As Long
Private selectButtons As Collection
Private menuItems As Collection
Public Event Selected(x As String)

Sub callBack(x As String)
    RaiseEvent Selected(x)
End Sub

Sub Init(previous_button As MSForms.CommandButton, _
    next_button As MSForms.CommandButton, _
    ParamArray select_buttons())
    
    Set previousButton = previous_button
    Set nextButton = next_button
    
    Set selectButtons = New Collection
    Dim b
    For Each b In select_buttons
        With New SelectButton
            Set .Parent = Me
            Set .btn = b
            selectButtons.Add .Self
        End With
    Next
    
    Set menuItems = New Collection
    pageNumber = 1
End Sub

Sub addMenuItem(menu_caption As String)
    menuItems.Add menu_caption
End Sub

Sub DrawCaptions()
    previousButton.Enabled = pageNumber <> 1
    nextButton.Enabled = pageNumber < maxPage

    Dim itemCursor: itemCursor = selectButtons.Count * pageNumber - selectButtons.Count
    Dim i As Long
    For i = 1 To selectButtons.Count
        If itemCursor + i <= menuItems.Count Then
            selectButtons(i).Enabled = True
            selectButtons(i).Caption = menuItems(itemCursor + i)
        Else
            selectButtons(i).Enabled = False
            selectButtons(i).Caption = "-"
        End If
    Next
End Sub

Private Property Get maxPage() As Long
    maxPage = roundUp(menuItems.Count / selectButtons.Count)
End Property

Private Function roundUp(x As Double) As Long
    roundUp = Int(x + 0.999)
End Function

Private Sub nextButton_Click()
    pageNumber = pageNumber + 1
    DrawCaptions
End Sub

Private Sub nextButton_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
    Call nextButton_Click
    If pageNumber >= maxPage - 1 Then
        Cancel = True
    End If
End Sub

Private Sub previousButton_Click()
    pageNumber = pageNumber - 1
    DrawCaptions
End Sub

Private Sub previousButton_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
    Call previousButton_Click
    If pageNumber <= 2 Then
        Cancel = True
    End If
End Sub

Public Sub ReleaseObject()
    Dim b As SelectButton
    For Each b In selectButtons
        b.ReleaseObject
    Next
    Set menuItems = Nothing
    Set selectButtons = Nothing
End Sub

ユーザーフォームの作り方

ユーザーフォームを挿入し、オブジェクト名を以下のように変更する。
f:id:t-hom:20171209062344p:plain

btn1~btn5はCaptionと同じくオブジェクト名もbtn1~btn5にしておく。

そしてフォームのコードに以下を張り付ける。

Private WithEvents menu As PagedButtons

Private Sub menu_Selected(x As String)
    Me.Label1.Caption = x & "が選択されました。"
End Sub

Private Sub UserForm_Initialize()
    Me.Label1 = vbNullString
    Set menu = New PagedButtons
    
    menu.Init Me.btnPrevious, Me.btnNext, _
        Me.btn1, Me.btn2, Me.btn3, Me.btn4, Me.btn5
    
    Dim i As Long
    For i = Asc("A") To Asc("Z")
        menu.addMenuItem "項目" & Chr(i)
    Next
    menu.DrawCaptions
End Sub

Private Sub UserForm_Terminate()
    menu.ReleaseObject
    Unload Me
End Sub

これで完成。

このテクニックのポイント

このテクニックのポイントは、メニューボタンが押された際に発生するイベントがmenu_Selectedに集約される点だ。

Private Sub menu_Selected(x As String)
    Me.Label1.Caption = x & "が選択されました。"
End Sub

それぞれのボタンがバラバラに機能するのではなく、あたかもひとつのPagedButtonsというコントロールパーツであるかのように扱うことができる。

↓つまりこういう形のひとつのコントロールパーツとして扱うことができるということ。
f:id:t-hom:20171209063932p:plain

また、ボタン数の増減がきわめて簡単に行えることもポイントのひとつ。
試しにボタンをひとつ増やしてみた。
f:id:t-hom:20171209064527p:plain

コードの変更箇所はたった1箇所。
ユーザーフォームのUserForm_Initializeメソッドのmenu.Initに引き渡すボタンを一つ増やすだけで済む。
f:id:t-hom:20171209065211p:plain

ボタンを減らした場合も同様に、menu.Initに引き渡すボタンを減らすだけ。

今回は紹介しないが、動的なコントロールの生成と、APIによるフォームのサイズ変更を組み合わせると、フォームサイズの変化に合わせて表示ボタン数が変わる柔軟なメニューを作成することもできる。

仕組みの解説

さて、どういうことなのか説明しよう。
今回はクラスモジュール、コントロールイベントの共通化、自作イベントなどのテクニックを利用している。

まずPagedButtonsオブジェクトの初期状態はこんな感じの構成。
f:id:t-hom:20171209072843p:plain

PagedButtonsオブジェクトにボタンがひとつ渡されると、SelectButtonオブジェクトを生成し、そこにボタンを保持させて自身が持つSelectButtonsCollectionに格納する。
また、このときに自身(PagedButtonsオブジェクト)をSelectButtonオブジェクトに保持させる。
f:id:t-hom:20171209073114p:plain

ここで循環参照が発生してしまうが、イベントのコールバック処理で必要になるので仕方がない。
オブジェクトにReleaseObjectプロシージャを作ってあるのはそのためだ。
※SelectButtonオブジェクトからPagedButtonsオブジェクトへの参照をオレンジ線にしたのは、後の図で青だと見づらくなった為で、特別な意味はない。

PagedButtonsオブジェクトにボタンやメニュー項目を引き渡していくと、最終的なオブジェクトの関係図はこうなる。
f:id:t-hom:20171209073531p:plain

※実際にはボタンはInitプロシージャで一気に引き渡されますが、最初の図は1つにしておかないとややこしかったので説明の都合上、引き渡していくという表現にしています。

ページ切り替えのボタンまで図に含めると複雑すぎるので割愛したが、ページ切り替えを行うとmenuItemsコレクションから項目が取得され、それぞれのSelectButtonオブジェクトに格納される。

ユーザーがボタンをクリックした際のプロシージャ呼び出しをシーケンス図で書くとこんな感じ。
f:id:t-hom:20171209075244p:plain

callbackとSelectedでそれぞれボタンのCaptionが引き渡されるので、ユーザーフォーム側でどのボタンがクリックされたのか検知できる。

利用しているテクニックについての参考記事

thom.hateblo.jp
thom.hateblo.jp
thom.hateblo.jp
thom.hateblo.jp

循環参照についての参考記事

thom.hateblo.jp
thom.hateblo.jp

今後の展望

動的なコントロールの生成を組み合わせると柔軟性が高まる。以下の記事で動的にラベルを生成させているので紹介。
thom.hateblo.jp

たとえば上記の記事ではSet L = Me.Controls.Add("Forms.Label.1")としているが、Set btn = Me.Controls.Add("Forms.CommandButton.1")とすれば、新しいボタンが生成されて変数btnに格納される。

あと今回はPagedButtonsのSelectedイベントでキャプションを返しているが、addMenuItemメソッドをSub addMenuItem(menu_caption As String, data As Variant)に改造して押された項目に対応するdataを返すようにすれば更に柔軟性が高まる。たとえば押したボタンに応じたオブジェクトが返ってくると、そこから色々操作できて面白い。

ただし、今後の展望に書いた案については、きっとこの記事に興味がある皆さんが素晴らしい実装を作ってくれるので私はこれ以上作らない。面倒だし。。

VBA フォームのボタンの反応が遅い理由と対策

今回はVBAでユーザーフォームに配置したボタンの反応が遅い理由とその対策について紹介する。
検証のため、以下のようなカウンターフォームを作った。
f:id:t-hom:20171207210933g:plain

作成方法

フォームに配置した各オブジェクトは以下のように名前を変更した。
f:id:t-hom:20171207210921p:plain

コードは以下のとおり。

Private Sub cmdCountUp_Click()
    Me.lblCounter.Caption = CLng(Me.lblCounter.Caption) + 1
End Sub

Private Sub cmdReset_Click()
    Me.lblCounter.Caption = 0
End Sub

課題

ぽちっぽちっと丁寧に押していく分には問題ないのだが、連続して速くクリックしたときに反応が悪い。
↓9クリックしてるのに5しかカウントされてない。
f:id:t-hom:20171207211316g:plain

この事象、単にVBAのフォームが遅いためと思い込んでいる方もいるかもしれないが、原因は別のところにある。
試しにCountUpボタンにフォーカスが当たっている状態でスペースキーを連打するとちゃんと連打スピードについてくるのだ。つまりキーでボタンを押した場合は問題ないのに、マウスでクリックした場合は遅いということになる。

原因

原因は、コマンドボタンがダブルクリックイベントを拾っているため。
連続で速くクリックすると、次のように判定される。

  1. クリック
  2. ダブルクリック
  3. クリック
  4. ダブルクリック
  5. クリック
  6. ダブルクリック

つまり、クリック間隔が短いと、偶数回目のクリックがダブルクリック扱いになってしまうため、クリックイベントとしては半分しか判定されないのだ。

対策

この対策は簡単で、単にダブルクリックイベントをキャッチアップしてシングルクリックイベントのプロシージャを呼んでやれば良い。

Private Sub cmdCountUp_Click()
    Me.lblCounter.Caption = CLng(Me.lblCounter.Caption) + 1
End Sub

Private Sub cmdCountUp_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
    Call cmdCountUp_Click
End Sub

Private Sub cmdReset_Click()
    Me.lblCounter.Caption = 0
End Sub

この対策を施した結果がこちら。
f:id:t-hom:20171207212052g:plain

ちゃんとクリックした分カウントアップされている。

別の問題

前述の対策は、とても良さそうに思える。
ただし、コマンドボタンの無効化と組み合わせると、ボタンが陥没して戻ってこないという別の問題が多発する。

試しに数値が10に達したらボタンを無効化するようコードを書き換えてみた。

Private Sub cmdCountUp_Click()
    Me.lblCounter.Caption = CLng(Me.lblCounter.Caption) + 1
    If CLng(Me.lblCounter.Caption) >= 10 Then
        Me.cmdCountUp.Enabled = False
    End If
End Sub

Private Sub cmdCountUp_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
    Call cmdCountUp_Click
End Sub

Private Sub cmdReset_Click()
    Me.lblCounter.Caption = 0
    Me.cmdCountUp.Enabled = True
End Sub

すると、このようにボタンが陥没するようになった。
f:id:t-hom:20171207212527g:plain

無効化を解除してもボタンは凹んだままで、クリックすると戻る。
ボタンの陥没は、ダブルクリックイベント時にボタンを無効化すると発生するようで、シングルクリック時は発生しない。

以下は検証の結果。

事象が発生するパターン

  1. クリック
  2. ダブルクリック
  3. クリック
  4. ダブルクリック
  5. クリック
  6. ダブルクリック
  7. クリック
  8. ダブルクリック
  9. クリック
  10. ダブルクリック ←ここで無効化されるので陥没する

事象が発生しないパターン1

  1. クリック
  2. ダブルクリック
  3. クリック
  4. ダブルクリック
  5. クリック
  6. ダブルクリック
  7. クリック
  8. ダブルクリック
  9. クリックし、次がダブルクリックにならないようしばらく時間を置く。
  10. クリック ←ここで無効化されるので陥没しない

事象が発生しないパターン2

  1. クリックし、次がダブルクリックにならないようしばらく時間を置く。
  2. クリック
  3. ダブルクリック
  4. クリック
  5. ダブルクリック
  6. クリック
  7. ダブルクリック
  8. クリック
  9. ダブルクリック
  10. クリック ←ここで無効化されるので陥没しない

ボタン陥没の回避方法(没案)

この事象はフォームのRepaintをしてみたり、DoEventsを挟んでみたりしたけれど改善されなかった。
ということで根本的な解決策は見つかっていない。(あるいは存在しない)
ひょっとするとWinAPIなどで何とかする方法はあるのかもしれないが、少なくともVBA単体では見つからなかった。

今のところ、ボタンの無効化を諦めるか、最後がダブルクリックにならないように回避コードを書くしかなさそうだ。

回避コードとしては以下のように、無効化の一歩手前でダブルクリックイベントからのシングルクリックイベント呼び出しをやめること。

※以下は私が考えた手、古い回避方法です。より良い方法が見つかったので、後述のボタン陥没の回避方法(改)をご覧ください。

Private Sub cmdCountUp_Click()
    Me.lblCounter.Caption = CLng(Me.lblCounter.Caption) + 1
    If CLng(Me.lblCounter.Caption) >= 10 Then
        Me.cmdCountUp.Enabled = False
    End If
End Sub

Private Sub cmdCountUp_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
    If CLng(Me.lblCounter.Caption) < 9 Then
        Call cmdCountUp_Click
    End If
End Sub

Private Sub cmdReset_Click()
    Me.lblCounter.Caption = 0
    Me.cmdCountUp.Enabled = True
End Sub

ただ軽快にカウントアップされて最後だけ1回「スカ」が入るので、イマイチだな。
↓9になるとダブルクリックイベントを無視するので最後だけボタンを2回クリックしている。
f:id:t-hom:20171207214833g:plain

ボタン陥没の回避方法(改)

imihitoさんにTwitterで回避方法を教えていただきました。ありがとうございます!

ということで、早速試してみた。

Private Sub cmdCountUp_Click()
    Me.lblCounter.Caption = CLng(Me.lblCounter.Caption) + 1
    If CLng(Me.lblCounter.Caption) >= 10 Then
        Me.cmdCountUp.Enabled = False
    End If
End Sub

Private Sub cmdCountUp_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
    Call cmdCountUp_Click
    Cancel.Value = (CLng(Me.lblCounter.Caption) >= 9)
End Sub

Private Sub cmdReset_Click()
    Me.lblCounter.Caption = 0
    Me.cmdCountUp.Enabled = True
End Sub

ふむふむ、これで意図したとおりに動作している。

あれ、ちょっとまてよ。
ということは、ダブルクリックイベント自体はキャンセルしても、Call cmdCountUp_Clickで呼び出しは成功している?

であれば、単にCancel = Trueで良さげ。

ボタン陥没の回避方法(最終案)→没案

ということで最終的に完成したコードがこちら。

…と思ったけれど、無条件でキャンセルすると環境によってはボタンクリックした際の描画がおかしいらしい。
Office2016 64ビット版でカウントアップはされるがボタンクリック動作が遅れて見えるとのこと。
ひょっとすると、マシンスピードが速すぎるとクリックイベントでの描画が完了する前にダブルクリックイベントのキャンセルまで実行されてしまって描画が発生しないのかもしれない。

当方の環境で描画がおかしくなることはなかったが、以下の案は没案とし、推奨は「ボタン陥没の回避方法(改)」で紹介した条件付きCancelとしたい。

Private Sub cmdCountUp_Click()
    Me.lblCounter.Caption = CLng(Me.lblCounter.Caption) + 1
    If CLng(Me.lblCounter.Caption) >= 10 Then
        Me.cmdCountUp.Enabled = False
    End If
End Sub

Private Sub cmdCountUp_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
    Call cmdCountUp_Click
    Cancel = True    '←無条件キャンセル。良いと思ったけれど環境依存の可能性が報告されたため没。
End Sub

Private Sub cmdReset_Click()
    Me.lblCounter.Caption = 0
    Me.cmdCountUp.Enabled = True
End Sub

あとがき(2017/12/9に執筆)

さて、今回の記事でボタン単品を連打する必要性に疑問を持たれた方もいると思うけれど、私が実際に作りたかったのはこちら↓
f:id:t-hom:20171209061338g:plain

ページめくり処理でもっさりしてストレスになったので今回記事にした。

今回のテクニック+諸々を使って、実際に作成したのでクラスモジュール上級者は、こちらも併せてどうぞ。
thom.hateblo.jp

VBA クラスモジュールでExcelのテーブル(ListObject)を操作する

今回はVBAでちょっと扱いにくいExcelのテーブル(ListObject)をクラスモジュールで包んで簡単に操作できるようにするコードを紹介。

作り方

まずVBEで対象のブックを選択し、挿入メニューからクラスモジュールを挿入する。
f:id:t-hom:20171204203659p:plain

挿入されたクラスモジュール(Class1)を選択し、プロパティウインドウ(表示されてなければF4キーで表示)から、モジュールのオブジェクト名を「TableOperator」に変更する。
f:id:t-hom:20171204203825p:plain

クラスモジュールTableOperatorをダブルクリックで開き、コードウインドウに以下のコードを張り付ける。

Option Explicit
Public ListObject As ListObject
Public OverwriteExtraBlankRecord As Boolean
Public ReadExtraBlankRecord As Boolean
Private rowCursor As Long

Sub MoveFirst()
    rowCursor = 0
End Sub

Sub SetTable(start_cell As Range)
    Set Me.ListObject = start_cell.ListObject
    Me.MoveFirst
End Sub

Function GetNext() As Range
    rowCursor = rowCursor + 1
    Set GetNext = Me.ListObject.ListRows(rowCursor).Range
End Function

Function HasNext() As Boolean
    If ReadExtraBlankRecord Then
        HasNext = rowCursor < Me.ListObject.ListRows.Count
    Else
        HasNext = rowCursor < GetLastUsedRowIndex
    End If
End Function

Private Function IsBlank(target_range As Range) As Boolean
    Dim arr: arr = target_range.Value
    Dim r
    For Each r In arr
        'If r = Empty Then ←12/5 1:01修正。
        'TwitterでimihitoさんにEmptyとの比較だと0もTrueになる旨の指摘と改善コード教えてもらいました。
        If IsEmpty(r) Then
            IsBlank = True
            Exit For
        End If
    Next
End Function

Private Function GetLastUsedRowIndex() As Long
    Dim ret As Long
    ret = Me.ListObject.ListRows.Count
    Do While IsBlank(Me.ListObject.ListRows(ret).Range)
        ret = ret - 1
    Loop
    GetLastUsedRowIndex = ret
End Function

Sub AddItem(ParamArray data())
    If UBound(data) + 1 = Me.ListObject.ListColumns.Count Then
        Dim targetRange As Range
        If OverwriteExtraBlankRecord Then
            Dim cursor As Long
            cursor = GetLastUsedRowIndex + 1
            If cursor < Me.ListObject.ListRows.Count Then
                Set targetRange = Me.ListObject.ListRows(cursor).Range
            Else
                Set targetRange = Me.ListObject.ListRows.Add.Range
            End If
        Else
            Set targetRange = Me.ListObject.ListRows.Add.Range
        End If
        
        Dim i As Long
        For i = LBound(data) To UBound(data)
            targetRange.Item(i + 1).Value = data(i)
        Next
    End If
End Sub

使い方

今のところ機能はデータの追加と、全データの列挙だけ。
初心者用に説明を書こうと思ったけれど、本文での説明が面倒だったのでサンプルコードはいつもよりコメント多め。

データの追加

今回は以下の食材テーブルに新しく、魚を追加するサンプルを作った。
f:id:t-hom:20171204204854p:plain

↓コピペ用の表はこちら

種目 品名 価格
果物 いちご 150
果物 ばなな 100
果物 りんご 200
野菜 キャベツ 150
野菜 なすび 100
野菜 レタス 120
牛肉 300
豚肉 200
鶏肉 100

Excelに貼り付けてテーブル化してください。

※標準モジュールに書くコードは以下を参照してください。

Sub データ追加()
    'まず任意の変数を、TableOperator型で宣言します。
    Dim foods As TableOperator
    
    '次にその変数に新しいTableOperatorを代入します。
    Set foods = New TableOperator
    
    'そして、SetTableメソッドにテーブルの開始セルを指定すると、準備完了です。
    foods.SetTable Range("A1")
    
    '↓これは記事中で解説します。
    foods.OverwriteExtraBlankRecord = True
    
    'あとは、データを指定して追加するだけ。
    foods.AddItem "魚", "いわし", 100
    foods.AddItem "魚", "あじ", 150
    foods.AddItem "魚", "さば", 200
End Sub
OverwriteExtraBlankRecordプロパティについて

※前置きが長くなるけれども、しばしお付き合いください。
Excelのテーブル機能はふつう、先に示したようにデータをぎりぎりまで入力する想定で作られている。
ただ悲しいかな一般的にテーブルの機能は正しく認知されておらず、ただのデザイン機能だと思って使ってる方も多い。

それで、以下のようにデータの追加に備えてテーブルに余裕を持たせておくなんてことが行われるのだが、、、
f:id:t-hom:20171204205217p:plain

これがVBAでテーブルを扱う際の悲劇の始まり。

Excelはテーブル範囲=データ範囲だと認識するため、上の画像では2~18行目まで17個のデータが入っていると認識する。
そこにデータをAddすると、当然こうなる↓
f:id:t-hom:20171204205608p:plain

本来は、「テーブル機能をきちんと理解する」という根本解決がベストだけれど、共有で使っているファイルなんかだとなかなか全員に周知徹底も難しい。

そこで!
長くなったけれど、OverwriteExtraBlankRecordの出番。
というかもう答え出たね。

そう、これは私が用意した、余分な空行を上書きするという意味のプロパティ。
デフォルトだとFalseだけれど、Trueにすることでこのとおり↓
f:id:t-hom:20171204210150p:plain

ちゃんと狙った位置に入る。

全データ出力

全データを出力するサンプルはこちら。
※Debug.Printを使用しているので、イミディエイトウインドウが表示されていなければ「Ctrl+G」で表示させておいてください。

Sub 全データ出力()
    Dim foods As TableOperator
    Set foods = New TableOperator
    foods.SetTable Range("A1")
    'ここまでの準備は一緒
    
    '最初の行へ移動
    foods.MoveFirst
    
    'ReadExtraBlankRecordをTrueにすると余分な空行も含めて出力。デフォルトはFalse
    '↓サンプルを示す目的なので一旦コメントアウトしている。
    'foods.ReadExtraBlankRecord = True
    Debug.Print "---start---"
    
    'HasNextがTrueの間、GetNextを繰り返す。
    Do While foods.HasNext
    
        'GetNextは1レコード分のRangeを返し、内部で保持しているカーソルを次の行へ移す。
        Dim food As Range
        Set food = foods.GetNext
        
        '返ってきたRangeに引数をひとつ与えると、その列のデータが取れるので、
        'Tabでくっつけて表示
        Debug.Print food(1) & vbTab & food(2) & vbTab & food(3)
        
    Loop
    Debug.Print "---end---"
End Sub

初心者向け(?)の解説は以上。
ここから下は余談。

改善が望まれる個所

  • HasNextが呼ばれるたびにGetLastUsedRowIndexが実行されるので効率が悪い。
  • Whereなどのメソッドでデータの絞り込みがしたい。

これは条件が1つなら実現したのだけど、複数条件を付けたくなったところで面倒臭くなって詰んだ。

※私はこれ以上つくりません。一応、コードの残骸を掲載しておくので好きにしてください。
 あくまで残骸なので、このままではただのDebug.Printです。

クラスモジュールTableOperatorに追加するコード

Private Function compare(value As Variant, condition As String) As Boolean
    Dim ret As Boolean
    Dim op As String: op = Split(condition)(0)
    Dim cond As Variant: cond = Mid(condition, InStr(1, condition, " ") + 1)
    Select Case TypeName(value)
        Case "String": cond = CStr(cond)
        Case "Date": cond = CDate(cond)
        Case "Long": cond = CLng(cond)
        Case "Integer": cond = CInt(cond)
        Case "Double": cond = CDbl(cond)
        Case "Currency": cond = CCur(cond)
        Case "Single": cond = CSng(cond)
        Case "Boolean": cond = CBool(cond)
        Case "Byte": cond = CByte(cond)
    End Select
    
    Select Case op
        Case "=": ret = value = cond
        Case ">": ret = value > cond
        Case "<": ret = value < cond
        Case ">=": ret = value >= cond
        Case "<=": ret = value <= cond
        Case "<>": ret = value <> cond
        Case "Like": ret = value Like cond
        Case Else: ret = False
    End Select
    compare = ret
End Function
Function Whare(column_name As String, condition As String) As Range
    Dim columnindex, i, j
    columnindex = Me.ListObject.ListColumns(column_name).Index
    For i = 1 To Me.ListObject.ListRows.Count
        If compare(Me.ListObject.ListRows(i).Range(columnindex).value, condition) Then
            For j = 1 To Me.ListObject.ListColumns.Count
                Debug.Print Me.ListObject.ListRows(i).Range(j).value,
            Next
            Debug.Print
        End If
    Next
End Function

用意するテーブル
f:id:t-hom:20171204220505p:plain

サンプルコード

Sub フィルターテスト()
    Dim foods As TableOperator
    Set foods = New TableOperator
    foods.SetTable Range("A1")
    Debug.Print "----"
    foods.Whare "価格", "<= 100"
    Debug.Print "----"
    foods.Whare "種目", "= 魚"
    Debug.Print "----"
    foods.Whare "種目", "= 果物"
    Debug.Print "----"
    foods.Whare "品名", "Like *ご"
End Sub

以上

VBA クラスモジュールで今月末・来月初などの日付計算を楽に扱う

今日はVBAで日付を扱う案件があった。
とりいそぎ、日付関連の関数で済ませたけれど、オブジェクトがあると楽だなと思ったのでクラスモジュールを作ってみた。

作り方

まずはクラスモジュールを挿入し、プロパティウインドウから名前をDateObjectに変えておく。
f:id:t-hom:20171130221453p:plain

DateObjectモジュール内に以下のコードを張り付ける。

Public Value As Date
Private Sub Class_Initialize()
    Value = Date
End Sub

Public Function SetDateByYYYYMMDD(yyyymmdd As String)
    Value = DateSerial(Left(yyyymmdd, 4), Mid(yyyymmdd, 5, 2), Right(yyyymmdd, 2))
End Function

Public Property Get ToString(string_format As String) As String
    ToString = format(Value, string_format)
End Property

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

Public Property Get FirstOfMonth() As DateObject
    With New DateObject
        .Value = DateValue(Me.ToString("yyyy/mm/01"))
        Set FirstOfMonth = .Self
    End With
End Property

Public Property Get MoveMonth(n As Long) As DateObject
    With New DateObject
        .Value = DateAdd("m", n, Me.Value)
        Set MoveMonth = .Self
    End With
End Property

Public Property Get LastMonth() As DateObject
    Set LastMonth = Me.MoveMonth(-1)
End Property

Public Property Get NextMonth() As DateObject
    Set NextMonth = Me.MoveMonth(1)
End Property

Public Property Get EndOfMonth() As DateObject
    With New DateObject
        .Value = Me.FirstOfMonth.NextMonth.Value - 1
        Set EndOfMonth = .Self
    End With
End Property

以上で準備は完了。

使い方

※適当にサンプルを用意してみたので、標準モジュールに貼り付けて実行してみてください。

Sub hoge()
    Dim d As DateObject: Set d = New DateObject
    
    'オブジェクト作成直後は、当日の日付が入る。
    Debug.Print d.Value
    
    '日付をyyyymmdd形式で設定できる。
    d.SetDateByYYYYMMDD "20171130"
    
    '普通に日付の代入もできる。
    d.Value = Date 'Date関数で今日の日付を作って代入
    
    'ToStringでフォーマット整形できる。
    Debug.Print d.ToString("yyyymmdd")
    
    '月初の日付を取得する
    Debug.Print d.FirstOfMonth.Value
    
    '月末の日付をフォーマット指定で出力
    Debug.Print d.EndOfMonth.ToString("ge年mm月dd日")
    
    '再来月末の日付をフォーマット指定で出力
    Debug.Print d.NextMonth.NextMonth.EndOfMonth.ToString("yyyy-mm-dd")
End Sub

結果は次のように出力される。

2017/11/30 
20171130
2017/11/01 
H29年11月30日
2018-01-31

このマクロを改造するのに必要な知識

例えばNextWeek命令が欲しいなど、改造したくなることがあるかもしれない。
その場合、まずはクラスモジュールの知識が必要になる。

以下に入門記事を書いているのでどうぞ。
thom.hateblo.jp

また、以下の記事のテクニックも使用しているのでご参考までに。
thom.hateblo.jp
thom.hateblo.jp

以上

VBAでランダムかつセキュリティポリシーを満たす8桁のパスワードを生成する

今回はパスワードを生成するコードを紹介する。
過去すでに書いた気がしてたのだけど、↓と勘違いしてた。
thom.hateblo.jp

簡易版のプログラム

さて、ただランダムなパスワードを作るだけなら簡単だ。
さっそくコードを書いてみた。

Sub ランダムパスワード簡易版()
    Dim i As Long
    For i = 1 To 8
        Debug.Print Chr(WorksheetFunction.RandBetween(33, 126));
    Next
    Debug.Print
End Sub

解説

コンピューター上で文字はコードで管理されており、特にアルファベット等の半角文字はASCIIコードと呼ばれるコードに含まれている。
ASCIIコードは0~127まであるが、デリートやタブなどの制御文字なども含まれるので、パスワードに使えそうな文字は33~126だ。
ASCII文字コード - IT用語辞典

そこで、ワークシート関数のRandBetweenで33~126までの数値をランダムに生成する。
あとはVBAのChr関数で文字コードを文字に変換して1文字分できあがり、これを8回繰り返すと8桁のパスワードになる。

ちなみにDebug.Printはセミコロンを最後に置くと改行せずに続けて出力することができる。
コードの最後にDebug.Printだけ入力しているのは、ここで改行だけ出力したい為だ。

参考
thom.hateblo.jp

出力されるパスワードにはアルファベットや数値以外にも特殊記号が含まれてしまうが、最近は特殊記号も含めなさいというポリシーが多いのでちょうどいい。

さて、このコードは簡易版なので、使えそうなパスワードが出るかどうかは運任せになる。
試しに8回実行してみた結果がこちら。
f:id:t-hom:20171129203251p:plain

最後の2つは使えそうだけれど、他のは特殊記号が多すぎたり、数値を含まなかったりとあまり宜しくない。
完全にランダムなので、セキュリティポリシーを満たさない場合もある。

確実にポリシーを満たすパスワードを生成する

最近はパスワードポリシーをアルファベット大文字・小文字・数字・特殊記号のすべてを含む8桁以上としているところが多いと思う。
そして私が利用するシステムによってはなぜか最初と最後の文字はアルファベットでなければならないという仕様がある。

また、文字種の少ない数字が多すぎると脆弱なイメージがあるし、特殊記号が多すぎると面倒くさい。数字と特殊記号は1つずつにしたい。
更に、他人に伝える初期パスワードの発行用途ではゼロとオーの違いなど、紛らわしい文字を回避したいという要件も出てくるだろう。

今回はこれらの複雑な要望を満たすパスワードを一発で生成できるプログラムを作る。

さて、今回は乱数を何度も利用するので、WorksheetFunctionではなく、VBAでRandBetween関数を作っておこう。

Function RandBetween(lower_bound, upper_bound)
    Call Randomize
    RandBetween = Int((upper_bound - lower_bound + 1) * Rnd + lower_bound)
End Function

次に本体を書き始める。
まずは文字種の定義を作りたいので、以下のような定数を準備する。

Function ランダムパスワード生成()
    Const UPPER_CASE = ""
    Const LOWER_CASE = ""
    Const NUMBERS = ""
    Const SYMBOLS = ""
End Function

定数の中身は手打ちだと面倒なので以下のコードで全文字種を出力して、切り取り&貼り付けする。

Sub hoge()
    For i = 33 To 126
        Debug.Print Chr(i);
    Next
    Debug.Print
End Sub

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

一旦文字種の定義が完成。ダブルクォーテーションはそのままだとエラーになるのでエスケープしても良いが、今回は単に削った。

Function ランダムパスワード生成()
    Const UPPER_CASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    Const LOWER_CASE = "abcdefghijklmnopqrstuvwxyz"
    Const NUMBERS = "0123456789"
    Const SYMBOLS = "!#$%&'()*+,-./:;<=>?@[\]^_`{|}~"
End Function

ここから紛らわしい文字を削っていく。

  • 見た目が紛らわしい文字「0Oij1lI|」を削除
  • 発音が間際らしい文字「bdBDQ9qGJgj」を削除
  • 視認性が悪い文字「',.:;`」を削除

その他、パスワードを電話や口頭で伝える必要がある場合は伝わりにくい文字を適宜削除すると良い。
また、発行パスワードが自分専用なら全て残しても問題ない。

できたのがこちら。

Function ランダムパスワード生成()
    Const UPPER_CASE = "ACEFHKLMNPRSTUVWXYZ"
    Const LOWER_CASE = "acefhkmnoprstuvwxyz"
    Const NUMBERS = "2345678"
    Const SYMBOLS = "!#$%&()*+-/<=>?@[\]^_{}~"
End Function

次にランダムに文字を選択する関数が欲しいので作成。

Function RandomCharPicker(source)
    Dim location: location = RandBetween(1, Len(source))
    RandomCharPicker = Mid(source, location, 1)
End Function

このRandomCharPickerに文字列を渡すとその中から1文字をランダムに返す。

この関数を使って、とりあえず全文字種を1つずつ出力してみた。

Function ランダムパスワード生成()
    Const UPPER_CASE = "ACEFHKLMNPRSTUVWXYZ"
    Const LOWER_CASE = "acefhkmnoprstuvwxyz"
    Const NUMBERS = "2345678"
    Const SYMBOLS = "!#$%&()*+-/<=>?@[\]^_{}~"
    Dim password
    password = password & RandomCharPicker(UPPER_CASE)
    password = password & RandomCharPicker(LOWER_CASE)
    password = password & RandomCharPicker(NUMBERS)
    password = password & RandomCharPicker(SYMBOLS)
    Debug.Print password
End Function

ただこれだと1桁目が大文字、2桁目が小文字という風に規則性ができてしまうので強度が落ちてしまう。
そこで、文字列をシャッフルする関数を作成した。

Function ShuffleString(source)
    Dim c As Collection: Set c = New Collection
    Dim i As Long
    
    'まず1文字ずつコレクションに格納していく
    For i = 1 To Len(source)
        c.Add Mid(source, i, 1)
    Next
    
    'コレクションが空になるまで、ランダムに取り出す。
    Dim ret As String
    Dim location As Long
    Do While c.Count > 0
        location = RandBetween(1, c.Count)
        ret = ret & c(location)
        c.Remove location
    Loop
    ShuffleString = ret
End Function

この関数に文字列を渡すと、順番をバラバラにして返してくれる。

これを使って、メインコードを完成させる。

Function ランダムパスワード生成()
    Const UPPER_CASE = "ACEFHKLMNPRSTUVWXYZ"
    Const LOWER_CASE = "acefhkmnoprstuvwxyz"
    Const NUMBERS = "2345678"
    Const SYMBOLS = "!#$%&()*+-/<=>?@[\]^_{}~"
    Dim password
    
    '最初に全文字種を含める。
    password = password & RandomCharPicker(UPPER_CASE)
    password = password & RandomCharPicker(LOWER_CASE)
    password = password & RandomCharPicker(NUMBERS)
    password = password & RandomCharPicker(SYMBOLS)
    
    '次の2文字をアルファベットのいずれかとし、
    password = password & RandomCharPicker(UPPER_CASE & LOWER_CASE)
    password = password & RandomCharPicker(UPPER_CASE & LOWER_CASE)
    
    '6文字をシャッフル
    password = ShuffleString(password)
    
    '先頭と末尾にアルファベットを付加
    password = RandomCharPicker(UPPER_CASE & LOWER_CASE) & _
                        password & _
                        RandomCharPicker(UPPER_CASE & LOWER_CASE)
    
    'ポリシーを満たしたパスワードの完成
    Debug.Print password
End Function

これで、先頭・末尾がアルファベットかつ、必要な全文字種を含む8桁のパスワード生成プログラムの完成

以下、8回実行した結果。

ct8+PUuX
F~HfH3Pe
mK~Vmp4m
sa^s3MXo
T%EEw2Rw
UV(8LuEZ
AP!aH7LW
oH/F3HzE

出力されるパスワードはすべてポリシーを満たしているので、あとは好きなものを選べば良い。

あとがき

今回はパスワードの生成方法を題材にしたけれど、本当に紹介したかったのは自分で新しい関数を発想するためのヒントだ。たとえば今回紹介したRandomCharPickerやShuffleStringであるが、どちらもその原点は「こんな関数があったらいいのに」という思いである。

最近、まだVBAをよく知らない初心者の方が、実は柔軟に発想できるということもあるんじゃないかと考えるようになった。

初心者はVBAにどんな関数があるのかを知らない。逆にいえば、「何ができないのか」を知らない。だから、自分がやりたいことをずばり解決してくれる関数があるんじゃないかと検索し、見つからずに悩む。

少し上達してくると、既存の関数(たとえばMidやRndなど)を使いこなせるようになる。逆に、「どんな関数が無いか」という現実も知ることになる。プログラミングは用意された関数の制約の中で、それらを組み合わせてやりたいことを実現していく作業である。しかし中級者になると、この制約が自分の発想の幅を狭めてしまっているのではないだろうか。

そこからもう一段上達するためには、初心の頃の自由な発想を取り戻す必要がある。やりたいことをずばり解決する関数はない。でも作ることはできる。さて、どんな関数が欲しい?

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