Quantcast
Channel: t-hom’s diary
Viewing all 493 articles
Browse latest View live

VBA マージソートの実装と図解

$
0
0

前回はトランプを使ってマージソートを説明した。
thom.hateblo.jp

今回は実際にVBAマージソートを書いてみる。

目次

準備

まずはソートの対象を用意しないといけないので、配列を作ることにした。

Sub MStart()'配列Arrの準備Dim Arr()AsLongConstMin=1,Max=10ReDim Arr(MinToMax)EndSub

後から配列の数を変更できるように動的配列として、ReDimにMinとMaxの定数を与える方式とした。

次に、配列にランダムな数を格納していく。
このとき、同時にイミディエイトウィンドウに出力するようにした。

Sub MStart()'配列Arrの準備Dim Arr()AsLongConstMin=1,Max=10ReDim Arr(MinToMax)'配列Arrにランダム数を格納しながら出力For i =LBound(Arr)ToUBound(Arr)Const LowerBound =1, UpperBound =100
        Arr(i)=Int((UpperBound - LowerBound +1)*Rnd+ LowerBound)
        Debug.Print i; ":"; Arr(i)NextEndSub

ランダム値の上限と下限は定数で決めておく。
Rnd関数のヘルプに次の表記があるので、これをそのまま使用している。

任意の範囲の整数の乱数を生成するには、次の式を使ってください。
Int((upperbound - lowerbound + 1) * Rnd + lowerbound)

出力結果の例はこちら

 1 : 71 
 2 : 54 
 3 : 58 
 4 : 29 
 5 : 31 
 6 : 78 
 7 : 2 
 8 : 77 
 9 : 82 
 10 : 71 


次に結果表示のコードを書く。
これはArrの中身を出力するだけ。
初回の結果と区切るために、String関数で区切り線(ハイフン20個)を出力している。

Sub MStart()'配列Arrの準備Dim Arr()AsLongConstMin=1,Max=10ReDim Arr(MinToMax)'配列Arrにランダム数を格納しながら出力For i =LBound(Arr)ToUBound(Arr)Const LowerBound =1, UpperBound =100
        Arr(i)=Int((UpperBound - LowerBound +1)*Rnd+ LowerBound)
        Debug.Print i; ":"; Arr(i)Next
    Debug.PrintString(20,"-")'■ここにソートのコードを書く'ソート結果表示For i =LBound(Arr)ToUBound(Arr)
        Debug.Print i; ":"; Arr(i)NextEndSub

この時点ではまだ何もソートしていないので、初回に出力されたランダム数がそのままもう一度出力される。

さて、次にソートのコードを書いていく。
前回のトランプのソートを図で表すと、次のようになる。赤が分割、緑がマージである。
f:id:t-hom:20160321102446p:plain

これは再帰的な構造になっている。

分割するコード

さて、ではまず分割の部分を再帰で書いてみよう。
分割といっても配列を本当に2つに分けるわけではなく、添え字の開始値と終了値を設定することで特定範囲を表す。

次のコードは配列(Arr)と、添え字の開始値(Top)、添え字の終了値(Bottom)を受け取り、Middleを求めてさらに左と右に分けてMergeSortを呼び出す再帰コードである。

Sub MergeSort(Arr, Top, Bottom)'分割点Middleを求めるDim Middle AsLong
    Middle =(Top + Bottom) \ 2
    
    Debug.Print Top; "-"; Bottom
    
   '値が複数個あれば、さらにソートを呼び出すIf Top <> Bottom ThenCall MergeSort(Arr, Top, Middle)'左側のソートCall MergeSort(Arr, Middle +1, Bottom)'右側のソートEndIfEndSub

途中でわかりやすいようにTopからBottomを出力させている。
TopとBottomが一致したら、それは範囲が1つになったということで、再帰を停止する。
すると呼び出し元に戻り、今度は右側のソートに入る。

ちょうど以前に書いたフォルダの階層を辿るサンプルと似た動きをする。
thom.hateblo.jp

今回分を図示するとこんな感じ。
f:id:t-hom:20160321111140p:plain
左、左、左、左、戻る、右、戻る、戻る、右、戻る、戻る、右、左、戻る、右、戻る、戻る、戻る、右、以下略

実際にMStartから呼び出してみる。
MStart自体の出力はコメント化しておいた。

Sub MStart()'配列Arrの準備Dim Arr()AsLongConstMin=1,Max=10ReDim Arr(MinToMax)'配列Arrにランダム数を格納しながら出力For i =LBound(Arr)ToUBound(Arr)Const LowerBound =1, UpperBound =100
        Arr(i)=Int((UpperBound - LowerBound +1)*Rnd+ LowerBound)'Debug.Print i; ":"; Arr(i)Next'Debug.Print String(20, "-")'ソート呼び出しCall MergeSort(Arr,LBound(Arr),UBound(Arr))'ソート結果表示For i =LBound(Arr)ToUBound(Arr)'Debug.Print i; ":"; Arr(i)NextEndSub

イミディエイトウインドウの出力はこうなる。

 1 - 10 
 1 - 5 
 1 - 3 
 1 - 2 
 1 - 1 
 2 - 2 
 3 - 3 
 4 - 5 
 4 - 4 
 5 - 5 
 6 - 10 
 6 - 8 
 6 - 7 
 6 - 6 
 7 - 7 
 8 - 8 
 9 - 10 
 9 - 9 
 10 - 10 

出力結果を配列の範囲に当てはめると次のようになる。
f:id:t-hom:20160321112226p:plain

確かに左優先で呼び出されているのが分かる。

マージするコード

さて、これで分割部分はできたので、次にマージ部分を見ていく。
再帰から戻るときにマージが発生するので、コードは戻る部分に書いていく。

マージの各部分を見ていくと、前段階でグループ内がソート済みなので先頭から順に拾っていくだけでマージできることが分かる。
f:id:t-hom:20160321103343p:plain

交互になっているのはたまたま。
数字を替えてみると、こういうのもありえる。
f:id:t-hom:20160321103743p:plain
それでも各グループの先頭からしかとらないのは分かる。

もしこれが未ソートなら、数字を飛ばしたり戻ったりしないといけないので結局バブルソートと変わらないスピードになってしまう。
f:id:t-hom:20160321104143p:plain

さて、次は並び替え用にもう一つ配列を用意する。
ArrをそっくりそのままコピーしてArr2を用意し、Arr2からArrへ並べながら転記していく為だ。
また、左グループと右グループをマージするので、それぞれの先頭を指すカウンタ(LC、RC)と、メイン配列への転記位置を指すカウンタ(C)を準備しておく。

Sub MergeSort2(Arr, Top, Bottom)'分割点Middleを求めるDim Middle AsLong
    Middle =(Top + Bottom) \ 2'値が複数個あれば、さらにソートを呼び出すIf Top <> Bottom ThenCall MergeSort(Arr, Top, Middle)'左側のソートCall MergeSort(Arr, Middle +1, Bottom)'右側のソートEndIf'並び替え用のコピーを確保する。Dim Arr2 AsVariant
    Arr2 = Arr
    
    LC = Top   '左側のカウンタ
    RC = Middle +1'右側のカウンタ
    C = Top'メイン配列のカウンタ'(2)へつづく

そして、左右のグループの先頭のどちらが小さいか比較しながら、小さい方を取得し、取得した方のカウンタとメインのカウンタを進める。

'(2)'左と右の小さい方を取得しながら、コピーしたArr2からArrへ転記DoWhile LC <= Middle And RC <= Bottom
        If Arr2(LC)> Arr2(RC)Then
            Arr(C)= Arr2(RC)
            RC = RC +1Else
            Arr(C)= Arr2(LC)
            LC = LC +1EndIf
        C = C +1Loop'(3)へ続く

これだけだと、片側のグループが空になったら転記が終わってしまうので、それぞれの残りを出力するためのループを書く。
片側しか残らないはずなので、ループは片方しか実行されない。

'(3)'以下のループはどちらか片方しか実行されない。'右側の残りがあれば全て転記DoWhile RC <= Bottom
        Arr(C)= Arr2(RC)
        RC = RC +1
        C = C +1Loop'左側の残りがあれば全て転記DoWhile LC <= Middle
        Arr(C)= Arr2(LC)
        LC = LC +1
        C = C +1LoopEndSub

以上でマージソートのコードが完成。

完成コードの全体

ソートの呼び出しコード

Sub MStart()'配列Arrの準備Dim Arr()AsLongConstMin=1,Max=10ReDim Arr(MinToMax)'配列Arrにランダム数を格納しながら出力For i =LBound(Arr)ToUBound(Arr)Const LowerBound =1, UpperBound =100
        Arr(i)=Int((UpperBound - LowerBound +1)*Rnd+ LowerBound)
        Debug.Print i; ":"; Arr(i)Next
    Debug.PrintString(20,"-")'ソート呼び出しCall MergeSort(Arr,LBound(Arr),UBound(Arr))'ソート結果表示For i =LBound(Arr)ToUBound(Arr)
        Debug.Print i; ":"; Arr(i)NextEndSub

マージソート本体

Sub MergeSort(Arr, Top, Bottom)'分割点Middleを求めるDim Middle AsLong
    Middle =(Top + Bottom) \ 2'値が複数個あれば、さらにソートを呼び出すIf Top <> Bottom ThenCall MergeSort(Arr, Top, Middle)'左側のソートCall MergeSort(Arr, Middle +1, Bottom)'右側のソートEndIf'並び替え用のコピーを確保する。Dim Arr2 AsVariant
    Arr2 = Arr
    
    LC = Top   '左側のカウンタ
    RC = Middle +1'右側のカウンタ
    C = Top'メイン配列のカウンタ'左と右の小さい方を取得しながら、コピーしたArr2からArrへ転記DoWhile LC <= Middle And RC <= Bottom
        If Arr2(LC)> Arr2(RC)Then
            Arr(C)= Arr2(RC)
            RC = RC +1Else
            Arr(C)= Arr2(LC)
            LC = LC +1EndIf
        C = C +1Loop'以下のループはどちらか片方しか実行されない。'右側の残りがあれば全て転記DoWhile RC <= Bottom
        Arr(C)= Arr2(RC)
        RC = RC +1
        C = C +1Loop'左側の残りがあれば全て転記DoWhile LC <= Middle
        Arr(C)= Arr2(LC)
        LC = LC +1
        C = C +1LoopEndSub

ソートのテストコード

Arrが正しくソートされていない場合は中断モードに入るようなテストを書いてみた。

Sub TestSorted(Arr)For i =LBound(Arr)ToUBound(Arr)-1
        Debug.Assert Arr(i)<= Arr(i +1)NextEndSub

MStartの最後に呼び出すとちゃんとソートされているか分かるので便利。

マージソートの存在意義

実装が簡単なバブルソート、最も高速なクイックソート
最初は、これ以外に何が必要なんだろうかと思っていた。

マージソートの存在意義を理解するのは、ソートの安定性という概念である。
例えば、Excelの表を氏名で並び替えた後に都道府県で並び替えると、都道府県順で並び、各都道府県の中身は氏名順に並んでいる。
これが安定ソートだ。

クイックソートは確かに早いが、不安定ソートである。
もしExcelの並び替えがクイックソートで実装されていたら、都道府県順に並び替えたときに氏名の順は無視され、バラバラになってしまう。
安定版のクイックソートというのもあるにはあるけれど、複雑になり、あまりクイックじゃなくなってしまう。

バブルソートは安定ソートである。実装も簡単だ。しかし、データ量が増えると致命的に遅い。

マージソートは、安定かつそこそこ早いソートなのである。
デメリットとしてはややメモリを食うこと。

理由はコレだ。

'並び替え用のコピーを確保する。Dim Arr2 AsVariant
    Arr2 = Arr

都度破棄されるので、大した量ではないが、元の配列が相当に大きい場合は気にする必要が出てくるかもしれない。

まぁ、なんでも自前主義というのは良くないので、普段は素直にExcelのソート機能を使えば良いけれど、プログラミングのスキルアップにはちょうど良いので興味のある方は自分の得意な言語で実装してみると良いと思う。

それにVBAだと、オブジェクトのコレクションをソートするような場合は自分で実装する必要があるので全く無駄ということもない。


コスパが決め手のSCOTCH GRAIN ~8年履いた革靴、まだまだ現役。

$
0
0

3万円の靴

…というと、「高っ」という反応が殆どだ。
ところが、トータルのコスパを考えるとそうでもない。

ビジネスシューズは安いものなら3千円くらいから売っている。高くても1~2万くらいのものを買う人が多いと思う(知人がたまたまそうなのかもしれないが)

安い靴を履いていた頃

私が社会人1年目の頃に履いていたのは5~7千円クラスのもの。
5千円以下になると明らかに履き心地が悪く、私の場合は履き慣れるまでは踵が擦れたり、指が当たって痛かったりと色々と不都合が出る。ようやく足になじんだ時にはすでにボロボロだ。
新品の頃はゴム臭がきつく、古くなるとカビ臭くなる。1足を毎日履いて、特に磨いたりもしなかったので、3カ月くらいで履きつぶしていた覚えがある。

靴の製法の違い

さて、靴といっても色々な製法がある。違いは、表革と底をくっつける手法。
3千円~1万円くらいの靴は、殆どがセメンテッド製法といわれる、いわゆる接着剤でくっつける方式。
工場で大量生産できるが、ムレやすい、つまりカビが発生しやすい、底の張り替えができないというデメリットがあるらしい。
その代り、水が浸入しにくいので、雨の日には最適。
セメンテッド製法の靴がすべて安物というわけではない。

次に縫い靴。主にマッケイ製法と、グッドイヤーウェルト製法がある。
マッケイは表革と底を直接縫い合わせたものだ。縫い靴なので底を完全交換できるが、1回が限度とのこと。

グッドイヤーウェルト製法は、間に1枚細い革(ウェルト)をかませたもので、表革はウェルトと縫い合わされ、底もウェルトと縫い合わされている。そのため、何度も底の縫い替えが可能だ。縫い直すたびに痛むのはウェルトなので、ウェルトを交換してしまえばリセットできる。

8年目の靴

さて、これが購入から8年目を迎えた靴。
f:id:t-hom:20160409084421p:plain

といっても普段は3足でローテしているので、実質履いたのは2.5年分くらいだろうか。

まだまだ現役である。
グッドイヤーの靴は大事にメンテナンスすれば一生履けるらしい。
ただし、一番安いものでも3万くらいはする。

底も革なので、雨の日に履くとすぐすり減ってしまう。雨の日も結構使ったので、3年目くらいで1回オールソール(底の縫い替え)をしている。天気を選べば4年くらいは持つ。
オールソールのときにゴムか革かを選べるのだが、革の方が柔らかく、若干履き心地が良い気がするので、革底にした。
革底は擦り減りやすいので、コスパだけ考えるならゴム底が良い。

コストパフォーマンス

1万5千円くらいの靴を毎年買い替える場合と、3万円の靴を4年に1回底交換する場合で10年経過後を比較すると、ちょうど同額になる。
f:id:t-hom:20160409094609p:plain

初期費用の差が大きいので、さらに履き続けた場合は底交換の方が安くなる。
それに、1万円台の靴と3万円台の靴では、履き心地も全然違う。最初からどこも痛くならないし、2~3日履けば足になじみ、特有のゴム臭もせず、革の良い香りがする。

ハンパに高い靴を買うくらいなら、初期投資でドンと3万出しといたほうが安上がりで快適。

逆に3万以上になるとあとは趣味の世界なので、コスパは3万くらい(グッドイヤーの最安ライン)がベスト。

いきなり9万は厳しいので、社会人2年目の終わり頃までは1足だけ良い靴を買って、あとは履きつぶし用と使い分けていた。

メンテナンス

長持ちさせるにはシューキーパーは必須。
私が買ったのはコロンブス社のもの。

安物でも良いが、レッドシダー(赤杉)で作られたものを選ぶと良い。
レッドシダーは防虫、吸湿、消臭の効果があり、履き続けた靴が全然臭くならない。
ニスでコーティングされたものは見た目は良いけど、吸湿効果が無いので、おススメしない。

あとは適当にクリームいれて、サボらずに磨けばびっくりするくらい長持ちする。

私が使ってるメンテナンスグッズ

↓これは汚れ落としと革に潤いを与えるためのローション。まずこれでサッと靴を拭く。

それから、こんな感じの小さいブラシ↓で、

このクリームを靴全体に薄く塗って

豚毛ブラシで余計なクリームをサッと取り除く。

[ぺダック] pedag 豚毛ブラシ ArtNo.8152 (BrownF)

[ぺダック] pedag 豚毛ブラシ ArtNo.8152 (BrownF)

あとは適当な古着の切れ端かなんかで磨く。

失敗談

さて、実は4足持っていたのだが、うち2足は底交換の時期にきておりベンチ入り。また、1足はフィッティングミスで、履くと小指が痛むのでベンチ入り。そのため、最近は雨の日用の1足に負荷が集中していた。

雨の日用の靴も、スコッチグレイングッドイヤー(表革は撥水加工、底はゴム)なのだが、撥水加工されているせいか、磨いても他の靴ほど光らない。あまり磨き甲斐のないヤツなのだ。まぁただ雨でも履けるし、ゴム底だから摩耗しにくいので便利は便利。

雨の日用の靴はいつの間にか履きつぶし用になっていた。そして7年目に表革にヒビが入り、リタイア。シューキーパーも使わず、クリームも入れずに履いてたらさすがにそうなるわな。。

アウトレットで安く入手する

先週の日曜は関西空港の手前にある、りんくうタウンプレミアムアウトレットに行ってきた。

南海電車なんば駅から、1,000円分の買い物クーポン付で往復2,010円。
www.nankai.co.jp

私の住んでいる東大阪からだと結構電車賃がかかってしまうので、事前に価格帯を電話で問い合わせた。税別で1万6千(ゴム底)~3万(革底)くらいとのこと。

直営店だと一番安くても2万8千円なので、これは安い。
1万6000円で外羽ストレートチップを1足購入した。

※ただし、アウトレットだとかなり品揃えは限られるので、サイズとデザインを伝えて在庫具合を確認してから行くと良い。

履かなくなった靴を売る

私は他人の履いた靴を買うという感覚が分からないので、まさか売れると思ってなかったが、グッドイヤー製法の靴は修理して履き続けることができるため、状態が良ければ売れる。

売ったのは例のフィッティングミスした靴で、1年くらいは我慢して履いたけれど、それ以来たまにしか履かなくなったもの。
購入価格が28,000円、売却は3,900円だった。底が少しすり減っており、中に足形が残っていたので、値がついたことに驚いている。

ネットで見つけた意見としては、共用施設のサンダルと同じ感覚で別に気にならないとのこと。
むしろ靴下を履く分トイレのサンダルよりマシで、クリーニングされていれば何も問題ないとのこと。

