どうでもいいプログラム研究所

とある編集者によるIT、Web、ソフトウェア、プログラミングに関する雑記と覚え書き

情シスから嫌われないエンドユーザーコンピューティングのために

f:id:tdyu5021:20210207231150p:plain

情シスではなくエンドユーザー自身でプログラムを作って業務を効率化することをエンドユーザーコンピューティング(EUC)といいます。エンドユーザーにITのスキルがあれば会社にとって大きなメリットですが、本当に問題点はないのでしょうか。エンドユーザーと情シスの関係性で私が感じたことを書きます。

エンドユーザーコンピューティングの再来か

一般的に会社は情シス部門みたいなところがITツールの運用だったりプログラム開発を行っていますが、EUCという言葉があるように業務部門の1ユーザーが自分でプログラムを開発したりして業務を効率化しているケースもあります。

ちなみに私は5年前から現職(IT業界専門の制作会社)に転職し、IT業界を取材し続けていますが、実際のところ最近はこの言葉はあまり使われていないようです。この用語で検索しても過去の記事がたくさんヒットしますし、取材の中で「EUC」という言葉を言及する人は割と上の世代が多い印象です。

EUCの定義は私も厳密には把握しておらず間違っているかもしれませんが、最近は改めてEUCの流れが来ているように思います。

というのも、最近はエンドユーザーがITを取り入れるハードルは明らかに下がっています。Twitterを見ていると、直近では「ノーコード/ローコード」も頻繁にタイムラインに流れてくるので、これからそうしたツールはますます流行ってくるでしょう。DXの流れもあり、ノンプログラマーシステム開発に携わるチャンスは増えてくるかもしれません。

IT製品導入だって情シスのお世話にならずとも、いまや勝手にクラウドサービスを契約して利用することだってできます(シャドーITの別の問題が出てきますが)

あと、これは目新しいものではありませんが、どの人のパソコンにも入っているExcelでもVBAを使ってれっきとしたプログラミングができます。持っているユーザーは限られそうですが、Accessなどを使っても簡易的なシステムが作れます。

エンドユーザーコンピューティングにデメリットはないのか

Excel VBAによって業務を効率化した話はちまたに腐るほどありますし、そのメリットはいまさら語る必要もないでしょう。エンドユーザーがプログラムをわかっているに越したことはありません。ただ今日の記事の本題はその先です。

エンドユーザーがプログラムを作れることが当たり前の時代になったとき、どのような問題がでてくるかということです。

問題の1つは「シャドーIT」のリスクです。シャドーITとは広義には情報シスの管轄になくユーザー自身が業務で勝手に利用しているITのことです。わかりやすい例として「会社で正式認められていないが勝手にSlackを導入してチーム内のコミュニケーションに使っている」というのがあります。自分で密かにプログラム開発しているというのもその例です。

Slack程度なら会社内で許可をもらうことはそこまで難しくないかもしれません。もっとたちの悪い狭義のシャドーITは、商用利用前提でないものを業務で使うことです。例えば「私用のスマホの個人LINEで業務連絡する」というイメージです。

シャドーITを問題だとに認識するのはほぼ100%情シス部門です。一方で現場で勝手にシステムを作ったり使ったりする当のエンドユーザー本人はほぼこの問題に関心はないでしょう。

それも無理はありません。会社全体のITを管理下に置きたいのが情報システム部門の思いですが、エンドユーザーからしてみたら「自分の周辺の業務が効率化できればいい」というくらいにしか思っていないからです。

ですが、そうした感覚は非常に危険であり、今すぐにでも是正されなければならないと私は考えています。その理由は次の項で書きます。

とばっちりを受ける情シス

そもそも、エンドユーザーが勝手にシステムを導入したりプログラムを開発した場合、「その面倒を誰が見るのか」という話がつきまといます。いわゆる運用の問題です。もしEUCによって導入されたプログラムが社内に広まったとき、開発者はエンドユーザーであるにもかかわらず、付随する問題が生じて情シスに質問が寄せられるケースは往々にして存在します。

情シスからしてみたら「なんで自分がこの問い合わせ対応しないといけないんだよ」と思うわけです。ほかの人が勝手に始めたIT活用で自分の仕事が増えたらたまったものではありません。

もう1つがトラブルの対応です。情シスが絡んでいないシステムやプログラムに何か問題があり、それによって業務に影響が出たとします。Excel VBA程度ではあまりない思いますが、もしセキュリティの事故が発生したら、会社への影響は大きなものです。

ですが、そのときに情シスとしては「このプログラムはエンドユーザーが勝手に入れたものだから私たちは知らない」とは簡単にいかないわけです。情状酌量くらいにはなるかもしれませんが、情報システムに関する尻拭いは残念ながら情シスなのです。

つまるところ、EUCによってプログラムが作られたとき、それに対する責任が取れなければ社内に逆にいろんなところに迷惑になってしまう可能性があるというのが問題点なのです。

こうしたリスクを指摘したところで、実際には当のエンドユーザー本人の心にはそこまで響かないでしょう。会社全体や情シスに起こりうるリスクへの良心の呵責が大きければ響くかもしれませんが、現実的にそれは、リスクを気にせず勝手なシャドーITを入れて業務効率化するメリットを上回ることはないでしょう。

「会社の全体最適を目指すべき」とか「情報システム部門に迷惑をかけてはいけない」などの正論ではなく、自分の仕事やその回りの業務が楽になるかという自分へのメリットがあるかどうかの判断で人は動いてしまいます。

ここで念のため付け加えますが、もちろん情シスの統制の範囲内にあり、セキュリティリスクのない簡易かつ自分自身の業務を効率化する用途のVBAのプログラム程度であれば問題ありません。ここで私が想定しているのは、情シスが管理していなく、なおかつ社内のいろんな人がそのプログラムを使って、それがないと業務が回らないようなものを想定しています。

エンドユーザーが知っておくべきこと

ただ、エンドユーザーとしては短期的には業務効率化ができたとしても、長い目で見て自分に跳ね返ってくるデメリットもあります。とっつきやすいエンドユーザーのプログラミングとしてExcel VBAを例に考えてみましょう。

いくら自分が業務を効率化してメリットがあったからといって、それが間接的に情シスの迷惑となったらどうでしょうか。そしてそれが世の中のいろんな企業で起こっていたとしたらどうでしょうか。

結局「Excel VBA」はいつまで経っても「中途半端なシステム」として情シスやプログラマー界隈からないがしろにされるだけです。Excel VBAは正しく扱い、特定の用途には効果を発揮します。しかし、その領域をわきまえずそれで何でもそれで構築しようとすると、エンタープライズのシステムではどこかで限界が生じます。

それはいろんな企業のシステム導入事例で「脱Excel」というスローガンが叫ばれていることからもよくわかります。(※注:脱Excelは、VBAピンポイントの話ではなくワークシートでの情報管理から脱却する、という話が多いですが)

Excel VBAが嫌われてしまう理由

情シスだったりプログラマーの人からの意見として、Excel VBAは低い評価を受けることがあります。それはExcel VBAプログラミング言語として貧弱だからという単純な理由だけではないと私は考えています。

ネガティブな印象が出る理由として私が推測するに、おそらくExcel VBAとは、「ITがわからないところに1人だけVBAがよくできる人がいて、その人がどんどん1人で開発していった」というシーンで使われやすいことに関係があると感じています。

これはどういうことか、次のような流れを考えてみましょう。

まずVBA愛用者(VBAer)は、ITがわからない従業員ばかりの職場の中でバリバリ開発してヒーローのような存在になったとします。しかし、それは本格的なシステム開発をしていたり、全社最適なシステムを考える人達にとって中途半端な部分最適なシステムに見えてしまう可能性があります。

そのVBAerもせいぜい「セミプロ」のような存在に映ってしまうかもしれません(もちろんスキルによります)。

ですが、当のVBAer本人はIT音痴な職場を救った達成感や使命感に誇りを持つわけです。そしてそういう人は「情シスに頼らなくても、この部署のIT化は俺がなんとかする!」と思うに違いありません。

こうした「詳しい自分1人でなんとかする」タイプのエンドユーザーが、全体最適のITシステムと相容れないことは想像に難くないでしょう。こうして生まれたプログラムは部分最適を突っ走る形になり、それによってExcel VBAも煙たがられるのだと私は思います。

よくExcel VBAは「属人化しやすいからダメ」という批判があります。それに対する反論として「属人化するリスクはほかの言語でも変わらない」という意見があります。この反論は確かにもっともだと思うのに、なぜExcel VBAだけが属人化批判のやり玉に挙げられてしまうのでしょうか。

それは言語自体の問題ではなく、まさに上に書いた「俺1人でなんとかする」タイプの人(特にエンドユーザー)によってExcel VBAが愛されるせいで、不運なことにもExcel VBAにそういう印象ができてしまった偏見と先入観の問題、というのが私の見解です。

エンドユーザーはどんなマインドを持つべきか

ですから、プログラムができるエンドユーザーにとって、情シスのことを考えなくても自分の業務効率化には関係ないから気にしなくてよい、と考えるのは長い目で見て得策ではありません。

つまり、業務効率化をしようという意識は、辺にプロ意識を持つことで全体最適を考える情報システム部門からは煙たがられてしまい、皮肉なことにもVBA使いであったとしたら、自分の誇りにしているVBAの評価すら下がってしまいかねないからです。

VBAに限らず自身が使用して頑張っているものに対する世の中からの評価が下がってしまうのは悲しいことではないでしょうか。

ではどうすればよいのか。私は情シス担当者でもありませんし、コンサルでもありませんが、おそらくきっと大事であるだろうと信じていることをまとめてみます。

情シスを理解しようとする

EUCを実践するエンドユーザーだって、情シスだってITを使って社内を効率化しようと考えている点で同じです。つまり同志であるはずです。それぞれ対話もないまま独自の取り組みが進んでしまうから、どんどん溝が深くなってしまうのだろうと思います。

これは世の常ですが、エンタープラズシステムの構築において、エンドユーザーが重要だと考えていることと、情シスが重要だと考えていることは必ずどこかに乖離があります。対話がなければその乖離はどんどん広がります。

このことは先日このブログで書いた以下の記事のTwitterの一連のいざこざで強く感じたことです。この物議を醸し出した元ツイートへの引用リツイートを見ると、エンドユーザーと情シスの間で明確に意見が違うことが見て取れます。

tdyu.hatenablog.jp

だからこそ真剣に話し合える環境は必ずお互いのために役立つはずだろうと考えています。

相手のためを思う

いざ情シスと対話するときに、EUC担当は「業務を効率化したい」という思いから「情シスがなんとかしてくれよ」という意図がにじみ出てしまうかもしれません。これが危惧すべきことです。シャドーIT的なことをやっていたり、もしくはEUCで頑張っているということは、裏を返せば情シスがITを整備してくれていないからそうなっているはずです。

つまり、EUCが業務効率化への思いを吐き出す対話の機会があるとしたら、それは高確率で「情シスへの不満」がにじみ出てしまいます。こうなってしまえば対話の空気感は非常にまずくなってしまいます。

しかし、EUC担当と情シスは同志であるはずです。EUC担当側としては「情シスができないなら自分たちで何かできることはないか。情シスが忙しくてできないならリソースをお互い分け合ってできる効率化の仕組みがないか」という対話の仕方をするべきではないかと私は考えています。

システムと業務のトレードオフを理解する

あと、「情シスがITを整備してくれていないからそうなっているはず」と書きましたが、そこには情シス側の事情があります。情シス側がどうにかしたいと思っていたとしても、社員が多ければエンドユーザー全員が楽になるシステムなどそうそうありません。

経営者や情シスにとって便利なシステムを導入したら現場のシステム操作の負担が重くなったなどしょっちゅうあることです。それは仕方ない、ということを私は言いたいのではなく、エンドユーザーは「情報システムはどこかで必ずトレードオフがあるということを理解するべき」ということです。この前提がわかっていないと全体最適なシステムなど作れません。

また、そもそもの話として、単にエンドユーザーが困っているということを情シスが把握していない可能性があります。この場合は情シスに文句をつける前にまずは要望を伝えることから始めなければなりませんが、とにかくお互いが対話するしかないと思います。

情シスを便利屋と思ってはいけない

エンドユーザー自身として「自分が何ができるか」と考えることは、エンドユーザーと情シスの関係に限ったことではありません。当たり前の話ですが、人に何かをしてもらいたいときに、一方的にこちらの要求をつきつけてもうまくいくはずがありません。本気で何かを変えたいのなら、自分もしっかり汗を流すこと、相手がやりやすいようにこちらからも何かできることを提示するべきではないでしょうか。

しかし残念なことに、ITシステムとはインフラのようなものになり、「動いて当たり前」の世界になってしまいました。これは喜ばしいことですが、それに携わる人にとっては悲しい一面もあります。

インターネットがつながって当たり前、PCのトラブルは直ぐに解決できて当たり前……残酷なことに情シスに対しても「社員を助けてくれて当たり前」のように知らぬ間に思ってしまうわけです。

システム導入についても「要件さえいえばあとは情シスが勝手に整備してくれる」と思っている人すらいるでしょう。もっと最悪のケースは「要件もシステム作る人が考えてよ」という思考です。

しかし、そんな態度をする人に優しくできる人間などこの世にいないでしょう。ですから、情シスを「便利屋」のようにこき使うのではなく、労いを持って接するべきではないかと思っています。

