Programming」カテゴリーアーカイブ

Delphiによるプログラミング関係のTips

無効な前方参照か、コンパイルされていない種類への参照です。

2023年12月30日、Excelファイル(.xlsm)の読み書きで、見たことないエラーメッセージに遭遇。
それがコレ!

朝から晩まで、右往左往、ほぼ1日ハマりました T_T

これは、その解決方法です。

このエラーに遭遇した際、改良していたのはこちらのプログラムから読み取ったマークをExcelへ出力するコードです。もし、よかったらあわせてご参照ください。

自作のマークシートリーダーです。プログラムは上記リンク先から無料でダウンロードできます。単体でも動作しますが、別途組み込みPython環境を追加するとさらに高速動作します。
自作のマークシートリーダーの読み取り速度をPython4Delphiで高速化。組み込み用Pythonのプログラム一式は上記リンク先から無料でダウンロードできます。

【もくじ】

1.まったく同じコードが動かない!
2.朝からのできごと
3.Web上の情報は?
4.壊れてたのはプログラムじゃなくて・・・
5.まとめ
6.お願いとお断り

1.まったく同じコードが動かない!

Delphiで作成した Excel Book 読み書き練習プログラムでは『何の問題もなく動いたコード』。これを、そのまま本番用プログラムにコピペしたら、なぜか動かない!・・・という、これまでに経験したことのない問題に遭遇しました。

解決するための唯一の手掛かりは、冒頭のエラーメッセージ。

(でも・・・このエラーは、これまでに 一度も見たことない・・・ T_T )

目を皿のようにしてコードを見ても、練習用プログラムと本番用プログラムで、一字一句違ってない。てか、コピペしたんだから、違ってるわけがない・・・。
なのに、練習プログラムは期待通りに動作し、本番用プログラムは絶対に動かない。
必ず、このエラーで停止する・・・

これまで、いろいろなエラーに遭遇したけれど、このエラーは未経験。
もちろん、周囲に頼れる人はいない。自分で何とかするしか、ない。

なんでこんなコトに・・・

2.朝からのできごと

朝、目覚めてからずっと、むかし書いたExcelのマクロ有効Bookを読み書きするDelphiのプログラムを改良してたら、なんかこのエラーが出るようになって・・・。それから、データの書き込みが出来ない!

今、思えば、この時点で気がつくべきことがあったんだけれど、その時の僕は自分で書いたコードの何処かに間違いがあるはずだと思い込んでいるので、その「間違い」をさがすことに夢中になっていて、その他のことはまるで眼中にない。

ってか、Excel関連のプログラムはそこがやっかいなんだけど、大ゴケすると起動したExcelのプロセスが残ってしまい、Ctrl+Alt+Delで残ってるExcelのプロセス探して終了させてって、その繰り返しになってしまう。

この日はエラーの原因がわからなくて、これを際限なく繰り返してしまい、システムも何度か再起動。

で、さんざん頑張って彷徨った挙句、考えたことは・・・

問題の切り分けのため、シンプルなプログラムで実験してみよう!

・・・ということ。

Delphiから接続するExcel Bookはマクロ有効テンプレートにしてあるから、このおおもとのテンプレートをダブルクリックするたびに新しいマクロ有効Excel Book(拡張子はxlsm)が生成される。

そうやって新しいマクロ有効Excel Bookを作成。これを入れる所定のフォルダを作り、保存。準備万端にして、新しい Windows VCLアプリケーションも作成。で、これまで勉強した中で、いちばん動作が確実と思えるコードで「ワークシート間で式をコピーする」手続きを作成、コンパイル、そして「実行」。期待通りに、エラーなく、データのコピー(読み出しと書き込み)終了。胸がすっきり。Bookを開いて結果を確認。データはちゃんと書き込まれ、ワークシートが初期化されてる。もちろん、Excelもきれいに終了。タスクマネージャーで確認してもプロセスは残ってない。

改良中の元のアプリに戻って、新しくボタンを一つ作成。ダブルクリックして新しい手続きを一つ、作り、ここへ、ついさっき動くことを確認したばかりのコードをコピペする。で、実行すると・・・

なんでだよ!!

みたいな・・・

3.Web上の情報は?

もう、こうなったら Googleせんせー に聞きまくるしか、ない。

いくつかのそれらしい情報がヒット。例えば・・・


『sheet名はマクロの実行順に並んでいる必要がある』

(ほんとにー?)

さっそくSheetの順番をそれっぽく入れ替えて実行。結果、変化なし。エラーは普通に出る。


『解決策はモジュール単位の再コンパイル』

(そうなのか?)

ビルドし直しても、変化なし。これも違った。少なくても僕のエラーには効かない。


『あんなことや、そんなことや、こんなことも』全部ダメ。

(マジ、泣きそう T_T )

(このプログラムが動かないと、約束が・・・)


徹底抗戦の覚悟も新たに、腰を据えて、検索結果を、上から順番に、全部開けて熟読。
そしたら次のリンク先ページに、今日一日考えもしなかった情報を発見!

Excel VBA ― マクロ実行時に謎のオートメーションエラー“-2147319767”が発生する場合の対処法

https://ippeintel.com/archives/4644 より引用

解決方法は二つあり、そのひとつは・・・

ブックを新しく作り直す!

これを読んだ僕は、おおきな声で
「あ っ」といいました。

もうひとつは・・・

「開いて修復する」でファイルを開く

とのこと。

4.壊れてたのはプログラムじゃなくて・・・

ファイルだったんですね。

それからExcelの名誉のために言うけど、
壊れたんじゃなくて、僕が壊しちゃったんですね・・・

エラーが出るようになった あの時から。

あわててテンプレートから新しくファイルを生成。今までのと入れ替えて書き込んでみると・・・

これはExcelからのメッセージ?

これは僕が用意した書き込み完了のメッセージ。

今日の苦労は、いったい なんだったんだ・・・

IPPEIさん、ほんとに、ほんとに、ありがとー!!

5.まとめ

(1)ソフトウェアはプログラムとデータからできています。
(2)コードの誤りでエラーになることは僕の場合「日常茶飯事」です。
(3)データが壊れても(壊せばもっと確実に)エラーになります。

・総合的に、僕の場合、エラーがでないことのほうが異常でした!!

そぉか、今日はいつも通りの
普通の日だったんだ☆

みなさん、よいお年を!!

6.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

マークシートリーダーをP4Dで高速化

マークシートリーダー第2弾!
今回は Python環境を組み込んで、マークの読み取り速度を高速化 します。
出来る限り丁寧に組み込み方法を説明しますので、どうか最後までお付き合いください。

前回の記事はこちらからどうぞ

追記(20240929)

当Blogで紹介してきた自作のデジタル採点プログラムを一つにまとめました。次のリンク先にその紹介とダウンロードリンクがあります。

【追記_P4D環境で読み取り実行時、エラーが発生するときは?】

Python環境を組み込んで、これを利用してマークシートの読み取りを実行する場合、次のエラーが発生することがあります。エラーの内容からは推測すると、エラーはテンプレートマッチングの際に利用するテンプレート画像のサイズに起因して起きているように見えますが、ほんとうの原因は違います。

このマークシートリーダーは、Python環境を利用して動作する際は、マークの有無を読み取るJpeg画像の名称(及びフォルダの階層)が次の規則に従っていることを前提としています。

ProcData\XXX\Sample-01.jpg
ProcData\XXX\Sample-02.jpg
ProcData\XXX\Sample-03.jpg
・・・
ProcData\XXX\Sample-40.jpg
ProcData\XXX\Sample-41.jpg

この命名規則にJpeg画像のファイル名(及びフォルダの階層)が従っていない場合、読み取りエラーが発生します。例えば、次のような場合です。

ProcData\XXX\Sample-01a.jpg
ProcData\XXX\Sample-01b.jpg
ProcData\XXX\Sample-02a.jpg
ProcData\XXX\Sample-02b.jpg
・・・
ProcData\XXX\Sample-40a.jpg
ProcData\XXX\Sample-40b.jpg
ProcData\XXX\Sample-41a.jpg
ProcData\XXX\Sample-41b.jpg

特に数学(や情報)用途で2枚1セットのJpeg画像を処理する際は、注意してください。このエラーを防止するには、ファイルメニューの「1 画像変換」⇨「専用画像を作成」を利用してファイル名が必ず連番になるように読み取り専用Jpeg画像を生成して、この画像に対して、マークの読み取りを実行してください。

以下、発生するエラーメッセージの一覧です。

このメッセージは2回表示されます


なお、Python環境を利用しないモード(P4Dを使用のチェックボックスをOFF:下図右上)であれば、読み取り対象Jpeg画像ファイルの名称は動作に関係しないので(読み取り速度は低下しますが)、読み取り可能です。

画面右上の □ P4Dを使用のチェックを外して、Delphi用のOpenCVで読み取りを実行。
読み取り速度は低下しますが、マークを正しく読み取っています。


【読み取り実行前に、選択肢の始まり番号も指定してください】

選択肢の番号は、デフォルト1始まりに設定してあります。教科「情報」用途で読み取りを実行する場合は、読み取り実行前に、選択肢が「1」始まりであるのか、「0」始まりであるのか、その指定を画面上の設定欄で必ず指定してください。

【もくじ】

1.Python環境を準備する組み込み用Pythonのダウンロードリンクがあります
2.Python環境のドッキング
3.高速化の確認
4.システムにC++ランタイムライブラリがない場合は?
5.Python Engine の初期化の問題?他
6.まとめ
7.お願いとお断り

1.Python環境を準備する

Qiita の記事で「 Embeddable Python 」なるものの存在を知り、ほぼ同時に Delphi に Python のスクリプトを埋め込んで、VCL で GUI を作成、内部的に Python のスクリプトを実行する方法を学びました。

この辺の詳しい経緯は、かなり前に記事として書いた通りです。

2022/01/01
2022/01/02


こうして出来上がった、マークシート読み取りに必要なライブラリだけをインストールした、組み込み用のPython環境の内容は、こんな感じです(組み込み用途に作成した Embeddable Python があるフォルダをコマンドプロンプトで開き、「 Python -m pip list 」コマンドを実行した結果です)

ライブラリの主役は Numpy と OpenCV-Python。
Pillow は、日本語を含む Path を読むためにインストール。


最初に用意した Embeddable Python が14MBくらいで(おー!ちいさい☆)と喜んだけど、上記のライブラリを三つ入れたら 158MB に・・・。

ライブラリを構成しているファイルの依存関係がわかれば、必要ないファイルを消しまくって、もっと小さく出来ると思うのですが・・・、その具体的な方法がわかりません!!

仕方がないので、そのまま組み込み用の「Python39-32」フォルダを作成。

フォルダ名の Python は、「Python関連のフォルダだよ!」ってコトが一目でわかるように工夫(?)しました。その次の 39 はVersion番号、ハイフンで繋いだ 32 は 32bit 用って意味です。

これを前回紹介したマークシートリーダーにドッキングさせます。

展開に少々時間がかかりますが、もし、よかったら使ってください。
MS_Reader 組み込み用 Embeddable Python です。

2.Python環境のドッキング

ダウンロードした「Python39-32.zip」を MS_Reader.exe のあるフォルダにコピー・貼り付け、展開してください。※ 動作確認が完了したら「Python39-32.zip」は削除しても OK です!

【展開前】

MS_Reader.exe とダウンロードした Python39-32.zip を同じ階層に置き、
zipファイルを展開(右クリックして「すべて展開」を選択)してください。
展開にはしばらく(1~2分)時間がかかります。

展開時のPC環境?によっては「ものすごく(20~30分)」時間がかかることが実際にありました!!(原因はわかりませんが、時間がかかるだけで、展開そのものは正しく行われました)

【展開後】

重要 MS_Reader.exe と Python39-32 フォルダは同じ階層に置いてください。

MS_Reader.exe と Python39-32 フォルダは必ず同じ階層に置いてください。


ここで念のため「Python39-32」フォルダの構造を必ず確認してください。

〇:Pathに注目してください。これならOK!

MS_Reader\Python39-32\Lib であり、また、
MS_Reader\Python39-32\Scripts であります。

これはダメです。Pathが二重になってます。

MS_Reader\Python39-32\Python39-32\Lib
MS_Reader\Python39-32\Python39-32\Scripts

上の「ダメな例のようにならない」ようにPython39-32.zipを作成しましたから、大丈夫だと思いますが・・・念のため、必ずご確認いただけますようお願いいたします。

以上が『 ドッキング作業 』です!!

MS_Reader.exe と同じフォルダに、Python39-32.zip をコピペして、展開すれば Python環境のドッキングは完了です。

これを夢見て、ンか月。マジ、挫けそうな時もあった・・・ けど。

MS_Reader.exe をダブルクリックして、マークシートリーダーを起動してみてください。

僕のマークシートリーダーは、自動的に、高速動作モードで、起動します。

3.高速化の確認

Python環境がないと(MS_Reader.exe がある場所に Python39-32 フォルダがない場合)・・・

MS_Reader 起動時、マークシートの読み取りを高速化するP4D(PythonForDelphi)モードは利用できませんが、

Python環境があれば(MS_Reader.exe がある場所に Python39-32 フォルダがある場合)・・・

マークシートの読み取りを高速化するP4D(PythonForDelphi)モードを利用する状態で、MS_Reader は起動します。

当たり前ですが、ダミー(中が空っぽ)の「Python39-32」フォルダを作成し、設定を偽ってMS_Readerを起動しても、メリットは何一つありません!

エラーが2つ出るだけです。

実際に、空の「 Python39-32 」フォルダを作成して実験してみました!


もう一つ。


こんなコトする方は皆無と思いますが。あくまでも、プログラムの動作検証として、ご参考まで。

【動作確認】

前回、設定したテンプレートを利用して動作確認します。

いったん、「P4Dを使用」のチェックを外して読み取りを実行します。前回試行した3列25行8選択肢の1枚あたり600マークあるシート3枚の読み取りにかかる時間は・・・


1枚0.805秒で読んでます(PC環境により、数値は当然異なります)が・・・

「P4Dを使用」のチェックを ON にして再び読み取りを実行します。私の PC での結果は・・・


1枚0.245秒強で読みました。

これが速度的に「はやい」か・どうか、このソフトウェアをお使いいただく方により、その判断基準は異なりますから、その思い(感じ方)は違って当然ですが、Python環境を利用しない場合に比較して、Python環境を組み込み、これを利用した場合は(PC環境により、その数値は悉く異なると思われますが)マークの読み取り速度は間違いなく高速化されるはずです(僕の環境では、「それがない」場合に比較して、「それがある」場合は3.3倍速で動作しました)。

ただ、Python環境を組み込んだ場合、プログラム全体の大きさは、12倍以上に巨大化します・・・

プログラムサイズを選ぶか、動作速度を優先するか、
ご使用目的、お使いのPC環境に合わせて選択していただけたら幸いです。

僕は・・・

今日の空みたいな・・・

プログラムを書きたかった・・・だけです *(^_^)*♪

僕が、この世から消えたあとも、動く。

いつか、夢みたとおりの・・・ プログラムを。

だいすきな・・・

大好きな Delphi と・・・

僕の Object Pascal で。

4.システムに Visual C++ランタイムライブラリがない場合は?

お使いのシステムに Visual C++ランタイムライブラリがインストールされていない場合は、MS_Reader 起動時に次のエラーが発生します。

『アプリケーションを正しく初期化できませんでした(0xc0150002)。「OK」をクリックしてアプリケーションを終了してください。』

英文の場合もあるようです。

PCの解像度の関係だと思うのですが、画像がボケていてごめんなさい!


このエラーが発生する原因を調べてみたところ、組み込みPython環境内にある「Python39.dll」が Visual C++ランタイムライブラリを必要とするようで、これがシステムにない場合は、プログラム起動時にバックグラウンドで行っているPython Engine の初期化に失敗して、上記のエラーメッセージが表示されることがわかりました。

お使いのPCで、Visual C++ ランタイム ライブラリのインストール状況を確認するには、[スタート] ボタンを右クリックし、「ファイル名を指定して実行」をクリックして、appwiz.cpl と入力して[Enter]を押します。Python環境を組み込んだ MS_Reader が動作する環境であれば、システムにインストールされている Microsoft Visual C++ ランタイム ライブラリが以下のように表示されるはずです。

現在、私のシステム(Windows 11 Pro 23H2)にインストールされているC++ランタイムライブラリの一覧。
もちろん、このシステムでPython環境を組み込んだマークシートリーダーが正常に動作しています。


システム内で起きていた別のエラーを解決するために、2023年12月上旬に工場出荷状態に戻すリカバリ作業を行いました。同時にOSを最新のバージョンに更新しました。それ以前のシステムの状態は次の通りです(OS のバージョンは 22H2)。※ 私のPCでの話です。

現在の状況とは異なっています。
この状態でもPython環境を組み込んだマークシートリーダーは正常に動作していました。


エラーを解決するには、Visual C++ランタイムライブラリをインストールすればいいわけですが、上の例のように Visual C++ ランタイムはたくさんあるので、手動でひとつひとつダウンロードしてインストールするより、Visual C++ ランタイムインストーラーを使って全ての Visual C++ ランタイムを一括インストールする方が簡単です。

システムをリカバリする前は、次のようにして Visual C++ ランタイムをインストールしていました。

【ご注意願います!】
ここで紹介する方法で Visual C++ ランタイムをインストールする場合、他のプログラムの実行環境との整合性は、一切保証できません。また、最悪の場合、Windowsが起動しなくなるトラブルが発生することも十分に考えられます。インストール作業の全てが自己責任であることを十分ご理解の上、重大な問題が発生した場合は元の環境に戻せるよう、システムのバックアップを取る・現在の設定をメモに記録する等、不具合の発生に備え、必要かつ十分な準備を整えた上で、Visual C++ ランタイムのインストールを行ってください。

以下のサイトから「Visual C++ v56.exe」をダウンロードしてインストール(私の環境にインストールする分には、なんの問題も起きませんでした。もちろん、マークシートリーダーも問題なく起動し、安定動作しました)。

Visual C++ Runtime Installer (All-In-One) v56

https://www.majorgeeks.com/files/details/visual_c_runtime_installer.html

こちらのWebサイトでも(次のリンク先Webページの下の方で)、このインストーラが紹介されています。

Microsoft Visual C ++ 再頒布可能ファイルを削除して再インストールする方法

https://www.autodesk.co.jp/support/technical/article/caas/sfdcarticles/sfdcarticles/JPN/How-to-remove-and-reinstall-Microsoft-Visual-C-Runtime-Libraries.html

インストーラーを立ち上げると、本当にインストールするかどうかを「YES」か「No」かで尋ねられるので、インストールする場合は「Y」をタイプします。その後はPCの画面に表示される英文の指示にしたがって操作してください。

ここから先は、上記のインストーラーを用いて Visual C++ ランタイムをインストールした際、私が実際に経験したトラブル?です(最終的にインストールは成功しました)。

お決まりのUAC起動後(PCの設定によっては)管理者ID 及びパスワードの入力が求められますが、これを入力すると、そのままPCがフリーズしたような状態になり、数分待機しても進展が見られないので、いったん作業を Ctrl+Alt+Delete でキャンセルし、再度、「Visual C++ v56.exe」を起動して Visual C++ ランタイムのインストール作業を実行、今度はトラブルなくインストールに成功する事例です。これは「ある特定のAD環境下にあるPCのすべてに共通して見られた」現象です。現在もその原因はわかりませんが、ご参考まで。

また、システムの状態によっては(現在システムにあるランタイムをアンインストールしているのか?)複数回(と言っても最高2回ですが)、再起動を求められることも(何度も)経験しました。

C++ランタイムライブラリのインストールについて、経験を加味して私がわかるのはここまでです(実は、何もわかってないのとイコールなのですが)。これ以外のエラーメッセージが表示されてインストーラーが起動しない場合も、もしかしたらあり得るかもしれません。大変恐縮ですが、そのような場合は原因の究明を含めて、自己責任でご対応ください。

5.Python Engine の初期化の問題?他

MS_Reader では、マーク読み取り時の体感速度を上げるため、FormCreate時にバックグラウンドで Python Engine の初期化を行っています。MS_Reader.exe のあるフォルダに小さなマークシートの画像とマーカー画像があるのにお気づきになった方がいらっしゃるかもしれません。これは Python Engine 初期化用に用意した画像です。

Python Engine 初期化用の画像をリソースに埋め込み、もし、それがない場合は再生して、
プログラム起動時に Python Engine の初期化が必ず行われるようにしています。


この初期化を「するか・しないか」で、MS_Reader 起動後、初めてマークを「読む」ボタンをクリックした際のプログラムの挙動がまるで違ったものになります。初期化を行った場合は、ごくスムーズにマーク読み取りが始まるのに対し、行わなかった場合は PC が一瞬フリーズしたような状態になり、その後、息を吹き返すかのようにマークの読み取りが始まります。

Python Engine の初期化コードです。

  AppDataDir:=ExtractFilePath(Application.ExeName)+'Python39-32';

  if DirectoryExists(AppDataDir) then
  begin
    //フォルダが存在したときの処理
    CheckPython.Enabled:=True;
    CheckPython.Checked:=True;
    PythonEngine1.AutoLoad:=True;
    PythonEngine1.IO:=PythonGUIInputOutput1;
    PythonEngine1.DllPath:=AppDataDir;
    PythonEngine1.SetPythonHome(PythonEngine1.DllPath);
    PythonEngine1.LoadDll;
    PythonDelphiVar1.Engine:=PythonEngine1;
    PythonDelphiVar1.VarName:=AnsiString('var1');
    PythonEngine1.Py_Initialize;
    //イニシャライズされたことを記憶
    P4D_ini:=True;
  end else begin
    CheckPython.Checked:=False;
    CheckPython.Enabled:=False;
    PythonEngine1.AutoLoad:=False;
    P4D_ini:=False;
  end;

(どこに問題があるのでしょうか?)

PC によっては、この Python Engine の初期化に非常に長い時間を要することがあるようです(エラーメッセージは出ません。この沈黙の時間が終わった後、プログラムは問題なく動作します)。偶然、ある PC でこの現象に巡り合い、あわてて時間を計ってみたところ、その PC では初期化に4分必要でした! なぜ、このような現象が発生するのか、その理由がわからないのですが、「そのようなことがある」ことだけは経験的に明らかですので、ここに書いておくことにしました。

また、マーク読み取り開始時に、マーカー画像の位置をテンプレートマッチングで確認して、それが「本当に見えている」ことをユーザーに明示的に知らせていますが、ここでもその処理に少し時間が必要なことがあります。私のPCでも、この現象は「起きたり・起きなかったり」するような気が・・・。エラーが出るわけでもなく、ただ・・・「ん?」みたいな時間があるだけなのですが・・・。こちらもその原因がよくわかりません。

以上が、現象としてはわかっているのですが、原因が解明できていないPython環境を使う上での問題点です。

それから、私の想定外の操作が行われた場合、メモリーリークが起きる可能性があります。Python環境をドッキングさせた当初は、このメモリーリークにかなり悩まされました。どう頑張っても小さなメモリーリークが発生するのを取り切れず、( Python環境はそういうもの? )と割り切ってしまおうかと思ったこともあったくらいです。

そのたびに思い直し、メモリーリークが発生する原因を突き止めて対応することを繰り返しました。なので、私が想定した操作範囲内でのメモリーリークは全て取り切れたと思います。が、もし、それが発生した場合は、その発生を知らせるメッセージがプログラム終了直後に表示されます( FormCreate時に実行されるコードの中にメモリーリークがあれば検出するコードを残してあります)。

  //メモリーリークがあれば検出
  ReportMemoryLeaksOnShutdown:=True;
メモリーリークが起きたことを伝えるメッセージ
(上の例のメモリーリークは故意に発生させたものです)

6.まとめ

(1)Python環境を利用するとマークシートリーダーは高速化できる。
(2)高速化できるかわりに、プログラム全体のサイズは大きくなる。
(3)原因不明のフリーズのような現象が発生することがある。

7.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容及びダウンロードしたプログラムを利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

マークシートリーダー

自分的に必要と思った機能は全部搭載しました・・・が、
プロが作った有償販売できるレベルのソフトウェアではありません。
見た目も、使い勝手も、よくないと思います。
もちろん、無料でお使いただけますが、サポートも、動作保証もありません。
ダウンロードから設定まで、ALL自己責任でお願いします。

快適と感じる速度で動作させるには、かなり高性能なCPU搭載のマシンが必要です。
私のプログラミング技術が足りない部分を、CPUパワーでカバーしてもらってます。
マシンによっては、読み取り結果のチェック等がかなりトロいかもですが・・・

それでも、もし、よかったら使ってください。
Delphiで作ったマークシートリーダーです。

【ご案内】追記(20240929)

当Blogで紹介してきた自作のデジタル採点プログラムを一つにまとめました。次のリンク先にその紹介とダウンロードリンクがあります。

当Blogで紹介したデジタル採点プログラムのすべてをまとめました!

【使い方のご案内】

1.デスクトップにMS_Reader.zipを展開(解凍)
2.高解像度ディスプレイへの対応
3.マークシート画像の読み取り準備
4.テンプレートを作成
5.マークの読み取りを実行
6.読み取り結果のチェック
7.CSVファイルへの書き出し
8.Excel Book の準備作業
9.Excel Book への書き出し
10.マークシート印刷用紙について
11.まとめ
12.お願いとお断り

どんな環境でも、100%動作する保証はできません・・・が、
私と同じ環境・条件を揃えていただければ、きっと動くと思います。

使用したPC及びOS、開発環境は、次の通りです。

・プロセッサ 11th Gen Intel(R) Core(TM) i7-1185G7 3.00 GHz
・実装 RAM 32.0 GB

・Windows 11 Pro 64ビット版
・バージョン 23H2
・OS ビルド 22631.2861

・Embarcadero® Delphi 12 バージョン 29.0.50491.5718

・設計時の画面解像度は「1366 × 768」です。これ以上の解像度でお使いください。

使い方をなるべく丁寧に説明しますので(マニュアルも同梱してありますが)、まず、ここに書かれている順番で、一通り操作してみていただけたら幸いです。

1.デスクトップにMS_Reader.zipを展開(解凍)

ダウンロードした MS_Reader.zip をお使いのPCのデスクトップにコピペして右クリックするとサブメニューが表示されます。この中の「すべて展開」をクリックしてください。

デスクトップに MS_Reader.zip を展開します。


無事、展開に成功したら、MS_Readerフォルダをダブルクリックして開きます。

フォルダ内に展開されたファイルの中に MS_Reader.exe があります。これをダブルクリックしてマークシートリーダーを起動します。


次のメッセージが表示された場合は、「詳細情報」(画像中、赤い枠で囲んで示した部分)をクリックします(プログラムの発行元が不明である場合に、Windows のDefender機能である SmartScreen がこの表示を出すそうです。自分の責任で実行すれば、次回からこのメッセージは表示されなくなります)。

「詳細情報」をクリックします。


すると、次の画面が表示されます。「実行」(画像中、赤い枠で囲んで示した部分)をクリックしてMS_Readerを起動してください。

「実行」をクリックします。

2.高解像度ディスプレイへの対応

高解像度ディスプレイをお使いの場合の対応方法です。高解像度ディスプレイをお使いの場合、設定から「システム」⇨「ディスプレイ」と順にクリックすると、次のように表示されると思います。

拡大縮小に「150~200」という値が設定されていれば、高解像度ディスプレイです。


この場合、起動したマークシートリーダーの画面が小さくて見えにくいと感じることがあるかもしれません。その場合は、次のように操作してください。

MS_Reader.exe を右クリックして、表示されるサブメニューのプロパティをクリックします。

MS_Reader.exe のプロパティを表示します。


「互換性」タブをクリックします。


高DPI設定の変更をクリックします。


「高いDPIスケールの動作を上書きします。」にチェックを入れて、「拡大縮小の実行元:」は「システム」をComboBoxの選択肢から選択して指定。OKボタンをクリックします。


続けて「適用」⇨「OK」とクリックして設定は終了です。これで画面が見やすい大きさで表示されるようになります。

3.マークシート画像の読み取り準備

デスクトップに展開した MS_Reader フォルダ内に「ScanData」フォルダがあります。この中に練習用のサンプル画像が2種類(解像度150dpiと200dpi)入っています。この画像を用いて説明します。

重要 マークシートは、解像度150~200dpiでスキャンしてください。

重要 1回の操作で読み取り可能な枚数は最大99枚です。

MS_Reader.exe をダブルクリックしてマークシートリーダーを起動したら、画面左上の「画像変換」をクリックし、表示されるメニューの「専用画像を作成」をクリックします。


画像変換用のWindowが表示されたら、画面右上の「選択」ボタンをクリックします。


「フォルダの選択」ダイアログが表示されます。ここでは「Scanner_A」フォルダを選択します。フォルダ名をクリックして、下のFolder欄に「Scanner_A」と表示されたことを確認し、「OK」をクリックします。

スキャンしたマークシート画像は「ScanData」内に適切な名前を付けたフォルダを作成し、必ずその中に保存してください!

重要 フォルダ名にハイフン(-)を使わないでください。

参考 フォルダ名には、文字の他、アンダースコア(_)が使用できます。

注意してください。選択するのは「フォルダ」で、「ファイル」ではありません。
(Scanner_Aをダブルクリックして開いても何も表示されません)


画面は、次のようになります。赤い枠で囲んだ部分にマークシート画像のサムネイルが表示されます。回転の必要性の有無と回転方向を確認してください。


この場合は、回転の必要性「有り」で、回転方向は左90°です。これを「画像の回転」のオプションボタンをクリックして指定します。

左90°のオプションボタンをクリックします。


必要であれば、次に画像のリサイズ指定を行います。リサイズを指定「する・しない」の判定基準は、スキャナーでマークシート画像をスキャンした際の解像度の数値で判断してください。

「Scanner_A」フォルダ内のマークシート画像は、ScanSnap iX1500 のノーマルモードでスキャンした画像で、その解像度は 150dpi です。この場合は、ちょうどよい大きさでマークシート画像が表示されますので、画像をリサイズする必要はありません。

重要 解像度150dpi ・A4横置きの場合、リサイズは必要ありません!

重要 解像度200dpi ・A4横置き・解答マーク欄4列の場合、80%の大きさにリサイズしてください。読み取り後のチェックまで含めて、作業しやすくなります。

マークシート画像の読み取り解像度が 200dpi でも、マークシートがA4横置き、解答マーク欄の列数が3列の場合、リサイズは必要ありません

また、A4以外の大きさのマークシートは使ったことがありません!
(用紙の左上にマーカー画像■■■を入れ、その他はここでダウンロードできるサンプルと同様に作成していただければ、用紙サイズに関係なく動作すると思いますが、試行したことがありませんので確かなことは言えません。ただ、画像のサイズが大きくなればなるほど、動作速度は間違いなく低下します。また、複合機のスキャナーを用いて、マークシートを画像化する際も、B4やA3の大きさだと私が使用している機材ではメモリがいっぱいになるのでしょうか? 30枚程度読み込んだあたりで一旦動作が停止します。数百枚単位での読み取りにはそれなりに時間がかかります。そのような理由から、マークシートに使う紙の大きさはA4サイズ以下が適切だと思います。)

参考:プログラムを書いた本人が言うのもナンですが、自動でのリサイズはおまけ程度にお考えください。
ScanDataフォルダのScanner_Bフォルダに保存されたサンプル画像の大きさは、2338 × 1653
これを自動リサイズオプションボタンを指定して、変換してみます。
ProcDataフォルダのScanner_Bフォルダに保存されたサンプル画像の大きさは、1760 × 1248
いちおう、これでマークシート画像が横方向のはみ出し「なし」で表示されました。

重要 画像のリサイズの有無を必ずメモ(記録)してください!

⇨ 複数クラスのマークシート読み取り時に、同じ設定を適用する必要があります。

重要 大きな解像度の画像を扱う場合、動作速度が大幅に低下します!

回転の有無と方向、リサイズの有無を指定したら、画面中央右にある「参照」ボタンをクリックして、保存先のフォルダを選択します。


「フォルダの選択」ダイアログが開きます。Pathを見ると「ProcData」フォルダが指定されていることがわかると思います。なお、Procは「Processed(加工済み)」という意味です。

プログラムは「ScanData」フォルダで指定したフォルダと同名のフォルダを「ProcData」フォルダに自動作成します。この自動作成されたフォルダをクリックして選択します(しつこいようですが、選択するのは「フォルダ」で、「ファイル」ではありません)。下のFolder欄に「Scanner_A」と表示されたことを確認し、「OK」をクリックします。

読み取り用のマークシート画像は、必ず「ProcData」内の自動作成されたフォルダに保存してください!

重要 ProcData以外のフォルダには画像を保存しないでください。

読み取り用画像を保存するフォルダは自動で作成されます!
(自動作成されたフォルダをクリックして選択してください)


「変換実行」をクリックします。

回転とリサイズの有無を指定して「変換実行」をクリック!


次に表示される案内メッセージには「いいえ」を選択してください。


このマークシートリーダーとは別に、手書き答案の採点プログラムを作成しました(準備が整い次第、公開する予定です)。このマークシートリーダーは、そちらと連動しての動作も可能な設計にしてあるため、このメッセージが表示されます。

画像の変換が完了すると、メッセージが表示されますので、OKをクリックします。


変換された読み取り専用画像のサムネイルが表示されます。作成された読み取り用の画像ファイルには連番の名前が自動的に付きます(自動生成されたファイル名は変更しないでください)。

重要 Python環境を利用する場合はファイル名は必ず連番にしてください。

画像処理のアルゴリズムは、GDI+を利用しています。画像の回転とリサイズが伴う場合は、変換に時間がかかります。処理が完了するまでお待ちください。

(後日、別途ご案内する予定の)手書き答案の採点プログラムと併用する場合は、採点やり直しのために必要な画像もここで作成します(Loopが二重にまわり、時間も2倍かかります)。

クラス別に処理する場合は、「画面の初期化」ボタンをクリックします。
変換元フォルダの選択から、画像の変換処理を再実行できます。

画像の変換処理が完了したら、「終了」ボタンをクリックして、この画面を閉じます。

参考:画像を変換する理由は以下の3つです!
(1)Jpeg画像のサイズを最適化するため(全体が画面内に収まるようリサイズしてください)。
(2)画像の名前が連番になるよう、自動的にリネームするため。
(3)証拠画像としてのオリジナルを残したまま、読み取りに最適な大きさの画像を生成するため。

4.テンプレートを作成

次に、マークシートの情報を記録した読み取り用のテンプレートを作成します。これを作成することにより、同じ採点を複数クラスに対して実行したり、設定(縮小処理の有無を含む)が同じマークシートを異なる考査での使いまわしが可能となる・・・

・・・ように設計したのですが、実際には使いまわしがなんとなく不安なので、考査毎にテンプレートを再生成して運用しています。ですので、同じ設定(大きさ)のマークシート画像の情報を記録したテンプレートの使いまわしが可能か・どうか、これについては未確認です。

「確実なマークシート読み取りを実行する」ためには、お手数をおかけしますが、試験ごとに使用したマークシートのテンプレートを作成していただくのが最良の方法であると思います。

メニューの「2 テンプレート」をクリックして表示されるサブメニューの「テンプレートの新規登録」をクリックしてください。別のWindowが開きます。


画面右上の「取得」ボタンをクリックします。


今度は「ファイルを選択」するダイアログが表示されます。任意のマークシート画像を選んでください(1番のファイルを選ぶ方が多いのではないでしょうか?)。ファイルをクリックしてファイル名を取得し、「開く」をクリックします。

マークシート画像のサムネイルをクリックするとファイル名が取得できます。


画面は次のようになります。

このプログラムでは、マーカー(特徴点)画像を利用してマークシートのマーク位置を計算しています。ですので、このプログラムで処理するマークシートには必ずマーカー(特徴点)画像が必要です。

重要 マークシート左上にマーカー画像(■■■)を必ず用意します。

重要 マーカー画像は、マークシート1枚に1つだけ用意します。

画面右の操作パネル上段にある「マーカー」オプションボタンをクリックして選択状態にします。

「選択対象」の「マーカー」オプションボタンをクリックしてください。


マークシートの画像が拡大表示され、マウスのカーソルが大きな「+」になります。

マーカー画像の「左上」をクリックし、ボタンを押したまま「右下」へドラッグしてください。画像上には点線のラバーバンドが表示されます。

マーカー画像の左上を左クリックして、マウスの左ボタンを押したまま、マーカー画像の右下へドラッグ。
点線のラバーバンドでマーカー画像が囲まれます。


ドラッグ中の画像です(わかりやすさのため、マーカー画像より大きめにドラッグしています)。

黒点線がラバーバンドを示します。


マーカー画像の座標を正しく取得できる例です。

マーカー画像とラバーバンドがぴったり重なるようドラッグしてください。


マウスの左ボタンから指を離すと、取得できたマーカー画像が画面右側に表示されます。

数値は、画像左上からの距離です。

マークの読み取り時、プログラムは、コンピュータの眼である「OpenCV」のテンプレートマッチングの機能を利用して、まず、最初にマークシート画像中にあるこのマーカー(特徴点)画像を探し出します(これはマークシート画像1枚1枚について必ず行います)。

次に、マーカー(特徴点)画像左上隅を原点(0,0)として、テンプレートに記録されたマーク欄の座標からマーク一つ一つの位置を割り出して、これを切り抜いて画像化(正確に言うと、マークの切り抜き処理前に、ボカシ・二値化・白黒反転の各処理を行い、マークの切り抜き後に白面積計算処理を行って)、マークの有無を判定しています。

この方式の利点は、印刷そのものが左右にズレでも、マーカー画像と解答欄の相対的な位置関係は一定で変わりませんから、印刷がズレすぎて解答欄が印刷されなかった場合以外は、必ずマークの位置を探し出せる(=マークの有無を判定できる)ことです。

事実、輪転機で印刷(非推奨ですが!)して、チェックから漏れた(チェックしなかった?)、正しい位置から印刷が5cmくらいズレたマークシートも、このプログラムでなんの問題もなく読み取れました・・・。印刷のズレを申告せず、そのまま解答して提出する受験者も受験者ですが・・・。A4横・4列のシートで、解答には3列めまでしか使わなかったから「4列めはなくてもOK! 大丈夫」と思ったのでしょうか? それともただ単にめんどくさかったのでしょうか? たぶん、後者だと思いますが・・・

次は、そのテンプレートマッチングの機能をテストします。画面右にある「マーカー画像の読み取りテスト」ボタンをクリックしてください。テンプレートマッチングが正しく実行されると、マーカー(特徴点)画像が太い赤枠で囲まれます。


表示されるメッセージをお読みいただき、「OK」をクリックしてメッセージを閉じてください。


結果が良好であれば「選択対象」グループの「解答欄」をクリックします。


次に、マークシートのマーク(解答)欄の「行数」と「列数」及び「選択肢の数」を指定します。


マークシートの列数・行数・選択肢数の数え方は次の通りです(Scanner_Aフォルダにあるマークシート画像は、A4横置き・3列・25行・8選択肢の形式です)。


ですので、これを次のように設定します。


ComboBox に正しく設定を入力したら、その下の「採点方法の設定」の座標「1列」のオプションボタンをクリックして選択状態にします。マウスのカーソルが大きな「+」になります。


第1列目のマーク(解答)欄の座標を取得します。マーカー(特徴点)画像の時と同様、第1列の枠のうち、設問番号欄の矩形を除いた選択肢のマークが印刷されている欄の矩形の左上隅を(左)クリックして、そのままボタンを離さずに、枠の右下隅へドラッグします。この作業は正確に、慎重に行ってください。この作業の良し悪しでマークの読み取りの可否が決まります。

極めて重要 設問番号欄を含めて指定してはいけません!

極めて重要 指定するのはマーク欄のみ!

プログラムは、ここで取得した座標値(矩形の高さ)を行数で割り算して列を設問毎1行ずつに切り出し、さらに切り出した1行を選択肢数で割って1つ1つのマークを切り出し、その塗りつぶし面積を計算して、マークの有無を判定しています。

マーク(解答)欄の枠線と、表示されるラバーバンドがぴったり重なるようにドラッグしてください。

※ 下図は2つともドラッグ直後の結果を示しています(〇はドラッグ開始点と終了点です)。

マーク欄第1列めの左上隅を(左)クリックしてそのまま指を離さずに右下隅へドラッグ


ドラッグ中は、黒点線のラバーバンドが表示されます。これを目安に位置決めを行ってください、

画像中の〇印の位置までドラッグします。


指を離すと、ドラッグした範囲が赤い矩形で囲まれます。画面右側に取得できた座標が表示されます。


「再範囲選択」ボタンをクリックして、座標の取得をやり直すこともできます。


1列目が済んだら、同様にして2列目の座標を取得します。この作業を「マークシートの列数」分だけ繰り返します。

すべての列の座標を取得できたら、「保存」ボタンをクリックして取得した座標を保存します。


「保存」処理が完了すると、次のメッセージが表示されます。

参考:テンプレートの名前について
例 N_R25C03S08
N:ノーマル(通常の大きさ:解像度150~200dpi)
R:Row(行数)は25行
C:Col(列数)は3列
S:Selection(選択)は8個


「二値化テストの実行」ボタンをクリックすると、第1列めを「平滑化(ぼかし)処理&白黒反転して二値化」した画像の状態が確認できます。「マークあり」の部分が白く表示されていればOKです!
(プログラムは、この白部分の面積を計算して、マークの有無を判定しています)

「終了」ボタンをクリックして画面を閉じ、マーク(解答)欄座標の取得作業を終了します。

二値化テストを実行した場合は、終了ボタンをクリックする前に、保存ボタンをクリックすることを忘れないでください!

二値化の閾値と平滑化(ぼかし処理)のパラメータは、まずデフォルト設定でお試しください。

5.マークの読み取りを実行

これでマークを読む準備ができました。メニューの「テンプレート」をクリックし、表示されるサブメニューの「テンプレートの選択」をクリックします。


次のように、テンプレートを選択するWindowが表示されます。マークシートの形式に合ったテンプレートをクリックして選択し、決定をクリックします。

シングル/ダブルとあるのは、数学や教科「情報」のテストで、マークシート2枚1セットの採点を行うための設定です。選択肢数が16のマークシートを選ぶと、この設定も選択できるようになります(選択肢数が16未満のマークシートでは、この設定は利用できません)。

数学及び教科「情報」用の設定は、後日別記事として掲載する予定です。


次のメッセージが表示されます。「はい」をクリックしてください。


マークの読み取りを実行したいマークシート画像のあるフォルダを選択し、「Ok」ボタンをクリックしてください。


保存してあるマーカー(特徴点)画像をもとに、自動的にテンプレートマッチングが行われ、見つかったマーカー(特徴点)画像から、マークシートのマーク(解答)欄第1列第1行目の座標が計算され、それぞれが赤い矩形で囲まれて表示されることを確認してください。

Python環境を利用する場合(ここでワンクッション置くような感じで)テンプレートマッチングにしばらく時間がかかることがあります。同じプログラムを走らせているのですが、PCにより、このフリーズしたような時間の長さが極端に違うようです・・・、その辺の理由が私にはさっぱりわかりませんが・・・。

Python環境利用時に、この画面が表示されるまで、フリーズしたようになることがあります!


ここまでの設定操作が順調に進行していれば(抜け・落ち・欠けがなければ)、間違いなくテンプレートマッチングが成功し、マーカーと1列1行目が赤い矩形で囲まれるはずです。次のメッセージが表示されますので、お読みになったら「OK」ボタンをクリックしてください。


「読む」ボタンをクリックすると、マークシートの読み取りがスタートします。


画面下部の StringGrid に読み取り結果がリアルタイムで表示されます。また、読み取り完了後、処理にかかった時間が画面左下に表示されます。


8選択肢・25行・3列だから、合計600マーク ×3枚=1800マークの読み取りで、早ければ2013ミリ秒、遅くて2467ミリ秒で読んでます(PCの性能により、この値は変わります)。


遅かった方で1マークあたりの読み取り時間を計算すると、

2.467秒 ÷3≒ 0.82秒/枚
0.82 ÷ 600 ≒ 1.4ミリ秒/1マーク

そう書くと、すごく早いような気がしますが・・・

600マーク3枚で2.5秒だから、30枚ならその10倍で25秒かかります。平均的な高校の1学年分の生徒数を1学年8クラス320名とすると、さらに10倍で280秒程度、約5分処理時間が必要です。

300名分、5分だと慣れてくるとちょっと遅く感じてしまうかな? みたいな気が・・・

このプログラムには、内部的にPython環境を組み込んで高速動作させるモードがあります。数学用途の16選択肢・25行・3列で1200マーク/枚のマークシートで処理速度を計算・比較してみます。
(組み込みPythonの利用方法は後日ご案内します)

まず、Python環境を利用しない場合、1200マーク×40枚=1クラス分の48000マークを読むのにかかった時間が・・・


約78秒です。2枚1セットのダブルモードならその倍になります。
1枚(1200マーク)読むのに1.95秒かかってます。

次に、Python環境を利用した場合です。同じ読み取り条件で実験すると・・・


約11.5秒。8クラスあっても2分かかりません。ダブルモードでも4分未満。
1枚0.3秒未満で読み取ってます。

何やってもダメな自分にしては、よく頑張ったって正直、思います・・・。
よほど、びみょーなマークでない限り、期待した通り、ほぼ正しく読み取ってるし・・・。
かあさん、オレ、がんばったよ☆☆☆

まぁ このプログラム作成そのものに50万枚くらい採点できる時間をかけてますから・・・

それと合算すれば、
たぶん、プラマイ0ですー!!

6.読み取り結果のチェック

マークシートリーダーで最も重要な部分は、マーク読み取りの正確さであることは言うまでもありませんが、読み取り結果のチェック機能も非常に重要であると考えます。

人によってマークの濃さや大きさは少しずつ異なり、また、マークを訂正した箇所に残る消し跡も判定に少なからぬ影響を及ぼします。常に100%正しい読み取り結果が保証されないのが現実ですから、如何に効率よく、読み取り結果をチェックできるかで、プログラムの使用感はずいぶん変わってくると思います(CPUパワーにかなり依存したプログラムを書いておいて、そう言うのもナンですが・・・)。

自分自身の書いたものがベストだなんて、到底、思えませんが、このプログラムを書くにあたり、マークの読み取り部分と同等か、それ以上に頑張って書いたのが、この読み取り結果のチェック部分です。

機械との協働。機械との融和。これをテーマに、ヒトと機械とが一体化しての「快適なチェック作業」の実現を目指しました。

・・・が、プログラミング技術の未熟。自分自身の勉強不足。見い出した妥協点。等々の理由により、視覚による機械と協働してのチェックも、聴覚(音声出力)による機械と協働してのチェックも、いずれも全面的にマシンのCPUパワーに依存した、もっさりした感のある処理となってしまいました・・・。

処理性能の高いマシンなら、それなりに快適に作業できると思うのですが。以下、チェック機能の使い方です。

マーク読み取り結果のチェックを実行。


上の図の左のGUIから説明します。

白紙にチェックすると、マークがひとつもないシートのチェックは行わない(飛ばす)設定で動作します。この機能はデフォルトでON(チェックあり)です。

マーク(解答)がなかった場合の読み取り結果の表示が「999」です(デフォルトOFFです。このプログラムでは、「空欄」のフラグを「999」としています。マークの番号にも、得点にも「999」は通常ないことがその理由です。ちなみに複数マークは「99」と表示しています。色は「999」が「青」、「99」が「赤」です。少しでも視覚に訴えた方がチェックしやすいと考えました)。

ごく薄い色でマークされた答案が混じっていないことが大前提ですが、答案全体(1クラス分!)のマークの濃さが十分「濃い」と保証されていれば、チェック開始時のみ「999」のチェックを外してチェック(機械がきちんと空欄を識別していることをヒトが目視して確認)、で、確実に空欄を見分けていることが確認できたら、「999」にチェックして続行。こうすれば大変スムーズな確認作業を実現することができます。あくまでもごく薄くマークされたシートがないことが大前提ですが・・・

いずれにしても「Check!」ボタンをクリックすると、プログラムは次の「空欄(999)」もしくは「複数マーク(99)」を探し、それが見つかった場合は該当箇所を赤い矩形で囲んで表示します。処理性能の高い(CPUパワーのある)マシンであれば、それなりに快適に動作しますが、そうでない場合は、かなり「もっさり」した動作になりますので、イライラするかも知れません。ごめんなさい。

【空欄と判定した場合】

マークがない場合の表示例(設問番号25が空欄であった場合)

【複数マークありと判定した場合】

複数マークと判定した場合の表示例(設問番号43が複数マークであると判定)


複数マークの判定はパラメータ設定を厳し目にしてあります(上の図はそれがわかるよう、大きめに表示しました)。ごく小さなシミは「平滑化(ぼかし)」処理である程度消えますが、ある程度の面積があるシミや汚れは上のように複数マークと判定されます。

いずれの場合も、ヒトの眼で確認して、訂正の必要がなければ「Check!」ボタンをクリックしてチェックを続行。読み取り結果の訂正が必要な場合は、正しい値を直接入力します(上の場合であれば「2」と入力してください)。


【処理をスキップして次のシートへ】

「Skip」ボタンをクリックすると、現在チェックしているシートの残りの部分のチェックを省略し、次のシートのチェックへ移動します。チェック対象シートの残りの行が全部空欄であった場合などに利用してください。


【チェックの再実行】

「ReDo」をチェックすると、初めからチェックを再実行できます。

ReDoにチェックすると表示されるメッセージ①
ReDoにチェックすると表示されるメッセージ②


【音声読み上げ】

読み取り結果が表示されているStringGridの任意の行をクリックして、「▶」ボタンを押すとWindowsに標準搭載されている日本語の音声合成エンジン(Microsoft Haruka Desktop)の音声で読み取り結果をアナウンスしてくれます。

マークの読み取りが正しく行われているか・どうか、少しでもラクに確認できないかと考え、この機能を搭載しました。処理性能の高いマシンでないと快適な動作は期待できませんが、CPUパワーのあるマシンであればそれなりに使えると思います。

「▶」ボタンの下にある「×」ボタンをクリックすると、音声読み上げを途中で中止することができます。


【列を指定して、任意の行からその列の最後の行までのチェックをスキップ】

数学用のマークシート等で、第1問の解答をシート第1列にマーク、第2問の解答をシート第2列にマーク、第3問の解答を・・・というような設定にしたい場合、「指定列の任意の行から最後の行までをチェックの対象から外す」ことができます。以下、その方法です。

任意の行を指定して、その行以降のチェックをスキップできます。


図のいちばん左にある「Skip」にチェックすると、この機能が有効になり、続けて「Check!」ボタンをクリックすると、ここでの設定に基づいたチェックを実行できます。

上の例であれば、1列目25設問あるうちの20設問目以降25設問目までのチェックをスキップ(チェックは19設問まで実行)、2列目は設問番号26から始まるので34設問目以降50設問目までを、3列目は設問番号51から始まるので70設問目以降75設問目までのチェックをそれぞれスキップします。スキップの設定はComboBoxへ入力した指定値「以降」であることにご注意ください。

また、シートの型式により、列の指定の可否をプログラムが自動的に判断し、ComboBoxのEnabled プロパティが設定されます(上の例では4列目は指定不可)。

「覚」ボタンをクリックすると、現在の設定を ini ファイルに書き込んで記憶します。「消」ボタンをクリックすると「設定なし」の状態に初期化できます。

数学用途等で2枚1セットの処理を実行する場合は、1枚目と2枚目を分けてスキップ処理の設定を行うことができます(数学用途の処理方法は後日掲載します)。

7.CSVファイルへの書き出し

マークの有無の読み取り結果は、CSVファイルとExcel Book への書き出しが可能です。

【CSVファイルへの書き出し】


「ファイルへの出力」にある「CSV」をクリックして選択し、「書き出し」ボタンをクリックしてください。


上記の場所にCSV形式で、読み取り結果が出力されます。

フィールド名として1行目に「設問番号」、レコード名としてA列に「マークシート番号」が書き込まれます。

8.Excel Book の準備作業

【Excel Bookへの書き出し準備】

Excel Book への読み取り結果の書き出しは、自分用に(あれば便利かなー☆)と思って作成したものです。ですので、式の入ったセルを保護する等、第三者が使うことへの配慮は何一つ行っていません。セルに入力された式やVBAの内容をご自身でメンテナンスできる方なら、お使いいだけるかな? という程度のシロモノです。

添付した Excel Book はこれまでに何度も「実際に使用して動作に誤りがないことを確認済み」ですが、誤って式を削除したりした場合は(当然ですが)意図した通りに動作しません。ですので、こちらも動作保証は一切ありません。ご使用はあくまでも自己責任でお願いします。この Excel Book に対しても、このプログラムの使用要件にあります免責事項がそのまま適用されますことを申し添えます。

以下、試験実施前に行っておくとよい採点準備作業です。

eFile フォルダに「一般用マークと手書き併用採点シート.xltm」というマクロ有効テンプレートがあります。これをダブルクリックすると「一般用マークと手書き併用採点シート1.xlsx」という名前で新しい Excel Book が作られます。拡張子に注意してください。「.xlsx」です。このままでは期待通りに動作しませんので、適切な名前を付け、拡張子を「.xlsm」(マクロが有効な Excel Book )に変更して eFile フォルダ(必ずこのフォルダに保存してください!)に保存します。

ここでは test.xlsm という名前で保存したことにして説明を続けます。

「コンテンツの有効化」をクリックしてマクロが実行できるようにしてください。


【インターネットからダウンロードしたマクロ有効 Excel Book の取り扱い】

いつからこうなったのか、わかりませんが、インターネットからダウンロードした拡張子 xlsm の Excel Book をダブルクリックして開くと、次のメッセージが表示されるようになりました。

「編集を有効にする」をクリックすると・・・
マクロを動かすことができません!


こうなった時は、いったん Book を閉じて、その Excel ファイルを右クリックして表示されるサブメニューのプロパティをクリックして、全般タブのいちばん下にある「セキュリティ:」の「許可する」にチェックします(チェックする=マクロの実行をご自身の責任で行うことになります。どうか、ご注意ください)。

全般タブの下の方にあるセキュリティの設定。
マクロの実行をご自身の責任で行う場合は、「許可する」にチェックしてください。


「許可する」にチェックした状態で、「適用」をクリックすると「セキュリティ」の表示そのものが消えます。「あなたの責任でマクロの実行が可能になりました」ということなのでしょう。「OK」をクリックしてプロパティの設定画面を閉じます。


これでマクロが実行できるようになります。


【欠席者がいた場合】

Excel Book を利用して採点する場合、大変重要な注意事項があります。それは欠席者がいた場合の処理です。該当試験に欠席者がいる場合は、その欠席者の出席番号位置に未使用のマークシートを挿入し、シートが確実に出席番号順に並んでいることを確認してから、スキャナーでスキャンしてください。
※ 可能であれば、この用途専用に未使用のマークシートを複数枚、最初から手元に準備しておくとよいと思います。

重要 未使用のマークシートを欠席者の出席番号位置に挿入しておく!

これを忘れると、あとから「すーぱーめんどくさい」コトになります(もし、忘れたらマークシートのスキャンからもう一度、採点をやり直した方が効率がいいかもしれません)。


【受験者の氏名データを準備する】

test.xlsm をダブルクリックして開き、「コンテンツの有効化」を行ったら、いちばん最初に「名票への貼付元名票」シートをクリックして開き、ここに「採点対象者全員分の氏名」を準備してください。

もっとわかりやすく言うと、採点したいテストを受験した生徒全員の「クラス・出席番号・氏名・ふりがな・性別」データを「クラスごと」に「出席番号順」で、「名票への貼付元名票」シートに用意します。なんで「ふりがな」まで必要なのか? 疑問に思う方もいらっしゃるかもしれませんが、最近の若い方々のお名前は難読である場合が多く、採点結果を個票でお知らせする際に、個票の氏名欄のところに「ふりがな」も印刷しておくとスムーズに答案返却が行えます。そのための「ふりがな」準備です。

また、テストの受験者全員分の氏名データを1シートに準備する理由は、次のような使い方を想定しているからです。

(1)同じテストを受験 ⇨ クラス毎に採点用 Excel Book を用意するのは非効率的。
(2)採点用 Excel Book は1個だけ作成し、これをコピーして全クラス分を作る。

具体的には、eFile フォルダの Excel Book(test.xlsm)をコピーして、クラス別(AHR.xlsm)に名前を変えて MS_Reader.exe がある場所に保存。採点結果もコピーした Excel Book(AHR.xlsm) に書き込みます。さらに、この作業はすべてプログラムから自動実行します。

採点者は、採点結果が書き込まれた Excel Book(AHR.xlsm)を開いて、「名票への貼付元名票」シートに用意した氏名データから「A組の受験者の氏名データ(クラス・出席番号・氏名・ふりがな・性別)を範囲選択してコピーし、「名票」シートに値のみ貼り付けます。

こうすることで同じ内容のファイルを複数個準備することなく、言わば「採点原本」として利用する Excel Book を1つ作成するだけで、試験を実施した全クラス分の採点が可能となります。

ここでは「クラス」と表現しましたが、用意する氏名データを適宜変更すれば「講座」等の採点もまったく同じように行えます。※ プログラムの仕様としては、1回の採点作業で採点する人数を100名以下と想定していますが、実際の採点作業は1採点40名程度で行っています。ですので、40名程度を1つのまとまりとして採点していただく方向でお考えください。


【正解を入力】

氏名データの準備が完了したら、「正解」シートをクリックして表示し、設問毎に「正解」の選択肢の番号を入力します。設問がない場合(無解答でよい設問番号の欄)は空欄のままにしておきます。入力したら、入力内容に間違いがないか、よく確認し、上書き保存してください。

正解の入力を間違えるとたいへんなコトになります!
慎重に入力し、最低2回は間違いがないことを確認してください。


【配点を入力】

次に、「マークシート配点」シートをクリックして「配点」を入力します。入力と同時に合計が自動的に計算されます。入力が完了したら上書き保存してください。なお、この配点表の下には観点別評価の表もありますが、この表には一切入力しないでください(観点別評価の表は入力禁止です)。

配点を入力すると「合計」が自動計算されます。
確認作業にお役立てください。


【観点別評価の区分を入力】

次に、「マーク&手書き観点別評価」シートをクリックして「観点別評価の区分」を入力します。
「知識・技能 ⇨ 1」、「思考・判断・表現 ⇨ 2」として設問毎に、半角数字で入力してください。デフォルト設定では、すべての設問に「1」が入っています。解答を要しない設問は「空欄」にしてください。入力したら上書き保存します。


以上で、試験実施前の準備は終了です!

9.Excel Book への書き出し

重要 すべての Excel Book を閉じてから実行してください!

危険 Excelが起動した状態で実行すると重大なエラーが発生します!

Excel へデータを書き込む際は、上記注意事項を必ずお守りください。この注意を忘れて Excel が起動したまま、Excel Book への書き込みを実行すると最悪の場合、Excel のプロセスが幽霊のように残り、これを終了することが出来なくなって、復旧するには、システムの再起動しかない状態になります。未保存の重要なデータがあるような場合、当然そのデータは失われます。Excel Book へのデータ書き込み時は、Excel が起動していないことを(タスクバーに眠っている Excel Book がないことも含めて)十分確認した上で、書き込み作業を行ってください。


【書き出し処理】

マークシートを読み取り後、読み取り結果のチェックまで完了したら、Excel Book への読み取り結果の書き出しが可能となります。次のようにマークシートリーダーを操作してください。

最初に、ファイルへ出力の Excel のオプションボタンをクリックして選択します。すると、その右側にある「選択」ボタンがクリックできるようになりますから、このボタンをクリックしてください。


ファイル選択のダイアログが表示されますので、読み取り結果を書き込む Excel Book をクリックして選択し、その後、下にある「開く」ボタンをクリックします。Pathの指定は、デフォルトで eFile フォルダになっています。準備作業で作成した test.xlsm を eFile フォルダに保存したのは、この読み取り結果を書き込む Excel Book を選択する作業を円滑に実行するためです。


次のメッセージが表示されます。

重要 ここで Excel が起動していないことを必ず確認してください!

選択した Excel Book が書き込み先として表示されていることを確認し、「書き出し」ボタンをクリックします。


書き込みには、しばらく時間がかかります。次のメッセージが表示されるまでお待ちください。


すぐに書き込み結果を確認する場合は、「はい」をクリックします(ここでは「はい」をクリックしたものとして説明を続けます)。

「はい」をクリックした場合は、エクスプローラーが自動的に開きます。先ほど選択した「test.xlsm」のコピーが「Scanner_A.xlsm」として、eFile フォルダではなく、MS_Reader.exe のあるフォルダに生成されています。


ファイル名がなぜ「Scanner_A.xlsm」になったかというと、マークシートの読み取り元フォルダとして選択したのが、ProcData\Scanner_A であったためです。プログラムは、マークシートの読み取り元フォルダの名称をそのまま、原本「test.xlsm」をコピーして生成する読み取り結果書き込み先 Excel Book の名称として利用します。

マークシートの読み取り元フォルダの名称が、Excel Book の名称になります!


マークシートの読み取り元フォルダの名称が「R05_情報Ⅰ_1A」であれば、MS_Reader.exe のあるフォルダに「R05_情報Ⅰ_1A.xlsm」が生成されます。

ここは、この仕様に慣れるまで混乱が生じやすいところと思われます。しかし、この仕様(仕組み)を十分に理解して、マークシートリーダーを使いこなしている職場の同僚からは「よく考えられた採点システムだと思います」と言ってもらえました。うれしかったなー!!


【成績一覧表の印刷】

生成された Excel Book をダブルクリックして起動します。起動したら「名票への貼付元名票」タブをクリックして開き、採点対象クラス(等)の氏名データを範囲選択してコピーし、「名票」タブをクリックして B3 セルに値のみ貼り付けます。次に「採点」タブをクリックしてください。次のような画面が表示されます。「氏名がある場合のみチェックする」ボタンをクリックしてください。画面上方に表示されている平均点が正しく再計算されます。なお、欠席者の得点は「0」と表示されていますので、この場合は手動で「受験確認」のチェックを外し、平均点の計算対象から除外してください。

「氏名がある場合のみチェックする」をクリックしてください。


このシートは通常の印刷操作で印刷できます。ただし、デフォルト設定で100名分を2枚に分けて印刷する仕様となっているため、成績一覧表が1枚でよい場合は、次のように指定して1ページ分のみ印刷を実行してください。

成績一覧表を1枚だけ印刷したい場合の設定


【観点別評価を行う場合】

観点別評価を行う場合は、「正答率」タブをクリックして、上と同様の操作を行ってください。欠席者がいた場合の処理も同じです(このシートは印刷しません)。


【個票の印刷】

最後に、試験の採点結果を受験者に知らせる成績個票を印刷します。よー書いた。さすがに私も疲れました。あと、もぉちょっとです!

「個人表」タブをクリックします。次のような画面が表示されます(表示倍率は異なります)。まず、考査名と科目名を入力してください(忘れやすい部分です! ご注意願います)。印刷はVBAでマクロを組んであります。設問数に合った「印刷(QXX)」ボタンをクリックしてください。

重要 セルを保護していません。誤って式を消さないでください!


次の印刷フォームが表示されます。開始番号と終了番号を入力し、「印刷実行」をクリックします。

重要 印刷は途中で中止できません!

VBAではプログラム書いてない!のに、Engterキー押し下げでフォーカスが移動します・・・

この印刷は Excel の仕様上、印刷データをためてからイッキにプリンタへ送信という方法が取れません。1枚ずつ送信しますので、ちょっとギクシャクした感じで印刷が実行されます。プリンタが壊れているわけではありません。


【個票を個別に確認したい場合は?】

受験者個々の個票を確認したい場合は、A2 セルに「採点」シートの通番を入力します。いろいろなクラスの生徒が混在した講座の処理に対応するため、入力値は「出席番号とは異なる」ことにご注意願います。

採点シートの通番を入力します!


個票を確認したい受験者の通番は「採点」シートを表示して確認してください。

受験者の通番を確認します。


【壊しちゃったときは?】

個人表シートを壊してしまった時は、次のようにすれば直せます。「個人票_Back」タブをクリックします(このシートは絶対に非表示にしないでください)。A 列の左、1行めの上(図の〇印を付けた部分)を右クリックしてシート全体を選択し、表示されるサブメニューのコピーをクリックします。

A 列の左・1行めの上を右クリック!
シート全部をコピーします。


個人表シートに戻って、先ほどと同じ A 列の左・1行目の上を右クリックして表示されるサブメニューの「数式fx」をクリックします(罫線データ等を壊してしまった場合は、すべてを貼り付けます)。

数式が壊れた場合は数式を貼り付けます。
面倒な場合は、いちばん左の全部「貼り付け」でもOK!

10.マークシート印刷用紙について

紙の「白さ」の度合いを「白色度」というそうです。このマークシートリーダーで読み取りに使用したマークシートはすべて「再生紙 or 再生コピー用紙」と呼ばれる紙に印刷したものです。

ですから、ここで紹介したマークシートの読み取り結果は、すべて「白色度70%」前後の「再生紙」に印刷してのもので、ホームセンターで一般的に販売されているような「白色度」が「再生紙」よりはるかに高い「真っ白に見える」用紙を用いての読み取り結果ではないことに、十分ご注意願います。

マークシートの印刷に使用する紙の「白色度」によっては、読み取りパラメータ設定の見直しが必要になるかもしれません(私自身は、実験・試行していませんので正確なことはわかりませんが)。入手可能なすべての紙について、実験することは現実的に無理でありますので、マークシートを印刷する用紙については、本ソフトウェア使用者の責任で十分な試行を行い、確実に動作するパラメータ設定を行った上で、このプログラムをお使いいただけますよう、お願いいたします。

印刷はインクジェットプリンタで行うことを推奨しましたが、長期にわたって使用していない(メンテナンスもしていない)インクジェットプリンタ(複合機)では、インクの吸い込みに問題が生じ、「いくら調整しても・何度クリーニングを行っても」期待した濃度での印刷ができないということも経験しました。サービスマンの方に伺ったところ、「こういう状態になると通常のクリーニングではなかなか復旧しない」と教えていただき、あらためて日常的に使用してインクを動かすことと、不具合が見えたらすぐにメンテナンスをお願いすることの大切さに気づいたこともあります。

そのサービスマンの方からは、マークシートに付着していた消しゴムの「屑」がスキャナーのローラー等可動部の動きを悪くして、マークシートがやや斜めにスキャンされたりする原因となり得ることも教えていただきました。実際に大量のマークシートを読み取ってきた複合機のスキャナー部分からは、かなりの量の消しゴム屑が・・・。受験者には消しゴム屑をよーく落としてから答案(マークシート)を提出するよう注意しておく必要があります。まさに塵も積もればなんとやら・・・です。

また、ご使用のスキャナーの読み取り設定によっては(デフォルトの読み取り設定が)0~255段階のグレースケールでなく、カラーであったり、ある閾値で白黒二値化しての読み取りであったりという、私の想定外の設定であることも、当然のようにあり得ると思います。それがカラー画像であった場合の影響はほとんどないと思われますが、ある閾値での白黒二値化画像であった場合は、判定に重大な影響を及ぼす可能性があります。ですので、マークシートの読み取りに、使用されるスキャナーの読み取り設定に関して、予め、使用者様の責任で十分ご確認いただけますよう、併せてお願い申し上げます。

11.まとめ

このマークシートリーダーで出来ること、出来ないことをまとめました。

【出来ること】

・マークシートのJpeg画像を回転&適切なサイズに縮小
・マークシート画像のマーク読み取り(1設問当たり最大16選択肢まで対応)
・読み取り結果の確認(GUI & 音声出力)
・読み取り結果のCSVファイル出力
・読み取り結果を採点結果通知用Excel Bookへ出力(新教育課程に対応)
・共通テスト形式の数学試験に対応(選択肢:-、±、0-9、記号:a~d)※ 後日掲載します。
・共通テスト形式の情報Ⅰ試験に対応(選択肢:0始まりの設定も可能)※ 後日掲載します。
・使用環境に合わせて各種パラメータ設定を変更可能
 ⇨ ScanSnap iX1500のノーマルモード(解像度150dpi相当?)、もしくはEPSON PX-M7110F(解像度200dpi)でスキャンしたJpeg画像のマーク読み取りに最適化した値をデフォルト値に設定済み。

【出来ないこと】

・1設問について、複数の解答が設定された採点
・前問の解答内容に応じて、次の問いの解答が変わる採点
・その他、答案1枚のみの採点等、このプログラムで想定外の採点全て
・1回の読み取り操作で処理できるJpeg画像は99枚までで、100枚を超える枚数は処理できません。

【その他の使用方法】

MS_Reader.exe の「ヘルプ」にある「PDFを表示」をクリックすると利用方法の手引きがお使いのPDFリーダーで表示されます。マークシートの作り方等、このブログの記事にないことも書いてありますので、必要に応じてこちらも併せてご参照いただけますよう、お願いいたします。

12.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容及びダウンロードしたプログラムを利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

ini ファイルの Section の有無を確認

レジストリを汚したくない気持ちと、ini ファイルの手軽さから、僕はパラメータの設定を、いつも ini ファイルに記録してしまう。

今回、あるプログラムを見直していて、これまで ini ファイル内のセクション自体の存在を「確認」するようなプログラムを書いた経験がないことに気がついた。

ini ファイル内に「XXX」という名前のセクションが、あるか・ないか?
これは、それを確認する方法の覚え書き。

【もくじ】

1.もし、セクションがなかったら?
2.セクションの存在を確認
3.まとめ
4.お願いとお断り

1.もし、セクションがなかったら?

この秋から、マークシートリーダーのプログラムを見直す作業を、ヒマを見つけては行ってきた。
なぜ、そんな作業をすることにしたかというと、読み取り速度に目をつぶってもいいから、ファイル容量的に軽い(=小さな)マークシートリーダーも用意したくなったというのがその理由。

そもそも、自作のマークシートリーダーを作ろうと思い立ったのが、2020年の2月。最初はDelphi用のOpenCVライブラリを利用。読み取り速度より何より、「読めるかどうか」それすらわからない中でのチャレンジ。読み取り性能に重きを置いたのは言うまでもない。結果、読み取り性能的には十分満足できる、夢見た通りのプログラムを完成させることができた。その動作速度は(PCの性能にも左右されるが、My環境では)1枚あたり1200マークあるシートを、1枚あたり約1.2秒で処理。これだと40名分のマークシート読み取りに48秒、200名分ならば6分。

MyPCのスペック・・・プロセッサ:11th Gen Intel(R) Core(TM) i7-1185G7 3.00GHz / 実装RAM 32GB

その後、がむしゃらに高速化を目指した時期があって、その学びの中で、僕は embeddable Python の存在を知った。これにPython用のOpenCVその他のライブラリをインストールして、Pythonの力を借りることで上記のシートを0.34秒/枚、40名分を14秒以下、200名分を1分強で読み取れるマークシートリーダーへ、プログラムは進化。きちんとマークしてあれば、読み取りミスはもちろん「0」。マークの濃さや塗りつぶし面積をさまざまに変えて、読み取りパラメータの設定を調整した結果、相当薄いが明らかにマークしてあるものや、逆に、消し方が不十分でうっすらとマークの痕跡が残ったものにもそれなりに対応、さらに、読み取り結果をヒトと協働して確認できるチェック機能も搭載(非力なPCではややもっさりした動作になるが・・・)。ついでに音声読み上げ機能も欲しくなり、Windowsに標準搭載されている日本語の音声合成エンジン(Microsoft Haruka Desktop)のHarukaさんにも登場してもらって(Windowsの機能の利用だから著作権的には問題ないと思うんだけど・・・)ユーザーが選択した任意のシートのマーク読み取り結果を、簡単にチェックできるプログラムとした。

性能的な部分では、十分、満足した・・・けど、そのかわり、プログラム全体のサイズは超巨大化。exe それ自体は3.81MB程度なのだが、Python用のOpenCVをインストールしたフォルダの容量が、なんと158MB!(フォルダのプロパティで確認したところ、ファイル数: 2,820、フォルダー数: 283 とのこと)

これら、プログラムの動作に必要な一式をZipファイル一つにまとめて、他のPCにコピー&展開すると、PCによってはその展開(解凍)処理だけで20分くらいかかってしまう・・・。ライブラリには相当数、このプログラムには必要のないファイルも含まれているはず、そう思えても、どれが不要なファイルなのかが僕にはわからない。

(多少、動作速度は遅くてもよいから、サクっとコピーして、すぐに動かせるマークシートリーダーも欲しい・・・ )

ってか、exeのある場所にPython環境があれば、Python環境を利用して高速動作、Python環境がなければ(自動的に)Delphi用のOpenCVを利用して動作するような、Python環境を「後付けで追加することも可能」(ユーザーが動作環境を選べる)ようなカタチにしたいって、いつの間にか思い始めて・・・。

そのような経緯から僕は、プログラムの起動設定の見直し作業に着手した。それが冒頭に書いたようにこの秋の始まりの頃だった。

まず、行ったことはPython環境の完全な切り離し。せっかく、くっつけたものを切り離すのは大変だったけど、あっちこっちをいじくりまわして、なんとか切り離しに成功☆ Delphi用のOpenCVだけで動作する状態に戻せた。

内部的にはPython環境を利用する際に必要なコードはすべて残してあるので、次に exe と同じ場所にPython39-32フォルダがあった場合には、Python環境を利用して動作するようにプログラムを修正。これも何とか実現できた。

次に、ミニマム構成でプログラムを動かすために最低限必要なDLL等を確認。実際にミニマム(と思える)構成を作ってみて、動作テストを繰り返し行った。その中で、起動時に設定する読み取りパラメータに関して解決しておいた方がいい問題があることを発見。

Delphi用とPython用のOpenCVでは、起動時に設定するパラメータの一部が異なっている。Python環境の有無で(具体的にはPython39-32フォルダの有無で判断)、当然デフォルト・パラメータ設定を変えて起動させなければならない。その部分のプログラムを見直していて ini ファイルに「もし、読みだすべきセクション(名)そのものがなかったら?」という場合も想定しておいた方がいいことに、僕は初めて気がついた。

ちなみに、これまで書いてきたのは次のコード。これでも第2引数に指定したキーがなかった場合に加え、第1引数に指定したセクションそのものがなかった場合にも、第3引数に指定したデフォルト値が変数にセットされるから、エラーにはならないのだけれど・・・、ユーザーには「セクションそのものがない」ということが伝わらない。ユーザーがデフォルト値として設定されたパラメータを調整・保存して初めて ini ファイルに「Section1」が生まれる・・・。

uses
  System.IniFiles;

procedure TFrmMain.FormCreate(Sender: TObject);
var
  Ini: TIniFile;
  str01, str02: String;
begin
  //iniファイルからデータを読込み
  Ini := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  try
    str01:=Ini.ReadString('Section1', '文字列型_XXX', 'ABC');
    str02:=Ini.ReadString('Section1', '文字列型_YYY', 'DEF');
  finally
    Ini.Free;
  end;
  AAA.Text:=str01;
  BBB.Text:=str02;
end;

つまり、これまでの僕のプログラムは、各パラメータ値の設定とデフォルト・パラメータの設定を記録したセクションが「必ず ini ファイル内にある」という大前提で動いていたわけだ。Ini.ReadString の第3引数で「セクション」や「キー」がなかった場合のデフォルト値を指定してあるから、ini ファイル内にそれらがなくてもエラーは発生しないのだけれど、ほんとうにそれでいいのか? って、そう考えるとそれは「よくない」気がして・・・ならないし。

いちばん気になる、各パラメータ値の設定を記録した「セクションそのものがない」場合にはどうするか?、その対処方法を、知ってるか? って問われたら、その答えを僕はもちろん、知らない。そのことに、ここでようやく気付いた。この場合、知らないでは済ませるのは「すーぱー嫌」なので、良い機会だと思い、それを調べてみることにした。

2.セクションの存在を確認

調べてみると、やはりini ファイルにセクションが存在するか・どうかを調べるメソッドがちゃんと用意されていた。TIniFile の仕様的にエラーが発生しないから、このメソッドの必要性をこれまで僕は感じなかったんだろう・・・。

System.IniFiles.TCustomIniFile.SectionExists

https://docwiki.embarcadero.com/Libraries/Sydney/ja/System.IniFiles.TCustomIniFile.SectionExists
SectionExists メソッドを使用すると,FileName で指定した INI ファイル内にセクションが存在するかどうかがわかります。Section は,SectionExists が存在することを決定する,INI ファイルのセクションです。SectionExists は Boolean 値を返して,対象のセクションが存在するかどうかを示します。 (上記リンク先より引用)


で、書いてみたのがこちら。

  //iniファイルから設定データを読込み
  Ini := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  try
    //Settingsセクションの有無を調査
    if Ini.SectionExists('Settings') then
    begin
      str1:=Ini.ReadString('Settings', '文字列型_閾値', '180');
      str2:=Ini.ReadString('Settings', '文字列型_平滑化強度', '41');
      str3:=Ini.ReadString('Settings', '文字列型_判定係数', '3');
      //Python39-32フォルダの有無を調査
      if System.SysUtils.DirectoryExists('Python39-32') then
      begin
        str4:=Ini.ReadString('Settings', '文字列型_手法', '正規化相互相関');
      end else begin
        str4:=Ini.ReadString('Settings', '文字列型_手法', '差分相関');
      end;
      str5:=Ini.ReadString('Settings', '文字列型_選択肢の始まり', '1');
      //str5:=Ini.ReadString('Settings', '文字列型_左補正', '');
      //str6:=Ini.ReadString('Settings', '文字列型_右補正', '');
      //str7:=Ini.ReadString('Settings', '文字列型_上下補正', '');
      str8:=Ini.ReadString('Settings', '文字列型_判定領域', '70');
      str9:=Ini.ReadString('Settings', '文字列型_読上速度', '3');
      //追加
      CheckZahyo.Checked:=
        Ini.ReadBool('Settings', '論理値型_座標チェック', True);
      CheckVolMixer.Checked:=
        Ini.ReadBool('Settings', '論理値型_音量ミキサー表示', False);
      boolSpeed:=Ini.ReadBool('Settings2', '文字列型_速度', False);
    end else begin
      //デフォルトのパラメータを表示
      str1:='180';
      str2:='41';
      str3:='3';
      //Python39-32フォルダの有無を調査
      if System.SysUtils.DirectoryExists('Python39-32') then
      begin
        str4:='正規化相互相関';
      end else begin
        str4:='差分相関';
      end;
      str5:='1';
      str8:='70';
      str9:='3';
      //追加
      CheckZahyo.Checked:=True;
      CheckVolMixer.Checked:=False;
      boolSpeed:=False;
      //設定を保存するようユーザーに案内するためのフラグを設定
      P4D_Exist:=False;
    end;
  finally
    Ini.Free;
  end;

プログラムが冗長化しただけ・・・のような気もするし、「セクションがない」場合には勝手にそれを作成して保存してしまう手もあるんだけど・・・。デフォルト・パラメータはあくまでも僕の環境(マークシートの紙質、画像化する際に利用するスキャナー、読み取り解像度、その他いろいろ)でのベストな値であって、環境が異なれば当然調整しなければならない場合もあり得る。そう考えると、もし、ini ファイル内にセクションそのものが存在しなかった場合は、それをユーザーに案内してきちんとセクションとして保存してもらえる仕様にするのがいちばんいいと思えてきた。

そこで、上記のように、ユーザーへの案内(通知)の要/不要を判断するフラグを用意。さらに、ユーザーへの案内(通知)は、Formが表示されてからの方が、なんとなく安心感があるかな? って思ったので、もし、セクションがなかった場合は、Form が表示された直後に案内(通知)するようにプログラミング。

private
    { Private 宣言 }
    //Python4Delphiの有無を知るフラグ
    P4D_Exist:Boolean;

procedure TFormMSReader.CMShowingChanged(var Msg: TMessage);
var
  strMsg: string;
begin
  inherited; {通常の CMShowingChagenedをまず実行}
  if Visible then
  begin
    Update; {完全に描画}
    if not P4D_Exist then
    begin
      strMsg:='読み取りパラメータの設定が、デフォルト値となっています。'+#13#10+
      '必要に応じて読み取りパラメータの調整を行い、'+
      '「設定を保存」ボタンがアクティブな状態で保存してください。';
      Application.MessageBox(PChar(strMsg), PChar('情報'), MB_ICONINFORMATION);
    end;
  end;
end;

セクションそのものがない場合には、i マークのアイコン付きでメッセージボックスを表示する。

3.まとめ

今回、初めて知った SectionExists メソッド。使わなくてもエラーは発生しないのだけれど、このメソッドについて調べたことで、あらためて「プログラムの仕様」について考える大変良い機会を得ることができました。いつか先輩に言われた言葉が胸によみがえります。

「きちんと動くプログラムが、いいプログラムなんだ。」

きちんと動く・・・って、その意味は、僕が思っていた以上に、深いようです。

4.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

クリーン・インストール

【20240204追記】

この記事で述べた方法では、問題は解決できません!(再発します)
この記事で述べている問題の正しい解決方法は、下記リンク先の記事をご参照ください。


Delphi のF9(実行)押下時に、原因がわからないエラーが発生(デバッグ出力)することに気づいた。
アプリケーションは何の問題もなく起動し、期待通りに動作、表面上はエラーも一切出ないのだけれど、水面下で「何か良くないこと」が起こってる感じ。

エラーを抱えたままの開発環境が作成した exe はさすがにヒトには配れない。なので、なんとかするべくいろいろ調べたが、なんと!情報の『欠片』すら見つからない・・・。困りに困って、頼るべき最終手段、サポートセンターに相談すると、親切丁寧なアドバイスを複数いただき、その中でDelphi自身の再インストールも状況改善のための選択肢の一つと知る。

「なるほど!」、早速実行。

再インストールは順調に終了。(再インストール時の)エラーの発生は皆無。
で、VCLアプリケーションを新規に作成し、実行(F9)すると

デバッグ出力:
clientcore\windows\advcore\ctf\shellhandwriting\client\handwritingclient.cpp(287)\Msctf.dll!77A3FC28: (caller: 77A345FD) LogHr(3) tid(988) 8007007E 指定されたモジュールが見つかりません。
    Msg:[onecore\internal\sdk\inc\wil\opensource/wil/winrt.h(1686)\Msctf.dll!77A37442: (caller: 77A3F94D) Exception(1) tid(988) 8007007E 指定されたモジュールが見つかりません。
]

プロセス Project1.exe (6408)

状況は変わらず・・・。まったく同じエラーが出現・・・。
そうか・・・。原因はDelphiじゃない。

Windows なんだ・・・。

もくじ

1.handwritingclient.cppって何だ?
2.インストールメディアを作成
3.クリーン・インストール
4.登録回数の上限に達しました
5.エラーが消えた!
6.まとめ
7.お願いとお断り

1.handwritingclient.cppって何だ?

去年の今頃、僕は「手書きカタカナ文字認識機能」を自作の手書き答案採点補助プログラムに搭載できないかと考え、毎日夢中で認識率100%を実現できる学習モデル作りに取り組んでいた。

Pythonのスクリプトもたくさん書いたけど、それ以外に、いろんなアプリも試した。
もしかして、それがエラーの原因に?

embeddable Python を内部的に動かして手書きカタカナ文字をPCに認識させるため、Lobeで学習モデルを作成し、GUIはDelphiで作って自分的には(OK!)と思えるレベルを達成(できたんだけど認識率100%じゃなかったから公開はしなかった)、あの時、相当トライ&エラーを繰り返してPCをフリーズさせたり、いろんなライブラリを思い出せないくらいインストールしたりしたからなー。でも、基本的にSDカードに仕込んだWinPythonで実験したから、Cドライブには影響ないと思うんだけど・・・。

(後に読み書き速度を向上させるため、使わなくなったノートPCから取り外したSSDをMyPCに外付けして、そちらにWinPythonを入れて実験を継続。もちろん、動作速度はめちゃめちゃ速くなった!)

でも、LobeはCドライブにインストールして学習モデル作ったしなー。
もしかして、原因はコレかなー?

いずれにしても、Windowsのどこかに不具合があることは間違いない。handwritingclient.cpp がどこからやってきたC++のソースファイルなのか? さえわかれば、手の打ちようもあると思うんだけど、それも解明できない。

僕の力では、この不具合は直せない。・・・とすれば、残された方法は一つ。
リカバリーメディアを作成して、PCを工場出荷時の状態に戻す。

そう、クリーン・インストールだ。

2.インストールメディアを作成

幸いにして日付時刻は週末の早朝。急いでしなければならない仕事はない。今日の用事を強いてあげれば、バイクのエンジンを動かすこと、それから、イケナイ水を買いに行くことくらいだ。それは夕方、いっぺんにできる。

( そう言えば、このPCのリカバリー用インストールメディア、作ってなかった・・・ )

僕はずっとPanasonic製の Let’s Note を使っている。現在の使用機種は CF-QV だ。高価なマシンだけど、頑丈で壊れないし、すごく入力しやすいし、重さも動作もすこぶる軽快。あの世に持っていけるものを一つだけ選べと言われたら、僕は間違いなく、Delphi を入れた Let’s Note を選ぶ。

スタートボタン ⇨ Panasonic PCリカバリーディスク 作成ユーティリティを起動。

初めて拝んだリカバリーディスク作成ユーティリティの起動画面(引用)。

昔はPC購入直後に必ずリカバリーディスク作って大切に保管してたんだけど・・・

いつのまにか・・・


手近にDVD-R DL(2層)メディアが数枚あった。これを外付けドライブに挿入して、リカバリーディスクの作成を開始。

( 80分かぁ ちょっと長いなー )

1枚目のディスクのチェックの進み具合を示すプログレスバーが半分くらいまで進んだとき、予期しないエラーメッセージが・・・

『・・・メディアの作成に失敗しました・・・』

ちゃんと made in Japan って書いてあるディスク使ったのにー T_T

仕方ないから、別のディスクに入れ替えて、再チャレンジ。

ところが、またチェックの進み具合を示すプログレスバーが半分くらいまで進んだところで、

『・・・メディアの作成に失敗しました・・・』

なんでー!

さすがに三度目の正直を目指そうとは思いませんでした。

PCを乗せた机が振動した可能性(はないと思ったけど、いちおう)も考え、PCの置き場所を変更。

さらに、外付けドライブも、むかし使ってた別のちょっと古い機種に変更。

さらに、使用するメディアも DVD-R( Made in Vietnam )に変更。

今度は順調に進行。1時間半ほどかけて無事DVD-R4枚のリカバリー用ディスクが用意できた。あとは、重要なデータをバックアップして、リカバリーするだけだ。

これまでに数限りなく、データのバックアップでイタいめにあってきて、今の僕は深い階層には一切データを置かないようになった。もうトシだし、ほんとにバカだから、どこに、なにを置いたのか、すぐに忘れる・・・、メモしてあっても、そのメモの存在を忘れてしまう。だから、リンク先確認等で起動に多少時間がかかろうと、そんなのは一切無視。重要なデータは全部デスクトップにおいている。

・・・とは言っても、現在、僕にとって重要なデータは2種類しかない。一つは、Delphiで書いたプログラムのソースを保存してあるフォルダとその中のファイルたち。もう一つは、その時々で遭遇したプログラミングする上で解決しなければならなかった問題と、その解決方法を記録したNaNaTreeのファイルたちだ。これがPictureやDBなど、分野別に分類して21ファイルある。更新し続けてもう20年・・・。Delphiを続ける限り、このファイルが完成することはない。僕が死んだその日に、更新が永遠に止まるだけだ・・・。

僕は、必要に応じて、この21個のファイルを、とっかえひっかえ表示しながら、ほとんどコピペを繰り返すようなスタイルでDelphiのプログラムを書いて(?)いる。だから、この21個のファイルには、僕とDelphiのこれまでのすべてが詰まっている。そう、僕のいちばんの『たからもの』だ。

これをバックアップすればPC本体はいつでもリカバリーできる。そこで別の外付けSSDを接続して現在デスクトップにある重要なファイルとフォルダのバックアップを新規に作成。リカバリーに成功したら、SSDのバックアップからデータをPCのデスクトップに書き戻す。

以前はPythonで書いたスクリプトも保存してたけど、Pythonの方はDelphi以上にコピペの集合体だし、バックアップを探すより、Webにある情報を検索した方が便利なことに気づいたことと、何よりPythonはスクリプト本体より、そこから呼び出しているライブラリが膨大なファイルを含んでいるため、バックアップに時間と容量が必要だったり、かつ、ライブラリのフォルダ階層が深すぎてコピー時にエラーが起きたり、とにかくバックアップそれ自体が何かと面倒で、いつの間にか、僕はWinPythonを仕込んだSDカードのバックアップをとるのをやめてしまった。

3.クリーン・インストール

外付けDVDドライブにリカバリー用のディスクを入れて、PCを再起動。なんか起こるかと思ったら、普通にWindows11が起動してしまった。そうだ、BIOSの起動設定を変更するのを忘れてた!

再起動してF2を連打。BIOS画面が表示される。こちらもMy PCで拝むのは、もしかして初めてかも。外付けDVDドライブの起動を最上位に持ってきて設定を保存。再々起動。

今度は見たことのない画面が表示され、リカバリーが始まった。ひたすら1枚目のDVDディスクを読んでいるようだ。じっと見ていても仕方ないので、いずれ「2枚目のディスクを入れてください」みたいな表示が出ると見込んで、別のPCをいじって過ごす。リカバリー完了までのインターネット接続はメインで使用しているPCの前に使っていたB5サイズの Let’s Note があるので、こちらを久々に起動して必要な情報を取得する。ただ、このPC。もう長いこと、OSのアップデートを行っていない。それどころか、前回、いつ、電源をONにしたのか・・・それすら記憶にない。

( このPCもメンテナンスしなきゃ )

気がつくと、リカバリーしているPCの画面に2枚目のディスクを求めるメッセージが表示されている。2枚目を入れると、また延々とデータの読み込みが始まった。これを3枚目、4枚目と繰り返す。

4枚目のディスクを挿入後、なんだか安心して、しばらくまどろんでしまった・・・。目が覚めたら、リカバリーは無事完了。PCは工場出荷状態に戻った。昔は、ここからの設定がほんとにたいへんだったけど、今はWeb上にほとんどすべてのMy設定が記録されているので、本当にセットアップが簡単になった。何回か、IDとパスワードなどを入力してWindows11が正常に起動する状態まで戻せた。

リカバリーする前のWindows11のバージョンは 22H2 だった。前に使っていたPCでWindowsの更新情報を検索して、最新のWindows11は23H2であることを知る(2023年12月上旬現在)。どうせならWindowsも最新の状態にアップデートしたい。そう思ってWindowsの更新をいくつか入れてるうちにWindows Update に 23H2 へのアップデートのリンクが表示されるようになった。ラッキー☆

迷うことなく、23H2へのアップデートを実行。PCは無事、復活。それも最新のOSを身にまとって。

4.登録回数の上限に達しました

今回のリカバリーの最大の目的は、Delphiのデバッグ出力で表示されるエラーを解消すること。OSは無事リカバリーできたから、エラーも解消されるはず。次は Delphi 12.0 のインストールだ。

my.embarcadero.comへ行き、Web Install のリンクをクリック!


インストーラーを取得して、起動。必要事項を入力して先へ進もうとすると・・・

登録回数の上限に達しました

でたー T_T また、コレかぁ・・・ まだ 12.0 インストールした回数は新規で1回、PythonEngineの登録で実験的にインストールしなおして1回、今回のリカバリー後の再インストールで1回、合計3回だと思うんだけど・・・。

しかも日付は土曜日。サポートセンターは間違いなくお休みの日。もちろん、明日も。

( 制限を解除してもらえるのは月曜日かぁ・・・ )

とりあえず、サポートフォームを開いて、「製品登録(使用許諾)の上限更新依頼」だけしておこう。

カチャカチャ・・・

・・・というわけで、Delphiのインストールは翌週に持ち越し(持ち越され)て、その他の必要なソフトウェアを導入して週末の1日を過ごすことに。

( 今、どれくらい空き容量が残ってるんだろう? )

ふと気になって、Cドライブの空き容量を確認。なんと!驚いたことに全460GBあるうち、100GB程度しか使ってない。あと350GBも、空きが残ってる。

( 確か・・・ リカバリーする前は、残りが100GBくらいだったぞ。いったいナニが入ってたんだ? )

容量を食いつぶすデータといえば、その筆頭はもちろん動画。しかし、僕はバイク関連のごくわずかな動画しかPCに保存してない。アプリはそれなりに入れてたけど・・・250GB分もそれがあったとは到底思えない・・・。

そんなことを考えていたら、突然、デスクトップに見たことのあるフォルダが次々に『全自動で生成』されて行く現象が発生・・・。あっけにとられた感じで、しばし呆然とこれを見つめる・・・

( そうか、OneDriveのデスクトップとデフォルトで同期がONなんだ・・・ )

( 今、クラウドにあるOneDriveのデータは、いずれ、そのうちMyPCに・・・ )

( これまではOneDriveになんでも投げ込んでたから・・・ ちりも積もればナントカで・・・ )

うわー☆ 同期の設定OFFにしないと
またすぐ空き容量がなくなるー!!

慌てて画面右下の雲のアイコンを右クリックして設定を開き、同期とバックアップの「バックアップを管理」ボタンをクリックして、「このPCのフォルダーをバックアップする」の設定をすべてOFFにする。僕は、僕自身が選んだデータだけ、OneDriveに置いておきたいんだ。バックアップは自己責任で行うよ。Windowsに全部、面倒を見てもらいたくない。PCのストレージ容量が「無制限」で、OneDriveも保存容量に「制限がない」なら、全自動バックアップ大歓迎だけど・・・。


これまで気がつかなかったけど、空き容量が減ってたのは、たぶん、この設定も原因の一つだ・・・。PCをリカバリーして、今までぼんやりとしか見えてなかったものが初めて『はっきり・くっきり見えてきた』ような・・・気がする。

あとは月曜日を待って、Delphiをインストール。
F9押下時のエラーが消えれば(絶対消えるはずだけど)・・・

もっと『すっきり』するんだけどなー☆

5.エラーが消えた!

月曜日、「製品登録(使用許諾)の上限更新依頼」に対する返信があり、登録回数の上限が更新されたことを知る。Delphi 12.0 を再インストール。

Python4Delphiがない状態で確認。

新しくVCLプロジェクトを作成。保存して、F9(実行)押下。

デバッグ出力にエラーは出ない。

( やった。多分、もう、大丈夫だ )

いったん、Delphiを終了。

Delphiを再起動して、GetItパッケージマネージャからPython4Delphiをインストール。

既存のプロジェクト(マークシートリーダーのプロジェクト)を呼び出す。

F9(実行)押下。

エラーメッセージは表示されない!(P4DのインストールもOK!)

Delphiが直ったー☆☆☆

6.まとめ

エラーの根本的な原因がわからないまま、リカバリーという最も強引で、安全、確実な方法で問題を解決することになったが、目的外に実にいろんなことを経験できたリカバリー作業だった。

なぜ、DVD-R DLの書き込みに2回連続失敗したのか?

気になったので調べてみると、あちこちで同じような経験をした人がかなりいるようで、「片面2層」の DVD-9 でディスクの枚数を減らすより、ディスクの枚数は増えても、片面1層(約4.7GB)の DVD-5 を使用した方が書き込みエラーが発生する頻度は低いようだ。

メディアと機械の相性とか、いろんな問題がありそうだけれど、画質にこだわりつつ、ディスク1枚に動画を記録するのとは明らかに目的が異なる(今回のリカバリーディスク作成みたいに)単にデータが記録できればOKって場合は、片面1層のメディアを選んだ方がいい。DVD-R DLへの書き込みに失敗した本当の原因は、こちらもわからないままなんだけど、経験としてリカバリー用途のメディアには何を使ったら良いのかがとてもよくわかった。

それから、今まで「わかっているようで、実はわかっていなかった」OneDriveの使い方を本気で考えるイイきっかけにもなった。

今回のリカバリー前後で比較したCドライブの空き容量の違いに気づいたことで、自分にとっていちばん便利なOneDriveの使い方(=付き合い方)が見いだせたのはとてもラッキーだった。なんでもかんでもOneDriveに保存することは止めて、いつでも・どこでも参照したい必要最小限度のデータのみを保存し、その他の重要なデータは複数の外付けのSSDにバックアップを取る。そうすれば、僕にとって必要なSSDの容量は、150GB程度であることまで明らかにできた。今後、新しいPCの購入を考える際にも、この数字は役に立ちそう。

あと、今後、役立つことはまぁないと思うけど、Delpjiの登録回数の上限もなんとなくわかってしまった・・・。これを知るためだけにインストールとアンインストールを繰り返す人はまずいないだろうから、ある意味、これは貴重な情報と言えば言えるかもしれない。

僕はDelphiという開発環境と、Object Pascal というプログラミング言語が好きだし、その文化の存続を願うから、例えそれを高額と感じても正規の利用料を支払って、Delphiのブラッシュアップを続けてくれている Embarcadero Technologies さんの発展を支えたい。

何より、最大の目的だったDelphiのF9(実行)押下時のエラーを解消できた。これで安心してプログラムが書ける。それがいちばんうれしい。プログラムを書いていると、自分自身がよい方向に歩いているような気がしてくる。それが何よりもうれしくて、僕はプログラムを書くのかもしれない。

僕がこの世から消えた後も、それが動くことを願って。

7.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

RAD Studio 12.0にPython4Delphiをインストールする!(その2)

前回、Delphi 12.0 に Python4Delphi をインストールする記事を書いた。

次は、その過去記事へのリンク。


実は、もっと、イイ方法が・・・☆☆☆

今まで知らなかったのですが、ウェルカムページの右上にある「GetItパッケージマネージャ」からインストールすれば、さらにカンタンでした!!

【もくじ】

1.エラーが消えない!
2.GetItパッケージマネージャからP4Dをインストール
3.まとめ
4.お願いとお断り

1.エラーが消えない!

いつからそうなったのか、わからないのだけれど、Delphi 11.2 & 12.0 で F9(実行)するとデバッグ出力にエラーがいくつか表示されるようになった・・・。

デバッグ出力:
clientcore\windows\advcore\ctf\uim\tim.cpp(800)\Msctf.dll!7577215A: (caller: 7576D910) LogHr(1) tid(1ff8) 8007029C アサーション エラーが発生しました。

プロセス XXX.exe (5568)

デバッグ出力:
clientcore\windows\advcore\ctf\uim\tim.cpp(800)\Msctf.dll!7577215A: (caller: 7576D910) LogHr(2) tid(1ff8) 8007029C アサーション エラーが発生しました。

プロセス XXX.exe (5568)

デバッグ出力:
onecore\internal\sdk\inc\wil\opensource/wil/winrt.h(1686)\Msctf.dll!757A7442: (caller: 757AF94D) Exception(1) tid(1ff8) 8007007E 指定されたモジュールが見つかりません。

プロセス XXX.exe (5568)

デバッグ出力:
clientcore\windows\advcore\ctf\shellhandwriting\client\handwritingclient.cpp(287)\Msctf.dll!757AFC28: (caller: 757A45FD) LogHr(3) tid(1ff8) 8007007E 指定されたモジュールが見つかりません。
    Msg:[onecore\internal\sdk\inc\wil\opensource/wil/winrt.h(1686)\Msctf.dll!757A7442: (caller: 757AF94D) Exception(1) tid(1ff8) 8007007E 指定されたモジュールが見つかりません。
] 

プロセス XXX.exe (5568)

「handwritingclient.cpp」ってなんだろ? ・・・と、Google先生にお伺いをたてても、関連する情報は何一つ得られず、「Msctf.dll」の方は検索結果にいろんな情報が出てくることは出てきても、なんかみんな的外れな感じで、走召!困った。

どうにもならないので、サポートセンターに援けを乞う。

サポートセンターの担当者の方と何度かメールでのやりとりを行った結果、Delphi 11.2 & 12.0 をどちらもアンインストールして、12.0 のみを再インストールすることに。

アンインストール & 再インストール作業は何のエラーもなく進行。

で、結果から言うと、F9(実行)で「エラーはそのまま表示され、消えません」でした。

T_T

エラーは消えなかったのですが・・・

ふと、思いついて update の有無を確認しに行った GetItパッケージマネージャで「愛しのP4D」を発見!

( こんなところにあったんだ。何で今まで気づかなかったんだろう? )

そう思いながら「インストール」ボタンを「ポチ」っとクリック。

結果・・・

今までの大苦労はなんだったんだ・・・
そう思うくらい、あっけなく
P4Dがインストールされました!

2.GetItパッケージマネージャからP4Dをインストール

転んでもタダでは起きない・・・とはまさにこのこと? 以下、GetItパッケージマネージャからP4Dをインストールする方法です。

Delphi 12.0 を起動すると表示される(デフォルト設定)、ウェルカムページ右上の

 ↑
これをクリック。

すると、次の画面が表示されるので、オプションボタンを図のようにクリック。

カテゴリ「Python」のオプションボタンをクリックすると、そこにP4Dがある。


ちなみに、著者の方のお名前の部分をクリックすると、GitHub の Python4Delphi のページに飛びます。

GitHub の Python4Delphi のページ

https://github.com/pyscripter/python4delphi


続けて、「インストール」をクリック。

「すべて同意する」をクリック。


インストールはすぐ完了。

カンタンなこと、この上なし!
七匹のヘビを無事発見!

3.まとめ

Delphi に Python4Delphi をインストールするなら、GetItパッケージマネージャからインストールするのが「走召」カンタンで便利!

さて、Delphi の「消えないエラー」をどうしよう・・・


追記(20241005)

2024年2月4日に、この問題の解決策を見つけていました!

僕のPCは、上記リンク先の方法で、正常な状態に復旧できました。
(ここに追記するのを忘れてました。ごめんなさい!!)

4.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

VCLのMessageBox

Delphi 12.0を使い始めて、最初に気づいたことは、メッセージダイアログの異変だ。

( なんだか、シンプルになったなー )

Delphi 12.0 のメッセージダイアログ


そう。MessageDlgからアイコンが消えちゃった!

【もくじ】

1.Delphi 12.0 のメッセージダイアログ
2.MessageBoxがあった!
3.MessageBoxの使い方を学ぶ
4.MessageBoxの使用例
5.ダイアログからの戻り値
6.まとめ
7.お願いとお断り

1.Delphi 12.0 のメッセージダイアログ

今まで( Delphi 11.2 )なら・・・

i マークのアイコンがあるメッセージダイアログを表示するコード


実行すると・・・

表示されたメッセージダイアログ


System.UITypes を uses してないと・・・

「System.UITypesをusesしなさい」とアドバイスされる(これは 12.0 も同じ)。

Delphi 12.0 では・・・、System.UITypes は、もちろん uses して、

Delphi 12.0 でメッセージダイアログを表示


11.2 と同じコードを実行すると・・・

青地に白の i マークのアイコンが「ない」


それに、なんかエラーも。

僕には理解不能


( どうする? )

取り敢えず、Help でVCLのライブラリ・リファレンスを調べてみた。

キーワードを指定してEnterキーを押すと・・・


表示されたリファレンスの下の方に、次の一文を発見!

mtInformation じゃなくて、mtConfirmation だけど「Microsoftは、・・・削除してしまいました。」とある・・・


( これかー T_T )

mtInformation ではなく、mtConfirmation で試してみると・・・

引数を mtConfirmation に変更


実行すると・・・

アイコンは表示されないが、タイトルは「確認」に変わる


タイトルが「情報」から「確認」に変わるから、mtConfirmation は確かに効いてる。

「このアイコンがメッセージの具体的な種類を明確に表さない」って、んじゃもっと良いアイコンに変更するとか・・・、元々、アイコンはキャプションの補佐的役割を果たしているだけだから、それが「ない」よりは「あった」方が親切だと思うんだけれど・・・。

リファレンスによれば、第2引数に指定可能な値とその意味は次の通り。

Delphi 12.0 の Help の VCL のライブラリ・リファレンスを引用


リファレンスには「以前のダイアログ ボックスの概観を使用するには、Vcl.Dialogs ユニットの UseLatestCommonDialogs 変数を False に設定しなければなりません。」と但し書きがある。

以前の・・・って、XPの時代のことかな?・・・なんて思いながら、次のように指定して、

UseLatestCommonDialogs 変数を False に設定


実行すると・・・

さらにシンプルに・・・T_T


僕的には「シンプルじゃないのがよかった」ので、
とても「Simple is Best 」的な気持ちにはなれない!

あぁアイコンが欲しい!

もう、二度と・・・


きみに会えないかと思うと。

魂が折れそうなくらいの、さみしさがこみあげて・・・くる。

なんでだよー!

そう思いながら、ついでに mtError も試してみる。すると・・・

エラーのアイコンは表示される


「 × 」マークはMicrosoftさん的には、全世界共通「ダメです」って通じるってこと?


mtWarning もやってみた!

警告もアイコンは表示された


黄色の三角形に「i」が表示されたら、世界中の人が「警告」だって思うのかなー☆
Microsoftさん的には、「思う」ってことなんだろうなー。


ともあれ、これで確認できた。「情報」と「確認」がダメなんだ・・・。

でも、ない袖は振れない。

自分専用のダイアログを作るという、奥の手がないわけじゃないけど、それは最終手段。

MessageDlg のかわりになるものは・・・、ShowMessage 以外になんか、なかったっけ?

2.MessageBoxがあった!

ものごころついた時から・・・ってわけじゃないけど、僕はずっと MessageDlg関数を使ってきた。
ダイアログで使用する Font の大きさや色を変えたいなど、余程の理由がない限り、ユーザーに何かを伝えるいちばんの方法は、僕にとっては MessageDlg関数 で表示するメッセージだった。

それが ShowMessage ではない最大の理由は、テキストの他にアイコンも表示できるから。

だから、階層化テキストエディターの NaNaTree に書き溜めてきた、さまざまなメッセージの表示方法に関する覚書も、そのほとんどが MessageDlg関数 についてのもので、これにかわる代替手段など、これまで僕は考えたこともなかった。

( バージョンアップとかすると、毎回、いろんな落とし穴があるけど・・・ )

( 今回のも、これまで普通に使ってきたダイアログの仕様変更だもんなー )

( すーぱー困るけど、元に戻してくれるわけないし・・・ )

( なんか、代替手段、なかったっけ? )

そう思いながら、NaNaTree に保存した「メッセージ」に関する記録を下のほうへスクロールしてみる。

すると・・・

MessageBoxなる文字が・・・


あんまり使ったことのない、未整理の項目が下の方にあって、その中にMessageBoxという文字を発見。

( ほぼ使った記憶がないけれど、そんな関数もそう言えばあった・・・ )

さっそく、Helpを読んで、書き方を確認し、次のコードを実行してみた。

MessageBox関数を試す


はたして、アイコンは表示されるか?

祈るような気持ちで実行すると・・・

アイコン付きのダイアログが表示された!

何にも(追加で) uses しなくてイイし、いきなり書いて、すぐ動く!

コレだ! コレ!!

今度からコレで行こう☆

自分でもあきれるくらいの変わり身の早さ・・・

MessageDlg が泣いてるぞ。

愛してたんじゃなかったのかい・・・?

3.MessageBoxの使い方を学ぶ

VCL ライブラリの Help を読んでわかったことは、「MessageBox は、Windows API MessageBox 関数をカプセル化したものである」ということ。

このカプセル化で「具体的に何がどうなったか」と言うと、Windows.MessageBox とした場合には必要であったウィンドウ ハンドル パラメータが欠けていても、自動的に補完される、つまり、所有者ウィンドウへのハンドルは引数内で指定しなくてもよいということ。

ありがたき しあわせ。

ちなみに、Win32APIのリファレンス(C++)では、次のようになっているが、

int MessageBox(
  [in, optional] HWND    hWnd,
  [in, optional] LPCTSTR lpText,
  [in, optional] LPCTSTR lpCaption,
  [in]           UINT    uType
);

Delphi の Application.MessageBox のリファレンスでは、次のように

function MessageBox(const Text, Caption: PChar; Flags: Longint = MB_OK): Integer;

所有者ウィンドウへのハンドルが確かに省略されている。

実際のコードで、Windows.MessageBox とした場合には・・・

procedure TForm1.Button4Click(Sender: TObject);
begin
  Winapi.Windows.MessageBox(Handle, PChar('Do you know Delphi?'), PChar('情報'), MB_OK or MB_ICONINFORMATION);
end;

だったのが、第一引数のHandle は必要なくなり、( OK ボタンのみの表示でよければ)MB_OK も省略できるようなので、次のように

procedure TForm1.Button3Click(Sender: TObject);
begin
  Application.MessageBox(PChar('Do you know Delphi?'), PChar('情報'), MB_ICONINFORMATION);
end;

・・・とずい分、短くなる。それどころか、PChar型への型変換も省略可能なようで・・・

procedure TForm1.Button5Click(Sender: TObject);
begin
  Application.MessageBox('Do you know Delphi?', '情報', MB_ICONINFORMATION);
end;

型変換も内部で自動的に処理してくれるようだ(このまま実行可能。エラーも、警告も、ヒントも表示されない)。

Good! Gooder! Goodest!

これからは MessageBox で行こう!

4.MessageBoxの使用例

「 i 」マークのアイコンで、OK ボタンのみ表示する場合は、

Application.MessageBox(PChar('メッセージ'), PChar('情報'), MB_ICONINFORMATION);


警告マーク(黄)

Application.MessageBox(PChar('メッセージ'), PChar('警告'), MB_ICONWARNING);


禁止・エラー・停止マーク(赤地に白)

× の意味は、第3引数にSTOPとあるから「停止」が正しいのかな?

Application.MessageBox(PChar('メッセージ'), PChar('禁止'), MB_ICONSTOP);
Application.MessageBox(PChar('メッセージ'), PChar('エラー'), MB_ICONSTOP);


?マーク(青地に白)

Application.MessageBox(PChar('メッセージ'), PChar('質問'), MB_ICONQUESTION);

文字列型の変数を用意して、

var
  strMsg:string;
begin
  strMsg:='メッセージ';
  Application.MessageBox(PChar(strMsg), PChar('情報'), MB_ICONINFORMATION);
end;


別の文字列型変数をさらに代入したり、また、改行を含む表示も、

procedure TForm1.Button2Click(Sender: TObject);
var
  strMsg, strPath:string;
begin
  strPath:='C:\abc\def';
  strMsg:='出力先は次の場所です。' + #13#10 + #13#10 + strPath;
  Application.MessageBox(PChar(strMsg), PChar('情報'), MB_ICONINFORMATION);
end;

複数のボタンを表示。例えば、「はい」・「いいえ」の二択なら、

procedure TForm1.Button3Click(Sender: TObject);
begin
  //Information
  if Application.MessageBox(PChar('Do you know Delphi?'), PChar('情報'), MB_YESNO or MB_ICONINFORMATION) = mrYes then
  begin
    //[はい]が選ばれた時
    Application.MessageBox(PChar('Gooooooooooooood!'), PChar('情報'), MB_ICONINFORMATION);
  end else begin
    //[いいえ]が選ばれた時
    Application.MessageBox(PChar('No!'), PChar('情報'), MB_ICONINFORMATION);
  end;
end;
二択だから「キャンセルはない」
(閉じるボタンは自動的に無効になる)

ユーザーに「キャンセル」も許可するなら、

procedure TForm1.Button4Click(Sender: TObject);
var
  StrMsg: String;
  intRet: Integer;
begin
  StrMsg := 'Do you know Delphi?';
  intRet := Application.MessageBox(PChar(StrMsg), PChar('情報'),
                         MB_YESNOCANCEL or MB_ICONQUESTION);
  if intRet = mrYes then begin
    //[はい]を選択した時の処理

  end else
  if intRet = mrNo then begin
    //[いいえ]を選択した時の処理

  end else
  if intRet = mrCancel then begin
    //[キャンセル]を選択した時の処理
    Application.MessageBox(PChar('ユーザーによる処理のキャンセル'), PChar('情報'), MB_ICONINFORMATION);
  end;
end;
閉じるボタンは自動的に有効化されている


ちなみに「キャンセル」ボタンではなく、ダイアログ右上の「閉じる」ボタンをクリックすると・・・

閉じるボタンをクリックした場合は、キャンセル扱いになるようだ。

デフォルトでフォーカスを与えるボタンの指定方法は、第3引数の中で MB_DEFBUTTON3 のように、MB_DEFBUTTON の後ろにフォーカスを与えるボタンの番号を付けて指定する。番号はダイアログに表示するボタンの左から順番に1、2、3、4となるようだ。

第3引数に MB_YESNOCANCEL or MB_DEFBUTTON3 or MB_ICONQUESTION を指定すると、

左から三つめの「キャンセル」ボタンにフォーカスされた状態でダイアログが表示される


数字を付けない MB_DEFBUTTON や、数字を付けても MB_DEFBUTTON0(ゼロ)や MB_DEFBUTTON5 は未定義の識別子エラーになることから、指定可能なボタンの数は1~4の範囲内と決まっているようだ。

MB_DEFBUTTON5 は未定義の識別子エラーになる


「はい・いいえ・キャンセル」の三つボタンを表示する設定で MB_DEFBUTTON4 を指定しても未定義の識別子エラーにはならないし、実行時コンパイラはヒントも警告も表示しないが、フォーカスは最も左のボタンに当たるようだ。

intRet := Application.MessageBox(PChar(StrMsg), PChar('情報'),
                         MB_YESNOCANCEL or MB_DEFBUTTON4 or MB_ICONQUESTION);
MB_DEFBUTTON4 を指定してもエラーにはならない。
MB_DEFBUTTON4 を指定したにもかかわらず、
4つめのボタンがない場合には、最も左のボタンにフォーカスされる。

ってか、四つ目のボタンでナニ?

想像したけど、ちょっと思いつかない。表示できるボタンの種類を調べてみた。

MessageBox に表示できるボタンの種類は全部で七つあるようで、Flags パラメータで指定可能な値は、次の通り(Delphi 12.0 の VCL リファレンスより引用)

意味
MB_ABORTRETRYIGNORE メッセージ ボックスには、次の 3 つのボタンが配置されます: 中止、再試行、無視。
MB_OK メッセージ ボックスには、次のボタンが配置されます: OK。 これがデフォルトの設定です。
MB_OKCANCEL メッセージ ボックスには、次の 2 つのボタンが配置されます: OK、キャンセル。
MB_RETRYCANCEL メッセージ ボックスには、次の 2 つのボタンが配置されます: 再試行、キャンセル。
MB_YESNO メッセージ ボックスには、次の 2 つのボタンが配置されます: はい、いいえ。
MB_YESNOCANCEL メッセージ ボックスには、次の 3 つのボタンが配置されます: はい、いいえ、キャンセル。
Flags パラメータで指定可能な値

リファレンスには、さらに「これらの値は、希望する効果を得るため、組み合わせて使うこともできます。」とある。組み合わせるって、どういうこと?

ちなみに、MB_OK と MB_RETRYCANCEL を組み合わせてみると・・・

procedure TForm1.Button5Click(Sender: TObject);
begin
  Application.MessageBox('Do you know Delphi?', '情報', MB_OK or MB_RETRYCANCEL or MB_ICONINFORMATION);
end;

OK と 再試行、キャンセル 三つのボタンが表示されるのかなー? って思ったけど・・・

OK ボタンは表示されませんでした!


これは MB_OK と MB_ABORTRETRYIGNORE を組み合わせても同じで、MB_OK はやはり表示されない。MB_DEFBUTTON4 の役割は何なんだろう???

Web上の資料を調べてみた!

MessageBox 関数 (winuser.h)

https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-messagebox

上記リンク先の説明によれば MB_HELP もあるらしい。

Application.MessageBox('Do you know Delphi?', '情報', MB_OK or MB_HELP or MB_ICONINFORMATION);

実行してみると・・・

OK とヘルプは共存できるようだ

ただ、ヘルプをクリックしても、なんにも起きなかったが・・・
(処理を書いてないんだから、当然と言えば、当然)

しかし、戻り値の中に「ヘルプ」に相当する値がない・・・ような気が。上記の資料には「ユーザーが [ヘルプ ] ボタンをクリックするか F1 キーを押すと、 WM_HELPメッセージが 所有者に送信されます。」とあったけど、これ以上、追いかけると深みにはまりそうな気がするので、ヘルプに関する勉強は後日それが必要となった時にあらためてすることにして、ここではお茶を濁す。

結局、MB_DEFBUTTON4 の果たす役割はわからずじまい。残念!

それから、MessageBox関数には「すべてはい・すべていいえ」のボタンがないようだ。これもまぁ、それが必要になった時、考えることにする。僕が書くプログラムでそれが必要になることは、まず、ないだろう・・・。

あとわかったことは、「Text パラメータの値はメッセージで、必要なら 255 文字以上になっても構いません。 長文メッセージは、メッセージ ボックスでは自動的に改行されます。」ふむふむ、なるほど。

さらに「Caption パラメータの値はキャプションで、ダイアログボックスのタイトル バー上に表示されます。 Captions は 225 文字より長く指定できますが、改行されません。長文キャプションの場合、メッセージ ボックスの幅が広げられます。」とのこと。

5.ダイアログからの戻り値

ダイアログからの戻り値は、次の通り(Delphi 12.0 の VCL リファレンスより引用)

定数意味
mrNone0 なしユーザーが終了する前にデフォルト値として使用される
mrOkidOK ユーザーが[OK]ボタンで終了した
mrCancelidCancelユーザーが[キャンセル]ボタンで終了した
mrAbortidAbortユーザーが[中止]ボタンで終了した
mrRetryidRetryユーザーが[再試行]ボタンで終了した
mrIgnoreidIgnoreユーザーが[無視]ボタンで終了した
mrYesidYesユーザーが[はい]ボタンで終了した
mrNoidNoユーザーが[いいえ]ボタンで終了した
ダイアログからの戻り値

6.まとめ

プログラムからユーザーに対してメッセージを表示する方法は、他にもいろいろあるみたいだけれど、MessageBox関数最大の強みは「すべての OS で利用可能」なこと。

OK と、はい・いいえ、キャンセル の各ボタンが利用できれば十分というのであれば、アイコンが表示されなくなった MessageDlg関数のかわりに MessageBox関数が使える。もちろん、ダイアログにアイコンも表示される。

ただ、これまでに書いてきたプログラムを Delphi 12.0 で修正・更新する際にはMessageDlg関数を、MessageBox関数に変更するという、地道な作業が待ってることだけがちょっと気になるが。

まっ ヒマだから、いいか!

7.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

RAD Studio 12.0にPython4Delphiをインストールする!

追記(20231208)

さらにカンタンな方法がありました!

https://coding-tips-memoranda.com/rad-studio-12-0%e3%81%abpython4delphi%e3%82%92%e3%82%a4%e3%83%b3%e3%82%b9%e3%83%88%e3%83%bc%e3%83%ab%e3%81%99%e3%82%8b%ef%bc%81%ef%bc%88%e3%81%9d%e3%81%ae%ef%bc%92%ef%bc%89/

以下、苦労を伴うインストール方法の記録です(お読みいただく価値のない情報です)。 T_T

2023年11月8日、RAD Studio 12.0(僕にとってはDelphi 12.0)がリリースされた(ようです)。
アップデート・サブスクリプションの支払いを終え(個人で購入しているのは僕くらいだろうが・・・)、届いたメールの製品アップデートリンクをクリックして、最新の更新をチェックしたら、12.0が!

(誰も教えてくれないから、リリースされたこと自体、まったく知りませんでした! T_T )

そういえば・・・RAD Studioのメジャーアップデートは毎年この時期だったような。

あわわわわわわわわわわ ひー!ひー!(驚愕的感動を表現)

早速、Web Installを実行。

Delphi 12.0 のインストールは何の問題もなく、15分くらいで終了(XEの頃は時間がかかったけど)。

11.2 が入っている環境にインストールしたためか(?)、シリアルナンバーの入力なども一切ありませんでした! カンタン。気持ちいい。

続けて、Python4Delphiも最新版(RAD Studio 12.0対応版)をインストール。

以下、その時のメモです!

【もくじ】

1.Python4Delphiの最新版をダウンロードして展開する
2.フォルダ構成を整える
3.Python4Delphiの最新版(RAD Studio 12.0対応版)をインストール
4.ライブラリパスを確認
5.まとめ
6.お願いとお断り

1.Python4Delphiの最新版をダウンロードして展開する

まず最初に、Python for Delphi(P4D)をGitHubから入手してDelphiにインストール。

P4Dの入手先URL https://github.com/pyscripter/python4delphi

Codeをクリックすると表示されるサブメニューのいちばん下にDownLoad ZIPがあるので、これをクリックしてZIPファイルをダウンロードし、任意の場所(フォルダ)に解凍する(ここではダウンロードするフォルダの名前を「P4D」として説明)。

Download ZIPをクリックして最新版を入手する

ダウンロードが完了したら、ダウンロード先フォルダにはコレがあるはず。

python4delphi-master.zipを任意の場所に「P4D」フォルダを作成して、そこへコピペする

P4D」フォルダを作成するのは、できればあまり階層の深くない、絶対に忘れない場所がよいと思います。理由は、後からそこにライブラリパスを通すから。バックアップなど取る時にも、忘れないような場所に作成してください。

zipファイルを右クリックして、表示されるサブメニューの「すべて展開」をクリック。

zipファイルを展開(解凍)

そのまま P4D フォルダの直下に展開(解凍)する。

Pathは敢えていじらずに、そのまま「展開」をクリック

展開(解凍)が完了すると、P4D フォルダの下に「python4delphi-master」フォルダができ、その下に同じ名前でもうひとつ「python4delphi-master」フォルダができる

この中に7匹のヘビがいる。はやく会いたい。

2.フォルダ構成を整える

この時点でフォルダ構成は・・・ちょっとややこしいが、次のようになっている(はず)。

¥任意の場所¥P4Dpython4delphi-masterpython4delphi-master

とりあえず、いちばん下の python4delphi-master フォルダをダブルクリックして開き、中にあるものすべてを CTRL+A で全選択して、CTRL+X で切り取り、ひとつ上の階層の python4delphi-master フォルダ内に CTRL+V(貼り付け)する。

で、いちばん下の階層の python4delphi-master フォルダは不要なので消去(削除)する。

さらに、上の階層の python4delphi-master フォルダの名前を手動で「P4D」に変更(リネーム)する。

これでフォルダ構成は、次のようになる。

¥任意の場所¥P4DP4D

いちばん下の P4D フォルダをダブルクリックして開くと・・・

Install フォルダ内にある「README.md」に、実は重要な情報が書かれている

【README.md】※ 原文のまま

## P4D Installation using [MultiInstaller](https://github.com/pyscripter/MultiInstaller)

Use for Delphi Seattle (10.4) or later to install all packages in one step. 

1. Clone or copy the Python4Delphi git repository to a folder of your choice.  **The setup.ini file assumes that the folder is called "P4D"**.  If you chose to name your folder differently then modify the "Folder" option in setup.ini.
2. Close all Delphi IDEs running.
3. Run MultiInstaller.exe
4. Select the packages you want and press Next
5. In the dialog box specify the _**parent folder**_ of "P4D" (i.e. the folder containing the directory to which you have copied Python4Delphi) and the Delphi target version.  Then press Next to install the components

Google先生、曰く・・・(文字に色付けしたのは僕です)

## [MultiInstaller]を使用したP4Dのインストール(https://github.com/pyscripter/MultiInstaller)

Delphi Seattle (10.4) 以降の場合は、すべてのパッケージを 1 ステップでインストールするために使用します。

1. Python4Delphi git リポジトリを選択したフォルダーにクローンまたはコピーします。 **setup.ini ファイルでは、フォルダーの名前が「P4D」であると想定しています**。フォルダーに別の名前を付けることを選択した場合は、setup.ini の「フォルダー」オプションを変更します。
2. 実行中のすべての Delphi IDE を閉じます。※ コレも重要!な注意点のひとつかと・・・
3. MultiInstaller.exe を実行します。
4. 必要なパッケージを選択し、「次へ」を押します
5. ダイアログ ボックスで、「P4D」の _**親フォルダー**_ (つまり、Python4Delphi をコピーしたディレクトリを含むフォルダー) と Delphi ターゲット バージョンを指定します。次に、「次へ」を押してコンポーネントをインストールします

僕なりの解釈は(間違ってるカモだけど)・・・

Python4Delphi をコピーしたフォルダ名は「P4D」であり(であることを想定しており)、
さらに、インストール時に表示されるダイアログボックスでは・・・

P4D」の _**親フォルダー**_ を指定

つまり、「その親フォルダ(階層がいちばん上の P4D )を指定せよ」

と言っている・・・。

フォルダ構成を README.md の指示通りに整えたところで、

¥任意の場所¥P4DP4D¥Install フォルダを開き、

そこにある MultiInstaller.exe をダブルクリックして実行する。

Install フォルダにある MultiInstaller.exe をダブルクリック

ちなみに、拡張子md は、「Web 用のドキュメントの作成によく使用される、読み書きしやすいように設計されたプレーンテキスト」に使う拡張子だそう。

ちなみに「プレーンテキスト」は、「文字だけで構成され、レイアウト情報や装飾情報などを持たないデータのこと」だそうで。

勉強になりますー。

3.Python4Delphiの最新版(RAD Studio 12.0対応版)をインストール

こうしてインストール前の最大の難関?を乗り越え、早速、Python4Delphi をインストール。

Install フォルダにある MultiInstaller.exe をダブルクリック(再掲)

次の画面が表示される。

フォルダの選択ダイアログ

Select Destination directory to install all the component packages. ・・・

こちらもGoogle先生曰く、

「すべてのコンポーネント パッケージをインストールするには、宛先ディレクトリを選択します。」

どうも、この、「宛先」という訳がピンとこないけど・・・。

まぁ、「宛先」は「参照元」に読み替えて・・・。

それが、先ほどの「README.md」に書かれていた「Python4Delphi をコピーしたディレクトリを含むフォルダー」・・・つまり、「P4D」フォルダなんだろうな・・・ みたいな・・・

ってか、もっと正直に言うと・・・、RAD Studio 12.0 をインストールしたから、唯一、僕が必要とするサードパーティー製コンポーネント Python4Delphi も入れなきゃって思って、前回の(11.2 への)インストール作業後、大切に保存しておいた P4D¥Installフォルダ内の MultiInstaller.exe を起動したら・・・

RAD Studio 12 Athens がインストール先の候補として出てこない!

つまり、この MultiInstaller.exe は RAD Studio 12 Athens のインストールパスを拾って・・・「ない」。

このダイアログを見たとき、一瞬、(もうダメだ・・・)と思ったのですが、その直後、このインストーラー自体が1年前のものだったことを思い出し、・・・だとすれば、RAD Studio 12 Athens が表示されなくて、むしろ当然・・・。ここで初めて Python4Delphi も最新版が必要だと気づき・・・

さらに、オプションボタンがこのダイアログに「1つしかない」意味まで見えた気が・・・

(オプションボタンだから、インストール対象としてパスを通すのは、1バージョンに限定ってことなんだろうけれど・・・)

(それよりも・・・、ダイアログのCompile packages ~の余白が有り余ってるのは、RAD Studio のメジャーバージョンアップを見越して、後からボタンを追加できるよう、予め余裕を持って設計したから?)

(・・・もし、そうなら12.0対応版があるに違いない。いや、きっとある!)

あわてて GitHub へ行って12.0 対応版の有無を確かめたというのが事の真相。

思った通り、GitHub の Python4Delphi は、12.0のリリースに合わせて最新版にアップデートされてました・・・。作者の方に心から感謝!

Go To 「3.Python4Delphiの最新版(RAD Studio 12.0対応版)をインストール

これで無限Loop に。Blogまでスパゲッティ化しちゃった・・・。

↑ コレは古い時代のプログラマーにしか、通じない言葉かな?

取り敢えず、無限Loopはなんとかして乗り越えたコトにして・・・

宇宙のはじまりだって、トンネル効果が起きた時、虚数時間が流れていて、上り坂が下り坂になった・・・みたいな話を、聴いたような・・・。聴かなかったような。で、宇宙って、通れないはずの壁から果たして沁み出すものなんだろーか。

ハイゼンベルクさんは、連合軍の科学者たちのことを、どう思っていたんだろう・・・

RAD Studio 12.0 対応版のP4D付属 MultiInstaller.exe を起動して表示されるフォルダの選択ダイアログは、前掲の通り。

Browseボタンをクリックして・・・

Compile packages ~の欄には RAD Studio 12 Athens が増えましたが、欄の上下に「まだまだ余裕」があります。これを見て、先ほどの予感は大きく自信を得て・・・「これはつまり、今後数十年以上先までDelphiのメジャーバージョンアップが続々と行われることを見通して、必要十分と思われる余白を予め用意した先見の明溢れる非常に大胆な先進的設計である」という確信に変わりました。

是非、そうであって欲しい・・・と、心から願っています!

ダイアログがこのままの大きさでも、

1、2、3・・・と、近未来、確実にそこに入るであろうオプションボタン位置を予想してみると、Delphi のメジャーバージョンアップにあと10回は余裕で対応できそうです*(^_^)*♪

いいぞ。さすが、P4D!

こういう応援の仕方もあったのか・・・

こんどから、

僕も真似しよー!!

解凍先フォルダの階層Topにある P4D フォルダを指定
Compile packages and install on IDEにチェックして、RAD Studio 12 Athensを選択

あとは Next ボタンをクリックしてインストーラーにすべておまかせで、P4Dをインストールするだけ。
無事完了すれば、次のようなダイアログが表示される(画像は前バージョンのもの)。

作業の記録をとり忘れたので、これは 11.2 に P4D をインストールしたときの画像

最後に Finish をクリックしてインストール作業終了。

Delphi 12.0 のIDEを起動して、パレットを確認。

7匹のヘビを無事発見。

4.ライブラリパスを確認

Delphi のIDEを起動し、「ツール」→「オプション」→「言語」→「Delphi」→「ライブラリ」の順にクリックして下の画面を表示。

プラットフォームを選択して、ライブラリパスの「…」ボタンをクリックする

ライブラリパスの一覧が表示されるので、そのいちばん下に P4D へのパスの設定があることを確認する(パスはインストール時に自動で設定されるようだ)。

Library パスの一覧の下から3つが P4D へのパス(自動で設定される)

上の画面では 「Windows 32ビット」 のプラットフォームに対する設定を確認している。念のため、「Windows 64ビット」 のプラットフォームに対しての設定も確認する。

プログラムのコンパイルを実行すると、Delphiはいちばん最初にプロジェクトファイル(.dproj)のあるフォルダ(ここはパスが通っているから登録は不要)を検索し、必要なユニットファイル等の有無を確認。もし、そこに必要なファイルがなければ、この画面に登録したライブラリパスを検索するようだ。

5.まとめ

(1)RAD Studio 12.0 のリリースに合わせ、Python4Delphi もアップデートされていた。
(2)Python4Delphi のインストールは専用の「MultiInstaller.exe」で実行する。
(3)Python4Delphi のデータは「¥任意のフォルダ¥P4D¥P4D」フォルダ内に置く。
(4)インストール後、念のため、ライブラリへのパスが設定されていることを確認する。

6.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

浮動小数点

学生の頃から、2進数が苦手だった・・・。
浮動小数点も、本当のところはよくわからなかった・・・。

それがわからなくても、日常生活で苦労することは皆無だったし、
PCに触れている時でも、これと言って計算に困るような出来事もなかった。

そう、これまでは・・・

ただ、職場が変わって、状況が変化。
苦手だった2進数や、浮動小数点を、ほっとけなくなっちゃった・・・。

もくじ

1.10進数を2進数に変換
2.浮動小数点
3.0.1(10)を2進数に変換
4.まとめ
5.お願いとお断り

1.10進数を2進数に変換

これが苦手で、小テストとかあって、本当に困ってるひとが、もし、いたら、
僕なりに苦しい中から見出した絶対忘れない変換方法をお伝え出来たらと思って、
この記事を書いています。

まず、僕は、よく見るコレが『なぜか苦手』でした(今でも)。
例えば7(10)を2進数に変換する場合、

10進数を2進数に変換(その1)

答えは、図の矢印の順に読んで、0111(2) 。だけど・・・
この書き方。すごく、思い出しにくい。
特に焦ったりすると、何か、どこかで混乱して、僕は必ず間違えてしまう。

自分なりに考えて出した「結論」は・・・この書き方、普段ほとんど使わないから・・・かな? みたいな

そこで、小学校以来慣れ親しんだ書き方がよかろうと思い、
自分では、より『素直』に思えるカタチに書き方を変更。
結果、これなら絶対間違えないと思えました。
自分的には、この書き方の方が、なぜかとても安心感があります。なんでかな?

10進数を2進数に変換(その2)

自分に対して、自信が持てない本当の理由は、わかってます。
答えを出す方法だけが知りたくて、『なんで2で割り算するのか?』考えたことがなかったからです。
つまり、『ほんとうのこと』から、僕は目を反らし続けてきた・・・から。

理由はいくらでもあげられます。

めんどくさかったから
考えたくなかったから
試験に通ればそれでよかったし
そんなこと、どうでもよかったカラ

でも、とうとう、ここで、それは通用しなくなりました。
『2で割り算する』、その理由を僕は説明しなければならない。

誰もがわかるように・・・。

とりあえず、わかりやすい10進数で考えてみます。
10進数の基数は「10」、では基数で割り算するってどういうことなのか?

基数で割り算した余りが、元の10進数の一の位、十の位、百の位、千の位と一致します。
つまり、各「位(くらい)」の数を求めていたわけですね!

2進数の場合、基数は「2」ですから、基数で割り算すれば、余りは「0」か、「1」のいずれかになります。これが、その桁の2が「あるか・ないか」を教えてくれるわけです。

例えば、10進数の「123」を2進数に変換する場合、


01111011(2)の0と1はそれぞれ、その桁の2が「あるか・ないか」だから

電卓の種類を「プログラマー」に変更して、ラクして確認。

こんなイイモノがあったんだ!

手計算でも確認(これが16進数で桁数が多かったら、挫けそうだなー)。

2進数でも、余りは、そのまま、各位(くらい)の数になりました。
よかった。よかった。

2.浮動小数点

数値のどこまでが信頼できる桁であるのかを表すのに、有効数字を用いますが、例えば


有効数字の 1.234 の部分を「仮数(かすう)」というそうです。

2進法では、次のような場合を考えると、


2倍すると1桁、位が上がりますから、2の2乗倍=4倍すれば2桁、位(くらい)が上がることになります。これを小数点の位置で言えば、2をかけるたびに、小数点の位置が右へ移動するわけです。

(だから「浮動小数点」って言うのかぁ・・・)

ここでの仮数は「1.0111」ですが、実は仮数には重要な決まりがあって、「整数部分を1桁とし、そこに0以外のいちばん上の位の数を置く」のだそうです(⇨ IEEE754 という方式に準拠した場合?)。

・・・と、いうことは、2進数なら「0」と「1」しかないから、仮数の整数部分は必ず「1」になることになります。

64ビットの浮動小数点数(2進数の場合)では、符号ビットは1ビット、指数部は11ビット、仮数部は52ビットで表わされることが多いそうで、符号は+とーのどちらかだから1ビットでOKとして、仮数部ではその整数部分「1」を省略してしまうとのこと。なんで?

仮数部で整数部分の「1」を省略する理由がわからなかったので調べてみました。結果を知って納得。

整数部分の1を省略すれば、仮数部を1ビット増やすことができるので、浮動小数点形式の精度を向上させることができる。

(すごーい!)

仮数の整数部分を必ず「1」とすることで、そんなことも可能になるわけですね!

3.0.1(10)を2進数に変換

すーぱー 苦しんだのが、コレです。

10進数の小数を2進数に変換するなんて、もう長いことやってない。
やったことがあるとしても、15年以上前です。その記憶の欠片すら、残ってません。

表し方を調べてみると・・・、整数でやったコレを、そのまま2のマイナス1乗みたく、右側へ拡張して小数点以下を表現するとのこと。


そー言えば、はるか、むかし。
なんで2のゼロ乗が1になるのか、さっぱりわからず、悩んだことがあったよーな・・・。
わかってみれば、カンタンだったけど。


この関係を含めて10進数の小数を位(くらい)の数として表せば、


2進数でも、考え方は同じ。
違うのは、それがいくつあるか? ではなく、単に「ある」か・「ない」か が、「ある」=「1」、「ない」=0で示されること。あとあと、これがかなり重要な理解のポイントになります!


特に、0.X とした場合の小数点以下の部分 は(1の位が0であることを明示して表せば)、


この関係がそのまま使えれば、話はカンタン。
例えば10進数の 0.625 を2進数に変換したい場合、0.625 が 0.5 と 0.125の和であることに気付けば、


んじゃ、10進数の 0.1 を2進数に変換するには?
理解のために、全桁「1」で数値が「ある」ものとし、位(くらい)の数を書き出して考えます・・・


2進数で計算すると、このような誤差が必ず生まれてしまう!

だから、0.1 に少しずつ近づくように(0.1 を超えないように)、足し算可能な、よりちいさな数を求め、どこまでもこれを繰り返して(=循環する理由)行くわけですか・・・

(それはわかったけど、0.1(10) を2進数に変換するわかりやすい方法は・・・)

Google先生に、いろいろたずねても、コレだぁ!・・・みたいな答えは教えてくれない!!

それでも、いろんなWebサイトさんの断片的な情報を集めて、ようやく変換方法だけはわかりました☆

それが、こちら(10進数の 0.1 を2進数に変換)

基数で割り算でなく、基数を「掛け算」!


なんで、これで変換できるのか?
次の式を書き出して、ひたすら考えます・・・(この場合、左辺のxは もちろん 0.1)。


10進数の 0.1 を2進数に変換するとき、なんで割り算でなく、基数を掛け算してるのか? まず、それがさっぱりわからないけれど、ここでまず思い出したことは、これは2進数表現だから、an は必ず0か1のいずれかになるということ。

つまり、an が「1」ならその部分は数値が「あり」で、an が「0」なら「ない」わけです。

一歩、前進。

問題は基数を「掛け算」する理由。

基数を掛け算する・・・、つまり、2倍するってことは、意味的にはナニをしているのかというと、2倍して1桁、位を上げていることになる。おそらく、この部分に重大な意味があるような気が・・・。

てか、ナンで桁を上げる必要があるのか?

それがアタマにこびりついて、離れません。

(あぁ ナニを見ても0と1に見える・・・)

誰か、教えてくれないかなー

ブラウザのタブを切り替えながら、既に何度も眺めたWeb上の情報にもう一度目を通します。

(なにか、見落としてること、あるんじゃないかなー)

あるWebサイトにあった次の文がなぜかキラキラ輝いて見えます。でも、なんで輝いて見えるのか、それがわかりません。

2進数で表された1.0と、10進数で表された1.0は、数の重みが等しい(同じ数!)

なんか、ものすごく大切なことを伝えてくれている気がするのですが、その「ものすごく大切なこと」がナンなのか? それがどうしてもわかりません!

0.1 かぁ・・・ これは10進数の小数で・・・

それを2進数にしたいんだよなぁ・・・


左辺の 0.1 は、10進数の領域にあって・・・、右辺は、2進数の領域にあると考えれば・・・

右辺のan は絶対に「1」か「0」のどちらかになる(2進数だから)。

「1」なら「数値あり」で、「0」なら「ない」。

で・・・

1 は2倍すれば、一の位に現れる・・・

2 は22=4倍すれば、一の位に現れる・・・

3 は23=8倍すれば、一の位に現れる・・・

2進数も、10進数も、一の位だけは・・・1が意味合い的に同じ「ひとつだけあるの1(イチ)」。

・・・って、コトは、

二つの異なる領域をイコールで結ぶために必要な、その条件が一の位・・・

そうか、一の位じゃないと・・・「0」か、「1」かが、きっと 見えない んだ!

だから、どんどん2倍して、
1、a2、a3・・・が、次々に一の位になるように、桁を上げるんだ☆

一の位になって初めて、それが「0」なのか、「1」なのかが 見える んだ!!

あー☆ わかったカモー!!

だから、(上の計算で)基数を掛け算した結果、整数部に「1」が出てきた時は、それが「ある」ことさえわかればイイから、その存在を無視して、小数部だけを基数倍(2倍)してるんだ!

コレ、考えた人、天才だ☆

4.まとめ

今回は内容が多岐にわたり、コレが最も適切かと・・・思われます。

5.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

FireMonkeyのMessageDialog

FMX事始め

1.FMXでMessageDialogを表示する
2.TMsgDlgTypeに指定できる値はVCLと同じ
3.表示できるボタン
4.表示できないボタン
5.ダイアログ右上の閉じるボタンの挙動
6.まとめ
7.お願いとお断り

1.FMXでMessageDialogを表示する

いろいろな事情からFMXプラットフォームで、あるプログラムを書くことになった。使い慣れたVCLと違って、FireMonkeyはずっと以前に一度だけWAVファイルの再生プログラムを作った時触れたことがあるだけで、まともに触るのは今回が初めて。

最初にいちばん困ったのはユーザーへのメッセージの出し方。なんでかわからないけれど、普通にShowMessageすると、その直後、FMXプラットフォームでは、結構な頻度でエラーが発生する気が・・・。

だから、最初に書いたデータベース接続のプログラムは、極力ShowMessageを使わない方向で書いたんだけど、2作目のテキスト入力練習プログラムではそうも行かず、良い機会だと思ってShowMessageより見た目が華やかなMessageDialogの正しい使い方を調べてみた。

思った以上に情報が少ない気がしたので、学んだことを備忘録として、まとめておく。

まずは、iマーク付きのMessageDialogの出し方。

implementation

uses
  FMX.Platform, FMX.DialogService;

{$R *.fmx}

procedure TForm1.Button1Click(Sender: TObject);
var
  ASyncService:IFMXDialogServiceASync;
begin
  //mtConfirmationだとBeep音が鳴らないが、mtInformationだとBeep音が鳴る
  if TPlatformServices.Current.SupportsPlatformService (IFMXDialogServiceAsync,
    IInterface(ASyncService)) then
  begin
    TDialogService.MessageDialog('Do you know Delphi?',
      TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK], TMsgDlgBtn.mbOK, 0,
      procedure(const AResult: TModalResult)
      begin
        if AResult = mrOK then
        begin

        end;
      end);
  end;
end;

実行すると・・・

ここにたどり着くまで、結構長かった・・・
ほんとに、ようやくって感じ。

調べてわかったことは・・・

var
  ASyncService:IFMXDialogServiceASync;

・・・と宣言するためには、

uses
  FMX.Platform;

uses に FMX.Platform が必要で、さらに、サポートの有無を調査するif文の・・・

if TPlatformServices.Current.SupportsPlatformService (IFMXDialogServiceAsync,
    IInterface(ASyncService)) then

TPlatformServices も FMX.Platform を参照している。

で、本命の MessageDialog を表示するには、さらに・・・

uses
  FMX.Platform, FMX.DialogService;

uses に FMX.DialogService も追加しなければならない。

2.TMsgDlgTypeに指定できる値はVCLと同じ

上のユーザーへの情報提供(Info)に加えて、ユーザーに確認する場合は、

TMsgDlgType.mtConfirmation

ユーザーに警告。

TMsgDlgType.mtWarning

ユーザーにエラーを報告。

TMsgDlgType.mtError

mtCustomってのもあったけど・・・

TMsgDlgType.mtCustom
画像は、何も出てこなかった・・・。
キャプションもProject1(アプリケーション名)になってる・・・。

実質、「情報提供・確認・警告・エラー」の4つ型があり、これはVCLと変わらない。

3.表示できるボタン

ユーザーの応答が「OKボタン押し下げのみ」であれば、MessageDialogの最後の引数を別手続きにして、それを呼び出す形にすればいいのかと・・・

  private
    { private 宣言 }
    procedure MsgDlgProc(const AResult: TModalResult);

として、Shift+Ctrl+Cで手続きを作成。

procedure TForm1.MsgDlgProc(const AResult: TModalResult);
begin
  //何もしない手続き

end;

応答が「OK」のみの場合は、これを呼び出し。

procedure TForm1.Button2Click(Sender: TObject);
var
  ASyncService:IFMXDialogServiceASync;
begin
  if TPlatformServices.Current.SupportsPlatformService (IFMXDialogServiceAsync, IInterface(ASyncService)) then begin
    TDialogService.MessageDialog('Do you know Delphi?',
      TMsgDlgType.mtInformation, 
      [TMsgDlgBtn.mbOK], TMsgDlgBtn.mbOK, 0, MsgDlgProc);
  end;
end;

コードが短くなって、なんとなくすっきりした。

でも、「はい」・「いいえ」・「キャンセル」のようにボタンを複数表示するとそうもいかない。

procedure TForm1.Button3Click(Sender: TObject);
var
  ASyncService:IFMXDialogServiceASync;
begin
  if TPlatformServices.Current.SupportsPlatformService (IFMXDialogServiceAsync, IInterface(ASyncService)) then
  begin
    TDialogService.MessageDialog('Do you know Delphi?',
      TMsgDlgType.mtInformation, [TMsgDlgBtn.mbYes, TMsgDlgBtn.mbNo, TMsgDlgBtn.mbCancel], TMsgDlgBtn.mbYes, 0,
      procedure(const AResult: TModalResult)
      begin
        if AResult = mrYes then
        begin
          ShowMessage('Goooooooood!');
        end;
        if AResult = mrNo then
        begin
          ShowMessage('No Good!');
        end;
        if AResult = mrCancel then
        begin
          ShowMessage('Cancel');
        end;
      end);
  end;
end;

case文でもよいようだ。

procedure TForm1.Button3Click(Sender: TObject);
var
  ASyncService:IFMXDialogServiceASync;
begin
  if TPlatformServices.Current.SupportsPlatformService (IFMXDialogServiceAsync, IInterface(ASyncService)) then
  begin
    TDialogService.MessageDialog('Do you know Delphi?',
      TMsgDlgType.mtInformation, [TMsgDlgBtn.mbYes, TMsgDlgBtn.mbNo, TMsgDlgBtn.mbCancel], TMsgDlgBtn.mbYes, 0,
      procedure(const AResult: TModalResult)
      begin
        case AResult of
          mrYes:ShowMessage('Goooooooood!');
          mrNo:ShowMessage('No Good!');
          mrCancel:ShowMessage('Cancel');
        end;
      end);
  end;
end;

caseのリストが表す値は、case文内で一意、部分範囲またはリストの重複がなければ昇順とかリストの並びは関係ないようだ。また、このようによく使用されるボタン値は、セットになった定数として用意されていて、例えば上の場合は次のように指定できる。

TMsgDlgType.mtInformation, mbYesNoCancel, TMsgDlgBtn.mbYes, 0,
こっちの方がカンタン!

embarcaderoさんのWebサイトでは、TMsgDlgBtnは種類がたくさん紹介されていて、

定数意味
mrNone0結果なし。ユーザーがフォームを終了するまでのデフォルト値として使用されます。
mrOkidOK = 1ユーザーは[OK]ボタンでフォームを終了しました。
mrCancelidCancel = 2ユーザーは[キャンセル]ボタンでフォームを終了しました。
mrAbortidAbort = 3ユーザーは[中止]ボタンでフォームを終了しました。
mrRetryidRetry = 4ユーザーは[再試行]ボタンでフォームを終了しました。
mrIgnoreidIgnore = 5ユーザーは[無視]ボタンでフォームを終了しました。
mrYesidYes = 6ユーザーは[はい]ボタンでフォームを終了しました。
mrNoidNo = 7ユーザーは[いいえ]ボタンでフォームを終了しました。
mrCloseidClose = 8ユーザーは[閉じる]ボタンでフォームを終了しました。
mrHelpidHelp = 9ユーザーは[ヘルプ]ボタンでフォームを終了しました。
mrTryAgainidTryAgain = 10ユーザーは[やり直し]ボタンでフォームを終了しました。
mrContinueidContinue = 11ユーザーは[続行]ボタンでフォームを終了しました。
mrAllmrContinue + 1(12 つまり $C)ユーザーは[すべて]ボタンでフォームを終了しました。
mrNoToAllmrAll + 1(13 つまり $D)ユーザーは[すべていいえ]ボタンでフォームを終了しました。
mrYesToAllmrNoToAll + 1(14 つまり $E)ユーザーは[すべてはい]ボタンでフォームを終了しました。
https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.StdCtrls.TCustomButton.ModalResultより引用

さらに、セットになった定数が5つあるとのこと。

定数意味
mbYesNoCancelmbYes、mbNo、および mbCancel
mbYesAllNoAllCancelmbYes、mbYesToAll、mbNo、mbNoToAll、および mbCancel
mbOKCancelmbOK および mbCancel
mbAbortRetryIgnorembAbort、mbRetry、および mbIgnore
mbAbortIgnorembAbort、mbIgnore
https://docwiki.embarcadero.com/Libraries/Alexandria/ja/Vcl.Dialogs.TMsgDlgBtnより引用

4.表示できないボタン

僕のPCだけ、そうなのかもしれないけど。中には表示できないボタンが・・・。例えば、

procedure TForm1.Button7Click(Sender: TObject);
var
  ASyncService:IFMXDialogServiceASync;
begin
  if TPlatformServices.Current.SupportsPlatformService (IFMXDialogServiceAsync, IInterface(ASyncService)) then
  begin
    TDialogService.MessageDialog('Do you know Delphi?',
      TMsgDlgType.mtInformation,[TMsgDlgBtn.mbRetry],TMsgDlgBtn.mbRetry,0,
      procedure(const AResult: TModalResult)
      begin
        case AResult of
          mrOK:ShowMessage('OK!:了解');
          mrCancel:ShowMessage('Cancel:取消');
          mrAbort:ShowMessage('Abort:中止');
          mrRetry:ShowMessage('Retry:再試行');
          mrIgnore:ShowMessage('Ignore:無視');
          mrYes:ShowMessage('Yes:はい');
          mrNo:ShowMessage('No:いいえ');
          mrClose:ShowMessage('Close:閉じる');
          mrHelp:ShowMessage('Help:要援助');
          mrAll:ShowMessage('All:すべて');
          mrNoToAll:ShowMessage('NoToAll:すべていいえ');
          mrYesToAll:ShowMessage('YesToAll:すべてはい');
        else
          //ないと思うけど、
          ShowMessage(IntToStr(AResult));
        end;
      end);
  end;
end;

ボタンに mbRetry を指定しても、上の手続きを実行すると表示されたダイアログは・・・

普通のOKボタン!

で、OKを押し下げ。・・・ると

なんでかなー?

でも、次のように指定すると、

TMsgDlgType.mtInformation, [TMsgDlgBtn.mbCancel,TMsgDlgBtn.mbRetry]
指定した順番と並びが逆だけど、「再試行」ボタンが表示された!

で、「再試行(R)」を押し下げ。・・・ると

表示できる場合とできない場合があるらしい。つまり、これはダイアログに表示可能なボタンの設定(組み合わせ)を FMX の MessageDialog は内部的に持っているということ? それから、キャンセルボタンは必ず右側へ設置される?・・・から、ボタンの表示される順番もまた、決まっているという理解でいいのかな?・・・みたいな。

他にも、mbAbortRetryIgnore を指定して、デフォルトで選択状態にするボタンに mbRetry を指定しても・・・

TMsgDlgType.mtInformation, mbAbortRetryIgnore, TMsgDlgBtn.mbRetry, 0,
なぜか「再試行」ボタンが表示されない!

しかも、ダイアログ右上の「閉じる」ボタンが押せなくなってる(勝手にEnabled?がFalseに設定されてしまう)。これは、キャンセルがないから、閉じるボタンはその必要がないという意味に思えてくる・・・。だから、閉じるボタンの無効化も、VCLならその方法が紹介されているんだけど、FMXでの情報は見当たらないのか・・・

実は、この記事を書こうと思ったのは、この閉じるボタンをクリックした時の戻り値が何なのか、どんなに調べても(僕が調べた範囲では)見つけることが出来なかったので、実験してみた結果を記録しておこうと思ったことがきっかけというか、はじまり。

FMX の MessageDialog を設計した人の気持ちがだんだん、わかってきた!

5.ダイアログ右上の閉じるボタンの挙動

実験結果から見えてきたこと。それは MessageDialog の閉じるボタンは、そのクリックの可否がダイアログに表示するボタンの組み合わせによって、内部的に制御されているんじゃないか?ってこと。

まず、OKボタンのみの場合、

procedure TForm1.Button9Click(Sender: TObject);
var
  ASyncService:IFMXDialogServiceASync;
begin
  if TPlatformServices.Current.SupportsPlatformService (IFMXDialogServiceAsync, IInterface(ASyncService)) then
  begin
    TDialogService.MessageDialog('Do you know Delphi?',
      TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK], TMsgDlgBtn.mbOK, 0,
      procedure(const AResult: TModalResult)
      begin
        case AResult of
          mrOK:ShowMessage('OK!:了解');
          mrCancel:ShowMessage('Cancel:取消');
          mrAbort:ShowMessage('Abort:中止');
          mrRetry:ShowMessage('Retry:再試行');
          mrIgnore:ShowMessage('Ignore:無視');
          mrYes:ShowMessage('Yes:はい');
          mrNo:ShowMessage('No:いいえ');
          mrClose:ShowMessage('Close:閉じる');
          mrHelp:ShowMessage('Help:要援助');
          mrAll:ShowMessage('All:すべて');
          mrNoToAll:ShowMessage('NoToAll:すべていいえ');
          mrYesToAll:ShowMessage('YesToAll:すべてはい');
        else
          ShowMessage(IntToStr(AResult));
        end;
      end);
  end;
end;


ダイアログ右上の「×」をクリックすると表示されたのは・・・

AResult は mrOKだった!

つまり、OKをクリックするしか、選択肢がない(未来をプログラマ自身が選択した)のだから、閉じるボタンが押された時の戻り値もmrOKでよい・・・ということか!

次に、表示するボタンを「OK」と「キャンセル」にして、デフォルト選択ボタンは「キャンセル」に指定して、再度実行。

TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK, TMsgDlgBtn.mbCancel], TMsgDlgBtn.mbCancel, 0,

右上の「×」をクリックすると・・・

AResult は mrCancel だった!

思った通り、mrCancel が設定されていた! プログラマが「キャンセルという選択肢を未来に与えた」んだから、閉じるボタンが押された時は「キャンセル」と判断してよい・・・ということ?

デフォルト選択ボタンをOKに変えてみた・・・

TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK, TMsgDlgBtn.mbCancel], TMsgDlgBtn.mbOK, 0,

右上の「×」をクリックすると・・・

AResult はmrCancel!

思った通りだ。

ボタンを「はい」・「いいえ」・「キャンセル」の3つにし、デフォルト選択を「はい」に指定して実験・・・

TMsgDlgType.mtInformation, [TMsgDlgBtn.mbYes, TMsgDlgBtn.mbNo, TMsgDlgBtn.mbCancel], TMsgDlgBtn.mbYes, 0,

右上の「×」をクリックすると・・・

AResult は mrCancel!

やっぱり、mrCancelが戻り値に設定されている!

ならば、ボタンを「はい」・「いいえ」の2つだけにすると、閉じるボタンは使用不可になるはずだ・・・。だって、プログラマの意向として、未来に「キャンセル」という選択肢は与えられていないから!

実際に動かして確認。

TMsgDlgType.mtInformation, [TMsgDlgBtn.mbYes, TMsgDlgBtn.mbNo], TMsgDlgBtn.mbYes, 0,
思った通り「×」はクリックできない!

僕の中に生まれた予測は、ここで「確信」に変わった!

これはVCLのMessageDlgでも同じなんだろうか?
今度、実験だ。

6.まとめ

(1)OKボタンのみ設置したダイアログでは、閉じるボタンクリックでmrOKが返る。
(2)ダイアログにキャンセルボタンを設置した場合は、閉じるボタンもクリック可能。
(3)キャンセルボタンがある場合、閉じるボタンクリックで返る値はmrCancelになる。
(4)キャンセルボタンがない場合、閉じるボタンはクリックできない。
(5)ボタンの組み合わせは内部的に不可もある(不可でもエラーにはならない)。

7.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

範囲チェックエラーが出た時は?

{$R-}で範囲チェックさせない!

Delphiで、画像をグレースケール変換するプログラムを作成。実行すると、

表示されたエラーメッセージ

プログラムのコードは、次の通り。
Image1に表示した画像をグレースケールに変換してImage2に表示するというモノ。

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, Jpeg,
  Vcl.ComCtrls;

type
  TForm1 = class(TForm)
    Image1: TImage;
    Image2: TImage;
    Button1: TButton;
    StatusBar1: TStatusBar;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

function CreateGrayScalePalette(Tone:Byte): HPALETTE;
var
  Palette: ^TLogPalette;
  i: Integer;
begin
  GetMem(Palette, SizeOf(TLogPalette) + SizeOf(TPaletteEntry) * Tone );
  Palette^.palNumEntries:=Tone+1;
  Palette^.palVersion:=$0300;
  for i := 0 to Tone - 1 do begin
    Palette^.palPalEntry[i].peRed:= Tone - i;
    Palette^.palPalEntry[i].peGreen:= Tone - i;
    Palette^.palPalEntry[i].peBlue:= Tone - i;
    Palette^.palPalEntry[i].peFlags:= 0;
  end;
  Result:=CreatePalette(Palette^);
  FreeMem(Palette);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  X, Y: Integer;
  Bmp: TBitmap;
  P: PByte;
begin
  Bmp := TBitmap.Create;
  try
   Bmp.Assign(Image1.Picture.Bitmap);
   Bmp.PixelFormat := pf8bit;
   Bmp.Palette := CreateGrayScalePalette(255);
   Image2.Picture.Bitmap := Bmp;
  finally
   Bmp.Free;
  end;
  Image2.Width:=Image2.Picture.Bitmap.Width;
  Image2.Height:=Image2.Picture.Bitmap.Height;
  Image2.Visible:=True;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  jpg: TJPEGImage;
begin
  StatusBar1.SimplePanel:=true;
  // TJPEGImageオブジェクトをインスタンス化
  jpg := TJPEGImage.Create;
  try
    // Jpegファイル読み込み
    jpg.LoadFromFile('Image.jpg');
    // Image1に割り当てる
    Image1.Picture.Bitmap.Assign(jpg);
    Image1.Width:=Image1.Picture.Bitmap.Width;
    Image1.Height:=Image1.Picture.Bitmap.Height;
    //StatusBar1.SimpleText:=IntToStr(Image1.Width)+'/'+IntToStr(Image1.Height);
  finally
    // TJPEGImageオブジェクトを破棄
    jpg.Free;
  end;
end;

end.

グレースケール変換実行のボタン(Button1)をクリックすると・・・
このButton1Click手続き内で呼び出しているCreateGrayScalePalette関数でエラーが発生。

ブレークして確認すると、エラーになるのはココ。

でも、なんでエラーになるのか、わからない・・・

Google先生に訊くと、次のような情報を発見。

[Delphi?][ネタ]透明に見えるパターンを描く

https://qiita.com/pik/items/25276e49fb131425db07

早速、範囲チェックさせないコンパイラ指令 {$R-} を追加。

ナニがどうして、そうなるのか?
原因も、理由も、皆目わからないけれど・・・

範囲チェックエラーは出なくなりました!

範囲チェックを実行しないというコンパイラ指令 {$R-} は知りませんでした。
同じ理由で困ってる方もいるかもしれないと思い、記録だけUpしました。

なお、画像のグレースケール化にあたっては、次のWebサイト様にあった情報を使わせていただきました。24bitのフルカラー画像を256階調のモノクロ画像に変換(グレースケール変換)する処理を行う際に役立つ情報が、そのアルゴリズムも含めて、多数紹介されています。

カラー画像をモノクロ画像に変換

http://rakasaka.fc2web.com/delphi/grayscale.html

また、上のWebサイト様で紹介されている配列要素を動的に確保する必要のない、Delphiが独自に定義しているTMaxLogPalette構造体を使用したCreateGrayScalePalette関数を利用した場合は、範囲チェックエラーは発生しませんでした。

お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

矩形検出器を改良

今までのアルゴリズムで僕の矩形検出器がユーザーに提示する「次に採点する解答欄候補」の順番は、だいたい、こんなイメージ・・・

これまでの僕のアルゴリズムでは、実際の採点順とは、まるで違う順番で
「次の採点候補」とする解答欄をユーザーへ提示してしまう・・・

今回、ほぼ採点する順番の通りに「次の採点候補」の解答欄を赤枠で囲んでユーザーに提示できるよう、解答欄矩形の座標を採点順に並べ替えるアルゴリズムを改良。その結果、矢印キー押し下げ時の「次の採点候補とする解答欄座標」を示す赤枠矩形の動きのイメージは次のようになった。

こんな僕の書いた稚拙で、頼りないプログラムでも、喜んで使ってくださる方がいる。
こんなに重たい事実はない。

プログラムが良くなることは、きっと・・・、僕自身が良くなることだ。
そう思いつつ、遅ればせながら、矩形検出器のアルゴリズムをようやく改良できた。

(矩形検出の成功率は100%で、OpenCVの性能は最高!です)

解答用紙の様式パターンの研究がまったく足りていなかったことが今回の改良が必要になったいちばんの原因。

開発の最初の段階で、解答欄矩形を余すところなく認識出来て、舞い上がってしまった自分が、幼かったんだなー。

【もくじ】

1.僕のアルゴリズムの問題点
2.解答欄をブロック化して認識処理を実行
 【間違えポイント①:範囲を指定して画像を切り出す】
 【間違えポイント②:OpenCVのfilenameはPAnsiChar型】
3.まとめ
4.お願いとお断り

1.僕のアルゴリズムの問題点

手書き答案をスキャンして得た画像データから同一設問の解答欄のみを抽出して一覧表示し、採点後、返却用答案画像に採点結果を書き戻すプログラムを書いた。

その際、スキャンした答案画像の解答欄を自動認識する矩形検出器も作成。採点プログラムに同梱して配布。同僚に使ってもらったのだけれど・・・。

解答用紙の解答欄が複数列(?)存在するような形式の解答用紙では、問題が発生。

それは、どんな問題かと言うと・・・

例えば、次のような横書き形式の解答用紙であった場合に、Myプログラムで矩形検出を実行すると・・・

検出した矩形データ(座標群)のうち、最も左上の矩形を最初に赤枠(ラバーバンドで囲って)表示、ユーザーが解答欄であるか・どうかを判定(選択)、座標自体はMemoに数値で一覧表示してあるから ↓ 矢印キーで次の矩形へ・・・という流れで、採点に必要な解答欄座標のみを取得するように設定。

解答欄と、その座標をGUIで表示する(実際の画像)。

ところが、次のような解答用紙の場合・・・

左の画像を右へコピペしたので、設問番号がオカシイのは無視してください・・・

・・・のですが、矩形検出を実行後、検出された座標群から、僕のアルゴリズムで解答欄の選択を実行すると・・・

解答欄矩形の座標自体は、確実に取得できているから、矢印キーを駆使して、採点したい向きに解答欄の座標が並ぶように、座標を選択していけばいいだけの話なんだけれど。

これが・・・

超絶。すーぱーめんどくさい!

さらに、どんなにまっすぐ解答用紙をセットしても、必ず右肩上がり(画像が左に0.05度くらい傾いた状態で)でスキャンしてくれるという、メインで使用しているスキャナーならではのヘンなクセもあり、しかも、その画像に対して「Y座標の小さい順に赤枠で囲む」という僕のアルゴリズムは「正しく」機能するから、解答欄を上から下へ、行単位では左から右へという夢見た処理の流れは完全に逆転。採点候補の解答欄は左右に飛び、行単位でも右から左へ、想定とは真逆の順番で次の採点候補矩形が延々と表示される結果に・・・。こんな状態で、提示(表示)された解答欄矩形の座標を、採点順に正しく選択することは、年配の同僚にはほぼ不可能・・・

解答欄矩形、それ自体は 100% 正しく検出できているのですが・・・
あまりにも、こちらの気持ちを無視したプログラムの挙動を目の当たりにして・・・

責任者を出せ!って
怒鳴りたくなるんだけど・・・

ちょっと・・・、待って。

責任者。オレじゃん、
みたいな・・・

解答用紙によっては、さらに・・・

この例だと、Y座標がムチャだから、解答欄の選択作業はさらに困難を極め・・・

もっと・・・、発展(?)して

もぉ T_T

2.解答欄をブロック化して認識処理を実行

どうすればいいか?

答えはひとつしかありません。そうです!

解答用紙の解答欄を、採点順になるよう「まとまりのブロック」に分けて、ブロックごとに解答欄座標の取得手続きを行えばいいのです。

これで「一部がCutされた解答欄」は、矩形として認識されないので、最後に手動で座標データを追加すれば、なんとかなります(手動設定のプログラムは期待通りに動作しているから安心だし、上の図のような特殊な解答欄は最後の方にある場合が多い)。

この新方針のもとで、プログラムを見直してみると・・・

もともとのアルゴリズムは画像全体を1ブロックとして扱って、解答欄の矩形座標を検出しているから・・・

procedure TForm1.btnGetSquareClick(Sender: TObject);
var
  //PythonのScriptを入れる
  strScrList:TStringList;
  //Pythonから送られたデータを保存する -> グローバル変数化
  //strAnsList:TStringList;
  //Sort
  i,j:integer;
  //strFileName:string;
  strList:TStringList;
begin

  //画像分割処理なし(初期状態)

  //初期化
  Memo1.Clear;
  //Scriptを入れるStringList
  strScrList:=TStringList.Create;
  //結果を保存するStringList
  strAnsList:=TStringList.Create;

  try
    //Python Script
    strScrList.Add('import cv2');
    strScrList.Add('import numpy as np');
    strScrList.Add('from PIL import Image');
    //strScrList.Add('img = cv2.imread("./ProcData/sample2.jpg")');
    //strScrList.Add('img = cv2.imread(r"'+StatusBar1.SimpleText+'")');
    strScrList.Add('pil_img = Image.open(r"'+StatusBar1.SimpleText+'")');
    strScrList.Add('img = np.array(pil_img)');
    strScrList.Add('gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)');
    strScrList.Add('gray = 255 - gray');
    strScrList.Add('ret, bin_img = cv2.threshold(gray, 20, 255, cv2.THRESH_BINARY)');
    strScrList.Add('contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)');
    strScrList.Add('contours = list(filter(lambda x: cv2.contourArea(x) > '+cmbThreshold.Text+', contours))');
    strScrList.Add('for i in range(len(contours)):');
    strScrList.Add('    im_con = img.copy()');
    strScrList.Add('    x, y, w, h = cv2.boundingRect(contours[i])');
    strScrList.Add('    var1.Value =str(x)+","+str(y)+","+str(x+w)+","+str(y+h)');
    //Scriptを表示
    Memo1.Lines.Assign(strScrList);
    //「0による浮動小数点数除算」のエラーを出ないようにするおまじない
    MaskFPUExceptions(True);
    //Execute
    PythonEngine1.ExecStrings(Memo1.Lines);
    //結果を表示
    Memo2.Lines.Assign(strAnsList);
  finally
    //StringListの解放
    strAnsList.Free;
    strScrList.Free;
  end;

end;

・・・という感じで、かなりシンプル!

このあと、横書き・縦書きという解答欄の書き方に応じて、解答欄座標の並べ替えを行っている。どちらかというと、解答欄矩形の検出作業はOpenCVにおまかせで、並べ替えのアルゴリズムの方を工夫した記憶が・・・。

とりあえず・・・

横書き解答用紙が選択された時のみ、解答欄を何ブロックに分割して処理するか、GUIで選択できるようにして・・・

ブロック数分Loopを廻す中で、OpenCVのcvSetImageROI関数を用いて答案画像を分割、結果を一時Memoに書き込んで、2ブロック以降の座標値に対しては、そのx座標を答案画像上での値に修正(←実はコレを忘れていて、動作確認の際 ??? なことになり、初めて取得した座標値の修正作業の必要性に気づく)、で、最後に採点する順番になるよう座標を並べ替えてユーザーに提示する準備を実行。さらに、横書き解答用紙の場合は、一時Memoから座標データ提示用Memoへデータを移動して終了。・・・みたいな手続きのカタチにプログラムを半日程度かけて修正。

【間違えポイント①:範囲を指定して画像を切り出す】

cvSetImageROI関数の使い方は、その内部に入れてるcvRect関数の第1引数が切り出し位置の左上x座標、第2引数が切り出し位置左上のy座標、第3引数が切り出す幅、第4引数が切り出す高さとなっている。

最初、よく考えずにcvRect関数の第3、4引数を切り出し位置右下のx、y座標だと思い込んで設定し、切り出した画像の幅が変化することから、設定の誤りに気づく。前にマークシートリーダーを開発した時(Python環境を導入する前の段階で)、Windows用のOpenCVで画像処理していたときに、この関数のお世話になったはずなんだけど、もぉすっかり忘れてしまっていたようです。

  //指定範囲の画像を切り出して保存
  //cvRect(x, y, Width, Height)
  cvSetImageROI(sourceImage, cvRect(top_x, top_y, xWidth, yHeight));

【間違えポイント②:OpenCVのfilenameはPAnsiChar型】

それから、画像データへのPathとファイル名を入れる変数p1が PAnsiChar 型であることを、こちらもすっかり忘れていて、String型で引数を指定してエラーになって初めてそれを思い出す。変数に値を代入する際、いったん AnsiString 型でキャストして更に PAnsiChar でキャスト。

  //画像データのファイル名
  p1:PAnsiChar;

begin

  ・・・

  //String 型の文字列を PAnsiChar 型の文字列に変換
  //AnsiString 型でキャストして更に PAnsiChar でキャスト
  p1:=PAnsiChar(AnsiString('CutImage0'+IntToStr(i)+'.jpg'));

  //画像を保存する
  cvSaveImage(p1, sourceImage);

  ・・・

end;

完成した手続きがこちら(変数名等は思いつくまま、意図した通りに動けば 可 とした)

procedure TForm1.btnGetSquareClick(Sender: TObject);
var
  //PythonのScriptを入れる
  strScrList:TStringList;
  //Pythonから送られたデータを保存する -> グローバル変数化
  //strAnsList:TStringList;
  //Sort
  i,j:integer;
  //strFileName:string;
  strList:TStringList;

  //画像の等幅分割
  //切り出し領域
  top_x, top_y:integer;
  yHeight:integer;
  //xの増分
  xWidth, iMax:integer;
  //for Imageの読み込み
  sourceImage: PIplImage;
  //画像データのファイル名
  p1:PAnsiChar;

  //x座標の補正
  str1, str2, str3, str4:string;

begin

  //画像分割処理なし(初期状態)
  {
  //初期化
  Memo1.Clear;
  //Scriptを入れるStringList
  strScrList:=TStringList.Create;
  //結果を保存するStringList
  strAnsList:=TStringList.Create;

  try
    //Python Script
    strScrList.Add('import cv2');
    strScrList.Add('import numpy as np');
    strScrList.Add('from PIL import Image');
    //strScrList.Add('img = cv2.imread("./ProcData/sample2.jpg")');
    //strScrList.Add('img = cv2.imread(r"'+StatusBar1.SimpleText+'")');
    strScrList.Add('pil_img = Image.open(r"'+StatusBar1.SimpleText+'")');
    strScrList.Add('img = np.array(pil_img)');
    strScrList.Add('gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)');
    strScrList.Add('gray = 255 - gray');
    strScrList.Add('ret, bin_img = cv2.threshold(gray, 20, 255, cv2.THRESH_BINARY)');
    strScrList.Add('contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)');
    strScrList.Add('contours = list(filter(lambda x: cv2.contourArea(x) > '+cmbThreshold.Text+', contours))');
    strScrList.Add('for i in range(len(contours)):');
    strScrList.Add('    im_con = img.copy()');
    strScrList.Add('    x, y, w, h = cv2.boundingRect(contours[i])');
    strScrList.Add('    var1.Value =str(x)+","+str(y)+","+str(x+w)+","+str(y+h)');
    //Scriptを表示
    Memo1.Lines.Assign(strScrList);
    //「0による浮動小数点数除算」のエラーを出ないようにするおまじない
    MaskFPUExceptions(True);
    //Execute
    PythonEngine1.ExecStrings(Memo1.Lines);
    //結果を表示
    Memo2.Lines.Assign(strAnsList);
  finally
    //StringListの解放
    strAnsList.Free;
    strScrList.Free;
  end;
  }

  //画像分割処理ここから

  //初期化
  //Memo1.Clear;
  Memo2.Clear;
  MemoTemp.Clear;

  //初期化(定数的に利用する)
  top_y:=0;

  //分割数
  iMax:=StrToInt(cmbPartition.Text);

  //結果を保存するStringList
  strAnsList:=TStringList.Create;

  //初期化
  xWidth:=0;

  try

    for i := 0 to iMax-1 do
    begin

      //画像を読み込む(Bitmap・JPEGどちらも読み込み可能)
      p1:=PAnsiChar(AnsiString(StatusBar1.SimpleText));
      sourceImage := cvLoadImage(p1, CV_LOAD_IMAGE_ANYDEPTH or CV_LOAD_IMAGE_ANYCOLOR);

      //intとTruncは小数点以下を切り捨て。異なるのは、戻り値がintは実数、Truncは整数になること
      xWidth:=Trunc(SimpleRoundTo(sourceImage.Width/iMax,0));
      yHeight:=sourceImage.Height;

      //切り出す座標を指定
      top_x:= xWidth * i;

      try

        //指定範囲の画像を切り出して保存
        //cvRect(x, y, Width, Height)
        cvSetImageROI(sourceImage,cvRect(top_x, top_y, xWidth, yHeight));

        //String 型の文字列を PAnsiChar 型の文字列に変換
        //AnsiString 型でキャストして更に PAnsiChar でキャスト
        p1:=PAnsiChar(AnsiString('CutImage0'+IntToStr(i)+'.jpg'));
        //画像を保存する
        cvSaveImage(p1, sourceImage);

      finally
        //イメージの解放
        cvReleaseImage(sourceImage);
      end;

    end;

    for i := 0 to iMax-1 do
    begin

      //Scriptを入れるStringList
      strScrList:=TStringList.Create;

      //x座標の補正値を計算
      top_x:= xWidth * i;

      try
        //Python Script
        strScrList.Add('import cv2');
        strScrList.Add('import numpy as np');
        strScrList.Add('from PIL import Image');
        //strScrList.Add('img = cv2.imread("./ProcData/sample2.jpg")');
        //strScrList.Add('img = cv2.imread(r"'+StatusBar1.SimpleText+'")');
        //strScrList.Add('pil_img = Image.open(r"'+StatusBar1.SimpleText+'")');
        strScrList.Add('pil_img = Image.open(r"'+'CutImage0'+IntToStr(i)+'.jpg'+'")');
        strScrList.Add('img = np.array(pil_img)');
        strScrList.Add('gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)');
        strScrList.Add('gray = 255 - gray');
        strScrList.Add('ret, bin_img = cv2.threshold(gray, 20, 255, cv2.THRESH_BINARY)');
        strScrList.Add('contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)');
        strScrList.Add('contours = list(filter(lambda x: cv2.contourArea(x) > '+cmbThreshold.Text+', contours))');
        strScrList.Add('for i in range(len(contours)):');
        strScrList.Add('    im_con = img.copy()');
        strScrList.Add('    x, y, w, h = cv2.boundingRect(contours[i])');
        strScrList.Add('    var1.Value =str(x)+","+str(y)+","+str(x+w)+","+str(y+h)');
        //Scriptを表示
        Memo1.Clear;
        Memo1.Lines.Assign(strScrList);
        //「0による浮動小数点数除算」のエラーを出ないようにするおまじない
        MaskFPUExceptions(True);
        //Execute
        PythonEngine1.ExecStrings(Memo1.Lines);
        //結果を表示
        if RadioButton1.Checked then
        begin
          //x座標を補正する
          MemoTemp.Lines.Assign(strAnsList);
          if i<>0 then
          begin
            for j := 0 to MemoTemp.Lines.Count-1 do
            begin
              //値を取得
              str1:=GetTokenIndex(MemoTemp.Lines[j],',',0);
              str2:=GetTokenIndex(MemoTemp.Lines[j],',',1);
              str3:=GetTokenIndex(MemoTemp.Lines[j],',',2);
              str4:=GetTokenIndex(MemoTemp.Lines[j],',',3);
              //カンマ区切りの文字列の1,3番目にtop_x値を加える(座標を修正)
              str1:=IntToStr(StrToInt(str1)+top_x);
              str3:=IntToStr(StrToInt(str3)+top_x);
              //書き戻し
              MemoTemp.Lines[j]:=str1+','+str2+','+str3+','+str4;
            end;
          end;
        end else begin
          Memo2.Lines.Assign(strAnsList);
        end;
      finally
        //StringListの解放
        //strAnsList.Free;
        strAnsList.Clear;
        strScrList.Free;
      end;

      //横書きの場合のみ実行
      if RadioButton1.Checked then
      begin

        //strFileName:=ExtractFilePath(StatusBar1.SimpleText)+'Temp.csv';
        //MemoTemp.Lines.SaveToFile(strFileName);

        strList := TStringList.Create;
        try
          for j := 0 to MemoTemp.Lines.Count-1 do
          begin
            strList.Add(MemoTemp.Lines[j]);
          end;
          //並び替え 降順 -> True
          //if RadioButton1.Checked then
          //begin
            fAscending := False;
            fIndex := 1; //2番目の項目を
            fStyle := ssInteger; //整数型でソート
            strList.CustomSort(MyCustomSort); //ソート開始
          //end else begin
          //  fAscending := True;
          //  fIndex := 0; //1番目の項目を
          //  fStyle := ssInteger; //整数型でソート
          //  strList.CustomSort(MyCustomSort); //ソート開始
          //end;

          //データ抽出
          //Memo2.Clear;
          for j := 0 to strList.Count - 1 do
          begin
            Memo2.Lines.Add(strList[j]);
          end;
        finally
          MemoTemp.Clear;
          strList.Free;
        end;
      end;
    end;

  finally
    //StringListの解放
    strAnsList.Free;
  end;

  //画像分割処理ここまで

  //縦書きの場合のみ実行
  if RadioButton2.Checked then
  begin

    //strFileName:=ExtractFilePath(StatusBar1.SimpleText)+'Temp.csv';
    //Memo2.Lines.SaveToFile(strFileName);

    strList := TStringList.Create;
    try
      for i := 0 to Memo2.Lines.Count-1 do
      begin
        strList.Add(Memo2.Lines[i]);
      end;
      //並び替え 降順 -> True
      //if RadioButton2.Checked then
      //begin
      //  fAscending := False;
      //  fIndex := 1; //2番目の項目を
      //  fStyle := ssInteger; //整数型でソート
      //  strList.CustomSort(MyCustomSort); //ソート開始
      //end else begin
        fAscending := True;
        fIndex := 0; //1番目の項目を
        fStyle := ssInteger; //整数型でソート
        strList.CustomSort(MyCustomSort); //ソート開始
      //end;

      //データ抽出
      Memo2.Clear;
      for i := 0 to strList.Count - 1 do
      begin
        //Memo2.Lines.Add(GetCommaText(strList.Strings[i],fIndex));
        Memo2.Lines.Add(strList[i]);
      end;
    finally
      strList.Free;
    end;

  end;

  if RadioButton2.Checked then
  begin
    ScrollBox1.HorzScrollBar.Position:=ScrollBox1.HorzScrollBar.Range;
  end else begin
    //ScrollBarが表示されていなくてもエラーにならない
    ScrollBox1.HorzScrollBar.Position:=0;
  end;

  //表示
  LBRow.Visible:=True;
  LBRow2.Visible:=True;

  //操作可能に設定
  btnOpen.Enabled:=True;
  btnSave.Enabled:=True;

  //操作不可に設定
  btnGetSquare.Enabled:=False;

  //先頭へスクロール
  Memo2.Perform(WM_VSCROLL,SB_TOP,0);

  //先頭行へ
  Memo2.SelStart:=SendMessage(Memo2.Handle, EM_LineIndex, 0, 0);
  Memo2.Perform(EM_SCROLLCARET, 0, 0);  //キャレット位置までスクロール
  Memo2.SetFocus;

  GetLinePos;

  //矩形を表示
  Memo2Click(Sender);

end;

ちなみに、最後の解答欄矩形を表示する処理は・・・

procedure TForm1.Memo2Click(Sender: TObject);
var
  i:integer;
  //x1,x2,x3,x4:integer;
  //y1,y2,y3,y4:integer;
  p1,p2:TPoint;
  //文字列切り分け///////////////////////////////////////////////////////////////
  function RemoveToken(var s:string;delimiter:string):string;
  var
    p:Integer;
  begin
    p:=Pos(delimiter,s);
    if p=0 then Result:=s
    else Result:=Copy(s,1,p-1);
    s:=Copy(s,Length(Result)+Length(delimiter)+1,Length(s));
  end;

  function GetTokenIndex(s:string;delimiter:string;index:Integer):string;
  var
    i:Integer;
  begin
    Result:='';
    for i:=0 to index do
      Result:=RemoveToken(s,delimiter);
  end;

begin

  if not EditTF then
  begin

    //座標を取得
    i:=Memo2.Perform(EM_LINEFROMCHAR, Memo2.SelStart, 0);
    //ShowMessage(IntToStr(i));

    //エラー対策
    if Memo2.Lines[i]='' then Exit;

    x1:=StrToInt(GetTokenIndex(Memo2.Lines[i],',',0));
    y1:=StrToInt(GetTokenIndex(Memo2.Lines[i],',',1));
    x2:=StrToInt(GetTokenIndex(Memo2.Lines[i],',',2));
    y2:=StrToInt(GetTokenIndex(Memo2.Lines[i],',',3));

    if Assigned(plImage1) then begin
      FreeAndNil(plImage1);
    end;

    //コンポーネントを生成し,イベントを定義し,位置を指定して画像を表示
    plImage1:=TplResizeImage.Create(Self);
    plImage1.Parent:=ScrollBox1;
    plImage1.TransEvent:=True;
    //クライアント座標をスクリーン座標へ変換
    //GetSystemMetrics(SM_CYCAPTION) -> タイトルバーの高さ
    //GetSystemMetrics(SM_CYFRAME) -> ウィンドウの枠幅
    p1.X:=x1-(GetSystemMetrics(SM_CYFRAME) div 2);
    p1.Y:=y1-GetSystemMetrics(SM_CYCAPTION)-(GetSystemMetrics(SM_CYFRAME) div 2);
    p2.X:=x2-(GetSystemMetrics(SM_CYFRAME) div 2);
    p2.Y:=y2-GetSystemMetrics(SM_CYCAPTION)-(GetSystemMetrics(SM_CYFRAME) div 2);
    p1:=Image1.ClientToScreen(p1);
    p2:=Image1.ClientToScreen(p2);
    plImage1.SetBounds(p1.X, p1.Y, p2.X-p1.X, p2.Y-p1.Y);

    //SelectedプロパティをTrueにするとラバーバンドとグラブハンドルが表示される
    plImage1.Selected := True;
    plImage1.BringToFront;

  end;

end;

最終的に完成したコードはまわりくどくて、汚いけど、動きは期待したとおり、例えば3ブロックある解答用紙での処理は・・・

1ブロックめの最初。

このまま、下方向へ解答欄矩形の座標データを選択して、いちばん下の座標まで移動すると、次の矢印キー押し下げと同時に赤枠は2ブロックめの先頭へ移動。

2ブロックめは上の例だと2列分あるので、ちょっと処理が面倒だけど、実際の解答用紙ではこんな例はまずないので大丈夫ということにしておいて、とりあえず、いちばん下の座標まで移動したところで次の矢印キー押し下げ、同時に赤枠は3ブロックめの先頭へ移動。

で、3ブロックめの解答欄矩形も余すところなく、選択。実に、イイかんじ。

コレだ! コレだ!!
コレを実現したかったんだ☆

やったー!!
できた!!!

3.まとめ

複数ブロックからなる解答用紙の解答欄矩形検出は(考えてみれば当たり前ですが)、次のように処理するとうまく行きます。

(1)解答用紙の画像を予め複数ブロックに分割して別画像として保存
(2)それぞれのブロックごとに解答欄の矩形を検出&採点する順番に並べ替え
(3)ブロックごとに取得した座標値を解答用紙画像全体の中での座標値に変換
(4)全座標値を結合して定義ファイル等に保存

今回は、上の(3)の処理を失念してプログラミングしていたので、必要だった修正は、ブロックごとの値として取得したx座標を、解答用紙画像全体の中でのx座標に変換する処理を追加するだけという、この修正わずか1回で期待したとおりに動作するプログラムを完成できました。これは僕的には極めて稀有な例で、言うのも恥ずかしい事実ですが、いつも七転八倒状態を延々と繰り返してなんとか思ったとおりの動作を実現しているので、たまにはこんなコトがあってもいいかなー。みたいな♪

4.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

Installer

・・・って言えるのかな?

正直、レジストリは汚したくない。でも、プログラムの動作に必要なユーザーの情報や設定は保存して再利用したい・・・そんな時、役立つのが定義ファイル。

今時、レジストリを使わずに定義ファイル(iniファイル)を使うなんて、完全に時代遅れなのかもしれないが、2つか、3つの設定内容を記録して利用するには、すごく便利なのは事実。ただ、ひとつだけ問題があるとすれば、exeファイルの周辺にユーザーの知らないファイルが生成されること。

【参考】
以前、この問題の解決方法として、パブリックのドキュメント(C:\Users\Public\Documents)に定義ファイル他を保存して、プログラムから利用したこともあった。それがスマートか、どうか、は別にして、それなりに目的は実現できたけど・・・なんか、どこか、すっきりしない感じが残って(毎回コレで行こう!みたいな気持ちになれなかった)。ユーザーに意識させたくない部分を意図的に「隠した」って、自分的には、どうしても思っちゃうからかなー。

今回は、その「困ったこと」を僕なりにどう解決したか? ・・・というお話。

【目次】

1.困ったこと
2.自分的解決策はただ一つ
3.作ってみた①(全自動)
4.作ってみた②(マニュアル)
5.まとめ
6.お願いとお断り

1.困ったこと

iniファイルを使用したり、リソースに埋め込んだDLL、もしくは画像やデータベースその他のファイルをプログラムからexeの周辺に生成して利用する場合、例えばデスクトップにexeファイルを置くと、プログラムの起動と同時に、ユーザーから見て「何、コレ?」みたいなファイル(or フォルダ)が EXE の周辺に出来てしまう。

例えば、次のようにリソースに埋め込んだDLLがインストール先フォルダになければ、それを EXE のある場所に生成する場合がそうだ。

procedure TForm1.FormCreate(Sender: TObject);
var
  dllFileName:string;
begin
  //リソースからDLLを(なければ)生成
  dllFileName:=ExtractFilePath(Application.ExeName)+'XXX.dll';
  //ファイルの存在を確認
  if not FileExists(dllFilename) then
  begin
    //リソースを再生
    with TResourceStream.Create(hInstance, 'Resource_1', RT_RCDATA) do
    begin
      try
        SaveToFile(dllFileName);
      finally
        Free;
      end;
    end;
  end;
end;

プログラムを終了しても、当然、それらはexeの周辺に残っている。これらはユーザーから見れば、突然生まれた不審なファイル(or フォルダ)としか思えなくても不思議はない。

特にデスクトップにiniファイルやDLLを生成するEXEを置いた場合には、キレイ好きなユーザーから見れば、「この画面を汚すEXE、なに?」ってことにもなりかねない。

2.自分的解決策はただ一つ

ユーザーに対して、このような不安を与えないようにするなら、プログラム配布専用のインストールプログラムを作り、まず、そのリソースに配布したいプログラム(EXE)を埋め込む。で、このインストール専用のプログラムを起動したら、例えばユーザーのマイドキュメント内に適切な名前のフォルダを作成して、そこにexeをリソースから生成してコピー。最後に、そのEXEへのショートカットをデスクトップに自動的に作る・・・みたいなインストール専用のプログラム(=Installer)を書けばいいのかな? ・・・って。

こうしておけば、ユーザーはデスクトップのショートカットをダブルクリックするだけでプログラムを使えるし、ユーザーに見せたくないプログラムの動作に必要な情報も、その存在を隠しながら、マイドキュメント等に作った専用フォルダ内に生成できるはず。

3.作ってみた①(全自動)

予め、リソースにインストールしたい完成した配布用EXEを埋め込んでおく。DelphiのIDEの「プロジェクト」→「リソースと画像」の順にクリックして、埋め込むEXEを指定。

埋め込むEXEは、後の混乱を避ける意味でも、このインストールプログラムのプロジェクトフォルダに「Resource」等の専用フォルダを作成して、そこに完成した配布用EXEをコピーしておき、それを指定するのが方法的には Best かと。

このEXEの中には、当該プログラムの動作に必要なDLL等が全て埋め込まれている

GUIは、こんな感じで作成(実行時の画面)。

基本的に「全自動でインストール」内のボタン1ClickでOK!(の予定)

わかりやすい、とか、わかりにくい、とか、そういう問題とは別に、Enterキーひと押しで完全に動作すれば、インストールプログラムのインターフェイスの良し悪しは、特に問題にならないはず。

で、「マイドキュメントに専用~」ボタンをクリックした時の手続きは次の通り。

  private
    { Private 宣言 }
    Setup_FolderPath:string;
    Setup_ExeName:string;

implementation

{$R *.dfm}

uses
  Winapi.ShlObj, Vcl.FileCtrl, System.UITypes, plShortcutUtils;

  //ShlObjはSHGetKnownFolderPath関数を使用するために追加
  //ShellExecute関数を使用してフォルダを開いて表示する場合はWinapi.ShellAPIも追加する

  //Vcl.FileCtrlは、新しいフォルダ作成ボタン付きフォルダの選択ダイアログの表示に必要

procedure TForm1.btnAutoClick(Sender: TObject);
var
  FolderID:TGUID;
  FolderPath:PChar;
  rsFileName:string;
  LDir:String;
begin

  //マイドキュメントフォルダへのPathを取得する
  FolderID:=StringToGUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}');
  if SHGetKnownFolderPath(FolderID,0,0,FolderPath)= S_OK then
  begin
    Setup_FolderPath := FolderPath;
  end;

  //インストール先フォルダの有無を調査->なければ作成
  if not System.SysUtils.DirectoryExists(ExtractFileDir(Setup_FolderPath+'\'+Setup_ExeName+'\')) then
  begin
    //フォルダ階層を作成
    System.SysUtils.ForceDirectories(ExtractFileDir(Setup_FolderPath+
      '\'+Setup_ExeName+'\'));
  end;

  //Path
  rsFileName:=Setup_FolderPath+'\'+Setup_ExeName+'\'+Setup_ExeName+'.exe';

  //ファイルがある場合は削除
  if FileExists(rsFilename) then
  begin
   //ファイルが存在したときの処理
    DeleteFile(rsfileName);
  end;

  //リソースを再生
  with TResourceStream.Create(hInstance, 'Resource_1', RT_RCDATA) do
  begin
    try
      SaveToFile(rsFileName);
    finally
      Free;
    end;
  end;

  //デスクトップにこのプログラムのショートカットを作成
  if CheckCreateShortCut.Checked then
  begin
    //plShortcutUtilsユニット内の関数類を使用
    //CSIDL_DESKTOP等の定数名の使用にはusesにShlObjが必要
    //CSIDLの値からフルパスを取得
    //ショートカットを作成する場所
    LDir := GetDirectoryFromCSIDL(CSIDL_DESKTOP);

    if CreateShortCutLink(rsFileName, LDir, Setup_ExeName) then begin
      //ショートカットの作成場所によっては,以下のコードで更新が必要
      //SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 0);
    end;

    MessageDlg('Done!', mtInformation, [mbOk] , 0);
  end;

end;

ショートカットの作成方法は、Mr.XRAYさんのWebページにある方法をコピペしました。

880_ショートカットの作成と削除

http://mrxray.on.coocan.jp/Delphi/plSamples/880_CreateShortcut.htm

Private 宣言した Setup_FolderPath には、FormCreate手続きで次のようにして(初期表示のため、取り敢えず)マイドキュメントフォルダへのPathを入れておきます・・・。

procedure TForm1.FormCreate(Sender: TObject);
var
  FolderID:TGUID;
  FolderPath:PChar;
begin

  //インストールするEXEの名前
  Setup_ExeName:=EditExeName.Text;

  //マイドキュメントフォルダへのPathを取得する
  FolderID:=StringToGUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}');

  if SHGetKnownFolderPath(FolderID,0,0,FolderPath)= S_OK then
  begin
    Setup_FolderPath := FolderPath;
    EditPath.Text:= Setup_FolderPath;
  end;

end;

それから、インストールするExeの名前はForm上で非表示のGUI(EditExeName.Text)に設定しています(FormCreate時にグローバル変数に名称を読み込んで利用)。

こうしておけば、リソースに組み込むExeファイルを変更した時も、InstallするExeの名称を変更するだけで、このインストールプログラムを使えます。

設計時の画面左下に、実行時には非表示のLabelとEditコントロールを配置。このEditコントロールのTextプロパティにインストールするExeの名称を設定。

InstallするExeの名称Labelとその右のEditコントロールのVisibleプロパティはFalse

動作を検証した結果、プログラムは期待通りに動作しました。
ただ、32bitバージョンを作成した際に、実行形式ファイルを作成出来なくなるエラーが何回かありましたが・・・(原因がよくわかりません)。

4.作ってみた②(マニュアル)

もし、ユーザーが「おまかせインストール」ではなく、「フォルダを指定してインストール」の方を選択した場合の「ルートディレクトリの指定」に関する手続きは・・・

procedure TForm1.RadioGroup1Click(Sender: TObject);
var
  FolderID:TGUID;
  FolderPath:PChar;
begin

  case RadioGroup1.ItemIndex of
    0:begin
      //マイドキュメントフォルダへのPathを取得する
      FolderID:=StringToGUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}');
      if SHGetKnownFolderPath(FolderID,0,0,FolderPath)= S_OK then
      begin
        Setup_FolderPath := FolderPath;
        EditPath.Text:= Setup_FolderPath;
      end;
    end;
    1:begin
      //マイコンピュータへのPathを取得する
      Setup_FolderPath := 'C:\';
      EditPath.Text:= Setup_FolderPath;
    end;
  end;

end;

ちなみに、PCを選択した場合に表示される「フォルダーの参照」ダイアログは・・・

PCのフォルダ構成に詳しい人向きの表示になります・・・

で、インストール先を選ぶ「変更」ボタンをクリックした際の挙動は・・・

procedure TForm1.btnGetPathClick(Sender: TObject);
var
  SelectDir: String;
begin

  case RadioGroup1.ItemIndex of
    0:begin
      //フォルダを選択 -> MyDocumentsを指定
      //if SelectDirectory('', '::' + GUIDToString(CLSID_MyDocuments), SelectDir) then

      //MyDocumentsを指定 -> MyDocumentsを指定 & 新しいフォルダ作成ボタン付き
      if SelectDirectory('', '::' + GUIDToString(CLSID_MyDocuments), SelectDir,
        [sdNewUI, sdNewFolder, sdShowEdit], Self) then
      begin
        EditPath.Text:=SelectDir;
        Setup_FolderPath:=EditPath.Text;
      end;
    end;
    1:begin
      //フォルダを選択 -> を指定
      //if SelectDirectory('', '::' + GUIDToString(CLSID_MyComputer), SelectDir) then

      //MyMyComputerを指定 -> MyMyComputerを指定 & 新しいフォルダ作成ボタン付き
      if SelectDirectory('', '::' + GUIDToString(CLSID_MyComputer), SelectDir,
        [sdNewUI, sdNewFolder, sdShowEdit], Self) then
      begin
        EditPath.Text:=SelectDir;
        Setup_FolderPath:=EditPath.Text;
      end;
    end;
  end;

end;

上の手続きで使用しているGUIDToString関数の引数CLSID_XXXには、その種類に制限があるようです。ShlObj.pas内のGUID定義を見てみると・・・

const
  CLSID_NetworkDomain: TGUID     = '{46E06680-4BF0-11D1-83EE-00A0C90DC849}';
  {$EXTERNALSYM CLSID_NetworkDomain}
  CLSID_NetworkServer: TGUID     = '{C0542A90-4BF0-11D1-83EE-00A0C90DC849}';
  {$EXTERNALSYM CLSID_NetworkServer}
  CLSID_NetworkShare: TGUID      = '{54A754C0-4BF0-11D1-83EE-00A0C90DC849}';
  {$EXTERNALSYM CLSID_NetworkShare}
  CLSID_MyComputer: TGUID        = '{20D04FE0-3AEA-1069-A2D8-08002B30309D}';
  {$EXTERNALSYM CLSID_MyComputer}
  CLSID_Internet: TGUID          = '{871C5380-42A0-1069-A2EA-08002B30309D}';
  {$EXTERNALSYM CLSID_Internet}
  CLSID_RecycleBin: TGUID        = '{645FF040-5081-101B-9F08-00AA002F954E}';
  {$EXTERNALSYM CLSID_RecycleBin}
  CLSID_ControlPanel: TGUID      = '{21EC2020-3AEA-1069-A2DD-08002B30309D}';
  {$EXTERNALSYM CLSID_ControlPanel}
  CLSID_Printers: TGUID          = '{2227A280-3AEA-1069-A2DE-08002B30309D}';
  {$EXTERNALSYM CLSID_Printers}
  CLSID_MyDocuments: TGUID       = '{450D8FBA-AD25-11D0-98A8-0800361B1103}';
  {$EXTERNALSYM CLSID_MyDocuments}

自分的に使いたいなーって思う定義は、MyComputerとMyDocumentsぐらいしかありません(Desktopがない!)。まぁ、ない袖は振れない・・・ということでしょう。

どうしてもデスクトップを指定したい場合は、上で使用した GetDirectoryFromCSIDL(CSIDL_DESKTOP) のように、CLSID_XXX ではなく、CSIDL_XXX を使える形式に書き直す必要がありそうです(今回は、書き換えずに進めることにします)。

で、「実行」ボタンの挙動は、ほとんど再掲ですが・・・

procedure TForm1.btnOKClick(Sender: TObject);
var
  rsFileName:string;
  LDir:String;
begin

  //Path
  rsFileName:=Setup_FolderPath+'\'+Setup_ExeName+'.exe';

  //ファイルがある場合は削除
  if FileExists(rsFilename) then
  begin
   //ファイルが存在したときの処理
    DeleteFile(rsfileName);
  end;

  //リソースを再生
  with TResourceStream.Create(hInstance, 'Resource_1', RT_RCDATA) do
  begin
    try
      SaveToFile(rsFileName);
      //MessageDlg('Generate!', mtInformation, [mbOk] , 0);
    finally
      Free;
    end;
  end;

  //デスクトップにこのプログラムのショートカットを作成
  if CheckCreateShortCut.Checked then
  begin
    //plShortcutUtilsユニット内の関数類を使用
    //CSIDL_DESKTOP等の定数名の使用にはusesにShlObjが必要
    //CSIDLの値からフルパスを取得
    //ショートカットを作成する場所
    LDir := GetDirectoryFromCSIDL(CSIDL_DESKTOP);

    if CreateShortCutLink(rsFileName, LDir, Setup_ExeName) then begin
      //ショートカットの作成場所によっては,以下のコードで更新が必要
      //SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 0);
    end;

    MessageDlg('Done!', mtInformation, [mbOk] , 0);
  end;

end;

案外簡単に、思った通りのインストールプログラムが作れました!

ふと疑問に思い、今回、調べて初めて知ったのですが、「インストール」と「セットアップ」は意味的に異なるようです。

セットアップはよく、「インストール」と同義語として解説されることもありますが、インストールは、ソフトウェアを動かすためのプログラムやデータなどの各種ファイルをコンピュータにコピーすることであり、セットアップは、インストール後に自分のコンピュータに合わせて必要な設定をすることまでを指す言葉です。

ネット用語辞典(https://bb-navi.jp/netjiten/sa25.html)より引用

5.まとめ

iniファイルを使用したり、リソースに埋め込んだDLLその他のファイルをインストール先フォルダ等に生成して使うようなプログラムを配布する場合、ユーザーに優しいプログラムとするため、必要のないファイルその他を見せない工夫があった方がよいのではないか? と思い、マイドキュメントフォルダ等に専用フォルダを作成して、そこへExeをインストールするプログラムを書いてみた。

これまでユーザーのPCに手作業でEXEのインストール作業を行ってきたが、このようなインストーラにExeを埋め込んで配布すれば、その作業がいらなくなる?

取り敢えず、現場で運用して見ます!

6.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

サインイン 4 アプリからオンラインのOneDriveを表示

これまで「サインイン」と題して、オンラインのOneDriveへ、いかに楽してサインインするか・・・という内容の記事を3つ書いた。

それはアプリのOneDriveから、簡単にオンラインのOneDriveを表示する方法がわからなかったから。で、ことここに至ってようやく、その方法を発見。

結局、これまでの全ては、オンラインのOneDriveへ「いかに苦労してサインインするか」に変わった気が・・・。

【目次】

1.アプリからオンラインのOneDriveを表示
2.アカウントの切り替えも簡単
3.まとめ
4.お願いとお断り

1.アプリからオンラインのOneDriveを表示

なんで今まで気がつかなかったんだろう・・・。アプリのOneDriveの「フォルダーを開く」の右隣にオンラインのOneDriveを表示する「オンラインで表示」があった!

こんなコマンドがあったなんて・・・ちっとも気がつきませんでした。

さらに、アプリのOneDriveの「フォルダーを開く」で表示されるエクスプローラーの右上の「同期しています」をクリックすると表示されるサブメニューの右下にも「オンラインで表示」が存在!(ここのキャプションは、その時々の状況を伝えるほかの文字列「例:エラー」等になることもあるようだ)

ここにも「オンラインで表示」があった!

いずれもクリックすると、Webブラウザが起動してオンラインのOneDriveが表示される。

データ交換用のUSBメモリのようにオンラインのOneDriveを使用したい時は、このWebブラウザに表示されたオンラインのOneDriveへ、必要なデータをアップロードして、別のPCで同様にオンラインのOneDriveにサインインして、必要なデータをダウンロードすればいい。

オンラインのOneDriveへデータをアップロード

追記(20230829)

回線速度とは別に、使用するWebブラウザによりダウンロード速度に違いが生じることがあるのだろうか? 昨日、150MB程度のZipファイルをOneDriveからダウンロードしたのだが、Myプログラムから実行したそれは「あまりにも」遅く、耐え難かったので、ChromeからOneDriveに接続してダウンロードしてみたら「ものすごく」速かった・・・です。

遅かったのはTWebBroeserを使ったプログラムだったので、TEdgeBrowserに変更した新しいプログラムで試してみたら、ChromeでOneDriveに接続した場合と変わらないダウンロード速度で快適に作業できました!

2.アカウントの切り替えも簡単

個人のアカウントから、組織のアカウントへ(もちろん、その逆も)の切り替えも簡単でした。オンラインで表示したOneDrive画面右上のアカウントマネージャーをクリックして切り替えたいアカウントを選択するだけです。

アカウントの切り替え画面

3.まとめ

(1)アプリのOneDriveからオンライン表示への切り替えは走召簡単(泣)
(2)複数のアカウントがある場合、アカウントマネージャーで切り替え可能
(3)間違った思い込みは無駄な苦労の元。アプリの使い方をよく勉強しよう!

4.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

サインイン 3

追記(20230827 OneDriveアプリからオンライン表示へ切り替え)

無駄にプログラムなんか書く必要はありませんでした!

以下、『いかに苦労してOneDriveにサインインするか』という、上記サインイン 4に辿り着くまでの、長いながいまわり道の記録です。なので、お読みいただく価値がないことを最初に申し添えます。m(__)m

この記事は、アプリとして実行(タスクトレイに常駐)するOneDriveではなく、Web上のOneDriveへ直接データをアップロードし、別のPCでそのデータをダウンロードする、言わばデータ交換用USBメモリのようにOneDriveを使用する方法の一例です。PC内のOneDriveフォルダにあるデータと、クラウド上のOneDriveにあるデータの同期などは、まったく考慮しておりませんので、その点にはどうかご注意願います。

プロローグ

2023年8月のある日を境に、OneDriveの挙動が変わったことに気づいた。組織アカウントと個人アカウントの両方で同一ID(メールアドレス)を使用している場合、個人アカウントとしてサインインしようとすると、個人アカウント用のサインイン画面が新たに表示され、その都度、パスワードの入力が必須になったようだ・・・。

1.あれは夢だったのか・・・?
2.IDの入力を2回求められるようになった・・・
3.イロイロ調べてみた!
4.パスワードも自動入力!
5.画面の表示設定
6.まとめ
7.お願いとお断り

1.あれは夢だったのか・・・?

これまでに、過去の記事として「サインイン」、「サインイン 2」と題し、クラウド上のOneDriveへ、いかに「楽して入るか」ということについて自分なりに工夫した点のまとめの記事を(プログラミングの一つの区切りでもあったし)掲載した。

実行形式ファイルを配布していないから、動作の確認のしようがないじゃないか・・・という声が聞こえてくるような気もするけど、exeの配布が僕の目的ではなく、夢中になったことのプログラミング記録を残すことがこのBlogを書く目的なので、そこは悪しからずご了解ください(バグに満ちた?実行形式ファイルを配って、多くの方にご迷惑をおかけしたくないという思いも当然あります)。

「サインイン 2」で思った通りのプログラムが完成して、「使うぞー!」という段階に入った8月中旬、これまでと異なる挙動をWebブラウザが示すことに僕は気づいた。

サインイン2で作成した接続専用プログラムを起動し、IDを自動入力して「次へ」をクリックして「個人用アカウント」をクリックしてもOneDriveに素直に入れないのだ・・・。例外なく新しい画面でIDの入力を再度求められ、さらに毎回パスワードも入力しなければならない。

今まではこんなことなかったのに・・・
IDを入力するだけで入れた、あのOneDriveは夢だったのか・・・?

2.IDの入力を2回求められるようになった・・・

具体的にはどうなるのか、画面をつけて説明すると以下の通り。
FormCreate手続きの最後で、次のようにOneDriveのサインイン画面を呼び出して・・・IDとして利用しているメールアドレスを自動入力。

  //Navigate
  EdgeBrowser1.Navigate('https://onedrive.live.com/about/ja-jp/signin/');
IDとして利用するメールアドレスを入力して「次へ」をクリック(もちろん入力は自動化)

すると、次の画面が表示されて、これまでなら「個人用アカウント」をクリックするだけでOneDriveにサインインできた(過去にパスワードを入力してサインインに成功していればそのCookieが残っているから?)。

過去(90日間以内?)にサインインに成功していれば、パスワード入力は必要なかった・・・はず。
この画面が今回の問題の根本的な原因がここにあることを示唆している(気がする)。

ところが、2023年8月中旬(頃?)からは、「個人用アカウント」をクリックすると、なんと・・・(おそらく、ここでサインインするアカウントが「個人用アカウント」であることがユーザーによって確定されたということで、Microsoftさん的には、今度は安心して・・・再度、個人アカウント専用のOneDriveへのサインイン画面を表示して、サインインしてください・・・という意味なのだと思いますが)

サインインの最初の画面に戻ってしまう・・・ 感覚的には、ウソだろ、なんで? って感じ。

「戻ってしまう・・・」と書いたが、正確にはカーソルのキャレットの点滅位置(スクリーン座標)が異なっていることと、その下に「サインイン オプション」なる最初の画面にはない表示があることから気づいたのだが、(最初に表示されたのとは)「別」の(=個人用アカウントの?)サインイン画面が表示されるのだ(サインイン画面のURLも確認したが、当然最初のサインイン画面とは異なっている)。

IDとして利用するメールアドレスは、この段階では疑似クリック&貼り付け失敗に備えてクリップボード上に送ってあるから、キャレットの点滅を確認し、Ctrl+Vして入力欄に貼り付けて「次へ」をクリックすると、さらに、これまでは出てこなかったパスワードの入力画面が表示される・・・。

Cookieの存在など忘れたかのようだ

これまで利用していたはずのCookieは何処へ消えたのか・・・不思議に思いながら、パスワードを入力してサインインボタンをクリックすると、やっとOneDriveに入れる・・・。

しかも、最後に表示される画面で、「今後このメッセージを表示しない」をチェックして「はい」を選択(クリック)しても、このメッセージは毎回必ず表示される・・・つまり、個人用アカウントでサインインした場合は、「サインインの状態は維持されない」。

My 環境では、「はい」をクリックしても設定は維持されない

拝啓 マイクロソフト様

オレみたいな輩がいるから、こんな仕様になったんですか?

Webブラウザが見えないところでやってることなんて、何にもわかりません。わからないけど・・・

「ボクのお父さんは、桃太郎というやつに殺されました。」

あの手紙を読んだときと同じくらいショックでした。

悪いことをするつもりはまったくありません。

いつも使ってるID(=メールアドレス)で、

OneDriveに楽して入りたいだけなんです。

「それが大きな間違いだ・・・」と言われたら、素直に「はい」と言うしかありませんが・・・

3.イロイロ調べてみた!

(期待したことなど一度もないが)これまで通りの七転八倒の結末に、今回も大いに落ち込む。が、唯一の救いは「OneDriveにサインインできなくなったわけではない」という部分だ。設定が変わって、セキュリティがより厳しくなった・・・というか、組織アカウントと個人アカウントの区分がより厳密になった・・・と言えばいいのかな? この問題の全体像は、多分、僕には把握できないだろうけれど、とりあえず、僕がわかるところまでOneDriveへサインインする仕組みについて調べてみることにした。

その結果、いちばんわかりやすかったのが、こちらの記事。

『サインインの状態を維持しますか ?』のオン/オフをユーザーごとに制御する

https://itbeginner.tech/2020/07/25/keep-me-signed-in/

上記Webサイト様の記事によれば、「有効期限が切れるシナリオ以外に、サインイン画面が表示される代表的な例」は次の5つがあるとのこと。

  • ユーザーのパスワードが変更されている
  • サインインの際に prompt=login パラメーターが付与されている
  • 多要素認証 (MFA) を実施する必要がある
  • inPrivate モードのブラウザでサインインしている
  • ブラウザが Cookie を保存できない、送信できない … など。

https://itbeginner.tech/2020/07/25/keep-me-signed-in/より引用

パスワードは変更してないから、それ以外の4つのうち、個人用アカウントでサインインする場合には、どの理由が該当するのか(自分的には、組織アカウントと個人のアカウントの両方に登録されているIDが使用された場合に、どちらのアカウントでのサインインであるかを確定することがサインイン画面が2回表示される最大の目的だと思うのだけれど)、いずれにしても原因がはっきりわかっても、そこから先が独力では解決できそうにありません。

おそらく、組織アカウントと個人アカウントを明確に切り分けない限り、現段階でこの問題の解決策はないように思えてきました。

また、上記Webサイト様の記事では『Fiddler』(フィドラー?)というHTTPS通信を解析するソフトが紹介されており、記事を読んで(僕には絶対に結果を上手く扱えない・・・)と直感的に思ったけれど、取り敢えずLink先へ飛んでプログラムをダウンロードしてMy PCにインストール。動かしてみた結果が次の通り。

HTTPS通信の内容(My IDやPW)が表示されてる・・・ すごいー!!

『Fiddler』のインストールと使い方は、次の記事を参照して行いました!

HTTPS パケット キャプチャ ツール Fiddler のインストールから使用開始まで。

https://qiita.com/Shinya-Yamaguchi/items/37347ec532824c2dccad

で、せっかくインストールして動かしてみた『Fiddler』ですが、この『Fiddler』が表示してくれているHTTPS通信の内容を、Delphi の Object Pascal で書く OneDrive への接続プログラムで活用する方法がわかりません・・・。残念ながら僕には、現時点でそれだけのプログラミングスキルが・・・悔しいけれどありません。それをイチから学ぶには、とんでもない時間がかかりそうです・・・

もはやこれまで・・・
あきらめるしかないかぁ・・・

っと、思ったところで気がつきました!

何をあきらめるというのだろう?
Cookieを利用した形でのパスワード入力を回避できないなら、
パスワードも自前で暗号化して定義ファイルに保存しといて、
自動入力すればイイだけのことじゃないか・・・

サインイン画面が再び表示されたらCtrl+Vで、クリップボードにあるIDのデータを貼ればいいだけだし、さらに続けてパスワード入力が要求される場面があっても、僕のプログラム側で対応して、ID入力同様にパスワード入力を半自動化してしまえばいい。

負け惜しみじゃなく、すべてを手入力するよりか、はるかにラクだ!
貼り付けのショートカットだって、Ctrl+Vだけなら覚えて貰えるはず・・・
目の前に見えてるボタンのクリックなら、なんの問題もない。

要は、困った時のサポートと、「慣れ」だ。

OneDriveにサインインする「敷居」さえ、もっと低くできれば・・・
みんなに やさしい プログラムになる。

風邪などのウイルス性疾患全般に効く特効薬はないみたいだけれど、たとえウイルスは退治できなくでも、ウイルスの引き起こす様々な症状への対症療法ならたくさんある。

それと同じように、Windows Hello や Cookie を利用してパスワード入力そのものを回避するというような根本的な問題解決は(今の僕には)出来ないけれど、サインインのID入力画面が表示されたら、IDを自動入力、クリックで進めるところは素直にクリックして次へ進み、もしパスワード入力画面が表示されたら、そこでまたパスワードを半自動入力するという、いわば対症療法的な方法で少しでも「楽に」サインインする方法が実現できるよう頑張ればいい。それだって、IDやパスワードを毎回全部手入力するよりは、ずっとラクなはずだ・・・。

繰り返せば、やがて「慣れ」という免疫ができる・・・。

それに、実験してて気がついたんだけど、自動的にCookieが適用?されて、パスワード入力を求められない(パスワード入力の画面が表示されない)IDもあるようだ。

詳しくは書かないけど、サインイン後の画面そのものがIDによって・・・違う。

なんでIDにより、Webブラウザの挙動が異なるのか?

おそらく、僕がOneDriveへのサインイン時に、IDとして使用しているメールアドレスは、Office 365の申し込み時にもその登録に使用したから当然Azure ADアカウント(=職場や学校のアカウント:組織アカウント)になっていて、さらに、同じメールアドレスが昔のLive IDつながりのMicrosoftアカウント(=個人のアカウント)としても登録されているから、このアカウントの二重登録状態をなんとか解消したい(させたい)Microsoft社の意向があって、こういうことになっているんだと思うんだけど・・・。

そうか、組織アカウントなら・・・。

ただ、僕のように、個人のアカウントのメールアドレスをどうしても変更したくない場合は、どうしたらよいのだろう?

そのへんの違いと仕組みは、これからの成長課題としておいて・・・、今は、今の僕に出来るいちばんイイことをしよう!

4.パスワードも自動入力!

さっそく、次のようにGUIを修正。

ID=メールアドレス入力用のGUIはそのまま利用(前回、作成したもの)

上のGUIの「待ち時間:1500(ミリ秒)」ComboBoxの右隣りに下のGUIを追加。

パスワード入力用のGUIを追加

上のように、パスワードをマスクするには、次のように設定。

パスワードを入力させるとき、入力した文字が他人に見られないように*などを表示(現在は黒丸●が標準?)するには、PasswordCharプロパティに * を設定するだけでOK!

//Password入力用文字列に'*'を設定
Edit1.PasswordChar:='*';

//Password入力用文字列設定を解除(''で#0を囲まないこと!)
Edit1.PasswordChar := #0;

【注意】
Editコントロールのプロパティで直接指定する場合は、アスタリスクをシングルクオートで囲んで ‘*’ としないこと! 「プロパティ値が違います」と即エラーになる。

シングルクオート囲みなし、単に #0 or * を入力すればOK!

マスク解除のプロパティでの指定例

パスワードのマスクを、CheckBoxのチェックと連動させるのであれば・・・

「確認」チェックボックスのチェックに連動してマスク状態が変化する
procedure TForm1.chkPWClick(Sender: TObject);
begin
  if chkPW.Checked then
  begin
    EditPW.PasswordChar := #0;
  end else begin
    EditPW.PasswordChar := '*';
  end;
end;

【再掲:マウスカーソルの現在位置座標の取得方法】

座標チェックに☑すると、マウスカーソルの現在位置のスクリーン座標がリアルタイムで表示される。その方法は前回も示しましたが、次の通り。

マウスのカーソルが現在置かれている位置のスクリーン座標を取得してLabelに表示。

procedure TForm1.chkZahyoClick(Sender: TObject);
begin
  if chkZahyo.Checked then
  begin
    //Enabled
    Timer1.Enabled:=True;
  end else begin
    //Enabled
    Timer1.Enabled:=False;
    LabelXY.Caption:='[X座標, Y座標]';
  end;
end;

Timer1のOnTimerプロパティをダブルクリックして作成されたTimer1Timer手続きに次のコードを記述。これでほぼリアルタイムにカーソルの位置座標を取得して表示できる。

procedure TForm1.Timer1Timer(Sender: TObject);
var
  lh_Handle:  HWND;
  lpt_Pos:    TPoint;
  lrc_Rect:   TRect;
  lrg_Region: HRGN;
  li_Ret:     Integer;
begin
  if chkZahyo.Checked then
  begin
    //マウスカーソル位置をスクリーン座標で取得
    GetCursorPos(lpt_Pos);
    //自身のウィンドウリージョンを調べる
    lh_Handle := Self.Handle;

    //ウィンドウリージョン取得のため空のリージョンを作っておく
    lrg_Region := CreateRectRgn(0,0,0,0);
    try
      //ウィンドウリージョン取得
      li_Ret := GetWindowRgn(lh_Handle, lrg_Region);
      if (li_Ret <> ERROR) then begin
        //ウィンドウのRectを取得
        GetWindowRect(lh_Handle, lrc_Rect);
        //スクリーン座標からウィンドウの左上を原点とした座標に変換
        lpt_Pos.X := lpt_Pos.X - lrc_Rect.Left;
        lpt_Pos.Y := lpt_Pos.Y - lrc_Rect.Top;
        //ウィンドウリージョン内にマウスカーソルがあるかテスト
        if (PtInRegion(lrg_Region, lpt_Pos.X, lpt_Pos.Y)) then begin
          LabelXY.Caption:=Format('OK %d (%d-%d)', [li_Ret, lpt_Pos.X, lpt_Pos.Y]);
        end else begin
          LabelXY.Caption:=Format('NG %d (%d-%d)', [li_Ret, lpt_Pos.X, lpt_Pos.Y]);
        end;
      end else begin
        LabelXY.Caption:=Format('[X:%d, Y:%d]', [lpt_Pos.X, lpt_Pos.Y]);
      end;
    finally
      DeleteObject(lrg_Region);
    end;
  end;
end;

これでパスワード入力欄のスクリーン座標を取得・保存しておいて、実際はパスワード入力画面が表示されたら、「入力」ボタンをクリック(パスワード入力画面が表示されなければ、GUIそのものを表示する必要もないので、接続環境に合わせてGUIそのものの表示もON/OFFできるようにした)。もちろん、GUIの表示状態そのものを保存可能。

パスワードはクリップボードに送信せず、Editコントロールにマスクをかけて表示しておき、入力が必要であればボタンクリックで実行できるように設定。

入力ボタンをクリックすると指定座標位置へパスワードを送信

パスワードの半自動入力は、次のコードで実行(前回のID入力用コードを修正)。

procedure TForm1.btnCopyClick(Sender: TObject);
var
  dwFlags : DWORD;
  X,Y : Integer;
  LKeyByte : Byte;
begin

  boolInput:=False;

  //Information
  if chkInfo.Checked then
  begin
    if MessageDlg('パスワード入力画面が見えていて、入力欄は空欄ですか?', mtInformation, [mbYes, mbNo], 0) = mrYes then
    begin

      try

        //クリップボードを初期化
        Clipboard.Clear;
        //文字列をクリップボードへ
        Clipboard.AsText:=EditPW.Text;

        dwFlags:=MOUSEEVENTF_MOVE or MOUSEEVENTF_ABSOLUTE;
        X:=Trunc(StrToInt(EditPWX.Text)/Screen.Width*65537);
        Y:=Trunc(StrToInt(EditPWY.Text)/Screen.Height*65535);

        //移動
        Mouse_Event(dwFlags,X,Y,0,0);
        //クリック
        Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
        //Mouse_Event(MOUSEEVENTF_LEFTUP,0,0,0,0);
        Application.ProcessMessages;

        WaitTime(StrToInt(cmbWaitTime.Text));

        Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
        Mouse_Event(MOUSEEVENTF_LEFTUP,0,0,0,0);
        Application.ProcessMessages;

        // [Ctrl] + [V] のキー操作
        LKeyByte := Ord('V');
        keybd_event(VK_CONTROL, MapVirtualKey(VK_CONTROL, 0), 0, 0);
        keybd_event(LKeyByte, MapVirtualKey(LKeyByte, 0), 0, 0);
        keybd_event(LKeyByte, MapVirtualKey(LKeyByte, 0), KEYEVENTF_KEYUP, 0);
        keybd_event(VK_CONTROL,MapVirtualKey(VK_CONTROL,0),KEYEVENTF_KEYUP, 0);

        boolInput:=True;


      except

        boolInput:=False;

      end;

      end else begin

        MessageDlg('パスワード入力画面を表示し、入力欄が空欄の状態で、再度実行してください。', mtInformation, [mbOk] , 0);

    end;

  end else begin

    try

      //クリップボードを初期化
      Clipboard.Clear;
      //文字列をクリップボードへ
      Clipboard.AsText:=EditPW.Text;

      dwFlags:=MOUSEEVENTF_MOVE or MOUSEEVENTF_ABSOLUTE;
      X:=Trunc(StrToInt(EditPWX.Text)/Screen.Width*65537);
      Y:=Trunc(StrToInt(EditPWY.Text)/Screen.Height*65535);

      //移動
      Mouse_Event(dwFlags,X,Y,0,0);
      //クリック
      Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
      //Mouse_Event(MOUSEEVENTF_LEFTUP,0,0,0,0);
      Application.ProcessMessages;

      WaitTime(StrToInt(cmbWaitTime.Text));

      Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
      Mouse_Event(MOUSEEVENTF_LEFTUP,0,0,0,0);
      Application.ProcessMessages;

      // [Ctrl] + [V] のキー操作
      LKeyByte := Ord('V');
      keybd_event(VK_CONTROL, MapVirtualKey(VK_CONTROL, 0), 0, 0);
      keybd_event(LKeyByte, MapVirtualKey(LKeyByte, 0), 0, 0);
      keybd_event(LKeyByte, MapVirtualKey(LKeyByte, 0), KEYEVENTF_KEYUP,0);
      keybd_event(VK_CONTROL,MapVirtualKey(VK_CONTROL,0),KEYEVENTF_KEYUP,0);

      boolInput:=True;

    except

      boolInput:=False;

    end;
  end;
end;

5.画面の表示設定

IDのみの入力でOneDriveにサインイン可能な場合は、不要なGUIは表示せずに運用。

これが理想的画面

ID&パスワードの入力が必要な場合は、画面左上の設定ボタンをクリックしてGUIを表示。初回のみ、ID&パスワード入力欄のスクリーン座標を計測&保存して、次回以降は半自動入力でサインイン。

対症療法的&非理想的画面(GUIを活用してサインイン)

現実世界に追従するカタチでのプログラミングは、夢を追いかけて・・・ではなくて、正直、必要に追われて・・・って感じで、書いていて楽しくはないけど。

でも、もし、これが誰かの役に立つなら・・・

OneDriveが使えなくて、すごく困っている人の役に立つなら・・・

僕のしたことに、ほんの少しだけ

意味や価値を見出せる気がします。

そうだ・・・。今、思い出せた・・・。

プロが書いた、見た目も美しい、あらゆる要求に対応した高価なプログラムではなく、
こんな僕の書いた、みすぼらしい、しかも機能限定のプログラムがいいと・・・

二者択一の場面で、僕のプログラムを選んでくださる人がいることを。

うん。そうだ。
きみも言ってくれたね。

「生きていれば必ず前進できます。
 もっとよくなれるんです。

 ・・・

 お互い 夢の実現に向けて、自分らしく歩きましょう。」

今、信じなくて、いつ、信じるんだ。
1ミリでもかまわない。
前へ行くんだ。

僕に今できる、唯一、確かなことをするんだ・・・

6.まとめ

(1)OneDriveへのサインインはIDによりその挙動が異なる。
(2)対症療法的にプログラミングすれば半自動ログインはできる。
(3)全自動ログインには、Cookieの利用を含めたさらなる学習が必要。

7.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

サインイン 2

追記(20230827 OneDriveアプリからオンライン表示へ切り替え)

無駄にプログラムなんか書く必要はありませんでした!

以下、『いかに苦労してOneDriveにサインインするか』という、上記サインイン 4に辿り着くまでの、長いながいまわり道の記録です。なので、お読みいただく価値がないことを最初に申し添えます。m(__)m

この記事は、アプリとして実行(タスクトレイに常駐)するOneDriveではなく、Web上のOneDriveへ直接データをアップロードし、別のPCでそのデータをダウンロードする、言わばデータ交換用USBメモリのようにOneDriveを使用する方法の一例です。PC内のOneDriveフォルダにあるデータと、クラウド上のOneDriveにあるデータの同期などは、まったく考慮しておりませんので、その点にはどうかご注意願います。

また、サインイン画面でのID(Microsoftアカウントに登録したメールアドレス)入力後のOneDriveの応答について、いろいろ調べたのですが、起こり得る個々の問題一つ一つについては、僕のプログラミングスキルでは到底対応できないと感じました。そこで、ID入力後にパスワード入力が必要になった場合の処理について、後日、自分なりのスーパー・ローレベル対応方法として「サインイン3」というタイトルで書きたいと思います。

コンピューターと同期する OneDrive フォルダーを選択する

クラウド上のOneDriveとPCのデータの同期については上のリンク先記事をご参照ください。

OneDriveのサインイン画面に、IDとして利用するメールアドレスを自動入力するプログラムを前回、作成した。目的通りのプログラムが出来たことは出来たが、WebView4Delphiコンポーネント(MITライセンス)のdemoフォルダにあったSampleをそのまま使わせてもらったため、「ID(メールアドレス)を自動入力する」以外にも様々な機能が実装(Sampleなんだから当然と言えば当然)されており、ただプログラムを起動するだけで、exeを置いたフォルダ内に容量がおよそ200MBくらいある「CustomCache」という名前のフォルダが出来てしまう。無視すればイイと言ってしまえば・・・それまでかもしれないけど・・・。

WebView4Delphi

https://github.com/salvadordf/WebView4Delphi

とりあえず、自分的には使わない機能をカットしようと思い、プログラムソースを読んでみたんだけれど・・・、コードどうしの関連がよくわからない。ヘタにいじって不具合とバグの山を築くより、「IDの自動入力」という初心に帰って、プログラムをイチから作り直した方がいい気がしてきた。

で、作ったのがコレ。

OneDriveのサインイン画面にID(メールアドレス)を自動入力する機能だけを搭載

【作成の手順】

1.TEdgeBrowserを使う
2.GUIの設計とプログラムコード
(1)GUIの設計
(2)VCLコントロールの表示/非表示を切り替え
(3)入力値の保存/読み込みと暗号化
(4)カーソル位置の座標を取得
(5)プログラムコードから指定位置をクリック
(6)ダウンロードフォルダを開く
(7)リソースにDLLを埋め込む
(8)操作方法の案内
3.まとめ
4.お願いとお断り

1.TEdgeBrowserを使う

Delphiで、Web コンテンツやローカルに置いたhtmlファイルの読み込みと表示を行うためのビジュアル コンポーネントには、TEdgeBrowser や TWebBrowser があるけれど、表示したいWebページがJavaScript のダイアログ ボックス、パネル、その他要素を使用しているとTWebBrowser では Web ページを正しく表示できないことがあるようだ。

正直に言うと、Edge には印刷その他の不具合でかつて悩まされた記憶があり、個人的にあまり良いイメージを持っていなかったので、新しい TEdgeBrowser コンポーネントではなく、古い TWebBrowser コンポーネントの方を使いたかった。だから、最初は、次のリンク先にあるような情報を参考にして、TWebBrowser コンポーネントで OneDrive のサインイン画面を表示するプログラムを書いてみたのだが・・・

Delphi / C++Builder Starter Edition の VCL で WebBrowser コンポーネントを使う

https://qiita.com/ht_deko/items/c69902d644ea03f61deb

上のリンク先記事のおしまいの部分でも述べられている通り、TWebBrowser コンポーネントを使って OneDrive のサインイン画面を表示するコードを書くと、結構盛大にスクリプトエラー発生のメッセージが表示される。

なんとかならないか・・・と思い、Google先生にお伺いをたてると、FMX版の TWebBrowser の記事ではあるが、本家本元embarcaderoさん提供のスクリプトエラー発生回避策を発見。

FMX.WebBrowser.TWebBrowser

https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.WebBrowser.TWebBrowser

それによれば「この問題を回避するには、アプリケーションは、Internet Explorer の FEATURE_BROWSER_EMULATION 機能を使用して、Web ページを IE11 エッジ モードで表示しなければなりません。」と説明があり、具体的な回避策として「FormCreate イベント ハンドラで、TForm1.SetPermissions メソッドを呼び出す」方法がソースコード付きで掲載されていた。

早速、スクリプトエラー回避策なるそのコードをコピペして実行してみたが、ナニがよくないのか、スクリプトエラーは発生状況に変化は見られなかった。

上のリンク先ページでは、スクリプトエラー回避策コードの下に「メモ: レジストリに対するこれらの変更は、アプリケーションが開始する前に適宜行わなければなりません。最初にアプリケーションを開始した際には、それを一度閉じ、再度開始します。」という説明があるので、プログラム起動時にレジストリに対する変更を行って、いったんプログラムを終了し、再度実行すればOKなのか? とも思ったが、原因の究明に時間を割くより、新しいTEdgeBrowserコンポーネントでOneDriveのサインイン画面を表示する方法を試した方が賢い気がして、ここで方針を変更。素直にTEdgeBrowserコンポーネントを使うことにする。その際、参考にさせていただいた記事がこちら

TEdgeBrowserでWebView2を使う ~Delphiソースコード集

https://mam-mam.net/delphi/tedgebrowser.html

OSがWindows11であれば、動作に必要なMicrosoft WebView2 ランタイムは、既に入っているので、インストール不要とのこと。作成するプログラムを動かす予定のPCのOSはすべてWindows11なので、その点は心配ないが、いちおうReadme.txtファイルを用意して、OSがWindows10の場合にはMicrosoft WebView2 ランタイムのインストールが必要であることを案内した方がよさそうだ。

2.GUIの設計とプログラムコード

(1)GUIの設計

Delphiを起動し、「ファイル」→「新規作成」→「Windows VCLアプリケーション」と辿って、表示されたFormにPanelを3つ図のように配置する。

Panel1が階層構造的にはいちばん下にあり、AlignプロパティはalTopを指定。その上にPanel2及び 3 を乗せて、Panel2のAlignプロパティはalLeft、Panel3のAlignプロパティはalClientをそれぞれ指定する。このようにプロパティを設定しておけば、Formの大きさが変化しても、Panel2の大きさ(幅と高さ)は変わらず、Panel3の高さはそのままで幅がFormの大きさ(幅)に合わせて自動的にサイズが変化し、各VCLコントロールの位置はFormの左上を原点とした設計時の位置に固定されて表示される。

VCLコントロールの階層構造

この後、Panel2の上には「ダウンロードフォルダを開く」ボタン、Panel3の上には暗号化してiniファイルに保存する予定の「ID」入力用のEditその他のVCLコントロールを次のように配置する。

必要と思われる最小限度のVCLコントロールを使い勝手を考えながら配置する

(2)VCLコントロールの表示/非表示を切り替え

ID(メールアドレス)や、自動的にクリックする座標値はいったん設定・保存してしまえば、通常の使用の際には必要ないので、普段は非表示に設定。つまり、□設定チェックボックス以外のVisibleプロパティはすべてFalseを指定する。で、設定を☑したときだけ、表示されるように設定。

procedure TForm1.chkSettingClick(Sender: TObject);
begin
  if chkSetting.Checked then
  begin
    LabelID.Visible:=True;
    btnCopy.Visible:=True;
    btnCopy.Enabled:=True;
    Edit1.Visible:=True;
    LabelX.Visible:=True;
    EditX.Visible:=True;
    LabelY.Visible:=True;
    EditY.Visible:=True;
    btnSave.Visible:=True;
    chkZahyo.Visible:=True;
    LabelXY.Visible:=True;
    LabelWaitTime.Visible:=True;
    cmbWaitTime.Visible:=True;
  end else begin
    LabelID.Visible:=False;
    btnCopy.Visible:=False;
    Edit1.Visible:=False;
    LabelX.Visible:=False;
    EditX.Visible:=False;
    LabelY.Visible:=False;
    EditY.Visible:=False;
    btnSave.Visible:=False;
    chkZahyo.Visible:=False;
    LabelXY.Visible:=False;
    LabelWaitTime.Visible:=False;
    cmbWaitTime.Visible:=False;
  end;
end;

(3)入力値の保存/読み込みと暗号化

各VCLコントロールに入力された値は、必要な個所は暗号化してiniファイルに保存する。

uses
  System.IniFiles;

procedure TForm1.btnSaveClick(Sender: TObject);
var
  strID:string;
  Ini:TIniFile;
begin

  //入力の有無をCheck
  if Edit1.Text='' then
  begin
    MessageDlg('IDとして利用するメールアドレスを入力してください', mtInformation, [mbOk] , 0);
    Edit1.SetFocus;
    Exit;
  end;

  if (EditX.Text='') or (EditY.Text='') then
  begin
    if EditX.Text='' then
    begin
      MessageDlg('自動クリックするX座標を入力してください', mtInformation, [mbOk] , 0);
      EditX.SetFocus;
    end;
    if EditY.Text='' then
    begin
      MessageDlg('自動クリックするY座標を入力してください', mtInformation, [mbOk] , 0);
      EditY.SetFocus;
    end;
    Exit;
  end;

  if cmbWaitTime.Text='' then
  begin
    MessageDlg('カーソル移動の待機時間をミリ秒単位で入力してください', mtInformation, [mbOk] , 0);
    cmbWaitTime.SetFocus;
    Exit;
  end;

  //暗号化
  strID:=EDText(Edit1.Text, IntToStr(HashOf('XXXXXXXX')), True);

  //iniファイルに保存
  Ini := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  try
    //保存
    Ini.WriteString('Section', 'ID', strID);
    Ini.WriteString('Section', 'IchiX', EditX.Text);
    Ini.WriteString('Section', 'IchiY', EditY.Text);
    Ini.WriteString('Section', 'WaitTime', cmbWaitTime.Text);
    //Userに通知
    MessageDlg('現在の設定を保存しました!', mtInformation, [mbOk] , 0);

    if not btnCopy.Enabled then btnCopy.Enabled:=True;

  finally
    Ini.Free;
  end;

end;

コードの中で使用しているEDText関数はテキスト暗号化の関数。

  private
    { Private 宣言 }
    //HashNameMBCS(Create hashed values from a Unicode string)
    //MBCS:Multibyte Character Set=マルチバイト文字セット
    function HashOf(const key: string): cardinal;

    //テキスト暗号化/復号化
    Function EDText(KeyStr,PassW:string; EncOrDec:Boolean):string;
    //KeyStr:平文 or 暗号化文のいずれかを指定
    //PassW:パスワード
    //EncOrDec:True -> Encode / False -> Decode

  public
    { Public 宣言 }
  end;

function TForm1.HashOf(const key: string): cardinal;
var
  I: integer;
begin
  Result := 0;
  for I := 1 to length(key) do
  begin
    Result := (Result shl 5) or (Result shr 27);
    Result := Result xor Cardinal(key[I]);
  end;
end;

function TForm1.EDText(KeyStr, PassW: string; EncOrDec: Boolean): string;
var
  {暗号化用変数}
  Source, Dest, Password:TStringBuilder;
  lpSource, lpPass:Integer;
  PassValue, SourceValue, EDValue:Word;
  {共用変数}
  //乱数の種
  Seed1,Seed2,Seed3:integer;
  //実数の一様乱数
  RandNum:Double;
  //秘密鍵Seed
  Seed:string;
  {復号化用変数}
  DecSource:string;
begin
  //1.シード値を準備
  // (1)Passwordを整数へ変換→シード値1へ代入
  Password := TStringBuilder.Create;
  //Seed1を初期化
  //Seed1:=0;
  try
    Password.Append(PassW);
    PassValue := 0;
    for lpPass := 0 to Password.Length - 1 do
    begin
      //パスワード→整数
      PassValue := PassValue + Word(Password.Chars[lpPass]);
    end;
    Seed1:=PassValue;
  finally
    Password.Free;
  end;

  // (2)パスワード文字列の長さを取得→シード値2へ代入
  Seed2:= ElementToCharLen(PassW,Length(PassW));

  // (3)シード値1とシード値2の排他的論理和を計算して、シード値3へ代入
  Seed3 := Seed1 xor Seed2;

  //2.実数の一様乱数を計算
  //---------------------------------------------------------------------------
  // 0より大きく1より小さい実数の一様乱数を発生する関数
  // B.A.Wichmann and I.D.Hill, Applied Statistics, 31, 1982, p.188 に基づく
  // Seed1-3に入れる初期値(整数)は16bit長(maxint=32767)で十分
  // Seed1-3には1から30000までの任意の整数値を準備する(0ではいけない)
  //---------------------------------------------------------------------------

  //Seed1:=171*Seed1 mod 30269 と同値
  Seed1:=(Seed1 mod 177)*171-(Seed1 div 177)* 2;
  if Seed1<0 then Seed1:=Seed1+30269;
  //Seed2:=172*Seed1 mod 30307 と同値
  Seed2:=(Seed2 mod 176)*172-(Seed2 div 176)* 35;
  if Seed2<0 then Seed2:=Seed2+30307;
  //Seed1:=170*Seed1 mod 30323 と同値
  Seed3:=(Seed3 mod 178)*170-(Seed3 div 178)* 63;
  if Seed3<0 then Seed3:=Seed3+30323;
  //See1-3それぞれの乱数を0<RandNum<1となるように
  //計算結果が0より大きく、1未満の実数に直し、和の小数部分をとる
  RandNum:=(Seed1/30269.0) + (Seed2/30307.0) + (Seed3/30323.0);
  while RandNum>=1 do RandNum:=RandNum-1;

  //3.秘密鍵を生成

  //整数の一様乱数の上限値を決めて、整数の一様乱数を生成し、
  //これに上で計算した実数の一様乱数を加えて秘密鍵を生成する
  //Seedが秘密鍵(文字列として利用)となる
  Seed:= FloatToStr(RandNum + trunc((Seed1+Seed2+Seed3)*RandNum));

  //4.暗号化 / 復号化
  if (EncOrDec) then
  begin
    //暗号化(Encode)
    Source := TStringBuilder.Create;
    Dest := TStringBuilder.Create;
    Password := TStringBuilder.Create;
    try
      Source.Append(KeyStr);
      //秘密鍵をセット
      Password.Append(Seed);
      lpPass := 0;
      //テキストのエンコード
      for lpSource := 0 to Source.Length - 1 do
      begin
        //パスワード→整数
        if Password.Length = 0 then
          PassValue := 0
        else begin
          PassValue := Word(Password.Chars[lpPass]);
          Inc(lpPass);
          if lpPass >= Password.Length then lpPass := 0;
        end;
        //テキスト→整数
        SourceValue := Word(Source.Chars[lpSource]);
        //XOR演算
        EDValue := PassValue xor SourceValue;
        //16進数文字列に変換
        Dest.Append(IntToHex(EDValue, 4));
        //処理結果を返り値にセット
        Result:=Dest.ToString;
      end;
    finally
      Password.Free;
      Dest.Free;
      Source.Free;
    end;
  end else begin
    //復号化(Decode)
    DecSource:=keyStr;
    Dest := TStringBuilder.Create;
    Password := TStringBuilder.Create;
    try
      //暗号化テキストのデコード
      Dest.Clear;
      Password.Clear;
      //秘密鍵をセット
      Password.Append(Seed);
      lpPass := 0;
      for lpSource := 1 to Length(DecSource) div 4 do
      begin
        SourceValue := StrToInt('$' + Copy(DecSource, (lpSource - 1) * 4 + 1, 4));
        if Password.Length = 0 then
          PassValue := 0
        else
        begin
          PassValue := Word(Password.Chars[lpPass]);
          Inc(lpPass);
          if lpPass >= Password.Length then lpPass := 0;
        end;
        EDValue := SourceValue xor PassValue;
        Dest.Append(Char(EDValue));
      end;
      //処理結果を返り値にセット
      Result:=Dest.ToString;
    finally
      Password.Free;
      Dest.Free;
    end;
  end;
end;

サインイン時にIDとして入力するメールアドレスは暗号化されてiniファイルに保存され、FormCreate時にこれを復号して、Editコントロールに表示する。

procedure TForm1.FormCreate(Sender: TObject);
var
  Ini: TIniFile;
  strID, strX, strY, strWaitTime: String;
  i:integer;
begin

  //Formを最大化して表示
  Form1.WindowState:=wsMaximized;

  //待ち時間の選択肢(100~3000ミリ秒を100ミリ秒単位で用意)
  for i := 1 to 30 do
  begin
    cmbWaitTime.Items.Add(IntToStr(i*100));
  end;

  //iniファイルの存在を確認
  if FileExists(ChangeFileExt(Application.ExeName, '.ini')) then
  begin
    //iniファイルからデータを読込み
    Ini := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
    try
      strID:=Ini.ReadString('Section', 'ID', '');
      strX:=Ini.ReadString('Section', 'IchiX', '580');
      strY:=Ini.ReadString('Section', 'IchiY', '420');
      strWaitTime:=Ini.ReadString('Section', 'WaitTime', '500');
    finally
      Ini.Free;
    end;
    //復号して表示
    Edit1.Text:=EDText(strID, IntToStr(HashOf('XXXXXXXX')), False);
    EditX.Text:=strX;
    EditY.Text:=strY;
    cmbWaitTime.Text:=strWaitTime;
  end;

  //Navigate
  EdgeBrowser1.Navigate('https://onedrive.live.com/about/ja-jp/signin/');

end;

(4)カーソル位置の座標を取得

マウスのカーソルが現在置かれている位置のスクリーン座標を取得してLabelに表示。

procedure TForm1.chkZahyoClick(Sender: TObject);
begin
  if chkZahyo.Checked then
  begin
    //Enabled
    Timer1.Enabled:=True;
  end else begin
    //Enabled
    Timer1.Enabled:=False;
    LabelXY.Caption:='[X座標, Y座標]';
  end;
end;

Timer1のOnTimerプロパティをダブルクリックして作成されたTimer1Timer手続きに次のコードを記述。これでほぼリアルタイムにカーソルの位置座標を取得して表示できる。

procedure TForm1.Timer1Timer(Sender: TObject);
var
  lh_Handle:  HWND;
  lpt_Pos:    TPoint;
  lrc_Rect:   TRect;
  lrg_Region: HRGN;
  li_Ret:     Integer;
begin
  if chkZahyo.Checked then
  begin
    //マウスカーソル位置をスクリーン座標で取得
    GetCursorPos(lpt_Pos);
    //自身のウィンドウリージョンを調べる
    lh_Handle := Self.Handle;

    //ウィンドウリージョン取得のため空のリージョンを作っておく
    lrg_Region := CreateRectRgn(0,0,0,0);
    try
      //ウィンドウリージョン取得
      li_Ret := GetWindowRgn(lh_Handle, lrg_Region);
      if (li_Ret <> ERROR) then begin
        //ウィンドウのRectを取得
        GetWindowRect(lh_Handle, lrc_Rect);
        //スクリーン座標からウィンドウの左上を原点とした座標に変換
        lpt_Pos.X := lpt_Pos.X - lrc_Rect.Left;
        lpt_Pos.Y := lpt_Pos.Y - lrc_Rect.Top;
        //ウィンドウリージョン内にマウスカーソルがあるかテスト
        if (PtInRegion(lrg_Region, lpt_Pos.X, lpt_Pos.Y)) then begin
          LabelXY.Caption:=Format('OK %d (%d-%d)', [li_Ret, lpt_Pos.X, lpt_Pos.Y]);
        end else begin
          LabelXY.Caption:=Format('NG %d (%d-%d)', [li_Ret, lpt_Pos.X, lpt_Pos.Y]);
        end;
      end else begin
        LabelXY.Caption:=Format('[X:%d, Y:%d]', [lpt_Pos.X, lpt_Pos.Y]);
      end;
    finally
      DeleteObject(lrg_Region);
    end;
  end;
end;

(5)プログラムコードから指定位置をクリック

前回作成したプログラムでいちばん、悩んだのがここ。最初はサインイン画面のウィンドウハンドルを取得して文字列を送信しようと思ったんだけれど・・・これがうまくいかない。その詳細は前回の記事を参照してください。

さんざん悩んで、ようやく思いついた方法がプログラムコードで画面上の任意の位置をクリックする方法。Formが完全に描画された段階で、指定位置のクリックと、その位置への文字列の入力を実行している。そのコードを再掲。

  private
    { Private 宣言 }

    //アドレス貼り付け実行の成否
    boolInput:boolean;

    fgWaitBreak : boolean;  //変数は「functionより先に定義」する

    //待ち関数  指定カウントが経過すれば True, 中断されたならば False
    function WaitTime(const t: integer): Boolean;

    //Formの表示終了イベントを取得
    procedure CMShowingChanged(var Msg:TMessage); message CM_SHOWINGCHANGED;


//待機関数
function TForm1.WaitTime(const t: integer): Boolean;
var
  Timeout: TDateTime;
begin
  //待ち関数  指定カウントが経過すれば True, 中断されたならば False
  fgWaitBreak := False;
  Timeout := Now + t/24/3600/1000;
  while (Now < Timeout)and not fgWaitBreak do begin
    Application.ProcessMessages;
    Sleep(1);
  end;
  Result := not fgWaitBreak;
end;


procedure TForm1.CMShowingChanged(var Msg: TMessage);
var
  dwFlags : DWORD;
  X,Y : Integer;
  LKeyByte : Byte;
begin
  inherited; {通常の CMShowingChagenedをまず実行}
  if Visible then
  begin

    Update; {完全に描画}

    if Edit1.Text='' then
    begin
      Edit1.SetFocus;
      Exit;
    end;

    if (EditX.Text='') or (EditY.Text='') then
    begin
      if EditX.Text='' then EditX.SetFocus;
      if EditY.Text='' then EditY.SetFocus;
      Exit;
    end;

    fgWaitBreak:=False;

    //さらに念のためちょっと待機
    WaitTime(StrToInt(cmbWaitTime.Text));

    dwFlags:=MOUSEEVENTF_MOVE or MOUSEEVENTF_ABSOLUTE;

    //クリック位置を取得
    X:=Trunc(StrToInt(EditX.Text)/Screen.Width*65537);
    Y:=Trunc(StrToInt(EditY.Text)/Screen.Height*65535);

    //移動
    Mouse_Event(dwFlags,X,Y,0,0);
    Application.ProcessMessages;

    //クリック
    Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
    Application.ProcessMessages;

    WaitTime(StrToInt(cmbWaitTime.Text));

    Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
    Application.ProcessMessages;
    Mouse_Event(MOUSEEVENTF_LEFTUP,0,0,0,0);

    //文字列を送信
    boolInput:=False;
    try

      //クリップボードを初期化
      Clipboard.Clear;

      //文字列をクリップボードへ
      Clipboard.AsText:=Edit1.Text;
      
      //[Ctrl] + [V] のキー操作
      LKeyByte := Ord('V');
      keybd_event(VK_CONTROL, MapVirtualKey(VK_CONTROL, 0), 0, 0);
      keybd_event(LKeyByte,   MapVirtualKey(LKeyByte, 0),   0, 0);
      keybd_event(LKeyByte,   MapVirtualKey(LKeyByte, 0),   KEYEVENTF_KEYUP, 0);
      keybd_event(VK_CONTROL, MapVirtualKey(VK_CONTROL, 0), KEYEVENTF_KEYUP, 0);
      //操作に成功
      boolInput:=True;
    except
      //操作に失敗
      boolInput:=False;
    end;

    //貼り付け操作に成功した場合は入力ボタンを操作不可に設定
    if boolInput then btnCopy.Enabled:=False;

  end;

end;

{入力ボタンのClick手続きは、確認メッセージの表示以外は上のコードとほとんど同じ}

また、予期せぬ事故を防止するため、プログラムの終了時にはクリップボードを空に(初期化)する。

procedure TForm1.FormDestroy(Sender: TObject);
begin
  //クリップボードを初期化
  Clipboard.Clear;
end;

(6)ダウンロードフォルダを開く

OneDriveからデータのダウンロードが無事終了すれば、次のようにダウンロードフォルダを開くリンク付きのWindowが表示されるから、特殊なフォルダである「ダウンロードフォルダを開く」ボタンは、別になくてもかまわない気もするけど。

このWindowは移動できない?

もしかしたら任意のタイミングで、それを開きたい時があるかもしれない。エクスプローラーを開けばいいじゃないかという意見は、ここでは聞かなかったことに。

ダウンロードフォルダを開くプログラムコード。

uses
  Vcl.Clipbrd, System.IniFiles, System.UITypes,
  Winapi.ShlObj, Winapi.KnownFolders, Winapi.ShellAPI;

procedure TForm1.btnOpenDLFolderClick(Sender: TObject);
var
  FolderID:TGUID;
  FolderPath:PChar;
  D_FolderPath, ExeFileName:string;
  LhInst:Cardinal;
begin
  FolderID:=StringToGUID('{374DE290-123F-4565-9164-39C4925E467B}');
  if SHGetKnownFolderPath(FolderID,0,0,FolderPath)= S_OK then
  begin
    D_FolderPath := FolderPath;
    //確認
    //ShowMessage(D_FolderPath);
    //ダウンロードフォルダを開く
    ExeFileName:= 'explorer.exe';
    LhInst:=ShellExecute(Handle, 'open', PChar(ExeFileName), PChar(D_FolderPath), nil, SW_SHOW);
    if LhInst <= 32 then
    begin
      MessageBox(Handle, '起動に失敗しました.', '情報', MB_ICONINFORMATION);
    end;
  end;
end;

(7)リソースにDLLを埋め込む

このプログラムの動作には「WebView2Loader.dll」が必須(WebView2Loader.dll は、アプリがデバイス上で WebView2 ランタイム (Microsoft Edge プレビュー チャネル) を見つけるのに役立つコンポーネントであるとのこと)。

WebView2 アプリを 1 つの実行可能ファイルとして配布する

https://learn.microsoft.com/ja-jp/microsoft-edge/webview2/how-to/static

このDLLがないと困るので、添付忘れを防止するため、リソースに埋め込んでおいて、プログラムの実行時にexeのある場所にその有無を確認し、なければリソースから生成するように設定。

メニューの「プロジェクト」→「リソースと画像」で埋め込むDLLを指定

で、FormCreate時に有無を確認、なければexeのある場所に生成。

procedure TForm1.FormCreate(Sender: TObject);
var
  Ini: TIniFile;
  strID, strX, strY, strWaitTime: String;
  i:integer;
  dllFileName:string;
begin

  //リソースからDLLを(なければ)生成
  //rijnファイルの位置を指定
  dllFileName:=ExtractFilePath(Application.ExeName)+'WebView2Loader.dll';
  //rijnファイルの存在を確認
  if not FileExists(dllFilename) then
  begin
    //リソースを再生
    with TResourceStream.Create(hInstance, 'Resource_1', RT_RCDATA) do
    begin
      try
        SaveToFile(dllFileName);
      finally
        Free;
      end;
    end;
  end;

  ・・・

end;

(8)操作方法の案内

この他に、画面最下部に設置したStatusBarに次のような案内を表示できるようにした。

操作方法の案内をStatusBarに表示(OKをクリックすると消える)
案内を表示する/しないはユーザーが選択して、その設定状態の保存も可能に

操作方法の案内の表示/非表示の切り替え。

procedure TForm1.chkInfoClick(Sender: TObject);
var
  strInfo:string;
  strWidth:integer;
begin
  if chkInfo.Checked then
  begin
    //表示する文字列
    strInfo:='ID(メールアドレス)が自動入力されないときは、Ctrl+V で入力できます!';
    strWidth:=StatusBar1.Canvas.TextWidth(strInfo);
    btnOK.Visible:=True;
    with btnOK do
    begin
      Parent:=StatusBar1;
      Left:=strWidth-20;
      Top:=1;
    end;
    //StatusBar1の設定(重要:このプロパティがFalseだとStatusBarにテキストが表示されない)
    StatusBar1.SimplePanel:=True;
    //Info
    StatusBar1.SimpleText:=strInfo;
  end else begin
    StatusBar1.SimpleText:='';
    btnOK.Visible:=False;
  end;
end;

案内を「表示する」が選ばれていた場合はFormCreate時に案内表示を出すよう設定。

procedure TForm1.FormCreate(Sender: TObject);
var
  Ini: TIniFile;
  strID, strX, strY, strWaitTime: String;
  i:integer;
  dllFileName:string;
  strWidth:Integer;
  strInfo:string;
  boolInfo:boolean;
begin

  if chkInfo.Checked then
  begin
    //表示する文字列
    strInfo:='ID(メールアドレス)が自動入力されないときは、Ctrl+V で入力できます!';
    strWidth:=StatusBar1.Canvas.TextWidth(strInfo);
    with btnOK do
    begin
      Parent:=StatusBar1;
      Left:=strWidth-20;
      Top:=1;
    end;
    //StatusBar1の設定(重要:このプロパティがFalseだとStatusBarにテキストが表示されない)
    StatusBar1.SimplePanel:=True;
    //Info
    StatusBar1.SimpleText:=strInfo;
  end;

  ・・・

  //iniファイルの存在を確認
  if FileExists(ChangeFileExt(Application.ExeName, '.ini')) then
  begin
    //iniファイルからデータを読込み
    Ini := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
    try
      strID:=Ini.ReadString('Section', 'ID', '');
      strX:=Ini.ReadString('Section', 'IchiX', '580');
      strY:=Ini.ReadString('Section', 'IchiY', '420');
      strWaitTime:=Ini.ReadString('Section', 'WaitTime', '500');
      boolInfo:=Ini.ReadBool('Section','Info',True);
    finally
      Ini.Free;
    end;
    //復号して表示
    Edit1.Text:=EDText(strID, IntToStr(HashOf('adminy')), False);
    EditX.Text:=strX;
    EditY.Text:=strY;
    cmbWaitTime.Text:=strWaitTime;
    chkInfo.Checked:=boolInfo;
  end;

  ・・・

end;

案内そのものを表示したくない場合は、ユーザーの自由意思でその設定も可能に。

procedure TForm1.btnSaveClick(Sender: TObject);
var
  strID:string;
  Ini:TIniFile;
begin

  //入力の有無をCheck
  ・・・

  //暗号化
  strID:=EDText(Edit1.Text, IntToStr(HashOf('adminy')), True);

  //iniファイルに保存
  Ini := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  try
    //保存
    Ini.WriteString('Section', 'ID', strID);
    Ini.WriteString('Section', 'IchiX', EditX.Text);
    Ini.WriteString('Section', 'IchiY', EditY.Text);
    Ini.WriteString('Section', 'WaitTime', cmbWaitTime.Text);
    Ini.WriteBool('Section','Info',chkInfo.Checked);
    //Userに通知
    MessageDlg('現在の設定を保存しました!', mtInformation, [mbOk] , 0);

    if not btnCopy.Enabled then btnCopy.Enabled:=True;

  finally
    Ini.Free;
  end;

end;

3.まとめ

(1)TEdgeBrowserを使えばOneDriveのサインイン画面をエラーなしで表示できる。
(2)サインイン画面へのIDの入力はプログラムコードで実行可能。
(3)IDはクリップボードに送信しておき、Ctrl+Vでも貼り付け可能に設定。

4.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

サインイン

追記(20230827 OneDriveアプリからオンライン表示へ切り替え)

無駄にプログラムなんか書く必要はありませんでした!

以下、『いかに苦労してOneDriveにサインインするか』という、上記サインイン 4に辿り着くまでの、長いながいまわり道の記録です。なので、お読みいただく価値がないことを最初に申し添えます。m(__)m

OneDrive接続専用Browserを作る!

OneDriveのサインイン画面にメールアドレスを自動入力
あとはEnterキーを叩くだけ
どうしてもコレを実現したかった!

【今回の記事】

1.動機は同期
2.WebView4Delphiコンポーネントを使う
3.プログラムでクリックを実行
4.特殊なフォルダを表示する
5.まとめ
6.お願いとお断り

1.動機は同期

この春から勤務先が変わり、それに伴って職場のPC環境も大きく変化して、ファイルのやり取りにOneDriveを利用することが多くなった。今までは、Windows11のアプリとして設定されているOneDriveを常に起動しておいて、必要な時、タスクバーから呼び出して使っていたが、バックアップ的な意味合いで利用することが多く、ファイル交換用途での利用はそれほど多くなかった。

複数のPC間でのデータのやりとりにOneDriveを利用するしかない現在の環境では、今まであまり考えたことがなかった同期のタイミングが問題になってきた。特に、それが『今すぐ』別のPCで使いたいファイルの場合、こっちのPCからクラウドにデータをアップロードして、直ちに、あっちのPCでそのデータをダウンロードしたいのだけれど、アプリのOneDriveでそれを実現する方法がわからない。

Google先生に尋ねても、『コレだ!』という答えは見つからず・・・。
(OneDriveの正しい使い方を私が知らないだけなのかもしれませんが)

仕方がないからアプリではなく、WebブラウザからOneDriveにサインインして、別のPCですぐに使いたいファイル(やフォルダ)をアップロード。別PC側でも同様にしてWebブラウザからOneDriveにサインイン、目的のファイル(やフォルダ)をダウンロードしていたんだけど・・・。

いったん接続してしまえば、ほっといても特に問題はないし、最新の状態に表示を更新したければ F5キー を押すだけだから、いいっちゃいいんだけど・・・。

OneDriveへのサインイン時にメールアドレスを入力するのが、かなりめんどくさい。

特に急いでいる時に入力を間違えたりすると、余計、イライラして、精神的に非常によろしくない。メモ帳にID替わりのメールアドレスを入力、デスクトップに保存しておいて、それをコピペすればいいかと思ったのだけれど、それすら面倒に感じてしまう自分がいよいよ情けなくなった・・・。

魂が腐っている・・・

他に作りたいプログラムも特に今はないし、仕事もそんなに忙しくはないから、思い切ってOneDriveへデータを送受信できる専用ブラウザを作ることにした。

仕様は単純明快。起動したらOneDriveのサインイン画面を表示、メールアドレスを自動入力、あとはEnterキーを叩くだけ。これさえ出来れば、仕事はかなり快適に。

Delphiと力を合わせれば、そんなの1日でできるー!(・・・と、いつも思う)
かくしてOneDrive接続専用Browser作りが日曜日の朝、スタートしたのでした。

2.WebView4Delphiコンポーネントを使う

すぐに思い出したのは、(いつか、いじってみたい)と思っていたWebView4Delphiコンポーネント。

何かでその存在を知り、ダウンロードして、ちょっと触れてみたのは・・・確か、去年のことだった・・・と思いながら、2022年の作業を記録したフォルダ内をさがすとやっぱりあったWebView4Delphiコンポーネント。さっそく、これを今年の作業フォルダへコピーする。試しにdemoフォルダ内のプログラムを動かしてみると、問題なく動く。去年、ダウンロードした際に、コンポーネントのインストールまで行っていたようだ。

WebView4Delphiは、GitHub の salvadordf / WebView4Delphi からダウンロードできる。

salvadordf / WebView4Delphi

https://github.com/salvadordf/WebView4Delphi

Aboutには次の記述があり、

WebView4Delphi is an open source project created by Salvador Díaz Fau to embed Chromium-based browsers in applications made with Delphi or Lazarus/FPC for Windows.

https://github.com/salvadordf/WebView4Delphi

ライセンスはMITだから、利用にあたっては「著作権表示および許諾表示をソフトウェアのすべての複製または重要な部分に記載」すればOK! 面倒なことは一切ない。

MITライセンスの正しい著作権表示および許諾表示の入れ方を教えてくれるWebサイト様もある。なんと有難いことか。作成者の方に心から感謝。

40代からプログラミング!
MITライセンスとは?無料ツール・テンプレートの利用方法と注意点

https://biz.addisteria.com/mit-license-1/

さっそく、WebView4Delphi コンポーネントに添付されている

demos\Delphi_VCL\MiniBrowser

これをベースにして、OneDrive接続専用Browser作りを開始する。

まず、次のようにGUIを作成。メールアドレスの自動入力という「目的」に合わせてVCLコンポーネントを追加する。

「ダウンロードフォルダを開く」は、後で説明。実行時、その右側のチェックボックス「設定」をチェックすると「入力ボタン」より右側のコントロールが出現する。

で、FormCreate手続きに自分用のコードを追加。

implementation

{$R *.dfm}

uses
  uTextViewerForm,
  uWVCoreWebView2WebResourceResponseView, uWVCoreWebView2HttpResponseHeaders,
  uWVCoreWebView2HttpHeadersCollectionIterator,
  uWVCoreWebView2ProcessInfoCollection, uWVCoreWebView2ProcessInfo,
  uWVCoreWebView2Delegates,
  //以下を追加
  Vcl.Clipbrd,
  System.IniFiles, System.UITypes,
  Winapi.ShlObj, Winapi.KnownFolders,
  Winapi.ShellAPI;

procedure TMiniBrowserFrm.FormCreate(Sender: TObject);
var
  Ini: TIniFile;
  strID, strX, strY: String;
begin
  FGetHeaders             := True;
  FHeaders                := TStringList.Create;
  FFileStream             := nil;
  FUserAuthFrm            := nil;
  FResourceContents       := nil;
  FBlockImages            := False;
  FDownloadIDGen          := 0;
  FDownloadOperation      := nil;
  WVBrowser1.DefaultURL   := URLCbx.Text;

  //Formを最大化して表示
  MiniBrowserFrm.WindowState:=wsMaximized;

  //iniファイルの存在を確認
  if FileExists(ChangeFileExt(Application.ExeName, '.ini')) then
  begin
    //iniファイルからデータを読込み
    Ini := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
    try
      strID:=Ini.ReadString('Section', 'ID', '');
      strX:=Ini.ReadString('Section', 'IchiX', '580');
      strY:=Ini.ReadString('Section', 'IchiY', '420');
    finally
      Ini.Free;
    end;
    //復号して表示(実際のプログラムではメールアドレスは暗号化処理している)
    Edit1.Text:=strID;
    EditX.Text:=strX;
    EditY.Text:=strY;
  end;

end;

あと、EXE の実行には、EXE のあるフォルダ内 に WebView2Loader.dll が必要。今回作成するのは64 ビット版の EXE だから bin64 フォルダ内の 64 ビット用DLL を使用しなければならない。

実際の動作の様子。実行時画面では・・・

「設定」にチェックすると・・・

ここにiniファイルから読み取ったサインイン用のID(メールアドレス)と、プログラムからクリックするアドレス入力欄の座標を表示(入力)する。

画面解像度が異なるPCでは、当然、フォーカスを当てたいサインイン用のメールアドレスを入力するフレームの表示位置が異なるから、最初にその位置座標を調べる必要がある。座標Checkにチェックを入れるとマウスポインタの現在位置のスクリーン座標が画面右上にリアルタイムで表示される仕組みだ。

マウスポインタが現在ある位置の座標を取得して表示するコードは・・・

procedure TMiniBrowserFrm.Timer2Timer(Sender: TObject);
var
  lh_Handle:  HWND;
  lpt_Pos:    TPoint;
  lrc_Rect:   TRect;
  lrg_Region: HRGN;
  li_Ret:     Integer;
begin
  if chkZahyo.Checked then
  begin
    //マウスカーソル位置をスクリーン座標で取得
    GetCursorPos(lpt_Pos);
    //自身のウィンドウリージョンを調べる
    lh_Handle := Self.Handle;

    //ウィンドウリージョン取得のため空のリージョンを作っておく
    lrg_Region := CreateRectRgn(0,0,0,0);
    try
      //ウィンドウリージョン取得
      li_Ret := GetWindowRgn(lh_Handle, lrg_Region);
      if (li_Ret <> ERROR) then begin
        //ウィンドウのRectを取得
        GetWindowRect(lh_Handle, lrc_Rect);
        //スクリーン座標からウィンドウの左上を原点とした座標に変換
        lpt_Pos.X := lpt_Pos.X - lrc_Rect.Left;
        lpt_Pos.Y := lpt_Pos.Y - lrc_Rect.Top;
        //ウィンドウリージョン内にマウスカーソルがあるかテスト
        if (PtInRegion(lrg_Region, lpt_Pos.X, lpt_Pos.Y)) then begin
          LabelXY.Caption:=Format('OK %d (%d-%d)', 
            [li_Ret, lpt_Pos.X, lpt_Pos.Y]);
        end else begin
          LabelXY.Caption:=Format('NG %d (%d-%d)', 
            [li_Ret, lpt_Pos.X, lpt_Pos.Y]);
        end;
      end else begin
        LabelXY.Caption:=Format('X:%d, Y:%d', [lpt_Pos.X, lpt_Pos.Y]);
      end;
    finally
      DeleteObject(lrg_Region);
    end;
  end;
end;

で、最適なクリックポイントのX座標とY座標を読み取り、Editに入力、保存ボタンをクリックでiniファイルに保存する。

クライアント座標でなく、スクリーン座標としたのは、話をカンタンにするため。だからFormCreate手続きでFormを最大化して表示するように設定している。こうすればどんな解像度のPCでも、画面左上からのスクリーン座標でメールアドレスを入力するフレームの位置が決定できると考えたのだ。

実は、最初は「他のアプリへ文字列を送信」する方法で、サインイン画面を狙い撃ちしようと思っていたのだが、ブラウザの入力欄は「ウィンドウではない」ようで、目印にするハンドルがなく、簡単には文字列を送信できないことがわかった。

朝5時くらいから作業を始めて、半日くらいイロイロ悩んだのだけれど、お昼過ぎにようやくサインインのフレームが表示されている位置をプログラムからクリックして、フォーカスを当て、さらにプログラムからCtrl+V(貼り付け)の操作を行い、iniファイルからクリップボードに読み込んでおいたメールアドレスを流し込めばイイと気づく。

そこで大きく方針を転換。文字列を送信ではなく、サインインのフレームをクリックしてメールアドレスを貼り付ける方向でGUIも、プログラムも準備した。

さらに「保存」ボタンは、メールアドレスとPCごとに異なるサインインのフレームの座標をiniファイルに書き込んで記録するために設置。

これで OneDrive接続専用Browser のGUIは完成。あとはプログラムを書くだけに。

※ 実際のプログラムでは、iniファイルにメールアドレスを保存する際に、さらにひと手間かけて暗号化、iniファイルから読みだす際に復号している。

3.プログラムでクリックを実行

普段、僕がいちばんよく利用するWebブラウザはFirefoxだ。インターネット黎明期、そう誰もが Netscape Navigator を使っていた頃からのファンなのだ。

プログラムを何度も動かして動作確認しているうちに、あることに気づく。それは何かと言うと、OneDriveのサインイン画面を表示した時の挙動が、Firefoxと作成中のOneDrive接続専用Browserではちょっと違うのだ。

FirefoxでOneDriveのサインイン画面を表示した場合は、メールアドレスを入力するフレームにセットフォーカスされた状態でサインイン画面が表示されるのに対し、作成中のOneDrive接続専用Browserではそうならない。しかも、アドレス入力欄を1回クリックしただけではダメ(セットフォーカスされない)で、2回クリックしないと入力待機状態にならない(1回めのクリックで、一瞬セットフォーカスされたように見えるが、その後、キャレットが消失してしまい、点滅状態にならない)。

DelphiのIDEから、Ctrl+F で「SetFocus」を検索キーワードにコード全体を確認しても、それは「見つからなかった」。なぜ、2回クリックしないと入力待機状態にならないのか、その原因はさっぱりわからない。

原因はわからなくても、とにかくそれが現実だから、Webにあった情報を頼りにプログラムからアドレス入力欄をクリックするコードを書いてみた。

動的にマウスをクリックするには?

https://www.petitmonte.com/bbs/answers?question_id=2014

予め、先に示したマウスカーソルの位置座標を調べるコードで、アドレス入力欄の座標を調査・記録しておいて、Formが完全に描画されたところでプログラムコードからアドレス入力欄をクリックする。

  private
    { Private 宣言 }
    //アドレス貼り付け実行の成否
    boolInput:boolean;
    //Formの表示終了イベントを取得
    procedure CMShowingChanged(var Msg:TMessage); message CM_SHOWINGCHANGED;

procedure TMiniBrowserFrm.CMShowingChanged(var Msg: TMessage);
var
  dwFlags : DWORD;
  X,Y : Integer;
  //LKeyByte : Byte;
begin
  inherited; {通常の CMShowingChagenedをまず実行}
  if Visible then
  begin
    Update; {完全に描画}
    fgWaitBreak:=False;
    WaitTime(1000);
    //クリップボードを初期化
    Clipboard.Clear;
    //文字列をクリップボードに格納
    Clipboard.AsText:=Edit1.Text;

    dwFlags:=MOUSEEVENTF_MOVE or MOUSEEVENTF_ABSOLUTE;
    X:=Trunc(StrToInt(EditX.Text)/Screen.Width*65537);
    Y:=Trunc(StrToInt(EditY.Text)/Screen.Height*65535);

    //移動
    Mouse_Event(dwFlags,X,Y,0,0);
    Application.ProcessMessages;

    //クリック
    Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
    Application.ProcessMessages;

    WaitTime(300);

    Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
    Application.ProcessMessages;

    Mouse_Event(MOUSEEVENTF_LEFTUP,0,0,0,0);

    // [Ctrl] + [V] のキー操作 -> btnCopyClick手続きで実行
    {
    LKeyByte := Ord('V');
    keybd_event(VK_CONTROL, MapVirtualKey(VK_CONTROL, 0), 0, 0);
    keybd_event(LKeyByte,   MapVirtualKey(LKeyByte, 0),   0, 0);
    keybd_event(LKeyByte,   MapVirtualKey(LKeyByte, 0),   KEYEVENTF_KEYUP, 0);
    keybd_event(VK_CONTROL, MapVirtualKey(VK_CONTROL, 0), KEYEVENTF_KEYUP, 0);
    }
    WaitTime(1500);
    btnCopyClick(nil);
    if boolInput then btnCopy.Enabled:=False;
  end;
end;

WaitTimeなる関数は、次のWebサイト様で紹介されていたものをそのまま使用。

待ち関数の必要性

https://gumina.sakura.ne.jp/CREATION/OLD/COLUMN/CD1MATI.htm
function TMiniBrowserFrm.WaitTime(const t: integer): Boolean;
var
  Timeout: TDateTime;
begin
  //待ち関数  指定カウントが経過すれば True, 中断されたならば False
  fgWaitBreak := False;
  Timeout := Now + t/24/3600/1000;
  while (Now < Timeout)and not fgWaitBreak do begin
    Application.ProcessMessages;
    Sleep(1);
  end;
  Result := not fgWaitBreak;
end;

WaitTime関数の引数の値を様々に変えて実行してみると、MyPCでは、上記の数値で確実にアドレス入力欄をプログラムコードからクリックすることに成功!

メールアドレスをCtrl+Vする部分は、GUIの「入力」ボタンと共用だからボタンのClick手続き側に記述して呼び出す(実行する)ことにする。これだと都合4回、アドレス入力欄をクリックすることになるが、単にクリックするだけだから2回でも、4回でも大差ないだろう。むしろ、確実に動かすための保険だと考え、このままにする。

procedure TMiniBrowserFrm.btnCopyClick(Sender: TObject);
var
  dwFlags : DWORD;
  X,Y : Integer;
  LKeyByte : Byte;
begin

  boolInput:=False;

  try

    //クリップボードを初期化
    Clipboard.Clear;
    //文字列をクリップボードに格納
    Clipboard.AsText:=Edit1.Text;

    dwFlags:=MOUSEEVENTF_MOVE or MOUSEEVENTF_ABSOLUTE;
    X:=Trunc(StrToInt(EditX.Text)/Screen.Width*65537);
    Y:=Trunc(StrToInt(EditY.Text)/Screen.Height*65535);

    //移動
    Mouse_Event(dwFlags,X,Y,0,0);
    //クリック
    Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
    //Mouse_Event(MOUSEEVENTF_LEFTUP,0,0,0,0);
    Application.ProcessMessages;
    WaitTime(500);
    Mouse_Event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
    Mouse_Event(MOUSEEVENTF_LEFTUP,0,0,0,0);
    Application.ProcessMessages;

    // [Ctrl] + [V] のキー操作
    LKeyByte := Ord('V');
    keybd_event(VK_CONTROL, MapVirtualKey(VK_CONTROL, 0), 0, 0);
    keybd_event(LKeyByte,   MapVirtualKey(LKeyByte, 0),   0, 0);
    keybd_event(LKeyByte,   MapVirtualKey(LKeyByte, 0),   KEYEVENTF_KEYUP, 0);
    keybd_event(VK_CONTROL, MapVirtualKey(VK_CONTROL, 0), KEYEVENTF_KEYUP, 0);

    boolInput:=True;

  except

    boolInput:=False;

  end;

end;

動作確認すると、ごく、たまーにメールアドレスが自動入力されないこともあるが、9割方予期した通りに動作してくれる。メールアドレスが自動入力されない場合でも、クリップボードにデータは間違いなく読み込まれているから、手動でCtrl+Vすればいいだけだ。

いちいち、メールアドレスを入力することに比べれば、Ctrl+VしてEnterキーを押す方がずっとカンタンだ。イイ感じになってきた。

4.特殊なフォルダを表示する

実際にファイルをOneDriveへ、アップロードしたり、ダウンロードしたり、動作確認を行ってみると、任意のタイミングでダウンロードフォルダを開く機能が欲しくなった。なので、これをボタンクリックで実行できるようにする。

ダウンロードフォルダができたのはWindows Vista以降のようで、この特殊なフォルダへのPathを取得するには、SHGetKnownFolderPath 関数を使うらしい。コードはいつもお世話になるMr.XRAYさんのWebサイトの記事を参考にして書く。

460_特殊フォルダのフルパスを取得

http://mrxray.on.coocan.jp/Delphi/plSamples/460_SpecialFolderPath.htm#08

GUIDの一覧は、次のWebサイトにあった。こちらの情報でダウンロードフォルダのGUIDがわかった。

ファイル ダイアログ ボックスのカスタム プレイス用既知のフォルダー GUID

https://learn.microsoft.com/ja-jp/dotnet/desktop/winforms/controls/known-folder-guids-for-file-dialog-custom-places?view=netframeworkdesktop-4.8

ボタンクリックで、ダウンロードフォルダを開く手続きは次の通り。

procedure TMiniBrowserFrm.btnOpenDLFolderClick(Sender: TObject);
var
  FolderID:TGUID;
  FolderPath:PChar;
  D_FolderPath, ExeFileName:string;
  LhInst:Cardinal;
begin
  //ダウンロードフォルダのGUIDを指定
  FolderID:=StringToGUID('{374DE290-123F-4565-9164-39C4925E467B}');
  if SHGetKnownFolderPath(FolderID,0,0,FolderPath)= S_OK then
  begin
    D_FolderPath := FolderPath;
    //ダウンロードフォルダを開く
    ExeFileName:= 'explorer.exe';
    LhInst:=ShellExecute(Handle, 'open', PChar(ExeFileName), 
      PChar(D_FolderPath), nil, SW_SHOW);
    if LhInst <= 32 then
    begin
      MessageBox(Handle, '起動に失敗しました.', '情報', MB_ICONINFORMATION);
    end;
  end;
end;

5.まとめ

WebView4Delphiコンポーネントのdemoにあるサンプル(MiniBrowser)に、以下の内容を追加したプログラムを使えばOneDrive自動サインインは可能。その手順は次の通り。

(1)プログラムコードでアドレス入力欄をクリックしてセットフォーカス。
(2)クリップボードに送ったメールアドレスを、プログラムコードでCtrl+V。
(3)Enterキー押し下げでOneDriveへ接続。データは即送受信可能になる。

さらに、SHGetKnownFolderPath 関数でダウンロードフォルダへのPathを取得して、エクスプローラで表示すればより一層便利に使えそう。

6.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

面談日程表の自動作成方法

Googleカレンダーを利用して、三者面談の日程表を自動的に作成する方法を学んだ。

【Gooleカレンダーとフォーム】全自動・三者面談の日程調整をつくる

https://www.fy1203.com/2020/03/21/calendar-form/

やりたいこと、そのものスバリの情報に大感謝!
ただ、残念なことに一部、情報が古くなってしまっており、そこでちょっと試行錯誤があったので、最新の情報を含めるカタチで、作成方法をここにメモ。

【今回の記事】

1.専用カレンダーの作成
2.入力フォームの準備
3.スプレッドシートを準備する
4.スクリプトを用意する
5.トリガーを設定する
6.プレビューで動作確認
7.まとめ
8.お願いとお断り

1.専用カレンダーの作成

(1)Googleアカウントがなければ作成。

(2)Googleのトップページの右上のGoogleアプリから、「カレンダー」をクリック

(3)カレンダーが開くので、左下の他のカレンダーを追加「+」をクリック

(4)「新しいカレンダーを作成」をクリック

(5)カレンダーの名前と説明を決めて、「カレンダーを作成」をクリック

2.入力フォームの準備

(1)次は入力フォームです。まず、フォルダを作成しておきます。

(2)Googleドライブの左上の「新規」をクリックして、サブメニューを表示します。

(3)「新しいフォルダ」をクリックして、

(4)フォルダに最適な名前を付けます。この場合、「三者面談」としました。

(5)そのフォルダに入り、「新規」→「Googleフォーム」と順にクリックします。

(6)新しいフォームの作成画面が開きます。

(7)このGoogleフォームで、入力フォームを作成します。タイトルと説明を入力し、画面中央右の設定ボタンをクリックします。

(8)私は、次のように設定してみました(誤りがあるかもしれません)。

(9)設定が終了したら、質問タブをクリック して、画面を切り替えます。

(10)1問目の出席番号は、「プルダウン」から選択するように設定しました。

(11)入力を「必須」にするため、状態を「ON」にします。

(12)質問を追加します。画面右上の 〇囲みの+マークをクリック します。

(13)2つめの問は名前です。次のように「記述式」にして、「必須」は「ON」の状態にします。終わったら、右の 〇囲みの+マークをクリック します。

(14)3つめの問は予約日です。次のように入力します(日付は適当です)。

(15)メニューから「年を含める」のチェックを外します(年を含めるをクリックすれば、チェックが外れます)。

(16)これで入力が「月、日」だけになりました。さらに・・・

(17)4つめの問は開始時間です。「時刻」にして、「必須」は「ON」の状態にします。

これで、フォームの作成は終了です。

3.スプレッドシートを準備する

(1)次に、回答を整理するスプレッドシートを用意します。画面上の「回答」→「スプレッドシートにリンク」をクリックします。

(2)下の画面が表示されるので、次のようにタイトルを入力して「作成」をクリック。

これで、スプレッドシートが作成されます。

4.スクリプトを用意する

(1)画面の表示が「スプレッドシートにリンク」から「スプレッドシートで表示」に変わっていることを確認して、「スプレッドシートで表示」をクリックします。

(2)新しいページにスプレッドシートが表示されます。ここに必要なスクリプトを追加します。

※ 追加するスクリプトは次のWebサイト様で紹介されているものです。

【Gooleカレンダーとフォーム】全自動・三者面談の日程調整をつくる

https://www.fy1203.com/2020/03/21/calendar-form/

(3)スプレッドシートの画面上から、「拡張機能」をクリックして、表示されるサブメニューにある「App Script」をクリックします。(2023年6月現在の操作方法です。ここが上記Webサイト様の記事と異なります。いつの間にか、現在の形式へと変更されたようです。)

(4)新しいページが開き、次の画面が表示されます。

(5)クッキーの使用を許可します。

(6)「無題のプロジェクト」をクリックして、プロジェクト名を「1年A組面談希望調査」に変更して、下の「名前の変更」をクリックします。

(7)次のようなスクリプトの入力画面が表示されます。

(8)次の引用リンク先のWebサイト様で紹介されているスクリプトを1行目から範囲選択して、クリップボードにコピーし、入力画面に(既存のテキスト全体を選択しておいて)上書きします(関数の名前がMyFunctionからsendToCalendarになり、引数(ひきすう)も「空」でなく「e」になります)。

【Gooleカレンダーとフォーム】全自動・三者面談の日程調整をつくる

https://www.fy1203.com/2020/03/21/calendar-form/

(9)コピペしたスクリプトを3か所「確認」または「変更」します。

【その1】

・4行目のYear指定を確認してください。必要であれば、現在の西暦年に変更します。

【その2】

16行目「カレンダーID」を変更します。

最初のカレンダーの画面を開き、画面左の「1年A組三者面談」の右にある「…」をクリックします。

Googleのトップページの右上から、「カレンダー」をクリック。

画面の左下にある「1年A組三者面談」をポイントすると表示される縦の「・・・」をクリックします。

表示されるサブメニューから「設定と共有」をクリックします。

「カレンダーの設定」画面が表示されます。下へスクロールすると、かなり下の方にカレンダーIDがあります。これをコピーします。

コピーしたIDをスクリプトの16行目に貼り付けます。

※ 「***カレンダーIDを入力***」部分とカレンダーIDを入れ替えます。

【その3】

31行目の面談時間を「確認」または「変更」します。

以上でスクリプトの確認と変更は完了です。

5.トリガーを設定する

(1)スクリプトが自動的に実行されるように設定します。画面右にある<>部分をポイントすると、時計のマークの「トリガー」が現れるので、この「トリガー」をクリックします。

(2)トリガー設定画面が開くので、画面右下にある「トリガーを追加」をクリックします。

(3)イベントの種類を「フォーム送信時」に設定して、「保存」をクリックします。

※ この画面で「実行する関数を選択」が空欄になっている場合は、スクリプトのコピペが1行目を含んだ形で(正しく)行われているかどうか、また、スクリプトが保存されているかどうか、確認してください。

(4)次のエラーメッセージが表示された場合は、ブラウザのポップアップブロック(別Windowを自動的に開かない設定がおそらくデフォルトになっている?)を、このトリガー設定ページからの保存操作であれば解除されるように設定してください(操作方法はブラウザにより異なります)。

(5)紐づけるアカウントをクリックします。

(6)My環境では、Googleからの確認画面が表示されました。画面左下の「Advanced(詳細?)」をクリックします。

(7)(必要であれば、画面を下にスクロールして)「unsafe:安全ではない」と表示されている「1年A組面談希望調査」へのリンクをクリックします。

(8)次の画面が表示されます。

Allow(許可)をクリックします。これで入力フォームからGoogleカレンダーへデータが送信されたときに、自動的にスクリプトが実行されるようになります。

6.プレビューで動作確認

(1)画面右上の「プレビュー(目のマーク)」をクリックします。

(2)入力フォームに必要事項を入力して、「送信」ボタンをクリックします。

(3)送信されたメールの内容を確認します。

(4)Googleカレンダーに予約状況が表示されます。

(5)カレンダーに予約状況が表示されない時は、トリガーの設定が正しく保存されているか、どうか、確認してください。(私が最初にテストした際は、Googleカレンダーに予約状況が表示されませんでした。ちょっと焦りましたが、手順を一つひとつ確認して行く中で、トリガーの設定が正しく保存されていなかったことに気づき、この部分の設定作業をやり直したところ、無事、予約状況がGoogleカレンダーに表示されるようになりました。)

7.まとめ

(1)Googleカレンダーを使えば、三者面談自動予約システムは作成可能。
(2)Webには多くの情報があるが、機能の更新が頻繁にあるので時々見直す必要あり?
(3)最終的な面談時刻の確認は、別の連絡手段で確実に行う必要がある。

8.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

Reorganization

「再編」

そこに、1台のNASがあった。

時とともに アクセスする人も変わり
記録方法も 場所も いつしか ランダムに・・・

参照したい「データ」 それが「どこ」にあるのか
わかりづらくなってしまった NAS・・・

エントロピーは 必ず 増大するから
長期間に及ぶ運用の結果として 「生まれた」 この状況は
理論的には 正しい ・・・ のかもしれない・・・。

ただ・・・ あの日、確かに・・・

なんとか 出来ないのか?

叫ぶような 声を聞いた。
それは 僕に向けての 声ではなかったけれど・・・

僕は 応えようと 思った。
これまでに培ったネットワークドライブ接続に関する知識。

そのすべてを 賭けて。

【目次】

1.ネットワークドライブ
2.システムエラー1219
3.アカウントに読み書き権限を付与
4.まとめ
5.お願いとお断り

1.ネットワークドライブ

かつて、別セグメントにあるファイルサーバに接続して、その共有フォルダをネットワークドライブとして、マイコンピュータに表示するプログラムを書いたことがあった。

そうだ。あのセグメント越えのプログラムを書いたのは・・・ もう10年以上、前のことだ。

あの時は、ファイルサーバを別セグメントに用意する必要があったけれど。
今、共有フォルダを再構成したいNASは、みんながログオンするセグメント上にある・・・。
なんとか・・・しなければならない敷居は、10年前より、ずっと低い。

もちろん、自分的にいちばん、イイのは・・・ 新しいファイルサーバを用意して、現在のファイル共有の仕組みそのものをイチから作り直すこと、なんだけれど。

でも、それは無理だ。今年度、そんな予算は1円だって計上されてない。
今の環境と機材の中で、なんとかするしか、ない。

今あるNASをなんとか創意工夫して、運用するしか、ないのだ。

( 今のファイル共有を維持したまま、新しい共有環境を作るには、どうしたらいい? )

自問自答を繰り返す。
無理だという前提は一切排除する。
あきらめない限り・・・何とか出来る方法が、必ずあるはずだ。
僕は毎日、あらゆる意味で、いちばんイイ方法を考え続けた。

( 再編する共有フォルダのみ ネットワークドライブとして表示すれば いい )

( 再編に必要なフォルダは 予め用意して・・・ )

( そこへ必要なファイルだけを移動させるんだ・・・ )

朝、出勤途中、クルマを運転しながら、そんな考えが浮かんだ。
ようやく答えに繋がるヒントが見えた・・・ 気がした。

職場に着いた僕は、NASを管理するDSMを起動してみた。前の職場では、自前でサーバ機を用意して、アクティブディレクトリを使ったファイル共有の仕組みを作っていたが、それがたまらなく懐かしくなる・・・。

おいおい アクティブディレクトリ・・・ きみは嫌いだったんじゃ ないか?

自分の気持ちの変化に驚きながら、管理画面を見ていて( あれ? )って思った。

( リサイクルフォルダがものすごい容量を喰ってる・・・ )

・・・ってことは、このNAS自体のバックアップの仕組みがどうなっているのか、それはわからないけれど、とにかくNAS全体でゴミ箱の設定が有効になってるわけだ。よくよく見ると一般ユーザーはそこにアクセスできないけれど、管理者にはそれができるようだ・・・。もしかして、バックアップの代わりに、ゴミ箱を有効化してる・・・?

それなら、話は簡単だ。
バックアップ用の媒体を別に用意して、そちらに日々のバックアップをとり、リサイクルフォルダを使わない設定に変更すればいい。これでNASの空き容量を増やせるはずだ。

そうだ。どこかにバックアップさえ、きちんと作れたら・・・
ファイルサーバを新しく用意しなくても、なんとか、なるんじゃないか?

そう思った瞬間、この問題の完全な解決方法が「見えた」気がした。

リサイクルフォルダを使わない設定にすれば、今より確実に空き容量は増える。で、NASのルートに、これまで使われていない「共有」って名前のフォルダを新しく作り、その下に、クライアントPCのマイコンピュータにネットワークドライブとして表示する新しいフォルダ群を用意して、必要なフォルダとファイルだけ、そこに「コピーではなく、移動」すれば、現在のファイル共有環境を生かしたまま、新しいファイル共有環境を同じNASの中に再構成することが出来るんじゃないか・・・。

お金と手間をかければ、もっといい方法もあるのかもしれないが、それが無理な現状ではおそらく、これがベストに近い解決策なんじゃないか? ・・・そう思えてきた。

で、バックアップは・・・ どこへ とればいい?

取り敢えず、僕の手元には、前任者から引き継いだ、用途を限定せずに利用できる空き容量1TBのSSDがある。これに最も重要なデータのバックアップをとろう。で、事務方の責任者に必要十分な容量のバックアップメディアがどうしても必要なことを説明して、理解が得られたら、速やかに可能な限り大容量のバックアップ用HDDを購入してもらおう。

それから、万一の火災等の事故への対応も考えなければならない。
バックアップのバックアップは、どこに、作ればいい?

僕は、先日、複合機のスキャナーでスキャンした画像データを出力する設定を行ったばかりの・・・ 別の部署にあるNASにかなりの空き容量があったことを思い出した。

新しく再編する共有フォルダだけをバックアップするなら、あのNASを利用すればなんとかなるんじゃないか?

これで、だいたいの見通しが立った。あとはやるだけだ。

まず、バックアップ(のバックアップ)用途に使いたいNASの保管場所を、安全な場所に変えなければならない。現在でも夜間はアラームのかかる部屋に、そのNASは設置されているのだが、これを24時間、施錠された部屋に移設することにした。

幸いにしてバックアップ(のバックアップ)用途に使いたいNASが設置されている部屋は、最初から情報処理用途に準備された部屋なので、床下がネットワークの配線に使える。

床の四角いカーペットを剥いで、その下の床板を外し、LEDライトを片手に、24時間施錠された小部屋までの経路を探ってみる。

床下に障害となるような構造物はない。なんとか、なりそうだ。竹製の1m物差しを何本も用意して、床下に差し入れ、LANケーブルを物差しの先端に養生テープで固定して、鍵のかかる小部屋へ向けて1本、2本とそれを養生テープで繋ぎ、少しずつ、慎重にLANケーブルを送る。

無事、LANケーブルは小部屋へ到達。
なんで汗まみれになるのか、知らないが。

バックアップ用のNASのユーティリティの起動方法がわからないので、覚悟を決めて、NASの電源ボタンを長押し。するとWebの解説にあった通り、Beep音が鳴ってNASはシャットダウンされた。

このNASには、もう一つ、何か別用途でのNASが接続されていた。シャットダウン時、果たしてどうなるか心配したけれど、そちらも同時に電源が切れた。なんだか、わからないけれど、取り敢えず二つのNASの電源は連動しているようだ。

NASに繋がっている電源とLANケーブルを全て外し、二つのNASをこの上なく大切に抱えて、24時間施錠された小部屋へ移設する。

鍵のかかる小部屋の床には、幸いにして電源コンセントが用意されていた。部屋の状況から判断しておそらく、以前はここにサーバ機が置かれていたのだろう・・・。

雷対策が施された電源の延長コードをコンセントに差し込んで、これにNASの電源ケーブルを繋ぐ。

続けてLANケーブルも接続。
接続状態に問題がないことを何度も確認し、祈るような気持ちで、NASの電源スイッチをONにする。

何事もなかったかのように、2台のNASが無事、再起動した。
これでハードウェアの準備はOKだ。

次は、NASの共有フォルダをネットワークドライブとして各クライアントPCのマイコンピュータに表示する、オリジナルプログラムを用意しなくてはならない。

僕は以前にDelphiで書いたネットワークドライブ接続のプログラムをバックアップ用のHDDから探し出し、プロジェクトを My PC のデスクトップにコピーした。

Delphiを起動すると、十数年前に作った、懐かしいGUIが現れた。

現在の状況に合わせて、必要な部分を書き換える。
ユーザーが自分の自由意思で、メイン画面からスタートアップに登録できるようにする。
あと、IDとPasswordも暗号化してイニシャライズファイルに保存。プログラム起動時に自動的に読み込んで表示するように設定を変更。

このNASの共有フォルダをネットワークドライブとして表示するプログラムの「接続」ボタンにたどり着くためには、ユーザーは生体認証とPIN入力の2段階認証を潜り抜ける必要があるから、この設定で、セキュリティ的に問題はないはずだ。

ネットワークドライブ接続のインターフェイス

このプログラムを書いた時は、別セグメントにあるファイルサーバに接続できるよう、確か、RouteADDコマンドを使ってクライアント機のルーティングテーブルを書き換えたんだ・・・。で、その際、設定の変更を残す(=記録する)pオプションはわざと指定せずに、シャットダウンすれば自動的に設定が、ルーティングテーブルを書き換える前の状態に戻るようにしたんだ。

このRouteADDコマンドを実行するBATファイルを、プログラム内部で生成して動かすところがすごく難しかったんだ・・・。RunAsAdmin・・・ 確か・・・夏に思い立って、プログラムが完成したのは秋が深まったころだった。

今回、接続したいNASは同じセグメント内にあるから、PCのルーティングテーブルを書き換える必要はない。NASの共有フォルダをクライアント機のマイコンピュータにネットワークドライブとして表示するだけだから、その手続きはより簡単に済む。

あの時、表示する共有フォルダは、「個人フォルダ」・「校内共有」・「校務分掌」・「教科」に設定した気がする。今回、個人フォルダはどうするか・・・?

NASの設定を調べてみるとラッキーなことに、新しくユーザーアカウントを作るとhomesフォルダにそのユーザー専用のホームフォルダが用意されることがわかった。試しに「test-u」というアカウントを作成してみると、homesフォルダに「test-uフォルダ」が確かに出来ている。これを「個人フォルダ」ネットワークドライブとして表示すればいい。

NASのルートには「共有」という名前のフォルダはなかったので、早速それを作成し、その下に「校内共有」・「校務分掌」・「教科」の各フォルダを準備する。あとは接続プログラム側で各共有フォルダへのPathを接続情報として指定すればOKのはずだ。

この画面を表示するにはパスワードが必要

DSMで確認したら、NASのドメイン名は指定されていなかった。koumu.localとでも設定しようかと思ったが、現状の変更を最小限に留めておくことにし、やめておくことにした。

ネットワークドライブを設定して表示するプログラムは次の通り。

procedure TForm1.Button1Click(Sender: TObject); // ネットワークドライブの接続
var
  PW,ID:string;
  //iniファイル読込み
  Ini: TIniFile;
  strW,StrX,StrY,strZ:String;
  strDomainName:String;
  //ネットワークドライブ名変更
  X,D:Variant;
  InfoStr1,InfoStr2,InfoStr3:string;
  //スタートアップに登録
  MyObject : IUnknown;
  MySLink  : IShellLink; // ShlObj
  MyPFile  : IPersistFile; // ActiveX
  Directory : String;
  WFileName : WideString;

const
    MyRegFile : string = 'Software\Microsoft\Windows\CurrentVersion\Explorer';
    MyMessage : string = 'スタートアップに登録しますか?';
    MyFolders : string = 'Startup';

begin

  //接続先ドメイン名を取得
  //iniファイル読込み
  Ini:=TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  try
    //実際にはマスターパスワードを暗号化文字列から復元している
    MasterPassW:='XXXXXXX';
    //テキスト暗号化設定情報を読込み
    strDomainName:=暗号化文字列から複合する関数(Ini.ReadString('セクション', 'DomainName', 'デフォルト値'),
      MasterPassW, False);
  finally
    Ini.Free;
  end;

  //【準備作業】Password,IDを確認して変数へ取得
  try
    //カーソルを待機状態に変更
    Screen.Cursor:=crHourGlass;

    //UserName(ID)確認
    if Edit1.Text='' then
    begin
      MessageDlg('IDが無効です!', mtInformation, [mbOk] , 0);
      Edit1.SetFocus;
      Exit;
    end else begin
      //有効なドメイン名がある場合
      //ID:=strDomainName+'\'+JTrim(Edit1.Text);
      //ドメイン名がない場合
      ID:=JTrim(Edit1.Text);
    end;

    //パスワードを確認
    if Edit2.Text='' then
    begin
      MessageDlg('パスワードが無効です!', mtInformation, [mbOk] , 0);
      Edit2.SetFocus;
      Exit;
    end else begin
      PW:=JTrim(Edit2.Text);
    end;
  finally
    Screen.Cursor:=crDefault;
  end;

  //【第1段階】ルーティング情報の設定を実行

  // 今回接続するNASは同じセグメントにあるので
  //ルーティングテーブルの書き換えは不要

  //【第2段階】ネットワークドライブを追加

  //カーソルを待機状態に変更
  Screen.Cursor:=crHourGlass;

  //ドライブ設定情報読込み
  //iniファイル読込み
  Ini:=TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  try
    //マスターパスワード
    MasterPassW:='XXXXXXX';
    //ShowMessage(MasterPassW);
    //テキスト暗号化設定情報を読込み
    strW:=暗号化文字列から複合する関数(Ini.ReadString('セクション', 'W_Drive', 'デフォルト値'), MasterPassW, False)+JTrim(Edit1.Text);
    strX:=暗号化文字列から複合する関数(Ini.ReadString('セクション', 'X_Drive', 'デフォルト値'), MasterPassW, False);
    strY:=暗号化文字列から複合する関数(Ini.ReadString('セクション', 'Y_Drive', 'デフォルト値'), MasterPassW, False);

    if ComboBox1.Text<>'' then
    begin
      //ComboBox1が空欄でなければ教科のドライブにも接続
      strZ:=EDText(Ini.ReadString('セクション', 'Z_Drive', 'デフォルト値'),
        MasterPassW, False)+ComboBox1.Text;
    end else begin
      StrZ:='';
    end;
  finally
    Ini.Free;
  end;

  //ネットワークドライブを切断
  NetDel;

  //ネットワークドライブ接続確認用変数を初期化
  NetDrvError:=False;

  try

    //Userへの通知
    ProgressBar1.Visible:=True;
    if ComboBox1.Text<>'' then
    begin
      ProgressBar1.Max:=4;
    end else begin
      ProgressBar1.Max:=3;
    end;
    ProgressBar1.Position:=0;

    //ネットワークドライブ(個人フォルダ)を追加
    AddNetworkDrive('W:', strW, '', PW, ID);
    if not (NetDrvError) then
    begin
      //ネットワークドライブ名を変更
      X:=CreateOleObject('Shell.Application');
      D:=X.NameSpace('W:\');
      D.Items.Item.Name:='個人フォルダ';
      ProgressBar1.Position:=ProgressBar1.Position+1;
    end else begin
      NetDrvError:=False;
      MessageDlg('Error:個人フォルダに接続できません!', mtError, [mbOk] , 0);
      Exit;
    end;

    //ネットワークドライブ(校内共有)を追加
    AddNetworkDrive('X:', strX, '', PW, ID);
    if not (NetDrvError) then
    begin
      //ネットワークドライブ名を変更
      X:=CreateOleObject('Shell.Application');
      D:=X.NameSpace('X:\');
      D.Items.Item.Name:='校内共有';
      ProgressBar1.Position:=ProgressBar1.Position+1;
    end else begin
      NetDrvError:=False;
      MessageDlg('Error:校内共有に接続できません!', mtError, [mbOk] , 0);
      Exit;
    end;

    //ネットワークドライブ(校務分掌)を追加
    AddNetworkDrive('Y:', strY, '', PW, ID);
    if not (NetDrvError) then
    begin
      //ネットワークドライブ名を変更
      X:=CreateOleObject('Shell.Application');
      D:=X.NameSpace('Y:\');
      D.Items.Item.Name:='校務分掌';
      ProgressBar1.Position:=ProgressBar1.Position+1;
    end else begin
      NetDrvError:=False;
      MessageDlg('Error:校務分掌に接続できません!', mtError, [mbOk] , 0);
      Exit;
    end;

    //ネットワークドライブ(教科)を追加
    if ComboBox1.Text<>'' then
    begin
      AddNetworkDrive('Z:', strZ, '', PW, ID);
      if not (NetDrvError) then
      begin
        //ネットワークドライブ名を変更
        X:=CreateOleObject('Shell.Application');
        D:=X.NameSpace('Z:\');
        D.Items.Item.Name:=ComboBox1.Text;
        ProgressBar1.Position:=ProgressBar1.Position+1;
      end else begin
        NetDrvError:=False;
        MessageDlg('Error:'+ComboBox1.Text+
          'フォルダに接続できません!', mtError, [mbOk] , 0);
        Exit;
      end;
    end;

    if not (NetDrvError) then
    begin
      //接続ボタンを使用不可に設定
      Button1.Enabled:=False;
      //接続状態の表示を設定
      Label4.Caption:='状態:接続中';
      Label4.Transparent:=False;
      Label4.Color:=clLime;
    end;

  finally
    Screen.Cursor:=crDefault;
    ProgressBar1.Position:=0;
    ProgressBar1.Visible:=False;
  end;

  //【第3段階】最終処理

  //コンピュータを開く
  ShellExecute(Handle, 'Open','EXPLORER.EXE','::{20D04FE0-3AEA-1069-A2D8-08002B30309D}','',SW_SHOW);

  //タスクトレイへ常駐する手続きを呼び出し
  MovetoTasktray;

  //カーソルを元の状態に変更
  Screen.Cursor:=crDefault;

  Ini := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  try
    //設定情報を初期化
    InfoStr1:='';
    InfoStr2:='';
    InfoStr3:='';
    //テキスト暗号化
    InfoStr1:=暗号化文字列を作成する関数(Edit1.Text,Unit1.MasterPassW, True);
    InfoStr2:=暗号化文字列を作成する関数(Edit2.Text,Unit1.MasterPassW, True);
    InfoStr3:=暗号化文字列を作成する関数(ComboBox1.Text,Unit1.MasterPassW, True);
    //ネットワークドライブ設定情報
    //iniファイルに保存
    Ini.WriteString('セクション', 'UserID', InfoStr1);
    Ini.WriteString('セクション', 'UserPW', InfoStr2);
    Ini.WriteString('セクション', 'UserSubject', InfoStr3);
  finally
    Ini.Free;
  end;

  //スタートアップに登録
  {
  if Application.MessageBox(PChar(MyMessage),'確認',
    MB_YesNo + MB_IconQuestion) = IdNo then exit; // Noなら何もしないで終わり
  }

  if chkStartup.Checked then
  begin
    //Yesなら
    MyObject := CreateComObject(CLSID_ShellLink);
    MySLink  := MyObject as IShellLink;
    MyPFile  := MyObject as IPersistFile;
    MySLink.SetPath(PChar(Application.ExeName));
    with TRegIniFile.Create(MyRegFile) do
    try
      Directory := ReadString('Shell Folders',MyFolders,'') + '\';
      WFileName := Directory + Application.Title + '.Lnk';
      MyPFile.Save(PWChar(WFileName),False);
    finally
      Free;
    end;
  end;

end;

ネットワークドライブの割り当て手続きは次の通り。

procedure AddNetworkDrive(Drive, UNC, Comment, Password, UserName: string);
var
  NetResource: TNetResource;
{$IFDEF UNICODE}
  user, pass: PWideChar;
{$ELSE}
  user, pass: PChar;
{$ENDIF}
begin
{$IFDEF UNICODE}
	with NetResource do
  begin
  	dwType := RESOURCETYPE_DISK;
    lpLocalName := PWideChar(Drive);
    lpRemoteName := PWideChar(UNC);
    lpComment := PWideChar(Comment);
    lpProvider := nil;
  end;
  if (Password = '') then
    pass := nil
  else
    pass := PWideChar(password);
  if (UserName = '') then
    user := nil
  else
    user := PWideChar(UserName);
{$ELSE}
	with NetResource do
  begin
  	dwType := RESOURCETYPE_DISK;
    lpLocalName := PChar(Drive);
    lpRemoteName := PChar(UNC);
    lpComment := PChar(Comment);
    lpProvider := nil;
  end;
  if (Password = '') then
    pass := nil
  else
    pass := PChar(password);
  if (UserName = '') then
    user := nil
  else
    user := PChar(UserName);
{$ENDIF}
  if (WNetAddConnection2(NetResource, pass, user, 0) <> NO_ERROR) then
  begin
    //エラー発生時の処理
  	NetErrorProc(GetLastError);
    NetDrvError:=True;
  end else begin
    //エラーが発生しなかった場合の処理
    NetDrvError:=False;
  end;
end;

ネットワークドライブの接続解除手続きは次の通り。

procedure RemoveNetworkDrive(Drive: string);
begin
  if (Drive = '') then exit;
{$IFDEF UNICODE}
	WNetCancelConnection2(PWideChar(Drive), 0, true);
{$ELSE}
	WNetCancelConnection2(PChar(Drive), 0, false);
{$ENDIF}
end;

ネットワークドライブを切断する手続きは次の通り。

procedure TForm1.NetDel;
begin
  //Network Driveを切断
  RemoveNetworkDrive('W:');
  RemoveNetworkDrive('X:');
  RemoveNetworkDrive('Y:');
  RemoveNetworkDrive('Z:');
end;

プログラムをコンパイルして出来たexeをクラウド経由で支給されたノートPCへ送った僕は、プログラムを起動して、必要事項を入力し、祈るような気持ちで「接続」ボタンをクリックした・・・。

テスト用のユーザーアカウントを作成して接続実験を行った

2.システムエラー 1219

こんなエラーメッセージが表示された(記憶を頼りにエラーを再現した画像)

最初から上手く行くとは思っていなかったけど、やはりエラーは心に痛い。しかも、数字しか表示されてないから、接続プログラム内に僕が予め用意したエラーの通知文にはないエラーだ。

procedure NetErrorProc(err: DWORD);
var
  s: String;
begin
  case err of
    ERROR_ACCESS_DENIED:  s := ERR_ACCESS_DENIED;
    ERROR_ALREADY_ASSIGNED:  s := ERR_ALREADY_ASSIGNED;
    ERROR_BAD_DEV_TYPE:  s := ERR_BAD_DEV_TYPE;
    ERROR_BAD_NET_NAME:  s := ERR_BAD_NET_NAME;
    ERROR_BAD_PROFILE:  s := ERR_BAD_PROFILE;
    ERROR_BAD_PROVIDER:  s := ERR_BAD_PROVIDER;
    ERROR_BUSY:  s := ERR_BUSY;
    ERROR_CANCELLED:  s := ERR_CANCELLED;
    ERROR_CANNOT_OPEN_PROFILE:  s := ERR_CANNOT_OPEN_PROFILE;
    ERROR_DEVICE_ALREADY_REMEMBERED:  s := ERR_DEVICE_ALREADY_REMEMBERED;
    ERROR_EXTENDED_ERROR:  s := ERR_EXTENDED_ERROR;
    ERROR_INVALID_PASSWORD:  s := ERR_INVALID_PASSWORD;
    ERROR_NO_NET_OR_BAD_PATH:  s := ERR_NO_NET_OR_BAD_PATH;
    ERROR_NO_NETWORK:  s := ERR_NO_NETWORK;
    //次の行はエラーメッセージから調べて追加
    53:               s := ERROR_BAD_NETPATH;
    1200:             s := ERROR_BAD_DEVICE;
    2202:             s := NERR_BadUsername;
  else
    s := IntToStr(err);
  end;
  MessageDlg(s, mtError, [mbOk], 0);
end;

Google先生にお伺いをたてると・・・

「システム エラー 1219 同じユーザーによる、サーバーまたは共有リソースへの複数のユーザー名での複数の接続は許可されません。」とのこと。

なんのこっちゃ? と思ったが、さらに調べてみると「Windows資格情報」が既に登録されているとこのエラーが発生するらしいことがわかった。そこで「コントロール パネル ⇨ ユーザー アカウント ⇨ 資格情報マネージャー」の順に辿って、Windows資格情報を確認するとNASのIPアドレスとともに、僕のIDとパスワードが登録されていた。いちばん最初にNASに接続した際に自動的に登録されたらしい。これを取り敢えず、削除してみる。

Windows資格情報を初期化

で、ID:test-uで再チャレンジするが「システムエラー1219」が再度表示され、NASの共有フォルダはネットワークドライブとして表示されない。

実在する一般ユーザーのアカウント設定を参照して作ったテスト用ユーザーだから、設定に間違いがあるとは思えないのだが・・・。

ふと、思い立って(=揮発性メモリにWindows資格情報が残っているためかと考えた、ここでいったんPCを再起動すれば、古いWindows資格情報は消えるはず・・・)Myアカウントで試してみる。僕のアカウントは管理者用のアカウントで何でもできるから、テストには不向きと考え、敢えて使わなかったのだ。

Myアカウントを入力して、接続テストを実行。すると・・・

無事、NASの共有フォルダへの接続に成功!
期待通りにネットワークドライブとして表示できたが、空き容量表示の色が「赤」なのが痛々しい・・・。

3.アカウントに読み書き権限を付与

Myアカウントなら繋がることはわかったので、ひと安心したが、なぜ一般ユーザーアカウントで繋がらないのかがわからない。まさか、全ユーザーのアカウント設定を管理者に昇格させるわけにも行かず(入れないフォルダがあるわけではないので、それでも運用上は特に問題は起こらないと思うのだが)、何としてもその原因を確かめないといけない。

DSMを起動して、ユーザーを選択し、「編集」⇨「権限」でMyアカウントと一般ユーザーアカウントの違いを見比べてみる。違いは一目瞭然。Myアカウントには「homes」と「共有」に「読込み/書込み」があるが、test-uアカウントにはそれがない・・・。

これかー!!

DSM ⇨ コントロールパネル ⇨ グループ と辿って、何か適当なグループはないか検討してみると、職員全員と説明のあるグループを発見。さっそくこれを編集して、「homes」と「共有」に「読込み/書込み」権限を付与(設定)する。

続けて、test-uアカウントに「職員全部」が所属するグループを追加する。

これでtest-uアカウントは「homes」と「共有」各フォルダに対する読み書きが可能に。

今度は、test-uアカウントでも無事接続でき、NASの共有フォルダがネットワークドライブとして表示された。やった。目標を実現できた!

あとは、このプログラムを含むネットワーク環境改善案を全体に提案して共通理解を持ち、共有資産を再編すればいい。

クライアントPCの数は100に満たない。これくらいなら僕ひとりで接続プログラムの導入と設定は十分可能だ。

接続プログラムを動かして、ネットワークドライブが見えている状態であれば、Windows資格情報が消されていても、揮発性メモリには接続先のIPアドレスだけでなく、ユーザーIDとパスワードも書き込まれるらしく、クライアントPCのデスクトップ上にあるNASへのショートカットも機能することがわかった。

作業の途中、NASのルートはネットワークドライブに指定できない(何らかの共有フォルダを指定しなければならない)という事実を初めて知り「愕然」とする瞬間もあったが、ネットワークドライブ接続後にデスクトップにあるNASへのショートカットが機能すれば何の問題もない。共有資産の再編作業は滞りなく実行できるはずだ。

クライアントPC1機を複数人で運用するのであれば、Windows資格情報を残したままだと他の人が接続しようとした時に「システムエラー1219」が発生するのは間違いないが、現状一人一台の生体認証でログインするクライアントPCだから、マシンを割り当てられた職員以外の人の使用は考えにくく、Windows資格情報は消去せずにそのままにしておいても大丈夫かもしれない。

いずれにしても、明日以降、試験的に運用しながら、もし問題点があるようならそれを発見・改善し、組織全体がよりよくなれるよう、力を尽くそう。

この方法がいちばん良い方法であるとは思えないが、今の僕にできるベストであることは間違いない。ならば、自分にできるいちばんよいことをする。それをずっと繰り返すしか、ないじゃないか・・・。

4.まとめ

現在のファイル共有環境を生かしたまま、新しいファイル共有環境を再構成するには、現在のファイル共有システムの中に、新しい共有フォルダを準備してそこへ古い環境から必要なファイルだけを移動する方法がよいのではないか? と今回の経験から思った。実際の運用は、これからだけれど・・・

5.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

Supports Zero-Starting!

ゼロ始まりに対応!

どんな理由があって、そうなったのか知りませんが、マークシート方式で行われる予定の大学入学共通テストサンプル問題、教科「情報Ⅰ」の解答の選択肢は、始まりが「1」ではなく、「0」からになってます・・・。

ディジタルのイメージから、「0」・「1」とつながる0始まりにしたのか?
それとも他に何か0から始めなければならない必然的な理由があるのか?
サンプル問題を作成した方に、その理由をぜひ伺ってみたい気もしますが・・・

今回は、My MarkSheet Reader を、解答の選択肢「0」始まりに対応させたというお話です。

【今回の記事】

1.なんで0始まりなの?
2.教科「情報」用マークシートを作成
3.ゼロ始まりに対応
4.まとめ
5.お願いとお断り

1.なんで0始まりなの?

大学入学共通テストの理科基礎科目は例外なく、解答の選択肢の始まりは「1」から。
物理も、化学も、生物も、地学も、全部、それが「1」になってるってのは、「選択肢の始まりは1から」って、誰もがそう感じるから・・・だと思うのですが。

教科「情報」では、なぜかそれが「0」からになってる・・・。

検索してみると、そのことに注意を促すWebサイトが複数見つかります。
例えば、選択肢の番号が「0」始まりであることに気付かず、「121」とマークしたつもりが実際には「010」だった。思い込みにはくれぐれも注意しましょう!・・・みたいな。

僕は、僕のマークシートリーダーをよくする云々ではなく、今回だけは、この無視できない現実にとにかく「対応する」ことにしました!

2.教科「情報」用マークシートを作成

まずはマークシートそのものを準備しなければいけません。これが理科の場合なら、選択肢の数は8個もあれば十分です。しかし、教科「情報」では、例えば著作権関係の内容を見てみると、著作者の権利として、氏名表示権、同一性保持権、複製権、上演権、演奏権、公衆送信権、口述権、展示権、頒布権、貸与権、翻訳権、翻案権の12の権利があることがわかります。これを解答の選択肢として準備する場合があり得ますから、選択肢の数は少なくても15程度、十分な余裕を持って設定した方がよさそうです。

幸いなことに僕のマークシートリーダーは、数学での利用を想定して選択肢数は最大16個まで対応できるように作成してあります。そこで、数学用のマークシートを改造して教科「情報」用のマークシートを作成することにしました。

Wordで作成した数学用マークシート

まず、行番号の「アイウエオ・・・」を「12345・・・」に書き換えます。

次に、マークを0始まりで15まで、16個用意します。
1行分作成したら、あとはひたすらコピペします。
これを3列分繰り返して、1列25問×3で75問に対応できるマークシートとしました。

列の行番号が半角のカタカナから、半角の数字2桁になったため、解答欄座標を取得するマーカー■■■(トリプルドット)の位置に関して若干の修正が必要でしたが、何とか思った通りの形に仕上げることができました。

完成した教科「情報」用マークシート

出来れば、問題数は100問まで対応可能としたいところですが、用紙サイズの関係もあり、75問でよしとすることにしました。どうしても75問以上の設問が必要な場合は、数学用途に2枚1セットで採点できるようにプログラムを組んであるので、それを活用すればなんとかなります。まぁ・・・試験時間60分なら、最大75問に対応していればOKでしょう。

完成した教科「情報」用マークシート

とりあえず、マークシートは完成です。ここまでは極めて順調に推移しました。次は、いよいよプログラムの改良です。

3.ゼロ始まりに対応

どのようにプログラムを改良しようかと考えた時、一瞬、教科「情報」専用のマークシートリーダーにしようか・・・と思ったのですが、やはり、そうではなく、1つのプログラムで様々な教科・科目に対応できる方が理想だと思い直し、選択肢の始まりは「0」とするか、「1」とするか、ユーザーが選べるようにプログラムを改良することにしました。

ユーザーが選択肢の始まりを「0」「1」のどちらかに設定できるよう改良

設定欄に、最初の選択肢の番号を指定するVCLコントロールを設置するスペースを何とか作成し、そこにComboBox1個とLabelを2つ貼って、上の図のように選択肢の始まりの番号を指定できるようにしました。もちろん、ここで指定した番号は必要であればイニシャライズファイルに保存して、次回起動時も有効化されるように設定。このComboBoxの名前は思いついたまま適当に「cmbOneZeroSelect」として、読み取りコードを次のように改良します。

  begin
    //StringGridに読み取り結果を表示
    //オリジナルのプログラムは1行で終わってた
    //StringGrid1.Cells[intSG_Col,intSG_Row]:=strAnsList[intSG_k];
    //選択肢の0始まりに対応できるようコードを改良
    if cmbOneZeroSelect.Text='1' then
    begin
      StringGrid1.Cells[intSG_Col,intSG_Row]:=strAnsList[intSG_k];
    end else begin
      if (strAnsList[intSG_k]='99') or (strAnsList[intSG_k]='999') then
      begin
        StringGrid1.Cells[intSG_Col,intSG_Row]:=strAnsList[intSG_k];
      end else begin
        strAnsList[intSG_k]:=IntToStr(StrToInt(strAnsList[intSG_k])-1);
        StringGrid1.Cells[intSG_Col,intSG_Row]:=strAnsList[intSG_k];
      end;
    end;
    ・・・
  end;

早速、実行してみました!

数学用のマークシートを読み込ませて動作確認

よかった☆ 期待通りに動作してくれました!

ちなみに「999」は「空欄」、「99」は「複数マークあり」を意味します。
Gridコントロールをクリックすれば、当該箇所のマーク状態をチェックできます。
赤い色の矩形は、プログラムを実行した際の採点結果チェック実行時に、画面上に実際に表示される矩形です。
サンプル画像を3枚読むのに要した時間は772ミリ秒

1行16選択肢で、1列に25行、これが1枚に3列あるから、マークの数は合計1200個/枚あります。MyPC環境では、マークシート1枚について1200あるマークを約250ミリ秒で読み取ってますから・・・。1秒でほぼ4枚、1クラス40名分なら約10秒で読み取り完了です。

自分で言うのもなんですが、結構、高速に動作しているんじゃないか・・・と。

ただ、10万枚くらいは手作業で採点できそうな時間を開発に費やしてますが・・・。

4.まとめ

ようやく、大学入学共通テストの教科「情報」に対応したマークシートリーダーが出来ました。解答欄の選択肢の始まりが「0」か、「1」か、ただそれだけのことなのですが、両方にきちんと対応するのは、やっぱりそれなりに大変でした。

5.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。