中古靴は8~9千円で販売されているようなので人の履いた靴でも気にならない人はトライしてみても良いかもしれない。

ちなみに、私は他人の履いた靴を買うのは嫌だ。

余談:靴のデザイン

プレーン、ストレート、Uチップ、ウイングチップ、ツーシームなど色々あるけど、冠婚葬祭で最もベターで、ビジネスにも使えるオールラウンダーは黒の内羽根ストレートチップ一択。(紹介した8年目の靴と同じデザイン)
最初の一側は黒の内羽根ストレートにしよう。あとはお好みで。

スコッチグレインアシュランスがおススメである。アウトレットで自分にあうサイズがあればラッキーだ。

VBA 式と式の評価 ~数式だけが式じゃない~

$
0
0

プログラミングの世界では式の評価という言い回しが登場する。
VBAの書籍では見かけない言い回しであるが、関数やオブジェクトの動作について説明・理解するのに便利な概念なので覚えておくと良い。

といえばまず数式をイメージする方が多いだろう。あの忌まわしい数字と記号の羅列である。(数学好きな人、ごめんなさい。)
また、評価という言葉は、「高評価・低評価」など、一般的には物の良し悪しに言及する際に用いられるが、プログラミングにおいては値を確定するという意味で用いられる。

ふつう、数式に対して答えを導く操作は「計算」という。
なぜプログラミングでは式に対し「計算」という語の代わりに「評価」という語を用いるのだろうか。

その理由は、実はプログラミングにおける式は数式だけではないからだ。たとえばオブジェクト式・比較式・論理式・関数・変数・定数・リテラル・文字列の結合などもすべて式である。

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

式=数式のイメージが離れない方は、式という言葉の定義に立ち戻って考えてみよう。式とは、記号・数字などを用いて物事の関係・構造などを表したものをいう。プログラミング以外でも、たとえば分子構造を表す化学式は、計算しない式の代表選手である。

プログラミングにおいても数式に対してはより直観的な「計算」という語を用いる場合が多いが、「計算」という語は加減乗除のイメージが強く、文字列の結合やオブジェクト式などに用いるには違和感がある。そこで、数式以外でもより汎用的に使える「評価」という言い回しが用いられる。

式が評価されるプロセス

さて、実際に簡単なプログラムコードを使って評価のプロセスを見ていく。
繰り返しになるが、ここでいう評価とは何らかの操作を行って式の値を確定することである。何らかの操作とは、計算かもしれないし、結合かもしれないし、参照かもしれない。それらをひっくるめて言い表せる便利な言葉なのだ。

数式の評価

まずは以下の代入文を例に説明する。

x =(1+1)*2

xに4が代入されるという一目瞭然なことを、あえて細かくプロセスを追っていくが、難しいことではないので字面通りに注意深く読んでほしい。

代入文の形式は「変数(またはプロパティ) = 値」であるが、上記コードの場合、右辺にあるのは式である。式は評価されて値になるので「変数 = 式」という書き方も結局は「変数 = 値」と同等であると言える。

上記のコードではまず(1 + 1)が評価され2になる。次に2 * 2が評価され4になる。これでx = 4という「変数 = 値」形式のシンプルな代入文が完成した。このように、式はプログラムの内部で最初に評価され、値になった後に文法のパターンに当てはめて処理される。

関数の評価

先に述べたように、関数も式の一種である。以下の代入文を例に、関数の評価プロセスを追っていく。

m =Left("Hello, World!",7)&StrConv("vba",vbUpperCase)

Leftは文字列を左から切り取る関数、StrConvは文字列の形式を変更する関数である。

このコードではまずLeft関数が評価され、

m ="Hello, "&StrConv("vba",vbUpperCase)となる。

次にVBAの組み込み定数であるvbUpperCaseが評価され、

m ="Hello, "&StrConv("vba",1)となる。

次にStrConv関数が評価され、

m ="Hello, "&"VBA"となる。

次に文字列の結合が評価され、

m ="Hello, VBA"となる。

これで代入文の形式「変数 = 値」ができた。

オブジェクト式の評価

最後にオブジェクト式が評価されるプロセスを見ていく。オブジェクト式の形式は、「オブジェクト.プロパティ」または「オブジェクト.メソッド」である。たとえば以下の式の左辺は一見、「オブジェクト.オブジェクト.オブジェクト.プロパティ」のように見える。そのように理解しても当面は問題ないが、変則的なコードに出くわすと応用が利かないので、ここで詳しく説明しておく。

ThisWorkbook.Sheets(1).Range("A1").Value=1

実際にはまず、ThisWorkbook.Sheets(1)が「Workbookオブジェクト.Sheetsプロパティ」として評価され、Worksheetオブジェクトになる。

次に「Worksheetオブジェクト.Rangeプロパティ」が評価され、Rangeオブジェクトになる。

最後に「Rangeオブジェクト.Valueプロパティ」に1が代入されるというプロセスである。

このように評価プロセスに注目すれば、最後まで一貫して「オブジェクト.プロパティ」のシンプルな形式を保っていることが分かる。

さて、もう少し実践的なコードでオブジェクト式の評価を追ってみよう。

Sub test()Dim WB As Workbook
    Set WB = Workbooks.Open("C:\Work\test.xlsx")
    WB.Sheets(1).Range("A1").Value=1
    WB.Save
    WB.CloseEndSub


上記のコードを1行ずつ解説していく。

Dim WB As Workbook

Workbook型の変数WBが作成される。

Set WB = Workbooks.Open("C:\Work\test.xlsx")

WorkbooksオブジェクトのOpenメソッドが評価され、Workbookオブジェクトになる。そしてWBにWorkbookオブジェクトが代入される。

ここで、「おや?」と思われた方もいるだろう。プロパティが評価されるのは分かるが、メソッドが評価されるというのはどうも不可解に思われるかもしれない。実はメソッドには値を返すものと返さないものがある。再度繰り返すが、評価とは値を確定することである。つまり値を返すということは、そのメソッドは式として評価できるということである。

WB.Sheets(1).Range("A1").Value=1

変数WBが評価され、Workbookオブジェクトになる。Workbookオブジェクト.Sheetsプロパティが評価されてSheetオブジェクトになり、Sheetオブジェクト.Rangeプロパティが評価され、Rangeオブジェクトになる。そしてRangeオブジェクト.Valueプロパティに1が代入される。

WB.Save

変数WBが評価され、Workbookオブジェクトになる。Workbookオブジェクト.Saveメソッドが実行され、ブックが保存される。

WB.Close

変数WBが評価され、Workbookオブジェクトになる。Workbookオブジェクト.Closeメソッドが実行され、ブックが閉じられる。

サンプルコードの流れは以上である。非常に面倒くさい書き方をしてきたが、ここで注目したいことが2点ある。

まず1点目は、オブジェクト変数はあくまで入れ物であり、中身を評価されて初めてオブジェクトとして扱えるということ。2点目は、やはり一貫して「オブジェクト.プロパティ」、「オブジェクト.メソッド」という基本の形式が守られているということだ。

次に、先ほどと同じ動作をするコードを少し変わった形に書き直してみた。

Sub test2()Dim R As Range
    Set R = Workbooks.Open("C:\Work\test.xlsx").Sheets(1).Range("A1")
    R.Value=1
    R.Parent.Parent.Save
    R.Parent.Parent.CloseEndSub

オブジェクトを単に入れ子構造として理解していると、上記のコードは「Workbookオブジェクト.Openメソッド.Sheetオブジェクト.Rangeオブジェクト」のように見える。

「ちょっと待てよ、なんでメソッドの後にドットを続けてオブジェクトを書けるんだ」と思われるかもしれない。しかし先ほどのオブジェクト式の評価のプロセスを思い出せばコードの意味は簡単に理解することができる。

さて、詳細の説明は省略するが、最後のParentはあまり見かけないと思うので、ここで解説しておく。

Parentは、親要素を指定するプロパティである。Rが評価されるとRangeオブジェクトになり、Rangeオブジェクト.Parentプロパティが評価されるとRangeの親要素であるWorksheetオブジェクトになる。Worksheetオブジェクト.Parentプロパティが評価されるとWorksheetの親要素である。Workbookオブジェクトになる。最後にそれぞれWorkbookオブジェクトのSaveメソッド、Closeメソッドが実行されている。

おわりに

さて、今回は「式の評価」という言い回し・概念について詳しく解説した。これはつまり、コードがやっていることを的確に表現する言葉を手に入れたということである。

新しい概念を獲得し、言葉によってそれを脳内辞書にマッピングしておく。回りくどいようだが、私はそれが物事の理解への早道になると信じている。今回の記事が読者のプログラミング上達の一助になれば幸いである。

なお、今回最後に紹介したコードはあくまでオブジェクト式の評価のプロセスを説明するためのサンプルなので、実務で参考にすべきコードではないということを改めて断っておく。何事もシンプルが一番である。

以上

VBA Sheetsの後にドットを入れても入力候補の自動補完がされない理由

$
0
0

'============▼2016/5/31追記▼============

Workbooksオブジェクトは存在しないと書きましたが、Microsoft MVPの伊藤さんから誤りを指摘いただいたので修正しました。伊藤さん、ありがとうございます。

【参考】
SheetとSheetsとWorksheetとWorksheets:エクセルマクロ・Excel VBAの使い方-Worksheet・Chartオブジェクト

'============▲20165/31追記▲============

表題の件、いつかの記事で書いたような気はするが、ブログ内を検索してもうまく見つからなかったので改めて紹介する。

ご存じのとおり、VBAにはオブジェクトに続けてドットを入力すると、入力候補を表示してくれる機能が備わっている。
↓こういうやつだ。
f:id:t-hom:20160528203051p:plain

これはMicrosoftVBエディタに搭載しているインテリセンス(IntelliSense)という機能の一部である。

ならばThisWorkbook.Sheets(1).と入力すればRangeやCellsなどのプロパティが自動で候補に出てきてもよさそうなものだが、実際には何も表示されず、Rangeと手打ちするはめになる。さらにRangeに対するValueやInterior.Colorなどもすべて手入力である。

これには理由がある。

なぜ自動補完が利かないのか

実はExcelシートの種類はワークシートだけではない。
適当なシートのツマミを右クリックして挿入メニューを選ぶと、4種類のシート種類が出てくる。
f:id:t-hom:20160528203852p:plain

たとえばグラフシートにはRangeやCellsなどのプロパティが存在しない。
VBAコンパイラはプログラムが記述される段階ではSheets(1)がどのタイプのシートか判別できないため、一旦汎用的に利用できるObject型として取り扱う。そのため、前述の入力補完が利用できないのだ。

VBAコンパイラにとってObject型というのは実際に動かしてみるまでどんなオブジェクトなのかわからない謎のオブジェクトである。(それゆえに何のオブジェクトでも入れられるのだが)

謎のオブジェクトが持つRangeプロパティは、ExcelのRangeオブジェクトであるとは限らない※。オブジェクト型であるかどうかもわからず、ただの値かもしれないし、そんなプロパティ自体そもそも存在しないかもしれないのだ。
※たとえばWordのDocumentオブジェクトもRangeプロパティを持っており、こちらはExcelのRangeとは別物である。

VBAコンパイラがオブジェクトの中身を保障できない以上、馴染みのRangeという名称が登場してもそのRangeがValueやInteriorなどのプロパティを持っているなどと、どうして言い切れるだろうか。
だから、ValueやInterior.Colorなども候補表示できない。だいいち、候補に表示しておいてそこから選んだ結果がエラーになったらみんな怒るだろう。
VBAエディタはそんな無責任なことはしないのだ。

VBAコンパイラ型を認識している場合、間違った記述をすると実行の前のコンパイルでエラーになる。下図では赤線のThisWorkbookがWorkbook型であると認識されているため、「Workbook型にそんなプロパティはないよ」というのを予め教えてくれるわけだ。
f:id:t-hom:20160528210758p:plain

ところが、VBAコンパイラ型を認識していない場合、間違った記述をしても実行するまではエラーにならない。
以下の場合はコンパイルエラーではなく、実行時エラーになる。
f:id:t-hom:20160528211015p:plain

Ahahahahahahhaなんていうプロパティは一目でオカシイと分かりそうなものだが、コンパイル段階で指摘してくれないのはなぜだろうか。

これは、赤線の部分がObject型(つまり謎のオブジェクト)として認識されるため、Ahahahahahahhaというプロパティが存在するか存在しないかはっきりしないからだ。

いや、まあ、たぶんオカシイんだけどその気になれば自作クラスにAhahahahahahhaって名前のプロパティを実装することはできるので、VBエディタは無責任にコンパイル段階でオカシイなどと指摘するわけにいかない。

で、実行時に「試してみたけど(やっぱり)そんなプロパティなかったよ」ってことである。

入力候補を自動補完させる方法

変数を使用する方法

Object型ではなくWorksheet型として認識させれば自動補完されるので、Worksheet型変数を作って代入してからその変数を操作すれば良い。

下図のようにWorksheet型の変数WSにセットすれば、最後までバッチリ自動補完が切れることがない。
f:id:t-hom:20160528211916p:plain

これの応用で、新規ブックを開く際に最初からシート型オブジェクトに対象ブックのシートをセットしてしまうテクニックがある。
以下がそのコードである。

Sub Sample()Dim WS As Worksheet
    Set WS = Workbooks.Open("C:\Work\test.xlsx").Sheets(1)
    WS.Range("A1").Value=1
    WS.Parent.Save
    WS.Parent.CloseEndSub

ブックを保存やクローズしたいときは、シートのParentプロパティで操作できる。
※シートに対していろいろ操作する場合は便利だが、Parentに対しては入力補完が利かないので、上の例のようにRangeを1ついじる程度ならブックを開いたほうが効率が良い。

オブジェクト名を使用する方法

または、最初からシートオブジェクトを指定してやる方法もある。
下図の赤線で示した表示がオブジェクト名である。
f:id:t-hom:20160529021214p:plain
オブジェクト名はプロパティから青枠の箇所で変更できる。また、緑線で示した個所はExcel上で表示・変更できるシート名である。

この方法は自分のブックで固定のシート(マクロ中で削除されたり追加されたりしない)を扱う場合に便利である。
マクロで開いた別ブックのシートを参照するような場合には使えない。

【参考記事】
thom.hateblo.jp

WorksheetsプロパティとSheetsプロパティの違い

Workbookオブジェクトには、Sheetsプロパティの他にWorksheetsプロパティも存在する。
これを使えば最初からWorksheet型になりそうなものであるが、残念ながらそうはならない。

Sheetsプロパティはすべての種類のシートを含むSheetsオブジェクトを返す。
Worksheetsプロパティはすべてのワークシートを含むSheetsオブジェクトを返す。

つまり、Worksheetsプロパティはワークシート型のシートだけを返すのだが、どちらもSheetsオブジェクトなのだ。
Sheetsオブジェクトのデフォルトプロパティに引数としてシート番号を渡すと、シートがObject型として返るという仕組みである。

以下のコードで、もう少し具体的にみていこう。

ThisWorkbook.Sheets(1).Range("A1").Value=1

上記コードはまずThisWorkbookが評価されてWorkbookオブジェクトになる。

【参考】評価という言葉に引っかかったらこちら。
thom.hateblo.jp

Workbookオブジェクト.Sheetsプロパティが評価され、Sheetsオブジェクトになる。
Sheets(シート番号)は実際には、Sheets.Item(シート番号)の短縮系である。

短縮せずに書くならこうなる。

ThisWorkbook.Sheets.Item(1).Range("A1").Value=1

※厳密にはItemではなく[_Default]という特殊なプロパティなのだが、話がややこしくなるのでここではItemとしておく。

さて、Sheetsオブジェクト.Itemプロパティ(1)が評価されると1番目のシートオブジェクトになる。しかし、Worksheetオブジェクトになるか、それともChartオブジェクト(グラフシート)になるかは実行してみるまでわからないので、Object型として扱われる。

Worksheetsプロパティを使用した場合はワークシート型のシートだけが選別されるが、結局Sheetsオブジェクトを返す時点で、そのデフォルトプロパティがシートをObject型として扱うため、以降の自動補完はされない。

プロパティ名と、そのプロパティが返すオブジェクト名の関係はちょっとややこしいので以下にまとめておく。

  • SheetsプロパティはSheetsオブジェクトを返す。
  • WorksheetsプロパティもSheetsオブジェクトを返す。
  • Worksheetsという名称のオブジェクトは存在しない。
  • Worksheetオブジェクトは存在するが、Sheetオブジェクトは存在しない。

※sの有無に気を付けて。

以上

VBA Cellsの正体は、全セルを包むRangeオブジェクトである

$
0
0

セルを行・列の数値で指定するCellsはRangeと並んで基礎中の基礎であるが、その正体を知る者は少ない。

まあ別に知ったところで使い方が変わるわけではないのだが、知っておいて損になるものではないので紹介しよう。

今回も評価という用語を用いるので、意味が分からない方はこちらを参照。
thom.hateblo.jp

さて、次のコードを例に評価プロセスを追ってみよう。

ThisWorkbook.Sheets(1).Cells(1,1).Value="a"

上記はいろいろと省略されているので、まずフルバージョンを記述する。

ThisWorkbook.Sheets.Item(1).Cells.Item(1,1).Value="a"

※実際にはItemプロパティではなく[_Default]プロパティであるが、話がややこしくなるのでここではItemとしておく。

  1. ThisWorkbookが評価され、Workbookオブジェクトになる。
  2. Workbookオブジェクト.Sheetsが評価され、Sheetsオブジェクトになる。
  3. Sheetsオブジェクト.Itemプロパティ(1)が評価され、Sheetオブジェクトになる。
  4. Sheetオブジェクト.Cellsプロパティが評価され、Rangeオブジェクトになる。
  5. Rangeオブジェクト.Item(1, 1)が評価され、Rangeオブジェクトになる。
  6. Rangeオブジェクト.Valueに"a"が代入される。

評価のプロセスは以上である。

上記の4と5のステップについて、混乱された方がいるかもしれないのでもう少し補足する。

実は、Cellsに渡す行・列の値というのはCellsプロパティの引数ではない。
Cellsはあくまでシートの全セルを含むRangeオブジェクトを返すプロパティで、Cellsそのものに引数は存在しない。
Cellsプロパティが評価された結果、全セルを含むRangeオブジェクトが戻り値として返り、そのRangeオブジェクトのデフォルトプロパティが引数(1, 1)をとる。

これが、Cells(1, 1)の内部動作である。

その証拠にクイックヒントを見ると、Rangeプロパティは直接引数を求められるのに対し、Cellsプロパティでは_Defaultとなっている。
f:id:t-hom:20160530204510p:plain

Rangeオブジェクトのデフォルトプロパティに(1, 1)を渡すというのもややこしい話である。
安直にRange(1, 1)と書くと失敗する。なぜだろうか。
f:id:t-hom:20160530204912p:plain