まとめ

だらだら長くなり、最後EUCとも少し離れましたが、ここで言いたかったのは以下の4点です。

  1. EUCによる独自プログラムは、業務の属人化や「シャドーIT化」が発生しリスクとなる可能性がある。
  2. 独自にプログラムを組めるEUC担当は、使命感と自尊心が変な方向にいくと、IT部門と相容れない存在となる可能性がある。それはExcel VBAでも起きやすい。
  3. だからこそEUC担当と情シスは対話と連携を行うべき。
  4. その際にエンドユーザー側としては情シスに「助けてもらう」という姿勢ではなく、双方にメリットのあるITによる業務効率化を実現できるように、エンドユーザー側でできることはないか、という相手を思う視点を持つべき。情シスはエンドユーザーのために都合よく動いてくれる便利屋だと思ってはいけない

私もユーザー部門のIT導入や情シスの声を取材し続けてきた外野の人間でしかないので、的はずれなこともあるかもしれませんが、ひとまず私が感じたことをまとめてみました。

Excel VBAから考えるセキュリティリテラシーのあり方

私はTwitterをやっていて、VBA界隈の方々をたくさんフォローしています。いつもは極めて平和的な界隈なのですが、今日珍しくあるツイートがきっかけで荒れていました。ただ、そのツイートには、VBAVBAユーザーが世間やら会社の中での存在感や立場を守る上で大事な観点があったので、私の考えをまとめてみます。

VBA界隈が荒れたきっかけ

今日VBAが界隈が少し荒れたきっかけのツイートが以下のものです。

前提を知らない方のために説明すると、いまTwitter上では、Excel VBAの大御所的な存在である「エクセルの神髄」さん(以下、神髄さん)が、VBA学習者のためにVBAの実務的な練習問題を出す「VBA100本ノック」を定期的に配信しています。その中のとある問題に「RR@IT・情シス勉強中」さん(以下、RRさん)が、セキュリティに問題があるとして引用リツイートで難癖をつけたのです。

上記のツイートを見ておわかりの通り、少し攻撃的な物言いであったために、神髄さんが反論し、さらにRRさんがそれに反論する形で言い合いになり、VBAer界隈でもRRさんの失礼な態度に批判が集まりました。

私はTwitterの基本スタンスとして、炎上していたり賛否両論が集まっている内容にはツイートも引用リツイートもせず、静観する主義なのですが、これに関しては思わずコメントせずにはいられませんでした。

その理由は、RRさんのツイートが「失礼なクソリプ」という印象だけが残ったまま片付けられることで、VBAの立場が中長期的に悪くなるリスク、ひいては世の中のセキュリティ意識が軽視されるリスクが隠蔽されてしまうと考えたからです。その意図を以下に説明します。

そもそもRRさんはなぜ批判したのか

RRさんのツイートの通りですが、RRさんが批判したのは、100本ノックの当該の問題にセキュリティ的に問題があったからです。ただし、VBA100本ノックは、あくまで「練習としてサンプル」であり、実務でそのまま使うことまで想定されていません。セキュリティを保証する義務もありません。

RRさんはそのことを知っていたかどうかは不明ですが、指摘されたVBA界隈から見れば、「こっちは神髄先生があくまで趣味として、しかも『練習問題』として好意でやってるだけなんだから、外野の人間が情シス目線の実用的なセキュリティの話でツッコミを入れるのはおかしい」と思うわけです。

まさにこれを例えたのが、次のツイートです。 

RRさんはツッコミは批判されるべきものであったのか

これに対して私の意見として、まず「神髄さんの赤の他人であるRRさんが神髄さんにあのような言い方で絡むのは失礼であり、その意味であのツイートは間違っていた

ということがいえると考えています。あのツイートは批判されてやむを得ないものです。

そして、絡み方が失礼か否かを切り離したもう1つの論点が

VBAの個人的な活動とはいえ練習問題でセキュリティが完全でないもの紹介していることに対して外部から批判をするのが妥当であるかどうか」

という論点です。要するに「意義のある批判」か「ただのクソリプか」どうかということです。

これに対して私は前者だと考えています。

RRさんの批判に妥当性があると私が考えるわけ

なぜRRさんの批判について意義があると私が考えるのかは次の理由があります。

もしVBAの100本ノックでやったことプログラムを実務に応用し、不十分なセキュリティのために例えば社内の情報システム担当者に面倒をかけた、もしくは会社に何らかのセキュリティインシデントが発生した――。このようなことが起きてしまったらどうでしょうか。VBAに影響力のある人が、好意とはいえそのようなリスクがあることを情報として配信することに対して、指摘してはいけないことなのでしょうか。

ExcelVBAなどは、高額な情報システムに投資できない会社にとって非常に有益ですが、属人化しやすく、より高度なシステム化をする際に「悪者」扱いされがちです。

今回もそのようなリスクがないとも言い切れないのです。RRさんのツイートには批判が寄せられましたが、一方でRRさんと同じく情シスと思わしき方からの意見では、RRさんのツイートに賛同する意見もありました。

こうした批判に対する反論として「VBA100本ノックはあくまで練習であり、それを実務で『そのまま』使おうとする人はいないだろう」と神髄さんは思っているでしょうし、VBA100本ノックをやられている方の多くもそう思うでしょう。

ただし、必ずすべての人がそうだといいきれるでしょうか。あまり知識のない人がたまたま神髄さんのツイートを見て、それをそのまま会社の業務に利用してしまう可能性はないといいきれるでしょうか。

だからこそRRさんの最初のツイートに

「これを実運用で使う人が出ないよう、きちんと警告するのがリテラシーだし。適切なユースケースの提示や警告を欠いたサンプルは、VBAを悪者にするだけじゃなくて?」

と書いてあったように、何らかの「注意書き」をするべきというのには妥当性があると私は同意します。

ですから、仮にRRさんのツイートが

「サンプル問題とはいえ、危険じゃない?
マクロの実行を無効化すれば、すぐ開けるし。xlsxファイルの中身はExcelでなくても読めるから。」

で終わっていたら、紛れもない正真正銘のクソリプですが、ちゃんとその後ろに「きちんと警告するのがリテラシーだし。」と書いてあったことは正論だと私は思っています。

RRさんのツイートに対する反論として「セキュリティに完璧などをいちいち求められない」というのがあります。例えば以下のものがあります。

しかし、これらの意見は論点がずれています。RRさんのツイートは「セキュリティを完璧にしろ」とは言ってません。RRさんのツイートはあくまで「実運用で使う人が出ないように注意書きを書くべき」といっているのです。

ですから、RRさんもそのことを神髄さんに反論すればよかったのに、以下のツイートみたいに「どうすれば安全にできるか」という本題と関係ない論点で噛み付いたので、話が関係ない方向に進んでややこしくなってしまい、批判をさらに招いた形です。

吉田さんのおっしゃるように「セキュリティを完璧にすることはできない」というのはまったくもってそのとおりです。しかし、だからといってそれを「放置する」理由にはなりません。セキュリティが完璧でないなら、「100本ノックの問題として出すのはやめようか」とか「該当のツイートに、セキュリティ上のことわりを一言ツイートする」という代替案があったはずです。

それに対する反論として、「では、趣味として行っている神髄さんがそこまで考える必要があるのか」という意見があります。この論点は後ほど説明するのでここではひとまずおいておきます。

RRさんの批判に妥当性があると考えるもう1つの理由

RRさんのツイートに「悪者」と書いてあったように、もしセキュリティに問題があるVBAが実運用され、それが社内の情報システムの中で問題視されたとき、それを作ったのが情報システム担当者自身だったとしても、VBAそのものは悪くないにしても「VBAはやっぱりだめだ」「VBAなんかでシステムが作れない」というような偏見や印象を招きVBAが悪者」扱いされてしまう可能性も否定できません。

これは悲しいことではないでしょうか。これを防ぐためにもセキュリティに関する意見として、RRさんが一石投じたことに私は意味があると考えています。これが、RRさんのツイートに妥当性があると考えるもう1つの理由です。

RRさんのツイートは一見するとVBAerの活動を批判しているように見えます(実際にRRさんもVBAを疑問視している節はありますが・・)。しかし逆説的なことに、RRさんの意見をクソリプと片付けずに耳を傾けることは、セキュリティ意識を高めて中途半端なVBAが業務利用をされない、つまり情報システムの中でVBAの存在を守るために大事な観点なのです。

VBAerの皆さんとしては、自分たちのやっていることがいちゃもんつけられたような感覚になっているでしょう。しかもTwitterVBA界隈を見ると「あくまで練習問題なんだから」というのを免罪符を使おうとしているようなニュアンスも感じます。

しかし、そうした考えだからこそVBAのプログラムは中途半端なものになり、回りに回って企業の情報システムから「邪魔者」のような扱いをされてしまうのではないでしょうか。

セキュリティの注意書きが必要だったと考える3つの理由

ここまでこの文章を書いてきた私の意見に対してもう1つ反論があるとしたら次のような意見でしょう。

「実用面で不十分だし、注意書きをする妥当性もわかった。だけど、神髄先生の100本ノックは『ただの趣味の活動』である。そんなところにまでセキュリティの啓蒙の責任を追わせるのか。趣味でやっていることに対して外野からとやかく言われる筋合いはない」

これについてはどうでしょうか。

私としては、趣味の活動だとしても「セキュリティの注意書き」をいれるべきと考えています。その理由は大きく3つあります。

1つに、情報セキュリティの事故は会社の業務や日々の活動に大きく関わることだからです。もちろん神髄さんに「法的な責任」は一切ありません。ただし、ITやプログラミングに関わり、しかも影響力が大きい存在だけに、セキュリティに対する注意書きは合ってしかるべきだと考えています。

先ほどRRさんのツイートを以下のように例えたツイートがあることを紹介しました。

この例えで根本的に欠けてて間違っているのは、素振り用のバットを試合に持っていても困るのはバットを使ったチームだけで自己責任の問題だということです。誰かに害が及ぶわけではありません。だから外野が指摘するのはおかしいのです。

一方で練習サンプルマクロの場合は(可能性が低いにしても)情報セキュリティの観点で情報漏えいを含む重大な事故になる可能性があります。

当然ですが、練習バットを使ってチームが勝てなかったことと情報セキュリティの事故を起こすことの間には社会的責任の重さが違います。

これに対して私も空リプ的な感じでよろしくはないのですが、思わず以下のようなつぶやきをしてしまいました。

ここまで読まれた方ならこのツイートの意図はわかるかと思います。この一連の騒動で多くの人はRRさんのツイートに対して「変なやつが絡んできた」という印象しか抱けていないのです。「セキュリティの不十分なマクロを発信することがどうなりえるか」というところまで想像力が及んでいないのです。

次に、2つめの理由として、VBAのような活動は企業が体系的に取り組むというものより、個人の担当者が会社の中で広げていく「草の根的」な活動で(悪く言えば「シャドーIT」的に)広まっていくからです。

そういうふうに広がるということは、まさにTwitterのような非オフィシャルな界隈による影響力が大きいと私は考えています。大きいからこそ、そのような場でVBAの立場が悪くなるようなことには注意を払うべきだと考えているからです。

そして、最後の理由の3つ目について、これはすごく大局的な話で完全に私の主観的、情緒的な理由なのですが、「ITを人に伝えるものはセキュリティのリスクをあわせて伝えていくべき」という考えがあるからです。

世界的にIT化が進み、それと同時に日々非常にたくさんの情報セキュリティの事故が起きています。サイバー攻撃をする人が悪いのはもちろんですが、セキュリティ事故の発生理由には、「実装がまずかった」という理由もあれば「設定ミス」という運用上のミスによる理由もあります。ITの開発に携わる人の責任も大きいのです。

私はIT業界に関わりがある仕事をしており、プログラミングに興味があります。ITの話題は好きです。ITに関わる人材が増えるのはとても良いことだと考えています。ただ、ITにふれるハードルが下がったのは良いのですが、肌感覚として世の中の人のセキュリティリテラシーがそれに追いついているのかどうか疑問に感じるところもあります。

エンドユーザーのシャドーITによってセキュリティにリスクが生じることもあるでしょう。そこにはExcel VBAによって生じるリスクも(極めて低いですが)ないとは言い切れません。

セキュリティの重要性を伝えられるのはITやプログラミングを教えられるような人たちです。アプリケーション開発の指導者もセキュリティを一緒に啓蒙できる存在であったほしいと、私個人として感じています。これは人に強制するものではなく私の単なる願いです。

まとめると

とても長くなりましたが、今回のRRさんのツイートに対してこの記事で私が言いたかったことをまとめると

  • RRさんの指摘には妥当性があるけど絡み方が失礼で避けるべきだった
  • 長期的に考えてVBAの立場を守るならセキュリティのことも考えるべき
  • そのことはITやプログラミングを人に伝える、もしくはそういう場を提供する者がしっかり啓蒙していくべき(という私の願望)

の3点です。

IT業界をずっと取材し続けている立場として、VBAが好きで触っている立場として思わずこんなことを書いてみました。

Twitterで誕生日に飛ぶ風船を再現するGoogle Chromeの拡張機能を作ってみた

f:id:tdyu5021:20210127000848p:plain

