t-hom’s diary

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

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分。うむ。上出来。
やっぱ朝イチの集中力は素晴らしい。

節目のブースト効果について

さて、そろそろ世間の皆様もお仕事が始まる頃だろうか。

新年を迎え、今年こそは「〇〇」と意気込んでいる方も多いと思う。
これに対し先日、Twitterでは冷ややかな反応が「いいね」を集めていた。

普段から「〇〇」してない連中が新年にこぞって「〇〇」を誓う姿が滑稽だし、どうせ三日坊主だろうという反応である。

なるほど一理ある。というか、私もどちらかと言えば冷ややかに見守る側の人間だった。

しかし、日々の生活はあまりに日常的すぎて、自堕落な人間が何かを始めるには相当な気力が必要になる。
まぁ、当たり前っちゃ当たり前のことなんだけれど、今更本当の意味で理解したというか、その冷ややかなツイートを見て改めて「いや、ちがうんだよ」という気持ちが湧いてきたので、一応記事に残しておこうと思った次第である。

最近私は、節目の力を借りるというのは、とても良いことだと考えている。
節目のブースト効果で、それほど大きな気力を必要とせず、何か新しいことを始められる。
この現象を節目ブースターと呼ぼう。節目ブースターを使えば、怠惰と戦うことなくスタートダッシュを切れる。

私の場合、昨年12月から今年の1月にかけて、3つの節目がある。
ひとつは転職、もうひとつはその転職先の企業が迎える節目、そして今月中旬の自宅引っ越し。

きのこ3つ。圧倒的、節目ブースター!
これはもう、色々と頑張るしかないっしょ。

VBScriptとSAPIとタスクスケジューラで、一定時間ごとに音声で休憩を促す仕組みを構築

前回の記事で紹介した音声読み上げの仕組みをVBScriptに移植して、タスクに登録することで一定時間ごとに音声で休憩を促してくれる仕組みを作った。Windows 8.1以降で動作すると思われる。7は多分、音声合成エンジンが標準で入っていないのでダメ。

今回は英語メッセージを読み上げるサンプルを紹介する。これは単に自己満足なので日本語が良い方は前回記事を参考にボイスを切り替えると良い。

■前回の記事
thom.hateblo.jp

VBSの準備

以下のようなコードを準備して、適当な場所にSpeakMessage.vbsとして保存する。
保存場所はバッチ用なので普段触らない場所が良いと思う。私の場合はCドライブ直下にTaskというフォルダを作ってその中に入れた。

Call SpeakMessage

Sub SpeakMessage()
    With CreateObject("SAPI.SpVoice")
        Set .Voice = .GetVoices().Item(1)
        .Speak "Hey Thom! It's time to take a break. Stop using computer and take a rest while 5 minutes."
    End With
End Sub

フランクでフレンドリーな感じにしたかったのだけれど、英語として合ってるのかは自信ない。

Googleさんに和訳してもらうとこんな感じ。
f:id:t-hom:20180103163842p:plain

タスクの登録

まず、タスクスケジューラを起動する。
Windows 10の場合、1)スタート→2)Windows 管理ツール→3)タスクスケジューラと辿る。
f:id:t-hom:20180103164554p:plain

起動したら操作メニューからタスクの作成を実行。
f:id:t-hom:20180103164948p:plain

全般タブでは任意の名前を設定する。今回はInformBreakTimeとした。
f:id:t-hom:20180103165118p:plain

トリガータブでは新規ボタンをクリックし、出てきたウインドウで次の赤枠のとおり設定する。
f:id:t-hom:20180103165610p:plain
青枠の箇所は任意。1時間に1回なら、キリの良い時間に開始設定しておくと良い。

操作タブでは新規ボタンをクリックし、出てきたウインドウで操作がプログラムの開始になっていることを確認し、参照ボタンをクリックする。
f:id:t-hom:20180103165858p:plain

先ほど作成したSpeakMessage.vbsを選択し、次の良いにセットされたらOKで閉じる。
f:id:t-hom:20180103170011p:plain


これで設定は完了したのでタスクの作成ウインドウもOKで閉じる。

タスクスケジューラライブラリに表示されたらタスクスケジューラを閉じて作業完了。
f:id:t-hom:20180103170252p:plain

