t-hom’s diary

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

Excelのセキュリティ設定と怪しいファイルの対策

Excelには、悪意のあるマクロによってコンピューターが被害を受けることがないようにいくつかの防御機構が備わっている。
しかしExcelマクロの解説書・解説サイト等では利便性の観点からか、設定を無効化しましょうという方向での解説が多く、セキュリティリスクについては「注意しましょう」という漠然とした勧告か「自己責任で」という免責のみで具体的に何をどう気を付ければ良いのかが書かれていないことが多い。

今回は、これらの防御機構の紹介と、正しい活用方法、外した場合の具体的なリスクについて説明する。
Excel 2013を基に紹介するが、これらの機能はExcel 2010にも存在する。(2007にも恐らくあるけれど、持っていないので不明)

目次

(1) 保護ビュー

説明

Excelの初期設定では、インターネット等のネットワーク経由で入手したファイルは次のように、保護ビューで開くような設定になっている。
f:id:t-hom:20180503230028p:plain

保護ビューはファイルを安全に閲覧することに特化したビューアで、一切の編集ができない。煩わしいことに、社内ネットワーク経由で入手した正当なファイルまでインターネット経由と判定されてしまうが、セキュリティに気を配るのであればたとえ社内ネットワーク経由で入手したとしても原則編集の必要が無いファイルは保護ビューのまま閲覧するのが安全である。

設定箇所

ファイル→オプション→セキュリティ センターからセキュリティ センターの設定ボタンを押し、左のメニューから保護ビューを開く。
f:id:t-hom:20180503230355p:plain

煩わしいので設定ごと無効化を推奨する記事も多いが、ファイルに悪意のあるコードが埋め込まれていた場合はコンピューターに被害を与えることも考えられるので、基本的には編集が必要かつ安全が確認されたものだけ個別に保護ビューを外すという使い方がオススメ。

保護ビューを外したファイルを再度保護ビューに戻す方法
基本的に一度安全を確認したファイルのはずなので、クリアする必要性はあまり無いが一応紹介。

セキュリティ センターの設定から信頼済みドキュメントをクリアすることで、保護ビューに戻すことができる。
f:id:t-hom:20180503233031p:plain

(2) 標準ブック形式へのマクロ保存不可

説明

Excel 2003まではマクロの有無にかかわらず、Excelブックの拡張子は「xls」だったが、Excel 2007以降はマクロが保存できない「xlsx」と、マクロが保存できる「xlsm」に分かれた。従来の「xls」や新しいバイナリ形式「xlsb」もサポートしているが、基本的にはマクロを利用しないファイルは「xlsx」とし、マクロを利用するファイルは「xlsm」とすることが望ましい。

熟練者がその気になれば悪意のあるマクロを作ることもできるため、マクロを使わないファイルではそもそもマクロを使えない「xlsx」形式にしてしまうことで、無用なリスクを減らすことができる。

ちなみに保存した後でファイル名の拡張子だけ変えてもダメで、以下のようなエラーになる。
f:id:t-hom:20180503232051p:plain

Excelはすべてお見通し。きちんと名前を付けて保存のダイアログから正しい形式を選択するべし。

(3) セキュリティの警告

説明

標準の設定では、マクロ付きのファイルを開く際に以下のような警告が表示され、マクロが一旦無効にされる。
f:id:t-hom:20180503232756p:plain

単にコンテンツを有効化してくださいと書かれている記事が多いが、なんでもかんでも有効化してはダメで、自分で作成した安全なマクロと信頼できる入手元から取得したファイルに限定してコンテンツを有効化すると良い。インターネットから入手したファイルは基本的に信頼性が低いものと考え、安易にコンテンツの有効化を行わないこと。

保護ビューとの違いはマクロを実行する以外の操作は自由に行えるという点である。
ファイルの編集もできるし保存もできる。マクロの編集もできる。ただ実行ができないというだけ。

入手元が完全に信頼できない場合、マクロが無効化の状態のままコードを読み、安全性を確認してから有効化すると良い。
コード確認の際のポイントをいくつか挙げてみた。

  • 完全には理解できなくとも、ある程度処理の流れが読めるコードであること。
  • わざと難読化されているコードや、プロジェクトが保護されていて閲覧できない場合はコンテンツを有効化しない。
  • 参照設定とCreateObjectを確認し、マクロが謳っている目的とかけ離れたオブジェクトが参照されていないことを確認する
  • ファイルシステムオブジェクトやファイル操作関連の関数が利用されている場合は特に内容を理解し、意図しないファイル操作が行われないことを確認する
  • 標準モジュールにAuto_Openプロシージャがあれば、特に注意深く処理内容を確認する。
  • ThisWorkbookモジュールにWorkbook_Openプロシージャがあれば、特に注意深く処理内容を確認する。