実は、シートオブジェクト.RangeというのはあくまでRangeプロパティであり、評価されて初めてRangeオブジェクトになる。
具体的に以下のコードでRangeプロパティの場合の評価プロセスを見ていこう。

ThisWorkbook.Sheets(1).Range("A1").Value=1
  1. ThisWorkbookが評価され、Workbookオブジェクトになる。
  2. Workbookオブジェクト.Sheetsが評価され、Sheetsオブジェクトになる。
  3. Sheetsオブジェクト.Itemプロパティ(1)が評価され、Sheetオブジェクトになる。
  4. Sheetオブジェクト.Rangeプロパティ("A1")が評価され、Rangeオブジェクトになる。
  5. Rangeオブジェクト.Valueに"a"が代入される。

以上がRangeプロパティを使った場合の評価プロセスである。

Cellsプロパティを使った場合、Cellsプロパティが単独で評価され、全セルを含むRangeオブジェクトになった。
Rangeプロパティを使った場合、引数"A1"と合わせて評価され、A1セルを指すRangeオブジェクトになった。

つまりRangeプロパティは、引数をとってRangeオブジェクトを返すプロパティなのである。

では次に、Rangeオブジェクトのデフォルトプロパティに(1, 1)を渡す処理を、Rangeプロパティを使ってやってみよう。

奇妙な書き方ではあるが、次のコードは正常に動作する。

ThisWorkbook.Sheets(1).Range("A1")(1,1).Value="a"

これはRangeオブジェクトのItemプロパティに(1, 1)を渡す、つまりCellsの時と同じ処理である。

Range("A1")の後に続けてカッコを書くと、Cellsのときと同じくクイックヒントに_Defaultが表示される。
f:id:t-hom:20160530210230p:plain

Cellsが全セルを指すRangeオブジェクトを返すなら、たとえばCells(2, 3)と書くのはつまり、Range("A1:XFD1048576")(2, 3)と書くのと同じ意味になる。実際にその通りなのだ。

以下に、いろんな方法で全セルを指定してみた。

Sub AllSame()
    Debug.Print Cells.Address
    Debug.Print Range("A1:XFD1048576").Address
    Debug.Print Range("1:1048576").Address
    Debug.Print Range("A:XFD").Address
EndSub

アドレスはどれも同じ、$1:$1048576が表示される。

では、Rangeオブジェクトの左上がA1じゃない場合にデフォルトプロパティに行・列を与えたらはどうなるんだろうか。

これは、Rangeオブジェクト範囲の一番左上が1, 1となる仕組みのようだ。
たとえば以下のように書いた場合はしたがって、C3に"a"が代入される。

Range("C3:K11")(1,1)="a"

また、左上の開始セルだけあれば他は省略しても動作するようで、以下のような記述でも、まるでOffsetプロパティのように機能する。

Range("C3")(5,5)="a"

※Offsetは自セルの開始点が0, 0なのに対し、Itemプロパティは自セルが1, 1である。

ということで、今回はCellsプロパティが全セルを含むRangeオブジェクトであることがお分かりいただけたと思う。

もう少しきれいに結論を纏めたかったけど、〆の文章を考えてるうちに眠くなってきたので今日はこのへんで失礼つかまつる。

VBA なぜ引数が一つのときは、カッコを付けても付けなくてもプロシージャを呼び出せるのか

$
0
0

初心者の方はプロシージャ呼び出しの時にカッコを付けるかどうか迷う方もいると思う。

基本ルールは、Callを書く場合と、戻り値を利用する場合にカッコを付け、それ以外では付けてはいけないである。

以下のように、不要なカッコを付けると、コンパイラに叱られてしまう。

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

しかし困ったことに、この基本ルールには例外がある。

引数がひとつなら、単体でカッコを付けても呼び出せてしまうのだ。

たとえば以下のようなケースである。

Sub Sample()
    x =2
    Twice (x)EndSubSub Twice(a)MsgBox a *2EndSub

基本ルールに則るとTwiceプロシージャを呼び出すのに、「Twice x」とカッコ無しで書かないといけないのではないか。

人に教えるときに非常にやりづらい。まったく、困った例外を作ってくれたものだ。

…と、最近まで考えていたのだが、実は単純に例外ルールというわけでもないようだ。

実は引数が二つ以上でも、個別の引数に対してカッコを付けることはできる。

こんな風に。

Sub Sample()
    x =2
    y =3Add(x),(y)EndSubSubAdd(a, b)MsgBox a + b
EndSub

つまり、前述のTwice (x)は、個別の引数xに対するカッコであり、引数全体を囲むカッコではないのだ。

呼び出しのパターン

以下にカッコの有無でパターンを洗い出してみた。

【個別引数にカッコをつけて呼び出し】

[1] プロシージャ名 (引数1)
[2] プロシージャ名 (引数1), (引数2)
[3] プロシージャ名 (引数1), (引数2), (引数3)

【個別引数にカッコをつけずに呼び出し】

[4] プロシージャ名 引数1
[5] プロシージャ名 引数1, 引数2
[6] プロシージャ名 引数1, 引数2, 引数3

【引数全体にカッコをつけて呼び出し】

[7] Call プロシージャ名(引数1)
[8] Call プロシージャ名(引数1, 引数2)
[9] Call プロシージャ名(引数1, 引数2, 引数3)

【個別引数と、引数全体にカッコをつけて呼び出し】

[10] Call プロシージャ名 ((引数1))
[11] Call プロシージャ名 ((引数1), (引数2))
[12] Call プロシージャ名 ((引数1), (引数2), (引数3))

【NGパターン】

[13] プロシージャ名 (引数1, 引数2)
[14] Call プロシージャ名 引数1, 引数2

こうして並べてみると、例外などではなく、一本きれいなルールが通っているのがわかる。

全体カッコの意味

以下のように全体カッコを外して書いても文法上は一見問題なさそうに見える。

Call プロシージャ名 引数1, 引数2, 引数3

ではVBAコンパイラはなぜこれをエラーとみなすのだろうか。
Callだと問題が見えてこないので、基本ルールの「戻り値を利用する場合」で検証してみよう。

以下の命令をどう解釈すべきだろうか。

プロシージャA プロシージャB 引数1, 引数2, 引数3

上記のようにカッコがない場合、解釈が次の3通りに分かれてしまう。

[1] プロシージャA プロシージャB(引数1), 引数2, 引数3
[2] プロシージャA プロシージャB(引数1, 引数2), 引数3
[3] プロシージャA プロシージャB(引数1, 引数2, 引数3)

引数全体にカッコを付けるのは「ここまでがプロシージャXの引数なので、先に評価してくださいね」とコンパイラに教えてあげる意味があるのだ。

Callの場合は人が見たら問題なさそうに見えるが、コンパイラはプロシージャの引数の範囲を柔軟に判定してくれるということはないので、やはりカッコが必要である。

個別カッコの意味

個別の引数につけるカッコにも非常に重要な意味がある。
VBAはデフォルトでは、プロシージャへの引数を参照渡しする。

下記のようにカッコなしでTwice aと書くと、変数aの参照が渡され、それが2倍されるので、最終的に20が表示される。

Sub Sample()
    a =10
    Twice a
    MsgBox a
EndSubSub Twice(x)
    x = x *2EndSub


ところが、個別のカッコをつけると引数が値渡しになるのだ。
次のコードではTwiceへは単に10が渡されるので、Twiceプロシージャでそれを2倍したところでSampleプロシージャの変数aは相変わらず10のままである。

Sub Sample()
    a =10
    Twice (a)MsgBox a
EndSubSub Twice(x)
    x = x *2EndSub

ただし、Call Twice(a)とすると、そのカッコは引数全体を囲むカッコとみなされ、aは参照渡しになる。

Sub Sample()
    a =10Call Twice(a)MsgBox a
EndSubSub Twice(x)
    x = x *2EndSub

さらにカッコで囲むと、外側のカッコは引数全体のカッコ、内側のカッコは値渡しを表す個別カッコになる。

Sub Sample()
    a =10Call Twice((a))MsgBox a
EndSubSub Twice(x)
    x = x *2EndSub

整理すると、以下の3パターンでは、[1]と[2]は参照渡しになるが、[3]は値渡しになってしまう。
[1] プロシージャ a
[2] Call プロシージャ(a)
[3] プロシージャ (a)

パターン[3]について、「例外として引数が一つの場合はカッコを付けることができる」という理解では、思わぬバグにつながることがあるということだ。

なぜ個別カッコは値渡しなのか

ふつう、値渡しをするには呼び出されるプロシージャ側でByValを使用する。
なんで個別カッコが値渡しになるという変なルールがあるのだろうか。

これはあくまで私の推測であるが、これも例外的なカッコの使い方ではなく、「カッコ内は先に評価する」という基本ルールに則って、結果的に値渡しになっているだけではないかと思う。

つまり、たとえば (5 + 2) * 3 と書いたときに5 + 2が先に評価されて7になるのと同じ理屈である。

先ほどのコードでは、(a)が評価されて10になった後に渡されるから、値渡しになる。

そう考えると不可解で例外的に思えるルールも、実は例外ではなく評価の大原則に基づいた至極当然な結果であるといえる。

あくまで推測であるが。

あとがき

プロシージャ (a)と書くと、そのカッコは引数全体のカッコではなく個別カッコなので値渡しになってしまう。
呼び出し先がByRefで待ち受けていても、値渡しになってしまう。
なぜならすでに呼び出し元で先に評価されて、値になってしまっているから。

参照渡しを想定している場合は、個別カッコを付けるとバグの元になるということをしっかり押さえておきたい。

今回の事実をテクニックとして使うのはお勧めしない。
基本は「Callを書く場合と、戻り値を利用する場合にカッコを付け、それ以外では(たとえ付けられても)付けてはいけない」という原則に忠実に、値渡しか参照渡しかは呼び出し先のプロシージャのByValまたはByRefで制御するのが良い。

VBAの書籍やネットの情報ではで引数のカッコについて細部まで説明しているものが見つけられなかった。
「例外」として説明しているか、そもそも基本ルール以外の説明がないケースが多い。

※ちゃんと探せばあるのかもしれない。

私はVBAではなく、WSHの書籍で今回の事実を知った。

WSHクイックリファレンス 第2版

WSHクイックリファレンス 第2版

さすがオライリー
ちなみに購入目的は、VBSをやりたかったわけではなく、FileSystemObjectやDictionaryなど、VBAからも便利に使用できるMicrosoft Scripting Runtime系のオブジェクト仕様を抑えておくため。

VBAのテクニック集などにも申し訳程度にFileSystemObjectの使用方法は掲載されているが、Microsoft Scripting RuntimeはWSHが本家なので、詳しく仕様を知りたいときはこちらがおすすめ。

執筆環境のグレードアップ

$
0
0

パームレスト

私は普段からプログラミングの学習やブログの執筆などでキーボードを良く使う。
それで結構いい値段のキーボードを使っているのだが、パソコン専用のローデスクを使うようになってから今一つ入力しづらい。
f:id:t-hom:20160612202810p:plain
スライド式のキーボードテーブルは、これ以上引き出すことができないため、腕を置く場所がない。仕方なくキーボードの下のわずかなスペースに手を置いて入力してたのだが、上端にあるキーが押しづらく、入力のたびに若干手を持ち上げる必要がある。ネットサーフィンくらいならそれでも十分だけれど、長文を書いていると結構疲れるのだ。

そこで、こちらのパームレストを買ってきた。

使用しているキーボードのメーカーの純正品である。使っているキーボードに合わせて作られたものなので高さがちょうど良い。
結構値は張るけどネットで評判が良く、天然木ならではの高級感もあるのでまあ仕方がない。

以下、設置してみたところ。
f:id:t-hom:20160612202852p:plain
ぎりぎりキーボードテーブルに収まった。

パームレストはクッション性を売りにした製品が多いのだが、モノによっては安定感がなくて入力しづらいという意見もあるが、こちらの製品は言ってしまえばただの木の板なので安定感は抜群。さらに、パームレストを滑るように手を動かすことで、遠くのキーでも楽々押せてしまう。

不便に慣れてしまうとなかなか気づかなくなるものだが、実はそのせいで肩が凝ったり入力の効率が落ちているかもしれない。一度、パームレストの代わりに本か何かを置いてみて、入力が楽になるようだったら購入を検討しても良いかもしれない。

パとバを間違えないフォント

さて、次は快適なフォントである。
私は日本語を「かな入力」でタイプしているので、濁点と半濁点を押し間違えることがある。先日もプログラムと書こうとしてブログラムと書いてしまった。

ローマ字入力なら間違えようがないのだが、一打で一文字の「かな入力」は慣れるとすこぶる快適で、今さらローマ字に戻れない。

そこで、半濁点がかなり大きい「Migu 1M」というフォントをダウンロードしてきた。
Miguフォント : M+とIPAの合成フォント

f:id:t-hom:20160612205224p:plain
これなら見間違えようがない。

これをエディタのフォントに設定する。
※私はVimを使用するので、以下のように設定した。

set guifont=Migu_1M:h12

アンチエイリアス

しかし、これだけだとVimで表示したときに表示が汚い。
こんな感じ。
f:id:t-hom:20160612203523p:plain

拡大してみると、線の太さがまちまちで醜い。
f:id:t-hom:20160612203728p:plain

そこで以下の記事を参考にさせていただき、Vimを綺麗に変身させる。
74th.hateblo.jp

できたのがこちら。
f:id:t-hom:20160612204128p:plain

拡大しても、線の太さが均一で美しい。
f:id:t-hom:20160612204503p:plain

なんか前にもこんな設定をした気がするのだが、メインPCが壊れてしまったのでどちらにしても再度環境構築である。

VBA Rangeでセル範囲に付けた名前を参照

$
0
0

Excelでは、セルに任意の名前を付けることができる。
任意のセルを選択した状態で、数式バーの左にある名前ボックスに名前を入れてEnterで確定させるだけだ。
f:id:t-hom:20160707232924p:plain

セル範囲に対しても同様の操作ができる。
f:id:t-hom:20160707233152p:plain

付けた名前は数式で参照させることができる。
f:id:t-hom:20160707233244p:plain

行や列を挿入すると、名前が指すセル範囲も自動的に移動してくれる。
f:id:t-hom:20160707233358p:plain

一度つけた名前を消したり範囲を変更するには数式タブから名前の管理を開く。
f:id:t-hom:20160707233607p:plain

すると次のようなダイアログで編集や削除ができる。
f:id:t-hom:20160707233710p:plain

さて、前置きが長くなったが、実はこの「名前」はVBAからシートのRangeプロパティでも参照できる。

標準モジュールに次のコードを入力して実行すると、

Sub hoge()Dim r As Range
    ForEach r In Range("データ範囲")
        Debug.Print r.ValueNextEndSub

このとおり。
f:id:t-hom:20160707234103p:plain

さて、データの個数が固定されている場合はこれで十分だが、可変の場合はいちいち名前を変更するのはわずらわしい。
そんな時はテーブル機能を利用する。

ホームタブからテーブルとして書式設定を選択し、
f:id:t-hom:20160707235110p:plain

データ範囲を選択してOKをクリックする。
f:id:t-hom:20160707235234p:plain

するとテーブルが作成される。
テーブル内のセルを選択すると、デザインタブが表示されるので、そこでデザインを変更したりテーブルの名前を変えたりできる。
f:id:t-hom:20160707235549p:plain

デザインタブのプロパティグループにテーブル名があるので、ここを編集することでテーブル名を変更できる。
f:id:t-hom:20160707235706p:plain

そしてこのテーブル名も先ほどの「名前」と同じようにRangeプロパティで参照できるのだ。

Sub hoge()Dim r As Range
    ForEach r In Range("テーブル1")
        Debug.Print r.ValueNextEndSub

しかもデータを追記すれば自動的に拡張される。

テーブルに対してVBAでもう少し高度な操作をしようと思ったらListObjectを使用することになるが、Rangeなら気軽に使えて良い。

ただし、「名前」や「テーブル名」はユーザーが変更可能な領域なので、どんなブックにでも使えるテクニックというわけではない。
利用シチュエーションによって最適なセルの指定方法は違うので、色々な方法を知って使いこなせるようになりたい。


VBA インターフェースを活用してコードの抽象度を上げるテクニック

$
0
0

以下のような注文票にマクロでデータを記入することを考えてみる。
f:id:t-hom:20160714200309p:plain

人間が記入する場合は、次のように考えながら記入していくだろう。
「注文票の、ひとつ目の商品名は"パソコン"で、単価は10万円、個数は40個。二つ目の商品は"複合機"で、単価は150万円、個数は2個。」

これをVBA風に書くと、こうなる。

Sub注文記入()With注文票.注文(1).品名 ="パソコン".単価 =100000.個数 =40EndWithWith注文票.注文(2).品名 ="複合機".単価 =1500000.個数 =2EndWithEndSub

やりたいことがそのまま表記され、極めてシンプルなコードである。

が、もちろん普通はこんな風には書けない。

それを何とかしてしまおうというのが今回のテーマ。

普通にマクロを組む場合

まず普通にマクロを組む場合について考えてみよう。

VBAのコードに落とし込むには、

「まず記入するのはこのブックのひとつ目のシートだから、ThisWorkbook.Sheets(1)、ひとつ目の商品があるのはシートの5行目、品名はC列だから、Range("C5")に"パソコン"と記入し、単価はD列、個数はE列だからRange("D5")に100000、Range("E5")に40と記入する。」

という風により具体的に考える必要が出てくる。

具体的なコードは次のようになる。

Sub注文記入()Dim RecordRow AsLong
    RecordRow =5With ThisWorkbook.Sheets(1).Range("C"& RecordRow).Value="パソコン".Range("D"& RecordRow).Value=100000.Range("E"& RecordRow).Value=40EndWith
    
    RecordRow =6With ThisWorkbook.Sheets(1).Range("C"& RecordRow).Value="複合機".Range("D"& RecordRow).Value=1500000.Range("E"& RecordRow).Value=2EndWithEndSub

VBAに慣れた人ならすこぶる簡単なコードに見えるだろう。
しかし実際には脳内では、「人間がやりたいこと」と、「VBAでの具体的なコード」の対応表が作られており、いくらVBAが得意な人でもコードが複雑になってくると脳内の対応表がオーバーフローを起こす。
あるいは久々に読んでみたら、そのコードが何をしたかったものなのかさっぱり思い出せないということがよくある。

コードの抽象度

人間にとって次の指示は非常に具体的である。
「注文票の、ひとつ目の商品名は"パソコン"で、単価は10万円、個数は40個。二つ目の商品は"複合機"で、単価は150万円、個数は2個。」

しかしコンピューターにとっては上記の指示は非常に抽象的である。
もしそんな指示を与えたら、「注文票って何?どのブックの何番目のシートのこと?ひとつ目ってシートの1行目でいいの?、商品名の列は?ってかそもそも列なの?もっと具体的に言って!!!」という具合に混乱(エラー)を起こす。