Twitterでは、プロフィールに誕生日を設定していると、その人の誕生日にホーム画面に風船が飛びます。あの仕掛けが個人的に好きなので、これをどのWebページでも再現するGoogle Chrome拡張機能を作ってみました。備忘録も兼ねてまとめてみます。

「毎日が誕生日」なGoogle Chrome拡張はこちら

このChrome拡張機能を使うとどんな感じになるのか。まずは、私が投稿した以下のツイート内の動画をご覧ください。

見ての通り、Webページを表示する度にTwitterの風船みたいなやつが飛んでくるというプログラムです。最初は楽しいけど、1分で飽きますね、これ…

まあ誕生日気分を味わいたい方にはぴったりなプログラムです。ちなみに本物のTwitterだと風船をタップすると割れるという仕掛けまでついているようですが、面倒なのでそこまではやりませんでした。

実際のJavaScriptのコード

というわけで最初にソースコードを載せます。Google Chrome拡張機能はHTML、CSSJavaScriptで作ることができ、しかもChromeストアに公開せず、自分のブラウザだけに適用するくらいならわずかな設定でできます。作り方は他のサイトみればすぐにわかりますので省略します(気が向いたらこのブログに書きます)

今回の拡張機能は既存の画面をいじるだけでなので使用するのはJavaScriptファイル(と風船の画像ファイル)だけです。その実際のコードが以下です。※jQueryで記述しているのでjQueryファイルを別途読み込んでいる前提です。

$(function(){
	//風船5種類
	img1 =chrome.extension.getURL('b1.png');
	img2 =chrome.extension.getURL('b2.png');
	img3 =chrome.extension.getURL('b3.png');
	img4 =chrome.extension.getURL('b4.png');
	img5 =chrome.extension.getURL('b5.png');
	imgArr = [img1,img2,img3,img4,img5];
	$('body').append('<div id="added"></div>');

	//画面の高さと幅を取得
	wh = $(window).height();
	ww = $(window).width();

	//風船オブジェクトの配列
	bl=[];
	for(var i = 0; i<30; i++){
		var startX=Math.floor(Math.random()*(ww/20))*20;
		var startY = wh+(Math.floor(Math.random()*10)*80);
		var ratio =1 / (Math.floor(Math.random()*5)+4);//Yが進む距離に対してXが進む距離の比率
		direction = 1-Math.round(Math.random())*2;
		bl[i]= new balloon(i,startX,startY,ratio,direction);
		bl[i].appear();
	}
});

function balloon(i,x,y,r,d){
	this.i=i;
	this.x=x;
	this.y=y;
	var dist1 =-8;
	var dist2 = Math.abs(dist1)*r*d;
	$('#added').append('<img id=image'+this.i+'>');

	//風船を出現させる
	this.appear = function(){
	  var target = $("#image"+this.i);
	  target.css({
	    position:'absolute',
	    top:this.y,
	    left:this.x,
	    zIndex:9999,
	    width:'80px'
	  });
	  var rnd = Math.floor(Math.random()*imgArr.length);
	  target.attr("src", imgArr[rnd]);
	  var xx = this.x;
	  var yy = this.y;

	  //風船を動かす
	  var si = setInterval(function(){
	    yy+=dist1;
	    xx+=dist2;
	   	target.css({
	      top:yy,
	      left:xx
	   	});
	  	if(yy<-200) clearInterval(si);
	  },20);
	};
}

作成のポイント

60行足らずの簡単なコードですが、ポイントをかいつまんで見てみます

①風船の画像を画面上に配置する

通常、HTML、CSS、JavaSciptなどで画像を読み込む場合は、参照先のディレクトリにもよりますが、もし相対パスなら

img1 ='img/b1.png'

上記のように書きますが、Chrome拡張機能を作る際には参照の仕方が異なります。

Chrome拡張機能を有効化するには必要なファイルをフォルダにまとめてアップロードするのですが、JavaScriptファイルと使用する画像ファイルが同じ階層にある場合、

img1 =chrome.extension.getURL('b1.png');

こんな感じで画像を参照させます。

次に、風船の画像ファイルを画面に配置するには、

(1)新しく<div>要素を作り、
(2)その中に風船の数だけ<img>要素を作成する

という感じで既存のHTMLの中に組み込む必要があります。

(1)の部分は上記のコードの中でこの部分です。appendメソッドでbodyタグ内の最後にまず<div>要素を付け足します。

$('body').append('<div id="added"></div>');

(2)に関しては、風船を格納する<img>要素を作ると同時に、画像を画面上で好きな位置に動かさなければならないので、 cssでpositionをabsoluteに設定します。最前面にくるようにzIndexプロパティも適当に大きな値にしておきました。

以下の部分で、付け足した<div>要素の中にappendメソッドで<img>要素を設定し、

$('#added').append('<img id=image'+this.i+'>');

以下の部分でスタイルを設定しています。

var target = $("#image"+this.i);
target.css({
   position:'absolute',
   top:this.y,
   left:this.x,
   zIndex:9999,
   width:'80px'
});

 風船オブジェクトとそのインスタンスをたくさん作る

今回は風船を大量に出現させ、またそれぞれが別の角度で別の場所で動いていくので、まず風船オブジェクトを定義し、そのインスタンスを生成して各風船(インスタンス)の位置やら進む角度や方向などを与えていくやり方がよさそうです。

この考え方を今回のコードで実行する際、骨子だけ作るとまず以下のような感じになるかと思います。 

for(var i = 0; i<30; i++){
  bl[i]= new balloon();
  bl[i].appear();
}

function balloon(){
	this.appear = function(){
		var si = setInterval(function(){
			$("#image"+[変数]).css({
				top:xxxxx,//天地の位置を動かす
				left:xxxx//左右の位置を動かす
			});
		},20);
	};
}

まずballonnという風船自体の関数オブジェクトを作り、その中に各風船を動かすappearというメソッド作ります。今回は適当に30個の風船を作ることにしたので、ループの上限を30にしてforループで回し、

bl[i]= new balloon();
bl[i].appear();

で各風船インスタンスを作成して動かします。

インスタンスを生成したときに風船オブジェクトに渡す情報(=new balloon();の引数)は以下です。

  1. 風船の通し番号
  2. 風船の左右の初期位置
  3. 風船の上下の初期位置
  4. 風船が進む上方向距離と左右方向距離の比率
  5. 風船の進む方向(左か右か)

 このうち4番目については、風船を動かすループの中で縦方向に動く距離に対して横方向に動く距離を比率をランダムに設定することで、風船が飛ぶ角度をバラバラにしており、その情報を渡すものです。もっといいやり方がないかなと思いつつ書いています。

だいたい要点はそんなところです。

この記事を書いた理由

 JavaScriptはたまにしか書かないので、結構関数オブジェクトとインスタンスの生成の書き方を忘れるんですよね。このプログラムも大した時間がかけてませんが、過去の自分のファイルを見ないと書き方わかりませんでした。

というかこの書き方で合ってるのか謎だし。別のプログラムでprototypeとかも使ってるけど、完全に雰囲気で書いている…(そしてその使い方はもう忘れた)

集中線を作成するしょうもないマクロの作り方【Excel VBA】

f:id:tdyu5021:20201120211445j:plain
以前Twitterに投稿したくだらないExcel VBAマクロシリーズです。需要はないだろうと思いますが、想像以上にバズったので、せっかくなのでその作り方を解説してみます。

集中線を作成するExcelマクロとは

まず以下のTwitterの投稿をご覧ください。集中線を作成するプログラムを実行した画面動画です。

「集中線」とは強調したものを目立たせるために放射上に周囲に線を引いた表現です。よくマンガで見かけるやつですね。

別にそこまでウケ狙いで作ったわけではないのですが、あまりにも線を増やし過ぎたせいで見た目のインパクトが強烈になり、

強調しすぎて他が見えなくなってるじゃねーかwww

そこ以外が見づれぇwwwww

などなど、いろんな方からのツッコミとともに結構バズってしまいました。中には「作り方を教えて下さい」というコメントもあったので、だいぶ今さら感はありますが、作成方法を紹介します。ソースコードは最後にまとめて掲載します。

円周上にオブジェクトを配置する

このプログラムは、実際のコードを順を追って解説する前に、まず「円周上にオブジェクトを配置する」方法を解説します。正直これさえわかれば後は簡単です。

円周上にオブジェクトを配置するとはどういうことかというと、例えば以下のような形を作成することです。

f:id:tdyu5021:20201017001726p:plain

これはオートシェイプを作成する「AddShapeメソッド」を、X座標、Y座標をずらしながら18回ループさせて円を配置したものです。ただ、通常のループの中で円の軌道をどうやって描けばよいでしょうか。

例えば直線上にオブジェクトを連続で配置していくには、X座標あるいはY座標の値をループで足していけばよいのですが、円周に沿って配置していく場合は単純にはいきません。

これは「円周上の座標の位置の求め方」を利用して対処します。その求め方は以下になります。

  • X座標…中心の座標+半径×Sinθ
  • Y座標…中心の座標+半径×Cosθ

f:id:tdyu5021:20201024225818p:plain

上記の式からわかるように、角度のθの値でX座標、Y座標は変化するので、θの値を変数にしてループさせればよいのです。サインθとコサインθの値は、VBAで標準で用意されている「Sin関数」と「Cos関数」を使用して求めます。この2つの関数は、Sin(X),Cos(X)のように記述し、Xに角度の数値の引数を入れて使用します。

角度をラジアンに変換する

ただ注意点として、このSin関数とCos関数の引数の角度の単位は、「ラジアン」を用います。そのため小学校の頃から慣れ親しんだ「度数」で管理したい場合、度数からラジアンへ変換するコードを挟まないといけません。

度数からラジアンへの変換自体は、

ラジアン=度数×(π/180)

で求められるので一見すると簡単ですが、なんと残念ながらVBAには円周率(π)を求める関数が存在しません。ですから円周率を何らかの方法で導く必要があります。

Excelのワークシート関数にはPi関数があるのでそれを使用してもよいですが、どうやら調べたところによると、アークタンジェントを求める「Atn関数」を用いて

Atn(1)×4

でも円周率(π)を求められるそうです。なぜ上記の式で円周率を求められるのか、私は理屈を説明できませんが、以下の記事に詳しく書いてあるので気になる方は読んでみてください。

excelmath.atelierkobato.com

 まとめとして、度数の変数をdegreeと置いた上で、Excel VBAで度数をラジアンに変換する記述は以下になります。

Pi = 4 * Atn(1) '//ここで円周率πを求める
radian = degree * (Pi / 180)

円周上にオブジェクトを配置するコード 

これを踏まえて、円周上に図形を配置するコードをまとめてみます。円の中心となるX座標、Y座標の変数をcenterX、centerYに、また半径の変数をRadiusにして、ひとまずここでは適当な数値を入れておきます。

配置する円については、上記サンプル画像のように18個配置したい場合は、360÷18=20、つまりループで20度ずつステップさせています。

Sub test()
    centerX = 200  '//円の中心となるX座標
    centerY = 200  '//円の中心となるY座標
    Radius = 100   '//円の半径
    For Degree = 1 To 360 Step 20
            Pi = 4 * Atn(1)
            radian = Degree * (Pi / 180)
            
            x = centerX + Radius * Sin(radian)
            y = centerY + Radius * Cos(radian)
                
            Dim s As Shape
            Set s = ActiveSheet.Shapes.AddShape(msoShapeOval, x, y, 20, 20)
    Next
End Sub

試しに上記のコードをコピペして実行してみてください。最初にお見せした画像のように円周上に配置されるはずです。

集中線を作成してみる

それでは今回の本題である集中線を実現するために、そのほか必要なポイントを見ていきましょう。

AddConnector メソッドで線を引く

まず集中線に必要な「線」はオートシェイプの「線(コネクタ)」を作成するメソッドであるAddConnectorメソッドを使います。

このメソッドは

Addconnector(Type、 [beginx]、 [beginy]、 [endx]、 [endy])

のように5つの引数を取ります。

[beginx]、 [beginy]はコネクタの開始位置のX座標、Y座標で、

[beginx]、 [beginy]は終了位置のX座標、Y座標です。

上記で円周上の座標の求め方を紹介しましたが、開始位置のX座標、Y座標は小さめの円周に、終了位置のX座標、Y座標は大きめの円周に沿って配置すれば線を放射上に配置することができます。

試しに、上に記した円周上に配置するコードのうち、Radiusの値を小さめに設定した上で該当部分を以下のように書き換えてみてください。

x = centerX + Radius * Sin(radian)
y = centerY + Radius * Cos(radian)
x2 = centerX + (50 + Radius) * Sin(radian)
y2 = centerY + (50 + Radius) * Cos(radian)

Dim s As Shape
Set s = ActiveSheet.Shapes.AddConnector(msoConnectorStraight, x, y, x2, y2)

線(コネクタオブジェクト)が放射上に並ぶはずです。

そのほか集中線っぽくするためのチューニング

ただ、放射上に線を配置しただけでは旭日旗みたいな模様になり、集中線っぽくはなりません。これを解消するために以下の調整をかけています。

  1. 線ごとに開始位置を変える
  2. 線と線の間隔は等間隔でなくバラバラにする
  3. 線ごとに先の太さを変える

サンプルコードを例に取ると、今回のマクロでは、

1については変数Radiusの値をランダムに生成

2については変数Degreeをランダムに生成してForループで大量発生

(このループの回数が多くなればなるほど線が増えるので集中度合いが増します)