対象のコードを読むスキルが足りない場合、熟練者にお願いするか、マクロの利用を諦めたほうが無難である。

コンテンツを有効化したファイルを再度無効化に戻す方法
保護ビューを外した場合のクリア方法と同じく、セキュリティ センターの設定から信頼済みドキュメントをクリアすることで、コンテンツ無効化に戻すことができる。

設定箇所

セキュリティ センターのマクロの設定から「警告を表示してすべてのマクロを無効にする」を選択すると上記の設定になる。これが一番お勧め。業務で一切マクロを利用しない場合は一番上の「警告を表示せずにすべてのマクロを無効にする」でも良い。
f:id:t-hom:20180503235302p:plain

(4) VBAによるマクロの書き換え防止

説明

VBAではExcelブックを開いてそのブックのマクロの内容をマクロで書き換えるといった処理も可能であるが、これは標準では無効化されている。なぜ無効化されているかというと、マクロによるマクロの書き換えができるなら、悪意のあるマクロによって、正当なマクロファイルに悪意のあるコードを埋め込むことができてしまうから。
これはマクロウイルスの感染の基本的な仕組みである。

設定箇所

前項と同じ画面で「VBA プロジェクト オブジェクト モデルへのアクセスを信頼する」のチェックが外れている状態が安全。
f:id:t-hom:20180503235302p:plain

チェックを付けると、前述のような高度な処理が可能になりプログラミングが捗る場合がある。このブログでもこの設定を外さないと実行できない便利なマクロを紹介している。しかし、設定を外すとマクロウイルス感染の危険性は高まるので、これまで紹介してきた保護機能を活用してより注意深く実行するマクロを選択・確認する必要がある。また、面倒でも入手元が不明なファイルを開く前にはこの設定を無効に戻してから開く方が安全である。

まとめ

Excelにおけるセキュリティの基本は、不必要に設定を緩めないことである。必要が生じたら、その都度、必要な分だけ個別に対応すること。そしてファイルの安全性を注意深く確認すること。そうすれば無用なリスクを避けることができる。

プログラミング言語はコンピューター語ではなく人間のための言語

プログラミング言語とは何か。
初心者に向けて、よく次のような説明がなされる。

コンピューターは日本語や英語で指示しても理解できない。だからコンピューターが理解できる言葉、つまりプログラミング言語でコードを書く必要がある。

プログラミングを学び始める段階ならこの説明で何の支障もない。

と、以前までは思ってたんだけど最近は、初めからきちんと伝えておいたほうが良いんではないかという気がしてきたのでこの記事を書いた。

プログラミング言語は本当はコンピューター語ではなく、人間のための言語である。だから動けば何でも良いわけではなく、基本的には人が読むことを想定してコードを書くべきである。

プログラミングから実行までの流れ

人間がアイデアをプログラムコードに落とし込むと、コンパイラまたはインタープリタと呼ばれるソフトウェアがそれを機械語に翻訳し、最終的にコンピューターによって実行される。
f:id:t-hom:20180430104346p:plain
※ただし、間に中間言語が挟まったり、仮想マシン上の実行だったりと言語によってこの流れは必ずしも同じではない。

人間が機械語を効率よく読み書きできるなら、コンパイラインタープリタは要らない。
機械語とは、以下のようなコードになる。

0100101011110101011101010100001...
※イメージです。内容はデタラメ。

コンピューターは0と1しか処理できないのだ。

これを読んで理解しろというのは無理がある。
だから、コンピューターにもっと簡単に指示を出すために、人間が読めるプログラミング言語が設計され、翻訳者となるコンパイラインタープリタが作られた。

プログラミング言語が作られたワケ

人間が話す言葉(自然言語)は時と場合で意味が変わり、また表現も無数に存在するためにプログラミングに適さない。もしコンピュータが解釈を間違えたために飛行機が墜落したり、大事な取引の金額を間違えたりしたら洒落にならない。だから厳密に意味を定義され、それ以外に解釈できないような言語が必要だった。そして作られたのがプログラミング言語である。

このように形式的に意味が定義されている言語を形式言語という。
つまりプログラミング言語は人間のために設計された形式言語である。

主張

人間の人間による人間のためのコードを書くべし。
プログラミング言語は、そのために作られたのだから。

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