ちなみにタスクスケジューラライブラリから目的のタスクを右クリックすることで手動実行したり無効にしたり、削除したりできる。
手動で実行すると音声が終わってもタスクが終了しなかったので、右クリックして終了させる。
f:id:t-hom:20180103170521p:plain

その際に次のようなメッセージが出るが、OKで良い。
f:id:t-hom:20180103170541p:plain

これで1時間ごとに自動で休憩を促してくれるようになる。
なお、自動実行の場合は実行が終わると状態が準備完了に戻る。

おわりに

5分経過した後に「作業にもどってOK」の旨を通知するスクリプトを書いても良いなと思った。
やり方としては、ひとつのVBSの中で5分Waitさせても良いし、2つスクリプトを作ってタスクをもう一つ登録するのも良い。
あるいはスクリプトに引数を持たせて1つのスクリプト、2つのタスクで引数によってメッセージを切り替えるという手もある。

モニターの見過ぎや、座りっぱなしは健康に悪い。適宜休憩をとろう。

追記

後から気付いたけど、これ5分休憩よりも「5分間、部屋の掃除をしなさい」という指示に変えたほうが良さげ。
体力仕事の休憩は文字通り体を休めることだけれど、コンピューター作業の休憩は逆に体を動かすことなので。
綺麗な部屋と健康を得られるので一石二鳥。

Excel VBAで音声読み上げ

今回はExcel VBAでSAPIを使った音声読み上げを紹介する。
SAPIとはSpeech Application Programming Interfaceの略で、Windowsアプリケーションで音声認識や音声合成を使うためにマイクロソフトが開発したAPIである。
Windows 8.1 以降のOSには標準で音声合成エンジンが同梱されている。

※Windows 7の場合、SAPIの仕組み自体はありますが音声合成エンジンが別途調達しないといけないので今回のマクロはそのままでは動作しないと思われます。

作成したコードはこちら。

Sub Greeting()
    With CreateObject("SAPI.SpVoice")
        Set .Voice = .GetVoices.Item(0)
        .Speak "こんにちは"
    End With
End Sub

このマクロを実行すると、日本語で「こんにちは」と読み上げられる。
※読み上げ中は他の操作ができなくなるので注意。
ただWindowsにインストールされている音声合成エンジンに依存するため、環境によってうまくいかないこともあるかもしれない。

利用できる音声合成エンジンを調べる方法

利用できる音声合成エンジンを調べるには次のマクロを実行する。

Sub ShowVoiceList()
    With CreateObject("SAPI.SpVoice")
        For i = 0 To .GetVoices.Count - 1
            Debug.Print i & ":" & .GetVoices.Item(i).GetDescription
        Next
    End With
End Sub

私の環境(Win10Home 64bit)の場合、イミディエイトウインドウに次のように表示された。

0:Microsoft Haruka Desktop - Japanese
1:Microsoft Zira Desktop - English (United States)
2:Microsoft David Desktop - English (United States)

冒頭で紹介したGreetingマクロにSet .Voice = .GetVoices().Item(0)という一文があるが、このItemプロパティの0を1に変えると私の環境ではZiraという英語ボイスになる。ただ英語なので日本語には対応していない。

外国人ボイスで日本語を読み上げる方法

日本語ボイスがインストールされてない場合、ローマ字で「kon nichiwa」と入力すればカタコトで、「コンニチハ」と読み上げてくれる。「Konnichiwa」だと「ケネティワ」に聞こえたのでスペースを空けた。

Sub GreetingKatakoto()
    With CreateObject("SAPI.SpVoice")
        Set .Voice = .GetVoices.Item(1)
        .Speak "Kon nichiwa"
    End With
End Sub

参照設定で利用する場合

参照設定を利用する場合は、Microsoft Speech Object Libraryを参照する。
ただし、環境によっては下図のように同名の参照が2つ存在するので、実体の場所を確認し、sapi.dllとなっているものを選択する。
f:id:t-hom:20180103153150p:plain

もう片方はsapi_onecore.dllとなっていたが、こちらはVBAから使えなかった。

参照設定させると、冒頭のGreetingは次のようになる。