3については、コネクタオブジェクトのLine.Weightプロパティで太さをランダムで1か2にする、というふうにしています。

楕円オブジェクトを中心に線を伸ばす

集中線自体の作成は以上です。ただ最初のTwitter内の動画でもおわかりの通り、このマクロははじめに楕円オブジェクトを作り、もしワークシート上に楕円オブジェクトが見つかれば、その円周上を開始X座標、開始Y座標にして線を生成するという処理を行っていますので、その部分をご紹介します。

この処理を実現するには、

  • 楕円オブジェクトと中心のX座標・Y座標
  • 楕円オブジェクトの直径(今回は楕円の横幅を想定してプログラムを作っています)

の情報を取得しておきます。中心の座標はシェイプオブジェクトのTopやLeft、Width、Heightを用いて簡単に求められます。以下の画像の通りです。

f:id:tdyu5021:20201024212419p:plain

真円ではなく楕円状に配置する

この記事のはじめに、円周上にオブジェクトを配置する例を示しましたが、その円は完全な真ん丸(真円)でした。これを楕円にする場合、単純に拡大したい方向に任意の値を掛け算すればOKです。

一応書いておくと、座標を求める部分を以下のようにすると、横幅の直径が縦幅の直径の2倍の楕円ができます。

x = centerX + Radius * Sin(radian)*2
y = centerY + Radius * Cos(radian) 

 今回の集中線では、線の開始位置をプロットする円の直径は横幅(=X軸方向)を基準にしています。

そのため、楕円の縦の直径が横の直径と異なる場合は、その比率を縦幅の方に加味してあげる必要があります(例:楕円の縦幅が横幅の1/2なら、X座標を求める式に÷2をする)

この処理を書いておけば縦長、横長どちらの楕円にも対応できる集中線を作成できます。

補足:この集中線の欠陥

実はこのコードには重大な欠点があります。画面をある程度縮小していても集中線で画面を覆えるようにするために、中心から線の開始位置までの長さと中心から線の終了位置までの長さを1000というかなり大きめの値に設定しています。

ただその長さを固定にしてしまうと、線の終端座標がワークシートの左端や上端をオーバーしてしまう可能性があり、そうなるとうまく対応できないのです。

ちなみにシェイプオブジェクトでTopとLeftが0未満になった場合は、自動で0として扱われるため、線の長さはそのままに線の終端座標が0を基準として図形が勝手に動かされてしまいます。

この対策として「最初に楕円を置いた場所によって上側、左側にある線の長さを変える」という方法もなくはないのですが、うまくいかなかったので今回は妥協しました。とりあえず、「線の終端座標のX座標、Y座標が0未満になった場合は値を0にする」という処理で逃げましたが、それをやっても以下みたいな感じになってしまい、完璧に対処はできていません。

f:id:tdyu5021:20201024222412p:plain

実際にできあがったコード

最後に今回作成したコードを紹介します。実用される方はまずいないと思いますが、試しにご自身のワークシートなどで遊んでみてもらえれば幸いです。

Sub createLines()
    Randomize
    Dim shp As Shape
    Dim s As Shape
    Dim flg As Boolean
    Dim target As Shape

    '//ワークシート内の楕円を探す
    For Each shp In ActiveSheet.Shapes
      If shp.AutoShapeType = msoShapeOval Then
          flg = True
          Set target = shp
          Exit For
      End If
    Next shp
    '//楕円が見つからなかったらマクロ終了
    If flg = False Then Exit Sub

    w = target.Width
    h = target.Height
    targetLeft = target.Left
    targetTop = target.Top
    
    '//楕円の中心のX,Y座標
    centerX = targetLeft + w / 2
    centerY = targetTop + h / 2
    
    ratio = w / h
    '//楕円を削除
    target.Delete

    For i = 0 To 360
        Radius = w / 2  '//楕円の半径
        Degree = Int(Rnd * 360) '//ランダムに角度を決定する
        '//↓↓↓角度をラジアンに変換する
        Pi = 4 * Atn(1)
        radian = Degree * (Pi / 180)
        '//↑↑↑
        
        gosa = Int(Rnd * 70) '//線を開始位置をばらつかせるためランダムな誤差を発生
        
        '//線の開始地点、終了地点を円状にプロット
        x = (Radius + gosa) * Sin(radian)
        y = (Radius + gosa) * Cos(radian) / ratio
        x2 = (Radius + 1000 + gosa) * Sin(radian)
        y2 = (Radius + 1000 + gosa) * Cos(radian) / ratio
        
        '//楕円の位置を基準にする
        x = centerX + x
        x2 = centerX + x2
        y = centerY + y
        y2 = centerY + y2
           
        '//↓線を伸ばした先が画面を越える場合の対策
        If x2 < 0 Then '//もし画面の最左端を超えたら
            x2 = 0     '//とりあえずx座標は0に
        End If
        If y2 < 0 Then '//もし画面の最上端を超えたら
            y2 = 0     '//とりあえずy座標は0に
        End If
        '//↑
        
        Set s = ActiveSheet.Shapes.AddConnector(msoConnectorStraight, x, y, x2, y2)
        weightNum = Int(Rnd * 2) + 1
        s.Line.Weight = weightNum
        s.Line.ForeColor.RGB = RGB(0, 0, 0)
    Next
End Sub

ワークシートのイベントプロシージャをアドイン化する方法【Excel VBA】

f:id:tdyu5021:20200919230157p:plain

Excel VBAで記述したマクロをどのExcelファイルからでも使えるために「アドイン(.xlam)」で保存する方法があります。ですが、シートモジュールのコードはアドイン化できません。そうなるとそこに記載したイベントプロシージャをアドイン化したい場合はどうすればよいでしょうか。不完全ながら暫定的な方法を発見したのでメモ代わりにまとめてみます。

ワークシートのイベントプロシージャとは

Excel VBAビギナーであれ、そこそこ慣れている方であれ、ワークシートのイベントプロシージャを使っている方は多いと思います。

ワークシートのイベントプロシージャとは何かを念のため説明すると、例えばワークシートやセルなどに対して何らかの操作が行われたことをトリガーに実行されるプロシージャのことです。通常は「シートモジュール」に記載されます。

以下の画面は、ワークシート上のセルなどの内容に変化があった場合、メッセージボックスを表示するマクロの例です。

f:id:tdyu5021:20200919231049p:plain

ワークシートのイベントプロシージャもアドイン化したい

このシートモジュールに書かれたイベントプロシージャはアドイン化することはできません。当然です。特定のシートモジュールに書かれているということは、コードがそのシートにだけしか適用されないからです。

ではどうすればワークシートのイベントプロシージャをどのExcelファイルからでも使えるようにできるでしょうか。実は私も100%の答えにたどり着けていないのですが、浅はかな知識なりに暫定的な方法を考えたので以下に紹介します。

私が思いついた方法

今回のアドイン化にあたって、一定の制約を設けています。

まず、あるExcelファイルを開いたとき、そのファイルに存在するすべてのワークシートでも、また開いたあとに追加したワークシートでも動くイベントプロシージャをつくるのは少々面倒なので、とりあえずExcel ファイルを開いたときの「アクティブなシートで」使えるようにするという方法に限定して考えます。

以下、その方法です。ここでは開くファイルををBook.xlsx、アドインのファイルをaddin.xlamと想定します。

  1. addin.xlamのクラスモジュールで、ワークシートのオブジェクトを生成しておき、そこでイベントプロシージャを記述しておく
  2. Excelファイル(ここではBook.xlsx)を開いたとき、上記の1で生成したワークシートオブジェクトのインスタンスとして、開いたExcelファイル(Book.xlsx)のアクティブシートオブジェクトを格納する
  3. 2で記した「ワークシートオブジェクトにExcelファイルのアクティブシートを格納する」というプロシージャをAuto_Openにして自動化する。

初心者の方やクラスモジュールを使われたことがない方は、上記を読んでもしかしたら「??」と思ったかもしれません。そのような方にご説明しておくと、実はイベントプロシージャはクラスモジュールに書くことができます。上記はそれを利用したものです。

クラスモジュールを利用されたことがない方であれば、「ユーザーフォームのイベントプロシージャだったらフォームモジュールに」、「ワークシートのイベントプロシージャだったらシートモジュールに」というふうに書いてきたかと思います。

ワークシートのイベントプロシージャをクラスモジュールに書く方法

話は少し脱線しますが、今回のテーマとなるコードを初心者の方でも理解できるように、そもそも「イベントプロシージャをクラスモジュールに書く」というのがどういうことなのかをここで説明しておきます。

そのため先述した

  1. addin.xlamのクラスモジュールで、ワークシートのオブジェクトを生成しておき、そこでイベントプロシージャを記述しておく
  2. Excelファイル(ここではBook.xlsx)を開いたとき、上記の1で生成したワークシートオブジェクトのインスタンスとして、開いたExcelファイル(Book.xlsx)のアクティブシートオブジェクトを格納する
  3. 2で記した「ワークシートオブジェクトにExcelファイルのアクティブシートを格納する」というプロシージャをAuto_Openにして自動化する。

こちらのステップが何を言っているかわかっている方はここは読み飛ばしてください。

ワークシートのイベントプロシージャの例

例えば、イベントプロシージャの例として、「シート上でセルを選択し直したら『選択セルを変更』というメッセージを表示する」というコードを考えてみましょう。Excel初心者の方でも10秒でかけるコードです。以下をシートモジュールに書くはずです。

Private Sub Worksheet_SelectionChange(ByVal Target As Range)
    MsgBox "選択セルを変更"
End Sub

これをシートモジュールを使わずに書くと以下のようになります。標準モジュールとクラスモジュール(クラス名はデフォルトのClass1としておく)にそれぞれ以下のように記述します。

標準モジュール

Dim c As New Class1
Sub test()
    c.setting ActiveSheet
End Sub

クラスモジュール 

Public WithEvents sh As Worksheet
Sub sh_SelectionChange(ByVal Target As Range)
    MsgBox "選択セルを変更"
End Sub
Sub setting(ByVal s As Worksheet)
    Set sh = s
End Sub

もちろんこのコードを記述しただけではシート上のイベントプロシージャは発動しません。上記の中の”test”というプロシージャを実行すると、実行時のアクティブシートでイベントプロシージャが実行されるようになります。

ただ”test”という標準モジュールをいちいち実行してからでないとイベントプロシージャを利用できないのは煩わしいので、プロシージャ名のtestを“Auto_Open”にしておけば、ファイルを開いたときに自動でこのイベントプロシージャが利用できるようになります。

WithEventsキーワード

上記に記した、クラスモジュールを用いたイベントプロシージャ生成を可能するのが「WithEvents」キーワードです。これはクラスモジュール内のみで使用でき、ワークシートオブジェクト、ワークブックオブジェクト、ユーザーフォームオブジェクトなどなど、イベントプロシージャが用意されているオブジェクトの変数を宣言でき、それにイベントを割り当てられるというすぐれものです。

クラスを利用して、「ひな形」としてのワークシートオブジェクト(上記のサンプルでは"sh"という変数)を宣言しておいて、その後変数"sh"に、開いたExcelファイルのアクティブシートを格納した(=settingというメソッドに引数"activesheet"を渡した)という流れです。

こうしてアドインはでき上がる・・か?

ここまで来たらおわかりかもしれませんが、イベントプロシージャをアドイン化するアプローチとして、シートモジュールを使わずにクラスモジュール(と標準モジュール)にコードを記載すれば、今回の目的のものができ上がるだろうというのが私の狙いです。

なぜか動かない!その理由は?

ここまで来たらできるはず!と思い、上記の"test"のモジュール名を”Auto_Open”に変更した上で実際にこのファイルをxlam形式で保存。

そしてアドインを有効にしていざ試してみたのですが、その結果、うまくいきませんでした……

実行してもエラーにはならず原因がわかりません。通常のExcelファイルとして開くとうまくいくのに、アドインにするとなぜかうまくいかない。

正直、この理由がわからずずっと悩んでいたのですが、いろいろ調べるうちに動かなかった原因がようやく判明しました。その理由は単純なものでした。

標準モジュール内では以下のように、インスタンスを生成したオブジェクトにActiveSheetを格納するという処理を書いているわけですが、

Sub test()
    c.setting ActiveSheet
End Sub

どうやらExcelファイルを開くとき、「アドインファイルを読み込んだ時点では開いたExcelファイルのワークシートオブジェクトはまだ存在していない(開かれていない)」ようです。

なので、ここで記載した“ActiveSheet”は、開いたExcelのワークシートではなくどのワークブックのものでもないため、不明なオブジェクトになってしまうのです。

なので結論を言うと私の考えた上記の方法でアドイン化を行うのは無理であることがわかったのですが、対症療法的な措置として実現する方法がありました。

つまり、アドインを読み込んだ時点でまだ開いたExcelファイルのシートが存在しないなら、それが存在するのを待ってからアドインのコードを実行すればよいという対処方法です。

具体的にはAuto_Openのプロシージャの中に

c.setting ActiveSheet

記載するのでなく、それを別のプロシージャに書いておき、Auto_Openではそのプロシージャを時間差で実行するようにするという方法です。コードに書くと以下の通りです。

Dim c As New Class1
Sub Auto_Open()
    Application.OnTime Now + TimeSerial(0, 0, 2), "test"
End Sub
Sub test()
    c.setting ActiveSheet