VBScript ドラッグ&ドロップでファイルをAccessDBに登録する仕組み

PCのファイルは通常「フォルダ分け」によって整理される。
ところがフォルダ―ツリーを使った整理は実際のところそんなにうまく機能しない。
なぜなら、コウモリ問題が存在するから。

コウモリ問題とは、物事を単一の基準で分類していくと両方の基準に合致した場合に詰む事象を言う。
イソップ寓話の「卑怯なコウモリ」が由来だとのこと。

例えばプロジェクトAフォルダと、手順書フォルダが別の場所にあり、プロジェクトAに関する手順書をどちらに入れるべきかといったものが代表的。片方にだけ入れると反対側を探して時間を浪費するリスクがあり、両方に入れると二重管理で内容が枝分かれするリスクがある。

この問題に対する解決策の一つとして、タグ付けによる管理がある。タグは一つのファイルに複数付けることができ、関連するファイル群を瞬時にリストアップできる。

一応Windowsのファイルにもタグをつける仕組みはあるけれどちょっと面倒なのでVBScriptAccessを使って簡易システム化してみた。
といっても登録部分はAccessの機能を殆ど使わず、VBScriptからADOで登録する形式だ。

作成するシステムの解説

スクリプトにファイルをドラッグ&ドロップすると用途や入手方法、より適切な新しいファイル名、タグを尋ね、それらを入力するとAccessDB上に登録される。その際に一意の番号を発行してファイル名の先頭に付加する。
特に人からもらったファイルはファイル名を変えてしまうと「あのファイル」と言われたときに分からなくなるけど、自分の言葉で適切にファイル名を付けておかないとそれはそれで分からなくなるので、新旧名称をDBに登録できるようにした。
AccessDB上にファイルを添付してしまうことも技術的には可能と思われるが、今回はそれはせず、保管場所は一旦元の場所を保持する仕組みにしている。

Accessファイルの準備

FileDB.accdbというファイル名で適当な場所に作成。
私の場合は "C:\Users\thom\Documents\FileDB.accdb" とした。

FileMasterというテーブルを作り、以下のフィールドを作成。
f:id:t-hom:20180416010510p:plain

VBScriptのコード

コードは以下の通り。RegisterFiles.vbsというファイル名にした。自分のOSが64Bitなので32Bitで動作を試してないんだけれど、基本的にはこのままで動くはず。動かなければ64Bit対策と書かれた部分をバッサリ削除したら動くかもしれない。

'64bit対策ここから---------->
Dim Sh 'As WScript.Shell
Set Sh = CreateObject("WScript.Shell")

Dim Processor: Processor = Sh.ExpandEnvironmentStrings("%PROCESSOR_ARCHITECTURE%")
Dim Systemroot: Systemroot = Sh.ExpandEnvironmentStrings("%SYSTEMROOT%")

Function Quote(x) 'As String
    Quote = """" & x & """"
End Function

Function ArgsToString() 'As String
    Dim ret 'As String
    If WScript.Arguments.Count > 0 Then
        For Each a In WScript.Arguments
            ret = ret & Quote(a) & " "
        Next
        ret = " " & Left(ret, Len(ret) - 1)
    End If
    ArgsToString = ret
End Function

If Processor = "AMD64" Then
    Sh.Run Quote(Systemroot & "\SysWOW64\wscript.exe") _
        & " " & Quote(WScript.ScriptFullName) & ArgsToString
    WScript.Quit
End If
'<----------64bit対策ここまで

Dim FSO 'As Scripting.FileSystemObject
Set FSO = CreateObject("Scripting.FileSystemObject")

Call Main

Sub Main()
    Dim dic 'As Scripting.Dictionary
    Set dic = CreateObject("Scripting.Dictionary")
    For Each arg In WScript.Arguments
        Set f = New FileInfo
        f.FullPath = arg
        f.Init
        dic.Add arg, f
    Next
    
    For Each k In dic.Keys
        AddDB dic(k)
    Next
    WScript.Echo "登録が完了しました。"
End Sub

