はもちくわ

コードについて自分なりの解釈を書いてます。

【ExcelVBA】セルの値を戻すを実装してみる〜なんちゃってアンドゥ(Undo)

 

さて!みなさんは、ExcelVBAで処理をさせているとき、「ここで入力された値によって、セルの値を元に戻したいなぁ。。。」って思う時ないですか?

でも、VBA実行後は「元に戻す」が使えないですよね。。。

じゃあ、Undoメソッドでコード側からなんとか制御できないものかと、、、、残念ながら、これもVBA処理は元に戻せないようです。

私は使ったことないので詳しくないですが、Microsoft Learnによると、このUndoメソッドはプロシージャの一番上、つまり、コードの最初に書かねばならないみたいです。調べてみると、このメソッドは、直前のユーザー操作を元に戻すことができるようですが、ユーザー操作とUndoメソッド実行の間に、VBA処理が入ってしまうとエラーになるらしい。なるほど、それで最初に書くのか、、、、でも、これだと使い道が限られちゃいますね。。。
じゃあ、今回みたいに入力された値によって、自由に操作したいんだよなぁ〜ってとき、どうしましょう??

 



そこでっ!上書きされたセルの値を元に戻す方法をご紹介します。
ただ、この方法は、Excelのリボンにある「元に戻す(Ctrl+z)」とは違い「なんちゃって」です。
前の操作を取り消して、完全に元に戻すわけではありません。その上、しっかり制御しないと、期待した結果にならないこともあります。なので、実装する際にはご自身でデバッグしながら調整が必要です。
また、おそらくこの方法は、大正解じゃないと思います(笑)きっと他にもいろいろ良い方法があるはず!
なので、「こんな方法もあるんだね〜」って、温かい目でご覧ください(笑)




はい、それではやっていきますが、、、実は、今回、コードの紹介というより、どうやって「値を元に戻す」を「表現するか?」を考える感じです。


ここで大事なのは、「直前の処理をなかったことにする」ではなく、、、「セルの値を元に戻す」ことだけを考ることです(笑)
はい、そうなると簡単ですね!
「処理の巻き戻し」なんて難しいことは考えずに、上書き入力されて変更された値を、上書き前の値で、再度上書きすればいいわけです!

それでは、実現したい状況を考えるために、
値が入力されているセルに、上書きしたあと、「元に戻す」処理に至るまでの、人(ユーザー)の動き、対象セルの値、Worksheetイベントのタイミングを、並べてみてみます。

こんな感じでしょうか。
この、③のChangeイベントで前の値を呼び出せればいいわけですね。では、このまま③のChangeイベントで前の値を呼び出せるでしょうか?
上の表をみてください。③のとき、すでに値が上書き後になってしまっています。残念ながら、タイミングがちょっと遅いみたいです。

では、他のタイミングを考えましょう。
上の表の中で、上書き前の値がセルに残っていて、さらに記憶させる処理を実行させるきっかけも一緒にあるのはどこになるのか、、、

はい、①だけしかチャンスはありませんね!

いやいや、②ならまだ値をゲットできんじゃね??って考える方もいらっしゃるでしょうか。
確かに、Excelの入力操作だけ考えてみると、上書き入力を開始して、前の値が消えたとしても、値の確定前にキャンセル(ESC)すれば、上書き前の値に戻りますから、どこかに前の値が隠れているかもしれません。
でも、残念ながら、この②のタイミングで発生するシートイベントがないんですよね。。。。
やはり、①のタイミングしかないですね!



さて、覚えるタイミングは①にしたとしても、、、、これだと、ちょっと困ることがありますよね。
そうです。プロシージャが①「前の値を覚えておく処理」と、③「元に戻す処理」で分かれてしまします。
プロシージャで内で宣言された変数は、宣言したプロシージャでしか使えませんから、前の値を変数で覚えておいても、元に戻す処理の時には変数の値を使えないことになります。
じゃあ、画面外の見えない場所を使って、任意のセルに前の値を覚えさせちゃいましょうか??、、、、まあ、それはそれで一つの方法だとは思いますが、シート上のセルって、行、列の挿入や消去に巻き込まれて場所が変わったり、消えちゃったりなど、結構簡単に崩されてしまうので、汎用に向かないと言うか、、、まあ、あまりおすすめできません(笑)