End Sub

これは”test”というプロシージャをアドインファイルを読み込んでから2秒後に実行するというものです。このように変更したところ無事に新しく開いたExcelファイルでシートモジュールのイベントプロシージャが動きました。

ちなみにApplication.OnTimeメソッドは指定した時間にマクロを実行するためのものです。TimeSerial関数は、引数で指定した数字を時刻で示す関数です。上記の例だと、0,0,2なので0時0分2秒です。

最後に

上記でも運用上は問題ないかもしれませんがなんかすっきりしません。これをどうにかする方法はないのかなーと思っていたのですが、Twitter上で得た情報によるとワークシートのイベントプロシージャをアドイン化するれっきとした方法はあるらしいです。(具体的な方法は結局のところわかりませんが…)

気が向いたらもう少し研究してみようと思います。

数式や関数を打ち間違えるとニコニコ動画風に煽ってくるExcel VBAマクロの作り方

f:id:tdyu5021:20200525220235p:plain

先日Twitterに「Excel操作中に関数や数式を打ち間違えたときニコニコ動画風に煽ってくるクッソうざいマクロ」というネタVBAを投稿したところ、想像以上にバズりまくってビビってます。汎用的に使えるものではないですし公開する気はさらさらなかったのですが、実態は初学者でもわかる簡易なプログラムですし、ネタが一人歩きしたことで私がすごい技術を使っていると誤解されたくもないので、中身を公開することにしました。ソースコードは最後にまとめて載せます。

話題のクッソウザいマクロがこちら

まずは以下のツイートをご覧ください。

見ての通り、関数の名前を打ち間違えたことでエラーを起こしてしまうと大量の煽りコメントで罵倒されるという精神的にやられるプログラムです。

まさかこんなにバズると思わなかった…というくらいバズってしまい、前回の集中線VBAと同様Togetterにまとめられただけでなく、まとめサイト(俺的ゲーム速報)にもまとめられてしまうという事態に…

togetter.com

jin115.com

(※2020年5月27日追記:ねとらぼにも取り上げられました。これは結構嬉しかった)

nlab.itmedia.co.jp

 そんなこんなでいろいろ引用RTやらリプやらでコメントをいただく中で、

「こんな知識と技術の無駄遣い、最高にステキです

だとか

「UZEEEEEEEE(褒め言葉)

とか言ってくださるコメントもあり、それはそれで嬉しいのですが、私は特にVBAに詳しいわけでもないですし、別に実態としては大したことはしてないです。

というわけで、その中身を1つずつ紹介していきます。

関数の打ち間違いを検出する

まず入力間違いの判定です。ここは何も難しいことをしていなく、ただIsError関数を使って「数式や関数がエラーになればプロシージャを呼び出す」ということをしているだけです。シートモジュールに以下のコードを記述しています。

Private Sub Worksheet_Change(ByVal Target As Range)
    If IsError(Target.Value) Then
        Call 今回のマクロ
    End If
End Sub

テキストボックスを生成する

多分、Excel VBAをまともに使っている人の大半はセルを操作したりするので、オートシェイプやテキストボックスを扱うことはあまりないんじゃないかなと思います。ここはとっつきにくい部分ですが、テキストボックスをアクティブなシートに生成するには以下のように、ShapeオブジェクトのAddTextboxメソッドを使用します。

(説明のために実際のコードから少々簡略化して記載しています)

Sub test()
    Dim tbx As Shape
    Set tbx = ActiveSheet.Shapes.AddTextbox(msoTextOrientationHorizontal, 1000, 10, 100, 30)
End Sub

1つの目の引数はテキストボックス上の文字の方向です。以降の引数は

第2引数:テキストボックスの左端位置
第3引数:テキストボックス上端位置
第4引数:テキストボックスの幅
第5引数:テキストボックスの高さ

です。つまり上記を実行すると、画面左から1000、上から10の位置に、幅100高さ30のテキストボックスが生成されます。

ちなみに、コメントは画面右からスタートして左に流れますが、残念ながら開始位置を厳密に制御するのはVBAでは難しいです。そのユーザーがExcelの画面のどの列まで表示させているかどうかは、画面の表示倍率やPCのディスプレイのサイズによって異なるからです。

というわけでこのプログラムでは、適当に「アクティブセルのLeftプロパティの数値からプラス400くらいしたところ」と決め打ちしています。この数字には特に意味はありません。

なので関数の打ち間違いを起こしたセルがA列とかだったらコメントは中央辺りから流れてしまいます。

テキストボックスのスタイルを整える

テキストボックスを生成しましたが、このままだと塗りつぶしや線などがデフォルトの状態に設定されているのでそれらを調整します。今回適用したテキストボックスのスタイルは以下です。

Sub test()
Dim tbx As Shape
    Set tbx = ActiveSheet.Shapes.AddTextbox(msoTextOrientationHorizontal, 10, 10, 1000, 30)
    With tbx
            .Fill.Visible = msoFalse    '//塗りつぶしの有無
            .Line.Visible = msoFalse    '//線の有無
            .TextFrame2.MarginTop = 0       '//テキストボックスの上の余白
            .TextFrame2.MarginRight = 1.8   '//テキストボックスの右の余白
            .TextFrame2.MarginBottom = 0   '//テキストボックスの下の余白
            .TextFrame2.MarginLeft = 2   '//テキストボックスの左の余白
            .TextFrame2.VerticalAnchor = msoAnchorMiddle    '//垂直方向の配置(ここでは上下中央)
            .TextFrame2.HorizontalAnchor = msoAnchorNone    '//水平方向の配置(ここでは設定無し)
            .TextFrame2.TextRange.Font.Size = 18     '//フォントサイズ
            .TextFrame2.TextRange.Font.NameFarEast = "MS Pゴシック"   '//フォントの種類
            .TextFrame2.TextRange.Font.Bold = True    '//ボールドかどうか
    End With
End Sub

(今思うと何でテキストボックスの余白まで設定しているかは謎)

あとはテキストボックスのTextFrame2.TextRange.Characters.Text プロパティで、流すコメントのテキストを入れておきます。今回のプログラムでは、20種類の決め打ちのコメントを配列で用意しており、それをランダムに入れています。

Twitterからのコメントでは「Average関数以外でもその関数に合った間違いを指摘してくれるのか?」的なコメントがありましたが、もちろんそんな高度なプログラムではありません 笑

もう一度いいますが、コメントは全部決め打ちです。

テキストボックスを左に動かす

いったん説明のために簡略化していますが、テキストボックス(tbx)を左に流すコードは以下のように記述しています。ループの回数は適当です。

For i = 1 To 500
   tbx.IncrementLeft -2
   Application.Wait [Now() + "0:00:00.01"]
Next i

IncrementLeftプロパティは、Shapeオブジェクトの左位置を指定するものです。これをループさせることでテキストボックスを左に2ごと移動させています。ただし、この記述だけだと一瞬でループの回数分移動してしまうのでアニメーションのように動きません。そこで以下の記述を追加します。

Application.Wait [Now() + "0:00:00.01"]

Waitメソッドはプログラムの処理を一瞬止めるためのメソッドです。これをループ中に仕込み、0.01秒止めることでテキストボックスの移動が連続して行われているように見せることができます。

テキストボックスが一番左に来たら、そのテキストボックスの位置を再度画面右側に移し、かつ中身のコメントも別のものに変更します。簡略化しますが、ここは以下のような処理です。

If tbx.Left < 10 Then
    tbx.Left = [画面の右側の方の位置をランダムに]
    tbx.TextFrame2.TextRange.Characters.Text = [用意したコメントからランダムに]
End If

大量のコメントを別々のスピードで流す

ここまでコードを簡略化して紹介してきましたが、このままのコードではテキストボックスは1つしか流れてきません。たくさんコメントを流すにはどうするかというと、配列変数を用いて変数tbxをForループでたくさん生成すればよいのです。

合わせて、それぞれのテキストボックスに異なる高さを設定しておけば画面のいろいろな高さから出てきます。

テキストボックスの流れる速さをそれぞれ変えるのは簡単で、先ほどご紹介したIncrementLeftプロパティの引数の数値をたくさん生成したテキストボックスごとに変えればよいのです。

これを行うには配列変数tbx()に対し、IncrementLeftの引数もspeed()のように配列変数にしてランダムな数字をいれておきます。tbx(0)にはspeed(0)で、tbx(1)にはspeed(1)で左に進むようにする、という感じです。

終わったらテキストボックスをすべて消す

ShapeオブジェクトにはTypeプロパティというものがあり、オートシェイプなのかテキストボックスなのかなど、Shapeの種類を取得できるプロパティがあります。テキストボックスはmsoTextBoxなので、テキストボックスを全て削除するプログラムは以下のように記述しています。

Sub deleteAll()
    Dim shp As Shape
    For Each shp In ActiveSheet.Shapes
        If shp.Type = msoTextBox Then
            shp.Delete
        End If
    Next shp
End Sub

終わりに

以上です。内容としてはざっとこんな感じです。

このプログラムは直接シートモジュールに、Changeイベントによる間違い検知のコードを記載していますし、何よりコメントが流れ始める位置はユーザーの表示環境によって異なり、そこを吸収できるように汎用的に作られてはいません。

あと、コメントが画面の最左端まで流れて来たとき、それより左には進めないので、そこでコメントを消さざるを得ないのも個人的には気になっています。

(おわかりかと思いますが、ニコニコ動画では、コメントが画面から見切れるまで流れ続けています)

このジョークプログラムは本当はアドイン化したかったのですが、なんか方法があるんですかね、これ。

シートモジュールのイベントプロシージャはクラスモジュールに書けるようですが、アドイン化できるかはいまのところ私の知識ではわかりません。

というわけで、いろいろ中途半端ですが、以下にコードを載せておきます。まぁ絶対に誰も使うことはないと私は確信していますが、ご使用の場合はすべて自己責任でお願いします…笑

'****************************************
'*シートモジュール
'****************************************
Private Sub Worksheet_Change(ByVal Target As Range)
    If IsError(Target.Value) Then
        Call test
    End If
End Sub
'****************************************
'*標準モジュール
'****************************************
Dim tbx(8) As Shape
Dim speed(8) As Integer
Sub test()
    Randomize
    
    '//コメントを全部配列に(数はいくつでもOK)
    arr = Array _
    ( _
    "だせえwwwwwwwwww", _
    "これ完全Excel初心者だろ", _
    "これは無能", _
    "こんなの同じ会社にいたら嫌だわ", _
    "wwwwwwwwwwwwwww", _
    "普通そんな間違いしないだろwww", _
    "これはありえない", _
    "ダサすぎてワロタ", _
    "俺でもさすがにAverageは打てるぞ", _
    "関数も使えないとか草", _
    "Average関数もわからないとかwwwwwwww", _
    "こいつは間違いなく無能", _
    "Average関数で間違えるやつ初めて見た", _
    "これがゆとり教育の弊害か・・・", _
    "ワロタ", _
    "えっそこ間違える!?", _
    "Excelよりも英語の勉強をやり直したほうが", _
    "そもそもこの表は何なんだよ", _
    "wwwwwwwwwwwwwwwwwwwww", _
    "wwwwwwwwwwwwwwwwwwwwwww" _
     )

    '//コメントの配列の長さを取得
    arrLen = UBound(arr)
    
    '//数字を適当に並べる
    hArr = Array(2, 5, 4, 1, 3, 7, 6, 8, 9)
    
    '//アクティブセルの左位置と高さを取得
    cpl = ActiveCell.Left
    cph = ActiveCell.Height
    
    For i = 0 To 8
        n = hArr(i) * 30
        '//テキストボックスを生成
        Set tbx(i) = ActiveSheet.Shapes.AddTextbox(msoTextOrientationHorizontal, cpl + 400, n, 400, 30)
        
        '//テキストボックスのスタイルをもろもろ設定
        With tbx(i)
            .Fill.Visible = msoFalse
            .Line.Visible = msoFalse
            .TextFrame2.MarginTop = 0
            .TextFrame2.MarginRight = 1.8
            .TextFrame2.MarginBottom = 0
            .TextFrame2.MarginLeft = 2
            .TextFrame2.VerticalAnchor = msoAnchorMiddle
            .TextFrame2.HorizontalAnchor = msoAnchorNone
            .TextFrame2.TextRange.Font.Size = 18
            .TextFrame2.TextRange.Font.NameFarEast = "MS Pゴシック"
            .TextFrame2.TextRange.Font.Bold = True
        End With
        '//コメントが進むスピード(距離)
        speed(i) = Int(Rnd * 4) + 3
        
        '//配列からテキストをランダムに選んでテキストボックスに入れる
        txt = arr(Int(Rnd * arrLen))
        tbx(i).TextFrame2.TextRange.Characters.Text = txt
    Next i
    
    For ii = 1 To 600
        For j = 0 To 8
            '//テキストボックスを左に移動
            tbx(j).IncrementLeft -speed(j)
            
    '//テキストボックスが左の方まで来たらもう一度右の方に戻してコメントも入れ直す
    If tbx(j).Left < 10 Then
        tbx(j).Left = cpl + Int(Rnd * 300) + 200
        tbx(j).TextFrame2.TextRange.Characters.Text = arr(Int(Rnd * arrLen))
    End If
        Next j
        Application.Wait [Now() + "0:00:00.01"]
    Next ii
    
    '//終わったらテキストボックスを全部消す
    Call deleteAll 