Sub AddDB(o)
    Const dbFile = "C:\Users\thom\Documents\FileDB.accdb"
    Set cn = CreateObject("ADODB.Connection")
    cn.Open "Driver={Microsoft Access Driver (*.mdb, *.accdb)}; DBQ=" & dbFile & ";"
    Set rs = CreateObject("ADODB.Recordset")
    Const adOpenKeyset = 1
    Const adLockOptimistic = 3
    rs.Open "FileMaster", cn, adOpenKeyset, adLockOptimistic
    rs.AddNew
    rs.Update    '←この後参照するAutoNumber型のrs.Fields(0)を確定させるために一度更新をかける。
    o.FileNumber = rs.Fields(0)
    rs.Fields(1) = o.OriginalFileName
    rs.Fields(2) = o.GetFrom
    rs.Fields(3) = o.GetBy
    rs.Fields(4) = o.Description
    rs.Fields(6) = Now()
    o.ChangeFileName
    rs.Fields(5) = o.NewFileName
    rs.Fields(7) = o.Tag
    rs.Update
    rs.Close: cn.Close
End Sub

Class FileInfo
    Public FullPath
    Public OriginalFileName
    Public Description
    Public GetFrom
    Public GetBy
    Public NewFileName
    Public FileNumber
    Public Tag
    Sub Init()
        OriginalFileName = FSO.GetFileName(FullPath)
        GetFrom = InputBox("「" & OriginalFileName & "」の提供者名を入力してください。")
        GetBy = InputBox("「" & OriginalFileName & "」の入手方法を入力してください。")
        NewFileName = InputBox("「" & OriginalFileName & "」に、より適切な新規ファイル名を入力してください。")
        NewFileName = NewFileName & "." & FSO.GetExtensionName(FullPath)
        Description = InputBox("「" & NewFileName & "」についての説明を入力してください。")
        Tag = InputBox("「" & NewFileName & "」に付けるタグを入力してください。")
    End Sub
    Sub ChangeFileName()
        NewFileName = "F" & Right("000000" & FileNumber, 6) & "_" & NewFileName
        FSO.MoveFile FullPath, FSO.BuildPath(FSO.GetParentFolderName(FullPath), NewFileName)
    End Sub
End Class

試しにtest.gifというファイルをドロップすると諸々の質問がなされ、それに答えていくとDBに登録される。そしてファイル名も新しくなる。
f:id:t-hom:20180416012808p:plain

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

64bit対策について

今回一番苦労したのが64bit対策。StackOverflowで解決策を見つけたんだけれど、勘違いしてドはまりした。
64bitOSではSystem32フォルダに64bit用のバイナリが配置されていて、SysWOW64フォルダに32ビット互換用のバイナリが配置されているということらしい。てっきりSystem32が32Bit、SysWOW64が64bitだと思ってたのでここで3時間ほどロス。
恐らく諸々の互換性のために標準のシステムフォルダはSystem32で固定化しているのだと思う。

環境変数の%PROCESSOR_ARCHITECTURE%を確認すれば32bit環境か64bit環境かが分かるので、64bitだったらSysWOW64フォルダ内の32bit版wscriptを呼び出す。
ちなみに%PROCESSOR_ARCHITECTURE%は64bit版WindowsAMD64となっている。AMDのCPUはもちろん、IntelのCPUでもAMD64。ややこしいけど、CPUアーキテクチャ名なので仕方がない。

VBA入門記事 インデント徹底解説!

VBA初心者のコードを見ると、インデントが乱れていることが多い。
中・上級者にとって、インデントはあまりに自明のことで、それゆえに詳しく解説されることがない。せいぜい入門書の最初の頃にちょろっと「きちんとインデントしましょう」と書かれる程度である。

インデントが多少乱れていたとしても、プログラムの実行に影響はない。しかしインデントの乱れはコードの構造を把握しにくくさせてしまう。今回は初心者向けの記事として、インデントについて徹底解説してみたいと思う。

インデントとは

コードの構造を分かりやすくするためにタブやスペースなどの空白文字を左に詰めて字下げすることをいう。

何をインデントするか

VBAでは、原則として「開始」と「終了」でセットになった構文の、中身をインデントする。たとえば、Sub~End Sub、For~Next、Do~Loop等である。ただし、分岐命令のIfやSelectは少し勝手が異なるので注意。

実際にやってみよう。

まず全くインデントされていないサンプルコードを用意する。

Sub FizzBuzz()
For i = 1 To 100
If i Mod 15 = 0 Then
Debug.Print "FizzBuzz"
ElseIf i Mod 3 = 0 Then
Debug.Print "Fizz"
ElseIf i Mod 5 = 0 Then
Debug.Print "Buzz"
Else
Debug.Print i
End If
Next
End Sub

まず外側から見ていく。
最初に目につくのは、Sub FizzBuzz()が「開始」、End Subが「終了」である。だからその中身をインデントする。