なので!!変数自体を「プロシージャを超えて使える」ようにします。

 

やり方としては、二通り方法があります。

どちらも同じ動きが期待できますが、変数の使える範囲が違ってきます。
1つ目は、モジュール内のプロシージャならどこでも使え、もう1つは、プロジェクト全体で使える方法です。

まず、モジュール内で使える方法です。

このように、プロシージャの外側でDimして宣言すればOKです。モジュール内では一番上になるようにします。簡単ですね。こんなラクチンなら、プロシージャごとに変数宣言しなくても、変数全部ここで宣言しちゃえば楽でいいじゃん!って思っちゃいそうですが、、、、まあ、実際にやってみたらわかりますが、そんなことすると、楽になるどころか、メチャメチャコードが難解になります(笑)
変数の値が追っかけづらくなるんです。

きっと、「あれ?値がおかしい?!どこで変わった?!もう!なんでこんなに見にくいのっ?!」

ってなります(笑)やはり、プロシージャごとに考えられるほうが断然楽っすよ〜。

 

そして、もう一つのプロジェクト全体で使える方法です。

先程のモジュール内で使いまわせる変数と書く場所は一緒ですが、宣言にはPublicを使います。このPublicはどこのモジュールに書いても良いです。
だからと言って、あちこちに書いちゃうと、、、、
「あれ?どこに書いたっけな。。。テヘペロ
ってのは嫌われますよ(笑)しっかりルールを決めて、書く場所は一箇所にまとめておきましょう!

あと、どちらにも言えることですが、使える範囲が広いっていうことは、言い換えると、変更されやすい変数と言えます。なので、通常使う変数と名前の付け方をかえるとか、わかりやすくしておくとよいと思います。あとは、ややこしくなるのでなるべく増やさない(笑)

ところで、ちょっと話がずれますが、VBAでコード書く時、変数の「宣言」を強制して使ってますか??
VBAって「宣言」しなくても変数使えちゃいますから、、、、わざわざ宣言するのは面倒くせ〜って思っちゃいますよね。。。でも、しっかり「Option Explicit」やって宣言を強制しておくと、コンパイルの段階で変数名の間違いをチェックできるので、エラーがずいぶん減ると思います。だまされたと思って、「Option Explicit」して、宣言を強制してみてください。デバッグ回数も減りますよ〜!




それでは、実際に私が作ったツールのコードを参考に、配置について説明していきます。コード自体はとても簡単です。
まずは値を覚える処理。

ちょっと、余分な処理が入っちゃってますが、元に戻すための値を覚えるだけなら、ActiveCellの値を、そのままモジュールレベル、パブリック変数に入れちゃえばいいです。
私のツールでは、値を覚える処理の他に、別の処理も並行してさせていたため、わざわざ別変数を準備しているだけです。
なので、覚える処理としては、コードを配置する場所をSelectionChangeにし、アクティブになっているセルの値をモジュールレベル、パブリック変数に入れるだけです。これで選択されるたびに値が保存されるわけです。
これ、ActiveCellではなく、Targetを使っても良いです。ここでActiveCellになっているのは、私の作成したツールが、値によってセル位置変更など、別の処理も多数つけていたので、意図していたセルからずれてしまうことがあったためです。
まあ、これは個人的な感想ですが、ActiveCellのほうが、ユーザーから見た挙動に沿った動きになりやすいですかね。
このように、シートイベントきっかけで始まる処理は、考えている以上に、他の処理によって邪魔が入り、挙動が変わります。ですから、実際に使う環境で動かしてみて、いい方を選択すると思っていたほうが良いでしょう。

次にChangeイベントで、元に戻す処理ですね。

 

私のツールの中では、値が指定した数字の並びだった時、TargetのValueに上書きしています。このように、変数の使い方は呼び出すだけでいいのでとても簡単です。