End Sub
Sub deleteAll()
    Dim shp As Shape
    For Each shp In ActiveSheet.Shapes
        If shp.Type = msoTextBox Then
            shp.Delete
        End If
    Next shp
End Sub

 

JavaScriptとCanvasでブラウザに星空を描く

f:id:tdyu5021:20191119005505p:plain

 CanvasJavaScriptの練習として星空を描いてみました。初心者がゼロから作ったものなのでわりと簡単です。

ソースコード

先にソースコードを載せておきます。HTMLはCanvasタグを設けるだけでOK。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1">
<script type="text/javascript" src="script.js"></script>
<title>夜空</title>
</head>
<body onload="draw()" style ="margin:0">
<canvas id = "canvas"></canvas>
</body>
</html>

 JavaScriptは以下です。

function draw(){
    canvas = document.getElementById("canvas");
    c = canvas.getContext("2d");
    //canvasの領域を画面サイズに
	canvas.width = document.documentElement.clientWidth;
	canvas.height = document.documentElement.clientHeight;

	//グラデーションの設定と描画
    var g = c.createLinearGradient(canvas.width/2, 0, canvas.width/2, canvas.height);
	g.addColorStop(0, '#000');
	g.addColorStop(1, '#021a69');
	c.fillStyle = g;
    c.fillRect(0,0,canvas.width,canvas.height);

	var maxX = Math.floor(canvas.width/10);
	var maxY = Math.floor(canvas.height/10);

	//流れ星*************************************	

	//translateする前の状態を保持
	c.save();

	var x00 = 100;
	var y00 = 100; 
	var len = 300;
	var dia0 = 2;
	c.translate(600,100);
	c.rotate(-(18 * Math.PI / 180));
	c.fillStyle = "#ffffff"; 
	c.beginPath();
	c.moveTo(x00,y00);
	c.lineTo(x00+len,y00+dia0*2);	
	c.lineTo(x00,y00+dia0*2);
	c.closePath();
	c.fill();	
	c.beginPath();
	c.arc(x00,y00+dia0,dia0,0, Math.PI*2, false);
	c.fill();	
	c.closePath();

	//前の状態を戻す
	c.restore();

	//光り輝く星*************************************		
	for(var i=0; i<15; i++){
		var x1 = Math.floor(Math.random()*maxX)*10;
		var y1 = Math.floor(Math.random()*maxY)*10;
		var dia = Math.floor(Math.random()*10)+15;
		c.lineWidth=0;
		c.strokeStyle = "#ffffff"; 
		c.fillStyle = "rgba(255, 255, 255, 0.1)"; 
		//外側の円
		c.shadowColor = "#ffffff";
		c.shadowOffsetX = 0;
		c.shadowOffsetY = 0;
		c.shadowBlur = 3;
		c.beginPath();
		c.arc(x1, y1, dia/2.5, 0, Math.PI*2, false); 
		c.closePath();	
		c.fill();
		//内側の円
		c.beginPath();
		c.fillStyle = "#ffffff"; 
		c.arc(x1, y1, dia/5, 0, Math.PI*2, false);
		c.closePath();	
		c.fill();
		//横棒
		c.beginPath();
		c.moveTo(x1-dia,y1);
		c.lineTo(x1,y1-1);	
		c.lineTo(x1+dia,y1);
		c.lineTo(x1,y1+1);
		c.closePath();
		c.fill();
		//縦棒
		c.beginPath();
		c.moveTo(x1,y1-dia);
		c.lineTo(x1-1,y1);	
		c.lineTo(x1,y1+dia);
		c.lineTo(x1+1,y1);
		c.closePath();
		c.fill();
	}

	//通常の星(円だけ)*************************************
	var a = [1,1,1,1,1.5,1.5,2,2.5];	
	c.fillStyle = "#ffffff";
	c.shadowColor = "#ffffff";
	c.shadowOffsetX = 0;
	c.shadowOffsetY = 0;
	c.shadowBlur = 3;
    for(var i = 0; i< 300; i++){   
		var x = Math.floor(Math.random()*maxX)*10;
		var y = Math.floor(Math.random()*maxY)*10;
		var n = Math.floor(Math.random()*a.length);
		var radius = a[n];
		c.beginPath();
		c.arc(x, y, radius, 0, Math.PI*2, false);
		c.closePath();
		c.fill();
	}

	//輝く星(円+十字)*********************************
	for(var i=0; i<20; i++){
		var x = Math.floor(Math.random()*maxX)*10;
		var y = Math.floor(Math.random()*maxY)*10;
		
		//円
		var dist = Math.floor(Math.random()*3)+1;
		c.fillStyle = "#ffffff";
		c.shadowBlur = 4;
		c.beginPath();
		c.arc(x, y, dist, 0, Math.PI*2, false); 
		c.closePath();
		c.fill();

		var diff = dist*(2/3);
		//横棒
		c.lineWidth=0.8;
		c.strokeStyle = "#ffffff"; 
		c.beginPath();
		c.moveTo(x-(dist+diff),y);
		c.lineTo(x+dist+diff,y);
		c.closePath();
		c.stroke();
		
		//縦棒
		c.beginPath();
		c.moveTo(x,y-(dist+diff));
		c.lineTo(x,y+dist+diff);
		c.closePath();
		c.stroke();
	}
}

このスクリプトで覚えたこと(個人的備忘録)

Canvas初心者の私が、これを作成するにあたって初めて知った知識を以下に備忘録兼学習メモ的に記します。

線形グラデーションを描く

これはcreateLinearGradientメソッドというものを使います。

createLinearGradient(x0, y0, x1, y1)という形を取り、引数1と引数2はグラデーションの開始地点のxとy座標、引数3と引数4はグラデーションの終了地点のxとy座標です。

画像を回転させる

描いた星空の中に、斜めの線で表現した流れ星があります。これは真横に書いた図形をrotateメソッドで斜めにしています。このメソッドは引数を1つ取り、ラジアンで回転させる角度を指定します。ただ、このメソッドは対象のオブジェクトを中心に回転させるのでなく、画面の左上を起点に回転させてしまうので、そのままでは使いづらいです。そこで一緒に使うのが次のに紹介するtranslateメソッドです。

座標位置をずらす

Canvasは、デフォルトでは座標の左上が(0,0)ですが、translateメソッドを使うと座標の位置をずらすことができます。これをrotateメソッドと組み合わせて使うと、rotateで回転させる座標を左上起点でなく、回転させる自分自身にすることができます。

※注:なのですが、今回のコードはそうなっていません。rotateしたとき、ほどよい場所に流れ星が来るよう適当な数字を入れているだけです。

canvasの状態を記憶して元に戻す

ちなみにtranslateメソッドで座標をずらすと以降が座標が全部ずれてしまうので、saveメソッドでtranslate前のCanvasを保持しておき、流れ星を作ったあとにrestoreメソッドを使って元に戻しています。こうすれば以降の記述はtranslateメソッドの影響は受けません。

ぼかしをかける

夜空に輝く星を作るのに欠かせないのが、ぼかしです。shadowBlurプロパティを指定するとオブジェクトにぼかしをかけられます。

Canvasのサイズを画面幅に合わせる

私がいままで作ってきたプログラムでは、CanvasのサイズはCSSやHTML側で固定値にすることが多かったのですが、今回は開いた画面いっぱいに描画したいので、可変にする必要があります。画面幅に合わせてCanvas領域を指定する方法を探したところ以下のように書けばOKのようです。

canvas.width = document.documentElement.clientWidth;
canvas.height = document.documentElement.clientHeight;

今回手を抜いたところ

星空はランダムに描画されますが、面倒だったので流れ星は場所が決め打ちになっています。その点は手を抜きました。

小さい星はただ丸とか単純な十字を書いただけですが、光り輝く星は割ときれいに作れたかなと個人的に気に入ってます。

Canvasでアート系はもっと作ってみたいです。(目指すはジェネレーティブアート)

PhotoshopとJavaScriptでフォルダ内の画像のサイズを一括変更する

f:id:tdyu5021:20191106014946p:plain

紙媒体、Web媒体問わず、制作業務を行っていると画像ファイルのサイズを変更する作業が多く発生します。これが面倒なので、JavaScriptを使ってPhotoshopで複数の画像サイズ変更を一発で行うスクリプトを作成しました。その方法を記します。

フォルダ内の画像のサイズを一括して変更するJavaScript

最初にコードを載せておきます。以下のコードをテキストエディタに貼り付け、JSファイルまたはJSXファイルとして保存したうえで、Photoshopの[ファイル]→[スクリプト]→[参照]からこのファイルを選択し実行すると処理が始まります。

(function(){
    
var arr = new Array();
arr = ["jpg","jpeg","png","gif","psd","bmp"];

//単位をピクセルに変更
preferences.rulerUnits=Units.PIXELS; 

//対象のフォルダを選択
var folderObj = Folder.selectDialog("フォルダを選択してください");
    if(folderObj===null){
        return;
    }

//幅を入力するダイアログを表示。数字以外入力できないようにする
var regex = false;
while(regex==false){
var n = prompt("幅を指定してください",1500);
var regex = new RegExp(/^[0-9]+$/);
regex = regex.test(n);  
    if(n===null){
        return;
    }
    if(!regex){
        alert("数字を入力してください");  
    }
}

//解像度を入力するダイアログを表示。数字以外入力できないようにする
var regex2 = false;
while(regex2==false){
var reso = prompt("解像度を入力してください",72);
var regex2 = new RegExp(/^[0-9]+$/);
regex2 = regex2.test(reso);  
    if(reso===null){
        return;
    }
    if(!regex2){
        alert("数字を入力してください");  
    }
}

//フォルダ内のファイル全部をフルパスで取得
var fileList = folderObj.getFiles();

for(var i = 0; i< fileList.length; i++){
    try{
        var fName = fileList[i].name;//ファイル名だけ取得
        var reg=/(.*)(?:\.([^.]+$))/;
        fName = fName.match(reg)[2];//拡張子だけ取得
    }catch(e){
            $.write("エラー" +'\n');
        }

    //拡張子を調べ、対象のファイルが画像以外ならスキップ
    fName = fName.toLowerCase();
    var isExist = checkExist(arr, fName);
    if(isExist==false){
        continue;
    }

    //ファイルを開く
    var fileObj = new File(fileList[i]);
    open(fileObj);
    var flg = fileObj.open("r");

    var ad = activeDocument;
    var x=ad.width; //開いたファイルの幅を取得
    var y=ad.height; //開いたファイルの高さを取得

    var newX;
    var newY;

    if(x>y){
        newX = parseInt(n);
        newY = (y * newX) / x;
    }else{
      newY = parseInt(n);
      newX = (x * newY) / y;
    }

    //リサイズする
    ad.resizeImage(newX, newY, reso, ResampleMethod.BICUBIC);
    ad.save();
    ad.close(SaveOptions.DONOTSAVECHANGES);
}
alert("完了しました");

})();

function checkExist(arr_, str){
    for(var i = 0; i<arr_.length; i++){
            if(str==arr_[i]){
                return true;
             }        
    }
    return false;
 }

内容としては次の通りです。

まずフォルダ選択ダイアログが表示されるので、任意のフォルダを選びます。その後、幅を指定するダイアログと解像度を指定するダイアログが現れますので、そこで指定したい数字を入力すると、先ほど選択したフォルダ内にある画像ファイルの長辺がすべて指定したピクセル数にサイズ変更され、すべて終わると「完了しました」というメッセージが表示されます。

この流れを実行したのが、以下のgif動画です。

f:id:tdyu5021:20191106020912g:plain

コードのポイントを解説

画像の単位を決める

まずサイズ変更するに当たり、1500pxにするのか、1500mmにするのか、単位を指定して置かなければなりません。今回はピクセルにします。単位をピクセルに指定する記述は以下です。

preferences.rulerUnits=Units.PIXELS;

サイズ変更するresizeImageメソッド

使いやすいようにいろいろ機能は足していますが、このスクリプトの大まかな流れは以下の通りです。

  1. フォルダ内のファイル名をすべて取得
  2. そのファイル名をもとにファイルを開く
  3. resizeImageメソッドでサイズを変更する
  4. ファイルを閉じる
  5. 上記の2-4をループ

その中でもコアとなるのが、画像をリサイズするresizeImageメソッドです。このメソッドは以下の引数を取ります。

f:id:tdyu5021:20191108232455p:plain

※このスクリプトを書いたときに知らなかったのですが、縦横比を固定にして拡大縮小したいときは、もう片方を"undifined"と書けば成り行きで変更されるそうです。私が今回作ったように、片方の辺の縮尺率に合わせてもう1辺のサイズをわざわざ記述する必要はないそうです。

画像ファイルだけ操作する

フォルダオブジェクトには、selectDialogメソッドという、選択ダイアログでフォルダを選ぶメソッドが存在します。まずはそれによってフォルダオブジェクトを生成します。その後、GetFilesメソッドでフォルダ内にあるファイル名の文字列をすべて取得します。ファイル名は配列で取得されます。

そのファイル名を利用してファイルオブジェクトを生成し、各ファイルのリサイズ処理をループさせます。ただフォルダに画像フォルダがなかったり違うファイルがあるとエラーになってしまうので、「画像ファイルが含まれているか」を調べる記述を書きました。それがコード内のcheckExistという関数です。