Sub FizzBuzz()
    For i = 1 To 100
    If i Mod 15 = 0 Then
    Debug.Print "FizzBuzz"
    ElseIf i Mod 3 = 0 Then
    Debug.Print "Fizz"
    ElseIf i Mod 5 = 0 Then
    Debug.Print "Buzz"
    Else
    Debug.Print i
    End If
    Next
End Sub

次に、For i = 1 to 100が「開始」、Nextが「終了」とみなすことができるのでその中身をインデントする。

Sub FizzBuzz()
    For i = 1 To 100
        If i Mod 15 = 0 Then
        Debug.Print "FizzBuzz"
        ElseIf i Mod 3 = 0 Then
        Debug.Print "Fizz"
        ElseIf i Mod 5 = 0 Then
        Debug.Print "Buzz"
        Else
        Debug.Print i
        End If
    Next
End Sub

ここで条件分岐が登場する。If~End Ifまでと言ってしまうと、こんな風になってしまう。

Sub FizzBuzz()
    For i = 1 To 100
        If i Mod 15 = 0 Then
            Debug.Print "FizzBuzz"
            ElseIf i Mod 3 = 0 Then
            Debug.Print "Fizz"
            ElseIf i Mod 5 = 0 Then
            Debug.Print "Buzz"
            Else
            Debug.Print i
        End If
    Next
End Sub

しかし、これは間違い。

If文も中身をインデントするけれど、中身のとらえ方が違う。

たとえば日本語でIfの複数条件を書くと、こんなイメージになる。

もしAならば、
    〇〇しなさい
それ以外で、もしBならば、
    〇〇しなさい
それ以外で、もしCならば、
    〇〇しなさい
どれにも当てはまらないなら
    〇〇しなさい
以上

つまり「〇〇しなさい」という命令が中身なのであって、条件の提示は中身ではない。
話をIf文に戻すと、If・ElseIf・Else・End Ifでワンセットの構文ということである。

よって、正しいインデントは次のようになる。

Sub FizzBuzz()
    For i = 1 To 100
        If i Mod 15 = 0 Then
            Debug.Print "FizzBuzz"
        ElseIf i Mod 3 = 0 Then
            Debug.Print "Fizz"
        ElseIf i Mod 5 = 0 Then
            Debug.Print "Buzz"
        Else
            Debug.Print i
        End If
    Next
End Sub

何文字分インデントするか

VBエディタの規定では、スペース4つ分となっているので特に理由が無ければそのままで良い。
目的は、文の構造を目立たせて理解を助けることにあるので、あまりスペースが少なすぎると効果が薄い。多すぎるとコードが右に伸びてやっかいである。

インデントを綺麗に揃えるための操作方法

まず最初に覚えるべき操作

デフォルトではTabキーを押すとスペースが4つ入るので、Tabキーでインデントする。
スペースでちまちまやると時間がかかるしズレやすい。

インデントされた行でEnterキーを押すと、次の行がインデントされた状態で始まる。
このインデントを消すにはBackSpaceを1回だけ押せばよい。

最初から綺麗なインデントでコーディングする。

次のGIFアニメで書いているように、基本的には外から内に向かってコードを書いていく。そうすれば自動インデントも活かしやすい。
f:id:t-hom:20180226231805g:plain

それから、ForとNextを書き終わって中身を書き始める操作だが、まずNextにあるテキストカーソルを、矢印キーの↑でForの行に持っていき、そこでEndキーを押す。するとテキストカーソルが行末に飛ぶのでEnterで行を挿入し、Tabでインデントしている。
ちなみにEndの代わりにHomeを押すとテキストカーソルが行頭に飛ぶのでこちらも覚えておくと良い。

ネットで拾ったインデントされていないコードを綺麗にする。

まず前提知識として、ウェブサイトの掲示板などでは、その仕様上、投稿するとインデントが消えてしまうものがある。だから上級者もインデントしてないからインデントしなくても良いんだというのは勘違いである。

さて、インデントされていないコードを綺麗に編集したい場合どうするか。

とりあえず、マズイやり方がこちら。
f:id:t-hom:20180226232657g:plain

これは非常に面倒なうえに、ズレやすい方法である。
じゃあTabキーで1行ずつインデントしていくのかというと、それも違う。VBエディタでは、複数行のインデントを一度に変更する機能があるのでそちらを使う。

次のGIFアニメでは、ズレたインデントをまともなインデントに修正する一連の流れを紹介する。
f:id:t-hom:20180226233023g:plain

まず、コード上でドラッグ操作をして複数行選択し、Shiftを押しながらTabを複数回押すことで、完全にインデントを消すことができる。