基本的な使い方としてはここまでです。
コード自体はとても簡単で、使い方だけ考えればいい感じでした。
変数の使える範囲(スコープ)を広げて、シートイベントごとに覚える、引き出すを実行すればよいだけでしたね。


ですが、、、、先にも書いてますが、、、、実は、実装するにはしっかり制御しないとうまく使えないです。
これは、「こうすれば解決!」っていうものでもなく、実際に使う環境で一度動かしてみて、意図した動きにならなかったら対策するという手法しかないかと思います。

実際に私の作ったツールで対策した実例をいくつかあげておきます。

その1 セルの選択がされずに変更される

これはSelectionChangeが働かないので、前の値が記憶されません。よく起こるのは、シートを選択したら、ちょうど変更したいセルだったという場合。なので、シートイベントのActivateを使って、シートがアクティブになったら、タイトルや項目など、ユーザーの変更対象にならないセルを一回選択するようにして、セルの選択が絶対に実行されるように仕向けたり、ActiveCellの値をActiveteイベントのたびに覚えておくなどで回避します。シートの移動が多い場合、変更対象外のセルを選択する方法を取ると、入力の邪魔になる可能性が高くなるので、最初の一回だけ実行するスイッチを入れるなど工夫が必要です。

その2 範囲選択される

場合によってはエラーになります。Targetのアドレスを取得して、アドレスに「:」(コロン)が入っていたら処理をしないなど工夫が必要です。

その3 イベントを停止している場合

シートイベントのChangeやSelectionChangeを使う時は、コード上で値を変更したり、選択したりすると、その度にイベントが発生して無駄なループが発生します。また、場合によっては無限ループになることもあるので、Application.EnableEvents=Falseでイベントを停止させてから処理を実行させ、終わったらTrueでイベントを再開させるのが得策ですが、これが仇になって値が記憶されない場合があります。セルの値を記憶させたい場所があれば、その箇所だけイベントを再開させるか、その箇所で値を覚えておく処理を入れ込む必要があります。

その4 前の前の値が残ってる

これはよくやっちゃいますね〜。モジュールレベル、パブリック変数は値が変更されやすいって先に書いてますが、値が残り続けるものでもあります。なので、前の値を覚える、破棄するは自動じゃありませんので、その都度必要かどうかを判定して、処理を書く必要があります。

その5 エラーで処理が止まると値が消える

このエラーというのは、該当のプロシージャだけのことではなく、すべての処理についてです。モジュールレベル、パブリック変数を使って値を覚えたとしても、何かしらのエラーが起こって、処理が止まり、デバッグを選択しなかった場合、その時点で値は消えてしまいます。。。こればっかりは気をつけてとしか言いようがない(笑)
あと、、、別件ではありますが、イベント停止を実行してから、処理がエラーで止まった場合、イベントも自動で再開しません。
エラーを修正してから、再び動かしたとき、イベント全部が止まっていて、ビックリなんてこともありますので、イベント再開は手動でできるように復旧用のプロシージャを準備しておきましょう。



さあ、いかがでしたでしょうか?
コードは簡単なんですが、シートイベントはまあまあ面倒くさいですよね(笑)

最初の方にも書きましたが、このシートイベントを使った処理は、どうやって使うのか?によって、しっかり制御しないと、楽するためのツールなのに、面倒なルールをつけないと使えないという、なんだか奇妙なものになる可能性が出てきてしまいます。
過去記事の「Changeイベントのくせもの」でも書いてますが、とにかく面倒なので、導入する価値があるのかどうかはしっかり考えたほうがよいです。

それでは、今回はここまでです。
しかし、このところの物価高。。。おかげさまで、バイトひとつじゃ生きていけなくなりまして(笑)
早朝に掛け持ちバイトぶっ込んでなんとか食いついないでます。
もう毎日眠くて眠くて(笑)
ブログ更新も時間がかかるようになってしまいました。。。。やはり、こういったことは余裕がないと続かないですね〜。。。。って愚痴ってみました。
それでは、ここまで読んでいただきありがとうございます。