普通、抽象的な話というのは分かりにくいものなのだが、VBAのコードは人間にとっては具体的過ぎて難しいので、「抽象的なコードのほうがわかりやすい」という反転現象が起こる。

【参考】
thom.hateblo.jp

抽象的な書き方

冒頭で紹介したVBA風のコードであるが、実はちゃんとしたVBAコードである。
これ単体ではもちろん動かないが、他にいろいろとコードを書いて準備すれば、以下は有効なコードになる。

Sub注文記入()With注文票.注文(1).品名 ="パソコン".単価 =100000.個数 =40EndWithWith注文票.注文(2).品名 ="複合機".単価 =1500000.個数 =2EndWithEndSub

以下、その準備について記す。

まずは注文票シートのオブジェクト名を「注文票」に変更する。
f:id:t-hom:20160714203955p:plain

【参考】
thom.hateblo.jp

次に、クラスモジュールを挿入し、オブジェクト名を「I注文レコード」、Instancingを「2 - PublicNotCreatable」にする。
f:id:t-hom:20160714204149p:plain

「I注文レコード」のIはインターフェースの意味である。
インターフェースは、メソッドやプロパティの存在を担保するためのモジュールで、プロシージャの形式だけを記入して中身は記入しない。

I注文レコードには、次のコードを記入しておく。
この時点ではインターフェースを使用する意味が分からないかもしれないが、最後に説明するので今は気にしないでほしい。

PropertyLet品名(x AsString)EndPropertyPropertyLet個数(x AsLong)EndPropertyPropertyLet単価(x AsLong)EndPropertySubClear()EndSub

次にもう一つクラスモジュールを作成し、こちらはオブジェクト名を「注文レコード」、Instancingはデフォルトの「1 - Private」のままにしておく。

注文レコードのコードは次のとおり。

Implements I注文レコード
Public ws As Worksheet
Public RecordRow AsLongPropertyLet Number(N AsLong)
    RecordRow = N +4EndPropertyPropertyLet I注文レコード_品名(x AsString)
    ws.Range("C"& RecordRow).Value= x
EndPropertyPropertyLet I注文レコード_個数(x AsLong)
    ws.Range("E"& RecordRow).Value= x
EndPropertyPropertyLet I注文レコード_単価(x AsLong)
    ws.Range("D"& RecordRow).Value= x
EndPropertySub I注文レコード_Clear()Me.I注文レコード_個数 =0Me.I注文レコード_単価 =0Me.I注文レコード_品名 =""EndSub

【参考】
thom.hateblo.jp

そして最後に「注文票」シートのコードに次のコードを記入する。

PropertyGet注文(N)As I注文レコード
    Dim O AsNew注文レコード
    Set O.ws =Me
    O.Number = N
    Set注文 = O
EndProperty

ここで注目してほしいのは、注文票シートのプロパティ「注文」が、「I注文レコード」インターフェースを返す点である。
これは、InstancingがPrivateなオブジェクトをパブリックプロパティの引数や戻り値にすることはできず、エラーとなるためである。

具体的なオブジェクトは「注文レコード」型として作成している。
InstancingがPublicNotCreatableなクラスはNewでインスタンスを作ることができない。

そのためこのように自作クラスを引数や戻り値に使おうと思ったらインターフェースを使わざるを得ないのである。

さて、ここまで準備ができたら、あとは標準モジュールに冒頭のコードを書くだけだ。

Sub注文記入()With注文票.注文(1).品名 ="パソコン".単価 =100000.個数 =40EndWithWith注文票.注文(2).品名 ="複合機".単価 =1500000.個数 =2EndWithEndSub

このコードは次のように動作する。

まず「注文票」の「注文」プロパティ(以下のコード)に1が渡される。

PropertyGet注文(N)As I注文レコード
    Dim O AsNew注文レコード
    Set O.ws =Me
    O.Number = N
    Set注文 = O
EndProperty

すると、「注文レコード」オブジェクトが生成され、Numberプロパティが1にセットされる。
注文レコードクラス内では、Numberプロパティは次のように定義されている。

PropertyLet Number(N AsLong)
    RecordRow = N +4EndProperty

つまり1が渡されると、4が足され、5になる。

「ひとつ目の商品があるのはシートの5行目」というのを思い出してほしい。
ここでは、「何個目」という人間の管理する情報を「何行目」というExcelの管理する情報に変更している。

次に「.品名 = "パソコン"」である。
これはWith文の中にあるので、実際は「注文票.注文(1).品名 = "パソコン"」と同じ。

「I注文レコード」インターフェースの品名プロパティが参照されると、具体的にはそれが指す「注文レコード」オブジェクトの次のコードが呼ばれる。

PropertyLet I注文レコード_品名(x AsString)
    ws.Range("C"& RecordRow).Value= x
EndProperty

ここでwsは「注文票」シートを指す。
※「注文票」の「注文」プロパティで「Set O.ws = Me」として自分自身を渡している。

RecordRowは先ほど5がセットされているので、注文票のRange("C5")に引数xに入った"パソコン"が入力される。

単価や個数についても類似の動作なので割愛する。

以上が抽象的な書き方を実現するための、具体的なコードの準備である。

余計複雑になった気がする?

そのとおり。メインのコードをスッキリ書くために、周辺のコードは相当に複雑になっている。
しかし重要なのは、メインコードのロジックを書く際、その複雑さは脇に置いておけるということだ。

一度仕組みを作ってしまえば、シート番号や記入する行、品名を記入するべき列など、具体的な情報は一切考慮する必要はない。

もしこれから書くコードが50ステップにも満たない簡単なものであれば、こうしたテクニックは余計な複雑さを持ち込むだけであまり役に立たないだろう。

しかし、相当に複雑なロジックの巨大なマクロを書こうと思ったとき、複雑さを分離してメインロジックに集中できる仕組みは有難い。

あとがき

VBAのクラスモジュールは、以下のようなジレンマを抱えているように思う。
「メリットが分からないから使わない→使わないからメリットが分からない」
「使われないから書籍などで紹介されない→書籍などで紹介されないから使われない」

私がオブジェクト指向を理解したのは、実はVBScriptが最初だった。
「なんでまた。。」と思われるかもしれないけど、その当時私はVBSでAccess連携する複雑なコードを書いていた。
複雑で具体的な処理をぐちゃぐちゃ書いていて、ある日それは破綻した。
「どうしてエラーになるのかさっぱり分からない。細部を追っても、全体を見渡しても分からない。これ以上一歩も進めない。」
そう感じて一旦コードを整理しようと思って書き直した際に採用したのが、「クラス」だった。

だから皆さんも、自分のコードが複雑すぎて理解が追い付かなくなってきたら、クラスモジュールのことを思い出してほしい。それはきっと力になってくれるはずだ。

プログラミングだって立派な趣味だ

$
0
0

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

家でプログラムを書いているという話をすると、仕事中毒だと言われることがある。わざわざ休日に家で仕事をしているつまらない奴だと思われているようで癪である。

でもプログラミングだって立派な趣味だ。絵を描いたり、作曲したりするのと同じ、創作活動だ。夢中になって時間を忘れてしまうことだってある。

残念なことにプログラミングというと、どうしても仕事のイメージが強いらしい。
確かにプログラミングがどういうものなのか知らない人からすると、テキストエディタに意味不明の文字をカタカタと打ち込んでいる姿はあまり魅力的には映らないのだろう。

よろしい。では、文字カタカタの何が楽しいのか、ノンプログラマーにも伝わるように説明してみよう。


プログラミングの楽しさの正体は多分、思った通りにプログラムが動いたときの達成感だ。特に、これまで何時間もかかって面倒くさかった作業を、プログラミングを駆使して一瞬で終わらせることができたときは「俺、天才!」と自惚れてしまうこと必至。

これは何かを発明した時や優れたアイデアを思い付いたときのワクワク感に似ている。そして、お金をかけなくてもパソコンさえあれば、自分の手で実際にそのアイデアを試してみることができる。たとえばDIYだったら、木材とか色々な工具が必要だけれど、プログラミングはパソコンだけでいい。

それと、プログラムのコードには審美性がある。プログラミングをやったことがない人にはイメージしづらいかもしれないけれど、同じことをするプログラムにも「美しいコード」と「醜いコード」があるのだ。それは美術的な美しさじゃなくて、機能美に近い。

ある物事の仕組みを知ったとき「へぇ、よくできているなぁ」と感心した経験があると思う。合理的で美しいコードを見たときも、そうしたちょっとした感動がある。たとえば同じことをするプログラムでも、素人が書いたら100行だけど、上手い人が書いたら30行で収まったりということもザラにある。自分も、そうしたより合理的で美しいコードを書けるようになりたいというのも、プログラミングを学習する一つの動機だ。

プログラムコードには、それを書いた人の設計思想が色濃く反映される。だからプログラムのソースコード表現物として著作権でも保護される。


コードを綺麗にしたって一銭にもならないのだから、さっさと次のプログラムに取りかかった方が経済的だと考える会社も多い。だから職業プログラマーが書くコードが必ずしも綺麗だとは限らない。(昨今ではソフトウェアは納品してからも改修が続くのでソースコードも保守しやすいように綺麗にしておくという考え方も一般的になってきたらしいが。)

一方で趣味で書くプログラムには人件費がかからないので、自分の好きなだけこだわることができる。
もちろん、平均的には職業プログラマーの方がスキルが高く、良いコードを書くと思うけれど、本当に綺麗なコードを書けるのは、意外とアマチュアの方だったりするかもしれない。

同じことをするにしても、どういった書き方ができるのか、どう書くと分かりやすくメンテナンスが楽なのか、どうすればもっと綺麗なコードになるのか。そういったことに頭を使い、工夫を凝らすのは楽しい作業だ。家でやる分には、好きなだけ時間をかけても、誰にも文句は言われない。

コードは手段であって目的ではないのだが、私は家でプログラムを書いていると、コードにこだわるあまり、いつまでたっても成果物ができないということもある。完全に本末転倒である。

でも良いんだ。どうせ趣味だから。楽しいから。

VBA Withを使ったコードをWith無しのコードに変換するマクロ ~ スタックの応用 ~

$
0
0

今回は、Withを使ったコードをWith無しのコードに変換するマクロを作ってみる。
何の役に立つんだというツッコミはなしで。

目次

マクロの概要

変換のターゲットとするコードはこちら。

Sub hoge()Dim R As Range
    Set R = Selection
    With R
        .HorizontalAlignment = xlCenter
        .Value="ABC".Interior.Color =vbYellowWith.Font
            .Color =vbRed.Bold =TrueEndWithWith.Borders
            .LineStyle = xlContinuous
            .Color =vbBlueEndWithEndWithEndSub

上記のコードを、マクロを使って以下のように変換する。

Sub hoge()Dim R As Range
    Set R = Selection
    R.HorizontalAlignment = xlCenter
    R.Value="ABC"
    R.Interior.Color =vbYellow
    R.Font.Color =vbRed
    R.Font.Bold =True
    R.Borders.LineStyle = xlContinuous
    R.Borders.Color =vbBlueEndSub

実現方法を検討する

さて、どう実現するのか。
まず、コードはクリップボードから読み込み、イミディエイトウインドウに出力することにしよう。
VBAクリップボードを扱うには、「Microsoft Forms 2.0 Object Library」を参照設定する必要がある。
あるいは、ユーザーフォームを挿入すると自動で参照設定される。

とりあえず、クリップボードにコピーしたコードをそのままイミディエイトウインドウに出力するコードを書いてみた。
ポイントは改行でSplitして配列Arrに格納しているところ。

Subとりあえず入出力()Dim CB AsNew DataObject
    CB.GetFromClipboard
    Dim Arr()AsString: Arr =Split(CB.GetText,vbCrLf)Dim s
    ForEach s In Arr
        Debug.Print s
    NextEndSub

次にWithで始まる文はそのまま出力せず、オブジェクト名を保持するように変更する。
そして、ドットで始まる文の先頭にオブジェクト名を付けていけばよい。

その通りに作ってみた。

Sub Withを外す()Dim CB AsNew DataObject
    CB.GetFromClipboard
    Dim Arr()AsString: Arr =Split(CB.GetText,vbCrLf)Dim s
    ForEach s In Arr
        IfLeft(Trim(s),4)="With"Then
            ObjectName =Right(Trim(s),Len(Trim(s))-5)ElseIfLeft(Trim(s),1)="."Then
            Debug.Print ObjectName &Trim(s)ElseIfTrim(s)="End With"Then'何もしないElse
            Debug.Print s
        EndIfNextEndSub

これで単体のWithならうまくいく。
ためしに以下をコピーして実行してみると、

Sub hoge()Dim R As Range
    Set R = Selection
    With R
        .HorizontalAlignment = xlCenter
        .Value="ABC".Interior.Color =vbYellowEndWithEndSub

このように、Withが外れて出力される。
f:id:t-hom:20160718092201p:plain

Trimしているためインデントが消えているが、とりあえずできている。
しかし、冒頭のターゲットコードに対して実行してみるとうまくいかない。
f:id:t-hom:20160718092459p:plain

入れ子になったWithの扱い方

Withが入れ子になっているコードにも対応させるべく、状態遷移図を使って考えてみよう。
まずWith文の外側をNoWithと定義する。

以下は、WithやEnd Withが実行されるたびにどのように状態が変化するのかを図示したものである。
図中の「With R」は、「With Rの内部にいる状態」を表す。
f:id:t-hom:20160718090500p:plain

1With R進む
2With .Font進む
3End With戻る
4With .Borders進む
5End With戻る
6End With戻る

このように、行って戻ってくるような構造の場合、スタックを使って処理することができる。
スタックとは、下から上にデータを積み上げていく構造で、常に一番上のデータだけを取り出すことができる。
図示すると次のとおり。
f:id:t-hom:20160718095524p:plain

1With R積む
2With .Font積む
3End With降ろす
4With .Borders積む
5End With降ろす
6End With降ろす

VBAでスタックを実装

以前VBAでスタッククラスを作成したので、そちらを使いまわすことにした。
クラスモジュールを挿入し、オブジェクト名をStackとして以下のコードを張り付ける。

Private Items()AsVariantPropertyGetCount()AsIntegerCount=UBound(Items)EndPropertyPropertyGet Top()AsVariant
    Top = Items(UBound(Items))EndPropertyPublicFunction Pop()AsVariantIfUBound(Items)>0Then
        Pop = Items(UBound(Items))ReDimPreserve Items(UBound(Items)-1)Else
        Pop =EmptyEndIfEndFunctionPublicSub Push(ByRef x AsVariant)ReDimPreserve Items(UBound(Items)+1)
    Items(UBound(Items))= x
EndSubPrivateSub Class_Initialize()ReDim Items(0)EndSub

スタックに積む操作をPush、降ろす操作をPopという。また、Top命令で一番上のデータを参照することができる。

実際にサンプルを書いて見た。

Sub StackTest()Dim ObjectStack AsNew Stack
    ObjectStack.Push "R"
    Debug.Print ObjectStack.Top &".Test1"
    ObjectStack.Push "R.Font"
    Debug.Print ObjectStack.Top &".Test2"
    ObjectStack.Pop
    Debug.Print ObjectStack.Top &".Test3"
    ObjectStack.Push "R.Borders"
    Debug.Print ObjectStack.Top &".Test4"
    ObjectStack.Pop
    Debug.Print ObjectStack.Top &".Test5"
    ObjectStack.Pop
EndSub

実行するとこのように、スタックの先頭データと".TextX"が結合されて出力される。

R.Test1
R.Font.Test2
R.Test3
R.Borders.Test4
R.Test5

スタックを使ったWithを外すコード

前項で紹介したスタックを使ってWithを外すコードを作成する。
With文が出てきたらスタックにオブジェクト名をPushし、End Withが出てきたらPopすれば良い。

完成したコードがこちら。

Sub UnWith()Dim CB AsNew DataObject
    CB.GetFromClipboard
    Dim Arr()AsString: Arr =Split(CB.GetText,vbCrLf)Dim ObjectStack AsNew Stack
    
   '先に配列内の値をすべてトリムしておくDim i
    For i =LBound(Arr)ToUBound(Arr)
        Arr(i)=Trim(Arr(i))NextDim s, ObjectName
    ForEach s In Arr
        IfLeft(s,4)="With"Then
            ObjectName =Right(s,Len(s)-5)IfLeft(ObjectName,1)="."Then
                ObjectStack.Push ObjectStack.Top & ObjectName
            Else
                ObjectStack.Push ObjectName
            EndIfElseIfLeft(s,1)="."Then
            Debug.Print ObjectStack.Top & s
        ElseIf s ="End With"Then
            ObjectStack.Pop
        Else
            Debug.Print s
        EndIfNextEndSub

実行結果はこの通り
f:id:t-hom:20160718102711p:plain

課題

インデントが消えてしまうのが難点だが、以前に自動でインデントを揃えるマクロを紹介したのでそれと組み合わせれば綺麗に作れそうだ。
thom.hateblo.jp

また、今回作成したUnWithマクロの課題として、ドットが文の中ほどにあるものは対応していない。
たとえば、選択されている四角いシェイプの面積を求める以下のマクロをコピーして実行すると、

Sub四角いシェイプの面積()Dim S As Shape
    Set S = Selection.ShapeRange(1)With S
        A =.Height *.WidthEndWith
    Debug.Print A
EndSub

以下のように出力される。

Sub 四角いシェイプの面積()
Dim S As Shape
Set S = Selection.ShapeRange(1)
A = .Height * .Width
Debug.Print A
End Sub

ドットの左にスペースまたは丸括弧が来るときは、親オブジェクトを挿入するなどの対処が取れそうだけれど、スペースと丸括弧以外は無いのだろうか。やるならもっと汎用的で確実な方法を採りたい。

そもそも単純変換してしまうと文字列中にあるドットにまで親オブジェクト名を挿入してしまう。

色々考慮する点はありそうだが、ステートメントの判定が複雑になりすぎる。
今は関数でごちゃごちゃやっているところも、別途ステートメントオブジェクトを作りたくなる。

今回とは逆の、With無しのコードをWith化するマクロも作ってみたいが、UnWithより難しそうなので一旦保留中である。

色々考えているうちに面倒くさくなってきたので今日はここまで。

VBA 業務フローチャートをマクロで簡単に作成する

$
0
0

フローチャートはプログラミングでよく用いられていたが、最近は専ら業務の流れを説明する図として利用されている。
特に部門をまたがる業務の全体像を把握するには便利である。

しかし、図なのでとにかく作成が面倒くさい。
どうしてもボックスのサイズをそろえたり、位置を揃えたり、コネクターをつなげたりといった細かい作業に時間を取られてしまう。

今回はマクロを使ってフローチャートを簡単に作成する方法について紹介する。

目次

作成するマクロの概要

まずはGIFアニメで楽々とフローを作成している様子を紹介。

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

