最近VBAでマクロを組んでいます。
(VBAとマクロの関係、これで合ってますかね 汗)
久しぶりに触って、「処理が重たくて固まって応答なしになる」という現象に苦しめられたので、対処方法をメモがてら記事にしてみようと思います。
テキストだけで分かりにくいのですが、後日時間があるときに加筆修正する予定です(しないかも)。
VBAが固まった処理
Excelファイルにシートが12枚。それぞれ、ひと月分、約30日のデータが記入されています。
ただし、それは1行=1日という形ではなく、印刷用の形になっていて、縦に数日分×横に数日分のような形で、データの絞り込みなどはできない形式です。
そこで、それを新しいシートにコピーしてきて、1行1レコードという、データとして扱いやすい形に変形するという処理を書いていました。
データを整えると1日が3レコードになります。
よって、データ件数は、
365(日) × 3 ≒ 1000(件)くらいになります。
小さくテストしながら作っていこうと思って、最初は1レコードだけコピーする処理を作りました。
次に、それを縦に並ぶ数だけループ(イ)
↓
さらに、横に並ぶ数ループ(ロ)
↓
最後に、シート12枚ループ(ハ)
という順で、冗長な処理をまとめてループ処理にすることで、コードの量を少なくしました。
それで、(イ)はもちろん問題なく動いて、(ロ)も、まあまあ処理に時間がかかるなと思ったものの、しばらく待てば処理が終わりました。
ちゃんと計ってないですが、数十秒くらいの感じです。
ところが、(ハ)のループをつけて、それでも計算上は数分で処理が終わるはずのところ、マクロを走らせるとExcelが固まるようになってしまいました。
白くなって、何の操作も受け付けなくなるやつです。
VBAが固まるときの対処方法
VBA DoEvents
対策を調べて最初に出てきたのが、DoEventsという処理を追加するという方法。
これは、処理を速くするというよりは、固まってしまうのを防ぐ方法のようです。
VBAの処理が始まると、パソコンはVBAの実行に集中してしまい、「操作を中断してくれ」というようなOSからの指示ができなくなってしまうことがあります。
そこで、「DoEvents」をコードに書いておくと処理をいったんOSにさせることができ、「Ctrl + break」などで処理を中断することができる、と。
「DoEvents」自体が重たい処理なので、処理の実行時間自体はむしろ長くなってしまうみたいですね。高速化が目的ではなく、応答なしになるのを回避するために使われるようです。
一応こちらを使ってみると、画面が真っ白になるのは防ぐことができましたが、処理を完了させることはできませんでした。
VBA 画面更新の停止
次に画面更新を停止すると処理が速くなるという情報がでてきました。
そういえば、昔VBAを触っていたころにも、そんなコードをよく見かけた気がします。
やり方は、「Application.ScreenUpdating = False」を使って画面更新を停止します。
画面の描写も結構大変な処理なので、それをやめるだけで処理は軽くなります。
しかし、これをやっても効果はパッとしませんでした。それは、次に書く根本的な原因が解決されていなかったからです。
ちなみに、「Application.ScreenUpdating = True」にして、すぐに「Application.ScreenUpdating = False」とすると、その瞬間の情報で画面を更新してくれるようです(必ずしも処理が全部終わるまでFalseにせず、途中経過をTrueで表示してもよいということ)。
VBA SelectとSelectionを削除する
これが全くの盲点だったのですが、「Sheets(“[シート名]”).Select」と「Selction.」を使うというのは、必要のない記述だったんですね。
同じシートでいくつか処理をするときに、これを先に使っておけばシート名を一回一回書かなくて良くなるので、見た目がスッキリしてよいなと思っていたのですが、処理上は大違いです(一緒だと思っていました)。
「Sheets(“[シート名]”).Cells」と書いてもシートの切り替えは発生しないので、処理は重たくなりません。
ところが、「Sheets(“[シート名]”).Select」は書くたびにシートの切り替えが発生してしまいます。この画面の切り替えというのが、たいそう重たい処理らしいのです。
私の場合、上に書いた(イ)の部分の処理で「Sheets(“[シート名]”).Select」でコピー元のシートの値を保持して、「Sheets(“[シート名]”).Select」でコピー先のシートに値を入れるということをしていました。
(イ)の処理1回だけで、2回も画面の切り替えが発生していたのです。
これを削除して「Sheets(“[シート名]”).Cells」の形式にするだけで、とんでもなく高速化が実現しました。
これまで数分待っても処理が終わらなかったのが、(イ)~(ハ)まで通して処理しても、改善前に(イ)だけやるのと変わらないのではないかというくらい、一瞬(は言いすぎですが)で処理が完了してしまいました。
感動ものです。
対処法まとめ
無駄な多重ループなどはない前提で、以下のような順番で見ていけばよいのかなと。
まず、画面の切り替えが発生する「.Select」がコードに書かれている場合は削除する。
「.Select」でシートを指定する代わりに、「Sheets(“[シート名]”).」を各処理の頭に付ければよい。
それでもやむをえない画面の切り替えによって処理が遅くなっていて、少しでも速くしたいのであれば、「Application.ScreenUpdating = False」を使用。
処理件数が多いなどで上記でも処理が固まってしまう。応答なしで「Ctrl + break」も受け付けないという場合は、「DoEvents」を適度に挟んで回避する
終わりに
パソコンのスペックが足りないのかとか色々考えたのですが、私のケースでは「select」が諸悪の根源でした。
これを一番内側のループに書いていたので、1000件のレコードで1000×2(コピー元を表示するのと、コピー先を表示するので2回)回というとんでもない数の画面切り替えが発生していました。
selectを使うとコード1行1行で「Sheets(“[シート名]”).」を書くのを省けるので、コードの見た目上はすっきりするんですよね。
ところが、プログラムの実行上は害になるのだというのがよく分かりました。
今後は、見やすさだけでなく処理の速さの面も考えてコーディングしていこうと思います。