それを判定するロジックとしては、あらかじめ画像ファイルの拡張子を配列に入れておき、対象のファイル名の拡張子がその配列内拡張子に合致すればリサイズし、そうでなければ処理をスキップ(Continue)します。

なお、ファイルが画像ファイルであるかどうかを判定するには、ファイル名から拡張子を抜き出す記述が必要ですが、Qiitaにピッタリの記事があったので、ありがたく拝借しました。

qiita.com

画像が縦長か横長かを判断する

今回のスクリプトでは、縦長画像だったら天地を指定したサイズに、横長だったら左右を指定したサイズにするというスクリプトにしています。

これについては、特に難しいことはなく、開いたファイルオブジェクトに対して、widthプロパティ、heightプロパティで天地左右それぞれの値を調べて比較し、その大小で処理を分岐するif文を書いているだけです。

ざっくり要点だけ説明すると以上になります。記述数も少なく、割と汎用的に使えるスクリプトではないかなと思っています。

 

Adobe ExtendScriptとは何か?JavaScriptとの関係性を調べた

前から存在は知っていたけど、あまり情報がない「ExtendScript」。今回は私の中での情報整理も兼ねてとても簡単にまとめてみようと思います。 

 

ExtendScriptAdobe製品を動かすJavaScriptの方言

ExtendScriptは、一言で言うと、Adobe製ソフトを自動化するために用意されたJavaScriptの亜種というか方言のようなものです。

Adobe社が作っている一部のデザイン系ソフトは、スクリプトを書いて操作を自動化することができ、そのためのプログラミング言語として以下の3つをサポートしています。そのうちの1つがExtendScriptです。

AppleScriptmacOS上動き、VBScriptWindows OS上で動きます。ExtendScriptJavaScript)はどちらのOSにも使用できます。

ちなみに、少々古いですが、Adobe社が公式に出しているガイドではExtendScriptについて以下のような説明がありました。

Adobe provides an extended implementation of JavaScript, called ExtendScript, that is used by many Adobe applications that provide a scripting interface. In addition to implementing the JavaScript language according to the ECMA JavaScript specification, ExtendScript provides certain additional features and utilities.

https://www.adobe.com/content/dam/acom/en/devnet/scripting/estk/javascript_tools_guide.pdf

精度はともかくとして、ざっと訳すと以下のような感じです。

AdobeJavaScriptを拡張して実装した「ExtendScript」を提供しており、スクリプトインターフェースを提供する多くのAdobeアプリケーションで使用することができます。ECMA JavaScriptの仕様に則って実装されているだけでなく、ExtendSciptでは、数々の役立つ機能や特徴を備えています。

また、Scripting Developer Centerのサイトによると、ExtendScriptで動かせるアプリケーションは

上記のようです。

JavaScript」と呼ぶのは厳密には間違い?

Adobe社が出しているドキュメントを見ても、ExtendScriptという言葉は使われず、ほとんどJavaScriptと書かれています。便宜上のためでしょう。

基本的にExtendScriptJavaScriptと呼んでも概ね問題ないのですが、これをJavaScriptと呼ぶのは、厳密には正しくありません。またECMAScriptJavaScriptExtendScriptの関係性を理解していないことで私もちょっとしたトラブルがありました。これは後ほど触れますが、まず以下にその成り立ちに触れておこうと思います。

ExtendScriptECMAScriptの実装の1つ

JavaScriptを厳密に説明すると、まずECMAScriptという標準化された言語があり、その仕様に則って各ブラウザが実装した言語のことをJavaScriptと呼びます。

ExtendScriptについても同様で、ECMAScriptに則ってAdobe社がAdobe製品用に実装したものをExtendScriptといいます。例えるならJavaScriptの「方言」というか「兄弟」といってもよいでしょう。

JavaScriptのコードで動かないものもある

なんでこんな厳密な説明を書いたのかというと、JavaScriptとして書いたものが何でもExtendScriptで動くと勘違いしないようにです。そうです。私は実際に勘違いしていました。

私もこうした関係性をよく知らなかったのでJavaScriptのコードのたいていはExtendScriptで動くのかなと漠然と思っていたのですが、実際そうではありませんでした。この理由は次のとおりです。

まず、先述のECMAScriptは、年々バージョンアップし、どんどん新しい機能が追加されています。ECMAScript1から始まり、ECMASCript2、ECMAScript3と続き、4は欠番で現在は6が最新版のようです。

ブラウザはそれに対応しようとするので、ECMAScriptのアップデートに合わせて、順次主要なブラウザは最新版で対応し最新の文法などが動くわけです。

一方で、ExtendScriptと解釈して実行するAdobe社の各製品はどうかというと、別にECMAScriptのアップデートに追随するわけではないのです。過去に策定されたECMAScriptベースのままです。

じゃあAdobe製品はどのバージョンに準拠しているのかというと、Wikipediaによれば、1999年に改訂されたECMAScript 3「ECMA-262 3rd edition」に準拠しているとのこと。

つまり、それ以降に追加されたJavaScriptの機能はExtendScriptで使用することができないというわけです。 

indexOfメソッドが動かない事件

というわけで、ECMAScriptJavaScriptExtendScriptの関係性をよくわかっていないと、私みたいに以下のようなことをやらかしてしまうわけです。

ツイートの通りですが、JavaScriptのindexOfは比較的新しいメソッドですが、Adobe製品はそれより古いJavaScriptの仕様に準拠しているため、このメソッドが動かないということです。

これが、先程最初のほうで基本的にExtendScriptJavaScriptと呼んでも「おおよそ」は問題ないと書いた理由です。逆に言えばブラウザ用のJavaScript感覚で書くと動かないところがポツポツ出てくるということです。

ExtendScriptJavaScriptの関係性だったりAdobe製品はJavaScriptで動かすことができる」の厳密な意味はちゃんと知っておくべきなだなー、というのが本日お伝えしたい内容でした。

VBAにおけるクラスモジュールの使い方と必要性をすぐろくプログラムで考えてみる

f:id:tdyu5021:20191019024036p:plain

エクセルVBAの中でも、初学者には非常にわかりづらいイメージのある「クラスモジュール」。今回はすごろくプログラムを例にとり、クラスモジュールと必要性と使い方について限りなく丁寧に解説してみます。

ワークシートで作るすごろく(みたいな)プログラム

まずはじめに以下のGIFをご覧ください。

f:id:tdyu5021:20191020025247g:plain

わけのわからないマクロですが、「ランダムに数字を生成」して、「コマを進めている」ので、ここではこれを一種の「すごろくプログラム」であると仮定してください…笑

「サイコロを人が振らずに勝手にプレーしているじゃん」とか「動物が競争しているだけじゃん」など細かいツッコミはおいておき、なぜこれをお見せしたのかと言うと、このプログラムはクラスモジュールの必要性を説明するのにとても都合が良いからです。

説明のために非常に簡略化していますが、このプログラムでは、以下のような内容を実装しています。

  1. シェイプオブジェクトですごろくのコマを何体か設置する
    (今回は、犬、猫、うさぎの3体)
  2. 数字をランダムに生成し、コマの名前をメッセージで発してマスを進める
  3. これを1体ずつ行う
  4. 上記の(2)~(3)を繰り返す

 ではこれをどのようにプログラムで表現したら良いでしょうか。

どういうコードで実現するか?

今回、クラスモジュールの必要性を際立たせるために、まずはあえてクラスモジュールを使わないやり方をいくつか並べてみます。プログラムをもっと簡略化するため、ゴールまでループさせずに上記の手順の1から4までの手順のうち(2)と(3)を実現する方法を記します。「そんなのはどうでもいいから、クラスモジュールを使った方法が見たい」という方は、「クラスモジュールを用いた実装方法」の見出しから読んでください。

 サンプルソースコード1(超初心者向け)

前提条件として、

・犬の図形オブジェクト名を"dog"に
・猫の図形オブジェクト名を"cat"に
・うさぎの図形オブジェクト名を"rabbit"に
・列の幅を90ピクセルに設定

上記の設定を済ませた上で、以下のコードを実行すると、3体の図形は1回ずつ移動します。

Sub sugoroku1()
    Dim dog As Shape, cat As Shape, rabbit As Shape
    Dim dogName As String, catName As String, rabbitName As String
    Dim n As Integer
    
    Set dog = ActiveSheet.Shapes("dog")
    Set cat = ActiveSheet.Shapes("cat")
    Set rabbit = ActiveSheet.Shapes("rabbit")
    dogName = "ワンコ"
    catName = "ニャンコ"
    rabbitName = "ウサコ"
       
    n = Int(Rnd * 3) + 1
    MsgBox dogName & "は" & n & "進みます"
    dog.IncrementLeft (45 * n)
    Application.Wait [Now() + "0:00:00.2"]
    
    n = Int(Rnd * 3) + 1
    MsgBox catName & "は" & n & "進みます"
    cat.IncrementLeft (45 * n)
    Application.Wait [Now() + "0:00:00.2"]
    
    n = Int(Rnd * 3) + 1
    MsgBox rabbitName & "は" & n & "進みます"
    rabbit.IncrementLeft (45 * n)
    Application.Wait [Now() + "0:00:00.2"]
End Sub

上記は各画像をshapeオブジェクトで変数に格納し、IncrementLeftという図形の左位置を変更するメソッドを用い、その画像を移動させるというプログラムです。Application.waitで0.2秒待っているのは、そうしないと、メッセージボックス→移動→メッセージボックス→移動とステップで実行されないからです。

もちろん、これはかなり汚いコードです。「ランダムに数字を生成して移動する」という処理は犬も猫もうさぎも共通ですので、それらをひとまとめにするのが合理的です。そのように書き換えたのが以下です。

サンプルソースコード2(改良版)

Sub sugoroku2()
    Dim piece(2) As Shape
    Dim pieceName(2) As String
    Dim n As Integer, i As Integer
    
    Set piece(0) = ActiveSheet.Shapes("dog")
    Set piece(1) = ActiveSheet.Shapes("cat")
    Set piece(2) = ActiveSheet.Shapes("rabbit")
    
    pieceName(0) = "ワンコ"
    pieceName(1) = "ニャンコ"
    pieceName(2) = "ウサコ"
    
    For i = 0 To 2
        n = Int(Rnd * 3) + 1
        MsgBox pieceName(i) & "は" & n & "進みます"
        piece(i).IncrementLeft (45 * n)
        Application.Wait [Now() + "0:00:00.2"]
    Next i
End Sub

この改良版のポイントとしては、配列変数を用いることで、各変数に対して行う処理をForループで集約しています。今回はIncrementLeftしかないので、このプロシージャ内に移動の処理を書いていますが、もっと行数の長い複雑な処理になればその部分を別のプロシージャや関数にして部品化してもよいでしょう。

本当にこれがベストなのか? 

しかし、このプログラムはこういう実装が正解なのでしょうか。確かにコードの重複がなくなったので記述こそはシンプルになりましたが、管理性があまり良くありません。その理由は次の通りです。 

●理由1:コマが増えたときが面倒

現在、コマは犬と猫とうさぎなので配列変数の要素数は”2”を入れています。しかし、コマ数を増やしたりすると、その都度、配列変数の要素数も書き換えなければなりません。もちろん、配列変数の要素数の数字も変数化してしまえば一括管理できるので実用に問題はありませんが、少し気持ち悪さが残ります。

●理由2:各コマの処理が増えたときの対処が面倒

今のプログラムでは、各コマの名前(ここではpieceName()という配列変数)だったり各コマを移動させるという処理しかつけていませんが、今後すごろくゲームを拡張していくとき、もっと機能をつけていくことがあるでしょう。

例えば、1回休みの機能をつけることもあるでしょう。その際には、各コマが1回休みかどうかを判定する配列変数が必要になります。このように、コマの状態や属性を判定する機能をつければ、どんどん管理すべき配列変数が増えていく可能性があります。

オブジェクト、プロパティ、メソッドで管理できればもっと便利になる

上記の疑問に対して「別にそれで問題ない」と思われる方もいるかもしれません。しかしこうしたプログラムの場合、すごろくのコマというオブジェクトを中心に、プロパティ、メソッドという概念で管理できるとより便利になります。それを実現するのが「クラスモジュール」なのです

この意図を説明するために、念のため超初心者の方に向けてここでVBA(に限らず多くのプログラミング言語)の基本に立ち返ってみたいと思います。

そもそもオブジェクト、プロパティ、メソッドとは何だ?

VBAを含む多くのプログラミング言語では、オブジェクトがあり、それに対するメソッドやプロパティという概念を持っています。

VBAの場合、セルというオブジェクト(操作の対象)があり、Copyというメソッドがあったり、ValueやInteriorというプロパティが存在します。例えると、人間というオブジェクトに対して、歩くというのがメソッド、年齢、性別というのがプロパティといえるかもしれませんね。

これをすごろくに当てはめるとどうなるか

この考えをすごろくプログラムに取り入れると非常にわかりやすくなります。

すごろくプログラムには、例えばコマに対して「名前(※すごろくゲームに必須ではありませんが・・)」「現在の位置」「1回休みであるかどうか」という属性があれば、「進む」「戻る」「スタートに戻る」などの処理が存在するはずです。言い換えればコマにはプロパティメソッドがあるともいえるでしょう。

つまり次の発想も可能になります。