プロセスボタンをクリックするとプロセス入力モードになり、あらかじめ用意された枠をクリックすることでプロセス名が入力できる。
コネクターボタンをクリックするとコネクター接続モードになり、プロセスを次々クリックしていくだけで矢印で接続される。
最後にチャートの完成ボタンをクリックすると、不要な枠が消え、まっすぐな鉤型コネクターは直線コネクターに変換されることでコネクターのズレも綺麗になる。

作り方

新しいブックに標準モジュール「Module1」を挿入し、以下のコードを張り付ける。

OptionExplicitPublicEnum Mode
    デフォルト =0消去 =2プロセス入力 =3コネクタ接続 =5判断入力 =4EndEnumPublicEnum Direction
    North =1
    West =2
    South =3
    East =4EndEnumPublic前にクリックしたシェイプ As Shape

PublicFunction反対方向(D As Direction)As Direction
    Dim ret As Direction
    If D <3Then
        ret = D +2Else
        ret = D -2EndIf反対方向 = ret
EndFunctionSubClick()Dim今クリックしたシェイプ As Shape: Set今クリックしたシェイプ _= Sheet1.Shapes(Application.Caller)If Sheet1.現在のモード <>コネクタ接続 ThenSet前にクリックしたシェイプ =NothingDimシェイプ文字列 AsStringSelectCase Sheet1.現在のモード
    Case Mode.消去
        Callシェイプ無効化(今クリックしたシェイプ)Case Mode.プロセス入力
        シェイプ文字列 =InputBox("入力してください")Ifシェイプ文字列 =""ThenExitSubCallシェイプ有効化(今クリックしたシェイプ)Callシェイプ変更(今クリックしたシェイプ, msoShapeFlowchartProcess)今クリックしたシェイプ.TextFrame2.TextRange.Text =シェイプ文字列
    Case Mode.判断入力
        シェイプ文字列 =InputBox("入力してください")Ifシェイプ文字列 =""ThenExitSubCallシェイプ有効化(今クリックしたシェイプ)Callシェイプ変更(今クリックしたシェイプ, msoShapeFlowchartDecision)今クリックしたシェイプ.TextFrame2.TextRange.Text =シェイプ文字列
    Case Mode.コネクタ接続
        IfNot前にクリックしたシェイプ IsNothingThenDimコネクタ As Shape
            Setコネクタ = Sheet1.Shapes.AddConnector(msoConnectorElbow,624,154,816,272)コネクタ.Line.EndArrowheadStyle = msoArrowheadOpen
            コネクタ.Line.Weight =1.5コネクタ.Line.ForeColor.RGB=vbBlackDim接続方向 As Direction
            接続方向 =方位判定(前にクリックしたシェイプ,今クリックしたシェイプ)コネクタ.ConnectorFormat.BeginConnect _
                ConnectedShape:=前にクリックしたシェイプ, _
                ConnectionSite:=接続方向
                
            コネクタ.ConnectorFormat.EndConnect _
                ConnectedShape:=今クリックしたシェイプ, _
                ConnectionSite:=反対方向(接続方向)EndIfSet前にクリックしたシェイプ =今クリックしたシェイプ
    CaseElseMsgBox"モードを選択してください。"EndSelectEndSubSubシェイプ有効化(sh As Shape)With sh
        .TextFrame2.TextRange.Font.Fill.ForeColor.RGB=vbBlack.Line.ForeColor.RGB=vbBlack.Line.Weight =2.Fill.Transparency =0.Line.DashStyle = msoLineSolid
        .Fill.ForeColor.RGB=vbWhiteEndWithEndSubSubシェイプ無効化(sh As Shape)With sh
        .AutoShapeType = msoShapeFlowchartProcess
        .Line.Weight =0.25.TextFrame2.TextRange.Delete.Line.ForeColor.RGB=RGB(150,150,150).Fill.Transparency =1.Line.DashStyle = msoLineDash
    EndWithEndSubSubひな形作成()Set前にクリックしたシェイプ =NothingCall全シェイプ削除
    DimAsDouble: 幅 =100Dim高さ AsDouble: 高さ =40Dim開始X AsDouble: 開始X =100Dim開始Y AsDouble: 開始Y =120Dim横間隔: 横間隔 =50Dim縦間隔: 縦間隔 =30Dim横数: 横数 =5Dim縦数: 縦数 =10Dim x, y
    For x =0To横数 -1For y =0To縦数 -1Dim sh As Shape
            Set sh = Sheet1.Shapes.AddShape( _Type:=msoShapeFlowchartProcess, _Left:=開始X +(x *(+横間隔)), _
                Top:=開始Y +(y *(高さ +縦間隔)), _Width:=, _
                Height:=高さ)Callシェイプ無効化(sh)
            sh.OnAction ="Click"NextNextCall Sheet1.ボタン状態クリア
    Sheet1.現在のモード =デフォルト
EndSubSub全シェイプ削除()Dim s As Shape
    ForEach s In ActiveSheet.Shapes
        If s.Type<> msoFormControl Then'←ボタンを削除しないため
            s.DeleteEndIfNextEndSubFunction方位判定(s1 As Shape, s2 As Shape)As Direction
    Dim s1横中央: s1横中央 = s1.Left+(s1.Width/2)Dim s2横中央: s2横中央 = s2.Left+(s2.Width/2)Dim横の距離: 横の距離 = s1横中央 - s2横中央
    
    Dim s1縦中央: s1縦中央 = s1.Top +(s1.Height /2)Dim s2縦中央: s2縦中央 = s2.Top +(s2.Height /2)Dim縦の距離: 縦の距離 = s1縦中央 - s2縦中央

    IfAbs(横の距離)-Abs(縦の距離)>0ThenIf横の距離 >0Then方位判定 = West
        Else方位判定 = East
        EndIfElseIf縦の距離 >0Then方位判定 = North
        Else方位判定 = South
        EndIfEndIfEndFunctionSubシェイプ変更(TargetShape As Shape, T As MsoAutoShapeType)Dim接続されたコネクタ一覧 AsNew Collection
    Dim s As Shape
    
   '現在TargetShapeに接続されたコネクタを一覧化しておくForEach s In TargetShape.Parent.Shapes
        If s.Connector ThenIf s.ConnectorFormat.BeginConnected ThenIf s.ConnectorFormat.BeginConnectedShape Is TargetShape Then接続されたコネクタ一覧.Add _Array(s, s.ConnectorFormat.BeginConnectionSite,True)'True=BeginEndIfEndIfIf s.ConnectorFormat.EndConnected ThenIf s.ConnectorFormat.EndConnectedShape Is TargetShape Then接続されたコネクタ一覧.Add _Array(s, s.ConnectorFormat.EndConnectionSite,False)'False=EndEndIfEndIfEndIfNext'シェイプタイプを切り替えたタイミングでコネクタの接続が外れる
    TargetShape.AutoShapeType = T
    
   '一覧に登録されたコネクタをTargetShapeに再接続するDim c AsVariantForEach c In接続されたコネクタ一覧
        Dimコネクタ As Shape: Setコネクタ = c(0)If c(2)Then'True=Begin, False=Endコネクタ.ConnectorFormat.BeginConnect TargetShape, c(1)Elseコネクタ.ConnectorFormat.EndConnect TargetShape, c(1)EndIfNextEndSubSubチャート完成()Dim s As Shape
    Dim Arr()AsStringReDim Arr(0)ForEach s In Sheet1.Shapes
        If s.Type<> msoFormControl Then
            s.OnAction =""If s.Connector ThenIf s.Height <2Or s.Width<2Then
                    s.ConnectorFormat.Type= msoConnectorStraight
                EndIfEndIfIf s.Fill.Transparency =1And s.AutoShapeType = msoShapeFlowchartProcess Then
                s.DeleteEndIfEndIfNextCall Sheet1.ボタン状態クリア
EndSub

次に、Sheet1モジュールに以下のコードを張り付ける。

Private cm As Mode

PublicPropertyGet現在のモード()As Mode
    現在のモード = cm
EndPropertyPublicPropertyLet現在のモード(m As Mode)
    cm = m
EndPropertySubボタン状態クリア()With Sheet1.Buttons(Array("Mode3","Mode4","Mode5","Mode2")).Font
        .FontStyle ="標準".ColorIndex =0EndWithEndSubSub ModeButton()Set Module1.前にクリックしたシェイプ =NothingDim押されたボタン As Button
    Set押されたボタン = Sheet1.Buttons(Application.Caller)Callボタン状態クリア
    
    If現在のモード =CInt(Right(押されたボタン.Name,1))Then現在のモード = Mode.デフォルト
    ElseWith押されたボタン.Font
            .FontStyle ="太字".ColorIndex =5EndWith現在のモード =CInt(Right(押されたボタン.Name,1))EndIfEndSub

次にSheet1にボタンを配置する。
f:id:t-hom:20160731023354p:plain

このボタンはフォームコントロールのものを使用すること。
f:id:t-hom:20160731023455p:plain

割り当てるマクロは、次のとおり。
「ひな形作成」ボタン→「ひな型作成」マクロ
「プロセス入力」ボタン→「Sheet1.ModeButton」マクロ
「判断入力」ボタン→「Sheet1.ModeButton」マクロ
コネクター」ボタン→「Sheet1.ModeButton」マクロ
「消去」ボタン→「Sheet1.ModeButton」マクロ
「チャートの完成」ボタン→「チャート完成」マクロ

ボタンを右クリックで選択すると、左上の名前ボックスで名前を付けることができる。
f:id:t-hom:20160731024059p:plain

今回は押されたボタンの判定にこの名前を使用するので、
以下のボタンにはそれぞれ名前をつけておく。
「プロセス入力」ボタン→Mode3
「判断入力」ボタン→Mode4
コネクター」ボタン→Mode5
「消去」ボタン→Mode2

以上でマクロは完成である。

使ってみる

ひな形作成ボタンを押すと、このように薄い枠が表示される。
f:id:t-hom:20160731024442p:plain

私の環境では、シートのズームは70%くらいにしておくと収まりが良い。
(設計したときにシートが70%ズームになっているのに気づかず、その状態でフィットするようにコーディングしてしまった。将来的にはサイズ調整するUIも付けたいけど、今のところ作りっぱなしの手抜き)

プロセス入力ボタンを押すとボタンが青字になり、この状態で枠をクリックすることでプロセスが入力できる。
f:id:t-hom:20160731024858p:plain
判断入力では「ひし形」の図形が配置される。
f:id:t-hom:20160731024923p:plain

コネクターボタンをクリックすると、最初のプロセスクリックでは何も起きないが、その時にクリックしたシェイプを記憶し、次にクリックしたシェイプと矢印で接続される。連続でプロセスをクリックすることで次々つなげることができる。
一旦コネクターを切って新しいシェイプから始めたいときは、一度別のモードに切り替えるか、もう一度コネクターボタンを押して一度デフォルトモードにもどしてから、再度コネクターボタンでコネクタ接続モードに切り替える。

プロセスを修正するときは、プロセスモードで既存のプロセスをクリックすればよい。
プロセスを判断に切り替えたいときも、判断モードで既存プロセスをクリックする。
また、不要なプロセスを削除したい場合はシェイプごと消すのではなく、消去モードにしてクリックする。
コネクターの削除は今のところ手動だ。

最後にチャートの完成ボタンを押すと、不要な枠が消える。

また、作成途中はコネクターの種類がすべて鉤型コネクターなので、真横や真下につないでもズレて見える。
f:id:t-hom:20160731032553p:plain

チャートの完成ボタンを押すことでこれらの本来まっすぐになるべきコネクタは直線コネクタに自動変換され、綺麗になる。

完成ボタンを押した後はプロセス入力はできなくなるので注意。

なお、再度ひな形作成ボタンを押すと、ボタン以外の全シェイプが消去されて新しい枠が配置される。

課題

斜めに配置されたプロセスを接続した場合はこのようになる。
f:id:t-hom:20160731031614p:plain

これは、シェイプ同士の中心距離を縦、横ではかり、より短い方のルートを通るように設計したためだ。
f:id:t-hom:20160731032147p:plain

ただ、フローの流れを考えると、この設計は失敗だった。

今のところ、このように混線してしまうので、これは手で付け替える必要がある。
f:id:t-hom:20160731032256p:plain

デザイン

冒頭のアニメーションで紹介したマクロはセル幅をフローが収まる幅に合わせてある。
また、背景色やヘッダや罫線もあらかじめ作成してある。
f:id:t-hom:20160731033417p:plain

フローの位置に合わせて手動でセル幅を変更する際は、ひな形の枠を全選択して書式設定からセルと連動して動かないように設定しておくとよい。
f:id:t-hom:20160731034023p:plain

完成したフローを別のシートへ移動

完成したフローがこちら。
f:id:t-hom:20160731032843p:plain

完成したフローを別のシートに移すには、オブジェクトの選択モードにして、
f:id:t-hom:20160731035133p:plain

全シェイプ選択し、
f:id:t-hom:20160731035211p:plain

この状態でコピーして別のシートに張り付ければ良い。
f:id:t-hom:20160731035250p:plain

背景をデザインしている場合はこのように背景のセルを必要範囲選択してコピーすると、その範囲に存在するシェイプも一緒に付いてくる。
f:id:t-hom:20160731035410p:plain

ただ、普通に張り付けるとセル幅に合わせてつぶれてしまう。
f:id:t-hom:20160731035743p:plain

この場合は張り付け直後に表示される「Ctrl」と書かれた小さなポップアップをクリックし、元の列幅を保持するように変更すると直る。
f:id:t-hom:20160731035937p:plain

マクロの解説

さて、マクロの動作についてざっくり簡単に概要を説明する。
まずひな形作成であるが、これは単に設定値に合わせてシェイプをAddしているだけ。
ポイントは「sh.OnAction = "Click"」でシェイプにマクロを登録している点である。
これは最終的にはチャート完成時に、「s.OnAction = ""」でマクロ登録を外している。

Clickマクロはシェイプで共通であるが、「Application.Caller」で呼び出し元のシェイプ名がわかるので、そのシェイプ名をもとに該当するシェイプオブジェクトを「今クリックしたシェイプ」変数に格納して操作している。

Clickマクロは現在のモードによって処理が分かれている。
モードはModule1の冒頭で列挙型「Mode」として定義しており、現在のモードはSheet1にプロパティとして保持させている。

モードは4つのモードボタンで切り替える。
これも共通のModeButtonマクロが呼び出されるが、呼び出したボタンはApplication.Callerで判定している。
「Dim 押されたボタン As Button」の部分で、Button型というのは見たことが無いかもしれない。
これはオブジェクトブラウザで非表示のメンバーを表示させると見ることができる。
f:id:t-hom:20160731040747p:plain

ボタンもシェイプの一種ではあるが、Shape型で処理すると、「押されたボタン.Font」でメンバーが見つからないというエラーになる。
Object型で対応しても良かったのだが、インテリセンスを使わないと扱い方がわからなかったので固有型で宣言した。

プロセスや判断の入力、消去などはシェイプの変更で対応している。

コネクタの接続部分は苦労したが、今クリックしたシェイプと前にクリックしたシェイプの位置関係を「方位判定」関数で処理して適切な位置でつないでいる。
この関数の戻り値はModule1で定義している列挙型「Direction」である。

東西南北あるいは英語でNEWSを考えると、並び順が以下のようになっているのは不自然に思われるかもしれない。

PublicEnum Direction
    North =1
    West =2
    South =3
    East =4EndEnum

これはシェイプのConnectionSiteが次の順になっているので合わせた結果である。
f:id:t-hom:20160731042043p:plain

最後にシェイプの完成時には高さまたは幅が2未満のコネクタを直線コネクタに変換している。
(1未満ではうまくいかなかったので2未満とした)

マクロの解説は以上

おわりに

私はこのマクロを「BreadChart(ブレッドチャート)」と名付けた。
最近たまたま電子工作で利用するBreadboardを見て閃いたためだ。
ブレッドボード - Wikipedia


もともと5年ほど前からフローチャートを楽に作るマクロを考えていたのだが、当時は今一つ「これだっ」と思えるアイデアが出ずに悩んでいた。
また実装スキルも低かったので、「こんな風にやればできるかも」と思ってもなかなか作れなかった。
いろいろと試行錯誤しながらガラクタを作り続けているうちに徐々に実装スキルが付き、今では自分が「できるかも」と思ったものは大体作れるようになった。

今はできないことでも、トライ&エラーを繰り返して悩むというプロセスを踏んでおけばスキルがついていつかできるようになる。そういうもんだと実感した。
みなさんも作りたいけどなかなかうまくいかないマクロがあったら、すぐに諦めずにあーだこーだ試行錯誤を続けると良い。たとえ今はガラクタであっても、いつかそれが宝の山になる日が来るかもしれない。

VBA 初学者こそ、なんでもVBAでやるべし

$
0
0

Excelには高度な機能が備わっており、VBAを使わなくても例えば関数やピボットテーブルなどの機能で問題が解決してしまうことも多い。
その意味で、VBAでなんでもやろうとするのは効率が悪い。
でも、ことVBAの学習においては、それらExcelの機能を知っていることがむしろ弊害になってしまうこともあるように思う。

だって、すでに機能としてあるんだったら、作る必要がなくなってしまう。
だったら別のものを作ればいいじゃないかと言われても、そういつも思いつくものではない。

わざわざVBAでやらなくたって、○○で簡単にできる。
確かにそうかもしれないが、それってVBAの適用範囲を狭めてしまう。
結果的にVBAに触れる機会が少なくなってしまい、上達の機会も減る。

別に機能を知っていても「あえて学習のためにVBAで作る」ことはできる。
でも車輪の再発明と知っていながら学習のためと割り切って作るのと「これを完成させれば自分の仕事が楽になる」と思って作るのでは全く楽しさが違う。

特に初学のうちはまだ大きなマクロを作るのは難しい。だから、完成したら仕事が楽になり、なんとか自分で作れそうだという、動機・レベルがマッチした題材はなかなか見つからない。
もし「こういうことをしたい。VBAで実現できそうだ。」と思ったら、それは絶好の学習チャンスだ。そのチャンスを逃してはいけない。

「うーん。でもVBA使わなくてもできるんじゃないか。」とか、「他の人が何か良いマクロ作ってるんじゃないか。」とか、「手でやったほうが早いんじゃないか」とか考えてしまうと、せっかくのチャンスを自ら潰してしまうことになる。

Excelの機能を調べるなという話ではないが、はじめにVBAという選択肢が思い浮かんだなら、まずは試しに作ってみると良い。

VBA初学者こそ、なんでもVBAでやるべし。
(逆にVBA上級者は、VBA以外の可能性も探ってみると良いと思う)

VBA 新しい色の指定方法 ~XlRgbColor定数

$
0
0

VBAで使える色定数は以下の8種類がある。

  • vbBlack
  • vbBlue
  • vbCyan
  • vbGreen
  • vbMagenta
  • vbRed
  • vbWhite
  • vbYellow