次に、コードの構造にしたがって構文の内側を複数行選択し、Tabキーで一度に選択行すべてをスペース4つ分インデントする。
このテクニックを必要箇所に繰り返し適用して完成。ただし飛び地は複数選択できないのでDebug.Printのところは一つずつインデントするしかない。

インデントを綺麗に見せる設定

スペース4つ分のインデントだと開きすぎという方もいると思う。私はフォントをメイリオにしているので標準のゴシックとちがってインデントがかなり詰まって見える。スペース4つ分でちょうど良い。

ついでにそのあたりの設定を変更するための記事もご紹介。
thom.hateblo.jp

以上

プログラミング用語はなぜ小難しいのか

プログラミングでは識別子・キーワード・文字列・演算子リテラルといった小難しい言葉が登場する。
意味は調べれば分かるが、なんでわざわざそんな小難しい言葉を使うんだろうか。

演算子は、計算記号って呼んじゃだめ?
文字列は、単語って呼んじゃだめ?
識別子は、名前って呼んじゃだめ?
リテラルは、値って呼んじゃだめ?

とまぁ、目にするたびにモヤっとしつつもプログラミングしているうちになんとなく慣れてしまった方もいるのではないかと思う。

この記事では、そういった小難しい言葉を使う理由に一応の説明をつけ、これらの言葉の正確な意味を掘り下げて紹介してみる。

なぜ小難しい言葉を使うのか

所説あるが~…などとうそぶいても仕方がないので正直に言うけれど、正確な理由は私も知らない。だからこれは巷の説ではなくて、あくまで私個人の考えである。「一応の説明」と言ったのはそういうことなので、あくまで参考にとどめてほしい。

まず考えられるのは、アメリカから入ってきたプログラミングの用語をそのまま和訳した為というもの。
Operator(オペレーター)が演算子、Identifier(アイデンティファイア)が識別子という風に。

それならKeyword(キーワード)を主要語・Literal(リテラル)を直値などと訳すこともできたはずだけれど、これが文章に登場する場合、「主要語」とか「直値」などと書かれているとややこしい。それがプログラミング用語としての「主要語」なのか、一般的な意味での主要な語なのか紛らわしい。

そして結果的に現在の用語に落ち着いているのは、結局プログラミング用語として特定の要素を指す名前として都合がいいからだと思う。

正確さ・厳密さを要求されるプログラミングでは、その解説においても正確さ・厳密さが必要とされる。だから一般用語とは異なる、誤解の余地のない言葉を選定する必要があった。といったところではないかと思う。

それぞれの言葉の意味

実はプログラミング言語によって微妙に指すものが違ってくる。たとえば演算子(+, -, *, / など)はVBAにおいては識別子ではないが、とある言語では演算子も識別子の一種として扱われることがある。
そこで、説明の食い違いを避けるため、この先の解説はVBA言語を前提とする。

識別子(しきべつし)

識別子とは、変数名・プロシージャ名・関数名・オブジェクト名などを指す名前のこと。その名の通り、モノを識別するための道具。個別にいうと「〇〇名」なのに、それらの総称が「名前」ではなく「識別子」なのは、そう言ったほうが厳密性が高まるからだと思う。例えばFor文やIf文にも"For"・"If"という名前がついているが、これらは後述する「キーワード」と呼ばれるものであって、識別子ではない。

とはいえ、プログラマー同士の会話でも普段は単に「名前」と呼ばれることが多く、識別子という単語が登場するのは主に解説書、言語のヘルプ、エラーメッセージなど、厳密性が問題になる場面が多いように思う。文章中に出てくる分には自然だが、普段の会話で得意げに「識別子」を連呼すると、最近覚えたの?(笑)となるので注意。

識別子の「子」はOperatorの-orに相当する接尾辞で、動詞を名詞化する意味がある。「〇〇子」とあれば、〇〇するモノ、〇〇を司るモノ、〇〇するための道具といった理解で合ってると思う。

識別子には英数字、漢字、ひらがな、カタカナ、アンダーバーが使える。
ただし1文字目を数字やアンダーバーで始めることはできない。後述するキーワードと同じ名前を付けることもできない。
また、あまり知られていない(もとい、知らなくてよい)けれど、一部の全角特殊記号も使える。

Sub()= 10= 20=*MsgBoxEnd Sub

同じスコープで、同一の識別子を複数回定義することはできない。
逆に言えば、スコープが異なれば同一の識別子名を付けることができる。