Sub GreetingOnReference()
    With New SpVoice
        Set .Voice = .GetVoices().Item(0)
        .Speak "こんにちは"
    End With
End Sub

あるいは変数を宣言しても良い。
以下は、型が分かるように逐一変数に入れた例。

Sub GreetingWithVariables()
    Dim greeter As SpVoice
    Set greeter = New SpVoice
    
    Dim speechVoice As ISpeechObjectToken
    Set speechVoice = greeter.GetVoices.Item(1)
    
    Set greeter.Voice = speechVoice
    greeter.Speak "Kon nichiwa"
End Sub

何がしたかったのか

パソコンの連続使用はとても目に悪い。仕事では会議に行ったり上司に呼ばれたりして適宜モニターの前を離れることもあるけど、自宅で作業に没頭してしまうとついつい何時間もモニターを見つめ続けることになる。
そこで、一定時間ごとに音声で知らせてくれるような仕組みを作ろうと思ったのがきっかけ。

とりあえず最初はExcelで作ったのでそのまま記事にしたけれど、オブジェクトの使い方さえ分かればVBSへの移植は簡単。あとはタスクスケジューラに登録して一時間ごとに音声で休憩を促す。この辺りは次回の記事で書こうと思う。

きっかけは最近購入した以下の書籍。

ヘルシープログラマ ―プログラミングを楽しく続けるための健康Hack

ヘルシープログラマ ―プログラミングを楽しく続けるための健康Hack

  • 作者:Joe Kutner
  • 発売日: 2015/07/23
  • メディア: 単行本(ソフトカバー)

その中に次の一節がある。

アメリカ労働安全衛生局によると、コンピュータを使う作業者が5分間の「ミニ休憩」を一日のうちに5回増やせば、痛みと眼精疲労が大きく減るのです。

なるほど。では1時間ごとに5分の休憩を取ろうと思ってこれを考えた。

ちなみにヘルシープログラマはわざわざエンジニアのために書かれた健康本である。「健康へのイテレーティブなアプローチ」などと、一般の方にはちょっと何言ってるか分からない用語が頻出し、ほとんど文字情報なので活字耐性のない非プログラマーは退屈するかもしれない。でも論理的に書かれていて、エンジニアには良い本だと思う。

2021/1/30 追記

以下のように音声を追加してみたところ、前述の方法では追加した音声が使用できないことが分かった。
f:id:t-hom:20210130182227p:plain

調べたところ、OneCoreという別の場所に分類されてるらしく、コードを変更することで対応できるようだ。

以下、参考サイト
qiita.com

以下、サイトを参考に作ってみたコード。JamesとZiraとHarukaがそれぞれ文を読み分ける。

Sub JamesZiraHarukaSpeaksByExcelVBA()
    Dim sapi As Object, tokenCategory As Object
    Set sapi = CreateObject("SAPI.SpVoice")
    Set tokenCategory = CreateObject("SAPI.SpObjectTokenCategory")
    tokenCategory.SetID "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices", False
    
    Dim James As Object
    Dim Zira As Object
    Dim Haruka As Object
    Dim token As Object
    For Each token In tokenCategory.EnumerateTokens
        If token.GetAttribute("Name") = "Microsoft James" Then
            Set James = token
        End If
        If token.GetAttribute("Name") = "Microsoft Zira" Then
            Set Zira = token
        End If
        If token.GetAttribute("Name") = "Microsoft Haruka" Then
            Set Haruka = token
        End If
        If Not (James Is Nothing Or Zira Is Nothing Or Haruka Is Nothing) Then Exit For
    Next
    
    Set sapi.Voice = James
    sapi.Speak "From Wikipedia Poisson's Ratio"
    sapi.Speak "In materials science and solid mechanics, " _
                    & "Poisson's ratio is a measure of the Poisson effect, " _
                    & "the deformation of a material in directions perpendicular to the direction of loading. "
    
    Set sapi.Voice = Zira
    sapi.Speak "The value of Poisson's ratio is the negative of the ratio of transverse strain to axial strain."
    sapi.Speak "For small values of these changes, " _
                    & "is the amount of transversal elongation divided by the amount of axial compression."
    
    Set sapi.Voice = Haruka
    sapi.Speak "以上で解説を終わります。ありがとうございました。"
End Sub

以上

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