私も今までこれ以外使ったことが無かったが、先日オブジェクトブラウザを探索していたらXlRgbColor列挙型なるものを発見した。rgbAliceBlueとか、rgbAntiqueWhiteといった色名が登録されており、定数として利用できる。

わざわざ色名なんて調べなくてもRGB関数で好きな色を作れるのだが、あえて色名で指定するのも情緒があって良い。

しかし実際にどんな色なのかは、いちいち指定してみないとわからないので面倒くさい。

ということで、一覧を作ることにした。

まずMSDNに色名の表があったので、そちらを選択してExcelに張り付ける。
https://msdn.microsoft.com/ja-jp/library/office/ff197459.aspx

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

そして値を全選択し、
f:id:t-hom:20160822215510p:plain

以下のマクロを実行すれば完成

Sub色付け()Dim R As Range
    ForEach R In Selection
        R.Interior.Color = R.ValueNextEndSub

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

気に入った色があれば、次のように指定できる。

Range("A1").Interior.Color = rgbLavender

また、色名の一覧から選択したい場合は列挙型名の「XlRgbColor」を入力し、ドット入力すると候補一覧が表示される。
f:id:t-hom:20160822221639p:plain

選択すると以下のようになるが、XlRgbColorは有っても無くても動作に変わりはない。
(同名の自作変数や関数が存在する場合を除く)

Range("A1").Interior.Color = XlRgbColor.rgbLavender

さて、MSDNのアドレスが変わってしまったりするとアレなので、一応単体で色一覧を出力するマクロも作ってみた。
(ちょっと長いけれど、これを実行するとアクティブシートに色一覧が作成される。)

Sub色一覧作成()Dim R As Range: Set R = Range("A1")
    R.Interior.Color = rgbAliceBlue: R.Offset(0,1).Value="rgbAliceBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbAntiqueWhite: R.Offset(0,1).Value="rgbAntiqueWhite": Set R = R.Offset(1,0)
    R.Interior.Color = rgbAqua: R.Offset(0,1).Value="rgbAqua": Set R = R.Offset(1,0)
    R.Interior.Color = rgbAquamarine: R.Offset(0,1).Value="rgbAquamarine": Set R = R.Offset(1,0)
    R.Interior.Color = rgbAzure: R.Offset(0,1).Value="rgbAzure": Set R = R.Offset(1,0)
    R.Interior.Color = rgbBeige: R.Offset(0,1).Value="rgbBeige": Set R = R.Offset(1,0)
    R.Interior.Color = rgbBisque: R.Offset(0,1).Value="rgbBisque": Set R = R.Offset(1,0)
    R.Interior.Color = rgbBlack: R.Offset(0,1).Value="rgbBlack": Set R = R.Offset(1,0)
    R.Interior.Color = rgbBlanchedAlmond: R.Offset(0,1).Value="rgbBlanchedAlmond": Set R = R.Offset(1,0)
    R.Interior.Color = rgbBlue: R.Offset(0,1).Value="rgbBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbBlueViolet: R.Offset(0,1).Value="rgbBlueViolet": Set R = R.Offset(1,0)
    R.Interior.Color = rgbBrown: R.Offset(0,1).Value="rgbBrown": Set R = R.Offset(1,0)
    R.Interior.Color = rgbBurlyWood: R.Offset(0,1).Value="rgbBurlyWood": Set R = R.Offset(1,0)
    R.Interior.Color = rgbCadetBlue: R.Offset(0,1).Value="rgbCadetBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbChartreuse: R.Offset(0,1).Value="rgbChartreuse": Set R = R.Offset(1,0)
    R.Interior.Color = rgbCoral: R.Offset(0,1).Value="rgbCoral": Set R = R.Offset(1,0)
    R.Interior.Color = rgbCornflowerBlue: R.Offset(0,1).Value="rgbCornflowerBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbCornsilk: R.Offset(0,1).Value="rgbCornsilk": Set R = R.Offset(1,0)
    R.Interior.Color = rgbCrimson: R.Offset(0,1).Value="rgbCrimson": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkBlue: R.Offset(0,1).Value="rgbDarkBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkCyan: R.Offset(0,1).Value="rgbDarkCyan": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkGoldenrod: R.Offset(0,1).Value="rgbDarkGoldenrod": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkGray: R.Offset(0,1).Value="rgbDarkGray": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkGreen: R.Offset(0,1).Value="rgbDarkGreen": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkGrey: R.Offset(0,1).Value="rgbDarkGrey": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkKhaki: R.Offset(0,1).Value="rgbDarkKhaki": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkMagenta: R.Offset(0,1).Value="rgbDarkMagenta": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkOliveGreen: R.Offset(0,1).Value="rgbDarkOliveGreen": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkOrange: R.Offset(0,1).Value="rgbDarkOrange": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkOrchid: R.Offset(0,1).Value="rgbDarkOrchid": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkRed: R.Offset(0,1).Value="rgbDarkRed": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkSalmon: R.Offset(0,1).Value="rgbDarkSalmon": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkSeaGreen: R.Offset(0,1).Value="rgbDarkSeaGreen": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkSlateBlue: R.Offset(0,1).Value="rgbDarkSlateBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkSlateGray: R.Offset(0,1).Value="rgbDarkSlateGray": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkSlateGrey: R.Offset(0,1).Value="rgbDarkSlateGrey": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkTurquoise: R.Offset(0,1).Value="rgbDarkTurquoise": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDarkViolet: R.Offset(0,1).Value="rgbDarkViolet": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDeepPink: R.Offset(0,1).Value="rgbDeepPink": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDeepSkyBlue: R.Offset(0,1).Value="rgbDeepSkyBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDimGray: R.Offset(0,1).Value="rgbDimGray": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDimGrey: R.Offset(0,1).Value="rgbDimGrey": Set R = R.Offset(1,0)
    R.Interior.Color = rgbDodgerBlue: R.Offset(0,1).Value="rgbDodgerBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbFireBrick: R.Offset(0,1).Value="rgbFireBrick": Set R = R.Offset(1,0)
    R.Interior.Color = rgbFloralWhite: R.Offset(0,1).Value="rgbFloralWhite": Set R = R.Offset(1,0)
    R.Interior.Color = rgbForestGreen: R.Offset(0,1).Value="rgbForestGreen": Set R = R.Offset(1,0)
    R.Interior.Color = rgbFuchsia: R.Offset(0,1).Value="rgbFuchsia": Set R = R.Offset(1,0)
    R.Interior.Color = rgbGainsboro: R.Offset(0,1).Value="rgbGainsboro": Set R = R.Offset(1,0)
    R.Interior.Color = rgbGhostWhite: R.Offset(0,1).Value="rgbGhostWhite": Set R = R.Offset(1,0)
    R.Interior.Color = rgbGold: R.Offset(0,1).Value="rgbGold": Set R = R.Offset(1,0)
    R.Interior.Color = rgbGoldenrod: R.Offset(0,1).Value="rgbGoldenrod": Set R = R.Offset(1,0)
    R.Interior.Color = rgbGray: R.Offset(0,1).Value="rgbGray": Set R = R.Offset(1,0)
    R.Interior.Color = rgbGreen: R.Offset(0,1).Value="rgbGreen": Set R = R.Offset(1,0)
    R.Interior.Color = rgbGreenYellow: R.Offset(0,1).Value="rgbGreenYellow": Set R = R.Offset(1,0)
    R.Interior.Color = rgbGrey: R.Offset(0,1).Value="rgbGrey": Set R = R.Offset(1,0)
    R.Interior.Color = rgbHoneydew: R.Offset(0,1).Value="rgbHoneydew": Set R = R.Offset(1,0)
    R.Interior.Color = rgbHotPink: R.Offset(0,1).Value="rgbHotPink": Set R = R.Offset(1,0)
    R.Interior.Color = rgbIndianRed: R.Offset(0,1).Value="rgbIndianRed": Set R = R.Offset(1,0)
    R.Interior.Color = rgbIndigo: R.Offset(0,1).Value="rgbIndigo": Set R = R.Offset(1,0)
    R.Interior.Color = rgbIvory: R.Offset(0,1).Value="rgbIvory": Set R = R.Offset(1,0)
    R.Interior.Color = rgbKhaki: R.Offset(0,1).Value="rgbKhaki": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLavender: R.Offset(0,1).Value="rgbLavender": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLavenderBlush: R.Offset(0,1).Value="rgbLavenderBlush": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLawnGreen: R.Offset(0,1).Value="rgbLawnGreen": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLemonChiffon: R.Offset(0,1).Value="rgbLemonChiffon": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLightBlue: R.Offset(0,1).Value="rgbLightBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLightCoral: R.Offset(0,1).Value="rgbLightCoral": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLightCyan: R.Offset(0,1).Value="rgbLightCyan": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLightGoldenrodYellow: R.Offset(0,1).Value="rgbLightGoldenrodYellow": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLightGray: R.Offset(0,1).Value="rgbLightGray": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLightGreen: R.Offset(0,1).Value="rgbLightGreen": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLightGrey: R.Offset(0,1).Value="rgbLightGrey": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLightPink: R.Offset(0,1).Value="rgbLightPink": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLightSalmon: R.Offset(0,1).Value="rgbLightSalmon": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLightSeaGreen: R.Offset(0,1).Value="rgbLightSeaGreen": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLightSkyBlue: R.Offset(0,1).Value="rgbLightSkyBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLightSlateGray: R.Offset(0,1).Value="rgbLightSlateGray": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLightSteelBlue: R.Offset(0,1).Value="rgbLightSteelBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLightYellow: R.Offset(0,1).Value="rgbLightYellow": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLime: R.Offset(0,1).Value="rgbLime": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLimeGreen: R.Offset(0,1).Value="rgbLimeGreen": Set R = R.Offset(1,0)
    R.Interior.Color = rgbLinen: R.Offset(0,1).Value="rgbLinen": Set R = R.Offset(1,0)
    R.Interior.Color = rgbMaroon: R.Offset(0,1).Value="rgbMaroon": Set R = R.Offset(1,0)
    R.Interior.Color = rgbMediumAquamarine: R.Offset(0,1).Value="rgbMediumAquamarine": Set R = R.Offset(1,0)
    R.Interior.Color = rgbMediumBlue: R.Offset(0,1).Value="rgbMediumBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbMediumOrchid: R.Offset(0,1).Value="rgbMediumOrchid": Set R = R.Offset(1,0)
    R.Interior.Color = rgbMediumPurple: R.Offset(0,1).Value="rgbMediumPurple": Set R = R.Offset(1,0)
    R.Interior.Color = rgbMediumSeaGreen: R.Offset(0,1).Value="rgbMediumSeaGreen": Set R = R.Offset(1,0)
    R.Interior.Color = rgbMediumSlateBlue: R.Offset(0,1).Value="rgbMediumSlateBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbMediumSpringGreen: R.Offset(0,1).Value="rgbMediumSpringGreen": Set R = R.Offset(1,0)
    R.Interior.Color = rgbMediumTurquoise: R.Offset(0,1).Value="rgbMediumTurquoise": Set R = R.Offset(1,0)
    R.Interior.Color = rgbMediumVioletRed: R.Offset(0,1).Value="rgbMediumVioletRed": Set R = R.Offset(1,0)
    R.Interior.Color = rgbMidnightBlue: R.Offset(0,1).Value="rgbMidnightBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbMintCream: R.Offset(0,1).Value="rgbMintCream": Set R = R.Offset(1,0)
    R.Interior.Color = rgbMistyRose: R.Offset(0,1).Value="rgbMistyRose": Set R = R.Offset(1,0)
    R.Interior.Color = rgbMoccasin: R.Offset(0,1).Value="rgbMoccasin": Set R = R.Offset(1,0)
    R.Interior.Color = rgbNavajoWhite: R.Offset(0,1).Value="rgbNavajoWhite": Set R = R.Offset(1,0)
    R.Interior.Color = rgbNavy: R.Offset(0,1).Value="rgbNavy": Set R = R.Offset(1,0)
    R.Interior.Color = rgbNavyBlue: R.Offset(0,1).Value="rgbNavyBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbOldLace: R.Offset(0,1).Value="rgbOldLace": Set R = R.Offset(1,0)
    R.Interior.Color = rgbOlive: R.Offset(0,1).Value="rgbOlive": Set R = R.Offset(1,0)
    R.Interior.Color = rgbOliveDrab: R.Offset(0,1).Value="rgbOliveDrab": Set R = R.Offset(1,0)
    R.Interior.Color = rgbOrange: R.Offset(0,1).Value="rgbOrange": Set R = R.Offset(1,0)
    R.Interior.Color = rgbOrangeRed: R.Offset(0,1).Value="rgbOrangeRed": Set R = R.Offset(1,0)
    R.Interior.Color = rgbOrchid: R.Offset(0,1).Value="rgbOrchid": Set R = R.Offset(1,0)
    R.Interior.Color = rgbPaleGoldenrod: R.Offset(0,1).Value="rgbPaleGoldenrod": Set R = R.Offset(1,0)
    R.Interior.Color = rgbPaleGreen: R.Offset(0,1).Value="rgbPaleGreen": Set R = R.Offset(1,0)
    R.Interior.Color = rgbPaleTurquoise: R.Offset(0,1).Value="rgbPaleTurquoise": Set R = R.Offset(1,0)
    R.Interior.Color = rgbPaleVioletRed: R.Offset(0,1).Value="rgbPaleVioletRed": Set R = R.Offset(1,0)
    R.Interior.Color = rgbPapayaWhip: R.Offset(0,1).Value="rgbPapayaWhip": Set R = R.Offset(1,0)
    R.Interior.Color = rgbPeachPuff: R.Offset(0,1).Value="rgbPeachPuff": Set R = R.Offset(1,0)
    R.Interior.Color = rgbPeru: R.Offset(0,1).Value="rgbPeru": Set R = R.Offset(1,0)
    R.Interior.Color = rgbPink: R.Offset(0,1).Value="rgbPink": Set R = R.Offset(1,0)
    R.Interior.Color = rgbPlum: R.Offset(0,1).Value="rgbPlum": Set R = R.Offset(1,0)
    R.Interior.Color = rgbPowderBlue: R.Offset(0,1).Value="rgbPowderBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbPurple: R.Offset(0,1).Value="rgbPurple": Set R = R.Offset(1,0)
    R.Interior.Color = rgbRed: R.Offset(0,1).Value="rgbRed": Set R = R.Offset(1,0)
    R.Interior.Color = rgbRosyBrown: R.Offset(0,1).Value="rgbRosyBrown": Set R = R.Offset(1,0)
    R.Interior.Color = rgbRoyalBlue: R.Offset(0,1).Value="rgbRoyalBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbSalmon: R.Offset(0,1).Value="rgbSalmon": Set R = R.Offset(1,0)
    R.Interior.Color = rgbSandyBrown: R.Offset(0,1).Value="rgbSandyBrown": Set R = R.Offset(1,0)
    R.Interior.Color = rgbSeaGreen: R.Offset(0,1).Value="rgbSeaGreen": Set R = R.Offset(1,0)
    R.Interior.Color = rgbSeashell: R.Offset(0,1).Value="rgbSeashell": Set R = R.Offset(1,0)
    R.Interior.Color = rgbSienna: R.Offset(0,1).Value="rgbSienna": Set R = R.Offset(1,0)
    R.Interior.Color = rgbSilver: R.Offset(0,1).Value="rgbSilver": Set R = R.Offset(1,0)
    R.Interior.Color = rgbSkyBlue: R.Offset(0,1).Value="rgbSkyBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbSlateBlue: R.Offset(0,1).Value="rgbSlateBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbSlateGray: R.Offset(0,1).Value="rgbSlateGray": Set R = R.Offset(1,0)
    R.Interior.Color = rgbSnow: R.Offset(0,1).Value="rgbSnow": Set R = R.Offset(1,0)
    R.Interior.Color = rgbSpringGreen: R.Offset(0,1).Value="rgbSpringGreen": Set R = R.Offset(1,0)
    R.Interior.Color = rgbSteelBlue: R.Offset(0,1).Value="rgbSteelBlue": Set R = R.Offset(1,0)
    R.Interior.Color = rgbTan: R.Offset(0,1).Value="rgbTan": Set R = R.Offset(1,0)
    R.Interior.Color = rgbTeal: R.Offset(0,1).Value="rgbTeal": Set R = R.Offset(1,0)
    R.Interior.Color = rgbThistle: R.Offset(0,1).Value="rgbThistle": Set R = R.Offset(1,0)
    R.Interior.Color = rgbTomato: R.Offset(0,1).Value="rgbTomato": Set R = R.Offset(1,0)
    R.Interior.Color = rgbTurquoise: R.Offset(0,1).Value="rgbTurquoise": Set R = R.Offset(1,0)
    R.Interior.Color = rgbViolet: R.Offset(0,1).Value="rgbViolet": Set R = R.Offset(1,0)
    R.Interior.Color = rgbWheat: R.Offset(0,1).Value="rgbWheat": Set R = R.Offset(1,0)
    R.Interior.Color = rgbWhite: R.Offset(0,1).Value="rgbWhite": Set R = R.Offset(1,0)
    R.Interior.Color = rgbWhiteSmoke: R.Offset(0,1).Value="rgbWhiteSmoke": Set R = R.Offset(1,0)
    R.Interior.Color = rgbYellow: R.Offset(0,1).Value="rgbYellow": Set R = R.Offset(1,0)
    R.Interior.Color = rgbYellowGreen: R.Offset(0,1).Value="rgbYellowGreen": Set R = R.Offset(1,0)EndSub

実行結果はこんな感じ。
f:id:t-hom:20160822220353p:plain

上記の長ったらしいマクロは、マクロを使って書いた。

MSDNからコピーした表の色名を全選択し、
f:id:t-hom:20160822220618p:plain

以下のようなマクロを作って実行すると、