あまり知られていない(もとい、知らなくてよい)けれど、VBAの関数はVBAモジュールに存在するただの識別子なので、同じ名前で標準モジュール等に再定義することができる。本家を呼び出すにはモジュール名のVBAを付けて、「VBA.関数」の形で呼ぶ。

Function Now()
    Now = Format(VBA.Now, "ggge年m月d日(aaa曜日)")
End Function

Sub Main()
    MsgBox Now    '再定義されたNowが呼び出される。
    MsgBox VBA.Now '本家VBAのNowが呼び出される。
End Sub

まあそういうことは、しないように。

キーワード

キーワードはFor・If・Subなど、VBAの文法の根幹をなす重要な命令群で、VBエディタで濃い青で表示されるものだ。
VBの解説書ではキーワードという語が用いられるが、一般用語としても広まっているのでプログラマーが口にする場合は「予約語」というケースが多い。名前の通りめ、束されているので、識別子と違って再定義したりできない。

文字列(もじれつ)

VBAで文字列は、その名前のとおり、文字が並んだもの。VBAでコード中に文字列を書く場合は、ダブルクォーテーションで囲む。
"こんにちは"も文字列だし、"-----"も文字列だ。1 + 1 は計算式であるが、"1 + 1"は文字列になる。
これらを総称して何と呼ぼうか。単語でも文でもない。つまり文字列としか呼びようがない。

文字と文字列を区別する言語では、文字はシングルクォーテーション、文字列はダブルクォーテーションで囲むルールが多い。
文字列型を持たないC言語では文字型の配列で文字列を現した。

VBAの文字列も実はByte型の配列と互換性があり、内部では文字の配列として管理されていると思われる。つまり文字の配列だから文字列という解釈もできる。

演算子(えんざんし)

そもそも演算という言葉が難しい。日常的には計算という。
計算の「計」は小計・合計の計と同じで、答えを求めることを主眼に置いた言い方。
演算の「演」は演じるの演で、計算の過程そのものに主眼を置いた言い方。

1 + 1 + 1を計算すると、まず1 + 1を演算して2、2 + 1を演算して3、よって計算結果は3という使い分けだろうか。

論理演算子のAndやOr、文字列を結合する&も演算子に含まれる。

演算子の大半は二項演算子といって、(1 + 1)のように演算子の左右に値をとるタイプであるが、論理演算子のNotは1つしか項を取らない単項演算子である。
三項演算子を持つ言語もあるが、VBAには無い。無いけれど一応以下に解説しておく。

例えばCheckBox1がチェックされていたら"Yes"、そうでなければ"No"を表示させたい場合を例に挙げると、三項演算子「?:」を使ってこのように書く。まぁVBAには無いので、書けないけど。

MsgBox CheckBox1.Checked ? "Yes" : "No"

代わりにVBAでは同等の機能が関数として用意されている。別言語から来た方はこれを三項演算子と呼ぶ方がいるけれど、VBAの場合これは演算子ではなく、ただの関数なので間違い。

MsgBox IIf(CheckBox1.Checked, "Yes", "No")

ちなみにC言語等で「?:」を指して三項演算子と呼ぶのは、たまたま三つの項を取る演算子が「?:」しか存在しないためであり、本来「?:」の固有名詞としては条件演算子という呼称が正しい。

リテラル

リテラルはプログラミングコード中に直接現れる値のこと。
たとえばa = 100と書いたときの100、msg = "Hello"と書いたときの"Hello"がリテラル
MsgBox a、MsgBox msgなど、変数として扱われるものはリテラルではない。

VBAリテラルは、数値・文字列・日付や時刻・論理値(TrueとFalse)がある。
数値リテラルと論理値リテラルは特に何も囲わずそのまま記述される。1や5.5、True、Falseなど。
文字列リテラルはダブルクォーテーションで囲まれる。"Hello"など。

日付や時刻はナンバーサインで囲まれる。
これはあまり知られてないので念のため解説すると、以下のような形式である。
#月/日/年#、#時:分:秒 AM・PM#
例) #2/26/2018#、#2:10:00 AM#

以上

VBA 部屋割りマクロ(ネタをいただきました)

いつも読んでるid:akashi_keirinさんのサイトに食指が動くネタがあったので乗っかってみようと思います。
akashi-keirin.hatenablog.com

Twitterで本人にコンタクトして快諾いただきました。

さて、まずは同じように部屋の表を用意。
f:id:t-hom:20180216232720p:plain

そして、部屋をコレクションとして取得する関数を作成。
この関数は部屋データの表範囲(今回ならA2:B7)を引き渡して、部屋が入ったコレクションを取得する想定。