皆様がCells(1,1).Copy~~とか、Cells(1,1).Value =~~でセルオブジェクトを操作するのとまったく同じように、すごろくの犬のコマをオブジェクトとみなし、dog.move~~とかdog.name=~~と記述するのです。

これにはいろんなメリットがあります。

1つにdog.moveで「ああ犬が動くのだな」という直感的な理解を促せること。もう1つは、プログラムを機能拡張したときにそれをプロパティやメソッドというパーツとして、メインのモジュールとは別に管理できるようになることです。

つまりメインモジュールには影響を与えないため、拡張性やメンテナンス性が高いプログラムを作ることができるのです。

独自のオブジェクトに対してメソッドやプロパティを作れる

いうまでもなく、dogというオブジェクトもなければmoveやnameといったプロパティは存在しません。しかしクラスモジュールを利用すればそれが可能になるのです。つまりクラスは独自のオブジェクトやメソッド、プロパティを定義できるのです。

独自のメソッドやプロパティは、もちろんdogというオブジェクトだけのものではありません。catでもrabbitでも、その他の動物でも構いません。cat.moveとか、rabbit.nameとかでもよいのです。

要するにクラスモジュールでは、後々オブジェクトをdogでもcatでもrabbitでも入れられるように、「入れ物」を定義して「ひな形」を作っておきます(ここでは仮に入れ物の名前を”animal”としておきます。同時にanimal.move、animal.nameというプロパティやメソッドなども定義します)

その後、必要に応じてdogやcatやrabbitといったオブジェクトの「実体」をanimalという入れ物に当てはめていくのです。このとき、作成したひな形を「クラス」とよび、dogやcatやrabbitなどでクラスに入った実体をインスタンスと呼んでいます。

クラスモジュールを用いた実装方法

それではここから先ほど作ったプログラムを、クラスモジュールを使って実現する方法を見ていきましょう。

まず先ほどのanimalという、オブジェクトを入れるためひな形の作り方です。「挿入」「クラスモジュール」を選択し、新たにクラスモジュールができるので、そこを任意の名前(今回はanimalというクラス名にします) 

f:id:tdyu5021:20191020015440p:plain

独自のメソッドを追加する

手始めに独自のメソッドを追加したいと思います。標準モジュールにプロシージャを作るのと同様に、まず独自に作るメソッド名にしたいものをプロシージャ名にして以下のプログラムを書きます(※後述しますが、一部ダミーの文字列あり)。

以下を見ておわかりのとおり、Excelのクラスモジュールにプロシージャを書くと、それがそのクラスのメソッドになるのです。(メッセージボックスの部分は記載していません。ここは後ほど解説します)

'******************
'*クラスモジュール*
'******************
Public Sub move()
    Dim n As Integer
    '//進む数をランダムに生成
    n = Int(Rnd * 3) + 1
    '//図形オブジェクトを進める
    xxxxxxxx.IncrementLeft (45 * n)
    Application.Wait [Now() + "0:00:00.2"]
End Sub

しかし当然ながらこのコードは動きません。incrementLeftするオブジェクトをまだxxxxxxxxのダミー文字列にしており、そこには何も入っていないからです。この部分をどうすればよいのかに触れる前に、ここで作成したanimalクラスに「実体」を当てはめる作業から見ていきます。

animalクラスに犬や猫などの実体をいれる

ここまでanimalクラスを作成し、さらにコマを動かすmoveというメソッド(暫定)まで作成できました。早速それらをワークシート上にある犬や猫やうさぎの図形とひも付けたいと思います。ここで標準モジュールに戻ります。

まずdog.moveなどができるようにするには、”dog”がanimalクラスのオブジェクトだということをプログラムに教えなければなりません。プログラミング的に言うと「クラスを宣言する」という作業です。標準モジュールに以下のように記述します

Dim dog As New animal

クラスの宣言は

dim [変数名] As New [クラス名] 

というかたちになります。

では、本当にdogはanimalというクラスの実体(インスタンス)となることができたのでしょうか。試しに、標準モジュールにdog.まで入力してみると…

f:id:tdyu5021:20191020020610p:plain

メソッドの自動メンバー表示にmoveが出てきた!

つまりdogというオブジェクトが作成され、さらにそれはmoveというメソッドを持っているということを意味しています。

ただし、ここでdog.moveを実行したところで、ワークシート上の犬の図形は動いたりしません。エラーを起こします。当然です。「moveするオブジェクト=犬の画像」だということがまだどこにも書かかれていないからです。

そのため、先ほどのクラスモジュールに書いたxxxxxxxxx.moveのxxxxxxxxの部分(オブジェクト)が犬の画像であるということを定義しましょう。

まずxxxxxxxxを適当な変数名にしておきます。今回moveさせるのは犬なのですが、このmoveはcatとかrabbitとかいろんな動物の実体に対するメソッドなのでオブジェクトは抽象的な命名にするべきです。図形をmoveさせるということで、ここではshpという変数名にしておきましょう。

そしてこの変数shpの中にワークシート上の図形を格納しなければなりません。しかしクラスはひな形であって今後dogでもcatでもrabbitでも何でも当てはめますから

set shp = ActiveSheet.Shapes("dog")

のようにdogという単語を直接記入するわけにもいきません。

そこでどうすればよいかというと、変数shpにオブジェクトを当てはめるという処理を、メソッドとしてanimalクラスに書いておき、それをshpに入れたい好きな図形の引数つきで標準モジュールから呼び出せばよいのです。まずクラスモジュールには以下のように書きます。

'******************
'*クラスモジュール*
'******************
Public Sub setting(animalShape)
    Set shp = animalShape
End Sub

オブジェクトをセットするのでモジュール名(メソッド名)はsettingとかにしておきました、引数をもらってくるので、括弧の中に、適当な引数名を入れてください。動物の図形をもらってくるのでanimalShapeとつけています。

またsettingプロシージャと先ほどのmoveプロシージャ間でshpを使えるようにモジュール間の変数として宣言するのを忘れないようにしましょう。

さてここまでで、クラスモジュールには以下のようなコードができ上がりました。

'******************
'*クラスモジュール*
'******************
Dim shp As Shape
Public Sub setting(animalShape)
    Set shp = animalShape
End Sub
Public Sub move()
    Dim n As Integer
    '//進む数をランダムに生成
    n = Int(Rnd * 3) + 1
    '//図形オブジェクトを進める
    shp.IncrementLeft (45 * n)
    Application.Wait [Now() + "0:00:00.2"]
End Sub

標準モジュールからメソッドを操作する

そして標準モジュールの方に戻りましょう。settingという独自のメソッドを定義したので、今度はdog.まで入力すると、今度は自動メンバー表示にメソッドとしてmoveとsettingの2つができていることがわかります。ここでsettingを選び、その後ろにsettingメソッドに渡したい引数を記入します。

引数の書き方ですが、メソッドから半角スペースを開けて記入します。今回はまず犬の図形を動かしたいので、その情報を引数として渡すためにmoveの後ろにActiveSheet.Shapes("dog")を記入しています。

f:id:tdyu5021:20191020022240p:plain

独自のプロパティをセットする 

クラスモジュールは独自のメソッドと同様に独自のプロパティを持つことができますので、これも解説しましょう。

このすごろくプログラムではコマそれぞれに「名前」をつけていました。そこで、nameというプロパティを定義します(※もちろんプロパティ名は何でも構いません)。それ自体は非常に簡単です。ただnameという変数をPublic変数で宣言すればOKです。

f:id:tdyu5021:20191021021040p:plain

試しに、標準モジュールに戻り先ほどと同じように、dog.まで入力してみてください。自動メンバー表示にnameが出てくるはずです。つまりdogオブジェクトはnameというプロパティを持っていることを意味します。

しかし、プロパティを作っただけでは、当然、まだ中身が何も入っていません。先ほど、shpに犬の図形を入れたように、こちらも犬の名前である「ワンコ」という文字列をセットしましょう。

先ほどはsettingというメソッドを設けて、クラスモジュールで必要な初期値をセットしていきました。別のメソッドを作ってもよいのですが、今回はこのsettingメソッドにてnameという変数に名前を格納する処理を足しておきましょう。

'******************
'*クラスモジュール*
'******************
Public Sub setting(animalShape, animalName)
    Set shp = animalShape
    name = animalName
End Sub

これだけではもちろんダメですね。このメソッドを実行する標準モジュールの方も、動物の名前を渡す引数をもう1つ追加する必要があります( 以下の「ワンコ」の部分です)

dog.setting ActiveSheet.Shapes("dog"), "ワンコ"

これで準備完了です。これでコマの名前をnameというプロパティで表示できるようになったので、まだ書かれていなかったメッセージボックスの記述を以下のようにクラスモジュールに追加しました。

MsgBox name & "は" & n & "進みます"

これで標準モジュールとクラスモジュールはそれぞれ以下のコードができたはずです。 

'******************
'*標準モジュール*
'******************
Option Explicit
Sub sugoroku3()
    '//dogという実体(インスタンス)を生成
    Dim dog As New animal
    '//犬の図形をクラスモジュール内へ渡す
    dog.setting ActiveSheet.Shapes("dog"), "ワンコ"
    '//犬を動かす
    dog.move
End Sub
'******************
'*クラスモジュール*
'******************
Public name As String
Dim shp As Shape
Public Sub setting(animalShape, animalName)
    Set shp = animalShape
    name = animalName
End Sub
Public Sub move()
    Dim n As Integer
    '//進む数をランダムに生成
    n = Int(Rnd * 3) + 1
    MsgBox name & "は" & n & "進みます"
    '//図形オブジェクトを進める
    shp.IncrementLeft (45 * n)
    Application.Wait [Now() + "0:00:00.2"]
End Sub

ここで標準モジュールのsugoroku3というプロシージャを実行してみると、「ワンコは○マス進みます」というメッセージボックスが表示されたあと犬の図形が動いたはずです。

dogという独自のオブジェクトを作成し、moveやnameという独自のメソッドとプロパティが働いていることがわかったでしょう。これこそがクラスの役割です。

猫やうさぎも足してみる

もちろん、まだこれだけでは完成ではありませんよね。冒頭で示したプログラムには猫の図形もうさぎの図形もありましたので、それを追加してみましょう。クラスのメリットでありますが、「動物のコマを処理する」というひな形はもうクラスモジュールに部品化されているので、そちらを触る必要はありません。

・dogという変数をクラスとして宣言し
・そのdogに初期値を与え
・moveというメソッドを書く

ということを、猫とうさぎにも適用すればよいだけです。以下のようになります。

'******************
'*標準モジュール*
'******************
Option Explicit
Sub sugoroku3()
    '//dogという実体(インスタンス)を生成
    Dim dog As New animal
    '//catという実体(インスタンス)を生成
    Dim cat As New animal
    '//rabbitという実体(インスタンス)を生成
    Dim rabbit As New animal
    
    '//犬の図形をクラスモジュール内へ渡す
    dog.setting ActiveSheet.Shapes("dog"), "ワンコ"
    '//猫の図形をクラスモジュール内へ渡す
    cat.setting ActiveSheet.Shapes("cat"), "ニャンコ"
    '//うさぎの図形をクラスモジュール内へ渡す
    rabbit.setting ActiveSheet.Shapes("rabbit"), "ウサコ"
    
    '//犬を動かす
    dog.move
    '//猫を動かす
    cat.move
    '//うさぎを動かす
    rabbit.move
End Sub

本当にこれがメリットになるの? 

あまりにもシンプルすぎるため、これだけ読んでもまだクラスのメリットをお感じになっていない方も多いかもしれません。

確かに、上記のプログラムがまさにそうですが、クラスモジュールを利用すると通常のやり方で書く方法に比べてむしろコードの量が増えることもあります。ただコード量が増える増えないは本質ではありません。

先ほど、「管理」や「拡張性」というキーワードを挙げたかと思いますが、今後上記のプログラムに、例えば「スタートに戻る」というメソッドを追加するときは、同じようにクラスモジュールにプログラムを書き、その機能をいざ呼び出すときは、標準モジュールで例えば、dog.backToStartとかのように書けば済むのです。その他のプロパティについても同様です。

いかがでしょうか。そう考えると少しメリットがあるような気がしないでしょうか。

ちなみに上記のクラスモジュールを使用したサンプルは、実際にはまだ美しいコードではありません。dogという名前でクラスを生成しましたが、そもそもdog、cat、rabbitはすべて同じ処理をするため、それらをループさせなければなりません。

ですので、冒頭の方に記した2個めのサンプルソースコードのように、実際には配列変数を使ってクラスを宣言していくことになるでしょう。

ただ、そうするとサンプルコードが見づらくになるため、今回はそこまでは書きませんでした。

最後に少し言い訳

解説としては以上になります。

私もオブジェクト指向プログラミング言語を触ったことがなく、VBAについてもクラスモジュールを使い慣れているわけでもありません。むしろ今でもよくわかっていないところがあります。

そのため、今回の説明がクラスの普遍的な説明になっていることわけではないことをご承知おきください。詳しい方が読んだら「そこは違う」という部分もあるでしょう。

ただ、私自身クラスモジュールの解説を他の記事で読んで思ったのが、厳密な説明をすればするほど説明や例えが難しいなという印象でした。そこで実用性はともかく、クラスを使うとこんなことができるというのを感覚的に理解できるように今回の記事を書いてみました。

私が苦戦したように、これからクラスモジュールを使う人に少しでも参考になればありがたいなと思います。