Sub色一覧作成マクロ生成()
    Debug.Print"Sub 色一覧作成()"
    Debug.Print"    Dim R As Range: Set R = Range(""A1"")"ForEach x In Selection
        Debug.Print _"    R.Interior.Color = "& x.Value& _": R.Offset(0,1).Value = """& x.Value&""""& _": Set R = R.Offset(1,0)"Next
    Debug.Print"End Sub"EndSub

イミディエイトウインドウに色一覧作成マクロが生成される。
f:id:t-hom:20160822220807p:plain

あとはコピペするだけ。

プログラムにプログラムを書かせるテクニックは過去にも紹介しているので興味があればどうぞ。
thom.hateblo.jp
thom.hateblo.jp

VBA 配列とコレクションの違いをメモリ上のデータ構造から理解する

$
0
0

VBAでは複数データを格納できるデータ型として、配列とコレクションがある。
それぞれ一長一短あり、どちらが優れているというものではないのだが、どちらかといえばデータの追加・削除が簡単に行えるコレクションのほうが使い勝手は良いかもしれない。

さて、今回は配列とコレクションのデータ構造に焦点を当ててそれぞれの違いを説明する。

配列のデータ構造

例えばInteger型の配列を次のように作成する。

Dim Arr(3)AsInteger
Arr(0)=10
Arr(1)=20
Arr(2)=30
Arr(3)=40

すると、メモリ上には単に直列にデータが並ぶ。VBAのIntegerは2バイトなので、ちょうど2バイトずつ隙間なく配置される。
f:id:t-hom:20160831000135p:plain

もし次のようにLong型で宣言したら、

Dim Arr(3)AsLong
Arr(0)=100
Arr(1)=200
Arr(2)=300
Arr(3)=400

Long型は4バイトなので、やはり4バイトずつ隙間なくメモリに配置される。
f:id:t-hom:20160831000436p:plain

このように隙間なく並べることで、添え字(インデックス)が大きくなっても、データの格納位置を瞬時に割り出すことができる。
どういうことか、先ほどのLong型配列の例で説明する。

例えば次のようにArr(3)の値を表示する命令が実行されたとする。

MsgBox Arr(3)

VBAはまず、Arr(3)がメモリ上のどの場所に格納されているかを探しだす。

配列Arrの先頭アドレスは10000000なので、これに(添え字×型のデータサイズ)を加えてやれば、アドレスが求まる。
添え字は3で、Long型のサイズは4なので、3 × 4 = 12。
先頭アドレスの10000000に12を加えた10000012が目的のデータのアドレスであるとわかる。

そして、アドレスの10000012番地にある値「400」をメッセージボックスで表示する。
以上が配列の参照時の動作である。

配列の添え字(インデックス)を先頭アドレスからのオフセットとして利用するためには、隙間なくデータが並んでいなければならない。

以下のように、別のデータがメモリに入っていたら、もうそれ以上はデータを増やせない。
f:id:t-hom:20160831001754p:plain

なぜなら、仮に次の空き番地にデータを入れたとしても、添え字からデータ位置を割り出すことができなくなるので、データを取り出せなくなってしまうからだ。

配列は宣言時にサイズを決め、そのあと原則サイズ変更ができないが、これはつまり、最初にメモリの連続した領域を確保してしまう必要がある為だ。

ちなみに動的配列はReDimでサイズ変更ができるが、あれも厳密にいえば別の領域に新たな連続したメモリ領域を確保して配列を再作成している。
だから命令の名前もExpand(拡張)じゃなくて、Re(再)Dimとなっている。Preserveを付けないと中身が消えてしまうという説明がなされるが、正確に言えばPreserveを付けると旧メモリ領域からデータをコピーしてくれるということである。

自分で確かめたい場合は以下を試してみると良い。

Sub ReDimでアドレスが変わるサンプル()Dim Arr()AsIntegerReDim Arr(2)
    Arr(0)=10
    Arr(1)=20
    Arr(2)=30
    
    Debug.Print"配列のアドレス"For i =0To2
        Debug.Print"Arr("& i &") : "; VarPtr(Arr(i))NextReDimPreserve Arr(3)
    Arr(3)=40
    
    Debug.Print
    Debug.Print"ReDim後のアドレス"For i =0To3
        Debug.Print"Arr("& i &") : "; VarPtr(Arr(i))NextEndSub

コレクションのデータ構造

Collectionという名称はMicrosoftが付けたものであるが、データ構造の一般名称としては「連結リスト」または単に「リスト」と呼ばれている。

リスト構造では、以下のようにデータと次のデータ位置を示すアドレスがセットで格納されている。
f:id:t-hom:20160831004315p:plain

(注)メモリ図はあくまでイメージです。紙面の都合でアドレス欄(水色)を1バイトとしましたが、アドレス番号を格納するには少なくとも4バイト必要です。

リストの場合、配列と違って隙間なくデータを並べておく必要がなく、空いているアドレスにデータを追加することができる。

情報処理試験などでは、次のような図で表されることも多い。
f:id:t-hom:20160831004105p:plain

新たにデータを追加したい場合は任意の場所にデータを追加し、前の要素のアドレス欄を書き換えれば良い。
f:id:t-hom:20160831004831p:plain

データの挿入も、前の要素のアドレス欄を書き換えてから、挿入する要素のアドレス欄に次の要素のアドレスを格納すれば良い。
f:id:t-hom:20160831005117p:plain

削除もアドレス欄の書き換えだけで済む。
f:id:t-hom:20160831005556p:plain

VBAのCollection型はリスト構造なので、データの追加や削除が簡単に行うことができる。
データを参照するにはリストの開始位置から順にリストを辿っていけば良い。

例えば次のプログラムは、3番目にAddされた「30」を表示する。

Sub fuga()Dim C AsNew Collection
    C.Add10
    C.Add20
    C.Add30MsgBox C(3)EndSub

この時、内部では次のようにリストを先頭から順に辿る処理がなされている。

1番目のアドレス欄を参照し、2番目のデータ位置を確認する。
2番目のアドレス欄を参照し、3番目のデータ位置を確認する。
3番目のデータにたどり着いたので、それを表示する。

配列のように添え字からの計算で格納位置を求めることができないため、愚直にリストを辿るしかないのである。
だから理屈上は、後ろのほうに追加されたデータのほうが参照するのに時間がかかることになる。

これを実証するには、次のマクロを利用すると良い。

Subコレクションの速度計測()Dim C AsNew Collection
    Debug.Print"準備しています。お待ちください。"For i =1To10 ^ 7
        C.Add i
    Next
    Debug.Print"計測を開始します。"
    
    n =100For i =1To7
        T =TimerFor j =1To n
            Void = C(10 ^ i)Next
        Debug.Print10 ^ i; "番目のデータを"; n; "回参照するのに"; Round(Timer- T,5); "秒かかりました。"Next
    Debug.Print"計測を終了しました。"EndSub

私のPC環境だと、次のような結果になった。
f:id:t-hom:20160831012802p:plain

確かに、後ろのほうに追加されたデータのほうが参照するのに時間がかかっている。

次に配列でやってみよう。

Sub配列の速度計測()Dim Arr(10 ^ 7)
    Debug.Print"準備しています。お待ちください。"For i =0To10 ^ 7
        Arr(i)= i
    Next
    Debug.Print"計測を開始します。"
    n =10000For i =1To7
        T =TimerFor j =1To n
            Void = Arr(10 ^ i)Next
        Debug.Print10 ^ i; "番目のデータを"; n; "回参照するのに"; Round(Timer- T,5); "秒かかりました。"Next
    Debug.Print"計測を終了しました。"EndSub

参照回数が100回ずつだとすべて0秒で終わってしまったので、nの値は10000とした。
これだけ見ても配列がいかに高速かがわかる。

そして、私のPC環境での実行結果は、次のようになった。
f:id:t-hom:20160831013124p:plain

すでに説明したとおり、添え字から計算でデータの格納アドレスを求めているため、参照位置が変わっても速度に差は出ない。
コレクションで後方のデータほど時間がかかるのとは対照的である。

まとめ

今回は普段使っている配列やコレクションのデータ構造について解説した。
サンプルでは検証のためにわざと有意差が出るようにループ回数やサイズを調整したが、これはあくまでデータ構造の説明に説得力を持たせるためのサンプルであって、配列のほうが高速だから優れていると言いたいわけではない。
実際に配列のメリットとして高速であるという説明がなされることがあるが、実務で扱う数万件程度のデータなら大差ないので速度はそれほどアピールポイントにはならない。

配列のメリットとしては、ArrayやSplitなどで動的に生成できることや、Join関数で結合できること、2次元配列のセルとの相互転記ができること、宣言時に型をカチっと決められるためオブジェクト型の配列にしたときにドットでプロパティとメソッドの入力候補が表示されること等が挙げられる。

コレクションのメリットは、データの追加・削除・挿入が容易であることと、キー文字列を設定でき、インデックスの変わりにキーを使ってデータ参照できる点が挙げられる。

それぞれ一長一短あるので、配列派、コレクション派ということではなく、どちらも自在に使いこなせるようになりたい。


amazonアソシエイトのウィジェットがChromeで自動再生されなくなったのでランダムに商品を並べる機能をJavaScriptで自作する。

$
0
0

このブログはamazonアソシエイトに参加しており、その広告収益を運営費に充てている。
先日までこのブログのサイドバーにはA8.netの固定バナーを表示していたが、まったく収益になってなかったので、そちらを削除して空いたスペースにおススメの小説を掲載することにした。

当初はamazonのスライドショーウィジェットが良さげだったのでそれを使う予定だったのだが、配置してみたところ自動再生されない。
調べたところ、去年の9月のアップデートで、Google Chromeでは重要でないFlash(つまり広告)は自動再生されない仕様になったとのこと。
www.itmedia.co.jp

広告というのは勝手に目に飛び込んでくるから興味を引くのであって、わざわざクリックして再生してくれるユーザーはほとんど居ない。
それでスライドショーウィジェットを使う案はボツ。

Flashを使わないお気に入りウィジェットというのも試してみたけれど、どうも商品画像が小さすぎて興味をそそられない。
「これ欲しい」とか、「これ面白そう」というのは直感的なもので、小っちゃい文字と小っちゃい画像ではインパクトに欠ける。
やはりバーンと画像で魅せないと。

やりたいことは、自分のおススメの小説をランダムに3つくらい、大きめの画像で表示させること。
ということで、JavaScriptで自作することにした。

まず、amazonアソシエイトの商品リンクで画像のみのサイズ中を選択し、URLを取得する。
すると、以下のようなURLが取得できる。

<ahref="https://www.amazon.co.jp/gp/product/B0093GE1UM/ref=as_li_tf_il?ie=UTF8&camp=247&creative=1211&creativeASIN=B0093GE1UM&linkCode=as2&tag=hogehoge"><imgborder="0"src="http://ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&ASIN=B0093GE1UM&Format=_SL160_&ID=AsinImage&MarketPlace=JP&ServiceVersion=20070822&WS=1&tag=hogehoge"></a><imgsrc="http://ir-jp.amazon-adsystem.com/e/ir?t=hogehoge&l=as2&o=9&a=B0093GE1UM"width="1"height="1"border="0"alt=""style="border:none !important; margin:0px !important;" />

このうち、可変項目は、商品コード「B0093GE1UM」と自分のID「hogehoge」なので、そこだけ変数にしてループで回せば別の商品を次々に表示できる。
また、おススメ商品群から3つピックアップしたかったので、配列からランダムでピックアップする処理にした。

サイドバーのモジュールに埋め込んだコードは以下のとおり。

<p>オススメ!松岡 圭祐 Kindle本</p><divid="amazon_km_kindle"style="margin-left:20px;"></div><scripttype="text/javascript"><!--window.onload = function(){var asins = ["B009GPMSN2", "B0093GE1UM", "B009VZ8L5G", "B00QJDTG2K", "B016KDOC36"] ;var associate_id = "hogehoge";var display_number = 3;for(var i = 0; i < display_number; i++){var a = Math.floor( Math.random() * asins.length )
var asin = asins.splice(a,1);var anchor = document.createElement("a");	    anchor.href = "https://www.amazon.co.jp/gp/product/" + asin	    	+ "/ref=as_li_tf_il?ie=UTF8&camp=247&creative=1211&creativeASIN=" + asin	    	+ "&linkCode=as2&tag=" + associate_id;var img1 = document.createElement("img");		img1.src = "http://ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&ASIN=" + asin			+ "&Format=_SL160_&ID=AsinImage&MarketPlace=JP"			+ "&ServiceVersion=20070822&WS=1&tag=" + associate_id;		img1.border = "0";		anchor.appendChild(img1)
var img2 = document.createElement("img");		img2.src = "http://ir-jp.amazon-adsystem.com/e/ir?t=" + associate_id + "&l=as2&o=9&a=" + asin;		img2.width = "1";		img2.height = "1";		img2.border = "0";		img2.alt = "";		img2.style = "border:none !important; margin:0px !important;";var div = document.getElementById("amazon_km_kindle");	    div.appendChild(anchor);	    div.appendChild(img2);}}// --></script>

JavaScriptで配列と呼ばれるものは、実はリスト構造になっているようで、データの切り出しや挿入用の命令が用意されている。
VBAやCを経験した人からするとリスト構造を配列と呼ぶのは違和感があるかもしれない。

【参考:VBAの場合、配列はそのまま配列構造】
thom.hateblo.jp

JavaScriptの配列は非常に多機能で使い勝手がよく、pushやpopでスタックのような操作ができたり、spliceでデータの一部を切り出したり、joinで文字列結合させたりできる。

さて、このブログの読者はVBA使いがメインだと思うので、前述のJavaScriptのうち、商品群からランダムに3つピックアップする処理について、VBAでもやってみようと思う。
VBAで書くと、こうなる。

Sub束からランダムに3つピックアップ()'配列を準備Dim asinArray()
    asinArray =Array("B009GPMSN2","B0093GE1UM","B009VZ8L5G","B00QJDTG2K","B016KDOC36")'コレクションに格納Dim asinCollection AsNew Collection
    Dim asinCode
    ForEach asinCode In asinArray
        asinCollection.Add asinCode
    Next'ランダムにピックアップDim i, n
    For i =1To3
        n =Int((asinCollection.Count)*Rnd+1)
        Debug.Print asinCollection(n)
        asinCollection.Remove n    '同じコードを取らないように取り除くNextEndSub

JavaScriptの場合は配列がリスト構造なので扱いやすいが、VBAの場合は一旦操作しやすいCollectionに格納する。
JavaScriptの配列はspliceメソッドで値を取出しつつ、配列から取り除くことができるが、VBAのCollectionの場合は別途Removeが必要となる。

束から重複なくランダムにピックアップする処理はいろいろと応用が利く。
たとえば、全て取り出して別のコレクションにAddすればコレクションのシャッフルができる。
あるいは問題集からランダムな30問を表示させることもできる。

言語が変わってもこうしたアルゴリズムは潰しが利くので覚えておくとよいと思う。

Excel TRIM関数で消えない謎の半角スペースをVBAでなんとかする

$
0
0

先日、仕事で受け取ったExcelデータにTRIM関数で除去できない謎の半角スペースが混じっていた。

その現象を再現したのがこちら。
f:id:t-hom:20160920135926p:plain

A1セルとA2セルは全く同じに見えるが、A1セルの1文字目はトリムできない謎のスペース。A2セルの方は通常のスペースである。

このようにTRIM関数を使っても、スペースが消えてくれない。
f:id:t-hom:20160920140154p:plain

VBAで謎のスペースを調査

いったいこのスペースは何なのか。VBAで調べてみた。
まずはふつうにイミディエイトウインドウに出力してみる。

Subとりあえず出力()
    Debug.Print Range("A1").Value
    Debug.Print Range("A2").ValueEndSub

すると結果は、、んん?
f:id:t-hom:20160920140434p:plain

なぜかA1セルのスペースはクエッションマークとして表示されるようだ。

ひと文字だけで試してみる。

Sub謎のスペースだけ出力()謎のスペース =Left(Range("A1").Value,1)
    Debug.Print謎のスペース
EndSub

結果は「?」がひと文字表示された。
これってただのハテナなのか?

比較してみる。

Sub謎のスペースはハテナなのか()謎のスペース =Left(Range("A1").Value,1)
    Debug.Print謎のスペース ="?"EndSub

結果はFalse。
ただのハテナではないようだ。
では文字コードを見てみよう。

Sub謎のスペースの文字コードは()謎のスペース =Left(Range("A1").Value,1)
    Debug.PrintAsc(謎のスペース)EndSub

「63」とのこと。
ASCIIコード表で調べてみると、、
ASCII文字コード : IT用語辞典

やっぱりハテナ。。
ちなみに文字コード同士の比較だと一致する。

Sub文字コード同士を比較()謎のスペース =Left(Range("A1").Value,1)
    Debug.PrintAsc(謎のスペース)=Asc("?")EndSub

これはお手上げかーと思ったそのとき、最後の一手がひらめいた。
そうだ、文字列とByte型配列は相互置換できるんだった。

Byte型配列を使って謎のスペースの正体にせまる。

ということで、謎のスペースをByte型配列に代入し、その数値を見てみることにした。

Subバイト型配列を使って正体を暴く()Dim謎のスペース()AsByte謎のスペース =Left(Range("A1").Value,1)
    
    Debug.Print"---謎のスペースのコード---"
    Debug.Print謎のスペース(0)
    Debug.Print謎のスペース(1)Dimハテナ()AsByteハテナ ="?"
    
    Debug.Print"---ハテナのコード---"
    Debug.Printハテナ(0)
    Debug.Printハテナ(1)EndSub

すると!
f:id:t-hom:20160920142410p:plain

でたっ!やっぱByteデータは嘘つかない。

文字コード 160」でGoogle検索してみたところ、謎のスペースの正体は、HTMLでよく利用される、NBSP(ノーブレークスペース)だった。

ノーブレークスペース - Wikipedia

NBSPを除去する関数を作成

さて、謎のスペースの正体が分かったところで、今度はそれを除去する関数がほしい。
Chr(160)で簡単にできるかなと思っていたけれど、失敗。
ここでもByte配列を使用することにした。
できたのがこちら。

Function NBSP2SP(strAsString)AsStringDim nbsp(0To1)AsByte
    nbsp(0)=160
    nbsp(1)=0
    NBSP2SP =Replace(str, nbsp," ")EndFunction

実際に使ってみた。

Subサンプル()
    Debug.Print"--普通にTrim出力--"
    Debug.PrintTrim(Range("A1").Value)
    
    Debug.Print"--NBSPを除去してTrim出力--"
    Debug.PrintTrim(NBSP2SP(Range("A1").Value))EndSub

結果はこのとおり。
f:id:t-hom:20160920144107p:plain
きれいにTrimされている。

今回のようなケースは、ブラウザからExcelに文字列を張り付けた際に発生する場合がある。
特に、表のマージンをCSSではなくnbspで調整しているケースで、表をそのまま文字選択してExcelに張り付けるような操作をすると発生する。
受け取ったExcelデータにTrimできない余分なスペースがあったら、VBAで除去しよう。

だれだ、俺の紅茶に砂糖を入れたのは!まずい。。

$
0
0

まぁ、実際に入れられたわけじゃないんだけれども。

今朝コンビニでサンドイッチと紅茶を買った。こちらの商品

キリン 午後の紅茶 あたたかい ストレートティーPET 345ml×24本

キリン 午後の紅茶 あたたかい ストレートティーPET 345ml×24本

実は私、砂糖入りの紅茶が大嫌いで、これまで注意深く避けてきたのだが、「ストレート」の表示で騙されて砂糖なしと勘違いして買ってしまった。

ストレートって言ったらふつうは砂糖なしでは?
喫茶店で買ったらそうなるやん。

まあ目を凝らしてよく見たら、うっすらと小さな文字で「甘さすっきり」とあるので、多少なり砂糖が入ってるのはわかるんだけど。。。

赤に金はひどい。
甘さすっきり
amazonの商品写真ではクッキリ見えるけど、見る角度によってはホントこんなもんだから。。

コーヒーには「加糖」または代替表示として「微糖」などの表示が義務付けられているらしいが、紅茶に表示義務はない。
たぶん、何も入れない派というのはマイノリティーなんだろうね。

だから「砂糖入りの紅茶なんて飲めない」って感覚がなかなか理解されないのだが、たとえば緑茶に砂糖が入ってたらどうだろう。まあなかには飲めるって人もいるだろうけど、日本人なら「オエーッ。。」って人が大半だろう。

ちなみにタイでも緑茶はポピュラーな飲み物だが、信じがたいことに「ノンシュガー」と書かれていない限り基本的には砂糖が入っている。
タイランドでは、緑茶は砂糖入りが常識なのだ。

あと茶の話からそれるけど、ヨーロッパでは基本的にミネラルウォーターは炭酸入り。
日本のみたいにシュワッと気持ちいいやつじゃなくて、何回か振ったみたいな気の抜けた炭酸。

オエーッ

これは天然の炭酸水で、ふつうに採取すると炭酸入りなんだとか。
※10年前に現地のガイドに聞いた話なので嘘かホントかはしらないけど、「ノーガス」と言って買わないかぎり、水は炭酸が入っているのはマジな話。


さて、話を茶に戻そう。
イギリスでは紅茶は砂糖入りがふつうらしい。イギリスではストレートはミルクやレモンが入っていないことを指し、砂糖の有無は問わないとのこと。
でもココは日本。ふつう、ストレートといえば無糖ですよ!
良かれと思って砂糖を入れたのかもしれないが、味覚は人それぞれなので私みたいに砂糖入りが大嫌いな人間も居るんだ!

ということでキリンのサイトからクレームいれてきた。
なにを飲み物くらいで大人げないって?

いや、私のいうクレームは英語本来の意味「主張」に近いものであって、「苦情」ではない。
【参考】クレーム(苦情)はclaim にあらず (上級 ★★★)

どちかといえば意見具申。
次なる被害者を出さないためにも、ぜひキリン様には「砂糖入り」を目立つように表示してほしい。

マイノリティーは積極的に要望を声に出していかないと、なかなか理解されない。
企業で働いてるとなんとなくわかるんだけれど、商品会議とかでも10人にひとりくらい、「砂糖入りって表示したほうがいいんじゃないですか?」なんて発言をするマイノリティーはいたんじゃないかと(願望も込みで)思う。でも声の大きいマジョリティーに「おまえの感覚がおかしい。紅茶はふつう砂糖いれるだろ。」なんて感じで普段から黙殺されてるんだ。きっと。

まぁ自分がわりと変わっているとか、変なところにこだわるねとか言われる方なので、マイノリティー代表として彼・彼女らへの援護射撃の意味も込めて、きちんとこういう人間もいるんだぞということを企業様にアピールしておかないとね。

というのが、私流のクレーム。
140円返せ!なんて大人げないことは言ってないからね?

10人の会議で1人が違うこと言ってるってことは、全国1億人のうち1000万人がそう思っててもおかしくないってことだろう。
変わり者の意見だからって無視して良いハズはない。私に言わせれば世間の9割の人間は変わり者なんだから。

ということでマイノリティーのみなさん。我慢せずに要望はきちんと企業に提出していこう。
いわゆるクレーム(怒)ではなく、ちゃんとClaimとして。

改善されるかどうかはしらないけれど、少なくとも一定の割合でヘンな奴が居るということは認知してもらえる。だいたいマジョリティーは自分の意見が絶対的に正しいなんて思ってたりするんだけど、「ひょっとしたら違う意見もあるのでは」と気付いてもらえるだけでも今後の新商品が違ってくるかもしれない。

ということでキリンさん、ペットボトル入り正真正銘の完全ストレートティーホット、お待ちしております。
【公開日19時に訂正】ごめんなさい。ありました。。

キリン 午後の紅茶 おいしい無糖 PET (500ml×24本)

キリン 午後の紅茶 おいしい無糖 PET (500ml×24本)

上はホットじゃないけど、ちゃんとコンビニにはホットが売ってた。

少数派に幸あれ!

VBA 脱初心者を目指す ~ Functionを使いこなすにはHowではなく、Whatに注目する

$
0
0

VBAではFunctionプロシージャを使いこなせるようになったら、中級の域に達したと言っていいと思う。あくまで個人的な意見であるが。

さて、Functionプロシージャの仕組み自体は、それほど難しいものではない。ただ初心者の方と話をしていると、Functionを使うと「あっちいったりこっちいったり」するので難しいとのこと。

ひょっとするとコードを読み込んでいくなかで、Functionの中身まで読んでしまうことが難しいと感じる原因かもしれない。

こういうコードの読み方をしていないだろうか。
f:id:t-hom:20160925031617p:plain

Functionはプロシージャのコードが見えてしまっているがために、中身を理解しようとして、本筋に戻った時に「あれ?なんだっけ」となりやすい。

Functionプロシージャはそういう読み方をするものではない。
むしろ、コードを隅々まで読みたくないがために、Functionプロシージャを作るのだと考えることもできる。
※それがFunctionの主目的ではないが、副次的なメリットである。

Functionを使いこなすにはHowではなく、Whatに注目する

VBAの組み込み関数を使用するケースをイメージしてほしい。
たとえば文字列を左から任意個切り取る、Left関数など。
使うときにいちいち仕組みがどうなっているか気にするだろうか。
まあ一度くらい気にしたことはあるかもしれないが、大抵の場合意識するのは「What(何がなされるか)」であって、「How(どのようになされるか)」ではない。

先ほどのLeft関数でたとえば、以下のプログラムを作るとする。

Sub Left関数サンプル
    MsgBoxLeft("Hello, VBA",5)EndSub

この場合、「"Hello, VBA"に対して左から5文字切り取られる」ということは意識しても、内部でどのように文字列を加工しているかは意識しないだろう。

自分でFunctionプロシージャを作るときも、基本的な考え方は同じである。
Functionを作るときはもちろんHowを意識する必要があるが、いちど作ってしまえばWhatを覚えておくだけで良い。

他人の書いたFunctionを読む

他人の書いたプログラムを読むとき、Functionが多用されているケースはどう読むのか。
メインコードから順にトレースしていきFunction呼び出しが出てきたときにその中身を確認する方法もあるが、この読み方だと「あっちいったりこっちいったり」になってしまう。

この読み方が難しいのは、メインコードのロジックを一旦脇においてFunctionの中身を確認する際、Functionのロジックに集中すると、脇においていたメインコードのロジックを忘れてしまう点にある。
あるいはメインロジックとFunctionのロジックがごっちゃになってこんがらがる。

個人的におススメする読み方は、まず最初にFunctionを読んでWhatを把握してしまうことである。
覚えておくのが難しければ、最初に理解したタイミングでコメントを書いてしまえばよい。

中にはFunctionの中でさらに別のFunctionを読んでいるケースもある。
難しいと感じる場合は一度呼び出し関係を図にしてしまうといい。

【参考】
thom.hateblo.jp

自分でFunctionを作るときに気を付けたいこと

まず一番大事なのは、Function名である。
そのFunctionが何をするものなのか、つまりWhatを適切に表現した名前を付けることで、中身を読む前にその関数の働きを想像することができる。
また、一度読んだ後に名前と結び付けて記憶に残りやすい。

myFunc1といった適当な名前を付けてしまうと、結局内部動作とmyFunc1という関数名の結び付けを頭の中で無理に覚えておかなければならない。
名前から自然にその働きが想起できることが重要である。ひねったりせず、安直で一目瞭然な名前を付けること。

また、自分だけでなく他人が読んだときにも分かりやすい名前にしておくことが重要だ。
ためしにFunction名だけ同僚に見せて、何をする関数なのか当ててもらうと良い。
的中または惜しい答えが返ってきたら、それは理想的な名前に近いと思われる。
「いや、そのまんまやんけお前!」とツッコミをいただいたら100点だ。

GetApplicationConfigurationStringやSaveAllChangeSetToDatabaseなんていう長い関数名の事例もある。
以下のページ、トピ主は長すぎる関数名に違和感を感じているようだが、対するコメントはおおむね「省略すべきではない」との見方が多い。

【参考】長い関数名、変数名、どこまで許せる? | スラド デベロッパー

VBAなら日本語でFunction名を付けることもできる。
例) GetApplicationConfigurationStringの代わりに「アプリ設定の文字列を取得」という関数名
個人や内輪で使うツールなら日本語を使っても問題ない。

次に大事なのは、参照透過性(さんしょうとうかせい)である。
これについては過去に書いた以下の記事を参照してほしい。
thom.hateblo.jp

まとめ

  • Functionは、Whatを把握するために読む。WhatがわかればHowは忘れて良い。
  • 呼び出し関係が分かりにくいときは図にまとめよう。
  • 自分でFunctionを作るときは、その働きを想起させる適切な名前を付ける。
  • Functionはなるべく参照透過に設計する。

VBA オブジェクト変数の宣言時にNewすると何がまずいのか

$
0
0

オブジェクト変数の宣言と使用については、以下の2パターンが存在する。

■パターン1 宣言と同時にNewしてしまう方法
Dim C As New Collection

■パターン2 宣言とオブジェクトのSetを分ける方法
Dim C As Collection
Set C = New Collection

私はこれまで大体のケースでパターン1を用いてきた。そのほうがコードが短くまとまってスッキリするからだ。

たかが1行とあなどるなかれ。1つのプロシージャのサイズはスクロールせずに全体が見渡せるくらいの長さが理想的であるが、特にノートPCのように画面が狭い環境では1行といえど貴重なスペースである。

ところがちょうど今日、VBAのお膝元であるMicrosoftのサイトでVB6.0においては変数宣言時にNewするのは避けるべきであるとの記述を発見してしまった。

https://msdn.microsoft.com/ja-jp/library/dd297716.aspx

VBAも実質はVB6.0なのでもちろんこれに該当する。

短く書けなくなるのは残念であるが、MSの公式見解である以上は方針転換を検討せざるを得ない。

そもそも変数宣言でNewした場合の動作について

前述のページには「Visual Basic 6.0 では、このコードのある行ではオブジェクトは生成されずに、それを利用するタイミングに、オブジェクトがすでに作られているかどうかをチェックし、なければ作成されるというものでした。これによる利点と欠点は次の通りです。」とある。

まぁここまでは知っていた。
驚いたのはここからで、「必ずオブジェクトが存在することが保証される (Nothing を設定してオブジェクトが破棄されたとしても、オブジェクト変数を再利用しようとすると、再作成される)」という記述である。

実際にやってみた。

Subあれれー()Dim C AsNew Collection
    Set C =Nothing
    C.Add"あれ?なんでNothing入れたのにAddできるん?"MsgBox C.Item(1)EndSub

マジだ。

オブジェクトを明示的にNothingで破棄しているにもかかわらず、C.Addができてしまう。

ちなみに、念のため宣言とNewを分けて書いてみたところ、エラーになる。これは予想どおり。

Subこっちはエラーになる()Dim C As Collection
    Set C =New Collection
    Set C =Nothing
    C.Add"このAddは無理"MsgBox C.Item(1)EndSub

さて、この実験により変数宣言時にNewするとオブジェクト変数にNothingを設定しても、そのオブジェクト変数を再利用しようとすると自動でオブジェクトが生成されてしまうことが判明した。

ところで、オブジェクトの利用とはプロパティの設定やメソッドの実行だけを指しているのではない。単にオブジェクト変数を参照するだけで利用したことになってしまうのだ。

以下のコードはオブジェクト変数にNothingを代入しているにも関わらず、Nothingと比較するとFalseになってしまうというもの。

Sub Nothingになってくれない()Dim C AsNew Collection
    
    Set C =Nothing
    Debug.Print C IsNothingSet C =Nothing
    Debug.Print C IsNothingEndSub

Set C = Nothingの時点ではちゃんとNothingが入っているけど、いざ比較しようとしたらやはりオブジェクト変数の中身を参照する必要があるので、その時点で中身が作られてしまいNothingではなくなってしまうのだ。

つまりCはNothingになるけれど、観測した瞬間Nothingではなくなってしまう。なんだか量子力学にでてくるシュレーディンガーの猫の話と似てる。まぁ、VBAの場合はローカルウインドウを使えばちゃんとNothingになっている瞬間を観測できるけどね。

オーバーヘッドについて

変数宣言するときにNewした場合、オブジェクト変数が参照されるたびにNothingかどうかをチェックするのでオーバーヘッドが発生する。
MSDNには変数宣言時にNewする方法について、「オーバーヘッドが大きいので、使うべきではありません。」と書かれているが、はたしてどれくらい違うものなんだろうか。

実際にやってみた。

Sub変数宣言とNewを分ける()
    t =TimerDim C As Collection
    Set C =New Collection
    For i =1To10000000
        C.Add i
    Next
    Debug.PrintTimer- t
EndSubSub変数宣言と同時にNew()
    t =TimerDim C AsNew Collection
    For i =1To10000000
        C.Add i
    Next
    Debug.PrintTimer- t
EndSub

結果は、大して変わらない。
いちおう変数宣言とNewを分けた書き方の方が早かった。
とはいえ、1000万回ループさせてやっと有意差が0.3~0.4秒なのでオーバーヘッドによる遅延は微々たるものだ。

変数宣言時にNewするとコンストラクターの発動タイミングが変わる。

実はDim C As New Collectionが実行されたタイミングではまだCにはCollectionが入っていない。
実際にCollectionが変数に格納されるのは、一度でもそのオブジェクト変数が利用されたときだ。

まぁ有名な話なので知ってる人は知ってると思うが、ここで今一度実験によってその挙動を明らかにしておきたい。
調査するためには、Collectionでは分かりにくいので自作のクラスにコンストラクターを実装して確認してみよう。

コンストラクターとは、オブジェクトが生成されたタイミングで自動的に発動される特殊なプロシージャである。

クラスモジュールClass1を作成し、次のコードを張り付ける。

PrivateSub Class_Initialize()
    Debug.Print"オブジェクトが生まれました。"EndSubSub hoge()
    Debug.Print"hoge"EndSub

次に標準モジュールのコード

まずはMicrosoftの推奨する方法で行儀よく。

Sub宣言とNewを分けた場合()Dim c As Class1
    Debug.Print"標準コード1"Set c =New Class1
    Debug.Print"標準コード2"
    c.hoge
EndSub

結果はこうなった。

標準コード1
オブジェクトが生まれました。
標準コード2
hoge

ちゃんとNew Class1とした瞬間にオブジェクトが生まれている。

では次に変数宣言時にNewした場合。

Sub宣言時にNewした場合()Dim c AsNew Class1
    Debug.Print"標準コード1"
    c.hoge
EndSub

結果は以下のとおり。

標準コード1
オブジェクトが生まれました。
hoge

ほら、宣言時のNewではオブジェクトが生成されておらず、次の標準コード1が表示されている。
そしてc.hogeと命令を出した瞬間、命令よりも一瞬早くコンストラクターのコードが実行される。

次に、メソッドの実行などを行わずにオブジェクト変数の参照だけするケース。
変数のアドレスを調べるVarPtr関数を使ってみた。
ザ・参照!って感じの関数なのでちょうど良いかと思って。

Sub宣言時にNewしたのち参照だけした場合()Dim c AsNew Class1
    Debug.Print"標準コード1"
    Debug.Print VarPtr(c)
    Debug.Print"標準コード2"
    c.hoge
EndSub

結果は次のとおり

標準コード1
オブジェクトが生まれました。
 3141520 
標準コード2
hoge

この場合も、初めて参照されたタイミングでコンストラクターが実行されている。

オブジェクト変数の宣言時にNewしたことで発生しうるバグ

まあ、変数宣言時にNewしたからといってそれが原因で起こるバグというのはあまり考えにくいのだけれど、そこはあえてバグが起きるコードを考えてみた。
※バグはエラーとイコールではない。作者の意図しない動作はすべてバグである。

次のようなクラスモジュールを作ってみた。
コンストラクターでSheet1のA1セルにHelloを書き込み、デストラクターでGood Bye!を書き込む。

PrivateSub Class_Initialize()
    Sheet1.Range("A1").Value="Hello"EndSubSub hoge()
    Sheet1.Range("A1").Value="hoge"EndSubPrivateSub Class_Terminate()
    Sheet1.Range("A1").Value="Good Bye!"EndSub

そして標準モジュール。
まずは宣言とNewを分けた場合。

Sub宣言とNewを分ける()
    Sheet1.Range("A1").Value="Start"Dim c As Class1
    Set c =New Class1
    Debug.Print Sheet1.Range("A1").Value
    c.hoge
    Debug.Print Sheet1.Range("A1").ValueSet c =Nothing
    Debug.Print Sheet1.Range("A1").ValueEndSub

結果はこうなった。

Hello
hoge
Good Bye!

最初にA1セルにStartを入れているが、cにNew Class1を代入した時点でコンストラクターによりA1にはHelloが入るため、Debug.Printで最初の出力はHelloとなっている。

次に宣言時にNewした場合。

Sub宣言時にNewする()
    Sheet1.Range("A1").Value="Start"Dim c AsNew Class1
    Debug.Print Sheet1.Range("A1").Value
    c.hoge
    Debug.Print Sheet1.Range("A1").ValueSet c =Nothing
    Debug.Print Sheet1.Range("A1").ValueEndSub

結果はこうなった。

Start
hoge
Good Bye!

変数宣言時にNewしているためここではまだオブジェクトは生成されず、A1の値はStartとでる。
次にhogeが実行される瞬間、コンストラクターによりA1の値がHelloに書き換えられるが、直後にhogeが実行されてA1の値はhogeになってしまう。

折角コンストラクターを作ったのに無意味な動作になってしまったため、これはバグだと言える。

あるいは別のシチュエーションとして、すでにNothingを代入して破棄したオブジェクトを間違って参照してしまったとき、そのコードが動いてしまうことだ。
さらにまずいことに、内部ではオブジェクトが再作成されるため、再度コンストラクターが実行されることだ。
まあこれはプログラマーのミスなのであるが、変数宣言とNewを分けておけば、破棄したはずのオブジェクト変数に再アクセスしてしまった際にちゃんとエラーで知らせてくれる。

まとめ

現実にはコンストラクターでRangeを書き換えるなんて特殊なことはやらないと思うので個人的には変数宣言時にNewを使って問題が発生するようなシチュエーションはめったにない。
ただし、今後も問題が発生するようなコードを書かないとは言い切れないし、Microsoftが推奨していない以上は変数宣言時のNewはやめたほうが良いだろうなと思う。
ということで、今後は変数宣言時のNewは封印しようと思う。

Viewing all 493 articles
Browse latest View live