Function GetRooms(roomRange As Range) As Collection
    Dim ret As Collection: Set ret = New Collection
    Dim rooms: rooms = roomRange.Value
    
    Dim noRoom As Boolean, i As Long
    Do
        noRoom = True
        For i = LBound(rooms, 1) To UBound(rooms, 1)
            If rooms(i, 2) > 0 Then
                ret.Add rooms(i, 1)
                rooms(i, 2) = rooms(i, 2) - 1
                noRoom = False
            End If
        Next
    Loop Until noRoom
    Set GetRooms = ret
End Function

これを使って部屋を順番に割り当てる処理がこちら。

Sub Main()
    Const 人数 = 8
    Dim rooms As Collection
    Set rooms = GetRooms(Range("A2:B7"))
    
    If 人数 > rooms.Count Then
        MsgBox "部屋が足りません。", vbExclamation
    Else
        Dim i As Long
        For i = 1 To 人数
            Debug.Print i; "人目は"; rooms(1); "号室です。"
            rooms.Remove 1
        Next
    End If
End Sub

表への書き出しまで再現するのは面倒だったのでDebug.Printで代用した。

※GetRooms関数とMain関数でそれぞれ変数roomsを用いているが、別物であることに注意。
 Main内のroomsはCollection、GetRooms内のroomsはバリアント型で、中身はセル範囲を転記した配列である。

発展

ここから先は私は作らないので妄想だけど、たとえばこんなふうに部屋代と予約状況が表示されてて、
f:id:t-hom:20180216234804p:plain

その範囲(Range)と、人数を渡すと自動的に再安値で必要な部屋数を確保してくれる関数とかも面白そうだ。

あとはランダムで部屋割りするマクロとか。
これは以下で紹介したShuffleCollectionプロシージャを使えば簡単にできる。
thom.hateblo.jp

以上

VBA 矩形選択範囲で重複データの入ったセルを塗りつぶす方法

重複データを探すというのは比較的よくやる処理だけれど、データが矩形に配置されていてその姿のまま処理したい場合は関数では少々面倒くさい。

今回は選択した矩形範囲のデータから内容の重複するセルを塗りつぶして可視化するマクロを作った。
ちなみに定数でどれを何色で塗るか、塗らないかを選択できるようにした。

Sub 重複orユニークを塗る()
    Const 重複を塗る = True
    Const 重複セル色 = vbYellow
    Const ユニークを塗る = False
    Const ユニークセル色 = vbRed
    
    Dim arr: arr = Selection.Value
    Dim arr2: ReDim arr2(LBound(arr, 1) To UBound(arr, 1), LBound(arr, 2) To UBound(arr, 2))
    Dim i, j, k, l
    For i = LBound(arr, 1) To UBound(arr, 1)
        For j = LBound(arr, 2) To UBound(arr, 2)
            For k = LBound(arr, 1) To UBound(arr, 1)
                For l = LBound(arr, 2) To UBound(arr, 2)
                    If Not (i = k And j = l) Then
                        arr2(i, j) = arr2(i, j) Or arr(i, j) = arr(k, l)
                    End If
    Next l, k, j, i
    
    Dim m, n
    For m = LBound(arr2, 1) To UBound(arr2, 1)
        For n = LBound(arr2, 2) To UBound(arr2, 2)
            If arr2(m, n) And 重複を塗る Then
                Selection(m, n).Interior.Color = 重複セル色
            ElseIf (Not arr2(m, n)) And ユニークを塗る Then
                Selection(m, n).Interior.Color = ユニークセル色
            End If
    Next n, m
End Sub

この手の処理はセル数の二乗の比較が必要になるので、いきなりセル上でやると効率が悪い。
そのため、まずセル範囲を配列arrに転記して配列内で比較を行い、別の配列arr2にその結果をBoolean型で格納している。

次のこの配列arr2を元に選択範囲を塗りつぶしている。

そして以下はテスト用に適当に選択範囲にアルファベットを埋めるコード。

Sub hoge()
    Dim r As Range
    For Each r In Selection
        r.Value = Chr(WorksheetFunction.RandBetween(Asc("a"), Asc("z")))
    Next
End Sub

適当に埋めたアルファベットのうち、前述のマクロで重複を塗りつぶしたのがこちら。
f:id:t-hom:20180214081217p:plain

着想からコーディング、記事公開まで約50分。うむ。上出来。
やっぱ朝イチの集中力は素晴らしい。

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