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

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

Checked プロパティのみ設定したい!

CheckBox がクリックされたら「メッセージを表示」して、ユーザーに「はい」・「いいえ」のいずれかを選択してもらう。

「はい」が選択された場合はプログラム自体を再起動。で、再起動後の FormCreate 時に当該 CheckBox の Checked プロパティをクリックされた(変更された)状態に設定。ただし、その際、メッセージは表示しない。

もし、「いいえ」が選択された場合は、CheckBox の Checked プロパティはチェック前の状態を維持、つまり、クリックを無効化する。もちろん、ここでもメッセージは出さずに、Checked プロパティのみ修正したい。

この動作を実現したくて、半日、ハマった。

【もくじ】

1.用意した手続きと関数
2.実行結果
3.お願いとお断り

1.用意した手続きと関数

なんとか、実現。完成したコードは以下の通り。

  private
    { Private 宣言 }

    //チェックボックスの状態をロード中に OnClick イベントがトリガーされるのを防止する
    IsLoading: Boolean;

    procedure SaveCheckCMS_State(CheckBox: TCheckBox);  //Checked プロパティを保存
    procedure LoadCheckCMS_State(CheckBox: TCheckBox);  //Checked プロパティを読込
    procedure ClearRestartFlag;  //再起動フラグをクリア
    function IsRestarting: Boolean;  
    procedure RestartApplication;


グローバル変数を一つ、手続きと関数を上のように準備。それから ini ファイルを使うので、System.IniFiles を uses に追加。

implementation

uses
  System.IniFiles;

Shift+Ctrl+C でそれぞれの手続きや関数を次のように作成。

まず、SaveCheckCMS_State 手続き。CheckCMS が CheckBox の名前。Checked プロパティの状態を保存する。ちなみに CMS は、組み合わせ採点(Combined Scoring Method)の略。

procedure TForm1.SaveCheckCMS_State(CheckBox: TCheckBox);
var
  IniFile: TIniFile;
begin
  IniFile := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  try
    IniFile.WriteBool('セクション', '組み合わせ採点', CheckCMS.Checked);
    IniFile.WriteBool('セクション', 'IsRestarting', True); //再起動フラグを設定
  finally
    IniFile.Free;
  end;
end;

次は LoadCheckCMS_State 手続き(保存した Checked プロパティの状態を読み込む)。

procedure TForm1.LoadCheckCMS_State(CheckBox: TCheckBox);
var
  IniFile: TIniFile;
begin
  IniFile := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  try
    IsLoading := True; // イベントを無効にするためのフラグを設定
    CheckCMS.Checked := IniFile.ReadBool('セクション', '組み合わせ採点', False);
  finally
    IsLoading := False; // フラグをリセット
    IniFile.Free;
  end;
end;

次は ClearRestartFlag 手続き( Checked プロパティの保存時に True に設定した再起動を知るフラグをクリアする)。

procedure TForm1.ClearRestartFlag;
var
  IniFile: TIniFile;
begin
  IniFile := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  try
    IniFile.WriteBool('セクション', 'IsRestarting', False);  //再起動フラグをクリア
  finally
    IniFile.Free;
  end;
end;

次は IsRestarting 関数( FormCreate 時に呼び出し)。

function TForm1.IsRestarting: Boolean;
var
  IniFile: TIniFile;
begin
  IniFile := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  try
    Result := IniFile.ReadBool('セクション', 'IsRestarting', False);
  finally
    IniFile.Free;
  end;
end;

次は RestartApplication 手続き。これを呼び出すことでプログラム自体を再起動する。

procedure TForm1.RestartApplication;
var
  FileName: string;
  StartupInfo: TStartupInfo;
  ProcessInfo: TProcessInformation;
begin

  FileName := ParamStr(0);
  ZeroMemory(@StartupInfo, SizeOf(StartupInfo));
  StartupInfo.cb := SizeOf(StartupInfo);
  ZeroMemory(@ProcessInfo, SizeOf(ProcessInfo));

  if CreateProcess(PChar(FileName), nil, nil, nil, False, 0, nil, nil, StartupInfo, ProcessInfo) then
  begin
    CloseHandle(ProcessInfo.hProcess);
    CloseHandle(ProcessInfo.hThread);
  end;

  Application.Terminate;

end;

以上のように手続き・関数を準備して、FormCreate 時の設定。

procedure TForm1.FormCreate(Sender: TObject);
begin

  //チェックボックスの状態をロード中に OnClick イベントがトリガーされるのを防止する
  IsLoading:=False;

  LoadCheckCMS_State(CheckCMS);  //Checked プロパティを復元
  if IsRestarting then
    ClearRestartFlag;  //再起動フラグをクリア

end;

最後に、いちばん肝心な CheckCMSClick 手続き。実際は、ここからすべてが始まる。

procedure TForm1.CheckCMSClick(Sender: TObject);
var
  strMsg: string;
begin
  //再起動状態でなければ実行
  if not IsLoading then
  begin
    SaveCheckCMS_State(CheckCMS);  //Checked プロパティを保存

    //最初はコレでいいかと思ったんだけれど・・・あまりにも乱暴な気が。
    //strMsg:='設定はプログラムの再起動後に有効になります。'+#13#10+
    //  'OKで再起動します。';
    //Application.MessageBox(PChar(strMsg), PChar('情報'), MB_ICONINFORMATION);
    //RestartApplication;

    //操作の取り消しができるように修正
    strMsg:='設定はプログラムの再起動後に有効になります。'+#13#10+
      '再起動してよろしいですか?';
    if Application.MessageBox(PChar(strMsg), PChar('情報'), MB_YESNO or MB_ICONINFORMATION) = mrYes then
    begin
      //[はい]が選ばれた時
      RestartApplication;
    end else begin
      //[いいえ]が選ばれた時
      //メッセージを表示せず、チェックボックスの状態のみ変更
      if CheckCMS.Checked then
      begin
        CheckCMS.OnClick := nil;  //OnClickイベントを一時的に無効にする
        CheckCMS.Checked := False;
        CheckCMS.OnClick := CheckCMSClick;  //OnClickイベントを再度設定
      end else begin
        CheckCMS.OnClick := nil;  //OnClickイベントを一時的に無効にする
        CheckCMS.Checked := True;
        CheckCMS.OnClick := CheckCMSClick;  //OnClickイベントを再度設定
      end;
    end;

  end;
end;

2.実行結果

(1)プログラムを起動。フォームが表示される。

練習用なので、CheckBox をひとつだけ用意。
CheckBox の Checked プロパティはデフォルトでは False に設定している。


(2)CheckBoxをクリックすると、メッセージが表示されるので、「はい」をクリックする。


(3)自分自身を再起動。CheckBox の Checked プロパティは終了時の True 状態で起動するが、上記のメッセージは表示されない。 これが実現したかったことのひとつめ。

Checked プロパティは True でも、メッセージは表示されない。


(4)再度、CheckBox をクリック。Checked プロパティは False に変わり、CheckBox のチェックは外れた状態でメッセージが表示される。今度は「いいえ」をクリック。

今度は「いいえ」をクリックする。


(5)「いいえ」を選択したから再起動はしない。「再起動しない」から CheckBox の Checked プロパティは元の True であった状態を維持(= False から True へ修正)するが、メッセージは表示されない。これが実現したかったことのふたつめ。

「いいえ」が選択された場合は、CheckBox の Checked プロパティはチェック前の状態を維持。
(直前のクリックを無効化)

3.お願いとお断り

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

DelphiのSmart CodeInsightを使ってみました!

Delphi 12.2で利用できるようになったSmart CodeInsightが使えるようになるまでの設定方法です。
ローカルで LLM を実行できる Ollama の環境構築から(見様見真似で)やってみました。

【もくじ】

1.はじめに
2.Ollama のインストール
3.LLM モデルのダウンロード
(1)Gemma 2
(2)Llama-3-ELYZA-JP-8B
4.動作環境の設定
(1)Gemma 2
(2)Llama-3-ELYZA-JP-8B
5.DelphiのIDEのスマート支援機能を設定
6.DelphiのIDEのスマート支援機能を使う
7.まとめ
8.お願いとお断り

1.はじめに

ちなみに LLM は(今回、初めて知った!のですが)、自然言語処理( Natural Language Processing :NLP )のタスクに使用される大規模言語モデル( Large Language Model )の略で、膨大な量のテキストデータを使って訓練された人工知能のモデルを意味するそうです。

※ 自然言語処理のタスク:「コンピュータがヒトの言語を理解し、生成し、処理する上での特定の課題や目的」のこと。すなわち、文章の生成、分類、翻訳、応答、人名・地名・組織名等の特定の名称認識( Named Entity Recognition:NER )、音声認識、要約など、実に様々な「タスク」があるようです。

調べてみると実にたくさんの LLM があり、果たしてどのモデルを選べばよいのか(例えば、日本語が得意で、プログラミングに適したモデルはどれなのか?)がわからず、当初、たいへん困りましたが、いくつかの Web サイトの情報を参考に、ここでは「 Gemma 2 」と「 Llama-3-ELYZA-JP-8B 」をダウンロードして使ってみました。

コマンドを叩くだけで簡単にインストールできたのは「 Gemma 2 」、自分で Modelfile を作る必要があり、設定に少し勉強が必要だったのが「 Llama-3-ELYZA-JP-8B 」です。私のノート PC 環境(もちろん、GPU などという結構なモノは、ハナからついておりません!)で、「実用になるか・どうかは別」にして、単に「応答の速さだけを見た」場合、後者の方が体感的には「圧倒的に速かった」です。

【My デバイスの仕様】
プロセッサ 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz 3.00 GHz
実装 RAM 32.0 GB (31.7 GB 使用可能)
システムの種類 64 ビット オペレーティング システム、x64 ベース プロセッサ

2.Ollama のインストール

Delphi 12.2 Athens をインストールして、その新機能に関する記事を読んでいたら、「Smart CodeInsight: コーディングにAIのパワーを活用」という見出しがあり、そのリンク先の記事で、オンラインソリューションや、オフラインソリューションを使用して AI LLM をコーディングに活用できることを知りました。これが今回の事始めです。ドキドキ。

あっ! ちなみに Ollama は「オラマ」と読むようです。ほんとか・どうか、わかりませんが・・・

https://blogs.embarcadero.com/ja/announcing-the-availability-of-rad-studio-12-2-athens-ja/#Smart_CodeInsight_%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E3%81%ABAI%E3%81%AE%E3%83%91%E3%83%AF%E3%83%BC%E3%82%92%E6%B4%BB%E7%94%A8

Windows11 の 23H2 で Copilot に触れてから、生成AIにコーディングを助けてもらうことが断然多くなりました。もし、DelphiのIDE上で生成AIが利用できたら、どれほど快適にプログラムが書けることか、想像するだけでワクワクします。これまでGoogle先生に質問をくり返しながら、四苦八苦していたことが、まるで夢のよう・・・。

そう思いつつ、別の記事を参照して、追加の情報もGet。

https://blogs.embarcadero.com/ja/using-ai-llms-in-the-rad-studio-ide-with-smart-codeinsight-ja/

上の記事によれば、オンラインソリューションは基本的に全て有料とのこと。で、唯一、オフラインソリューションの Ollama だけが無料で使えるソリューションでした。

※ ソリューション:「問題解決方法、手段、対応策」

上記リンク先の記事からの引用です。

「アカウントと支払いに関して唯一の例外はOllamaで、Ollama はローカル (または任意のサーバー) にインストールでき、サービス料金を支払うことなくオフラインで使用できます。」

RAD Studio IDEで「AI LLM」と「Smart CodeInsight」を使用する より引用

・・・ということなので、迷うことなく Ollama に決めました。で、Ollama は、MacOS 版、Linux 版、Windows 版があるとのことで、Windows のユーザーである私はもちろん Windows 版をチョイス。

https://ollama.com/download

上記リンク先から( Windows 環境であれば Windows 版の ) Ollama をダウンロードしました。

特に意識しなくても、自動でWindows版が選択されていた気が・・・しますが、OS が Windows11 であればダウンロードフォルダに OllamaSetup.exe(version 0.3.14:2024/10/29 現在)が保存(663MB)されます。これをダブルクリックしてインストール。

インストール自体は、ただ待つだけ。何の問題もなく終了。

(設定で、何か変わったところはあるのかなー?)

・・・と思って、いちおう確認すると、環境変数のPathの最後に

C:\Users\ユーザー名\AppData\Local\Programs\Ollama

が追加されてました。変更は、はたして、これだけか?

ここでの表記は「Ollama」
「ollama」じゃなくて、「Ollama」が正しい表記なのだろうか?


とりあえず、コマンドプロンプトを起動し、次のコマンドを叩きます。

ollama -v

【実行結果】


大丈夫。ダウンロード & インストールは無事完了!

3.LLM モデルのダウンロード

Ollama のインストールが終了したら、Ollama を使って実行する LLM のモデルを入手しなければなりません。「 LLM 日本語 無料 おすすめ」等のキーワードで検索してみると、実にたくさんの LLM モデルがあることに気付きます。

( 百花繚乱・・・)

LLM は、まさに現在進行形で進化中、きっとお互いにしのぎを削っているような状況なのでしょう。

(1)Gemma 2

いくつかの Web サイトを参照して、まず「Gemma(ジェマ)」という LLM を試してみようかと思いました。正直、専門的なことは「チンプンカンプン」で「まったくわからない」私ですが、様々なサイトで「高性能」と評価されていたこと、そして何より、インストールがとても簡単そうだったのがいちばんの理由です。

スタートボタンの隣にある「検索」欄に「cmd」と入力してコマンドプロンプトを起動、んで、次のコマンドを叩くだけで Gemma2 モデルがダウンロードされて、ただちに起動しました。カンタン!

これ作ったひと、すごい! これ作ったひとみたいに、なりたいなー☆

ollama run gemma2
C:\Users\ユーザー名>ollama run gemma2
pulling manifest
pulling ff1d1fc78170... 100% ▕██████████████|略|██████████████▏ 5.4 GB
pulling 109037bec39c... 100% ▕██████████████|略|██████████████▏  136 B
pulling 097a36493f71... 100% ▕██████████████|略|██████████████▏ 8.4 KB
pulling 2490e7468436... 100% ▕██████████████|略|██████████████▏   65 B
pulling 10aa81da732e... 100% ▕██████████████|略|██████████████▏  487 B
verifying sha256 digest
writing manifest
success
>>> Send a message (/? for help)

いくつか質問してみましたが、英語での質問には英語で、日本語での質問には日本語で答えてくれました。さらにいちばん気になる Object Pascal に関する質問にも、よさげな返事を返してくれました。

(2)Llama-3-ELYZA-JP-8B

もうひとつ気になった LLM が「 Llama-3-ELYZA-JP-8B 」です。なぜ、気になったかというと・・・

・「日本語に特化している」という情報が得られたこと。
・「小さい、軽量、ローカルでの実行に適している」という記述が多数のサイトで見られたこと。
・「Code Llama」を含む「Llama3(ラマ3)」がベースになっていること。

これ以外にもたくさんの情報がありましたが、自分的にはこの3つだけでもう十分に魅力的だと感じました。特に、最後の「Code Llama」というプログラム作成を支援してくれる機能が含まれているという解説は「 Delphi を愛して十数年・・・これを試さずには死ねない!」と思うほど、強烈な輝きを放っていました。びゃかー!ってカンジです。あぁ目が眩む。

さて、肝心のダウンロードですが、次のサイトの情報に従って行いました。

「よーしパパ、Ollama で Llama-3-ELYZA-JP-8B 動かしちゃうぞー」

https://qiita.com/s3kzk/items/3cebb8d306fb46cabe9f

上記サイトの記事の作成者様に心から感謝申し上げます。

まず、上記サイトの案内にある通り、「 Hugging Face 」から、「Llama-3-ELYZA-JP-8B-q4_k_m.gguf」をダウンロードしました。

他にも、よくわからないファイルがたくさんあったけど、とにかく「Llama-3-ELYZA-JP-8B-q4_k_m.gguf」だけををダウンロード!

ファイルの大きさは 4.58 GBほどありました。

4.動作環境の設定

(1)Gemma 2

設定は必要ありませんでした。上の記事に書いた通り、コマンドプロンプトを起動して、

ollama run gemma2

これだけで Gemma2 が起動、使用できました。

(2)Llama-3-ELYZA-JP-8B

こちらのモデルは、Ollama で標準サポートされているモデルではない(2024年10月現在)ため、使用するには少し事前準備が必要でした。以下、見様見真似で行った準備作業の手順とその内容です。

・Modelfile という「拡張子のない」ファイルをメモ帳などのテキストエディタで作成する。
・ダウンロードした Llama-3-ELYZA-JP-8B-q4_k_m.gguf と Modelfile を任意の場所にコピーする。
・コマンドを実行して Modelfile から Ollama 用のモデルを作成する。

最初に行ったのは、Modelfile の作成です。Llama-3-ELYZA-JP-8B はその名前を見れば、Llama3 モデルの発展型(そう呼んでいいのかな?)であることが明らかですから、モデルの作成に使用するフォーマットは、Llama3 モデルのそれと同じになるように設定する必要があるとのこと。なので Modelfile はモデルのフォーマット方法を書いたものなんだと理解しました。

※ フォーマット:「モデルがどのようにデータを扱い、出力を生成するか」について、その形式や構造を決めたもので、モデルが入力を理解できるように、また、効率的にデータを処理できるようにするためのルールや方法が定義されている。

【作成した Modelfile 】

作成にあたって、複数のWebサイトにあっ たModelfile の例を調べましたが、Webサイトによって、下の例の最後の行:PARAMETER stop “<|reserved_special_token” 部分が「ない」などの違いがありました。ちなみにこれは、特定のトークン(単語、フレーズ、または文字)に到達した際に出力を停止するようにモデルに指示、つまり特定の条件で出力を停止させることで、予期せぬ長い出力を防ぐために設定するパラメータのようです。

FROM ./Llama-3-ELYZA-JP-8B-q4_k_m.gguf
TEMPLATE """{{ if .System }}<|start_header_id|>system<|end_header_id|>

{{ .System }}<|eot_id|>{{ end }}{{ if .Prompt }}<|start_header_id|>user<|end_header_id|>

{{ .Prompt }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>

{{ .Response }}<|eot_id|>"""
PARAMETER stop "<|start_header_id|>"
PARAMETER stop "<|end_header_id|>"
PARAMETER stop "<|eot_id|>"
PARAMETER stop "<|reserved_special_token"

次に作成した Modelfile と、ダウンロードした Llama-3-ELYZA-JP-8B-q4_k_m.gguf の保存先ですが、どこにしまったらいいのか、さっぱりわかりません。いろいろ調べてみると、この2つのファイルの保存場所は「任意のフォルダ」としているサイトが数多く見受けられました。これより、この2つのファイルは同じフォルダ内に置けば、それでいいのではないかと思えてきました。

そこで「任意のフォルダ」に保存することに決めたのですが、その場所が次の大きな問題です。出来れば、あとから思い出せるように、「どこに保存したか?」絶対忘れない場所がいいです。

で、思いついたのが Ollama のインストール先フォルダのルートに置けばいいのではないか? ということです。調べて見ると Ollama のインストール先は次の場所でした。

C:\Users\ユーザー名\.ollama

さらに、「モデルがどこに作成されるのか」を調べたら、次の場所に作成されるようでした。

C:\Users\ユーザー名\.ollama\models

以上のことから、作成した Modelfile と、ダウンロードした Llama-3-ELYZA-JP-8B-q4_k_m.gguf の2つのファイルを Ollama のインストール先フォルダのルートに保存し、このフォルダを作業ディレクトリにしてモデル作成のコマンドを叩けば上手く行く(= Llama-3-ELYZA-JP-8B のモデルの作成に成功する)のではないかと考え、次のように操作してみました。

まず、作成した Modelfile と、ダウンロードした Llama-3-ELYZA-JP-8B-q4_k_m.gguf の2つのファイルを C:\Users\ユーザー名\.ollama フォルダにコピペする。

「Modelfile」に拡張子はない!


次に、コマンドプロンプトを起動して、モデル作成のコマンドを実行して、Modelfile から Ollama のモデルを作成します。モデル作成のコマンドは、次の通りです。

ollama create elyza:jp8b -f Modelfile

この時、コマンドプロンプト起動直後の状態のまま、カレントディレクトリを .ollama に変更せずにモデル作成のコマンドを実行したらエラーが発生してしまいました。

C:\Users\ユーザー名>ollama create elyza:jp8b -f Modelfile

Error: open C:\Users\ユーザー名\Modelfile: The system cannot find the file specified.

( Modelfile が見えません・・・ あっ☆)

そこで次のようにしてカレントディレクトリを .ollama に変更。

C:\Users\ユーザー名>cd .ollama

もう一度、上記のモデル作成のコマンドを実行。

C:\Users\ユーザー名\.ollama>ollama create elyza:jp8b -f Modelfile
transferring model data 100%
using existing layer sha256:91553c45080b11d95be21bb67961c9a5d2ed7556275423efaaad6df54ba9beae
creating new layer sha256:8ab4849b038cf0abc5b1c9b8ee1443dca6b93a045c2272180d985126eb40bf6f
creating new layer sha256:c0aac7c7f00d8a81a8ef397cd78664957fbe0e09f87b08bc7afa8d627a8da87f
creating new layer sha256:bc526ae2132e2fc5e7ab4eef535720ce895c7a47429782231a33f62b0fa4401f
writing manifest
success

おしまいに success と表示されたので、上手くいったようです。
モデルを実行するには、次のコマンドを入力すればよいとのこと。

ollama run elyza:jp8b

やってみました!

C:\Users\ユーザー名\.ollama>ollama run elyza:jp8b
>>> Send a message (/? for help)

Send a message とある部分に、次のように入力してみました。

>>> DelphiでTLabelのAlignmentにtaCenterを指定しても実行すると設定が反映されないことがあります。どうしたらLabelのキャプションを中央揃えにして表示できますか?

入力後、Enterキーを叩くと・・・

TLabelのAlignmentにtaCenterを指定した際、設定が反映されない場合があります。この問題は、LabelのParentComponentにContainerControlを使用することで解決します。

以下の手順で解決します:

1. LabelをContainerControl (例えばTPanelやTFrame) のChildにします。
2. ContainerControlのAlignmentプロパティにtaCenterを指定します。

上記の方法で、Labelのキャプションが中央揃えになります。

AI の回答の内容の良否は検証の要有りですが、それより、何より・・・

動きましたー☆

5.DelphiのIDEのスマート支援機能を設定

これがいちばんやりたかったことです。

Delphiを起動して、「ツール」→「オプション」の順にクリックします。


オプションのダイアログ(ユーザーに情報を提供したり、ユーザーから入力を受け取るための小さなウィンドウ。ダイアログボックスともいう)が表示されたら、左ペインの「IDE」の中にある「スマート支援機能」をクリックします。

IDEは、前回、開いた場所を記憶しているようです。
必要であれば、「IDE」→「スマート支援機能」の順にクリックしてください。


次のように設定します。


(1)スマート支援機能の下にある「 Enable AI Engine 」のチェックをONにします。
(2)「エディタのデフォルト AI 」のComboBox は選択肢から Ollama を選択します。
(3)「チャットウインドウのデフォルト AI 」も選択肢から Ollama を選択します。

私の環境では、CheckBox のキャプションは「 Enable AI Engine 」でしたが、
Web上で見たダイアログではそれが「AIエンジンの有効化」となっていました。
なぜなんでしょう?


続けてプラグインの部分も次のように設定します。


(1)最初に「 Ollama 」タブをクリックして選択します。
(2)有効にチェックします。
(3)BaseURL に「 http://localhost:11434 」と入力します。
(4)LLM のモデルが Llama-3-ELYZA-JP-8B 場合、モデルには「 elyza:jp8b 」と入力してください。

モデルの設定部分については、何と設定すればいいのか? 当初わからなくて困ったのですが、コマンドプロンプトで Llama-3-ELYZA-JP-8B を実行する時に入力するコマンドが

C:\Users\ユーザー名\.ollama>ollama run elyza:jp8b

であることから、モデルの設定は「 elyza:jp8b 」に違いない!と考え、実際その設定で動作しましたので、多分、これで OK なのではないかと・・・思われます。

入力が終わったら「保存」ボタンをクリックして、オプション設定のダイアログを閉じてください。

6.DelphiのIDEのスマート支援機能を使う

さっそく使ってみます。既存のプロジェクトファイルを開くか、新規にアプリケーションを作成して、コードエディタが表示された状態にします。

編集画面の余白部分を右クリックすると、次の図のようにポップアップメニューが表示されるので、そのいちばん上にある「 Smart CodeInsight 」をポイント(or クリック)します。すると、さらにサブメニューが表示されます。

全部! 試してみたい機能ばかり☆


AIチャットをクリックしてみました☆

Ollama>に質問を入力すればよさそうです!


質問してみました!


少し、待ち時間がありましたが、待ちくたびれるほどではありません。ちゃんと計ったわけではありませんが 30 秒くらいかなー? AI の返事が表示されました。

表示されたコードは Object Pascal ではありませんでした!


質問に「 Object Pascal で書いて!」という内容を追加して再チャレンジ。

すごーい! すごーい!!


ふと思い立って・・・ DelphiのIDEのコードエディタに「VB.NET」のコードを貼り付けて、

ムチャしてます・・・


コード全体を選択して、コードの変換を試してみます。

「Delphi に変換」をクリック


かなり待ち時間がありましたが・・・

結果が表示されるまで、上のような画面になります。


3分くらいかな? ちょっと長かったけど、無事、変換できました!

すごーい! すごーい!!


もし、わからないコードがあった時は・・・

わからないコードを選択して、「コードの説明」をクリック


すると・・・

すごーい! すごーい!!


感動!

7.まとめ

(1)Ollama をインストールすれば、ローカルな環境でも LLM を利用した生成 AI を使用できる。
(2)LLM は Ollama で標準サポートされているものなら、run するだけで使える。
(3)Ollama で標準サポートされていない LLM でも gguf ファイルから create して使用できる。
(4)Delphi のIDEのスマート支援機能で Ollama を指定すれば、無料でローカル LLM を使える。
(5)GPU がないPCでも(待ち時間はあるが)スマート支援機能は使えそうな気がする。

8.お願いとお断り

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

Excelのプロセスを終了させる

プログラムがフリーズするなどして、実行中の Excel のプロセスが残ってしまうことがある。また、そうでなくても、例えば Delphi の Try ~Finally 文で、確実に Excel のオブジェクトを解放したつもりであるにもかかわらず、プログラムで読み書きした特定の Excel のファイルが「編集のため、ロックされています」と表示され、「読み取り専用」でしか開けなくなり、Ctrl+Alt+Del でタスクマネージャーを起動して確認すると、場合によっては5つも6つもExcelのプロセスが実行中であったり、する。

そこで Delphi で書いた Excel のファイルを操作するアプリケーションを終了する際に、実行中のプロセスが残らないようにする方法を考えてみた。

【もくじ】

1.確認メッセージを表示して終了させる
2.確認メッセージを表示せずに起動中の全てのプロセスを終了させる
3.お願いとお断り

1.確認メッセージを表示して終了させる

最初に書いてみたのがコレ!
Excel のプロセスが実行中であれば(残っていれば)、確認メッセージを表示して、プロセスを強制的に終了させる。ただし、このコードで終了できるプロセスは1つのみ。

procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
var
  strMsg: string;

  //Excelのプロセスが実行中であるか、どうかを調査する関数
  function IsExcelRunning: Boolean;
  var
    Snapshot: THandle;
    ProcessEntry: TProcessEntry32;
  begin
    Result := False;
    Snapshot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if Snapshot = INVALID_HANDLE_VALUE then Exit;

    ProcessEntry.dwSize := SizeOf(TProcessEntry32);
    if Process32First(Snapshot, ProcessEntry) then
    begin
      repeat
        if SameText(ProcessEntry.szExeFile, 'EXCEL.EXE') then
        begin
          Result := True;
          Break;
        end;
      until not Process32Next(Snapshot, ProcessEntry);
    end;
    CloseHandle(Snapshot);
  end;

  //プロセスのリストを取得し、特定のプロセスを終了する関数
  function TerminateExcelProcesses: Boolean;
  var
    Snapshot: THandle;
    ProcessEntry: TProcessEntry32;
    ProcessHandle: THandle;
  begin
    Result := False;
    Snapshot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if Snapshot = INVALID_HANDLE_VALUE then Exit;

    ProcessEntry.dwSize := SizeOf(TProcessEntry32);
    if Process32First(Snapshot, ProcessEntry) then
    begin
      repeat
        if SameText(ProcessEntry.szExeFile, 'EXCEL.EXE') then
        begin
          ProcessHandle := OpenProcess(PROCESS_TERMINATE, False, ProcessEntry.th32ProcessID);
          if ProcessHandle <> 0 then
          begin
            if TerminateProcess(ProcessHandle, 0) then
            begin
              Result := True;
            end;
            CloseHandle(ProcessHandle);
          end;
        end;
      until not Process32Next(Snapshot, ProcessEntry);
    end;
    CloseHandle(Snapshot);
  end;

begin
  if IsExcelRunning then
  begin
    //Excelのプロセスを終了させる
    strMsg:='Excelのプロセスが実行中です。'+#13#10+#13#10+
      '終了してもよろしいですか?';
    if Application.MessageBox(PChar(strMsg), PChar('警告'), MB_YESNO or MB_ICONWARNING) = mrYes then
    begin
      //[はい]が選ばれた時
      if TerminateExcelProcesses then
      begin
        strMsg:='Excelプロセスを終了しました。';
        Application.MessageBox(PChar(strMsg), PChar('情報'), MB_ICONINFORMATION);
      end else begin
        strMsg:='実行中のExcelプロセスは見つかりませんでした。';
        Application.MessageBox(PChar(strMsg), PChar('情報'), MB_ICONINFORMATION);
      end;
    end else begin
      //[いいえ]が選ばれた時
      strMsg:='Ctrl+Alt+Delキーを同時に押してタスクマネージャーを起動し、実行中の'+
      'Excelのプロセスを必ず終了してください。';
      Application.MessageBox(PChar(strMsg), PChar('情報'), MB_ICONINFORMATION);
    end;
  end else begin
    strMsg:='Excelは実行されていません。';
    Application.MessageBox(PChar(strMsg), PChar('情報'), MB_ICONINFORMATION);
  end;

end;

ただ、コレだと、もし、Excelのプロセスが実行中であった場合、アプリケーションの終了時に突然表示されるメッセージに、ユーザーが驚き、「はい」・「いいえ」のどちらを選べばいいのか、操作上の混乱が生じる可能性があるように思えてきた・・・。

それに、複数の Excel のプロセスが実行中であった場合、この方法では1つしか、終了できない。

そこで、ユーザーには何も知らせずに、もし実行中の Excel のプロセスがあれば、バックグラウンドですべてのプロセスを終了させるようにプログラムを修正。それが次の「確認メッセージを表示せずに起動中の全てのプロセスを終了させる」例。

2.確認メッセージを表示せずに起動中の全てのプロセスを終了させる

実行中の全ての Excel のプロセスを強制的に終了させる。ユーザーに対する確認メッセージは表示しない。

procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);

  //Excelのプロセスが実行中であるか、どうかを調査する関数
  function IsExcelRunning: Boolean;
  var
    Snapshot: THandle;
    ProcessEntry: TProcessEntry32;
  begin
    Result := False;
    Snapshot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if Snapshot = INVALID_HANDLE_VALUE then Exit;

    ProcessEntry.dwSize := SizeOf(TProcessEntry32);
    if Process32First(Snapshot, ProcessEntry) then
    begin
      repeat
        if SameText(ProcessEntry.szExeFile, 'EXCEL.EXE') then
        begin
          Result := True;
          Break;
        end;
      until not Process32Next(Snapshot, ProcessEntry);
    end;
    CloseHandle(Snapshot);
  end;

  //プロセスのリストを取得し、特定のプロセスを終了する関数
  function TerminateExcelProcesses: Boolean;
  var
    Snapshot: THandle;
    ProcessEntry: TProcessEntry32;
    ProcessHandle: THandle;
  begin
    Result := False;
    Snapshot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if Snapshot = INVALID_HANDLE_VALUE then Exit;

    ProcessEntry.dwSize := SizeOf(TProcessEntry32);
    if Process32First(Snapshot, ProcessEntry) then
    begin
      repeat
        if SameText(ProcessEntry.szExeFile, 'EXCEL.EXE') then
        begin
          ProcessHandle := OpenProcess(PROCESS_TERMINATE, False, ProcessEntry.th32ProcessID);
          if ProcessHandle <> 0 then
          begin
            if TerminateProcess(ProcessHandle, 0) then
            begin
              Result := True;
            end;
            CloseHandle(ProcessHandle);
          end;
        end;
      until not Process32Next(Snapshot, ProcessEntry);
    end;
    CloseHandle(Snapshot);
  end;

begin

  //Excelのプロセスが実行中である限りLoopさせ、完全にExcelのプロセスを終了させる。
  While IsExcelRunning do
  begin
    TerminateExcelProcesses;
    Application.ProcessMessages;
  end;

end;

これが、いちばんスマートかな?

3.お願いとお断り

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

デジタル採点 All in One !

これまでに書いてきたデジタル採点プログラムをひとつにまとめました!

手書き答案採点・マークシートリーダー・採点結果通知&成績一覧表の作成プログラム


「AC_Reader」は、手書き答案のデジタル採点に、「MS_Reader」は、マークシート形式の試験のデジタル採点に、それぞれ使用します。

「ReportCard_2024」は、「AC_Reader.exe」及び「MS_Reader.exe」と連携して動作するプログラムで、受験者へのデジタル採点の採点結果を通知する個票及び採点者用の成績一覧表を作成することが出来ます。

「デジタル採点 All in One! 」では、3通りのデジタル採点の実行と、受験者に採点結果を通知する個票及び成績一覧表の作成が可能です。その概要は次の通りです。

1.マークシートの読み取りとデジタル採点
2.手書き答案のデジタル採点(縦書き・横書き、両方の答案に対応)
3.マークシートと手書きを併用した試験のデジタル採点

ただ、プログラミングには素人である筆者が作成したプログラムですので、使いにくいのはもちろんのこと、未発見の不具合もまだきっとあると思います・・・が、掲載したプログラムはすべて「実際に採点の現場で使用」し、動作確認を行ったもので、その際に発見できた不具合はすべて修正してあります。したがって、筆者の想定する範囲内での運用であれば、確実に動作するはずですが、ご使用に際しては事前に必要十分な試行・動作確認を行っていただけますよう、心からお願い申し上げます。

掲載したプログラムは、何の保証もサポートもありませんが、すべて無料でお使いいただけます。ただし、ご使用に際しては、完全に自己責任での運用をお願いいたします。ここに記載した内容及びダウンロードしたプログラムを利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

プログラムは今後も改良し続け、掲載したプログラムは随時改良版に更新する予定です。
見た目も、内容も不出来なプログラムですが、万一にでも、使ってくださった方の採点業務のご負担の軽減に貢献できましたなら、私にとって、それは何よりの喜びです。

プログラムのダウンロード(ZIPファイル)

プログラムのダウンロード後、任意の場所に展開してください。

【もくじの前書き】

今回の記事では、採点プログラムそれぞれについて、ダウンロードしていただいたZIPファイルを展開すればすぐに試せる簡単な試用方法をご紹介しています。

実際の試験においては、スキャナーを使用してマークシートや手書き答案の画像化処理を行ったり、専用プログラムを使用して、マークシートや手書き答案の解答欄の座標を取得する等、採点の事前準備作業が必要です。

ダウンロード後展開していただいたZIPファイル内のファイルやフォルダの構成及び以下の説明の内容は、それらの必要な事前準備作業を終えた段階以降の『実際の採点作業部分のみ』を手軽にお試しいただけるように作成してあります。それぞれ、説明の通りに操作していただけたら幸いです。

操作に際し、予期しないエラーが出た場合の対処方法や、実際の試験の採点に必要な事前準備作業の詳細は、以下の説明の中でご紹介する「過去記事のご案内」リンク先の各採点プログラムの取扱い説明記事をご参照ください

【もくじ】

1.マークシートの読み取り
2.手書き答案のデジタル採点
3.マークシートと手書きを併用した試験のデジタル採点
4.採点結果通知の作成
5.お願いとお断り

1.マークシートの読み取り

プログラムアイコン


【スクリーンショット】

マーク読み取り実行直後の画面


マークシートは、市販のものでなく、再生コピー用紙にインクジェットプリンタで印刷したものを使用します。輪転機で印刷するとマークが濃く印刷されてしまい、「複数マークあり」の誤判定が出やすくなります。ですので、マークシートの印刷には、インクジェットプリンタを使用してください。

シートの左上には特徴点(例:■■■)が必要です。プログラムはマーク読み取り時に、まずシート内の特徴点を探し、そこからの距離情報をもとに一つ一つのマークを切り出して塗りつぶし面積を計算、マークの有無を判定しています。

マークシートの画像は、複合機等のスキャナーで200dpiの解像度でスキャンして作成してください。解像度を大きくしても読み取り処理に必要な時間が大幅に増加するだけでメリットは何一つありません。

デジタル採点の現場で実際に使用した様々な形式のマークシートを添付しましたので、こちらを印刷してお試しください。オリジナルマークシートの作成方法は、添付したPDFファイル「01_マークシートリーダーご利用の手引き」の「7 マークシートの作り方」をご参照ください。

【添付したマークシート】

・1列25行×4列(100設問まで対応)、選択肢は1始まりで8選択肢(A4横R25C04S08)
・1列25行×3列(75設問まで対応)、選択肢は1始まりで10選択肢(A4横R25C03S10)
・1列25行×2列(50設問まで対応)、1始まりで8選択肢。右余白は手書きの解答欄に使用。
・1列25行×3列(大問3個に対応)、16選択肢の数学用(2枚1セットで大問6個に対応)
・1列25行×3列(75設問まで対応)、選択肢は0始まりで16選択肢の教科「情報」用
・1列25行×4列(100設問まで対応)、大語群(選択肢番号は0~99まで使用可能)マーク試験用


マークシートのサンプル①

一般的な塗りつぶす形式のマークシートです。実際の試験の現場で過去5年以上使用しています。読み取り精度が問題になったことは一度もありません。

塗りつぶすマークシート(Wordで作成)


マークシートのサンプル②

線でマークすれば、大語群を使用する試験で解答に要する時間を大幅に短縮できます。ちなみに、芯の太さ0.9mm、硬さ・濃さ2Bのシャープペンシルを使用してマークし、読み取りテストを行ったところ、読み取りパラメータの設定はデフォルト値のまま、すべてのマークを正しく読むことができました。

例:線で「35」をマーク(Excelで作成)


MS_Reader.exe の詳しい使い方は、当Blogの過去記事をご参照ください。
(プログラムを動かすために必要な諸設定についての情報も記載しています)

【過去記事のご案内】

重要 数学採点用途で使用される場合は、当Blogの過去記事「マークシートリーダーを数学用に設定」にあります使用方法を必ずご確認ください。


今回掲載したプログラムには、すぐにお試しいただけますよう、マークシート情報設定済みのサンプルを添付してあります。ファイルのダウンロード及び展開に時間がかかるデメリットはありますが、マーク読み取りを圧倒的に高速化するPython4Delphi(=P4D環境)も今回は、ダウンロードサイズと展開時間を顧みず、敢えて同梱しました。ですので、ここでご紹介する筆者作のマークシートリーダーは自動的にPython環境を使用する高速読み取りモードで起動します。

以下、Zipファイルのダウンロード後、ファイルを任意のフォルダに展開した後の、筆者作マークシートリーダーの試用方法です。

(1)MS_Reader を起動

MS_Reader.exe をダブルクリックして、MS_Reader を起動します。MSはもちろんマークシートの略ですが、筆者のイニシャルが M.S なので、それにもかけてあります。

ここで発生すると思われる不具合とエラーの解決方法は、当Blogの次の過去記事をご参照ください。


(2)マークシートの情報を記録したテンプレートを選択

画面左上のメニューの「2 テンプレート」をクリックすると表示されるサブメニューの「テンプレートの選択」をクリックします。


(3)リストボックスに表示された候補から「N_R25C04S08」をクリックして選択し、「決定」ボタンをクリックします。


ちなみに、テンプレート名の N は、解像度200dpiでスキャンした際の画像ファイルの大きさをノーマルと考えて画像サイズから自動で付けています。

その後ろのR、C、D、Sはそれぞれ次のような意味です。

RはRow、すなわち「行」です。R25なら1列あたり25行のマークシートを意味します。
CはCol、すなわち「列」です。C04なら4列で構成されたマークシートを意味します。
DはDouble、複数マーク可能なマークシートを意味(19選択肢のシートのみ設定可能)。
SはSingle & Select、複数マーク不可で、S08なら選択肢の数は8個のシートを意味します。


(4)表示されるメッセージを読んで、「はい」をクリックします。


(5)「Sample_Data_01_一般用」フォルダをクリックして選択し、「OK」をクリックします。

選択するのは「フォルダ」で、「ファイル」ではありません!


(6)正しくプログラムが動作していれば、画面は次のようになります。

バックグラウンドで動作するPython環境のOpenCVが特徴点画像(■■■)を探し出し、赤枠の矩形でそれを囲んで表示します。同時に、Delphi側のプログラムでテンプレートに記録したマークシートの座標情報を読み込み、最も左側の列の第1行目の選択肢欄を赤枠で囲んで表示します。これでマークシートの読み取り準備が出来ました!


(7)操作方法を案内するバルーンが表示されますので、その先にある「読む」ボタンをクリックしてください。プログラムがマークシートのマークを読み取り、結果をグリッドコントロール上に表示します。


筆者のPCでは、Python4Delphi(P4D)を利用した状態で、1枚100設問(800マーク)×3枚で合計2400マークを986ミリ秒で読み取り、結果を表示しました。1マーク2.43ミリ秒、1枚329ミリ秒で読み取っていますので、この形式(25行×4列・8選択肢)のマークシートを使用した場合、筆者の環境では平均的な1クラス分(40名)を約13.2秒で読むものと推測できますが、使用するPCの性能によりこの値は変化します。


(8)「Check!」ボタンをクリックして、読み取り結果をヒトの目でチェックします。次の例のように、読み取り結果の確認(修正)が必要と思われる箇所で赤枠を表示してチェックプログラムは一時停止します。

【空欄(マークなし)と判定した場合】

「空欄(マークなし)」と判定した場合、グリッドコントロール上には「999」と表示されます。
なお、「白紙(全マークが空欄)」のマークシートは読み飛ばす設定が可能です。


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

「複数マークあり」と判定した場合、グリッドコントロール上には「99」と表示されます。
マークの状況を確認し、必要な場合は読み取り結果を直接入力して修正します。


読み取り結果の確認・修正後、再度「Check!」ボタンをクリックすると、一時停止が解除され、チェックが続行されます。次のメッセージが表示されたら、読み取り結果のチェックは終了です。


(9)読み取り結果の書き出しを実行

マークの読み取り結果はCSV形式でファイルに出力できます。表計算ソフトを利用して読み取り結果を処理する場合はもちろんですが、筆者が作成した「ReportCard_2024.exe」を用いて、採点結果を受験者に通知する個票を作成する場合は、必ずここで読み取り結果をCSVファイルに書き出す作業を行ってください。

読み取り結果をCSVファイルに出力


同梱の「ReportCard_2024.exe」を用いて、採点結果通知を作成できます。こちらのプログラムの使用方法は後述します。


「ReportCard_2024.exe」を用いた採点結果通知の作成例です。

設問ごとに採点結果と観点別評価の区分及び正解マークを表示できます。
また、任意の位置に得点を表示できます。


(10)その他の機能

MS_Reader には、マーク読み取りに加え、読み取り結果の音声読み上げ機能や、チェックのスキップ機能など、筆者が必要と考えた機能を搭載してあります。使い方の詳細は、当Blogの過去記事(上のリンク先)にありますので、必要に応じてご参照ください。

2.手書き答案のデジタル採点

プログラムアイコン


【スクリーンショット】

横書き答案の採点実行時の画面
(添付した答案枚数3枚の採点サンプルを使用)


採点する答案は、横書き・縦書きを問いません。どちらの形式の答案でも採点可能です。また、複合機のスキャナーで読み取り可能な大きさであれば、解答用紙のサイズも問いません(ただし、採点する答案すべてのサイズと解答欄の形式は同じである必要があります)。前述のマークシートでは、解答用紙の左上に特徴点(■■■)が必要でしたが、こちらの手書き答案の採点補助プログラムでは必要ありません。

答案画像は複合機のスキャナー等を用いて、解像度200dpiでスキャンしたカラー画像を使用してください。解答欄矩形は、採点準備作業時に、別に作成した矩形検出プログラムを用いて解答用紙画像より検出・座標データを取得してiniファイルに保存します。手書き答案の採点プログラムは、この座標データをもとに解答欄矩形を答案画像から切り出して、切り出した解答欄を画面に並べて表示します。

解答用紙の解答欄を作成する際は、矩形検出されたくない部分を点線で作成していただく必要がありますが、これさえ守っていただければ、かなりスムーズに解答欄矩形の検出作業(座標データ化)が行えると思います。実際に採点に入るまでに必要な採点準備作業の詳細は、当Blogの過去記事をご参照ください。

解答欄の作成例:矩形検出されたくない部分の罫線は点線を使用します。
これにより設問番号を含んだ解答欄矩形の切り出しや、
字数を指定しての解答欄作成が可能になります。


【過去記事のご案内】


今回掲載したプログラムには、すぐにお試しいただけますよう、採点準備作業を行ってあるサンプルを添付してあります。以下、手書き答案採点補助プログラムの試用方法です。

(1)AC_Readerを起動

AC_Reader.exe をダブルクリックしてプログラムを起動します。AC は Answer Column(解答欄)の略です。プログラム起動時(初回)に次のメッセージが表示されます。

PCのボリューム設定値が0より大きい場合に表示されるメッセージです。

消音して作業できます。
(ボリューム設定値が0の場合は、表示されません)


採点作業内容の確認メッセージです。

必要に応じて「はい」・「いいえ」のいずれかをクリックします。


「いいえ」をクリックすると、次のメッセージが表示されますが、これは表計算ソフトを使用して採点結果通知を作成していた頃の名残りで、表計算ソフトを使用せずに採点結果通知の作成ができるようになった現在はどちらから採点を始めていただいても問題は生じません。


試用される場合、次のメッセージには「いいえ」を選択(クリック)してください。

添付した採点試行用のサンプルは「横書き」です。


初回起動時には複数のメッセージが表示されますが、次回起動時からこれを表示しない設定にすることができます。お好きな方のボタンをクリックしてください。


あらゆるケースを想定した場合、このようなメッセージも必要と判断しました・・・。
(確か、採点結果通知の個票作成プログラムで、合計点を計算するコードを書いていた際に「何か」問題が起きて、このメッセージを表示することにしたような記憶があります)


お断りしたように不出来なプログラムですので、こちらの注意も必ずお守りください。


同じく、こちらの注意も必ずお守りください。


採点方法のご案内です。複数のユーザーより、「前回の採点から2~3か月も経過すると忘れてしまう!」との指摘がありましたので起動時に採点方法を案内するメッセージを表示するようにしました。


このメッセージは、プログラムの画面右下にある「入力方法のご案内」ボタンをクリックすれば、いつでも表示することができます。


(2)既存の採点設定を選択

試用に際しては、筆者が設定・保存した採点設定をお使いください。
画面右上にある「採点作業」ボタンをクリックします。


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


採点設定ファイルの選択を促す案内バルーンが表示されます。
ComboBox右端の∨をクリックしてください。


表示された選択肢の「テスト採点.ini」をクリックして選択します。


(3)続けて採点したいクラス/講座の答案画像が保存されているフォルダを選択します。


上のメッセージの「OK」をクリックすると、フォルダの選択ダイアログが表示されます。

「Sample_Data_04_Markと横手書併用」フォルダをクリックして、「OK」をクリック


(4)採点を実行

最初にフローティング状態のパネルを適切な位置へ移動します。

フローティングパネルのタイトルバーをクリックして任意の位置へD&Dします。


点数を一括入力する場合は、「入力と確認」のComboBoxから入力したい値を選択して「入力」ボタンをクリックします。選択した値がすべての解答欄に設定されますが、入力値が「0」であれば×、そうでない場合は○と得点が表示されます。


個々の採点は、採点したい解答欄の中央付近をクリックして、採点方法の案内にあった方法で採点します。


(5)採点結果の保存

採点結果を答案画像に書き込むには、フローティングパネルの「書込」ボタンをクリックします。

重要 作業の状態は「書込」ボタンをクリックしたところまでが保存されます。「書込」ボタンをクリック後はいつでも終了できます。

重要 採点は何度でもやり直すことができます。


何設問目まで採点したかについては、答案画像を表示して確認できます。

「返却用答案を表示」にチェックを入れると、現在採点している答案画像が表示されます。


◀ボタンや▶ボタンをクリックして表示する答案を変更することができます。


(6)返却用答案及び成績一覧表の作成

手書き答案の採点プログラムには、単独で受験者に返却する答案の印刷や教科担任用の成績一覧表を作成する機能があったのですが、今回、新しく採点結果通知作成プログラムを作成しましたので、独自に返却用答案を作成する機能はCut(正確には非表示に)してあります。

今回、新しく作成した採点結果を通知する個票及び教科・科目担任用に成績一覧表を作成するプログラム「ReportCard_2024」は、「返却用答案&成績一覧作成」をクリックすると起動できます。


「ReportCard_2024」の使い方は、この後の説明をお読みください。

3.マークシートと手書きを併用した試験のデジタル採点

次のような解答用紙を用いて、マークシートと手書きを併用した試験を実施・採点することも可能です。


採点は、マークシート部分のマークの読み取りはMS_Readerで、手書き解答欄の採点はAC_Readerでそれぞれ行ってください。

MS_Readerを用いてマークの読み取りを行った後はCSVファイルに読み取り結果を出力、AC_Readerを用いて手書き答案の採点を行った後は採点結果を「書込み」ボタンをクリックして自動保存(保存先ファイル等を指定していただく必要はありません)していただければ、採点結果通知の作成準備も内部的に完了します。

どちらの採点を先に行うかについて、その作業順は問いませんが、マークシートの採点→手書き答案の採点という流れの方がプログラムが表示するメッセージの内容に矛盾を感じることなく作業できると思います。

採点終了後、次にご案内する採点結果通知を作成するプログラムで、それぞれの試験の合計得点を計算します。

4.採点結果通知の作成

プログラムアイコン

【スクリーンショット①】

受験者への採点結果通知の作成例


【スクリーンショット②】

採点者用の成績一覧表の作成例
氏名データは架空のものです。また、得点データが2件しかないのは添付した試用サンプルを用いて作成したためです。

ReportCard_2024の使い方

(1)起動

ReportCard_2024.exe をダブルクリックしてプログラムを起動します。PCのボリューム設定値が0でない場合は、Beep音を消音するかどうかを確認するメッセージが表示されます。


(2)採点作業を選択します。

ここでは併用タイプを選択しました。


(3)採点対象の答案画像を保存したフォルダを選択します。

「開く」ボタンをクリックします。


採点対象の答案画像を保存したフォルダを選択します。


(4)採点設定がある場合

既存の採点設定がGridコントロールに表示され、採点できる状態になります。

「実行」ボタンをクリックして採点を行ってください。


マークシートの採点の場合、形式の確認メッセージが表示されます。


マークシートの採点の場合、使用したテンプレートを指定します。


採点結果は次のように表示されます。

【マークシート部分①】

デフォルト設定では、左から「採点記号・配点・観点別評価の区分」がそれぞれ表示されます。


【マークシート部分②】

空欄もしくは誤りがある設問には正解の選択肢が数字で示されます(デフォルト設定を利用した場合)。


【手書き答案部分】

観点別評価の区分を表示することはできませんので、受験者に口頭で区分を説明する必要があります。

得点は下の例のように表示されます。得点の表示位置は任意の位置を選択・設定を保存できます。

手書き答案部分には「採点記号と得点」が表示されます。
(観点別評価の区分を表示することはできません)


(5)採点設定がない場合

次のメッセージが表示されます。内容をよく読んで「OK」をクリックしてください。


設問数を入力し、「入力完了」をチェックしてください。


操作方法を案内するメッセージが表示されます。


入力をクリックします。


デフォルトの配点を設定します。入力は半角数字で整数を入力し、「OK」をクリックしてください。


マークシート用の採点設定には「正解の選択肢の番号」・「配点」・「観点別評価の区分」をそれぞれ入力してください。また、手書き答案用の採点設定には「配点」・「観点別評価の区分」を入力してください。


採点設定を入力後、「保存」ボタンをクリックして、設定を保存してください。


採点設定の保存が完了すると、採点の「実行」ボタンがクリックできる状態になります。


(6)採点結果通知個票の印刷

「印刷」ボタンをクリックしてください。


出力先プリンタ・用紙・印刷の向きを指定して「OK」をクリックしてください。
元々の答案のサイズがA3やB4であっても、用紙サイズでA4を指定すれば縮小印刷されます。


答案すべてを印刷するか、個別に印刷するか、いずれかを指定してください。


「いいえ」(個別印刷を選択)を選んだ場合は、印刷したい答案の番号を指定してください。


印刷例です。


(7)成績一覧表の作成と印刷

成績一覧表の作成の「講座名票」ボタンをクリックします。


受験者の氏名等のデータを保存したCSVファイルを選択します。

予めsNameフォルダ内にクラス・講座の氏名データを所定の様式で作成・保存してください。


【参考:氏名データの様式】

クラス・出席番号は「半角」で入力、氏名・よみがな・性別は「全角」でそれぞれ入力し、CSV形式でsNameフォルダ内に保存してください。

フィールド名は入れないでください。
(添付したデータは架空のものです)


平均点を正しく計算するため、試験を欠席した受験者を計算対象から除く処理を行います。
「欠席者をチェック」のCheckBoxをチェックしてください。


得点の合計が「0」の受験者について、平均点の計算処理の対象とするか・しないかを指定します。「はい」をクリックした場合は、成績は「空欄」扱いとなり、平均点の計算対象からは除かれます。
「いいえ」をクリックした場合は、その受験者の得点合計は0点であったものとして平均点を計算します。


印刷プレビューを表示します。「PreView」ボタンをクリックしてください。


添付したファイルのデータはすべて架空のものです。

添付したサンプルデータが3件しなないため、このような表示となっています。


「印刷」ボタンをクリックして印刷します。「CSV出力」ボタンをクリックすれば、CSVファイルに出力することも可能です。表計算ソフトを利用した追加の処理にお役立てください。


CSVファイルのファイル名は自動的に設定されます。また、CSVファイルは処理対象の答案画像があるフォルダ内に出力されます。

「保存」ボタンをクリックしてください。


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


「はい」をクリックした場合、エクスプローラーが起動し、保存先フォルダを開きます。
「いいえ」をクリックした場合は、CSVファイルの保存先を示すメッセージが表示されます。

「はい」をクリックした場合、エクスプローラーが起動し、保存先フォルダを開きます。

5.お願いとお断り

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

[dcc32 致命的エラー] F2039 ファイル ‘.\Win32\Release\project1.exe’ を作成できません


Delphiで「実行(F9)」すると、時々、コレが・・・

コンパイル結果の表示

さらに・・・

メッセージにも(泣)


原因は人それぞれなのかもしれませんが、私は以下の方法でこれを解決できました!

【これまでの解決方法】

既存の「xxx.exe」を手動で削除して、再度「実行(F9)」。
今までは、ずっとこの方法で対応。

【新しい解決方法】

「プロジェクト」ツールウィンドウの xxx.exe を右クリックして、


表示されるメニューの「クリーンアップ」をクリック。


「クリーンアップ」というタイトルの画面が表示され・・・


「成功」!


これで「実行(F9)」で、再び exe が生成されるようになりました。

【まとめ】

「実行(F9)」時に exe を作成できない場合は、プロジェクトツールウインドウの exe を右クリックして「クリーンアップ」を実行する。

【お願いとお断り】

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

StringGridのデータを印刷プレビューして印刷

Delphiで、どうしても書きたいプログラムがあった。
そのために絶対に越えなければならないハードルが、データの印刷プレビューと印刷だ。

正直、これは、ごく簡単に思えてならないことなんだけれど・・・
今まで何度もチャレンジして、そのたびに失敗してきた。。。

今回、ようやく、自分自身、納得の行くものが書けた。
これはその備忘録。

【もくじ】

1.印刷したいデータを準備
2.罫線も印刷する
3.CSVファイルをStringGridに表示する
4.印刷のコード
5.rect:TRectとしてはいけません!
6.印刷プレビューのコード
7.まとめ
8.お願いとお断り

1.印刷したいデータを準備

表計算ソフトを使って、印刷したいデータを作成し、CSV形式で保存する。
例えば、こんな感じ。

データはすべて架空のもの


書きたい印刷プログラムは、A4版・縦の用紙に収まる範囲の列数で、行数は1ページに最高50行を予定。ただし、データとして50行なので、フィールド名を入れれば1ページあたり51行となる。

CSVファイルの先頭には「フィールド名も保存」する。ただし、フィールド名があるのはファイルの先頭のみ。50行ごとに入れたりはしない。

自分の場合、印刷データが50行を超えることは、まず「ない」・・・のだが、冒頭で述べた「どうしても書きたいプログラム」の使用予定者の中にはそうでない方もいる。

なので、書きたいプログラムでは、データ数(=行数)がどんなに増えても、各ページの先頭行にはフィールド名を入れる仕様とする。これは譲れない自分との約束。

2.罫線も印刷する

これも、どうしても越えたいハードルのひとつ。フォントの大きさに関係なく、1行毎に罫線(=下線)を印刷する。もちろん、「罫線無し」の印刷も可能とするが、「罫線有り」の場合は1ページについて必ず51行分の罫線が引かれるのではなく、印刷する行数に合わせて罫線(=下線)を引くようにしたい。

今まで、これがどうしても「できなかった」。

だから、この壁は必ず乗り越える。これも譲れない自分との約束。


追記(20240901)

フォントの大きさは9ポイントに固定しました。

3.CSVファイルをStringGridに表示する

Delphiを起動。次の構造ペインに示すような形でVCLコントロールをForm上に配置。

最低限必要なVCLコントロール
AlignやVisibleなど、各プロパティは必要に応じて設定


Formは常に最大化して表示されるように設定。FormCreate手続きに次のコードを記述。

procedure TForm1.FormCreate(Sender: TObject);
begin
  //Formを最大化して表示(幅も最大化される)
  Form1.WindowState:=wsMaximized;
end;

この場合は、Form1が親だからこれでOKだが、子の場合は注意が必要。

どこに記述するか?
・自分自身が親Formの場合:FormCreateでOK!
・自分自身が子Formの場合:FormShowに書くこと(FormCreateに書くと一般保護違反のエラーが発生する)
・自分自身が子Formの場合にFormのWindowStateプロパティで直接指定しておいたらMy環境では何の問題もなく動作した。

また、Form1のScaledプロパティをFalseに設定することも忘れない。

これはどんなプログラムでも必ず最初に設定する


これをTrue にすると OS の DPI (ユーザが指定した DPI)によってフォームサイズやコントロールサイズが勝手に変更されてしまう。デフォルトでTrueなので注意が必要。Formを作成したら毎回忘れずに設定する。

で、exeがあるフォルダ以下の構成は次の通り。この階層構造をもとにしてPathを設定する。

ProcはProceed(処理済み)の略(のつもり)


StringGridに読み込むCSVファイルの置き場所は、「\sNameフォルダ」とする。

sNameフォルダ内にCSVファイルを用意する


これを読み込んでStringGridに表示する。OpenDialogをFormに追加する。

Form上に追加


読み込むCSVファイルのデータは次の通り。
(フィールド名が「ない」場合や、データに通し番号がなく、行番号を表示したい場合にも対応できるコードを含めて記述した。必要ない部分はコメントアウトしている)

今回使用するデータは「フィールド名・通し番号あり」


次のコードを記述。

procedure TForm1.Button1Click(Sender: TObject);
var
  //CSVファイルの読み込み
  CSVFileName: string;
  CsvFile:TextFile;
  CsvRowStr: string;
  i: Integer;
  strMsg: string;
  //列幅の調整
  iCOL: Integer;
  MaxColWidth: Integer;
  iROW: Integer;
  TmpColWidth: Integer;
begin

  //表示設定
  StringGrid1.Visible:=False;

  //列数
  StringGrid1.ColCount:=7;

  //OpenDialogのプロパティはExecuteする前に設定しておくこと
  With OpenDialog1 do begin
    //表示するファイルの種類をcsvに設定
    Filter:='CSVファイル(*.csv)|*.csv';
    //データの読込先フォルダを指定
    InitialDir:=ExtractFilePath(Application.ExeName)+'sName';
  end;

  //ダイアログ呼び出し
  if OpenDialog1.Execute then
  begin
    CsvFileName:=OpenDialog1.FileName;
    AssignFile(CsvFile, CsvFileName);
    Reset(CsvFile);
  end else begin
    strMsg:='ユーザーによる処理のキャンセル';
    Application.MessageBox(PChar(strMsg), PChar('情報'), MB_ICONINFORMATION);
    Exit;
  end;

  //フィールド名が必要なCSVファイルなら記述する
  //StringGrid1.Rows[0].CommaText:=
  //  '通し番号,氏名,よみがな,年齢,生年月日,性別,血液型';
  //Fixed Colが1列あって、そこに行番号を設定する場合
  //  ',通し番号,氏名,よみがな,年齢,生年月日,性別,血液型';

  //読込み開始行を指定(FixedRowがある場合 -> ない場合は[0]にする)
  i:=0;
  try
    while not EOF(CsvFile) do
    begin
      //CSVファイルを1行読み込み、その1行分を文字列として代入する。
      Readln(CsvFile, CsvRowStr);
      //グリッドの行数が読み込み行数より少なければ、グリッドの行数を追加する。
      if StringGrid1.RowCount <= i then StringGrid1.RowCount := i + 1;
      //グリッドの指定行目に読み込み行を代入
      //[0]列はFixedCol-> 行番号を設定したい場合
      //StringGrid1.Rows[i].CommaText:=IntToStr(i)+','+CsvRowStr;
      StringGrid1.Rows[i].CommaText:=CsvRowStr;
      i := i + 1;
    end;
  finally
    //行番号を設定した場合
    //StringGrid1.Cells[0,0]:='行番号';
    CloseFile(CsvFile);
  end;

  //列幅の自動調整
  for iCOL := 0 to StringGrid1.ColCount-1 do
  begin
    MaxColWidth := 0;
    for iROW := 0 to StringGrid1.RowCount-1 do
    begin
      TmpColWidth := Canvas.TextWidth(StringGrid1.Cells[iCOL,iROW]) + 10;
      if MaxColWidth < TmpColWidth then
        MaxColWidth := TmpColWidth;
    end;
    StringGrid1.ColWidths[iCOL] := MaxColWidth;
  end;

  //表示設定
  StringGrid1.Visible:=True;

end;

実行結果は次の通り。

文字コードはANSI(CP932、Shift_JIS が拡張されたもの?)


以下、データを読み込む上での注意のあれこれ。

まず、CSVファイルの文字コードがUTF-8だと・・・

たいへんなコトに・・・


また、氏名やよみがなの「姓と名の間にあるスペースが全角でなく、半角」だと・・・

このコードでは、半角スペースが区切り文字として認識されてしまう!


CSVファイルのデータ形式には、文字コードも含めて十分、注意する必要がある。

4.印刷のコード

正直に言うと、今回のチャレンジでは「印刷プレビュー」のプログラムの方を先に書いた。
自分自身の感覚に自信などあるわけないが、通常(?)の感覚からすれば「印刷プレビュー」⇨「印刷」という流れが自然であるような気がして、そうなったのだ。

そこで問題になったのがデータ数が多く、印刷(出力)が「複数ページ」となる場合、プレビューの2枚目、3枚目をどう表示するか? という部分。

先に述べた通り、2枚目以降の先頭行にも「フィールド名を表示」するという自分との約束もあったし・・・。この「ページ毎、先頭行にはフィールド名を表示する」処理の方法をいろいろ考え、試してみたが、どうにも上手く行かない。

これだ! と思える処理手順が思いつかないまま、1ページ目だけの表示であれば問題なくできるプログラムを作成。とりあえず、印刷プレビューは1ページ目だけ表示することで妥協して、(仮)印刷プレビュープログラムとしておき、複数ページの印刷に対応した印刷プログラムが完成したら、もう一度、夢見た通りの印刷プレビューとなるよう、ここに戻ってくることにする。

そうして様々な問題をひとつひとつ自分なりに丁寧にクリアして最終的に書き上げたのが、下に掲載した「印刷」のプログラムコード(データ全体の行数や列数は限定せず、汎用的に使える=再利用できるコードを目指したつもりだが、どうだろうか?)。

FormにPrinterSetupDialog、その他のVCLコントロールを追加。

PrinterSetupDialogの方が用紙サイズと印刷方向を選ぶには便利!


if PrinterSetupDialog1.Execute then ~で、呼び出したDialogの画面。

今回の用途なら、出力先のプリンタ・用紙・向きの指定が出来ればOK!
追加したVCLコントロールとそれらに設定した名称
設定した値と状態

Button2は「印刷プレビュー」機能を、Button3は「印刷」機能を、それぞれ割り当てる予定。
なので、Button3をダブルクリックして手続きを作成し、「印刷」プログラムのコードを入力。

implementation

uses
  System.Math,
  Vcl.Printers;
procedure TForm1.Button3Click(Sender: TObject);
var
  //用紙サイズ、縦置き・横置きの設定を知る(Charだと推奨されない警告が表示される->Stringに変更)
  //Device, Driver, Port: array[0..255] of Char;
  Device, Driver, Port: string;
  DeviceMode: THandle;
  DevMode: PDeviceMode;

  //StringGrid->CSVファイル名とそこまでのPathを入れる
  csvFN:string;

  StringList: TStringList;
  i, j, k, MaxWidth: Integer;
  Fields: TStringList;
  FieldWidths: array of Integer;
  ColMargin: Integer;
  MarginX, MarginY: Integer;
  intLoop: Integer;
  FontHeight: Integer;
  eNum: Integer;
  iPlus: Integer;
  myFieldElement: string;
  LowNum: Integer;
  HighNum: Integer;
  MyRect:TRect;
  //平均値・最高値・最低値 -> 汎用性を考えExtended ではなく、Double とした
  DSum: Double;
  DAvg: Double;
  MinValue, MaxValue: Double;
  intDenomin: Double;

  //StringGrid -> CSV File
  procedure SaveStringGridToCSV(StringGrid: TStringGrid; const FileName: string);
  var
    CSVFile: TextFile;
    Row, Col: Integer;
    Line: string;
  begin
    AssignFile(CSVFile, FileName);
    Rewrite(CSVFile);
    try
      for Row := 0 to StringGrid.RowCount - 1 do
      begin
        Line := '';
        for Col := 0 to StringGrid.ColCount - 1 do
        begin
          Line := Line + StringGrid.Cells[Col, Row];
          if Col < StringGrid.ColCount - 1 then
            Line := Line + ',';
        end;
        WriteLn(CSVFile, Line);
      end;
    finally
      CloseFile(CSVFile);
    end;
  end;

  // ビットマップ用印刷ルーチン
  procedure StretchDrawBitmap(Canvas:TCanvas;  // 描画先キャンバス
                              r : TRect;       // 描画先範囲
                              Bitmap:TBitmap); // ビットマップ
  const
    InfoSize = SizeOf(TBitmapInfoHeader) + 4 * 256;
  var
    OldMode   : integer;      // StretchModeの保存用
    pInfo     : PBitmapInfo;  // DIBヘッダ+カラーテーブルへのポインタ

    InfoData  : array[0..InfoSize-1] of Byte; // DIBヘッダ+カラーテーブル
    Image     : array of Byte;// DIBのピクセルデータ
    DC        : HDC;          // GetDIBits 用 Device Context
    OldPal    : HPALETTE;     // パレット保存用
  begin
    pInfo :=@InfoData;

    // 24 Bit DIB の領域を確保
    SetLength(Image, ((Bitmap.Width * 24 + 31) div 32) * 4 * Bitmap.Height);

    // DIB のBitmapInfoHeader を初期化
    with pInfo^.bmiHeader do begin
      biSize := SizeOf(TBitmapInfoHeader);
      biWidth := Bitmap.Width;     biHeight := Bitmap.Height;
      biPlanes := 1;               biBitCount := 24;
      biCompression := BI_RGB;
    end;

    // 24bpp DIB イメージを取得
    DC := GetDC(0);
    try
      OldPal := 0;
      if Bitmap.Palette <> 0 then
        OldPal := SelectPalette(DC, Bitmap.Palette, True);

      GetDIBits(DC, Bitmap.Handle, 0, Bitmap.Height,
                Image, pInfo^, DIB_RGB_COLORS);
      if OldPal <> 0 then SelectPalette(DC, OldPal, True);
    finally
      ReleaseDC(0, DC);
    end;

    // 拡大モードを カラー用に変更
    OldMode:=SetStretchBltMode(Canvas.Handle,COLORONCOLOR);

    // 描画!!
    StretchDIBits(Canvas.Handle,
                  r.Left,r.Top,r.Right-r.Left,r.Bottom-r.Top,
                  0,0,pInfo^.bmiHeader.biWidth,pInfo^.bmiHeader.biHeight,
                  Image,pInfo^,DIB_RGB_COLORS,SRCCOPY);
    // 拡大モードを元に戻す
    SetStretchBltMode(Canvas.Handle,OldMode);
  end;

  procedure GetMinMaxValues(StringGrid: TStringGrid; ColIndex: Integer; out MinValue, MaxValue: Double);
  var
    Row: Integer;
    Value: Double;
  begin
    if StringGrid.RowCount = 0 then
      raise Exception.Create('StringGridにデータがありません。');

    MinValue := MaxDouble;
    MaxValue := -MaxDouble;

    for Row := 1 to StringGrid.RowCount - 1 do
    begin
      if TryStrToFloat(StringGrid.Cells[ColIndex, Row], Value) then
      begin
        if Value < MinValue then
          MinValue := Value;
        if Value > MaxValue then
          MaxValue := Value;
      end;
    end;

    if MinValue = MaxDouble then
      raise Exception.Create('指定された列に数値データがありません。');
  end;

begin

  //複数回クリックを防止する
  Button3.Enabled:=False;

  //初期化
  Image1.Picture:=nil;
  Image2.Picture:=nil;
  Image1.Visible:=False;
  Image2.Visible:=False;

  //印刷設定(用紙・向き)後に印刷
  if PrinterSetupDialog1.Execute then
  begin

    //プリンタの設定を取得
    Printer.GetPrinter(Device, Driver, Port, DeviceMode);
    DevMode := GlobalLock(DeviceMode);
    try
      //用紙サイズをA4に設定
      DevMode^.dmPaperSize := DMPAPER_A4;
      //用紙方向を縦に設定
      DevMode^.dmOrientation := DMORIENT_PORTRAIT;
      //設定をプリンタに反映
      Printer.SetPrinter(Device, Driver, Port, DeviceMode);
    finally
      GlobalUnlock(DeviceMode);
    end;

    //プリンタの解像度を取得
    //DPI := GetDeviceCaps(Printer.Handle, LOGPIXELSX);
    //家庭用のEPSONのプリンタは360DPI
    //業務用のEPSONの複合機は、600DPI
    //FinePrintは、600DPI

    //A4サイズの用紙の寸法は210mm x 297mm。インチに換算:約8.27インチ x 11.69インチ
    //プリンタの解像度(DPI: Dots Per Inch)

    //100DPIの場合
    //幅: 8.27インチ × 100 DPI = 827ピクセル
    //高さ: 11.69インチ × 100 DPI = 1169ピクセル

    //200DPIの場合
    //幅: 8.27インチ × 200 DPI = 1654ピクセル
    //高さ: 11.69インチ × 200 DPI = 2338ピクセル

    //100DPIとして描画したものをStretchDrawする

    //TImageの初期設定
    Image1.Width := 827;
    Image1.Height := 1169;
    Image1.Picture.Bitmap.Width := 827;
    Image1.Picture.Bitmap.Height := 1169;

    //背景を塗りつぶす
    Image1.Picture.Bitmap.Canvas.Brush.Color := clWhite;
    MyRect:=Rect(0, 0, 827, 1169);
    Image1.Picture.Bitmap.Canvas.FillRect(MyRect);

    //使用するフォント(必ず等幅フォントを指定する)
    //数値の右揃え用に追加(20240820)
    Image1.Picture.Bitmap.Canvas.Font.Name:='Consolas';

    //フォントサイズ -> 実際にはComboBoxで指定・選択できるようにする
    Image1.Picture.Bitmap.Canvas.Font.Size:=11;

    //平均値を計算 -> 実際のプログラムではこのような計算も行っている
    {
    DSum:=0;
    for i := 1 to StringGrid1.RowCount do
    begin
      if StringGrid1.Cells[5,i] <> '' then
      begin
        DSum:= DSum + StrToInt(StringGrid1.Cells[5,i]);
      end;
    end;
    DAvg:= SimpleRoundTo(DSum / intDenomin, -2);

    //最高値及び最低値を計算
    GetMinMaxValues(StringGrid1, 5, MinValue, MaxValue);
    }

    //StringGrid -> CSV
    //実際のプログラムでは、sNameフォルダ内のCSVファイルを読み込み、
    //さらに幾つかフィールドを追加して新しいデータを追加している。
    //追加したデータを含めて印刷する仕様

    //実際のプログラムでは、LabelSaveFolderName.Captionは別手続きで取得・表示済み
    LabelSaveFolderName.Caption:='SampleData';

    //保存するフォルダへのPath
    csvFN:=IncludeTrailingPathDelimiter(ExtractFilePath(Application.ExeName))+
      'ProcData\'+LabelSaveFolderName.Caption+'\';

    //フォルダの存在を確認、なければ作成
    if not System.SysUtils.DirectoryExists(ExtractFileDir(csvFN)) then
    begin
      //フォルダ階層を作成
      System.SysUtils.ForceDirectories(ExtractFileDir(csvFN));
    end;

    csvFN:=IncludeTrailingPathDelimiter(ExtractFilePath(Application.ExeName))+
      'ProcData\'+LabelSaveFolderName.Caption+'\'+LabelSaveFolderName.Caption+'.csv';
    SaveStringGridToCSV(StringGrid1, csvFN);

    StringList:=TStringList.Create;
    Fields:=TStringList.Create;

    try

      //Create
      StringList.LoadFromFile(csvFN);
      //Create
      SetLength(FieldWidths, 0);

      //各フィールドの最大幅を計算
      for i := 0 to StringList.Count - 1 do
      begin
        Fields.CommaText := StringList[i];
        if Length(FieldWidths) < Fields.Count then
          SetLength(FieldWidths, Fields.Count);

        for j := 0 to Fields.Count - 1 do
        begin
          //MaxWidth := Printer.Canvas.TextWidth(Fields[j]);
          MaxWidth := Image1.Picture.Bitmap.Canvas.TextWidth(Fields[j]);
          if FieldWidths[j] < MaxWidth then
            FieldWidths[j] := MaxWidth;
        end;
      end;

      eNum:=StringList.Count div 50;

      //51,101,151,201,251,301,・・・,XX1番目にフィールド名を挿入しておく

      //0番目の要素をコピー
      myFieldElement:=StringList[0];
      //要素を挿入(追加)
      if eNum<>0 then
      begin
        for i := 1 to eNum do
        begin
          StringList.Insert((50*i)+1, myFieldElement);
        end;
      end;

      //ここから印刷Loop
      try

        for intLoop := 0 to eNum do
        begin

          //初期化(白紙にする)
          Image1.Picture.Bitmap.Canvas.Brush.Color := clWhite;
          MyRect:=Rect(0, 0, 827, 1169);
          Image1.Picture.Bitmap.Canvas.FillRect(MyRect);

          if intLoop=0 then
          begin
            Printer.BeginDoc;
          end else begin
            Printer.NewPage;
          end;

          //タイトルを描画
          Image1.Picture.Bitmap.Canvas.Font.Color:=clBlue;
          Image1.Picture.Bitmap.Canvas.TextOut(
            StrToInt(EditMarginX.Text), StrToInt(EditMarginY.Text)-30,
            LabelSaveFolderName.Caption);

          //タイトルを描画
          {
          Image1.Picture.Bitmap.Canvas.Font.Color := clBlue;
          Image1.Picture.Bitmap.Canvas.TextOut(
            StrToInt(EditMarginX.Text), StrToInt(EditMarginY.Text)-30,
            LabelSaveFolderName.Caption + ' 【平均値:'+FloatToStr(DAvg)+
            '、最高値:'+ FloatToStr(MaxValue)+
            '、最低値:'+ FloatToStr(MinValue)+'】');
          }

          Image1.Picture.Bitmap.Canvas.Font.Color:=clBlack;

          k:=0;
          MarginX:=StrToInt(EditMarginX.Text);
          MarginY:=StrToInt(EditMarginY.Text);
          ColMargin:=StrToInt(EditColMargin.Text);

          iPlus:=0;
          //次のcase文でelseを使って何らかの値が必ず代入されるようにしたので不要
          //LowNum:=0;
          //HighNum:=0;

          case intLoop of
            0:begin
              LowNum:=0;
              if StringList.Count > 50 then
              begin
                HighNum:=50;
              end else begin
                HighNum:=StringList.Count-1;
              end;
            end;
            {
            1:begin
              LowNum:=51;
              if StringList.Count > 100 then
              begin
                HighNum:=100;
              end else begin
                HighNum:=StringList.Count-1;
              end;
            end;
            2:begin
              LowNum:=101;
              if StringList.Count > 150 then
              begin
                HighNum:=150;
              end else begin
                HighNum:=StringList.Count-1;
              end;
            end;
            }
          else
            //一般化
            LowNum:=(intLoop*50)+1;
            if StringList.Count > (intLoop*50)+50 then
            begin
              HighNum:=(intLoop*50)+50;
            end else begin
              HighNum:=StringList.Count-1;
            end;
          end;

          for i := LowNum to HighNum do
          begin
            Fields.CommaText := StringList[i];
            for j := 0 to Fields.Count - 1 do
            begin
              //処理できる列数を無制限にする
              case j of
                0:k:=0;
              else
                k:=k+FieldWidths[j-1]+ColMargin;
              end;
              //フィールド名に「備考」を追加する
              if i=0 then
              begin
                if j=Fields.Count-1 then
                begin
                  Fields[j]:=Fields[j]+' 備考';
                end;
              end;

              //データを出力(数値の右揃え:なし)
              //Image1.Picture.Bitmap.Canvas.TextOut(MarginX+k,MarginY+(iPlus*20),Fields[j]);

              //データを出力(数値の右揃え:あり))
              //数値の右揃え用に追加(20240820
              if TryStrToInt(Fields[j], intValue) then
              begin
                //数値である -> 右揃えで出力する
                Image1.Picture.Bitmap.Canvas.TextOut(MarginX+k,MarginY+(iPlus*20),
                  Format('%3d', [strToInt(Fields[j])]));
              end else begin
                //数値でない -> 左揃えで出力する
                Image1.Picture.Bitmap.Canvas.TextOut(MarginX+k,MarginY+(iPlus*20),Fields[j]);
              end;

              //罫線を描画
              if cbLine.Checked then
              begin
                Image1.Picture.Bitmap.Canvas.Pen.Color:= clBlack;
                FontHeight:= -1 * Image1.Picture.Bitmap.Canvas.Font.Height;
                Image1.Picture.Bitmap.Canvas.MoveTo(MarginX+k, MarginY+(iPlus*20)+FontHeight+4);
                Image1.Picture.Bitmap.Canvas.LineTo(Image1.Picture.Bitmap.Width-50, MarginY+(iPlus*20)+FontHeight+4);
              end;
            end;
            inc(iPlus);
          end;

          //大きさを指定
          MyRect.Top:=0;
          MyRect.Left:=0;
          MyRect.Bottom:= Trunc((Printer.PageWidth / Image1.Picture.Width) * Image1.Picture.Height);
          MyRect.Right:= Printer.PageWidth;
          //ファイルを描画
          StretchDrawBitmap(Printer.Canvas, MyRect, Image1.Picture.Bitmap);
          Application.ProcessMessages;

        end;  //intLoop

      finally
        Printer.EndDoc;
      end;

    finally
      StringList.Free;
      Fields.Free;
      //複数回クリックを防止する
      Button3.Enabled:=True;
    end;

    //ファイルの完全削除
    DeleteFile(PChar(csvFN));

    //TImageの表示位置を指定
    ScrollBox1.VertScrollBar.Position:=0;
    ScrollBox1.HorzScrollBar.Position:=0;
    Image1.Top:=ScrollBox1.VertScrollBar.Position+14;
    Image1.Left:=ScrollBox1.HorzScrollBar.Position+14;

    //TImageの表示
    Image1.Visible:=True;  //印刷プレビューを実行しなければ不要

    Button2Click(Sender);  //印刷プレビューを表示する

  end else begin
    //キャンセルに対応
    Button2Click(Sender);  //印刷プレビューを表示する
  end;

end;

上のコードには、このサンプルでの処理には不要な部分や、(コードを一般化する前に場合分けして処理手順を考えた)冗長な部分も含まれている。自分のバカさを全世界にPRするようなものだが、計算処理を追加したり、コードを一般化する際の考え方の参考となるよう、敢えてそのまま残した。

そもそも、この記事を書こうと思ったきっかけは、DelphiのStringGridの内容を定型用紙に印刷するサンプルコードが(Web上に)あまりにも少ない気がしたこと。ただ、この例では、いったんCSVファイルにして保存したり、PrinterのCanvasではなくTImageのCanvasに描画したり、普通とは言い難い方法を行って印刷している気がするので、普通(?)の印刷方法を学びたい方にはまるで参考にならないかもですが、万一にでも、どなたかのお役に立てれば何よりの幸いです。

【実行結果】

まず、PrinterSetupDialogが表示されるので、A4・縦を選択する。

プリンター名に「FinePrint」とあるのはお気に入りのプリントユーティリティ
プレビュー的に印刷内容を確認したり、まとめ印刷を行ったり、縮小・両面設定で印刷枚数を減らしたり、とにかく使えるユーティリティ
FinePrintへ出力
FinePrintへの出力の印刷プレビュー部分を拡大


これで数値データを右寄せ表示できれば、大満足なんだけど・・・ 。

追記(20240819)

Format関数を使えば、数値データの右揃えが簡単に実現できることを忘れてた!

(最近は「データを保存する」プログラムばかり書いていて、「データを印刷する」プログラムはほとんど書いたことがないことにあらためて気づいた。

 例1:csv形式で保存 -> 表計算ソフトで読み込んで活用。
 例2:表計算ソフトのファイルにADO接続して、直接書き込み。
 例3:データベースにADO接続して、データを保存、必要な部分をクエリで抽出。みたいな・・・

遠い昔、VBでデータを縦・横罫線付きの一覧表形式で印刷するプログラムをさんざん書いていたのが夢のよう・・・。

てか、今回も最終的に印刷しているのは、プリンターのCanvasに描画した「絵」なんですが。)

「印刷プレビュー」及び「印刷」の手続きを次のように追加・修正。

1.手続きの冒頭で、「等幅フォントを忘れずに指定」する(追加)。

  //使用するフォント(必ず等幅フォントを指定する)
  Image1.Picture.Bitmap.Canvas.Font.Name:='Consolas';

  //フォントサイズ -> 実際にはComboBoxで指定・選択できるようにする
  Image1.Picture.Bitmap.Canvas.Font.Size:=11;

2.データ出力部分を次のように修正する。

  //データを出力
  //Image1.Picture.Bitmap.Canvas.TextOut(MarginX+k,MarginY+(iPlus*20),Fields[j]);

  //数値データは右揃えで出力する
  if TryStrToInt(Fields[j], intValue) then
  begin
    //数値である -> 右揃えで出力する
    Image1.Picture.Bitmap.Canvas.TextOut(MarginX+k,MarginY+(iPlus*20),
      Format('%3d', [strToInt(Fields[j])]));
  end else begin
    //数値でない -> 左揃えで出力する
    Image1.Picture.Bitmap.Canvas.TextOut(MarginX+k,MarginY+(iPlus*20),Fields[j]);
  end;

【実行結果】

通し番号(と年齢)が右揃えになった!

追記(20240819)ここまで

2ページ目以降も先頭行にフィールド名を表示

今までの自分には罫線付きでこのように表示することが、どうしてもできなかった・・・


先頭行にフィールド名を表示する部分は、いちばん悩んだところ。
最終的に変数eNum(LoopのEndNumber)から印刷に必要なページ数を取得し、StringListに格納した印刷データの0番目の要素をコピーして、これをStringListの51、101、151のように、eNumの現在の値( i * 50)+1番目に挿入して行く方法が計算的にも、処理的にも、いちばんラクなのではないか?・・・と考え、このアルゴリズムでプログラムを作成。

こうすれば1ページ目には要素0から50、2ページ目には要素51から100、3ページ目には101から150・・・のように印刷データの割り当てが決まり、プログラムも心もすっきり。0番目の要素(フィールド名)のコピーと所定の位置への挿入さえ行ってしまえば、あとは単純にLoopを廻すだけだ。

      eNum:=StringList.Count div 50;

      //51,101,151,201,251,301・・・番目にフィールド名を挿入

      //0番目の要素をコピー
      myFieldElement:=StringList[0];
      //要素を追加
      if eNum<>0 then
      begin
        for i := 1 to eNum do
        begin
          StringList.Insert((50*i)+1, myFieldElement);
        end;
      end;


最も重要なデータ出力部分は、生成AIに教えてもらった!
次のデータを出力するコードの(iPlus * 20)の部分は、自分では絶対に書けなかったと思う・・・。
ついに、と言うか、とうとう、わからない部分は生成AIに聞きながらプログラムが書ける、夢のような時代がやってきた!!

でも、頼りすぎは禁物。実際、今回もかなり痛い目にあった・・・。
その内容は、後述。

//データを出力
Image1.Picture.Bitmap.Canvas.TextOut(MarginX+k,MarginY+(iPlus*20),Fields[j]);

印刷枚数が1ページだけなら、変数はLoop変数の i がそのまま使えるのだが、複数枚印刷を実行する必要があるので、Loop用の変数 i とは別に iPlus という名前の変数(特に意味はない)を用意し、ページが切り替わる毎にゼロで初期化するように生成AIが教えてくれたプログラムを改良。

それから、これはあった方が親切かな? と考え、先頭行のフィールド名の最後に「備考」も追加。

Loopの様子をわかりやすくしたのが次のコード。
ページ内のデータ印刷作業で、i , j , k を使ってしまったので、いちばん大きな(外側の)ページを切り替えるLoopの変数名をどうするかで悩み l(エルの小文字)はちょっと・・・って感じがしたので、最終的に変数名はintLoopとした。

最終的に、TImageのBitmapに出力したものをPrinterのCanvasにコピーして印刷している。

for intLoop := 0 to eNum do
begin
  k:=0;
  iPlus:=0;
  for i := LowNum to HighNum do
  begin
    for j := 0 to Fields.Count - 1 do
    begin    
      //フィールド名に「備考」を追加する
      if i=0 then
      begin
        if j=Fields.Count-1 then
        begin
          Fields[j]:=Fields[j]+' 備考';
        end;
      end;
      //データを出力
      Image1.Picture.Bitmap.Canvas.TextOut(MarginX+k,MarginY+(iPlus*20),Fields[j]);
    end;
    inc(iPlus);
  end; 

  //大きさを指定
  MyRect.Top:=0;
  MyRect.Left:=0;
  MyRect.Bottom:= Trunc((Printer.PageWidth / Image1.Picture.Width) * Image1.Picture.Height);
  MyRect.Right:= Printer.PageWidth;
  //ファイルを描画
  StretchDrawBitmap(Printer.Canvas, MyRect, Image1.Picture.Bitmap);
  Application.ProcessMessages;

end;  //intLoopの終わり


実は、ここでひと悶着あって・・・

5.rect:TRectとしてはいけません!

実際には、最初に1ページ目だけを表示できる「印刷プレビュー」のプログラムを書き、それを元にして「印刷」プログラムを書いたのだが、いつもの通り、というか、お決まりの「解決策がまったくわからずにトホーに暮れる」・・・ 後から考えれば(理由がわかってみれば)実に「なぁーんだ。そんなコトか」みたいな、でも、それがわかるまでは七転八倒の苦しみとなるイベントに今回も遭遇。

毎回、これが楽しみでプログラムを書いている、そんな気がしないでもないが。

今回のそれは・・・ナニかというと、

「印刷プレビュー」の手続き内では「何の問題もなかった」次のコードだが、

procedure TForm1.Button2Click(Sender: TObject);
begin

  ・・・じんせい、イロイロ・・・

  //背景を塗りつぶす
  Image1.Picture.Bitmap.Canvas.Brush.Color := clWhite;
  Image1.Picture.Bitmap.Canvas.FillRect(rect(0, 0, 827, 1169)); 

  ・・・タコは、イボイボ・・・

end;

これを「印刷」手続き内に複写して、「印刷プレビュー」手続きにはないPrinterのCanvasへの描画コードを追加するなど、あちこちいじっていたら、いつの間にか・・・

エラーの!マークが付いてる(問題が起きた状況を再現)


構造ペインには・・・


『はぁ?』

だって行末にちゃんとセミコロンあるし・・・みたいな感じ。
・・・てか、なんで、こっちの手続きだけ、エラーになるの???

この時点で、早朝2時頃から連続15時間くらいPCと向かい合っていたため、精神的にはもうフラフラの状態。もちろん、エラーの原因は、まったくわからない。

まったく同じプログラムコードが、あっちの手続きではOK! こっちの手続きではダメな理由は、いったい何なんだろう???

この後も、しばらく、がんばって考えたんだけど、原因はさっぱりわからず、


Delphiを再起動したら直るかなー?

もちろん、直るわけもなく・・・。


もしかして、PCを再起動したら直るかなー??

もちろん、再起動してもエラーは消えない。


万策尽きた感じで、とりあえず、いけない水に手を伸ばし・・・ 心は折れたまま、遥かなる夢の国へ。

翌朝、ってか、午前0時前に目覚めたから、日付はまだ今日だけど・・・
とりあえず、すっきりした頭で問題に再挑戦。ようやくエラーの原因が判明。

ほんとに偶然発見したのだけれど、エラーにならない「印刷プレビュー」の手続きでは・・・

このRectはSystem.TypesのRect関数・・・


これに対し、エラーになる「印刷」の手続きでは・・・

このrectはvar宣言したTRect型の変数・・・

あー!!
わかったー☆☆☆ みたいな

1ページ目だけ表示可能な最初に書いた「印刷プレビュー」の手続きをそっくり「印刷」手続きに複写して、「印刷プレビュー」の手続きには「存在しなかった」プリンタのCanvasへの描画コードを追加したのだが・・・ その時、var宣言部でTRect型の変数rectを宣言していたのだ。

これが追加したPrinterのCanvasへ描画するプログラムの主要部分。

procedure TForm1.btnPrintASheetClick(Sender: TObject);
var
  i, j: Integer;
  strMsg: string;
  PrintALL: Boolean;
  intLoopNum: Integer;
  rect:TRect;
  StrCaption:String;
  StrPrompt:String;
  StrValue1, StrValue2:String;
  Chr : array [0..255]  of  char;

  // ビットマップ用印刷ルーチン
  procedure StretchDrawBitmap(Canvas:TCanvas;  // 描画先キャンバス
                              r : TRect;       // 描画先範囲
                              Bitmap:TBitmap); // ビットマップ
  ・・・省略・・・

begin
  if PrinterSetupDialog1.Execute then
  begin

    //背景を塗りつぶす
    Image1.Picture.Bitmap.Canvas.Brush.Color := clWhite;
    Image1.Picture.Bitmap.Canvas.FillRect(rect(0, 0, 827, 1169));  //エラーになる部分

    //Info
    strMsg:='全員分印刷しますか?'+#13#10+'(個別印刷は「いいえ」)';
    if Application.MessageBox(PChar(strMsg), PChar('情報'), MB_YESNO or MB_ICONINFORMATION) = mrYes then
    begin
      PrintALL:=True;
    end else begin
      PrintALL:=False;
    end;

    //全部印刷
    if PrintAll then
    begin
      //先頭のデータを表示
      btnFirstClick(Sender);
      for i := 1 to ListBox1.Items.Count do
      begin
        //まず現在のImageを印刷
        with Printer do
        begin
          if i=1 then
          begin
            BeginDoc;
          end else begin
            NewPage;
          end;
          //大きさを指定
          rect.Top:=0;
          rect.Left:= 0;
          rect.Bottom:= Trunc(( PageWidth / Image1.Picture.Width) * Image1.Picture.Height);
          rect.Right:= PageWidth;
          //TImageのBitmapをPrinterのCanvasに描画
          StretchDrawBitmap(Printer.Canvas, rect, Image1.Picture.Bitmap);
          
          if i=ListBox1.Items.Count then
          begin
            EndDoc;
          end;
        end;

        //次を表示
        btnNextClick(Sender);
      end;

      ・・・

だから、結果的に(当然だが)「印刷」手続き内ではコードで意図したSystem.TypesのRect関数は呼ばれずに、「rect」と記述するとそれはvar宣言したrect変数の方を意味(参照)することになって・・・

これはエラーになって当然。Delphiさん、あなたはやっぱり正しかった。

ちなみに、Delphiは大文字・小文字を区別しないから、R でも r でも問題は起きない。問題の根源であるvar宣言部のrect変数の名前を変え、次のようにコードを書き直せば・・・

☆エラーは消えました☆

System.TypesのRect関数と名前が衝突しないように、変数名をMyRectに変える
もしくはSystem.Types.pasのRect関数を明示的に指定する


(試してみたい方は、次のコードをコピペしてください。)

procedure TForm1.Button4Click(Sender: TObject);
var
  MyRect:TRect;
begin

  //背景を塗りつぶす
  Image1.Picture.Bitmap.Canvas.Brush.Color := clWhite;

  //解決方法その1
  MyRect:=rect(0, 0, 827, 1169);
  Image1.Picture.Bitmap.Canvas.FillRect(MyRect);

  //解決方法その2
  Image1.Picture.Bitmap.Canvas.FillRect(System.Types.Rect(0, 0, 827, 1169));

end;

【まとめ】

関数名として使われているような語句をそのまんま変数名として利用するのはNo Good! であります。

6.印刷プレビューのコード

そういう訳で、「印刷」手続きのコードが完成し、問題も解決したので、この完成したコードを「印刷プレビュー」に流用することにした。

基本的に、「印刷」手続きのコードからBeginDocとNewPage、それからEndDocを消し、PrinterのCanvasに描画してる部分をコメントアウトして、1ページ目以降を表示する方法を追加すればOKのはずだ・・・。そう考えて書いたのが次のコード。

procedure TForm1.Button2Click(Sender: TObject);
var
  //用紙サイズ、縦置き・横置きの設定を知る(Charだと推奨されない警告が表示される->Stringに変更)
  //Device, Driver, Port: array[0..255] of Char;
  Device, Driver, Port: string;
  DeviceMode: THandle;
  DevMode: PDeviceMode;

  //StringGrid->CSVファイル名とそこまでのPathを入れる
  csvFN:string;

  StringList: TStringList;
  i, j, k, MaxWidth: Integer;
  Fields: TStringList;
  FieldWidths: array of Integer;
  ColMargin: Integer;
  MarginX, MarginY: Integer;
  intLoop: Integer;
  FontHeight: Integer;
  eNum: Integer;
  iPlus: Integer;
  myFieldElement: string;
  LowNum: Integer;
  HighNum: Integer;
  MyRect:TRect;
  //平均値・最高値・最低値
  //DSum: Double;
  //DAvg: Double;
  //MinValue, MaxValue: Double;

  //StringGrid -> CSV File
  procedure SaveStringGridToCSV(StringGrid: TStringGrid; const FileName: string);
  var
    CSVFile: TextFile;
    Row, Col: Integer;
    Line: string;
  begin
    AssignFile(CSVFile, FileName);
    Rewrite(CSVFile);
    try
      for Row := 0 to StringGrid.RowCount - 1 do
      begin
        Line := '';
        for Col := 0 to StringGrid.ColCount - 1 do
        begin
          Line := Line + StringGrid.Cells[Col, Row];
          if Col < StringGrid.ColCount - 1 then
            Line := Line + ',';
        end;
        WriteLn(CSVFile, Line);
      end;
    finally
      CloseFile(CSVFile);
    end;
  end;

  procedure GetMinMaxValues(StringGrid: TStringGrid; ColIndex: Integer; out MinValue, MaxValue: Double);
  var
    Row: Integer;
    Value: Double;
  begin
    if StringGrid.RowCount = 0 then
      raise Exception.Create('StringGridにデータがありません。');

    MinValue := MaxDouble;
    MaxValue := -MaxDouble;

    for Row := 1 to StringGrid.RowCount - 1 do
    begin
      if TryStrToFloat(StringGrid.Cells[ColIndex, Row], Value) then
      begin
        if Value < MinValue then
          MinValue := Value;
        if Value > MaxValue then
          MaxValue := Value;
      end;
    end;

    if MinValue = MaxDouble then
      raise Exception.Create('指定された列に数値データがありません。');
  end;

  //Image1のBitmapをImage2の指定位置へ複写する
  procedure CopyBitmapToImage(Image1, Image2: TImage; DestX, DestY: Integer);
  var
    SrcRect, DestRect: TRect;
  begin
    // ソースの矩形を設定
    SrcRect := Rect(0, 0, Image1.Picture.Bitmap.Width, Image1.Picture.Bitmap.Height);
    // 目的地の矩形を設定
    DestRect := Rect(DestX, DestY, 
      DestX + Image1.Picture.Bitmap.Width, DestY + Image1.Picture.Bitmap.Height);
    // Image2のCanvasにImage1のBitmapを複写
    Image2.Picture.Bitmap.Canvas.CopyRect(DestRect, Image1.Picture.Bitmap.Canvas, SrcRect);

    //追加(20240820)
    //ページ区切り線を表示するコードを追加
    //ペンの色を青に設定
    Image2.Picture.Bitmap.Canvas.Pen.Color := clGray;
    //ページ区切り線の太さ
    Image2.Picture.Bitmap.Canvas.Pen.Width:=3;
    //ペンのスタイルを点線に設定
    //Image1.Canvas.Pen.Style := psDot;
    Image1.Canvas.Pen.Style := psSolid;
    //線を引く
    Image2.Canvas.MoveTo(0, DestY + Image1.Picture.Bitmap.Height); // 線の開始位置
    Image2.Canvas.LineTo(Image2.Picture.Bitmap.Width, 
      DestY + Image1.Picture.Bitmap.Height); // 線の終了位置
    //ページ区切り線の太さを元に戻す
    Image2.Picture.Bitmap.Canvas.Pen.Width:=1;
    //ペンの色を黒に設定
    Image2.Picture.Bitmap.Canvas.Pen.Color := clBlack;
    //ペンのスタイルを直線に戻す
    //Image1.Canvas.Pen.Style := psSolid;
  end;

begin

  //複数回クリックを防止する
  Button2.Enabled:=False;

  //初期化
  Image1.Picture:=nil;

  //印刷設定(用紙・向き)後に印刷

  //プリンタの設定を取得
  Printer.GetPrinter(Device, Driver, Port, DeviceMode);
  DevMode := GlobalLock(DeviceMode);
  try
    //用紙サイズをA4に設定
    DevMode^.dmPaperSize := DMPAPER_A4;
    //用紙方向を縦に設定
    DevMode^.dmOrientation := DMORIENT_PORTRAIT;
    //設定をプリンタに反映
    Printer.SetPrinter(Device, Driver, Port, DeviceMode);
  finally
    GlobalUnlock(DeviceMode);
  end;

  //TImageの初期設定
  Image1.Width := 827;
  Image1.Height := 1169;
  Image1.Picture.Bitmap.Width := 827;
  Image1.Picture.Bitmap.Height := 1169;

  //背景を塗りつぶす
  Image1.Picture.Bitmap.Canvas.Brush.Color := clWhite;
  MyRect:=Rect(0, 0, 827, 1169);
  Image1.Picture.Bitmap.Canvas.FillRect(MyRect);

  //使用するフォント(必ず等幅フォントを指定する)
  //数値の右揃え用に追加(20240820)
  Image1.Picture.Bitmap.Canvas.Font.Name:='Consolas';

  //フォントサイズ
  Image1.Picture.Bitmap.Canvas.Font.Size:=11;
  //フォントサイズ <- 要らなかった!
  //Image2.Picture.Bitmap.Canvas.Font.Size:=11;

  //平均値を計算  intDenominはグローバル変数として宣言
  {
  DSum:=0;
  for i := 1 to StringGrid1.RowCount do
  begin
    if StringGrid1.Cells[5,i] <> '' then
    begin
      DSum:= DSum + StrToInt(StringGrid1.Cells[5,i]);
    end;
  end;
  DAvg:= SimpleRoundTo(DSum / intDenomin, -2);

  //最高値及び最低値を計算
  GetMinMaxValues(StringGrid1, 5, MinValue, MaxValue);

  //タイトルを描画
  Image1.Picture.Bitmap.Canvas.Font.Color := clBlue;
  Image1.Picture.Bitmap.Canvas.TextOut(
    StrToInt(EditMarginX.Text), StrToInt(EditMarginY.Text)-30, LabelKoza.Caption);

  //フォント色を変更
  Image1.Picture.Bitmap.Canvas.Font.Color := clBlack;
  }

  //Grid -> CSV
    //実際のプログラムでは、sNameフォルダ内のCSVファイルを読み込み、
    //さらに幾つかフィールドを追加して新しいデータを入力している。
    //印刷では、この新しく入力されたデータを含めて印刷している

    //実際のプログラムでは、LabelSaveFolderName.Captionは別手続きで取得・表示済み
    LabelSaveFolderName.Caption:='SampleData';

    //保存するフォルダへのPath
    csvFN:=IncludeTrailingPathDelimiter(ExtractFilePath(Application.ExeName))+
      'ProcData\'+LabelSaveFolderName.Caption+'\';

    //フォルダの存在を確認、なければ作成
    if not System.SysUtils.DirectoryExists(ExtractFileDir(csvFN)) then
    begin
      //フォルダ階層を作成
      System.SysUtils.ForceDirectories(ExtractFileDir(csvFN));
    end;

    csvFN:=IncludeTrailingPathDelimiter(ExtractFilePath(Application.ExeName))+
      'ProcData\'+LabelSaveFolderName.Caption+'\'+LabelSaveFolderName.Caption+'.csv';
    SaveStringGridToCSV(StringGrid1, csvFN);

  StringList:=TStringList.Create;
  Fields:=TStringList.Create;

  try

    //Create
    StringList.LoadFromFile(csvFN);
    //Create
    SetLength(FieldWidths, 0);

    //各フィールドの最大幅を計算
    for i := 0 to StringList.Count - 1 do
    begin
      Fields.CommaText := StringList[i];
      if Length(FieldWidths) < Fields.Count then
        SetLength(FieldWidths, Fields.Count);

      for j := 0 to Fields.Count - 1 do
      begin
        //MaxWidth := Printer.Canvas.TextWidth(Fields[j]);
        MaxWidth := Image1.Picture.Bitmap.Canvas.TextWidth(Fields[j]);
        if FieldWidths[j] < MaxWidth then
          FieldWidths[j] := MaxWidth;
      end;
    end;

    eNum:=StringList.Count div 50;

    //PreView用TImageの初期設定
    Image2.Width := 827;
    case eNum of
      0:Image2.Height := 1169;
    else
      Image2.Height := 1169 * (eNum + 1);
    end;
    Image2.Picture.Bitmap.Width := 827;
    case eNum of
      0:Image2.Picture.Bitmap.Height := 1169;
    else
      Image2.Picture.Bitmap.Height := 1169 * (eNum + 1);
    end;    

    //背景を塗りつぶす
    Image2.Picture.Bitmap.Canvas.Brush.Color := clWhite;
    case eNum of
      0:MyRect:=Rect(0, 0, 827, 1169);
    else
      MyRect:=Rect(0, 0, 827, 1169 * (eNum + 1));
    end;
    Image2.Picture.Bitmap.Canvas.FillRect(MyRect);

    //51,101,151,201,251,301,・・・,XX1番目にフィールド名を挿入しておく

    //0番目の要素をコピー
    myFieldElement:=StringList[0];
    //要素を挿入(追加)
    if eNum<>0 then
    begin
      for i := 1 to eNum do
      begin
        StringList.Insert((50*i)+1, myFieldElement);
      end;
    end;

    //ここから印刷Loop
    try

      for intLoop := 0 to eNum do
      begin

        //初期化(白紙にする)
        Image1.Picture.Bitmap.Canvas.Brush.Color := clWhite;
        MyRect:=Rect(0, 0, 827, 1169);
        Image1.Picture.Bitmap.Canvas.FillRect(MyRect);
        {
        //印刷プレビューだから不要
        if intLoop=0 then
        begin
          Printer.BeginDoc;
        end else begin
          Printer.NewPage;
        end;
        }

        //タイトルを描画
        Image1.Picture.Bitmap.Canvas.Font.Color:=clBlue;
        Image1.Picture.Bitmap.Canvas.TextOut(
          StrToInt(EditMarginX.Text), StrToInt(EditMarginY.Text)-30,
          LabelSaveFolderName.Caption);

        //タイトルを描画(計算が必要な場合の例)
        {
        Image1.Picture.Bitmap.Canvas.Font.Color := clBlue;
        Image1.Picture.Bitmap.Canvas.TextOut(
          StrToInt(EditMarginX.Text), StrToInt(EditMarginY.Text)-30,
          LabelKoza.Caption + ' 【平均値:'+FloatToStr(DAvg)+
          '、最高値:'+ FloatToStr(MaxValue)+
          '、最低値:'+ FloatToStr(MinValue)+'】');
        }

        Image1.Picture.Bitmap.Canvas.Font.Color:=clBlack;

        //水平方向の各フィールドの印字開始位置決定用変数を初期化
        k:=0;

        //水平方向の印字開始位置
        MarginX:=StrToInt(EditMarginX.Text);
        //垂直方向の印字開始位置
        MarginY:=StrToInt(EditMarginY.Text);
        //列(フィールド)と列の(余白的な)間隔
        ColMargin:=StrToInt(EditColMargin.Text);

        //ページが変わったら初期化する
        iPlus:=0;

        case intLoop of
          0:begin
            LowNum:=0;
            if StringList.Count > 50 then
            begin
              HighNum:=50;
            end else begin
              HighNum:=StringList.Count-1;
            end;
          end;
          {
          1:begin
            LowNum:=51;
            if StringList.Count > 100 then
            begin
              HighNum:=100;
            end else begin
              HighNum:=StringList.Count-1;
            end;
          end;
          2:begin
            LowNum:=101;
            if StringList.Count > 150 then
            begin
              HighNum:=150;
            end else begin
              HighNum:=StringList.Count-1;
            end;
          end;
          }
        else
          //一般化
          LowNum:=(intLoop*50)+1;
          if StringList.Count > (intLoop*50)+50 then
          begin
            HighNum:=(intLoop*50)+50;
          end else begin
            HighNum:=StringList.Count-1;
          end;
        end;

        for i := LowNum to HighNum do
        begin
          Fields.CommaText := StringList[i];
          for j := 0 to Fields.Count - 1 do
          begin
            //処理できる列数を無制限にする
            case j of
              0:k:=0;
            else
              k:=k+FieldWidths[j-1]+ColMargin;
            end;
            //フィールド名に「備考」を追加する
            if i=0 then
            begin
              if j=Fields.Count-1 then
              begin
                Fields[j]:=Fields[j]+' 備考';
              end;
            end;

            //データを出力(数値の右揃え:なし)
            //Image1.Picture.Bitmap.Canvas.TextOut(MarginX+k,MarginY+(iPlus*20),Fields[j]);

            //データを出力(数値の右揃え:あり)
            //数値の右揃え用に追加(20240820)
            if TryStrToInt(Fields[j], intValue) then
            begin
              //数値である -> 右揃えで出力する
              Image1.Picture.Bitmap.Canvas.TextOut(MarginX+k,MarginY+(iPlus*20),
                Format('%3d', [strToInt(Fields[j])]));
            end else begin
              //数値でない -> 左揃えで出力する
              Image1.Picture.Bitmap.Canvas.TextOut(MarginX+k,MarginY+(iPlus*20),Fields[j]);
            end;

            //罫線を描画
            if cbLine.Checked then
            begin
              Image1.Picture.Bitmap.Canvas.Pen.Color:= clBlack;
              FontHeight:= -1 * Image1.Picture.Bitmap.Canvas.Font.Height;
              Image1.Picture.Bitmap.Canvas.MoveTo(MarginX+k, MarginY+(iPlus*20)+FontHeight+4);
              Image1.Picture.Bitmap.Canvas.LineTo(Image1.Picture.Bitmap.Width-50, MarginY+(iPlus*20)+FontHeight+4);
            end;
          end;

          inc(iPlus);

        end;

        //Image1のBitmapをImage2の(XX, YY)の位置に複写
        case intLoop of
          0:CopyBitmapToImage(Image1, Image2, 0, 0);
        else
          CopyBitmapToImage(Image1, Image2, 0, 1169 * intLoop);
        end;

      end;

    finally
      //Printer.EndDoc;
    end;

  finally
    StringList.Free;
    Fields.Free;
    //複数回クリックを防止する
    Button2.Enabled:=True;
  end;

  //ファイルの完全削除
  DeleteFile(PChar(csvFN));

  //Imageの高さをScrollBoxのスクロール範囲に反映
  ScrollBox1.VertScrollBar.Range := Image2.Picture.Bitmap.Height;

  //TImageの表示位置を指定
  ScrollBox1.VertScrollBar.Position:=0;
  ScrollBox1.HorzScrollBar.Position:=0;
  Image1.Top:=ScrollBox1.VertScrollBar.Position+14;
  Image1.Left:=ScrollBox1.HorzScrollBar.Position+14;
  Image2.Top:=ScrollBox1.VertScrollBar.Position+14;
  Image2.Left:=ScrollBox1.HorzScrollBar.Position+14;

  //TImageの表示
  Image1.Visible:=False;
  Image2.Visible:=True;

end;


【実行結果】

実行結果は、次の通り。用意したデータ件数は320件。

1ページ目


スクロールして下へ。表示されているのは、最終ページ。
※ マウスのホイールを廻してスクロールさせるには別途コードの記述が必要(後述)。

最終ページ


マウスのホイールを廻して、TImageをスクロールさせるには、FormのOnMouseWheelイベントの手続きを次のように作成する。

procedure TForm1.FormMouseWheel(Sender: TObject; Shift: TShiftState;
  WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
var
  LDelta:Integer;
  //追加
  LWinCtrl:TWinControl;
  LCurPos:TPoint;
begin

  {
  //TScrollBox のマウスホイールによるスクロール
  //マウスがTScrollBoxの外にあってもスクロールする・・・ならこちら☆
  LDelta:=WheelDelta div 5;
  if ssCtrl in Shift then
  begin
    ScrollBox1.HorzScrollBar.Position:=ScrollBox1.HorzScrollBar.Position-LDelta;
  end else begin
    ScrollBox1.VertScrollBar.Position:=ScrollBox1.VertScrollBar.Position-LDelta;
  end;
  Handled:=True;
  }

  //マウスカーソルが TScrollBox の領域内にある時だけスクロールを可能にする
  LCurPos := ScrollBox1.Parent.ScreenToClient(MousePos);
  if PtInRect(ScrollBox1.BoundsRect, LCurPos) then
  begin
    LDelta := WheelDelta div 3;
    if ssCtrl in Shift then
    begin
      ScrollBox1.HorzScrollBar.Position := ScrollBox1.HorzScrollBar.Position - LDelta;
    end else begin
      ScrollBox1.VertScrollBar.Position := ScrollBox1.VertScrollBar.Position - LDelta;
      //Memoも連動してスクロールさせる
      {
      if LDelta > 0 then
      begin
        Memo2.Perform(WM_VSCROLL, SB_LINEUP, 0);
      end else begin
        Memo2.Perform(WM_VSCROLL, SB_LINEDOWN, 0);
      end;
      }
    end;
  end else begin
    //マウス直下のコントロールを取得
    LWinCtrl := FindVCLWindow(MousePos);
    //TStringGridの場合
    if LWinCtrl is TStringGrid then
    begin
      if WheelDelta > 0 then
      begin
        LWinCtrl.Perform(WM_VSCROLL, SB_LINEUP, 0);
      end else begin
        LWinCtrl.Perform(WM_VSCROLL, SB_LINEDOWN, 0);
      end;
    end;
  end;

  //この1行を忘れないこと!
  Handled:=True;

end;


【印刷プレビューコードの工夫】

1.ページ毎に作成されるImage1のBitmapを、Image2のCanvasの指定位置に複写する。
2.最初にページ数を調べ、Image2の高さをページ数にあわせて高くしておく。
3.1の複写手続きの最後に、ページ区切り線を描画するコードを追加(20240820)。

1.に関して、Image1のBitmapをImage2のCanvasに複写する手続き

  //Image1のBitmapをImage2の指定位置へ複写する
  procedure CopyBitmapToImage(Image1, Image2: TImage; DestX, DestY: Integer);
  var
    SrcRect, DestRect: TRect;
  begin
    // ソースの矩形を設定
    SrcRect := Rect(0, 0, Image1.Picture.Bitmap.Width, Image1.Picture.Bitmap.Height);

    // 目的地の矩形を設定
    DestRect := Rect(DestX, DestY, DestX + Image1.Picture.Bitmap.Width, DestY + Image1.Picture.Bitmap.Height);

    // Image2のCanvasにImage1のBitmapを複写
    Image2.Picture.Bitmap.Canvas.CopyRect(DestRect, Image1.Picture.Bitmap.Canvas, SrcRect);
  end;

    //追加(20240820)
    //ページ区切り線を表示するコードを追加
    //ペンの色を青に設定
    Image2.Picture.Bitmap.Canvas.Pen.Color := clGray;
    //ページ区切り線の太さ
    Image2.Picture.Bitmap.Canvas.Pen.Width:=3;
    //ペンのスタイルを点線に設定
    //Image1.Canvas.Pen.Style := psDot;
    //ペンのスタイルを直線に設定
    Image1.Canvas.Pen.Style := psSolid;
    //線を引く
    Image2.Canvas.MoveTo(0, DestY + Image1.Picture.Bitmap.Height); // 線の開始位置
    Image2.Canvas.LineTo(Image2.Picture.Bitmap.Width, 
      DestY + Image1.Picture.Bitmap.Height); // 線の終了位置
    //ページ区切り線の太さを元に戻す
    Image2.Picture.Bitmap.Canvas.Pen.Width:=1;
    //ペンの色を黒に設定
    Image2.Picture.Bitmap.Canvas.Pen.Color := clBlack;
    //ペンのスタイルを直線に戻す
    //Image1.Canvas.Pen.Style := psSolid;
  end;

ページ区切り線については、いろいろ試行した結果、やや太い灰色の直線が最も適している(ほどよく自己主張するが、データほどではない)と感じたので、そのように設定。上のコードにはその痕跡を残している。


1.に関して、複写する手続きを呼び出すコード。

        //Image1のBitmapをImage2の(XX, YY)の位置に複写
        case intLoop of
          0:CopyBitmapToImage(Image1, Image2, 0, 0);
        else
          CopyBitmapToImage(Image1, Image2, 0, 1169 * intLoop);
        end;


2.に関して、ページ数に応じてImage2の高さを高くするコード。

    eNum:=StringList.Count div 50;

    //PreView用TImageの初期設定
    Image2.Width := 827;
    case eNum of
      0:Image2.Height := 1169;
    else
      Image2.Height := 1169 * (eNum + 1);
    end;
    Image2.Picture.Bitmap.Width := 827;
    case eNum of
      0:Image2.Picture.Bitmap.Height := 1169;
    else
      Image2.Picture.Bitmap.Height := 1169 * (eNum + 1);
    end;

7.まとめ

(1)StringGridのデータを用紙と向きを指定して罫線付きで印刷するコードを掲載
(2)(1)のコードを流用して、印刷プレビューを表示するコードを掲載
(3)変数名を付ける時は既存の関数名等との衝突に十分に注意する。

8.お願いとお断り

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

塗りつぶさないマークシート

「塗りつぶす」方式でなく、より簡易な「線を引く」方式でマークするシートの例。

複数マーク可能なマークシート(選択肢は0~99 に対応)を作成したら、マークする時間そのものを短縮する必要性を痛感。今回は「よりはやく」・「よりカンタン」にマーク可能なシート作りに挑戦。

追記(20240929)

当Blogで紹介してきた自作のデジタル採点プログラムを一つにまとめました。次のリンク先にその紹介とダウンロードリンクがあります。マークシートも、ここに紹介した形式の他、様々なタイプのものを同梱しています。

上のリンク先で、ここで紹介したマークシートを含む、デジタル採点プログラム一式をダウンロードできます。

【もくじ】

1.もっとはやくマークできないか?
2.凡例として線でマークして違和感のないカタチは?
3.「線」マークの読み取りテスト
4.まとめ
5.お願いとお断り

1.もっとはやくマークできないか?

設問数が100ある試験で、複数マークを可とした場合、受験者は制限時間内に最大で200近いマークを塗りつぶさなくてはならない。制限時間が50分の試験で、190個マークするとして、1個2秒でマークした場合、マークするのに必要な時間は380秒、すなわち6分20秒となる。1個3秒でマークした場合の必要時間は、なんと9分30秒。制限時間の1/5がマークするためだけに使われてしまう。

採点者がラクをするのに、無理やり付き合わされる受験者の不満が爆発するのが見えるような気がして、小心者の筆者は「マークするために必要な時間を短縮する」にはどうしたらいいか、必死で考えた。

これまでに使用してきたマークシートのマークは、すべて選択肢の番号を縦長だ円で囲んだもの。

これまでに使用してきたマークシート


実際には、テストしてみると、次のようにマークしても読み取りパラメータの設定次第で十分読み取り可能なのだが・・・

こんなマークでも、実際には読み取りが可能。


しかし、受験者の心理として、選択肢が縦長だ円で囲まれていれば、だ円内を塗りつぶしたくなるもの。塗りつぶさず、「線」でマークしてもらうには、「線」でマークしたくなる形状にマークシートを改良しなければならない。

最初に考えたのは、「選択肢の番号を縦長四角形で囲む」という方法。

たとえば、こんな感じ


ただ、これだと、凡例で次のようにマーク方法を示してあっても・・・

どちらかと言えば、これはむしろ「悪いマーク」の例


真面目な受験者であれば、あるほど・・・

枠内を塗りつぶさなくてはいけないと考えるのが自然。


かといって、次のようにすると、デザインとして美しくない気がするし、四角形の幅が狭まったたけで「塗りつぶし」たくなる気持ちは同じ。

塗りつぶし面積が減少しただけ?


そんな、こんな理由から縦長四角形で選択肢の番号を囲むというアイデアは見送ることに決定。

2.凡例として線でマークして違和感のないカタチは?

では、凡例として線でマークしてあっても受験者に違和感を与えないマークとは、どんなマークか?

思いついたのは「囲みのない」マーク。これなら・・・

凡例として「線」が使えるし、塗りつぶしたくても塗りつぶす枠がない!


ただ、これだとマークシートっぽくない気が・・・。

マークシートらしくするために一工夫した結果・・・

[ ] 記号で数字の上下を囲んでみた


これならマークの凡例として、「線」が使えるのではあるまいか?


で、出来上がったのが、このマークシート。

複数マーク可能な「線」でマークするシート(設問数は100設問まで)


このマークシートは、次のリンク先からダウンロードすることができます。

3.「線」マークの読み取りテスト

例え、「線」であっても、これまでの経験からまず間違いなくマークの読み取りには成功すると思ったが、「やっぱりできませんでした!」では使ってくださる方に申し訳がたたないので、念のためテストを実行。

読み取り結果を確認しやすいように、設問番号にマークした次のようなマークシートを作成。やっぱり「線」で引く方が、「塗りつぶし」より遥かにラクであることを実感。これなら1設問に対し2か所マークしても「塗りつぶす」より、全然はやい!!

筆記用具は、シャープペンシルを使用。
芯は、HBの0.5mm。
用紙はホームセンターなどで普通に販売されている白色度の高いコピー用紙を使用。
(普段マークシートの印刷に使用している再生紙より、白色度が高いもの)


MS_Reader.exe を起動し、パラメータ設定はデフォルトのまま、読み取りテストを実行。

スキャナーの解像度は200dpiに設定。
カラー画像としてスキャン。
テスト1回目で、全てのマークの読み取りに成功!


間違いなく読めると思ってはいたけれど、ちょっと(あれっ?)って思ったのは、デフォルトパラメータの設定のまま、テスト第1回目で、全てのマークを正しく読み取ることに成功したこと。

実は、今回、実験で使用した用紙とは異なる、白色度が70%程度の再生コピー用紙を用いて予備的に実験した際は、閾値のパラメータをデフォルト設定より1~3程度大きく設定しないと読み取れないマークがあったのだ。今回、実験したものと「線」そのものの濃さや太さが厳密には異なるから正確なことは言えないが、白色度が高い用紙の方が二値化の際に有利なのだろうか?

・・・ということで、今回の実験では何も問題が起きなかったので、その前に行った白色度が70%程度の再生コピー用紙を使って行った予備実験で発見できた問題と対応方法を紹介。

(これは当Blogの過去記事「100選択肢対応マークシートを使用した試験の実施方法」に書いたものの再録です)

【重要】二値化閾値の修正方法(20240707追加)

マークが「うすい」場合、これを正しく読み取ることができず、「空欄」と判定して「999」と表示される場合があります。同じ理由で、複数マークされた解答欄の「1の位」が読めなかった場合も、読み取り判定は「マークの状態に問題あり」となり、「999」と表示されます。

これらの場合は、この後、実行する「読み取り結果のチェック」時に、該当箇所の解答欄が赤枠で囲まれて表示されますので、マークの状態をヒトの眼で確認し、読み取り結果を修正できます。

最も困るのが、複数マークされた解答欄の「10の位」のマークは薄くて読めなかったが、「1の位」のマークの読み取りには成功している場合です。この場合は正しく読み取れた「1の位」のマークが読み取り結果として表示されてしまいます。大変申し訳ないのですが、MS_Reader.exe のチェックプログラムは、この誤読を見つけることができません!!(これは原理的な問題なので、チェックする方法がありません)

この事故を防ぐには、事前に読み取りテストを十分に行って、読み取りパラメータを調整するしか方法がありません。具体的には、最もマークの濃度が薄い受験者のマークシートが正しく読めるようになるまで、閾値の値を1ずつ大きくして読み取りテストを実行します。筆者の行ったテストでは、デフォルト設定の閾値(180)では正しく読めなかったマークも、閾値を大きくすれば読めるようになりました。

「塗りつぶす」方式でなく、より簡易な「線を引く」方式でマークするシートを用いて行った読み取りテストの例

「64」と読むべき箇所の「10の位」を「空欄」と判定、「1の位」は正しく読めた場合、プログラムは読み取り結果を「4」と表示してしまう。


閾値を「1」大きくすると・・・

正しく読めるようになりました。


ごく薄いマークも正しく読めるようになるまで、この操作をくり返します。筆者の行ったテストでは、閾値を1ずつ大きくすることで、指示通り普通の濃さ(マーク箇所の数字が読めなくなる濃さ)でマークされたシートであれば、最終的にすべてのマークを正しく読めるようになりました(上記のマークシートを使用して行った実験では、最終的にすべてのマークを正しく読めた段階で、二値化の閾値の値は「184」でした。なお、このマークシートは後日公開する予定です)。

ただ、あまりにもマークの濃さが薄い場合は「原理的に読めません」ので、「対応不可」としてヒトの眼で読むしかないと思います・・・。

そのような事態にならないよう、予め受験者に対し「薄いマークは読み取れない」旨の注意を徹底しておく必要があります。マークシートを用いた試験では、これが最も重要なことかもしれません。

4.まとめ

(1)複数マーク可能なマークシートを使う場合は、「線」でマークできるようにする。
(2)マークシートを印刷する用紙は、白色度の高いものがよい?カモ
(3)マークシートは「濃く」マークするよう、予め注意を徹底する。
(4)筆記用具は、硬さと濃さが2B、太さ0.9mmのシャープペンシルがよいと思われます。

追記_20240709

【注意】複数マーク可能なマークシートでは、音声読み上げ機能は正しく動作しません!

5.お願いとお断り

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

100選択肢対応マークシートを使用した試験の実施方法

自作マークシートリーダーのプログラムを書き替え、大語群(使用できる選択肢の番号は 0~99)の使用に対応したマークシートを Excel で作成した。

今回は、このマークシートを用いた試験を実施する方法です。

【もくじ】

1.事前の準備と受験者への注意事項
2.スキャナーでマークシートをスキャンしてJpeg画像に変換
3.指定のフォルダ内にフォルダを作成してスキャンした画像をコピー
4.採点専用画像に変換
5.テンプレートとして登録
6.マーク読み取りを実行
7.マーク読み取り結果のチェック
8.CSV形式で出力
9.採点結果通知シートの作成方法
10.お願いとお断り

追記(20240929)

当Blogで紹介してきた自作のデジタル採点プログラムを一つにまとめました。次のリンク先にその紹介とダウンロードリンクがあります。マークシートも、ここに紹介した形式の他、様々なタイプのものを同梱しています。

1.事前の準備と受験者への注意事項

(1)マークシートの印刷

プログラムに添付した R25C04D19.xlsx をダブルクリックして開き、必要な枚数をインクジェットプリンタで印刷します。印刷設定はA4・横に設定済みです。用紙はホームセンター等で購入できるコピー用紙を使用してください(読み取りパラメータの設定とマークの読み取りテストは再生コピー用紙を使用して行っています)。マークや罫線枠が設定よりも濃く印刷される輪転機での印刷はお止めください。

100選択肢(0~99)に対応した、複数選択可能なマークシート 「R25C04D19.xlsx」


(2)受験者への注意事項

試験を実施する前にHB以上の濃さの鉛筆とプラスチック消しゴムを用意するよう、受験者全員に連絡してください。可能であれば、シャープペンシルの使用は禁止した方がよいと思います。

※追記(20240924)
どうしてもシャープペンシルを使用したい場合は、硬さ・濃さは「2B」で、芯の太さは「0.9mm」以上のものであれば使用可とするような「条件付き許可」とした方がよろしいかと思います。

問題冊子の表紙には、次の注意事項を印刷してください。


上記注意事項のサンプルをダウンロードできます。

2.スキャナーでマークシートをスキャンしてJpeg画像に変換

(1)スキャンの準備

解答用紙(マークシート)をスキャナーでスキャンする前に、解答用紙が裏返しだったり、逆さまになっていないかどうか等の確認に加え、次のことを必ず実行してください。

・解答用紙に付着している消しゴムの屑をしっかり落とす。
・受験番号(出席番号)が昇順になるよう、解答用紙の並び順を2回以上確認する。
・欠席者がいる場合は、未使用の解答用紙を該当箇所に挿入する。

(2)スキャナーの設定

解答用紙(マークシート)をスキャンする際の読み取り解像度は 200dpi で十分です(解像度を大きく設定しても、MS_Reader.exe での利用に関する限り、メリットは何一つありません)。また、画質は「カラー」を指定し、出力先はPDFファイルではなく、Jpeg画像を指定してください。

3.指定のフォルダ内にフォルダを作成してスキャンした画像をコピー

スキャンしたJpeg画像は、MS_Reader.exeと同じ階層にあるScanDataフォルダ内に適切な名称のフォルダを作成し、そこに保存してください。

【利用できるフォルダ構造】

〇:ScanData¥1年A組

【利用できないフォルダ構造】

×:ScanData¥1年¥A組

ScanData フォルダ内に新規にフォルダを作成し、さらにそのフォルダ内に新規にフォルダを作成して、そこにスキャンした Jpeg 画像を保存するような使い方は出来ません。ご注意ください。

4.採点専用画像に変換

ScanData フォルダ内に新規に作成したフォルダにスキャンしたJpeg画像が用意できたら、次にこの画像をマークシートリーダーで読むための専用画像に変換します。その理由はいくつかありますが、主なものは次の三つです。

(1)読み取り原本は、オリジナル状態のまま残しておく。
(2)マーク読み取り後に人の眼でチェックする際、作業しやすい大きさに整える。
(3)Loop 処理するため、画像の名称を統一し、1から始まる連番の番号を付ける。

具体的な方法は次の通りです。

(1)ダウンロードした Zip ファイルを展開したフォルダ内にある MS_Reader を起動します。

上のアイコンをダブルクリックして起動します。


(2)画面左上のメニューの「1 画像変換」をクリックします。


(3)表示されたサブメニューの「専用画像を作成」をクリックします。


(4)次のフォームが表示されます。画面右上の「選択」ボタンをクリックします。


(5)ScanDataフォルダの内容が表示されます。マークの読み取り処理を行いたいJpeg画像を保存したフォルダをクリックして選択します。選択したフォルダ名が下の「Folder」部分に表示されていることを確認して「OK」をクリックしてください。

【重要】選択するのは「フォルダ」で、「ファイル」ではありません!

Folder部分に、選択したフォルダ名が表示されます。


(6)画面は次のようになります。画面の中央左に表示されたサムネイル画像を参考にして、画像の回転の有無及び回転方向を選択し、続けて、リサイズの有無と縮小率を指定します。読み取り解像度が200 dpi ・A4横形式で、マークシートの列が4列ある場合は、「80%に縮小」してください。


(7)変換処理を行った画像データの出力先(書き出し先)フォルダを指定します。「参照」ボタンをクリックしてください。再びフォルダの選択ダイアログが表示されます。


(8)プログラムは、(5)で指定されたScanDataフォルダ内のフォルダと同じ名前のフォルダをProcDataフォルダ内に自動的に作成します。フォルダをクリックして選択し(下のFolder部分に選択したフォルダ名が表示されたことを確認)、OKをクリックしてください。

【参考】ProcData は、Processed(加工済み)の意味です。短くしすぎカモですが・・・


(9)ProcDataに続くPathを確認し、「変換実行」ボタンをクリックします。

〇:ProcData¥(自動的に作成されたスキャンした画像を保存したフォルダと同名のフォルダ)

×:ProcData¥AAA¥BBB¥CCC


(10)次のメッセージが表示されます。MS_Readerは、筆者が別に作成した手書き答案の採点プログラムと併用して使用することもできますが、今回の処理ではそのような形では運用しませんので「いいえ」ボタンをクリックしてください。


(11)専用画像の作成が完了すると、次のメッセージが表示されます。OKをクリックしてください。


(12)「終了」ボタンをクリックして、変換作業を終了します(画面下・中央の「画面の初期化」ボタンをクリックすれば、続けて他のクラス/講座の画像を同様に処理することも可能・・・なようにプログラミングしたのですが、「画面の初期化」ボタンのクリックでは「初期化されない何か」が残ってしまうバグが発現することがあるようです。意図した通りに変換されない状況を1度、経験しました)。なので、他のクラス/講座の画像を処理する際は、一旦終了してから再度このプロセスを呼び出していただいた方がよろしいかと思います。

ド素人が書いたプログラムであります。内在する不具合につきまして、もし、それが発現するようなことがありましたならば、ただ、ただ、伏してお詫び申し上げます。不具合がありましたら、一旦「終了」をクリックして、再度、画像変換処理を呼び出していただけますよう、お願い申し上げます。

変換後のファイル名はProcDataフォルダ内に作成した変換先フォルダの名称+01から始まる連番となります。

5.テンプレートとして登録

筆者の作成したマークシートリーダーでは、マークの読み取りに際し、まず特徴点(マークシート画像内のトリプルドット:■■■ )をコンピュータの眼である OpenCV を用いて探し出し、三つの ■ のうち最も左の ■ の左上隅を座標原点(0,0)として、ここからの距離情報を利用してマークシートの第1列を画像として切り出し、二値化・色の反転処理を行って、さらにそこから1行ずつ「行の画像」を切り出し、この「行の画像」を選択肢数個に切り分けて白面積を計算、それが大きい場合に「マークあり」と判定しています。

このため最初にマークシートの特徴点の位置と各列の左上隅及び右下隅の位置を座標として登録し、これをそのマークシート形式の定型フォーマット(=テンプレート)として利用できるように名前を付けて登録する処理を行います。

テンプレートの名前の意味は、次の通りです。

例:R25C04D19

1列あたりの行数・全列数・選択肢の形式と選択肢数を「行・列・選択肢」順に並べています。
R は Row (=行)、すなわち1列 25 行より成ること、
C は Column (=列)、すなわち4列あること、
D は Double 型、すなわち複数マーク対応で、1行あたりの選択肢数は 19 個。
(ここが S の場合は Single 型、複数マーク不可)

Word や Excel で作成したマークシートを、同じインクジェットプリンタで印刷して使用しているので、試験を実施する度にテンプレートを登録する必要はないはずなのですが、筆者はなんとなく不安で、毎回新しくテンプレートを登録し直して作業しています・・・

テンプレートの登録方法は、次の通りです。

(1)「2 テンプレート」をクリックして表示されるサブメニューから「テンプレートの新規登録」をクリックして選択します。


(2)別のWindowが開き、次の画面が表示されます。画面右上の「取得」ボタンをクリックします。


(3)ファイル選択ダイアログが表示されます。任意のマークシート画像を1枚選んでクリックして選択してください。下のファイル名欄にクリックした画像の名称が表示されていることを確認して、「開く」ボタンをクリックします。


(4)選択したマークシートが表示されます。画面右上の「マーカー」オプションボタンをクリックしてください。


(5)画像左上の特徴点部分が3倍の大きさで拡大表示されます。特徴点画像の左上位置をポイント(マウスのカーソル:+の中心を合わせる)してクリックしたらそのまま指を離さずに特徴点画像の右下へドラッグしてください。ドラッグ中は黒い太い枠線(=ラバーバンド)が表示されます。

赤枠の中、ラバーバンドの様子がよくわかるように、特徴点画像より少し大きめにドラッグしています。


実際は、次の図のように、ラバーバンドが特徴点画像の外側をぴったり包む(両者の幅と高さが同じになる)ようにドラッグします。


(6)ドラッグ終了時に矩形選択された部分が特徴点画像としてコピーされ、下の図のように表示されます。照合手法は自動的に設定されますので、変更しないでください。続けてコンピュータの眼である OpenCV がマークシート画像内の特徴点を見つけることができるか、どうかのテストを実行します。

「マーカー画像の読み取りテスト」ボタンをクリックしてください。


(7)OpenCVが発見した特徴点画像の位置が赤い矩形で表示されます。特徴点画像を完全に一致していることを確認し、表示されるメッセージを読んでOKをクリックしてください。


(8)選択対象グループの「解答欄」オプションボタンをクリックすると、案内バルーンが表示されます。マークシートの1列あたりの行数と、マークシート全体の列数、1行あたりの選択肢数をコンボボックスの選択肢から選んで順に設定します。


(9)次の図は、複数選択可能なマークシートの設定例で、行数・列数・選択肢数をそれぞれ入力した直後の状態です。複数マークを許可するか、どうかの設定を行います。

【重要】
複数マークを許可する場合は「複数マークによる採点を実施」チェックボックスをクリックして、チェックが入った状態 にしてください!!

GUIのデザインが悪いためか、作った本人でも! この設定の操作を時々忘れます。複数選択可能なマークシートのテンプレート設定を行う場合は、くれぐれも注意してください。


(10)マークシートの1列目から順に、その座標を取得します。1列ラベルの左のオプションボタンをクリックしてください。マウスカーソルの形状が+になります。

次の図に示したように、まずマークシートのマーク欄枠の左上をクリックし、そのまま指を離さずに、第1列めの右下隅へ向かってドラッグしてください。なお、ドラッグ中は、細い点線のラバーバンド矩形が表示されます。

設問番号欄を含めないようにご注意願います。
必要な座標は、マークシート欄の座標です!


列の右下隅までドラッグした状態を示します。


ドラッグを終了する(マウスの左ボタンから指を離す)と、選択範囲が赤の矩形で囲まれます。取得できた第1列目の座標が画面右のラベルに表示されます。

第1列めの範囲を指定し、座標を取得したところ

【重要】
この作業にマーク読み取りの成否がかかっています!
くれぐれも慎重に、正確に、作業してください。

うまく列を選択できなかった場合は、「再範囲選択」ボタンをクリックして作業をやり直すことができます。


(11)以降、4列目まで同じように作業します。4列目の座標を取得できたら、「保存」ボタンをクリックして取得した座標を ini ファイルに保存します。

保存処理が完了するとメッセージが表示されます


(12)最後に「終了」ボタンをクリックして、テンプレート作成の画面を閉じます。

ボタンは画面右下隅にあります。


これでマークシートを読む準備ができました!

6.マーク読み取りを実行

(1)最初に使用するテンプレートを選択します。

「2 テンプレート」をクリックするとサブメニューが表示されます。
「テンプレートの選択」を選んでください。


(2)テンプレートの選択画面が開きます。使用したいテンプレート名をクリックして反転表示させ、「決定」ボタンをクリックしてください。


【参考】テンプレートの削除方法
必要のなくなったテンプレート名をクリックして選択、「テンプレートの削除」チェックボックスをチェック、「実行」ボタンをクリックすれば不要なテンプレートを削除できます。削除したテンプレートを元に戻すことはできません。テンプレートを削除する際は、その要不要に十分ご注意ください。

(3)次のメッセージが表示されます。これはメニューの「3 作業フォルダ」をクリックすると表示されるサブメニューの「作業フォルダの選択」をクリックした際に表示されるメッセージと同じものです。「はい」ボタンをクリックしてください。


(4)フォルダの選択ダイアログが表示されます。ProcData フォルダ内の読み取りたいマークシート画像のあるフォルダをクリックして選択してください。下のFoleder部分に選択したフォルダ名が表示されたことを確認して、「OK」ボタンをクリックしてください。

選択するのは「ファイル」ではなく、「フォルダ」です。


(5)テンプレート名と関連付けて保存されている特徴点画像が見つかった場合は、それを赤の矩形で囲んで表示します。また、特徴点画像からの距離座標を用いてマークシート欄第1列の1行目がどこにあるのかを計算し、その位置をこちらも赤い矩形で囲って表示します。

【参考】PCによっては、ここで Python Engine の初期化に時間がかかることがあります!

次の図のように表示されれば、マークの読み取り準備は完了です。

諸設定が意図した通りに反映され、OpenCVが正しく動作していることを確認したら、
「OK」ボタンをクリックしてください。


(6)マークの読み取りを実行します。案内バルーンが表示されますので、その下にある「読む」ボタンをクリックしてください。


Python4Delphi が使用できる環境(組み込みPython環境を入れた Python39-32 フォルダが MS_Reader.exe と同じフォルダにある場合)ならば、P4D チェックボックスに自動的にチェックが入り、Python 用のOpenCVを用いてプログラムは動作します。


Python4Delphi が利用できない場合、プログラムはDelphi用のOpenCVを利用して動作します。

画面下に表示されている Grid コントロールにすべてのマークシート画像の読み取り結果が表示されたら、マークの読み取りは完了です。通常の動作モードでは、マークの読み取り完了を知らせるメッセージは表示されません。

複数選択可能なマークシートの場合、空欄や3個以上マークされている等、
読み取り結果に何らかの問題がある場合は「999」と表示されます。

【重要】二値化閾値の修正方法(20240707追加)

マークが「うすい」場合、これを正しく読み取ることができず、「空欄」と判定して「999」と表示される場合があります。同じ理由で、複数マークされた解答欄の「1の位」が読めなかった場合も、読み取り判定は「マークの状態に問題あり」となり、「999」と表示されます。

これらの場合は、この後、実行する「読み取り結果のチェック」時に、該当箇所の解答欄が赤枠で囲まれて表示されますので、マークの状態をヒトの眼で確認し、読み取り結果を修正できます。

最も困るのが、複数マークされた解答欄の「10の位」のマークは薄くて読めなかったが、「1の位」のマークの読み取りには成功している場合です。この場合は正しく読み取れた「1の位」のマークが読み取り結果として表示されてしまいます。大変申し訳ないのですが、MS_Reader.exe のチェックプログラムは、この誤読を見つけることができません!!(これは原理的な問題なので、チェックする方法がありません)

この事故を防ぐには、事前に読み取りテストを十分に行って、読み取りパラメータを調整するしか方法がありません。具体的には、最もマークの濃度が薄い受験者のマークシートが正しく読めるようになるまで、閾値の値を1ずつ大きくして読み取りテストを実行します。筆者の行ったテストでは、デフォルト設定の閾値(180)では正しく読めなかったマークも、閾値を大きくすれば読めるようになりました。

「塗りつぶす」方式でなく、より簡易な「線を引く」方式でマークするシートを用いて行った読み取りテストの例

「64」と読むべき箇所の「10の位」を「空欄」と判定、「1の位」は正しく読めた場合、プログラムは読み取り結果を「4」と表示してしまう。


閾値を「1」大きくすると・・・

正しく読めるようになりました。


ごく薄いマークも正しく読めるようになるまで、この操作をくり返します。筆者の行ったテストでは、閾値を1ずつ大きくすることで、指示通り普通の濃さ(マーク箇所の数字が読めなくなる濃さ)でマークされたシートであれば、最終的にすべてのマークを正しく読めるようになりました(上記のマークシートを使用して行った実験では、最終的にすべてのマークを正しく読めた段階で、二値化の閾値の値は「184」でした。なお、このマークシートは後日公開する予定です)。

ただ、あまりにもマークの濃さが薄い場合は「原理的に読めません」ので、「対応不可」としてヒトの眼で読むしかないと思います・・・。

そのような事態にならないよう、予め受験者に対し「薄いマークは読み取れない」旨の注意を徹底しておく必要があります。マークシートを用いた試験では、これが最も重要なことかもしれません。


【参考】Grid コントロールの高さを変更する方法

Grid コントロールの高さは自由に変更できます。マークシート画像と Grid コントロールの境界部分にマウスカーソルを持って行く(ポイントする)と、=の上と下に上下向きの矢印のついたポインタ形状に変化する場所があります。この部分を上下にドラッグすることで、Grid コントロールの高さを変更することができます。

Grid コントロールの高さを1行分にしたところ

7.マーク読み取り結果のチェック方法

(1)マークの読み取りが完了したら、結果をチェックします。設問数が100である場合はそのまま「Check!」ボタンをクリックしてください。

Check! ボタンをクリックして、読み取り結果を確認します。


設問数が100 未満の場合、空欄その他の判定フラグである「999」が多数入力されていますので、チェック時にそれらのチェックを省略する設定を行います。

例えば、設問数が「80」である場合、4列目の設問「81」以降の「999」はチェックの必要性がありませんから、Skip チェックボックスにチェックして、4列目のコンボボックスに選択肢から「81」を選択します。「覚」ボタンをクリックすると、この設定を記憶します(他のクラス/講座のマークシートを読み取る場合は記憶させてください)。この設定を行ってから、Check! ボタンをクリックして読み取り結果のチェックを実行してください。


(2)読み取り結果に問題があると判定された場合は、次のように「問題あり」と判定された箇所が赤い矩形で囲まれて表示されます。(この場合は、マークが横に長すぎて隣のマークの領域に入ってしまっているのだと思います)

実際の画面では、14 設問目の「999」は青く反転表示されています。
(画面をキャプチャーした際、青の反転表示が消えてしまいました)


確認を行った結果、マークが「15」であり、正解と見なせる場合は Grid コントロールの「999」を消して「15」と入力し、読み取り結果を修正することができます。

読み取り結果を「15」に変更したところ


ちなみにこのような場合、「P4Dを使用」のチェックを外し、判定領域をマーク画像の中心付近のみに設定して読み直せば正しく読めるようになります。(ただし、動作速度は遅くなります)


読み取りパラメータを上のように変更して再度マーク読み取りを実行した場合、

「999」表示は消え、さっきは読めなかったマークを正しく読むことができました。


(3)再度「Check!」ボタンをクリックして、チェックを続行します。

10の位に二つマークがある場合等、不正解と見なせる場合は、「999」を修正せず、チェックを続行します。
続けて「Check!」をクリックしてください。


次のメッセージが表示されたらチェックは完了です。

8.CSV形式で出力

マークの読み取り結果の確認作業が完了したら、Grid コントロールに表示されているデータをCSV形式でファイルに出力できます。このCSVファイルは筆者作の採点結果通知作成プログラムから読み込んで採点結果通知票の作成に利用できます。Excel Book にもこのデータを書き出すことができますが、複数マーク対応の採点結果通知を作成できる Excel のワークシートは作成しておりませんので、ここではその処理方法の詳細は説明しません。

CSV形式で出力する方法は、次の通りです。

画面右下の「ファイルへ出力」グループの CSV オプションボタンをクリックして選択し、「書き出し」ボタンをクリックします。


書込みが完了すると、次のメッセージが表示されます。


表計算ソフトその他を用い、ご自身で採点結果を処理される場合は、出力先をメモしてください。

出力されたCSVの内容は、次の通りです。

1行目は「設問番号」、A列が「マークシート番号(=出席番号)」です。

9.採点結果通知シートの作成方法

採点結果通知の作成方法は、当Blogの過去記事をご参照下さい。

10.お願いとお断り

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

100選択肢に対応したマークシートリーダー

追記(20240929)

当Blogで紹介してきた自作のデジタル採点プログラムを一つにまとめました。次のリンク先にその紹介とダウンロードリンクがあります。マークシートも、ここに紹介した形式の他、様々なタイプのものを同梱しています。

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

先日、電車にゆられていたら先輩が。

「選択肢がたくさんあると、マークシート使うの、難しいかなー?」って。

なんかおもしろいこと、ないかなー☆って、毎日、ひまなんだもん。

すぐに出来そうな気がしたので、さっそく大語群に対応したマークシートリーダー作成にチャレンジ。

今までのは数学や教科「情報」の試験用に作成した16選択肢が最大だったが。

数学の試験用に作成したマークシート(マーク部分は-記号)
マークの色が濃いのは、開発初期のマークシートの画像であるため。
教科「情報」の試験用に作成したマークシート(選択肢の番号はゼロ始まり)
経験を積む中で、誤判定を防止するため、マークの色はどんどん薄くなった。

今回、作成したマークシート(最終的なかたち・Excel で作成)。

大語群に対応したマークシート(選択肢の番号は0ー99、合計100)、遂に完成!

正直、思ったほど、かんたんではありませんでした!!

【もくじ】

1.最初に作ったのはB4縦型のマークシート
2.次に作ったのはA4横型のマークシート
3.Excel でマークシート作成に挑戦
4.マークシートを最適化
5.読み取りプログラムも修正
6.発見した問題点と解決策
7.まとめにならないまとめ
8.プログラムのダウンロード
9.お願いとお断り

1.最初に作ったのはB4縦型のマークシート

選択肢の数が多いことを、ここでは『大語群』と呼ぶことにする。この大語群に対応したマークシートを作るにあたり、最初に決めておくべきことはもちろん選択肢の最大数。30個もあれば十分な気もしたが、「大きいことはいいことだ!」とも言うし、どうせ作るなら100個まで対応できるようにしようと決心。

数年前にマークシートリーダーを作ったとき、選択肢数50個に対応した複数マーク読み取り可能なプログラムを書いた記憶があり、10の位と1の位を分けてマークする次のような形式のマークシートがすぐに思い浮かぶ。1行あたり、2個までのマークを読み取れるようにコードを修正すれば、このマークシートで選択肢の番号を0-99として、計100個の大語群を使った試験にも対応できるはずだ。

10の位のマーク欄が空欄なら、プログラムは1の位のマークのみを読むよう設定


このイメージを実現するにあたり、差し当たって問題になるのはマークシート用紙の基本サイズ。さすがにA3サイズの用紙は、マークシートとして使うには巨大すぎる気がする・・・が、1行あたり10の位のマークに①~⑨で9個、1の位のマークに⓪~⑨で10個、合計19個のマークを用意して、得点設定は1設問1点で合計100点とするためには、当然100設問分の行を用意しなければならない。

1列50行で2列作成するとなると・・・、やっぱり、B4版で、縦置きか?

これまでのマークシートは Word で作成していたので、今回も Word を利用。・・・と言うか、本当は印刷設定の自由度が大きい Excel を使いたいのだが、Excel で縦楕円の丸囲み数字を上手に作成する方法がわからない。そこで縦楕円の丸囲み数字が簡単に作成できる Word を利用した・・・というのが正直なところ。

ちなみに Word で縦楕円の丸囲み数字(=「囲い文字」というらしい)を作成する方法は・・・

Word なら、Font は「メイリオ」を選択(フォントサイズを大きくしない場合)、丸囲みしたい数字を半角で入力、入力した数字をマウスでドラッグして選択してから、フォントリボンの「囲い文字」アイコンをクリックすると・・・

赤い枠で囲んだのが「囲い文字」を作成するアイコン。
数字を入力して、ドラッグして選択したのち、これをクリック。


ダイアログが表示されるので、スタイルを設定して・・・

スタイルは「文字のサイズを合わせる」を選択。


さらに数字の選択状態は解除しないまま、段落リボンの「拡張書式」をクリックして、表示されるサブメニューのいちばん下にある「文字の拡大/縮小」をクリックして、さらに表示されるサブメニューの「66%」をクリックすれば・・・

マークシートのマーク領域(縦楕円の囲い文字)を作成


思った通りの囲い文字が完成!


あとはマークシート用途に利用できるよう、色の設定を薄めに変更する等して、必要な選択肢の数だけこれを作成すればいいんだけど・・・

これを Excel で実現する方法がわからない・・・


そのような理由から、とりあえず Word で作成してみたB4版・縦置き型のマークシート。
思ったより巨大で、マークするのがたいへんな気が。

マークするだけで疲れた・・・


読み取り実験用に設問番号1から順に、読み取りデータが設問番号と同じになるようマークしてみる。
100個目は1の位の「0:ゼロ」をマーク。これで1から99と0(ゼロ)で、合計100の選択肢が使える大語群対応型マークシートが完成・・・したと思ったんだけど。

複数マークの読み取りを可能にするため、リーダーのプログラムを少し変更。

Delphiを起動して、マークシートリーダーのプロジェクトファイル一式をコピーして、新たな複数マークの読み取りに対応したプロジェクトを作成。

マーク読み取り手続き部分のコードを次のように変更。最初に手直ししたのは、P4Dを使ったスクリプト部分。

      //複数マークの読み取り方法
      if (Copy(strMS_Type,10,2)='19') and (chk_MultipleMarks.Checked) then
      begin
        //選択肢数が19で、複数マーク許可であった場合
        StrList.Add('                var1.Value = str(res)');
      end else begin
        //複数マークは不許可であった場合
        StrList.Add('                var1.Value = "99"');
      end;

Python側で読み取った値をDelphi側で処理する部分も変更(一部を抜粋)。

//選択肢の始まりは「ゼロ」
  if (Copy(strMS_Type,10,2)='19') and (chk_MultipleMarks.Checked) then
  begin
    //複数マークに対応
    //strAnsList[intSG_k]の文字数を調査
    strCount:=ElementToCharLen(strAnsList[intSG_k],Length(strAnsList[intSG_k]));

    //チェック内容は、以下の通り
    {
    文字数が2文字の場合、末尾の1文字を取得する
    10 -> 0
    11 -> 1
    19 -> 9
    末尾1文字がマークした選択肢の番号になる

    文字数が5文字の場合、
     1 10 -> 2文字目が1、末尾2文字が10 -> 10
     2 11 -> 2文字目が2、末尾2文字が11 -> 21
     3 12 -> 2文字目が3、末尾2文字が12 -> 32
    (2文字目×10)+(末尾2文字 - 10)がマークした選択肢の番号になる
    }

    case strCount of
      2:begin
        //2文字の場合は、末尾1文字が選択した選択肢の番号
        StringGrid1.Cells[intSG_Col,intSG_Row]:=RightStr(strAnsList[intSG_k],1);
      end;
      3:begin
        //空欄と判定された場合
        if strAnsList[intSG_k]='999' then
        begin
          StringGrid1.Cells[intSG_Col,intSG_Row]:=strAnsList[intSG_k];
        end;
      end;
      5:begin
        //(2文字目×10)+(末尾2文字 - 10)がマークした選択肢の番号
        StringGrid1.Cells[intSG_Col,intSG_Row]:=IntToStr(
          (StrToInt(Copy(strAnsList[intSG_k],2,1)) * 10) +
          (StrToInt(RightStr(strAnsList[intSG_k],2))) - 10);
      end;
    end;
  end else begin

次に、P4Dを使用しないDelphi用のOpenCVを利用したマーク読み取り部分のコードも変更(一部を抜粋)。

  //1行につき選択肢数分Loopする_複数選択肢に対応(New)_20240614
  if (Copy(strMS_Type,10,2)='19') and (chk_MultipleMarks.Checked) then
  begin
    //複数選択可能な場合_選択肢の数だけLoopする
    for p := 0 to intCol-1 do
    begin
      //対象値pが平均値の3倍より大きいか、どうかでマークありと判定
      if AryVal[p]>dblAvg * intKeisu then
      begin
        //マークありとした判定の数を記録
        q:=q+1;
        //マークした番号(記号)を記録
        //intMark:=p+1;
        //10の位(0-8)
        case p of
          0:strMark_A:='1';
          1:strMark_A:='2';
          2:strMark_A:='3';
          3:strMark_A:='4';
          4:strMark_A:='5';
          5:strMark_A:='6';
          6:strMark_A:='7';
          7:strMark_A:='8';
          8:strMark_A:='9';
        end;
        //1の位
        case p of
           9:strMark_B:='0';
          10:strMark_B:='1';
          11:strMark_B:='2';
          12:strMark_B:='3';
          13:strMark_B:='4';
          14:strMark_B:='5';
          15:strMark_B:='6';
          16:strMark_B:='7';
          17:strMark_B:='8';
          18:strMark_B:='9';
        end;
      end;
    end;
    //Loop終了時にマーク数を判定
    if q=0 then
    begin
      //マークした番号がない場合
      iArr[i,Rep]:=999;
    end else begin
      //マークした番号があり、それが一の位である場合
      if (q=1) and (strMark_A='') then
      begin
        //マーク数が1、かつ十の位が空欄であったら
        iArr[i,Rep]:=StrToInt(strMark_B);
      end else begin
        //マーク数は1だが、それが十の位であったら
        iArr[i,Rep]:=100;
      end;
      if (q=2) and (strMark_A<>'') and (strMark_B<>'') then
      begin
        //マーク数が2、かつ十の位と一の位がともに空欄でなかったら
        strMark:=strMark_A+strMark_B;
        iArr[i,Rep]:=StrToInt(strMark);
      end;
      if q>2 then
      begin
        //トリプル以上のマーク数を見分けるフラグは100
        iArr[i,Rep]:=100;
      end;
    end;
  end else begin


Delphiでマークシートリーダーを作成する方法の基本は過去記事をご参照ください。

マークの読み取りそのものは「絶対成功する」自信があった(?)ので、複数マークの読み取り処理を既存のプログラムに追加すれば、速度的なことも含めて楽勝でプログラムは完成するはず・・・だったんだけれど。

実際に上のB4版・縦置き型マークシートをスキャンして読み取りテストを行ってみると・・・

P4D利用時の読み取り結果は期待した通り、100 %正確にマークの読み取りに成功するが、P4Dを利用しない場合に不具合が発生。50設問目は正しくは「2」と読み取らなければならないはずなのに、読み取り結果の表示には、なぜかトリプルマークの判定結果である「100」が表示されている。

※ この時点では、必要数以上にマークがあった場合の表示フラグとして「100」を使用していた。
※ 最終的に、読み取れない解答欄は全て空欄の表示フラグ「999」で示すようプログラムを修正した。

「100」は3つ以上のマークがあった場合に表示されるはずなんだけど?


さらに、よく見てみると 100 設問目もヘン。91、92、・・・、97、98、99 と順調に読み取って、最後は「 0:ゼロ」とくるはず!なのに、読み取り結果はトリプルマーク以上の判定結果である「100」がここにも登場。ヒトならともかく、機械が勢い余るはずもなく、誤認識の原因はまったくもって不明。

どうみてもマークしたのは「0:ゼロ」なんだけど。


今までさんざんテストして、読み取りパラメータ設定も変更の必要が「ない」ところまで煮詰めたと思っていたのに、この結果には唖然とするしかなく、悪夢を見ているのではないかと思ったが。

現実は現実。

変更したコードを見直してみるが、おかしなところは見当たらない(ように思う)。
実際、大多数のマークは「ちゃんと読み取ってる」し・・・

なんで、部分的に読めない箇所があるのか???

読めないなら読めないで、「全部」間違うのが機械だと思うんだけど。

仕方がないから、パラメータ設定をいじってみるが・・・

これがデフォルトのパラメータ設定。
(自分的には、変更の必要がないと思えるところまで、さんざん修正を繰り返して決めた値)


パラメータをどう設定しても、一部のデータを誤って読んでしまう・・・。

例えば、閾値を「200」、判定領域を「20」に変更した場合、

47、48、49ときて、次は2のはずなのになぜか「42」
機械のクセに、勢い余ってるとしか思えない・・・。

こんなプログラムは使えない!!

2.次に作ったのはA4横型のマークシート

誰も助けてくれる人なんていないから、問題は自分で解決するしかない。まぁ、問題そのものを自分で作り出しているとも言えるわけで、自業自得と言えばそれまでのこと。自分以外の誰も困ってないし、それを幸いにあきらめてしまうのがいちばん簡単なことだが、それは最終手段。

原因はわからないが、今まで起きたことのないことが起きている、つまり、今までと違うことをしてるから、そこに問題の発生する原因そのものがあるはずだ。何が違うのか、そこを考えてみる。

今までと違うのは、まず、マークシートのサイズそのもの。B4版なんて使ったことがない。もしかしてそれが原因か? 判定プログラムでは行を図として切り出して、さらにマーク1つずつに分解し、二値化して「白」面積が大きいものを「マークあり」と判定しているから、二値化の閾値の設定にもよるが面積的な部分にも誤認識の原因があるような気もしてきた。そうでなくても、実際に使ってみて、やはりB4サイズは「マークシートとして大きすぎる」気がしたのは、ほんとう・・・。

もし、マークの読み取りプログラムそのものに誤りがあるなら、全てのマークを正しく読めないはずだが、ほとんど正しく読めているから、読み取りプログラムそのものに致命的な問題はない・・・はず。

そこで、これまでに正しく読み取れたものとサイズ的に同じ「A4版・横置き型」のマークシートを作成して実験してみることにする。

そう思って作成したのがこちらのマークシート。縦置きにしなかったのは、「今までと同じにする」という部分にあくまでもこだわった結果。

かなり無理して「詰め込んだ」感、満載。


さすがにA4版・横置き型で1列50行のマークシートは(自分の技術では)Word で作成できず、作成にあたっては(念願の?)Excel を使用。Excel で作成した際の画面はこんな感じ。

縦に長い楕円の囲い文字の作り方がわからず、仕方がないから丸囲みの番号でマークを作成
どこか、なにかが「チープな感じ」で、出来栄えもいまいち。


これで実験すると・・・、P4Dを使った場合からして

まったく読めてない!


高速読み取り処理が可能なP4D環境で正しく動作しなかった時点で(これはダメだ)と思ったが、とりあえず非P4Dモードで動かしてみると・・・

B4版使用時より、さらに悪い結果に。


B4版使用時は「唖然」とする思いであったが、今度は「暗澹たる」思いが。

この結果を目の当たりにしたときは、驚愕のあまり、言葉を失い、ついでにやる気もほぼ全部失い、PCの蓋を閉じて(この表現でいいのか?)、火酒を求めてバイクで現実から逃走・・・

あの土曜日の夜は、まじで、つらかった。

ひー(こころの声)

今、冷静になって考えると、P4Dモードでほとんど読み取れてないのはおそらくマークシートの罫線に問題があり(太すぎ)、これが複数マークの判定につながったのではないかと思えてならないが、最初からマークシートの作り自体が気に入らなかったこともあり、自分史的には・・・この実験自体を「なかった」ことにして、心のバランスを保つことに決定。

ただ、まだ「あきらめる」という気持ちには到底なれないので、このピンチをまたとないチャンスと前向きに捉え、Excel で縦長楕円の丸囲み文字を作成するところからチャレンジを再開。

あきらめられない以上、自分も、プログラムも良くなるしか「ない」。
それが嫌なら、はじめからこんなこと、しないほうがイイ。

このチャレンジは、僕にある唯一の「自由」なんだ。
暗澹たる思いなんかで、終わりにはしたくない。

3.Excel でマークシート作成に挑戦

Excel を起動し、何も入力されていない白い画面をじっと見つめて考える。

(縦長の楕円で、囲い文字をつくるには・・・)

経験から唯一思いつく方法は、図形(楕円)をセル内に収まるように挿入して右クリック、テキストの編集を選択して、中に数字を入力する方法だ。楕円の挿入方法は次の通り。

挿入タブをクリック ⇨ 図のリボンにある図形から楕円を挿入


まずは、準備作業。

(楕円を挿入するより先に)あとあと作業しやすいよう、画面右下の「ズーム」で画面表示を拡大(238%くらいにした)して、全セルを選択(下図を参照)。で、列幅を28ピクセル、行の高さを32ピクセルくらいに設定。

全セルを選択し、列幅と行の高さを変更する


B2のセルに収まるよう、楕円を挿入(ズーム300%)。

楕円を挿入


挿入した楕円を右クリックして、表示されたサブメニューから「テキストの編集」を選択(左クリック)。

図形の中にテキストを挿入


半角で1と入れてみた。

数字は入ったが、位置がよくない・・・


ホームタブをクリックして、配置リボンにある「上下中央揃え」と、その下の「中央揃え」をクリック。

数字の位置はいい感じになった。


あとはマークシートのマークらしくするため、楕円をクリックして選択すると表示される図形の書式タブをクリックし、図形のスタイルリボンのコマンドを使い、楕円の中を白くして、囲いを灰色に設定。

マークシートのマークらしくする


フォントの色も灰色に変更。

数字の色も灰色にする


で、セル内での図形オブジェクトの位置を微調整。

セル内で中央に揃うよう、楕円を選択して左右の矢印キーで位置を微調整


できた!

スキャナーで実際にスキャンしてみた結果から言うと、
実際に使用するマークシートではもう1段階濃い灰色を選んだほうがよさそう


あとは、コレを等間隔で上下の位置もそろえて並べれば・・・いい・・・んだが、その方法がわからない。

藁にも縋る思いで、Google先生に訊ねると・・・

単に「楕円を挿入したセルをクリックして選択し、オートフィルの機能を使って右方向へコピー」するだけ! だよって。

半信半疑で、やってみた。

楕円ではなく、セルを選択


表示された緑の枠の右下隅にあるハンドルをクリックして選択して、そのまま右へドラッグ。
すると・・・

祈るような気持ちとは、まさにこのことか・・・


マウスのボタンを離すと、表示されたのは・・・

思った通りにコピーできた!


これだ。これ!
これを待っていたんだ。

Google先生、ありがとう!

あとは数字を変更すれば・・・

マークシートのマークができた!


こうして得た知識をベースに、これまでの経験を加えて Excel で作成したマークシートがこちら(枠の線の設定状態がよくわかるように、枠線の表示はONに設定した状態)

マークを塗りつぶす際に、上下左右のマークに影響が及ばないだけの間隔を確保


ページレイアウトタブの配置リボンの「配置」をクリックして、「枠線の表示」をオフに設定。

「枠線の表示」をクリックする度に、表示のONとOFFが切り替わる


先の実験では、罫線の太さで痛い思いをした(?)ので、再びその轍を踏まないよう、罫線はいちばん細いものを選び、色もオレンジに設定。こうしておけば二値化する際に、罫線は完全に消えるはずだ。

注意:この時点では、そう、考えて罫線の色を設定しましたが、以下で述べる通り、いちばん細い罫線に対する色の設定は、印刷時に無効になり、印刷色は必ず黒になります!

確か、いちばん細い罫線はこれだったはず・・・


で、罫線の色も文字の色(=マークの色)と同じ薄い灰色に設定して試しに印刷してみると、なにか違和感を感じる印刷物がプリンターから排出された。よく見ると罫線の色が濃い! 指定した灰色でなく、普通の黒のような気がしてならない。他の灰色を選んでも、印刷すると罫線の色は「まったく変わっていない」ように見える。もしかして、色の濃さの設定が反映されていない?

(オレンジ色にしてみるか?)

早速、設定 ⇨ 印刷を実行して確認 ⇨ 結果は「黒いまま!」

再び、Google先生にお伺いをたてて知った驚愕の事実。

「Excel はその仕様のため、いちばん細い罫線は印刷時に必ず黒で印刷されてしまう」とのこと。

まじですか? まったく知りませんでした!!

あわてて2番目に細い罫線に変更。色はオレンジ色を指定(この色がなぜか、すーぱー気に入った)。

そうこうして、ようやく、これなら大丈夫と思えるマークシートが完成。

上記の方法で枠線の表示はOFFに設定した状態の画面。
自分的には、満足できる出来栄え。

4.マークシートを最適化

完成したマークシートの全体のイメージはこんな感じ。

1列あたり25行×4列、1行について19選択肢(10の位:1-9、1の位:0-9)、A4版・横置き、
最大100設問に対応


ちょっとマークが小さいような気もしたが、数学・情報用に作成した1行あたり16選択肢のマークシート同等に、隣り合うマークどうしの間隔もあけることが出来た気がする。これが近すぎると乱暴にマークされた場合、「複数マークあり」と判定してしまう危険性が高まってしまう。

また、1列あたり25行の設定としたことで、上下のマークの間隔も十分広くなった。B4版・縦置きの1列50行やA4版・横置きの1列50行よりも遥かに圧迫感は減少している気がする。

でも、試しに100設問分ぬってみたら、やっぱり、疲れた・・・。100設問分マークするってことは、その約2倍マークしなければならないから、疲れて当然と言えば、当然。

これをスキャンして、最終動作確認。


上のマークシートを、実際にスキャナーでスキャンしてJpeg画像に変換し、マークの読み取り処理を行ってみた。結果はPython環境を使っても、使わなくても、読み取り成功率は100%、ようやく期待通りに動くようになってきた。でも、途中、マークシート作成作業での失敗がなければ、Excel を使って(縦長楕円の囲い文字で)マークシートを作る技術は習得できなかった。「失敗は成功のもと」というけれど、今回あらためて諺の重みを実感。

次はマークシートの印刷の濃度の調整(最終仕上げ)。・・・と言うのも、実はマークの読み取り結果をExcel で処理して採点結果の通知シートを作ってる時は全く問題にならなかったことが、読み取ったマークシート画像に直接 〇 や × 、個々の設問の得点や配点、合計点等を入力して返却するように処理系全体を改良したら、これまで思っても見なかったことが重大な問題となってきたのだ。

次の画像を見れば、それは一目瞭然。

マークシートがほとんど見えない!


マークシートの印刷濃度を薄くしすぎると、スキャナーで読み取る際に枠やマークが本当に薄くなってほとんど見えなくなってしまうのだ。単にマークの有無を読み取るだけなら、■■■ からの距離でマークを切り出して二値化し、白面積を計算しているだけだからマーク以外の画像は真っ白でも何の問題もない(むしろ、それくらいの画像の方がより確実にマークを読み取れる)のだが、スキャンした画像そのものを採点結果通知に再利用するとなると、枠やマークがある程度は「見える・読める」ようにスキャンしなければならない。

マークシートのマークや罫線枠の灰色の濃度を少し濃く(下図を参照)して画像をスキャン、どの程度見えるようになったか、確認してみる。

1段階濃い灰色を指定(実際には Ctrl + A でオブジェクトをすべて選択してから設定)


スキャンして画像を表示してみると・・・

マークは読めるようになったが、罫線枠はまだ見えにくい。


罫線の色をオレンジ色に設定したら、それがとても気に入ってしまったのだが、残念ながらスキャンすると罫線枠はほとんど消えて見えなくなってしまう(マークの有無のみを正確に読み取るという意味では、それは実に理想的なのだが)。ただ、マークシート情報の取得プログラムでは、マークひとつひとつを切り出すために罫線の枠の座標を利用しているから、罫線枠の左上隅と右下隅は座標を取得する場面では確実に見えるようにしておきたい。そこで、罫線枠の左上隅と右下隅だけは線の色を灰色にすることにした。(実は、上の画像はそれがほどこしてある画像)

まず、左上隅を設定。

罫線枠の左上隅の「 部分のみ灰色に変更


同様に、右下隅も設定。

罫線枠の右下隅の 」部分のみ灰色に変更


マークシートの情報を取得する際に、罫線枠が十分よく見えることを確認。

すみっこはよく見える!

5.読み取りプログラムも修正

最終的に実用上問題のないプログラムにするため、思いつく様々なパターンで(誤りを含む)マークを作成し、これをプログラムがどのように判定するか、テストしてみた。

テスト用に、次のマークシートを作成。

動作検証用に作成したマークシート
(スキャンしたら、画像中央やや上に横線が入っていた。原因は不明。)


マークシートのスキャンに使っているスキャナーでスキャンすると、時々、黒い線の入ったJpeg画像が生成される。しかも、この黒い線はマークの読み取り判定になぜか?影響を与えない。

上半分だけ塗りつぶしたマークでも、正しく「4」と読んでいる。
複数マークありと判定されないのはなぜ?


この不思議な現象の原因はまったくわからないが、判定に影響を与えないから、これまでは(まぁいいか)としてきたが・・・。

いずれにしろ、このマークシートを使って動作検証を行った結果、先に記した判定プログラムでは対応できない問題が複数あることが判明。検証をくり返し実行して、一つ一つの問題に対応。最終的に完成したのが次のコード。

  //選択肢の始まりは「ゼロ」(1の位を基準)
  if (Copy(strMS_Type,10,2)='19') and (chk_MultipleMarks.Checked) then
  begin
    //strAnsList[intSG_k]の文字数を調査
    strCount:=ElementToCharLen(strAnsList[intSG_k],Length(strAnsList[intSG_k]));

    //チェック内容は、以下の通り
    {
    文字数が2文字の場合、末尾の1文字を取得する
    10 -> 0
    11 -> 1
    19 -> 9
    末尾1文字がマークした選択肢の番号になる

    文字数が5文字の場合、
     1 10 -> 2文字目が1、末尾2文字が10 -> 10
     2 11 -> 2文字目が2、末尾2文字が11 -> 21
     3 12 -> 2文字目が3、末尾2文字が12 -> 32
    (2文字目×10)+(末尾2文字 - 10)がマークした選択肢の番号になる
    }

    case strCount of
      1:begin
        if StrToInt(strAnsList[intSG_k])<10 then
        begin
          StringGrid1.Cells[intSG_Col,intSG_Row]:='100';
        end;
      end;
      2:begin
        //2文字の場合は、末尾1文字が選択した選択肢の番号
        StringGrid1.Cells[intSG_Col,intSG_Row]:=RightStr(strAnsList[intSG_k],1);
      end;
      3:begin
        //空欄と判定された場合
        if strAnsList[intSG_k]='999' then
        begin
          StringGrid1.Cells[intSG_Col,intSG_Row]:=strAnsList[intSG_k];
        end;
        //3文字と判定された場合、十の位の1~9のダブルマークの場合、
        //2文字目は必ず半角の空欄になる
        if Copy(strAnsList[intSG_k],2,1)=' ' then
        begin
          StringGrid1.Cells[intSG_Col,intSG_Row]:='999';
        end;
      end;
      5:begin
        //文字列の置き換え(先頭2文字を抽出&半角スペースを削除する)
        strData:=StringReplace(Copy(strAnsList[intSG_k],1,2),
          ' ', '', [rfReplaceAll, rfIgnoreCase]);
        //Case 5で先頭2文字が10である場合はダブル以上のマークあり
        if StrToInt(strData) > 9 then
        begin
          StringGrid1.Cells[intSG_Col,intSG_Row]:='999';
        end else begin
          //2文字目が半角スペースでなければ処理可能
          if Copy(strAnsList[intSG_k],2,1)=' ' then
          begin
            StringGrid1.Cells[intSG_Col,intSG_Row]:='999';
          end else begin
            //(2文字目×10)+(末尾2文字 - 10)がマークした選択肢の番号
            StringGrid1.Cells[intSG_Col,intSG_Row]:=IntToStr(
              (StrToInt(Copy(strAnsList[intSG_k],2,1)) * 10) +
              (StrToInt(RightStr(strAnsList[intSG_k],2))) - 10);
          end;
        end;
      end;
      6..99:begin
        StringGrid1.Cells[intSG_Col,intSG_Row]:='999';
      end;
    end;
  end else begin

    //複数選択を許可しないマークシートの処理

  end;

end;


ここでいちばん困ったのは、必要以上にマークされていた場合の処理。

複数マークを容認しないプログラムなら、1行について2個以上マークされていた場合は「複数マークあり」を意味するフラグとして「99」、マークなしの場合(=空欄)は「999」というフラグを用意して対応したが、今回のように複数マークを許可し、読み取り結果を 0 – 99 の100分類で表示する場合、空欄すなわち「マークなし」を「999」と表示するのは同じでよいとしても、十の位や一の位のマーク欄それぞれに2つ、ないし、3つ以上マークされていた場合のフラグをどうしたらいいのか? 最適と思われる答えが見つからずにかなり悩んだ。

出来れば、既存かつ(プログラムによっては)数年をかけて動作検証済みの、読み取り結果のチェックプログラムや、採点結果通知のプログラムを修正せずに、それらをこの複数マーク対応採点システムにもそのまま適用できるように、処理の流れを作りたい。

必要数以上のマークがあった場合、当初、選択肢としては決して使うことのない「000」、「100」、「XXX」等をフラグとして利用することも、かなり真剣に考えたが、これらのフラグを新規に採用した場合、これまでに書いてきたマークシートの読み取り結果を記録したCSVファイルを利用して動作するプログラムをことごとく修正しなければならない。そして、それは新しいバグを生むことに、間違いなく直結する。それだけは、どうしても避けたい。

この際、読み取りエラーをすべて「999」で処理すれば、これまでの経験から、読み取り結果のチェックプログラムは確実に「空欄」=「999」位置を教えてくれるし、もし、それが本当に「空欄」である場合は、人が見ればそれは一目瞭然、もし、それが空欄でない場合は、それを見た「人」に、マークの有無 or 空欄 or その他複数マークの判断を委ねればいい。そしてもし、「人」が見て、マークが正しければプログラムの判定結果を正しく修正、そうでなく、マークが「空欄でない」・「必要数以上にマークされていた」場合は、そのまま「空欄として処理(999)」してもらえば、採点結果には一切影響を与えないはずだ。

そう考えて、「トリプル以上のマークあり」をユーザーに伝えるフラグは用意せず、10の位に1つ、1の位に1つ以外のマークがあった場合はすべて同一に「空欄」フラグの「999」で処理することにした。

6.発見した問題点と解決策

上記動作検証用に作成したマークシートで、実際に動作確認を行った結果、ひとつだけ気になった点があった。それは、マークから横にはみ出て(横に広く)マークされると「トリプル以上のマークあり」という判定が出やすいこと。

「12」を読み取れていない。
おそらく1の位の「2」の横棒マークが「1」の領域に侵入している?


マークとマークの間隔を、これ以上広くするのはさすがに困難。1行19選択肢+行番号というマークシートの形式そのものに起因する問題だから、これは試験の問題用紙の表紙に図付きで「横棒型の塗りつぶし禁止」&「なるべく横に広がらないようマークする」注意を載せて、読み取り不能の「999」判定がなるべく出ないようにすることくらいしか、対策を思いつかなかった。

(縦に長い四角形、もしくは縦型の [ ] で数字を囲うことも考えたが、実際には試していない)

Python環境を利用しない場合は、判定領域をマークの中心付近のみに狭めることで読み取り精度を上げることができる。ただし、読み取り速度はかなり遅くなってしまう。

Python環境を使わず、判定領域を70→50に狭くして、実行すれば上の読み取りエラーは解消できる。


マークシートリーダー作成の初期、まだPython環境を利用できなかった頃、少しでも読み取り速度を早くできないかといろいろ考え、二値化後の白面積の計算領域をマークの中心付近のみとすれば、読み取り速度を向上させることができるのではないかと思って作った機能が思わぬところで役に立った!

以上が、解決策とは言えない対策と、読み取り速度を気にしなければ使える解決策。

7.まとめにならないまとめ

(1)実用的ではないかもしれないが、大語群(100選択肢)に対応したマークシートリーダー完成
(2)Excel を使って1行19選択肢で25行4列、A4横置きのマークシートを作成して試験を実施
(3)採点結果通知は付属の ReportCard.exe で作成

ReportCard.exe の使い方は、下のリンク先をご参照ください。

上のリンク先から、デジタル採点プログラム一式をダウンロードできます。
同梱した採点結果通知作成用のプログラム


ただし、選択可能な採点オプションの指定は「採点のみ」or「採点と配点」のみとなります。観点別評価の区分を含めて表示するオプションを選択しても、プログラムは複数マークに対応していないバージョンの流用なので、正解マークの位置を正しく表示できません。少なくても観点別評価の区分は表示できるよう、今後プログラムを改良する予定です。

追記 改良したプログラムに更新しました(20240625)

複数選択可能なマークシートについても、観点別評価の区分や正解マークの位置を表示できるよう、プログラムを更新しました。

採点及び観点別評価の区分と、不正解の設問について正解マークの位置を表示
得点は返却用シートの右下に表示

この大語群専用マークシートを使用した試験の実施方法と、試験後の処理方法について次回の記事で詳細を説明する予定です。よろしければ、そちらの記事もご参照ください。

この記事で作成方法を説明した大語群専用マークシートを利用した試験の実施方法です。


マークを塗りつぶす時間を少しでも短縮できないかと考え、作成したマークシートもあります。

「線を引く」方式でマークする、複数マーク対応型のシートをダウンロードできます。

8.プログラムのダウンロード

大語群に対応したマークシートリーダーは、下のリンク先からダウンロードできます。
なお、バックグラウンドでPython環境を利用し、より高速にマークの読み取り処理を実行するには、別途Python環境の組み込みが必要です。動作に必要なライブラリをインストール済みのPython環境は、当Blogの過去記事へのリンクからダウンロードできます。

Python環境の組み込みはカンタンです。ダウンロードしたZipファイルをダウンロードした後、任意のフォルダに展開、生成されたPython39-32フォルダを、そのまま MS_Reader.exe があるフォルダにコピーするだけです。

こちらの大語群に対応したマークシートリーダーは、当ブログの過去記事に掲載した複数選択不可のマークシートリーダーを、複数選択を前提として設計された専用マークシートのマーク読み取りに特化させたバージョン、いわば派生版です。複数選択不可のマークシートの読み取りにも使用できると思いますがテストは行っておりませんので、ダウンロードしたZipファイルを展開して出来る MS_Reader.exe は複数選択可能なマークシートの読み取り処理専用にお使いください。

この記事で紹介した通り、様々な要因から、マークシートによっては(基本設計が同じであっても)マークを正しく読み取れないことがあります。
また、添付した動作検証済みのマークシートをご利用いただいた場合でも、その印刷方法(輪転機使用等でマークが濃く印刷されていた場合)によっては、マークを正しく読み取れないことがあります。印刷用紙は再生コピー用紙で十分ですが、印刷には 必ずインクジェットプリンタを使用 し、スキャンしたJpeg画像において、マークや罫線枠がうっすらと判別できる程度の濃さで印刷していただく必要があります。

以下、読み取りテスト実行時の環境です。

・A4用紙は、(白くない)再生コピー用紙
・スキャナーは有名メーカー製複合機のスキャナー(カラー/読み取り解像度200 dpi)
・PCはPanasonic製Let’s Note CF-QV
・スキャンした画像をこのプログラム用に変換する際の倍率は80%を指定

「1 画像変換」をクリックすると表示されるサブメニューの「専用画像を作成」をクリック
倍率は80%を指定


上記の環境で、筆者がテストした結果を記事としてここに掲載しました。発見した不具合も正直に書きましたが、筆者が発見していない不具合が他にまだあるかもしれません。ですので、ダウンロードしたマークシートリーダーのご使用はあくまでも自己責任でお願いします。

また、派生版であるため、プログラムには Excel Book に読み取り結果を出力する機能がありますが、大語群に対応した採点結果通知作成用の Excel ファイルは、Zipファイルを展開後、 eFile フォルダ内にあるテンプレートから生成できる Excel ファイルをマクロ有効な Excel Book として保存し、これを元にご自身で作成していただく必要があります。※ Zip ファイルに添付した Excel Book は、大語群マークシートに対応しておりません。

採点結果通知が必要な場合、Zipファイルに同梱した ReportCard.exe をお試しください。こちらは「まとめ」で紹介した通り、動作検証済みです。同梱の ReportCard.exe は選択肢が「ゼロ」から始まる教科「情報」用のマークシートにも対応したものです。

※ お使いのPC環境により、Python Engine の初期化に異様に時間がかかったり、おまけの機能である成績一覧表作成時に、罫線位置が誤って描画される不具合があります。罫線の描画に問題がある場合は、罫線機能をOFFにしてデータのみを出力してください。こちらの成績一覧表はメモ程度にお使いください。

なお、大語群対応のマークシートリーダーでは、マークの読み取り結果を最後に一括して表示する速度優先モード及び読み取り結果の音声読み上げ機能は使用できません。

本記事で紹介したマークシートを同梱しました。Zipファイル展開後に作成されるSample_MarkSheet フォルダ内にある R25C04D19.xlsx をご参照ください。

塗りつぶし形式のマークシートは、解答に時間がかかります!
「塗りつぶさないマークシート」の改良版が上記リンク先からダウンロードできます。こちらもあわせてお試しください。

次のリンク先からマークの高速読み取りを可能にする Python 環境を含めたデジタル採点プログラム一式をダウンロードできます。マークシートも、ここに紹介した形式の他、様々なタイプのものを同梱しています。



この記事で紹介した100選択肢対応マークシートリーダーの使い方は、次の記事をご参照ください。

9.お願いとお断り

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

PowerShellに文字列を送信

職場の同僚に頼まれて、任意に選択したフォルダへのアクセスを禁止したり、それを解除したりするスクリプトを書いた。PowerShellの使い方を勉強すれば、フォルダの属性の変更は容易に実行できるから、セキュリティ対策として不完全なことは百も承知。

ダブルクリックでは開けないフォルダを作っただけなんだけれど、起動したPowerShellのWindowを閉じる部分でちょっとハマったので、備忘録として、ここにメモ。

フォルダへのアクセスを禁止したり、それを解除したりするスクリプトは、使い方を誤るとたいへんなコトになる(と思う)ので、ここには書かない。

【もくじ】

1.PowerShellを起動してスクリプトを実行
2.PowerShellのWindowを閉じる
3.PowerShellのWindowを表示しない方法もある
4.まとめ
5.お願いとお断り

1.PowerShellを起動してスクリプトを実行

PowerShellで実行したいスクリプトを用意する。ここではそれを「SetDeny.ps1」とした。Formに用意したEditコントロールのキャプションを次のように指定する。

コントロールの名前は、EditDeny


スクリプトを実行するButtonを1つ用意。名前をButtonDeny、Captionを「変更」として、次の手続きを作成。

procedure TForm1.ButtonDenyClick(Sender: TObject);
var
  strExeFile: string;
  strParams: string;
begin
  strExeFile:= 'PowerShell.exe';
  strParams:= '-noExit  -ExecutionPolicy RemoteSigned '+EditDeny.Text;
  ShellExecute(0, '', PChar(strExeFile), PChar(strParams), nil, SW_SHOWNORMAL);
end;

実行(F9)すると・・・

何も表示されないが、Editコントロールに指定したスクリプトが実行される。


スクリプトを実行したら、PowerShellのWindowも閉じたい。

2.PowerShellのWindowを閉じる

そこで上の手続きを次のように変更。

procedure TForm1.ButtonDenyClick(Sender: TObject);
var
  strExeFile: string;
  strParams: string;
  hWndInstance: HWND;
begin
  strExeFile:= 'PowerShell.exe';
  strParams:= '-noExit  -ExecutionPolicy RemoteSigned '+EditDeny.Text;
  //ShellExecute(0, '', PChar(strExeFile), PChar(strParams), nil, SW_SHOWNORMAL);
  hWndInstance:= ShellExecute(0, '', PChar(strExeFile), PChar(strParams), nil, SW_SHOWNORMAL);
  //Windowの存在を確認
  if hWndInstance > 32 then
  begin
    //タイムラグを設定
    //Sleep(100);  //短すぎる
    Sleep(1000);  //My環境では500でも動作した
    //ExitとEnterキーの入力を送信
    ButtonExitClick(Sender);
  end;
end;

PowerShellのWindowを閉じる部分は、別の手続きにしないと期待した通りに動作しなかった・・・。
なんでかな?

「閉じる」ボタンの名前は、ButtonExit に設定。
procedure TForm1.ButtonExitClick(Sender: TObject);
var
  hWndPSWindow: HWND;
begin
  //PowerShellを閉じる
  hWndPSWindow:=FindWindow(nil, PChar('Windows PowerShell'));
  if hWndPSWindow <> 0 then
  begin
    SetForegroundWindow(hWndPSWindow);
    //文字列の送信
    SendKeys('Exit');
    //Enterキーの送信
    SendKeys(#13#10);
  end else begin
    ShowMessage('PowerShellのウィンドウが見つかりません!');
  end;
end;

あと、最初は、PowerShellのWindowを探す部分を次のように書いていたんだけれど、これではWindowが見つからなくて・・・

hWndPSWindow:=FindWindow(nil, PChar('PowerShell'));
メッセージのキャプションのCfaは実行ファイルの名前。
名前は「Change Folder Attributes」から作成。


よく見たらPowerShellのタイトルは Windows PowerShell だった・・・

hWndPSWindow:=FindWindow(nil, PChar('Windows PowerShell'));

3.PowerShellのWindowを表示しない方法もある

自分的には「画面に何か動きがあった」方が安心できる気がしたので、PowerShellを表示する方法を選んだが、もちろん表示しない方法もある。いつもお世話になるMr.XRAYさんのWebページに詳しい解説がある。

Delphi で Windows PowerShell を使用する

http://mrxray.on.coocan.jp/Delphi/Others/PowerShellDelphi.htm#09

4.まとめ

(1)PowerShellに文字列を送信するには、FindWindow関数を使う。
(2)手続き的には、SetForegroundWindow関数とSendKeysを組み合わせて実行。
(3)PowerShellを閉じるタイミングはSleep関数で調整。

5.お願いとお断り

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

無料で使える手書き答案採点補助プログラム

Answer Column Reader

横書き答案の採点実行時の画面です。


スキャナーで読み取った手書き答案のJpeg画像から、大問1の設問(1)なら(1)のみを抽出、一覧表示してイッキに採点。採点記号( 〇・△・× )& 得点付きで元の答案画像に書き戻し、最後に得点合計を自動計算、指定位置に描画して、返却用答案画像(A4サイズに統一/縦・横の指定は可能)を印刷できる無料の手書き答案採点補助プログラムです。

一般的な横書き答案に加え、国語で使用される縦書き答案の採点も可能です。

縦書き答案の採点実行時の画面です。


新教育課程の観点別評価にも対応。もちろん、表計算ソフトを使わずに成績一覧表の作成・印刷・CSVファイルへの出力が可能です(ただし、成績一覧表の出来栄えは、メモ程度)。

画像処理に使用しているPython用OpenCV関連のファイルサイズが巨大ですが、このファイルサイズを許容していただければ、採点現場で十分使えると(複数の高校で使用中)評価していただけました!

もちろん、完全無料。ただし、動作保証は一切ありません。作成者(僕)は開発環境のDelphi(Object Pascal)の大ファンで、この他にも自作のマークシートリーダーなどを開発・このblogの過去記事で公開していますが、学問領域で評価の対象となるようなプログラミングに関しては全くの素人です。ですから、このプログラムのご使用に際しては、あくまでも素人が趣味で作ったものであるということを十分にご理解いただき、ダウンロードから展開・実行までALL自己責任でお願いします(有償販売禁止の他は、それが唯一の使用条件です)。発見できた不具合はすべて改良改善しましたが、取り切れていない未発見の不具合もまだきっとあると思います。それでも、もし、よろしければお使いください。僕の夢のカタチ、Answer Column Reader。

手書き答案採点補助プログラム、名付けて AC_Reader です。

追記(20240929)

当Blogで紹介してきた自作のデジタル採点プログラムを一つにまとめました。次のリンク先にその紹介とダウンロードリンクがあります。この記事で紹介している手書き答案のデジタル採点プログラムAC_Readerもプログラムセットに同梱されています。

【もくじ】

1.使い方
(1)zipファイルを展開
(2)プログラムを起動
(3)スキャンした答案の画像を準備
(4)採点用画像の準備
(5)解答欄の座標を取得
 ・【座標データを追加したい場合は?】
 ・【字数制限のある解答欄座標の簡単な取得方法は?】
 ・【機械が認識しやすい解答欄】
 ・【生徒の番号・氏名も解答横に表示して採点したい】
(6)採点
 ・【全員正解を入力】
 ・【全員不正解を入力】
 ・【個別に採点】
 ・【次の設問を採点】
 ・【定型文を入力】
 ・【入力した定型文の削除・消去方法】
(7)採点状況の確認
(8)返却用答案の印刷
(9)成績一覧表の作成・印刷
2.まとめ
3.お願いとお断り

1.使い方

もくじへ戻る

(1)zipファイルを展開

ダウンロードしたzipファイルをデスクトップ上に展開します(任意の場所に置いても動作すると思いますが)。PCによっては展開(解凍)に20分程度かかることがあるようです。

もくじへ戻る

(2)プログラムを起動

展開されたフォルダ内に「AC_Reader.exe」があります。これをダブルクリックしてプログラムを起動します。

このアイコンをダブルクリックしてプログラムを起動します


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

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


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

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


プログラムの起動時に、次のメッセージが表示されます。「はい」・「いいえ」のいずれかを選択してください。

差し支えなければ「はい」を選択してください。


Excelの採点シートを使って処理する場合は、拙作マークシートリーダーとこの手書き答案採点補助プログラムを併用することも可能です(その方法についての説明は、今回は行いません)。

マークシート方式と併用することも可能ですが、今回は「はい」をクリックしてください。


国語の試験では縦書きの解答用紙が使われますので、この手書き答案採点補助プログラムも縦書き答案の採点が出来るよう設計しました。デフォルト設定の答案書式は「横書き」です。国語の縦書き答案を採点したい場合はここで設定画面を開き、縦書きを指定してください。

今回は「いいえ」で先に進みます。


ちなみに「はい」を選んだ場合は・・・

答案の書式を指定できます。


答案の書式の設定変更を起動時に問われなくするよう設定できます。

採点する答案の書式が決まっている場合は「はい」を選択してください。


試験は毎日行われているわけではなく、定期考査として2~3か月に1回実施されるのが普通です。これくらい間が空くと、△の付け方などをどうしても忘れてしまいます。「忘れた!」と毎回のように質問がありましたので、プログラムの起動時にメッセージとして、採点方法を表示することにしました。

特に「△」の入力方法を問われることが多かったです!


この入力方法の案内は、画面右下の「終了」ボタンのとなりにある「入力方法のご案内」ボタンをクリックすれば、いつでも再表示できます。

入力方法はいつでも確認できます。

もくじへ戻る

(3)スキャンした答案の画像を準備

答案の画像は必ず「解像度200dpi程度」でスキャンし、「Jpeg画像として保存」してください。

重要 白黒の二値化画像としてスキャンしないでください。

なお、答案をスキャンする際は、次のことにご注意ください。

・答案が出席番号順に並んでいることを必ず確認してください。
・答案の向きは問いませんが、上下が揃っていることを必ず確認してください。
・試験を欠席した生徒がいる場合は、そこに未使用の解答用紙を挿入しておきます。
・消しゴムの屑等はよく払い落としておきます。
・一度に採点できる枚数は100枚を想定しています。
・答案に折り目がある場合は、なるべく平らになるよう折り戻しておきます。

スキャンした答案の画像は、科目名とクラス・講座名がわかるよう適切な名前をつけたフォルダ内に保存し、このフォルダをAC_Reader.exeがあるフォルダの「ScanData」フォルダにコピーしてください。

重要 スキャンした画像は、必ず「ScanData」フォルダ内にフォルダを作成し、保存してください。

重要 ScanDataフォルダ内のフォルダに階層構造を作らないでください。

よい例:

ScanData¥数学Ⅰ_1A

わるい例:

ScanData¥1年¥数学Ⅰ_1A

もくじへ戻る

(4)採点用画像の準備

上記の手順で、スキャンした答案のJpeg画像を所定の場所に準備したものとして説明します。

プログラムの画面右上にある「画像変換」ボタンをクリックします。

重要 採点用画像には、必ずこの画像変換プログラムが生成したJpeg画像を使ってください。それ以外の方法で作成した画像は使用できません!


次のWindowが開きます。「選択」をクリックしてください。

ScanDataフォルダ内に用意した「答案画像を入れたフォルダ」をクリックして選択します。
練習では予め用意されているSampleフォルダを選択してください。

重要 選択するのは「フォルダ」で、「ファイル」ではありません。

採点したい答案画像のあるフォルダをクリックすればOKです。
(ダブルクリックして開ける必要はありません)

サムネイル表示を見て、画像の回転の有無・回転方向を指定します。Sampleの画像で練習する場合は「なし」を指定してください。

次に画像のリサイズの有無を指定します。複合機のスキャナーを使用し、解像度200dpiでスキャンした画像の場合、80%程度に縮小すると採点しやすいと思います。答案画像をプレビューして縮小率を確認しながら作業することができます。

画像のリサイズ設定を行ったら、次に採点用画像の保存先を指定します。「参照」ボタンをクリックしてください。画像の保存先を選択するWindowが表示されます。

採点用画像の保存先は、ScanDataフォルダ内ではなく、「ProcData」フォルダです。

Procはprocessed(処理済み)の略です。

重要 ProcDataフォルダ以外の場所は、作業フォルダに出来ません!

画像の変換元として選んだScanDataフォルダ内のフォルダと同じ名前のフォルダを、プログラムはProcDataフォルダ内に自動的に作成します。ここでは、この自動的に作成されたSampleフォルダをクリックして選択し、OKをクリックしてください。

フォルダは自動的に作成されたものを選びます。

「変換実行」ボタンをクリックすると採点用にリサイズされた画像が上で指定したフォルダ内に作成されます。この処理はGDI+で書きましたので、それなりに高速だと思いますが、答案の枚数が多く、回転を伴う場合は少し時間がかかります。処理が完了するまでしばらくお待ちください。

この処理では用途の異なる2種類の画像を作成します。一つは採点マークのない各解答欄画像の読み取り元として利用する画像、もう一つは採点マークその他必要事項を上書きした返却用答案画像として利用する画像です。このようにすることで、何度でも採点のやり直しができる仕組みを実現しています。

注意していただきたいのは(めったにないことですが)、採点結果を答案画像に書き戻している最中に何らかの原因でプログラムが落ちた(クラッシュ/フリーズ)場合です。プログラムは採点結果を数値データとしてCSVファイルに書き込むと同時に、採点マークを付けて返却用答案画像にも書き込みます。CSVファイルへのデータの書き込みは一瞬ですが、返却用答案画像への書き込みには少し時間がかかります。したがって、この書き込み処理の最中にプログラムが落ちると、確かに採点した(採点データを保存したCSVファイルが存在する)のに、採点結果が正しく書き込まれていない答案画像が出来てしまうといった現象が起こります(過去1回だけ、この現象を確認しました)。このような場合には、それを発見した時点で採点済みのデータを読み込んで、再度(画像への)「書き込み」処理を実行すれば不具合を解消できます。

変換が終了すると、そのことを知らせるメッセージが表示されます。メッセージのOKをクリックすると注意のメッセージが表示されます。この注意のメッセージを確認した後、「終了」ボタンをクリックして、画像変換処理を終了してください。

終了をクリックして、この窓を閉じます。

メッセージの「OK」をクリックすると表示されるメッセージです。

画像のリサイズを行った場合は、その際設定した縮小率を試験で使用した解答用紙の残部などに必ずメモしてください。複数クラスで様式の異なる解答用紙を使って試験を行い、それぞれに74%、87%など細かな値を指定した場合は2日も経てばかなりの確率でその値を忘れます。この値を忘れた場合には、採点設定作業をすべてやり直すことになります。十分注意してください。

もくじへ戻る

(5)解答欄の座標を取得

次に解答欄の座標を取得します。その際、重大な注意事項があります。

重要 実際に試験で使用した解答用紙の画像を使用する

わるい例:
・輪転機で大量に印刷した解答用紙でなく、PCからプリンターに出力した解答用紙を使用

上のわるい例のように、実際に試験で使用した解答用紙とは異なる印刷環境で作成した解答用紙は、解答欄座標の取得には使用しないでください。見た目はほとんど同じでも、ほんのわずかな印刷位置のずれが採点作業のすべてに悪影響を及ぼします。この点には、どうか十分にご注意願います。

最初に開発したバージョンでは、拙作マークシートリーダーと同じように解答用紙に座標原点とするマーカー画像を設け、OpenCVのテンプレートマッチングの機能を利用して、マーカー画像からの距離で解答欄の座標を記録し、解答欄矩形の選択に利用していましたが、解答欄矩形を自動的に認識する方法を学んでからは、マーカー画像を利用し、手動で一つ一つ解答欄矩形を指定するよりも、解答欄矩形を自動認識して採点対象とする矩形の座標データのみを取捨選択して保存した方が、実際の採点に入るまでの準備作業時間を大幅に短縮できることがわかりました。また、輪転機を使用して印刷した解答用紙自体に解答欄の印刷位置のずれはほとんど生じないことも、マーカー画像を利用した解答欄座標の取得から、解答欄矩形を自動認識する方向へ設計を変更する大きな要因となりました。

以上の理由からご理解いただけると思うのですが、この手書き答案採点補助プログラムで使用する解答用紙は「解答欄の印刷位置がすべて揃っているもの」でなければなりません。

前置きが長くなりましたが、その具体的な方法は次の通りです。

最初に画面右上の「採点作業」ボタンをクリックしてください。


以前に使用した採点設定ファイルが見当たらない場合は、次のメッセージが表示されます。

よく読んで、OKをクリックしてください。


以前に使用した採点設定ファイルがある場合は、次のメッセージが表示されます。


使用する採点作業の入力欄に下の例のように入力します。

例:R06_考査①_物理基礎

前の方が見えませんが・・・

重要 採点作業の名称にはクラス名を入れないでください。

同じ採点作業の設定を複数クラスに適用する際、採点作業名に特定のクラスの名称が入っていると、なんとなく違和感を感じませんか?(僕は違和感を感じました)

このプログラムでは、(同一問題で実施した)試験の答案をクラス・講座毎のフォルダに準備して、同じ(一つの)採点設定をそれぞれのクラス・講座に適用して採点します。したがって、採点作業の名称には「クラス名を入れない」ことが望ましいわけです。※ クラス名が入っていても採点作業に使えないわけではありません。

採点作業名を付けたら、入力欄の右側にある「Auto」ボタンをクリックしてください。

ほんとうは「解答欄矩形の自動選択」のような名称にしたかったのですが、スペースが・・・


次のメッセージが表示されます。よく読んでOKをクリックしてください。

AC_Readerとは別に、解答欄矩形を見分けて自動選択するプログラムが起動します。このプログラムもObject Pascal に埋め込んだ Python Script で Python 用の OpenCV の機能を利用して動作します。

重要 RectangleDetector.exeを直接起動しないでください

重要 矩形検出機能はAC_Readerから呼び出して使ってください

解答欄矩形を認識するプログラムの名称は「Rectangle Detector(長方形検出器)」です。最初に画面左下にある「画像選択」ボタンをクリックしてください。


ここではフォルダではなく、「ファイルを選択」するダイアログボックスが表示されます。どれでもよいのですが、欠席者がいる場合は、解答欄に何も書き込まれていない欠席者分の解答用紙の画像を選択した方が、誤検出は明らかに減ると思います。ファイルを選択したら「開く」ボタンをクリックしてください。

重要 ここではフォルダではなく、ファイルを選択します。

重要 実際の試験で使用した解答用紙の画像で作業します。

練習では、添付したSampleフォルダ内のファイルを選択してください

解答用紙の画像が表示されます。上下のスクロールバーを操作して、図のように解答用紙の解答欄の直線部分とRectangleDetectorの画面枠の二つを見比べやすい位置に画像を上下に動かして、解答用紙が大きく傾いていないことを確認します。

スキャナーによっては、その機材特有の「クセ」のようなものがあり、どれほどきちんと解答用紙をセットしても必ず0.3~0.4°くらい読み取った画像が傾いてしまう場合があります。サービスマンの方に相談したところ、「答案に付着した消しゴムの屑がローラー等に詰まって、読み取り結果に悪影響を及ぼしているのではないか?」との意見をいただき、実際、スキャナーの可動部をきれいに清掃して試したところ、読み取り結果が改善された経験があります。しかし、その後、またすぐにその機材で読み取った画像は同じ方向に傾くようになりましたので、毎回クリーニングする必要があるのかもしれません。ただ、可動部をクリーニングしなくても、ほとんど傾かずに読み取ることもあり、結局、「これは運だ!」と割り切って、プログラム側で傾きがあった場合は修正できるよう、傾き補正の機能を追加しました。

傾き補正の機能を追加する際に気づいたのですが、回転させた画像をさらに回転させると、画像の質が著しく劣化し、これを繰り返すほどに全体がぼやけて、解答の読み取りに支障をきたす恐れがあるように感じました。そこで、画像の初期状態を保存しておき、回転は必ず初期状態のものに対して行うようプログラミングしました。「なぜ、少しずつ連続して回転させることができないのだろう?」と疑問に思われる方もいらっしゃるかもしれませんが、これがその疑問への回答です。

赤線部分を見比べて、画像の回転の要/不要を判断します。


上の画像のような状態であれば、傾きの補正は必要ありません。オプションボタンは「実行」をクリックして選択してください。また、あまりにも小さな矩形は「解答欄ではない」と判断できるよう、矩形の面積閾値を設定してあります。こちらはデフォルト設定700のままでお試しください。

傾きの修正が必要な場合は「試行」を選択し、修正量を確認してください。
修正量を確認後、その値で傾きの補正を「実行」してください。
(「試行」を選択した場合は、最終的なデータの保存ができません)


続けて答案の「横書き・縦書き」を指定します。Sampleは横書き答案ですので、オプションボタンは「横書き」をクリックして選択してください。


ブロックというのは(表現に苦しんだのですが)、「解答欄の集合をブロックとして見分けられるか・どうか」という意味です。下の図のような解答用紙の場合、1ブロックと表現しています。


ちなみに、次のような場合が2ブロックです。ご理解いただけましたでしょうか?


傾きの修正が必要な場合は、次のGUIで操作してください。「傾き修正」に✅を入れて、▲は修正値を増やす(回転方向は時計回り)、▼は修正値を減らす(回転方向は反時計回り)、「適用」は回転の実行、「やり直し」は画像を初期状態に戻します。


解答欄を取得する準備が整ったら、「解答欄取得」ボタンをクリックしてください。


誠に心苦しいのですが、PCによっては初回実行時、Python Engineの初期化に異常に時間がかかることがあります(職場のPCでは4分程度)。自分のPC(Panasonic製 Let’s note CF-QV)では数秒で終了する処理がなんでPCによってはとんでもない時間を要する処理になるのか? その理由は未だにわかりません。

とにかく、マウスカーソルが砂時計?表示になっていればプログラムは正常に機能していると思われますので、5分程度お待ちください。いったんPython Engineの初期化に成功すれば、プログラムを終了しない限り、2回目以降の実行は何の問題もなく、ほんの数秒で解答欄座標の取得が完了するはずです。

参考 横書き答案の場合、解答欄矩形の座標はx軸方向については左から右へ、Y軸方向については上から下へという順番で読み取ります。

参考 縦書き答案の場合、解答欄矩形の座標はx軸方向については右から左へ、Y軸方向については上から下へという順番で読み取ります。

ただし、解答用紙の画像が右肩上がりに傾いていた場合、Y軸(上下)方向の座標の上下関係から、より値の小さな(座標原点0,0は解答用紙画像の左上であるため)上の方をプログラムは先に読み取ってしまいます。そのため、横書き答案であっても解答欄矩形の読み取り順が右から左になる現象が発生します。こうなると解答欄矩形の座標の選択作業が著しく煩雑になってしまいます(解答欄矩形の座標自体は読み取れていますから作業ができないわけではありません)。これを防止するために、最初に答案画像の全てに対し、傾きの修正を行う必要があります。

解答欄座標の取得が完了すると、次の図にあるように解答用紙上に赤い矩形が描画されます。小さくてわかりづらいかもしれませんが、画面右上の解答欄座標の値が表示されている部分で、カーソルがある(カーソルが点滅している位置の)解答欄座標が赤の矩形で示されています。ここから必要な座標と、いらない座標を取捨選択する作業を行ってください。


上の図で示されている矩形(座標)は採点には不要です。このまま無視して次へ進んでも構いませんし、面倒でなければ不要な座標は削除することもできます。


「編集」ボタンをクリックすると、キャプションが「編集中」に変わり、カーソル位置の座標が選択された状態になります。DELキーを押し下げして、不要な座標を削除します。

次の図は(削除作業を行わずに)上の図の状態から↓矢印キーを1回押し下げして、カーソルを2行目に移動させた状態を表しています。不要な解答欄座標の削除作業を行った場合は、自動的にこの状態になります(1行目にあった不要な座標は当然消えています)。

カーソルを下の行へ移動させて、解答欄矩形のみを選択(移動)して行きます。


2行目の座標が示す矩形はまさに解答欄ですから、これは必要な座標ということになります。このような座標は「移動」ボタンをクリックして、必要な座標ばかり集めたメモの方へ移動させます。次の図は2行目の座標を移動させた直後の状態です。

必要な解答欄座標のみを選択します。

下向きの矢印キーを押す。必要な座標であれば「移動」ボタンで下のメモに移動する。この作業を繰り返して採点する順番になるよう、解答欄の座標をすべて取得します。次の図は一通り、解答欄の座標を取得した状態です。


続いて正しく解答欄座標が取得できていることを確認します。上の図の移動済み解答欄座標が表示されているメモ(赤枠内)の先頭の座標データをクリックしてください。メモは必要であれば上にスクロールしてください。メモの先頭の座標データをクリックしたら、答案の画像も上にスクロールしてください。画面は、次の図のようになります。

メモ内のフォーカスがある座標データに該当する矩形が赤枠で示されています。


このまま、下向きの矢印キーを次々に押し下げして、赤枠で示される解答欄矩形が必要数あるか・どうか、及び、採点順に並んでいるか・どうかを確認して行きます。

もくじへ戻る

【座標データを追加したい場合は?】

様々な事情から、座標データを後から追加・変更したい場合もあるかと思います。例えば、次の図のように青枠で囲った解答欄AとBを抱き合わせて採点(両方正解で〇等)したい場合です。

青枠部分を抱き合わせて採点したい場合も当然あるかと思います。


このような場合は、該当の座標データの「末尾」にフォーカスした状態で(=座標データの末尾にカーソルを置いて)、「移動」ボタンの隣にある「追加」ボタンをクリックし、さらにEnterキーを1回押し下げして改行します。次の図は、その状態を示します。

「追加」ボタンのキャプションは「追加中」に変わります。


次に、画面の真ん中よりやや右にある追加ボタンをクリックします。


答案画像の上に赤枠の矩形が表示されます。この矩形を新しく解答欄座標を取得したい解答欄に重なるように移動・変形してください。矩形を移動させたい時は、矩形の上の横線中央よりやや右の位置をポイント(マウスのカーソルを載せる)すると、マウスカーソルが上下左右の白い矢印に変わり、ドラッグアンドドロップできる状態になります。

任意の座標を取得可能です。


抱き合わせて採点したい解答欄を矩形で囲んだら(下の図のような状態)、キャプションが「取得」に変わったボタンをクリックします。すると、ボタンの右側に、現在表示されている矩形の座標が表示されます。同時に、この矩形データはクリップボードにも送信されています。


続けて、右側のメモ内の先ほど改行して空行になっている箇所をクリックしてCtrlキーを押しながらVキーを押す(右クリックして表示されるサブメニューから「貼り付け」を選択)等して、取得した座標データを付け加えます。正しくメモに追加できたら、メモの上の「追加中」ボタンをクリックして、キャプションを「追加」に戻します。

上下の矢印キーを押して、解答欄Aの座標を探し、「追加」ボタンをクリックして、メモを編集可能な状態に変更、データを削除します。削除後、編集が終了したことをPCに伝えるため、「追加中」ボタンをクリックして「追加」に切り替えます。

解答欄Bの座標も、解答欄Aと同様に作業してメモから消去します。

注意 「追加中」状態で作業しないとエラーが発生します!

もくじへ戻る

【字数制限のある解答欄座標の簡単な取得方法は?】

例えば、次のような多数の細かい枠で構成された字数制限のある解答欄がある場合、このまま矩形座標の自動取得処理を実行すると一つ一つのマス目の座標をもれなく取得・表示してしまいます。

解答欄を構成する枠がすべて実線の場合、解答欄座標の取得が煩雑になります。


このような場合は、解答欄を作成する段階で、外枠のみ実線で描き、内部の枠はすべて「点線」で描くようにします。点線は、色が薄く、間隔の狭い、細い点線でなく、次の図に示すように、色が濃く、間隔が広い、太い点線を使用してください。

解答欄内部の枠を「点線」で描くとプログラムは外側の枠のみを解答欄座標として認識します。


実は、最初の段階からこの「字数制限のある解答欄の認識処理をどうするか?」という問題は大変気になっていたのですが、親しい国語の教員が作成した解答用紙をスキャンして、解答欄の座標を自動取得する作業を手伝った際、解答用紙の点線部分をプログラムが認識しないことを偶然発見、大喜びしたというのが本当です。最初から、僕に、そのような知識があったわけではありません。

偶然とは言え、僕の不出来なプログラムの動作を信じて、それでも使いたいと言ってくれた彼女に、心から、ほんとうに、こころから、「ありがとう」です。巡り合ってから、もう、30年になりますが、Sさん、ほんとうに、ありがとう! あなたがいてくれて、ほんとうに、よかった!!

ただし、これは「諸刃の剣」で、何らかの原因で解答欄の枠線の一部が途切れていると、プログラムは正直にその部分は「矩形ではない」と判断して、座標データの取得対象から除外します。ですので、解答用紙を印刷する際は、解答欄が完全に実線で囲まれているか・どうかを、よく確認してから印刷する必要があります。

解答欄の枠線の一部が途切れていると座標を取得できません!

もくじへ戻る

【機械が認識しやすい解答欄】

解答欄を構成する矩形は必要最小限度に留めるのが、解答欄座標を自動認識・取得する作業を効率よく進めるための何よりのポイントです。

解答欄を構成する矩形は必要最小限にしてください。

もくじへ戻る

【生徒の番号・氏名も解答横に表示して採点したい】

重要 横書き答案の採点時のみに利用できる機能です。

こちらは同僚からの要望があって付け加えた機能です。解答用紙の氏名欄の画像を取得して、採点時に該当生徒の解答欄の横(位置の指定も可能)に、試験を受けた生徒の出席番号や氏名を表示できます。「追加」ボタンをクリックして赤枠の矩形を描画・適切な位置へ移動後、解答欄矩形としての「取得」の代わりに、「氏名欄取得」のボタンをクリックして、次の図に示すようなかたちで解答用紙の氏名欄の座標を取得してください。ただし、指定する矩形の高さは、解答用紙の解答欄の高さの最小値を超えないよう、十分注意してください。

重要 「解答欄の高さの最小値を超えない高さ」で範囲指定してください。

座標が空欄でなければ、氏名情報ありとして保存されます。


最後に、取得した解答欄の座標を保存して作業は終了です。画面右にある「保存」ボタンをクリックしてください。


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

「はい」をクリックして、解答欄座標を保存します。


採点作業名として設定した名称で、イニシャライズファイルが作成されています。この採点作業名をクリックするとダイアログの下のファイル名が採点作業の名称に変化します。この状態で「保存」ボタンをクリックしてください。

採点作業名を設定した際にiniファイルも作成されています。
解答用紙の種類に合致するファイルをクリックして選択・上書き保存します。


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

既存のiniファイルに上書きします。


解答欄の数によっては、少し(数秒程度)時間が必要です。保存作業が完了すると次のメッセージが表示されます。このメッセージが表示されるまで、何もしないでそのままお待ちください。


画面右下隅にある「閉じる」ボタンをクリックしてプログラムを終了します。解答欄矩形の座標の候補を表示する上のメモにデータがある場合は、「閉じる」をクリックすると、次の確認メッセージが表示されます。「はい」をクリックしてプログラムを終了させてください。


以上で、解答欄の座標の取得作業は完了です。

もくじへ戻る

(6)採点

解答欄座標取得後、すぐに採点を実施する場合は、タスクバーにAC_Readerが眠っていますので、クリックして起こしてください。そうでない場合は、AC_Readerを起動してください。

解答欄矩形取得直後、AC_Readerはタスクバーに眠っています。
タスクバーにある上のアイコンをクリックしてください。
AC_Readerが目覚めます!


画面の右上にある「採点作業」ボタンをクリックしてください。


次のメッセージが表示されます。既存の採点設定を利用して採点しますので「はい」をクリックしてください。


バルーン型のヒントが表示されます。V マークをクリックして表示される選択肢から採点設定ファイルを選んでください。


採点設定ファイルを選んだ直後の状態です。


画面中央には、次のメッセージが表示されます。OKをクリックするとフォルダの選択ダイアログが表示されます。


採点したいクラスのフォルダを選択してOKをクリックしてください。

採点したいクラスのフォルダを選択して、OKをクリックします。


採点結果を記録したCSVファイル(場所はユーザーに提示しません)がない場合には、次のメッセージが表示されます。


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

個人識別情報が保存されているので、番号や氏名も表示されています。


画面上方、中央よりやや右に、どこにもドッキングしないフローティング状態の必要最小限の採点機能をまとめたパネルがあります。このパネルのタイトルバーの部分を左クリックしてドラッグ&ドロップすると任意の位置へ移動できます。採点しやすい位置へ移動してお使いください。

もくじへ戻る

【全員正解を入力】

解答をざっと見て、過半数が正解であるような場合は、全員に正解を入力し、後から不正解の解答のみチェックして、採点を × に変更します。

この設問の得点は2点として、全員に2点を入力します。


ComboBoxの選択肢に「2」を指定して、「入力」ボタンを

採点記号の位置や大きさは「設定」から変更できます。


設定画面から、採点記号の表示位置や大きさなど、各種設定を変更・保存できます。

何も変更せず、デフォルト設定のまま、みなさんお使いのようです。

もくじへ戻る

【全員不正解を入力】

フローティングパネルの得点欄に0を設定して、入力をクリックすれば、全員不正解となります。

0(ゼロ)は〇(まる)と見間違える可能性があるため、
デフォルト設定では、不正解の場合、得点0を表示しません。

もくじへ戻る

【個別に採点】

重要 左手で入力作業、右手は選択作業(クリックに専念)

・正解 〇 を入力

まず、個別に採点する際の正解入力は、次のように行います。

解答欄の中心付近をクリックして、得点に相当する数字キーを押します。


解答欄に採点記号〇と得点が描画されます。

・不正解 × を入力

不正解を入力する場合は、次のように操作してください。

× は「Batsu」だから「B」キーに割り当てました。


もちろん、数字キーの0(ゼロ)でも × を入力できます。ただ、0はちょっと位置が遠い・・・

・部分点あり △ を入力

部分点ありの場合は、採点記号△と部分点を入力します。方法は、次の通りです。

「部分点あり」のフラグは「-」記号の有無です。
プログラムは負の数の入力を部分点ありと判定しています。
(合計点は絶対値で計算するので、問題ありません)
部分点ありの場合、採点記号△と得点を表示

重要 最後に「書込」を忘れずにクリックします。

もくじへ戻る

【次の設問を採点】

右向きの三角マークをクリックすると、次の設問の解答欄が表示されます。

上で解説した手順で、採点を行います。

右側の操作パネルからも同じ操作を実行することができます。

もくじへ戻る

【定型文を入力】

記述式の設問等で「ここまで何点」のような定型文を記録しておいて適宜入力できます。

「設定」をクリックして、「入力定型文の編集」にチェックを入れます。


画面左上に次の表示が出ますので、内容を編集します。「記録」ボタンをクリックすると編集内容が保存されます。保存後、「入力定型文の編集」のチェックを外し、編集欄を非表示にします。


定型文を入力したい設問の解答欄を採点します。採点後、定型文を入力したい箇所の左上隅あたりにマウスのカーソルを持ってきて右クリックします。表示されるサブメニューから「定型文入力」を選択(クリック)してください。

重要 採点しないと定型文入力はできません!

「定型文入力」をクリックします。


編集済みの定型文が指定位置に入力されます。

もくじへ戻る

【入力した定型文の削除・消去方法】

入力済みの定型文を削除・消去するには、まず、定型文を削除・消去したい解答欄の真ん中付近をクリックします。次に、右側のGridコントロールの青く反転表示された数値を消去して、Enterキーを押してください。

もくじへ戻る

(7)採点状況の確認

現在の採点状況を、解答用紙全体の画像を表示して確認することができます。次のように操作してください。

画面右側の中ほどにある「返却答案を表示」をクリックします。画面は現在選択されている生徒の解答用紙が表示されます。画面をスクロールして、採点状況を確認してください。


移動のボタンで、別の生徒の答案も確認することができます。

左のボタンで「一枚前へ」、右のボタンで「次へ」移動します。

もくじへ戻る

(8)返却用答案の印刷

採点が終了したら、返却用の答案を印刷します。まず、画面右下のプリンタの選択肢から、出力先のプリンタを選択します。次に「合計の印刷」の有無を指定します。「有」を選択した場合は、次の案内が表示されます。


印刷は採点終了後、最後に実行するので、採点と印刷の処理をお互いに行ったり来たりすることは「ない」と判断し、印刷実行後はプログラムの終了のみ可能となっています。

「いいえ」をクリックした場合は、採点処理が継続されます。「はい」をクリックした場合は、次の案内が表示されます。

出力するプリンタの確認です。


「はい」を選択すると、次に合計点の印刷処理の案内が表示されます。


フォントサイズは、40~50程度が適切な場合が多いように思います。半角の数字で入力してOKをクリックしてください。


OKをクリックすると、次の案内が表示されます。


OKをクリックして、合計点印刷位置を指定します。


クリックした瞬間に自動計算された合計点が指定位置に表示され、次のメッセージが表示されます。


よろしければ「はい」を、位置の指定をやり直す場合は「いいえ」をクリックします。「いいえ」をクリックした場合は、再度、合計点を印刷する位置の指定をやり直してください。その際、前回に指定した位置にゴーストというか、残像のようなものが残りますが、実際の印刷時にはゴースト・残像は印刷されません。

「はい」をクリックした場合は、次のメッセージが表示されます。


画面右下の「印刷」ボタンをクリックしてください。

バルーンヒントが案内します。


「印刷」をクリックすると、次のメッセージが表示されます。


OKをクリックすると、プリンタの設定画面が表示されます。この画面はお使いのプリンタにより異なりますが、重要なチェックポイントは次の3点です。

重要 印刷する用紙がA4版であることを確認する

重要 印刷用紙の縦・横指定を答案に合わせて指定する

重要 両面印刷は必ずOFFに設定する

設定画面を閉じると、次のメッセージが表示されます。


「はい」をクリックした場合は、全員分の返却用答案がプリンタへ出力され、次のメッセージが表示されます。


「いいえ」をクリックした場合は、次のインプットボックスが表示されます。

答案の通し番号を入力してOKをクリックしてください。
採点対象がクラスであれば、出席番号となります。


OKをクリックするとプリンタへ印刷データを送信後、次のメッセージが表示されます。


「はい」をクリックすると、再びインプットボックスが表示され、引き続き単票の印刷処理が継続して行われます。「いいえ」をクリックした場合は印刷処理を終了します。画面右下の「終了」ボタンをクリックして、プログラムを終了してください。その際、次の案内が表示されます。

「はい」をクリックすると、プログラムが終了します。

もくじへ戻る

(9)成績一覧表の作成・印刷

画面右にある「成績一覧表を作成」の「Excelを使わずに作成します!」をクリックします。


画面は成績一覧表作成モードになります。クラス単位の採点である場合は、学年・クラスを指定(選択)します。

重要 予めsNameフォルダに生徒氏名データを用意しておきます。

重要 講座単位の処理の場合も、講座名等で氏名データを準備しておきます。

重要 氏名データの並び順は、答案の並び順と一致させてください。

クラスを指定する場合は、直接入力してください。


講座を指定する場合は、学年・組は「空欄」のまま、「観点区分入力」に進んでください。

設問毎に「知識・技能」は1、「思考・判断・表現」は2を入力します。


観点別評価の区分を入力後、「保存」をクリックしてください。

保存後、「採点結果表示」をクリックして、採点結果の一覧を表示します。

氏名データは架空のもので、得点はダミーデータです。


学年・組を「空欄」で処理していた場合は、ここで「講座等」の名票を選択します。

氏名データは架空のもので、得点はダミーデータです。


次に、合計点が0の生徒について、欠席者であるか(平均点の計算から除きます)・真に0点であるのかを指定する処理を行います。「欠席者を除外」をチェックしてください。


合計点が0の生徒がいる場合は、次のメッセージが表示されます。

試験を欠席していた場合は「はい」を、0点であった場合は「いいえ」をクリックします。
(ここでは「はい」で処理します)


「再計算」ボタンをクリックして、平均点他の再計算を実行します。


プレビューをチェックして、印刷プレビューを表示します。


プレビューをチェックすると、印刷プレビューとともに、次のメッセージが表示されます。

印刷プレビュー画面(氏名データは架空のもので、得点はダミーデータです)


表示されるメッセージ。


プレビューのチェックを外すと、次のバルーンヒントが印刷ボタンを案内します。


「印刷」ボタンをクリックすると、印刷設定のダイアログが表示されます(ダイアログはプリンタにより異なります)。成績一覧表はデフォルトで「A4・縦置き」印刷に設定されます(この設定を変更することはできません)。


OKをクリックすると、印刷データがプリンタへ送信されます。送信が完了すると、次のメッセージが表示されます。


なお、これとは別に、このプログラム用に作成したExcel Book(添付したマクロ有効テンプレートのコピー)へ採点結果を出力し、成績一覧表及び個人成績票を作成する機能もこのプログラムにはありますが、これに関する説明はまた後日、このblogに掲載できたら・・・とも、考えています。が、ほとんど!!どなたにもお読みいただけないであろうMy blogですので、もしかしたらそれは、はるか未来の話になるかもしれません。

ただ、PCの操作及びExcel Bookの扱いに慣れた方なら、このプログラムに添付したマニュアル(以前のバージョンのものなので画面や内容が現行バージョンと若干異なります)と、マクロ有効のExcel Bookの式とマクロをご覧いただければ、操作方法並びに機能の概要はおわかりいただけるのではないかと考えます。

このExcel Bookに対する出力機能は、(ここに掲載した)成績一覧表を独自に作成する機能をこのプログラムに追加する以前に作成し、実際の試験の採点で何回も活用済みのものですが、こちらも動作保証等は一切ありません。もし、お使いになる場合は自己責任でお願いいたします。

以上で、成績一覧表の印刷は終了です。

もくじへ戻る

2.まとめ

今回、掲載した手書き答案採点補助プログラム(新教育課程観点別評価「知識・技能」及び「思考・判断・表現」の評価に対応)の概要は以下の通りです。

【出来ること】

(1)スキャナーで読み取った答案画像から設問ごとに解答欄を抽出して一括採点。
   ※ 答案画像からの解答欄座標の取得は矩形認識プログラムで(半)自動実行。
(2)解答欄画像の隣に受験者氏名等を表示(予め氏名欄等の読み取り設定が必要です)。
(3)記述式の解答に対する定型文コメントの入力。
(4)採点結果を出力した返却用答案画像の作成と印刷(A4版限定・縦横指定は可能)。
   ※ 得点合計を自動計算、返却用答案の指定位置に印刷可。
   ※ B4やA3の答案画像は、A4サイズに縮小して印刷します。
(5)表計算ソフトを使わずに、成績一覧表(教科担任用)を作成。
(6)成績一覧表データをCSVファイルに出力(観点別評価のうち、2観点の評価に対応)。
(7)拙作マークシートリーダーを利用した試験との併用も可。
   ※ マークシートの読み取りプログラム一式も同梱しています。
(8)PDF化した答案画像をJpeg画像化して採点(添付のPdf2Jpg.exeを使用)。

【出来ないこと】

機械学習による手書き文字の認識にも過去にチャレンジ(〇・× 及びカタカナのアイウエオを判定)したことがあるのですが、どう頑張っても認識率が100%にならない(控えめな表現で9割程度は正しく認識するのですが、解答欄からはみ出した文字や、それは「ア」でなく「つ」と「ノ」でしょ!みたいな文字を構成する部品が極端に離れている字?や、大きく傾いた文字は正しく認識できない)ので、残念ですが、この機能は搭載を見送りました。

〇×記号やカタカナ一文字の認識結果を目視でイチイチ確認するのはどう考えても二度手間です。現時点では、ヒトが行った採点結果を機械にチェックさせる方向で活用した方がいいかもしれません。学習モデルの作成については、Pythonを利用した事例がWeb上に読み切れないほど存在しますが、(僕が実験した範囲では)それらよりMicrosoftのLobeで作成した学習モデルの方が高い認識率を示しました。このことについては当blogの過去記事でその例を幾つか紹介しています。ここで紹介した採点補助プログラムには搭載を見送った自動採点機能ですが、僕の実験結果が何かの参考になれば幸いです。

もくじへ戻る

3.お願いとお断り

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

もくじへ戻る

GetPrinterを使用することは推奨されていません

Delphiで印刷のプログラムを書いていて、動作確認のため実行(F9)する際、次の警告メッセージが表示されることに気がついた。

[dcc32 警告] UnitXXX.pas(1858): W1000 シンボル ‘GetPrinter’ を使用することは推奨されていません

以前は「印刷して終了」というパターンでプログラムを作成することが多かったが、いつの間にか、紙への印刷ではなく、「ファイルへデータを出力して終わり」というパターンが普通になった。だから、印刷を実行するプログラムはこの数年間書いた記憶がない。

(印刷関連のプログラムも進化してたんだ・・・)

では、GetPrinterのかわりに何を使えばいいのか、現在、推奨されている方法を調べたが、GetPrinterにかわって推奨されている「はず」の手続き名がわからない。

さんざん悩んだ末、ようやくGetPrinterの引数の型の問題であることに気づく。もしかしたら、同じ問題で悩んでいる人がいるかもしれない・・・。ふと、そんな気がして。

【もくじ】

1.推奨されている「はず」の手続き名は?
2.GetPrinterが2つある・・・
3.まとめ
4.お願いとお断り

1.推奨されている「はず」の手続き名は?

「Delphi シンボル’GetPrinter’を使用することは推奨されていません」をキーワードに検索してみても、現在推奨されているはずの手続き名を知る手がかりになりそうな情報が表示されない。

この段階で、なんとなく前途に暗雲が漂うのを感じ、気分が重くなる。

(この警告の意味は「互換性のために残されている機能を使っている」ってことだと以前どこかで読んだ気がする。推奨されないってことはそれに代わる方法が必ずあるはずなんだけど・・・)

どうしても代替方法を見つけたい気持ちはもちろんある。その反面・・・

(推奨されないだけで、まだ使えるから、とりあえず、このままでいいかぁ・・・)

そう思ってしまうのも、また本当。

Copilotさんに訊ねても、納得できる回答は得られない。

(なんでかな?)

ただ時間だけが静かに流れて行く・・・

2.GetPrinterが2つある・・・

問題解決の手掛かりは思わぬところにあった。それがコレ!

GetPrinterが2つある!


検索しても代替手段の情報が出てこない。ならば(直接入力で)片っ端から探せばいいと考え、既存のGetPrinter手続きをコメントアウトして改行。Printer.getと入力してみると、あろうことか、GetPrinterが2つ表示される。

よく見ると引数の型がPCharとstringで異なっている。

(コレかー!)

おそらく、上の引数がPChar型なのが「推奨されないGetPrinter手続き」で、下の引数がstring型なのが(誰も教えてくれなかったけど、おそらく)「推奨されるGetPrinter手続き」なんだろー。

そんなことを思いながら、変数の宣言に戻って、次のように引数の型を変更。

var  
  //プリンタ設定
  //Device: array [0..127] of Char;
  //Driver: array [0..127] of Char;
  //Port: array [0..127] of Char;
  Device: string;
  Driver: string;
  Port: string;
  DeviceMode: THandle;
  pDevMode: ^TDevMode;

追記(20240821)

データをPDFファイルに出力するプログラムを書いていて、また、同じ警告に遭遇。(前にもあったなー)と思いながら検索すると自分の書いた過去記事がヒット。まさか、自分に救われるとは・・・記事を書いた時には思ってもみませんでした。

このブログは備忘録と言うことで、補足的に全体の状況をメモ。
警告が表示される状況とその対応方法のまとめです。

上のようなコードを書いて実行(F9)すると、成功するが警告が表示される。


【表示された警告】


【警告を消す方法】


上と同じなんだけど、こちらでは変数をまとめて1行で宣言。

procedure TForm1.Button1Click(Sender: TObject);
var
  //DeviceName, DriverName, PortName: array[0..MAX_PATH - 1] of Char;
  DeviceName, DriverName, PortName: string;
  DeviceMode: THandle;
begin
  Printer.GetPrinter(DeviceName, DriverName, PortName, DeviceMode);
end;

これで実行(F9)すると・・・

警告が消えた!

追記(20240821)ここまで

「プロジェクト」⇨「XXXの構文チェック」を実行し、警告のメッセージが消えたことを確認。同時に何か既視感のようなものが・・・

(そう言えば下にSetPrinter手続きもあったはず・・・)

そう思って、そちらを確認すると・・・やはり、

こちらも2つあった!


2つあるのはいっしょでも、よく見ると違いはあって、GetPrinter手続きの方は引数の前にvarキーワードがついてる「参照渡し」だが、SetPrinter手続きの方はそれがない「値渡し」になってる・・・

それに、変数の型を次のように宣言してあっても・・・

var  
  //プリンタ設定
  Device: array [0..127] of Char;
  Driver: array [0..127] of Char;
  Port: array [0..127] of Char;
こちらの引数の型はstring型でもエラーにならない・・・


(なんでかなー?)

MessageBox関数の引数でも同じことを感じたことがあったような・・・。PChar型の変数をstring型の変数に代入する際には、Delphiの場合、自動的に変換されるって理解であってるのかな?

摩訶不思議。

3.まとめ

GetPrinter手続き、SetPrinter手続きは同名の手続きがそれぞれ2つずつ存在し、引数の型がPChar/stringという違いがある。

引数の型がPChar型のGetPrinter手続きを使うと「シンボル ‘GetPrinter’ を使用することは推奨されていません」という警告のメッセージが表示される。

警告のメッセージを消すには、引数の型がstring型のGetPrinter手続きを使用する。

(これでいいのかなー?)

4.お願いとお断り

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

PDFを画像化したい!

PDF文書ファイルをページごとに画像化したくて様々な方法を探し(AND 試し)ましたが、なかなか「コレだ!」と思える方法が見つからず、ほぼ1日を費やして(ダメか・・・)とあきらめかけた、まさにその時、やっと出会えたのが Swanman (id:tales)さんの Blog の次の記事でした。

外部ライブラリ無しでPDFを描画する。

https://lyna.hateblo.jp/entry/20160625/1466783114

「それならWeb上の変換サービスを使えば・・・」というご意見はもっともですが、利用目的が「スキャナーでPDFファイル化した個人情報を含むデータの画像化処理」で、それなりに枚数もあるし・・・どこかのサーバーにアップして・・・というのは絶対に避けたいところです。

Swanman (id:tales)さんのBlogの記事を頼りに、何とか目的を実現することができました。これは、その際行ったことの備忘録です。

【もくじ】

1.FireMonkeyならPDFiumがある
2.WinRT
3.まとめ
4.お願いとお断り

1.FireMonkeyならPDFiumがある

最近、何かを調べる時はCopilotさんにお伺いをたてることが多いです。そこで、今回も早速きいてみました。

自分:
DelphiでPDFを画像化したいときはどうしたらいいですか

するとPDFiumというライブラリがあるとCopilotさんが教えてくれました。ただ、紹介されたのは「PDFium Component Suite for FireMonkey」だったので、どちらかというとWindows専用にVCLコンポーネントを使ってプログラムを書きたい自分的には(FireMonkeyはちょっと・・・)という感じだったのですが・・・、「溺れる者は藁をもつかむ」と、まさにそんな気持ちでありましたから・・・記事に目を通してみることに。

Copilot さんが教えてくれた FireMonkey 用の PDFium の紹介ページはこちら

PDFium – Delphi/C++Builder FireMonkeyアプリケーション向けPDFエンジン

https://blogs.embarcadero.com/ja/pdfium-pdf-engine-for-your-delphi-c-builder-firemonkey-applications-ja/

そこで紹介されていたコードの一部(抜粋して引用)。

  Bitmap := FPdf.RenderPage(0, 0,
    Round(PointsToPixels(FPdf.PageWidth, PixelsPerInch)),
    Round(PointsToPixels(FPdf.PageHeight, PixelsPerInch)));
  try
    Bitmap.SaveToFile(FileName + '_Page' + IntToStr(I) + '.jpg');
  finally
    Bitmap.Free;
  end;

このRenderPageなる手続きが使えれば、目的はカンタンに実現できそう。
それに PDFium はライセンス的にも問題なさそうだし・・・

(VCL版があればなぁ・・・)

ダメ元で探してみるとPDFiumのVCL版を発見。早速、ダウンロード!

ahausladen/PdfiumLib: PDF VCL Control using PDFium

https://github.com/ahausladen/PdfiumLib

大喜びで、使ってみたんだけれど・・・

RenderPageが「なぁーい!」 T_T

なんと、PDFiumのVCL版には RenderPageがありません でした!!

(間違えていたら、ほんとうに、ごめんなさい)

必要なものは全部 uses して、exeと同じフォルダに PDFium.dll も用意したのですが、何か足りないものでもあったのでしょうか?

なら、FireMonkey で書けばいいじゃん!って気持ちにそう簡単にはなれないのが人間です。実は、それなりに頑張ってFireMonkeyでいくつかのプログラムをこれまでに書いてみたのですが、エラーの原因解明に手間取ることが多くて、Windows PCしか使わない自分にとって、無理してまでクロスプラットフォームでプログラムを書くメリットはないように思えてきて・・・やはり、書きなれたVCLの方が自分にはあってる・・・と、どうしても、そう思ってしまい・・・

それに、ここでFireMonkeyにすると、この先もずっとFireMonkeyで書くことに。

現在、書き続けている一連の採点処理用途のプログラムは、すべてVCLで書いてきたこともあり、大変残念ですが PDFium の使用は、ここで断念することに決めました。

2.WinRT

Swanman (id:tales)さんのBlogの記事に紹介されていた Windows Runtime(略称がWinRT)なるものの存在を、これまで僕は知りませんでした。Win32 API なら名前だけは知ってましたが、どうやらそれより新しいAPI であるとのこと。難しいことはわかりませんが、このWinRTでPDFの画像化ができるのであれば、Windowsの機能を使ってそれが実現できるのですから、新規に何かライブラリを追加したりする必要がなく、それこそ理想的です。

ようやく発見したSwanman (id:tales)さんのBlogの記事を読んで、とりあえず、上記の内容だけは確実に理解できました。早速、アップロードされていたユニットとサンプルコードを有難くダウンロードさせていただき、Project2 のサンプルコードを実行(F9)してみました。すると・・・

[dcc32 致命的エラー] PdfDoc.pas(7): F2613 ユニット ‘WinAPI.Foundation.Types’ が見つかりません。


これは困りました。解決方法がまったくわかりません!

(APIだから、もしかしてMicrosoftさんのほうで、ここ数年のうちに何か変更があったのかな・・・)

pas、すなわち必要なユニットはすべてPathの通ったところに置きましたから、これはライブラリ自体が「ない」ということなのかな・・・?

(ない袖は振れないから・・・、ダメもとで外してみようか・・・)

唯一、思いついた解決方法にならない強引な戦法で前進?することに決め、問題の行をコメントアウト。幸い、誰もこのライブラリを参照していなかったようで、「未定義の識別子」エラーは表示されません。これはラッキー!とばかりに、このまま再度、実行(F9)します。

すると・・・、別のユニットでもうひとつ同じエラーが発生。

[dcc32 致命的エラー] WinAPI.Data.Pdf.pas(21): F2613 ユニット ‘Winapi.Foundation.Types’ が見つかりません。


もう、ムチャを承知で無理やり、前進! 前進!!
この行も // コメントアウト。こちらもラッキーなことに「未定義の識別子」エラーは表示されません。これ幸いと、再び、実行(F9)。

このプログラムが動かなくて困るのは僕だけです。他に誰一人、悲しい思いをする人はいません。それだけが唯一の救いです。

結果は・・・

何の問題もなく、
プログラムは無事に動作しました!

開いたPDF文書はページごとにJpeg画像として保存できました☆

PDFファイルの指定ページをJpeg画像として保存できました!


ちなみに Types の他にはどんなメンバーがいるのかと思い、WinAPI.Foundation をコピペして、n の後ろに「 . 」を入力してみると、入力補完機能が表示した選択肢は・・・

Collectionしかありませんでした


もちろん、誰もこのユニットを参照していないことは明らかなので、次のようにしても動作しました。

存在するからusesしてもエラーにはなりません・・・
(usesするとexeの大きさは少しだけ大きくなりました)


(もしかして、必要なユニットがまだ他にあるのかな・・・)

(でも、どこからも参照されていなかったし・・・)

(何で uses されてたんだろー?)


結局、Winapi.Foundation.Types の謎は解けませんでしたが、PDF文書ファイルの各ページをJpeg画像として保存したいという目的は実現することができました。

今回、僕が探した範囲ではDelphiを使ってPDF文書の画像化を実現する方法は、FireMonkeyでPDFiumを使う方法と、このWinRTを使う方法の二つしか見つけられませんでした。したがって、FireMonkeyという選択肢を選ばないのであれば、WinRTを使うこの方法しか、選択肢はありません。

そのような意味で、Swanman (id:tales)さんのBlogの記事にあった情報は大変貴重な情報であると思いました。Swanman (id:tales)さんにこころから感謝しております。ありがとうございました。

3.まとめ

(1)Delphiで、PDF文書の各ページを画像化する方法はある。
(2)FireMonkeyでPDFiumを使用。
(3)WinRTのAPIを使う方法もある(VCLで動作確認/FireMonkeyでの動作は未確認)。

今回の記事を書くにあたって、プログラムの動作確認に使用したPCの環境は次の通りです。

【デバイスの仕様】
プロセッサ 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz 3.00 GHz
実装 RAM 32.0 GB (31.7 GB 使用可能)
システムの種類 64 ビット オペレーティング システム

【Windowsの仕様】
エディション Windows 11 Pro
バージョン 23H2
OS ビルド 22631.3296
エクスペリエンス Windows Feature Experience Pack 1000.22687.1000.0

【Delphiのバージョン】
Embarcadero® Delphi 12 バージョン 29.0.50491.5718

4.お願いとお断り

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

マークシートの採点結果通知(個票)及び成績一覧表の作成

ただし、表計算ソフトは使わずに。

【ご案内】20240929追記

この記事で紹介した採点結果通知作成プログラムにはバージョンアップ版が存在します。使い方の詳細は、次のリンク先の記事をご参照ください。使用要件に同意していただければ、マークシートリーダーの最新版や、その他のデジタル採点プログラムのダウンロードが可能です。

※ 記事中の「マークシートリーダー」とは、僕がDelphiで作ったマークシートリーダーのプログラムのことです。その最新バージョンと、この記事で紹介するプログラム一式を同梱したZipファイルをダウンロードできます。

【はじめに】

マークシートを利用する際、マークをミスなく読み取れたら、次に読み取り結果を適切に処理する作業が待っています。読み取り結果をCSVファイルに出力し、表計算ソフトで作業するのが一般的な処理の流れだと思いますが、表計算ソフトに苦手意識を持つ方が多いのも事実です。

そこでアンケート集計用途ではなく、試験の成績処理用途専用という「但し書き付き」ですが、『表計算ソフトを使わない』・『入力作業は必要最小限度に留める』・『作業はほぼクリックするだけでOK!』というコンセプトを決めて、マークシートリーダーで読み取り処理を行ったマークシート方式試験の採点結果通知シート(=生徒さんに返却する答案)と成績一覧表(教科担任用)の作成にチャレンジしました。

完成までに、ほぼひと月を要しましたが、マークシートリーダーへの「後付け」成績処理プログラムが出来ました(僕自身が採点現場で動作検証を行った範囲では、期待通りに動作しました)。

マークシートリーダー(最新版を同梱)と合わせて、上記リンク先からデジタル採点プログラム一式をダウンロードできます。

【もくじ】

1.ダウンロードしたプログラムとその高速化について
2.マークシート画像に採点結果を出力
3.成績一覧表も作成
4.使い方
5.まとめ
6.お願いとお断り

1.ダウンロードしたプログラムとその高速化について

今回紹介する採点結果通知シート作成用のプログラム『ReportCard.exe』は単体では動作しません。動作には、僕のマークシートリーダーが作成したCSVファイル等が必要です。また、動作に必要なフォルダ配置その他の動作環境も、僕のマークシートリーダー専用に作成したものをそのまま利用していますので、僕のマークシートリーダー(MS_Reader.exe) が「確実に動作する環境」で実行していただく必要があります。

ここでは採点結果通知シート作成用のプログラム『ReportCard.exe』の動作検証が手軽に行えるよう、Python環境を除いたマークシート読み取りプログラム(最新版Version 1.1.4)に『ReportCard.exe』を同梱する形でダウンロード用zipファイルを作成し、(上に)掲載しています。もちろん、過去記事で紹介しているPython環境を組み込めば(・・・と言っても、ダウンロードして解凍したPython39-32フォルダをMS_Reader.exeがあるフォルダにコピペするだけですが)、マーク読み取り部分は、より一層高速に動作します。

Python環境:Python4Delphiを利用して Object Pascal に埋め込んだ Python Script を実行し、Python用の OpenCV でマークシートのマークの有無、マークした番号を読み取ります。(PCによっては)Python Engine の初期化になぜか?すごく時間がかかることもありますが、1回初期化すれば、どのPCでも大変高速に動作します。拙作マークシートリーダーの動作に必要なライブラリをすべてインストールしたプログラム埋め込み用の Embeddable Python 一式が下記リンク先からダウンロード可能です。

この「採点結果通知シート作成」プログラムも、Python環境があれば自動的にそれを利用して動作するように設計してありますが、テストしてみた結果で率直な感想を言うと、やはり初回起動時の(必須)Python Engine の初期化に(PCによりますが)かなり時間がかかる(数分!)ことがあります。

僕のPC:Panasonic製Let’s Note CF-QV ではそのようなことはまったく起きませんが、職場で使っているPCではそれが必ず起こります。

とにかく Python Engine の初期化に「それなりに時間がかかる」PCでこのプログラムを使う場合は、例えPython環境があっても、起動直後に画面左上の「✅P4D」のチェックを外し、Python環境を利用せずにプログラムを実行していただいた方が良いかもしれません。

【過去記事へのリンクです】

2.マークシート画像に採点結果を出力

採点結果通知シートのイメージは、こんな感じ(確認画面として表示する手続きは作成しましたが、画像データとして保存する手続きは「その必要なし」と考え、作成しなかったので、これは確認用画面のハードコピーです)。

シートの左上部分を切り取り
シートの右下部分(得点合計等はここに表示)


・・・ですので処理は、採点結果を画面に表示 → そのまま印刷という流れになります。採点の計算は一瞬で終わり、採点画面はすぐに作成できるから、データは保存しません(そもそも保存しておいて、何回も利用するようなモノではないと思いますから)。

まず最初に考えたのは(当たり前ですが)、マーク読み取り結果と配点をマークシート画像に出力(〇の場合は配点=得点となります)し、得点を観点別評価とともにシートの余白(設問番号付近)に表示することです。

正答ならば採点マークと配点(=得点)を表示
不正解の場合、採点マークと配点を表示


採点マークのサイズと水平方向の表示位置は微調整が可能です(ただし、調整結果を保存する機能はありません)。

採点結果の表示位置は、負の数で左・正の数で右に微調整可能

ここで、配点に加え、不正解の場合は正解も表示したくなりました。ただ、記号フォントに縦長の楕円はなかった?・・・と思うので、フォントは好みに応じて選択できるよう、思いつくままにいろいろ設定。

カタチ的には「θ」が最もマークの形状に近い気がします。


ふと、思い立って数字も選べるように設定。

「Num」を選択すると正解のマークの上に数字を表示します


あと、新教育課程では、観点別評価が導入されているので、観点別評価の「知識・技能」は K1、「思考・判断・表現」は K2 として評価の分類も出力できるように設定。正解マークと合わせて表示すると、こんな感じです・・・。

自分的には、コレがいちばん気に入りました!

正解マークの番号を、マークすべき場所に数字で表示する


得点合計と観点別評価ごとの得点合計は(デフォルト設定)シート右下に表示します。もちろん、フォントの大きさは任意の値を設定でき、表示位置は水平・垂直両方向に微調整が可能ですが、こちらも調整後の座標を保存することはできません。


フォントの大きさや表示位置の微調整は、凝り始めたらキリがなくなりそうで、それが表計算ソフトに代わる高い敷居となる可能性(=危険性)を感じ、デフォルト設定で(この程度でまぁいいか?)とユーザーに判断してもらえるよう設定値を調整しました。

すべて控えめな数値を設定しました!
足りない場合は、ちょっと増やせばOKかな?

3.成績一覧表も作成

これがないと採点結果を記録簿に転記し(ここで間違いが発生する可能性があります)、電卓をパチパチ叩いて平均点等を計算するか、一歩進んで、プログラムが出力したCSVファイルを表計算ソフトで処理して、成績一覧表を作成しなければなりません。

転記したり、電卓を使うのは昭和のスタイルだし、働き方改革の流れにも逆行します。CSVファイルを自由自在に操れる方なら、拙作マークシートリーダーには、マーク読み取り結果をCSVファイルに出力する機能を付けてありますから、そちらをご利用ください・・・ってことでOKかな?・・・なんだけれど、「表計算はちょっと苦手で」という方も少なくありません。

PCを使って何かの処理を行うこと自体が、手作業で行ってきた作業を効率よく自動化することに他なりませんから、・・・ほんとうのことを言えば、マークシートリーダーに付属の一機能として最初から成績一覧表の作成機能を付けたかったのですが・・・マークシートリーダー開発当初は、何よりもまず、確実にマークを読み取れることが最重要課題で、それが可能になった時点で実はもう僕自身が(精神的に)ヘトヘトになっていて、(読み取り結果をCSVファイルに出力できれば、あとは表計算ソフトで・・・)みたいな思い(と強い思い込み)があり・・・

新教育課程で導入された観点別評価も、プログラミングして処理するより、表計算ソフトで処理した方がずっと簡単そうに思えたし・・・

同僚からの要望に応え、マークシートリーダーとは別に作成した「手書き答案の採点プログラム」と、マークシートによる解答を併用した採点に対応する場合でも、表計算ソフトは便利だったし・・・

このような諸々の理由から先延ばしになっていた成績一覧表の作成でしたが、2024年、冬、ここで一念発起して、マーク読み取り後の処理に表計算ソフトを一切使わず、ソフトウェアの機能として必要な帳票を出力できるプログラムを書くことに決め、ダミーデータを使って動作確認をくり返し、不具合箇所を発見するたびに少しずつ手直しして、実際に使ってみてどうかという段階にたどり着いたのが、まさに今です。

ただし、どちらかと言えば「採点結果通知シートの方が主」で、成績一覧表は「読めればイイ」程度の、言わばメモみたいなもの・・・表計算ソフトが苦手な方でも、CSVファイルに出力された採点結果を表計算ソフトで開き、得点データを他のワークシートへコピペする作業は可能で、それさえ出来ればあとは協働作業で現場はなんとか動く・・・という勝手な理由で作りは大いに簡素化。

様々な理由から、氏名は「最初の3文字のみ表示」することにしました。
罫線も、横一線のみ。

(氏名と成績はダミーデータです)

ほんとにナイよりマシ・・・というレベルで完成。T_T

プログラムは技術的な知識不足から(だと思うのですが)、罫線が上手く描画されたり、(同じプログラムなのに)PCによっては罫線が予定位置に描画されなかったり・・・。この罫線が上手く描ける場合と、描けない場合の違いがいまだによくわからないのですが、次のようにして無理やり解決?(しましたが、最終的に問題のあるコードは全面的に書き直しました)

追記

罫線が予定位置に描画されない問題は、解決できていませんでした。罫線データの描画がおかしくなる場合は、罫線の描画なしでデータを出力してください。

【罫線描画問題解決用GUI の勇姿】

CheckBoxとButtonを一つずつ用意


(1)設定 → システム → ディスプレイ設定変更画面の表示を1クリックで行えるボタンを作成。非常の場合は、これで画面の拡大率を100%に戻してもらう。拡大率100%なら確実に予定の位置に描画されるハズ。

・・・と、思ったのですが、結論から言うとこれはダメでした!!

その後、奮闘努力して問題を解決 → (3)へ

ディスプレイ設定を呼び出すコードは1行でOK!

procedure TForm1.btnDispSettingClick(Sender: TObject);
begin
  //usesにWinapi.ShellAPIが必要
  ShellExecute(0, 'open', 'ms-settings:display', nil, nil, SW_SHOWNORMAL);
end;

(2)CheckBoxを利用して「罫線を描画しない」設定を用意する。チェックOFFだと・・・

ある意味では、究極ともいえる罫線問題解決方法。
(これは、ほとんどムチャですな・・・)


(3)罫線の描画に使っていたコードそのものを新たに書き直し、TImage の Canvas と TPrinter のCanvas それぞれに罫線を描画するようにしたところ、罫線が予期しない位置に描画されてしまう問題は解決できました。最初に書いたコードで、(PCにより)罫線が正しく描画される場合とされない場合がある、その本当の理由は未だにわかりませんが・・・

追記(20240929)

上の記事で紹介した採点結果通知個票及び成績一覧表を作成・出力するプログラムを改良し、デジタル採点プログラム一式に同梱して、次のリンク先で公開しています。

4.使い方

使ってくださる方がいるとも思えませんが、使い方のマニュアルは以下の通りです。

(1)プログラムを起動

「MS_Reader.exe」と同じフォルダにある「ReportCard.exe」をダブルクリックしてプログラムを起動します。

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

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


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

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

アメリカでは、採点結果を通知する個票のことを、高校段階までは “Report Card” と呼ぶそうです。Python4Delphiを使用していることを考えると、プログラムの名称に漢字を使用することは、極力、避けたいところです(これは、Pathに含まれる全角文字に関連するエラーに、Pythonスクリプトを書いていて、これまでさんざん悩まされた経験から)。

また、当初、アイコンは濃い目にデザインしたのですが、100 が赤だと目に痛い。そう、痛切に感じた経緯があって、通常アリエナイ色の 100点 をモチーフにしたアイコンにしました。Report Card の文字は、ほぼ読めませんが!「枯れ木も山の賑わい」とお考えいただけたら幸いです。

100 という数字さえ読み取れれば、何をするプログラムなのか?
お使いいただけた方には、わかってもらえるんじゃないかと・・・。

(2)「開く」ボタンをクリックして、ProcDataフォルダ内にある採点結果通知シートを作成したいクラス(or 講座)のマークシート画像を保存したフォルダを選択。

Python環境が利用できる場合は、P4Dに自動的にチェックが入ります。
※ Python Engine の初期化に時間がかかるPCでは、起動時にチェックをOFFにしてください。


選択するのは「ファイル」ではなく「フォルダ」です。


(3)採点結果通知シートを新規に作成する(既存の採点作業の設定ファイルがない)場合は、次の表示が出るのでOKをクリックし、設問数を入力して、画面左に表示されるGridコントロールに必要事項を入力します。


設問数を最初に入力します。


次に、作業の「入力」を選択(オプションボタンをクリック)します。


配点は最も多く設定する値をデフォルト配点として指定(入力)します。


正解とするマークの番号を入力します。

最初だけフォーカスを与えるために入力するGridをクリックしてください。


配点を変更する箇所があれば、正解に続けて入力します。
最後に観点別評価の区分を入力します。「知識・技能」は半角数字で 1 を、「思考・判断・表現」は半角数字で 2 を、それぞれ間違えないように入力してください。

m(__)m:「主体的に学習に取り組む態度」の評価は、この採点システムでは行えません。

観点別評価の入力を行っているところ。


全項目の入力が完了したら、入力に間違いがないことを必ず確認してください。もし、誤りがあれば、ここで確実に発見し、訂正しておかないと・・・、後から大変なコトに・・・。

必要事項をすべて入力し、内容を確認したら採点設定を保存します。

「MySettei.csv」が(上で指定した)マークシート画像のあるフォルダに保存されます。


保存が完了すると、次の確認メッセージが表示されます。

(4)採点ボタンをクリックして、採点を実行します。


表示されている画像の座標情報を記録したテンプレートを選択します。
(テンプレートの作成は、マークシートリーダーで実行)

テンプレート名をクリックして、決定ボタンをクリック。


適切な採点オプションを選択します。


「観点含全部」を選択した場合は・・・

採点記号と配点(正解の場合は得点)、観点別評価の区分を設問番号付近に表示
得点合計と観点ごとの得点合計を右下に表示
空欄と不正解の場合は、正解を表示


前述した通り、正解記号は選択肢から選択して指定できます。

Numを指定した場合は、正解マークの番号が表示されます

(5)画像の切り替え

表示している画像の切り替えはボタンクリックで実行できます。

ボタンは左から順に「先頭へ」・「一つ前へ」・「一つ次へ」・「最後へ」

(6)印刷

「印刷」ボタンをクリックして、採点結果通知シートを印刷します。


クリックすると表示されるメッセージに答えて、全員分 or 個別 印刷のいずれかを選択してください。


用紙の縦横指定を間違えないように注意してください。

(7)成績一覧表の作成

最初に「学年」と「クラス」を選択してください。


選択制の授業等、特別な編成(=「講座」と表現)の名票は出席番号順・氏名のみのデータを予めsNameフォルダ内に分かりやすい名前を付けて、CSVファイルで準備してください。

学年・組は空欄のままにして、「講座名票」ボタンをクリック


ファイルの選択ダイアログが表示されるので、予め作成・保存しておいた講座の名票を選んでOKをクリックしてください。採点結果一覧がGridコントロールに表示されます。

得点等はダミーデータです。


続けて、平均点を正しく計算するため、未受験者の処理を行います。「編集」チェックボックスをチェックしてください。


未受験と思われるデータがある場合、次のメッセージが表示されます。

テストを受験しており、採点結果が0点の場合は「いいえ」をクリックしてください。


得点「0」はすべて未受験として処理した場合、採点結果の一覧は次のようになります。

未受験者のデータを空欄に変更。


「再計算」ボタンをクリックして、平均点等を更新します。


続けてプレビューをクリックするよう案内が出ます。
プレビューをクリックして成績一覧表を表示します(設定はA4・縦、50名/枚で、この設定を変更することはできません)。

なお、受験者数が51名以上の場合でも、プレビュー画面には最初の1枚目の成績一覧表が表示されます。また、任意のページをプレビュー画面に表示する機能は、このバージョンにはありません。

印刷されるデータをプレビュー画面で確認してください。


プレビューに問題がなければ、プレビューのチェックをOFFにして(外して)ください。
印刷ボタンがクリックできるようになります。

印刷ボタンをクリックすると、プリンターへデータが送信されます。受験者数が50名を超える場合は、プリンターへのデータ送信後、印刷最終ページが画面に表示されます。

5.まとめ

今回、拙作マークシート・リーダーのCSV出力を利用するかたちで作成したプログラム(新教育課程観点別評価「知識・技能」及び「思考・判断・表現」の評価に対応)の概要は以下の通りです。

(1)表計算ソフトを使わずに、マークシート方式試験の採点結果通知(個票)を作成。
(2)表計算ソフトを使わずに、マークシート方式試験の成績一覧表(教科担任用)を作成。
(3)マークシート方式試験の成績一覧表をCSVファイルに出力。

6.お願いとお断り

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

0.1ずつ増減したい!

画像をほんの少しだけ回転させるプログラムを書いた。そこでの角度の指定は、最大でも1°未満を想定(整数値だと大きすぎ)。そこで、回転角度の値を指定するTEditへ、キーボードから直接入力する場合は、0.01刻みでのインプットを可能としたが、TEditとTUpDown等の数量を扱うVCLを組み合わせての入力、つまり、マウスで▲・▼マークをカチカチクリックして値を増減させて入力する場合には、TEditに設定する値は「0.01」刻みでは小さすぎるから、「0.1刻みで増減」させようと思った・・・んだけれど、これが難しかった。

いちばんの問題はコレ(差し引きゼロなら、ゼロと表示すること)
整数値なら、なんでもないことなのに・・・

【訂正】 これが難しかった → これが、(僕には)難しかった。

最初はTEditとTUpDownを組み合わせて、これを実現しようとしたが中々上手く行かず、仕方がないから、TEdit1個とTButton2個を組み合わせて、なんとか当初の目的を実現。その後、当初、気がつきませんでしたが、TEditとTUpDownの組み合わせで、Float値を増減させる方法も見つけました。これはその覚え書きです。

【もくじ】

1.TEditとTUpDownでチャレンジ
2.TEditとTButton2個でチャレンジ
3.増減値が0にならない理由
4.コードを修正
5.0.1ずつ増減
6.まとめ
7.お願いとお断り

1.TEditとTUpDown

とりあえず定番と思われるTEditとTUpDownの組み合わせ。

FormにTEditを1つおいて、Textプロパティに0を代入。
TUpDownも1つ用意。

UpDown1のAssociateプロパティにEdit1を設定

Edit1にUpDown1がくっついた!

くっつく位置は左右いずれかを指定できるらしい。

udRightなら右にくっつく

画像の回転が目的だから、とりあえずUpDown1のMaxプロパティは90、Minプロパティは-90に設定。
(実際に使用する値は1°未満の予定)

MInプロパティを0のままにして設定し忘れると、大変なコトに・・・
(負の数が入力できなくなります)

で、Edit1の値を0.1ずつ増減させる「UpDown1Click手続き」を次のように作成。

procedure TForm1.UpDown1Click(Sender: TObject; Button: TUDBtnType);
var
  Value: Real;
begin
  //注意:このコードは、期待通りに動作しません
  Value := StrToFloatDef(Edit1.Text, 0);
  case Button of
    btNext: Value := Value + 0.1;
    btPrev: Value := Value - 0.1;
  end;
  Edit1.Text := FloatToStrF(Value, ffNumber, 1, 1);
end;

実行(F9)して、▲ボタンを3回クリックしたところ

クリック1回目
クリック2回目
クリック3回目

1ずつ、増加してる・・・。

なんでかなーって、ちょっと思ったけど、UpDown1のincrementプロパティを見て納得。

Incrementがデフォルト「1」になってる・・・

足したり、引いたりしている 0.1 はどこに消えたのか・・・?

とりあえず、原因の一つはコレだ☆
やりたいのは「1」ずつじゃなくて「0.1」ずつ増減だから、そう!

Increment プロパティを 0.1 に変えてみた *(^_^)*♪

そしたら、Delphiに怒られた (T_T)

ひー(心の悲鳴)

Incrementプロパティには、整数しか設定できないようだ。

整数・・・ そぉか、0ならOK?
Value(=Editに表示されている値) に 0.1足したり、引いたりしてるから、これでイケる?

Increment 0 を設定 やった! これで完璧だ☆

実行(F9)すると・・・

▲・▼どっちを何回クリックしても「0」のまま・・・

Value + 0.1や、Value – 0.1はどこに消えた?

1.1 とかになってたから、完全に無視されてるわけでもなさそうだけど・・・???

Increment プロパティが整数値指定だとわかった時点で、なんか嫌な予感がしたんだよなー
他にも、Maxとか、Minとか、入力を制限する値(こちらも整数値で指定)もあるし・・・

ここで謎を追いかけて、無駄に時間を使うより、TUpDownは「整数値専用」と決めて、他の方法を試すことに決定。※ 実際、TEditとTUpDownの組み合わせで、小数値の増減も可能です(後述

2.TEditとTButton2個でチャレンジ

Edit2の右に、Buttonを2つ置いて、こんなふうにしてみた。

見た目の美しさは二の次。目標の実現が最優先。

で、コードは Copilotさん に教えてもらった☆ その質問内容と答えのコードがこちら。
(コード部分はコピー可能だったから著作権的な問題はないと判断)

Q:DelphiでTEditの値を0.1ずつ増減するにはどうしたらいいですか?

A:Copilotさんが教えてくれたコード。

procedure TForm1.Button1Click(Sender: TObject);
var
  Value: Double;
begin
  if TryStrToFloat(Edit2.Text, Value) then
  begin
    Value := Value + 0.1;
    Edit2.Text := FloatToStr(Value);
  end
  else
    ShowMessage('Invalid number');
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  Value: Double;
begin
  if TryStrToFloat(Edit2.Text, Value) then
  begin
    Value := Value - 0.1;
    Edit2.Text := FloatToStr(Value);
  end
  else
    ShowMessage('Invalid number');
end;

これなら間になんにも入らず、直接Editを編集してるから、絶対、大丈夫 だろ?
最初からコレでよかったんだ・・・ みたいな気持ちで、コピペして編集し、実行(F9)。

▲をカチカチ、2回クリックしました。イイ感じです!

で、減算(▼クリック)は、初期値が上の 0.2 のところから始めると・・・

クリック1回目。そうそう、それでOK!

次の▼クリックで、値は「0」になるはず。

ところが・・・

クリック2回目。

はぁ? 0.1 – 0.1 = 0.0(ゼロ)になるはずなんだけど・・・
ナニ、コレ?

クリック3回目。これはOKなんだけど・・・

3.増減値が0にならない理由

そうでした。コンピュータは小数の演算が苦手でした。

僕自身、前にさんざん苦しみました。なんで上の計算が「0」にならないのか?

その理由は、次の記事をご参照ください。

Win11の23H2より前のバージョンなら、正しいと思える答えにたどり着くまでGoogle先生に質問を繰り返すのがこれまでの問題解決の定番と言える方法でしたが、今はとなりにCopilotさんがいてくれます。

期待を込めて、訊いてみました。

Q:Delphiで0ではなく、5.54975987041018E-18と表示されます

以下、Copilotさんの返答の概要です。

・浮動小数点数(DoubleやSingle型など)は完全な精度で表現されない。
・これは、すべてのプログラミング言語で共通の問題。
・解決策の一つは、ある閾値(epsilon)よりも結果が小さい場合は 0 と表示。

完全に納得。

Copilotさん、優秀!
ぼく、きみのファンになりました☆

で、以下、Copilotさんが教えてくれた閾値を設定したコードです(コメント文は短縮)。

var
  Value: Double;
  Epsilon: Double;
begin
  Epsilon := 1E-15;  //閾値を設定
  Value := SomeCalculation();  //計算を実行
  if Abs(Value) < Epsilon then
    Value := 0;
  Edit1.Text := FloatToStr(Value);
end;

4.コードを修正

Copilotさんが教えてくれたコードを読んで、「0.0」と表示されるように修正しました。

procedure TForm1.Button3Click(Sender: TObject);
var
  Value: Double;
  Epsilon: Double;
begin
  Epsilon := 1E-15;  //閾値を設定
  if TryStrToFloat(Edit3.Text, Value) then
  begin
    Value := Value + 0.1;
    if Abs(Value) < Epsilon then
    begin
      Value := 0;
      Edit3.Text := '0.0';
    end else begin
      Edit3.Text := FloatToStr(Value);
    end;
  end;

end;

procedure TForm1.Button4Click(Sender: TObject);
var
  Value: Double;
  Epsilon: Double;
begin
  Epsilon := 1E-15;  //閾値を設定
  if TryStrToFloat(Edit3.Text, Value) then
  begin
    Value := Value - 0.1;
    if Abs(Value) < Epsilon then
    begin
      Value := 0;
      Edit3.Text := '0.0';
    end else begin
      Edit3.Text := FloatToStr(Value);
    end;
  end;
end;

ゼロの時は、’0’ではなく、’0.0′ と文字列指定しているところに「こだわり」ました!

5.0.1ずつ増減

上記コードの実行結果です☆(初期値は 0.2)

▼をクリック
▼をクリック
▼をクリック

閾値が効きました☆

逆(▲クリック)も正しく動作することを確認。

【TEditとTUpDownの組み合わせで実現】

TUpDownのMax及びMinプロパティの値(特にMin)を適切な値に設定し、適切なコードを記述することで、TEditとTUpDownというVCLコンポーネントの組み合わせでも 0.1 刻みの選択肢設定が可能であることを確認しました。その方法は以下の通りです。

※ オブジェクトインスペクタで Increment プロパティの値を予め設定しておくという前提で、

【重要】 負数の入力を想定する場合は、TUpDownのMinプロパティの値に適切な負数を設定します!

【重要】 Increment プロパティに 1 を設定 → ▲をクリック → 値が 0.1 ずつ増加

ちなみに Increment プロパティに 2 を設定した場合は、値が 0.2 ずつ増減します。

procedure TForm1.UpDown1Click(Sender: TObject; Button: TUDBtnType);
begin
  Assert(Sender is TUpDown);
  with TUpDown(Sender) do
  begin
    Assert(Associate is TEdit);
    TEdit(Associate).Text := FloatToStrF(Position / 10, ffNumber, 1, 1);
  end;
end;

上記コードの出典は、こちら

Delphi-PRAXiS

https://www.delphipraxis.net/143779-tupdown-floats.html

貴重な情報をご教示くださいましたこと、投稿者様に対し、心より感謝申し上げます。

上のコードを参考に、使っているVCLの名前を指定して、もっと短くして書けば・・・

procedure TForm1.UpDown1Click(Sender: TObject; Button: TUDBtnType);
begin
  Edit1.Text := FloatToStrF(UpDown1.Position / 10, ffNumber, 1, 1);
end;

増減値に 0.1 ではなく、0.2 以上の値を設定したい場合、Increment プロパティの値は予めオブジェクトインスペクタで指定しておいた方が良いようです。

ちなみにオブジェクトインスペクタのIncrement プロパティに設定してある値は「1」のまま、コードで「2」を指定した場合は・・・

procedure TForm1.UpDown1Click(Sender: TObject; Button: TUDBtnType);
begin
  UpDown1.Increment := 2;
  Edit1.Text := FloatToStrF(UpDown1.Position / 10, ffNumber, 1, 1);
end;

実行(F9)してみるとエラーにはなりませんが、最初の▲押し下げ時に 0.2 ではなく、0.1 と表示されます。また、実行(F9)後、最初の▼押し下げ時には -0.2 ではなく、-0.1 と表示されます。それ以降(▲・▼2回目押し下げ以降)は正しく表示(意図した通り表示)されます。

オブジェクトインスペクタのIncrement プロパティに設定してある値が「1」のまま、コードで値を設定・変更する場合は(実行途中での変更はできませんが)FormCreate時に設定しておけば期待通りに動作するようです(もし、違っていたら、ごめんなさい)。

procedure TForm1.FormCreate(Sender: TObject);
begin
  UpDown1.Increment:=2;
end;

procedure TForm1.UpDown1Click(Sender: TObject; Button: TUDBtnType);
begin
  //UpDown1.Increment:=2;
  Edit1.Text := FloatToStrF(UpDown1.Position / 10, ffNumber, 1, 1);
end;

PCの画面で確認すれば、設定状況がわかりやすいと思います。

この設定なら期待通りに 0.2 ずつ増減しました


以上が、TEditとTUpDownの組み合わせで小数値を増減させる方法のまとめです。

6.まとめ

(1)TEditとTUpDownの組み合わせで0.1刻みの数値の増減は可能。
(2)TUpDownで負の数を扱う場合は、Minプロパティの値も適切に設定する。
(3)TEditとTButtonの組み合わせで小数値の増減計算を行う場合は値「0」に注意する。

7.お願いとお断り

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

IMEでかな漢字変換すると文字が白くなる・見えない!

2024年2月上旬頃からか?
テキストボックス等を置いて、その中へ文字列を入力すると、かな→漢字変換するためのスペースキー押し下げで、変換候補の文字が白くなり、見えなくなる現象が発生。

PCを壊すような操作は、「なんにもしてない」から、これは(お決まりの?)Windows11のアップデートに伴う不具合かと、推測。

IME(Input Method Editor)の設定を変更して、とりあえず、安定動作するオールド・バージョンで、その場しのぎをしてみました☆

【もくじ】

1.かな→漢字変換がヘン!
2.IMEの設定を変更
3.まとめ
4.お願いとお断り

1.かな→漢字変換がヘン!

2024年2月上旬頃(?)から、テキストボックス等を置いて、その中へ「かな」入力して、スペースキー押し下げ、変換する漢字の候補が表示されるシーンで、候補の画面が全部白色で表示され、現在表示されているはずの変換先の文字列が見えなくなる現象が発生。

2023年12月にクリーンインストールしたばかりのWin11なのに、もう壊れたの?

・・・みたいな気もしたけど、なんか、似たような現象が前にもあった気が。
IMEの機能が更新されて不具合が出ているなら、確か、更新されたIMEを使わない設定があったはずと、思い返し、Win11の設定を変更して様子をみることに。

2.IMEの設定を変更

IMEの設定を変更する前の状況は、次の通り(ここへきてくださったみなさん、同じだと思いますが)


Wordで普通に入力している時は、何でもないんだけれど、テキストボックス等を置いて、その中へ文字列を入力すると、かな入力後のスペースキー押し下げで変換先候補の文字が白くなって消える・・・みたいな問題が発生。Enterキーを押して確定するまで変換先の文字列候補は見えなくなる。

困った・・・。

確か、バージョンアップされたIMEを使わない設定があったはず・・・と思い返し、設定を次のように変更。

① スタートボタン ⇨ 設定
② 左ペインの「時刻と言語」をクリック
③ 右ペインの「言語と地域」をクリック
④ 「日本語」項目の右側にある「・・・」をクリック
⑤ 下へスクロールして、「Microsoft IME」の項目の右側にある「・・・」をクリック
⑥ 表示されるサブメニューで「キーボードオプション」を選択(クリック)
⑦ いちばん上の「全般」をクリック
⑧ 下へスクロールして「互換性」の「以前のバージョンのMicrosoft IMEを使う」を「ON」に変更

デフォルト設定は「オフ」


機能上、そりゃデフォルト設定は「オフ」でなきゃいけませんね・・・。これがデフォルト「オン」なら最新版は永遠に使えません。

「以前のバージョンのMicrosoft IMEを使う」を「オン」に変更


⑨ 「オン」に変更した瞬間、次のメッセージが表示されるので、「OK」をクリック。


⑩ 設定の画面右上の閉じるボタンをクリック。

スペースキー押し下げで、変換候補が表示される以前の設定に戻ったことを確認!

3.まとめ

(1)IMEの不具合を感じたら、IMEの設定を安定動作した以前のバージョンに戻す。
(2)Win11の更新情報をこまめにチェック。
(3)不具合が解消されたらIMEの「互換性」の設定を元に戻す。

4.お願いとお断り

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

フローティングパネル

浮かんだままがいいときもあったりして!

【もくじ】

1.フローティングパネル
2.ドッキングさせたくない
3.メモリが解放できない
4.メモリが解放できた(その1)
5.メモリが解放できた(その2)
6.まとめ
7.お願いとお断り
8.【追記】

1.フローティングパネル

手書き答案の採点プログラムで、常にフォーム上に浮かべたまま、自由にその位置を変更可能な、作業補助用のGUI(フローティングパネル)が欲しくなった。

前回、縦書き答案の採点用に作成した横スクロールのフローティングGUIはControlBarの上にToolBarを置いて、その上にToolButtonやBevelを並べたけれど、今回はその名の通り、ベースとなるVCLはTPanelを選択。この上に効率よく採点作業を行うために最低限必要なButton他を置いて、ユーザーの視線やマウスの移動量を極めて限られた範囲に限定。操作に払う注意とストレスをできるだけ軽減して、より快適な作業環境を実現したい・・・。

そう考えて作成したGUIがコレ。

搭載した機能は、ほんとに必要最低限

画面をタッチして操作することも考え、各Buttonの高さは44ピクセルに設定。
これを設問毎に、答案画像から切り抜いた画像を並べて表示したTImageのとなりに表示する。

フローティング状態だとTPanelの上に「閉じる」ボタンのあるタイトルバー?(キャプションバー?)が表示される。この部分をクリックして、そのままドラッグすれば、TPanelをForm上の任意の位置へ移動できる。ただし、Formにドッキング(FormのDockSiteプロパティをTrueに設定)した瞬間、タイトルバー?はかき消されたかのように消えてしまう・・・

Formにドッキングしない状態ではPanelの上部にタイトルバーのようなものが表示される


以前作った(縦書き答案の採点を効率よく行うために左右方向のスクロールを行う)ドッキング・コントロールは、ドラッグ&ドロップでFormにドッキングするように設計。必要であれば、Bevelをクリックしてそのままドラッグすることでドッキングを解除。任意の位置へ移動できる仕様とした。そんなに頻繁に位置を変更するようなモノでもなかったし。

ところが、今回はFormにドッキングさせると、いろいろ不都合が起きることが判明。

例えば、解答欄の大きさ(特に幅)に合わせて、この入力補助Toolの位置を頻繁に変えるような場合、クリックしてそのままドラッグできる「タイトルバー(キャプションバー)」がないと極めて不便。Formにドッキングした瞬間にこれが消えてしまうと、掴みどころがなくてほんとうに困るのだ。

Formにドッキングして、掴みどころがなくなった状態(フローティングしてないパネル)


それから、Formへドッキングしている状態から、引きはがしてフローティング状態になるときの挙動がとにかく急で! うまく文字に表現できないのだけれど、感覚的には「うわっ」て感じ。GUIをクリックした瞬間に、「びっくりして飛び起きる」イメージでフローティングするのだ。

(数学風?に言えば、GUI上のクリックした位置が、GUIの左上の座標原点(0,0)の位置になるように、Button等のコントロールを載せているPanelの左上隅が瞬時にクリックした位置へ移動する)

また、最初の解答欄を表示している場合、1つ前に戻る「◀」ボタンはEnabledプロパティをFalseに設定し、クリックできないようにしてあるが、この Enabled:= False 状態のボタンをクリックすると上で述べたGUIが「びっくりして飛び起きる」ように瞬間移動する現象がおきてしまう。

さらに(原因はわからないけれど)、縦書き答案の採点時にFormにドッキングさせると、解答欄の表示が部分的にずれて二重に表示されてしまう(ドッキングさせなければ、この現象は起こらない)。

あれや、これやで、どぉーにもドッキング時の挙動が気に入らない。

じゃあ、
ドッキングさせなければいいじゃん!

・・・

そのとおり・・・

そのとおり、なんだけど・・・

そうすると・・・ コレが ・・・

フローティング状態で位置を変更するたびに、メモリーリークが・・・

コレが・・・ どぉーしても 消えなくて・・・

T_T

2.ドッキングさせたくない

メモリーリークの原因はわかってる。フローティングさせたTPanelをドラッグし始めた時に発生するStartDock手続きでCreateしているTToolDockObjectだ。

procedure TFormXXX.PanelXStartDock(Sender: TObject;
  var DragObject: TDragDockObject);
begin
  DragObject:= TToolDockObject.Create(Sender as TPanel);
end;

実は、これがなくても、フローティング動作は出来る。出来るんだけど、見た目に問題があって、高解像度画面で表示倍率を200%拡大のように設定している場合、ドラッグ(=移動)中は灰色の枠だけのフローティングパネルのゴースト?が現れる。(画面のハードコピーがうまくとれなかったので)ゴースト?は、次の図のような感じ。

ドラッグ時に現れるフローティングパネルのゴースト?
(これは練習用に作ったプログラムの実行時画面)


そして、このゴーストが目に痛い感じでちらつきながら移動する。さらに困ったことに、ドラッグ中はフローティングパネルの本体(ゴースト?でない方)は表示されない。ドラッグしてドロップした瞬間に、まるでテレポーテーションしたかのように(突如として)ドロップした位置にパネルが出現するのだ。

ただし、メモリーリークは起きない。
(なんにもCreateしていないから、起きるわけがない)

一方、メモリーリークを起こすとわかっていても、ドッキング可能なコントロールのドラッグ操作を専門的に管理するTToolDockObjectをCreateして動かすと、動きがたいへんスムーズ! 気持ちイイ☆

だから、どうしてもドッキングさせたくない!
フローティングさせたまま、使いたい!!

  DragObject:= TToolDockObject.Create(Sender as TPanel);

そうなると、この1行はどうしても削れない・・・。

ただし、裏側では「メモリーリーク」がフローティングさせたパネルをドラッグする(=位置を変える)度に、発生・・・

表面的には何事もなく、静かなんだけど。

3.メモリが解放できない

解決策はただひとつ。

ドラッグが終了した時点で、StartDock時に確保したメモリを解放すればいい。

ところが、FormのDockSiteプロパティがFalseのままだと
それが、どうにも、こうにも、難しい・・・

ちなみに次のように書いてみたんだけど、上の条件下では、これは、どうやら無効!!!のよう。

procedure TFormXXX.FormDockDrop(Sender: TObject;
  Source: TDragDockObject; X, Y: Integer);
begin
  if IsDragObject(Source) then
  begin
    Source.Free;
  end;
end;

procedure TFormXXX.FormDockOver(Sender: TObject;
  Source: TDragDockObject; X, Y: Integer; State: TDragState;
  var Accept: Boolean);
begin
  Accept:=IsDragObject(Source);
end;

OnDockDropは、「別のコントロールがコントロールにドッキングした際に発生」つまり「DockSite が True の際にのみ、発生する」ということで、そもそもFormのDockSiteプロパティは意図的にFalseに設定してあるんだから、OnDockDropイベントが起きるわけがない

OnDockOverもおんなじで、「DockSite が True の際にのみ、発生する」とのこと。100万回ドラッグしようとFormのDockSiteプロパティがFalseである限り、OnDockOverイベントも絶対に起きない。

ダメ元で、OnDragDrop手続きと、OnDragOver手続きを作成して上と同じコードを書いてみたんだけど、手続きの引数をよく見たら・・・

procedure TFormXXX.FormDragDrop(Sender, Source: TObject; X, Y: Integer);

Sourceが、TDragDockObjectじゃなくて、TObjectになってる・・・。ってコトは、こっちで受け取るためには、型キャストが必要ってコト? なのかなーって思いつつ、FormのDockSiteプロパティをFalseに設定したまま、型キャストなしで次のコードを書いてみたが、Panelをドロップしても反応がない。どうやら、この2つのイベントは、FormのDockSiteプロパティがFalseだとTToolDockObjectのドロップに対しては発生しないようだ。⇦ 間違いだったら、ごめんなさい!

procedure TFormXXX.FormDragDrop(Sender, Source: TObject; X, Y: Integer);
begin
  if IsDragObject(Source) then
  begin
    Source.Free;
  end;
end;

procedure TFormXXX.FormDragOver(Sender, Source: TObject; X,
  Y: Integer; State: TDragState; var Accept: Boolean);
begin
  Accept := IsDragObject(Source);
end;

その他、FormのDockSiteプロパティをFalseに設定したまま、フローティングさせたPanelをドッキングさせずにドロップ時にメモリを解放する方法はないかとさんざん 悩んだが、解決策が見つからない。まさに七転八倒。終いには、ナニをどういじったらそうなったのか、自分でもわからないのだけれど、フローティング状態のパネルをクリックしただけで一般保護違反のエラーが出るようになり、元に戻せなくなってしまった・・・。

フローティング部品がない状態のバックアップをとっていて、ほんとうによかった。

【これが間違いであっても、前に進むために出した、自分なりの結論】

TToolDockObjectを使いたいならOnDockDropとOnDockOverイベント側で、ドラッグ&ドロップを受け取るしかない。

4.メモリが解放できた(その1)

何かをCreateして使うプログラムを書くとき、FormCreate時に、次のようにメモリーリークがあれば検出する設定を僕は付け加えることに決めている。多数の画像を読み書きする答案処理のプログラムを書いた際に、Createしたオブジェクトの解放を書き忘れ、あとからCreateしている箇所を全部点検することになった「痛い経験」から学んだ予防的措置だ。

procedure TFormXXX.FormCreate(Sender: TObject);
begin
  //メモリーリークがあれば検出
  ReportMemoryLeaksOnShutdown:=True;
end;

メモリーリークがあった場合、実行(F9)したプログラムを終了させると(メインフォームを閉じると)、次の画面がリークを起こした回数付きで表示される。

回数なんか数えなくていいから、メモリーリークを止めてくれ!!

丸1日がんばって、(この方法じゃダメなんだ)ということがはっきりわかったところで、少し休もうと思い、ベッドに倒れて・・・ そのまま、眠ってしまったようだ。

なんにも考えない時間が数時間あって・・・

目覚めたのは日付が変わる直前。眠って疲れがとれたからか? その原因はわからないけど、目覚めた瞬間に、あることを思いついた。それは何かと言うと・・・

FormのDockSiteプロパティをTrueに設定すれば、OnDockDropとOnDockOverの2つのイベントが間違いなく? Panelのドラッグ&ドロップに反応してくれるから・・・

ドラッグしてるときだけ、FormのDockSiteプロパティをTrueに切り替えて、ドロップした瞬間の位置座標を取得し、そこへPanelをManualFloatさせて、最後にメモリを解放、DockSiteプロパティをFalseに戻せばいいんじゃない? ってこと。

いろいろ実験的に書いていて(プロパティをあちこち変更)、設定忘れがあると困るので、ドラッグ開始時、コードの中でFormのDockSiteプロパティをTrueに設定。

procedure TFormXXX.PanelXStartDock(Sender: TObject;
  var DragObject: TDragDockObject);
begin
  //これでちらつかなくなった
  DragObject:= TToolDockObject.Create(Sender as TPanel);
  //設定し忘れないための予防的措置
  if not FormXXX.DockSite then
  begin
    FormXXX.DockSite:=True;
  end;
end;

ドロップ時のOnDockDropイベントは・・・

procedure TFormXXX.FormDockDrop(Sender: TObject;
  Source: TDragDockObject; X, Y: Integer);
var
  r:TRect;
begin
  if IsDragObject(Source) then
  begin
    r.Left:=X;
    r.Top:=Y;
    r.Right:=X+PanelX.Width;
    r.Bottom:=Y+PanelX.Height;
    PanelX.ManualFloat(r);
    //解放
    Source.Free;
    if FormXXX.DockSite then
    begin
      FormXXX.DockSite:=False;
    end;
  end;
end;

FormのDockSiteプロパティはFalseに設定。

実行(F9)して、フローティングパネルをドラッグ&ドロップ。ちょっと気になったのはドロップ時のManualFloat(r)時の挙動。ドロップした場所でなく、ドロップ時にマウスポインタのカーソルがあった位置へ移動して表示されてしまう。次の画像の上がドロップ時、下がマウスの左ボタンを離した際にフローティングパネルが表示される位置。

マウスのカーソル位置を取得しているから、パネルのドロップ位置から少しずれて表示される。


ドロップ時、XにはPanelのLeftの値、YにはPanelのTopの値を取得するよう、プログラムを修正。


    r.Left:=PanelX.Left;
    r.Top:=PanelX.Top;
    r.Right:=r.Left+PanelX.Width;
    r.Bottom:=r.Top+PanelX.Height;
    PanelX.ManualFloat(r);
    //解放
    Source.Free;

これで表示位置に関する問題は解決できたが、今度は表示される際の挙動が気に入らない。ドロップ位置でPanelの画像が一瞬、最小化され(閉じるボタンだけになり)、それから全体が表示されるので、ドロップするたびにPanelが1回点滅するように見えるのだ。

それはそれで何とかするとして、いちばん何とかしなきゃいけないのはメモリーリーク。

恐る恐るプログラムを終了。

DelphiのIDE画面だけが表示され、リークの警告画面は出ない!
ようやく問題を解決できた・・・。

ただし、一難去ってまた一難。
表示方法を何とかしなきゃ・・・

5.メモリが解放できた(その2)

点滅の原因はわかっている。ドロップ時の ManualFloat(r) だ。

  DragObject:= TToolDockObject.Create(Sender as TPanel);

メモリーリークは起きるけど、上の一行だけでフローティングさせ、ドラッグ&ドロップしている時は点滅なんてしなかった。要は ManualFloat させなければいいのだ。

(かならず、解決方法はある)

そう信じて、OnStartDockイベントの手続き部分でShift+Ctrl+1、OnDockDropイベントの手続き部分でShift+Ctrl+2を実行して、Ctrl+1、Ctrl+2でそれぞれの手続きへ移動できるように設定。

2つの手続き間を行きつ戻りつしながら解決方法を考える・・・

どう考えてみても、OnDockDropイベント側でなんとかするのは無理そう・・・

TToolDockObjectをCreateして移動させてる時は、ドロップ時に点滅しなかった・・・

表示された位置も、ドロップした場所だった・・・ だから、ManualFloat は不要?

確保したメモリを最後に解放すれば、メモリーリークは起こらない・・・

『最後に解放』?

そうだ「 try ・・・ finally ・・・ end; 」があった。

プロシージャを思った通りに書き換える。

procedure TFormCollaboration.PanelXStartDock(Sender: TObject;
  var DragObject: TDragDockObject);
begin
  DragObject:= TToolDockObject.Create(Sender as TPanel);
  try
    if not FormXXX.DockSite then
    begin
      FormXXX.DockSite:=True;
      Application.ProcessMessages;  //おまじない
    end;
  finally
    DragObject.Free;  //メモリの解放
  end;
  FormXXX.DockSite:=False;
end;

実行(F9)

フローティングパネルは快適に移動し、ドロップ時の挙動もごく自然・・・

祈りながらFormをClose.

見慣れたIDEだけが現れる。

メモリーリークは 起きてない。

やっと、思っていたとおりの ・・・ プログラムになった?

・・・ってか、ちょっと待て。オレ、OnDockDrop手続きの ManualFloat 消してないぞ。

もしかして・・・ OnDockDrop手続き 呼ばれていないんじゃないか?

自分の書いたプログラムを、もう一度、よく読んで考える・・・

  DragObject:= TToolDockObject.Create(Sender as TPanel);
  try
    if not FormXXX.DockSite then
    begin
      FormXXX.DockSite:=True;
      Application.ProcessMessages;  //おまじない
    end;
  finally
    DragObject.Free;  //メモリの解放
  end;

DragObject を、最後に Free してるから、ドロップ時にはもう TToolDockObject は消えてる・・・。
消えてるんだから・・・ もう、『ない』んだから、呼ばれてないんじゃなくて・・・

OnDockDropイベントは起こらない!

・・・ってコトは、もしかして

DragObject:= TToolDockObject.Create(Sender as TPanel);
try
  //何もしない
Finally
  DragObject.Free;
end;  

Createして、Freeするだけで

よかったの?

6.まとめ

今回の状況を何かに例えるならそれはナニ?って、生成AIに訊ねたところ、

 “Still waters run deep”

そう答えてくれました。深く、静かに、感動。

7.お願いとお断り

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

8.【追記】

  finally
    //DragObject.Free;  //メモリの解放のみ->インスタンス(オブジェクトの実体)はあるので参照可能
    FreeAndNil(DragObject);  //インスタンスも消える!
  end;

.Freeでなく、FreeAndNilすると、何もしなかったコトと同じになってしまいます。
注意してください!!

デバッグ出力: clientcore\windows\advcore\ctf\shellhandwriting\client\handwritingclient.cpp(287)\Msctf.dll!77A3FC28: (caller: 77A345FD) LogHr(3) tid(988) 8007007E 指定されたモジュールが見つかりません。

Delphiでプログラムを書いて実行(F9)すると、必ずこれが・・・。

さんざん、悩みましたが、
ようやく原因と解決方法がわかりました。

【もくじ】

1.再発
2.Copilotさんが言うには・・・
3.ViVeToolをダウンロードして実行
4.まとめ
5.お願いとお断り

1.再発

以前、こちらの記事で「問題は解決した」と書きましたが、OSからクリーン・インストールし直したPCで同じ現象が・・・いつの間にか、また発生するようになり・・・


この内部エラーだけは再現してくれなくても、全然イイんだけど。ほんとに・・・ T_T

だって、解決策の糸口もつかめないから。

それに、なんで再発すんの???

Msctf.dllをキーワードにGoogle先生にお伺いをたてても全然埒があかないし・・・

その前の handwritingclient.cpp ? 手書きクライアント? MyPCにそんなアプリあった?

アルワケナイ。

ってか、拡張子が cpp って、コレ、C++のファイルでしょ?

オレ、C++ いじった記憶ないんだけど・・・

確かに一昨年、手書き文字認識にハマって、Pythonのいろんなライブラリを扱ったけど、でもあれはSDカードや外付けのSSDにインストールしたWinPythonに入れたはずで・・・。

しかも、MyPCは23年12月にOSからクリーン・インストール。
溜まってた余計なモノはきれいさっぱり、消えたはず・・・。

もう、何がどうして、こうなるのか・・・

自分にはどうすることもできないと、ほとんどあきらめかけていたのですが、ふと、Win11の23H2から使えるようになった生成AI、Copilot(プレビュー)が、いつも、とても良いアドバイスをしてくれることを思い出し、・・・ひょっとしたら・・・問題解決のヒントを教えてくれるかも・・・と、突然、思えてきて。

こんなイイコトに、なんで今まで気がつかなかったんだろー。

2.Copilotさんが言うには・・・

さっそく、訊いてみました!(同じ内容をGoogle先生に訊ねると画像が9枚紹介されて、おしまい)
キーワードはそのものズバリ。

clientcore\windows\advcore\ctfshellhandwriting\client\handwritingclient.cpp(296)がエラーになる理由は?

単刀直入に、いちばん、知りたいコトを入力して、Enter。

Copilotさんは、しばらく、考え(検索し)ているようでしたが、やがて驚くべき返答が☆

(返答はそのまま載せたらいけないらしいので要約です)

『エラーは、Windowsの新しい「シェル手書き」機能(MTestAbSh1, 41799415)のバグに関連している可能性あり』

で、

『ViVeToolを使えば当該機能が有効になっているか・どうかを確認できる』

とのこと。まぁ 状況から考えて、MyPCでは絶対!有効になってると思うし、それより・・・。

ViVeTool(ビーブツール)ってナンだか、さっぱりわかんないけど、

Copilotさん、すごすぎます!

さらに、「詳細情報」として stackoverflow.com の記事へのリンクを示してくれました。
こちらがその記事です。

MSCTF.dll complains ‘An assertion failure has occurred’

https://stackoverflow.com/questions/77556770/msctf-dll-complains-an-assertion-failure-has-occurred

以下、リンク先記事の要約です。

(1)原因は、現在実験段階にある新しい「シェル手書き」機能(MTestAbSh1、41799415)のバグ。
(2)ViVeToolをダウンロードして、機能が有効になっているかどうかを確認。
(3)確認コマンドは、「vivetool /query /id:41799415」
(4)機能が有効であった場合に無効化するコマンドは、「vivetool /disable /id:41799415」
(5)機能を無効に設定したら、システムを再起動する。

完璧すぎ。これこそ知りたかった究極の解決策。問題の解答そのものです!!

リンク先記事の質問の回答者様に心から感謝。

Copilotさん、ありがとう!

3.ViVeToolをダウンロードして実行

【ダウンロード】

さらにCopilotさんは「ViVeToolとは何か?」についても教えてくれました☆
以下、その要約です。

(1)ViVeToolは、Windowsの隠し機能を有効化・無効化できるサードパーティ製のツール。
(2)Windows Insiderでも利用できない機能を有効にしたいときに使用する。
(3)ViVeToolを使えば、Windows 11の新機能(隠し機能)を公開前に使用できる。

そぉなんだー♪

私自身は、別に公開前でなくてもかまいませんから、安定した機能を安心・安全に使いたいだけですが、すごいToolがあるんですね。ほんと、びっくり!!です。

で、ViVeToolのダウンロード先と、使い方の詳細は・・・(こちらも要約)

(1)GitHub からzipファイルをダウンロード。
(2)ダウンロードしたzipファイルを展開。
(3)コマンドプロンプトを起動、ViVeToolのフォルダーに移動
   ↑
  管理者として実行する(じゃないと有効状態の確認はできても、設定の変更ができない)

(4)ViVeToolコマンドを実行

もちろん実行するコマンドは、機能が有効であるか確認するための「vivetool /query /id:41799415」と、有効であった場合にそれを無効化する「vivetool /disable /id:41799415」です。

ViVeToolのダウンロードは、こちら

ViVeTool v0.3.3

https://github.com/thebookisclosed/ViVe/releases/tag/v0.3.3

【実行】

手順通りにViVeToolをダウンロード&展開(解凍)して、コマンドプロンプトを管理者権限で実行(真実はちょっと違って、最初は普通にコマンドプロンプトを呼び出して、問題の機能が有効であることを確認。続けて無効化しようとしたら、エラーに。T_T )

なので再度、コマンドプロンプトを管理者権限で起動して、上記コマンドを実行し、問題の機能の無効化に成功。

以下、問題の機能の状態を確認&無効化作業時、MyPCのコマンドプロンプトに表示された内容です。
※ 赤字は、状態の確認と機能の無効化のコマンド

【コマンドプロンプト】※ 管理者として実行します!

Microsoft Windows [Version 10.0.22631.3007]
(c) Microsoft Corporation. All rights reserved.

C:\Windows\System32>cd \

C:\>cd C:\Users\XXX\Downloads\ViVeTool-v0.3.3

C:\Users\XXX\Downloads\ViVeTool-v0.3.3>vivetool /query /id:41799415
ViVeTool v0.3.3 - Windows feature configuration tool

[41799415]
Priority        : Service (4)
State           : Enabled (2)
Type            : Experiment (1)

C:\Users\XXX\Downloads\ViVeTool-v0.3.3>vivetool /disable /id:41799415
ViVeTool v0.3.3 - Windows feature configuration tool

Successfully set feature configuration(s)

C:\Users\XXX\Downloads\ViVeTool-v0.3.3>

で、Successfullyの表示を信じ、祈るような気持ちでシステムを再起動。
これでダメなら、イチから出直しです。

・・・

Delphiを起動。現在、開発中のプロジェクトを読み込んで、実行(F9)。

【結果】

スレッドの開始 : スレッド ID: 30832. プロセス XXX.exe (32324)
プロセスの開始: C:\Users\XXX\Win32\Release\XXX.exe. ベースアドレス: $004E0000. プロセス XXX.exe (32324)
モジュールの読み込み: XXX.exe. デバッグ情報あり. ベースアドレス: $004E0000. プロセス XXX.exe (32324)
モジュールの読み込み: ntdll.dll. デバッグ情報なし. ベースアドレス: $77890000. プロセス XXX.exe (32324)
モジュールの読み込み: KERNEL32.dll. デバッグ情報なし. ベースアドレス: $75990000. プロセス XXX.exe (32324)
モジュールの読み込み: KERNELBASE.dll. デバッグ情報なし. ベースアドレス: $77560000. プロセス XXX.exe (32324)
モジュールの読み込み: apphelp.dll. デバッグ情報なし. ベースアドレス: $74550000. プロセス XXX.exe (32324)
モジュールの読み込み: COMDLG32.dll. デバッグ情報なし. ベースアドレス: $76290000. プロセス XXX.exe (32324)
スレッドの開始 : スレッド ID: 15468. プロセス XXX.exe (32324)
モジュールの読み込み: msvcp_win.dll. デバッグ情報なし. ベースアドレス: $75D70000. プロセス XXX.exe (32324)
スレッドの開始 : スレッド ID: 21404. プロセス XXX.exe (32324)
モジュールの読み込み: ucrtbase.dll. デバッグ情報なし. ベースアドレス: $76BD0000. プロセス XXX.exe (32324)
スレッドの開始 : スレッド ID: 30816. プロセス XXX.exe (32324)
モジュールの読み込み: combase.dll. デバッグ情報なし. ベースアドレス: $75A80000. プロセス XXX.exe (32324)
モジュールの読み込み: WINSPOOL.DRV. デバッグ情報なし. ベースアドレス: $70E00000. プロセス XXX.exe (32324)
モジュールの読み込み: WINMM.dll. デバッグ情報なし. ベースアドレス: $74600000. プロセス XXX.exe (32324)
モジュールの読み込み: d3d9.dll. デバッグ情報なし. ベースアドレス: $66C20000. プロセス XXX.exe (32324)
モジュールの読み込み: RPCRT4.dll. デバッグ情報なし. ベースアドレス: $76E70000. プロセス XXX.exe (32324)
モジュールの読み込み: msvcrt.dll. デバッグ情報なし. ベースアドレス: $75760000. プロセス XXX.exe (32324)
モジュールの読み込み: SECHOST.dll. デバッグ情報なし. ベースアドレス: $77250000. プロセス XXX.exe (32324)
モジュールの読み込み: SHCORE.dll. デバッグ情報なし. ベースアドレス: $77330000. プロセス XXX.exe (32324)
モジュールの読み込み: SHCORE.dll. デバッグ情報なし. ベースアドレス: $01BF0000. プロセス XXX.exe (32324)
モジュールの読み込み: bcrypt.dll. デバッグ情報なし. ベースアドレス: $76CF0000. プロセス XXX.exe (32324)
モジュールの読み込み解除 : SHCORE.dll. プロセス XXX.exe (32324)
モジュールの読み込み: USER32.dll. デバッグ情報なし. ベースアドレス: $76F30000. プロセス XXX.exe (32324)
モジュールの読み込み: GDI32.dll. デバッグ情報なし. ベースアドレス: $755C0000. プロセス XXX.exe (32324)
モジュールの読み込み: win32u.dll. デバッグ情報なし. ベースアドレス: $77400000. プロセス XXX.exe (32324)
モジュールの読み込み: win32u.dll. デバッグ情報なし. ベースアドレス: $01740000. プロセス XXX.exe (32324)
モジュールの読み込み解除 : win32u.dll. プロセス XXX.exe (32324)
モジュールの読み込み: SHLWAPI.dll. デバッグ情報なし. ベースアドレス: $772E0000. プロセス XXX.exe (32324)
モジュールの読み込み: gdi32full.dll. デバッグ情報なし. ベースアドレス: $758A0000. プロセス XXX.exe (32324)
モジュールの読み込み: SHELL32.dll. デバッグ情報なし. ベースアドレス: $76350000. プロセス XXX.exe (32324)
モジュールの読み込み: dwmapi.dll. デバッグ情報なし. ベースアドレス: $71030000. プロセス XXX.exe (32324)
モジュールの読み込み: COMCTL32.dll. デバッグ情報なし. ベースアドレス: $74070000. プロセス XXX.exe (32324)
モジュールの読み込み: OLEAUT32.dll. デバッグ情報なし. ベースアドレス: $754F0000. プロセス XXX.exe (32324)
モジュールの読み込み: ADVAPI32.dll. デバッグ情報なし. ベースアドレス: $755F0000. プロセス XXX.exe (32324)
モジュールの読み込み: ole32.dll. デバッグ情報なし. ベースアドレス: $76D10000. プロセス XXX.exe (32324)
モジュールの読み込み: VERSION.dll. デバッグ情報なし. ベースアドレス: $74B00000. プロセス XXX.exe (32324)
モジュールの読み込み: WTSAPI32.dll. デバッグ情報なし. ベースアドレス: $74AD0000. プロセス XXX.exe (32324)
モジュールの読み込み: dxcore.dll. デバッグ情報なし. ベースアドレス: $690D0000. プロセス XXX.exe (32324)
モジュールの読み込み: IMM32.dll. デバッグ情報なし. ベースアドレス: $75590000. プロセス XXX.exe (32324)
モジュールの読み込み: MSCTF.dll. デバッグ情報なし. ベースアドレス: $77150000. プロセス XXX.exe (32324)
モジュールの読み込み: UxTheme.dll. デバッグ情報なし. ベースアドレス: $73FF0000. プロセス XXX.exe (32324)
モジュールの読み込み: AppCore.dll. デバッグ情報なし. ベースアドレス: $748E0000. プロセス XXX.exe (32324)
モジュールの読み込み: bcryptPrimitives.dll. デバッグ情報なし. ベースアドレス: $770E0000. プロセス XXX.exe (32324)
モジュールの読み込み: WINSTA.dll. デバッグ情報なし. ベースアドレス: $747D0000. プロセス XXX.exe (32324)
モジュールの読み込み: gdiplus.dll. デバッグ情報なし. ベースアドレス: $6ED10000. プロセス XXX.exe (32324)
スレッドの開始 : スレッド ID: 32320. プロセス XXX.exe (32324)
モジュールの読み込み: TextShaping.dll. デバッグ情報なし. ベースアドレス: $6D760000. プロセス XXX.exe (32324)
モジュールの読み込み: CLBCatQ.DLL. デバッグ情報なし. ベースアドレス: $75670000. プロセス XXX.exe (32324)
モジュールの読み込み: MMDevAPI.DLL. デバッグ情報なし. ベースアドレス: $5BA00000. プロセス XXX.exe (32324)
モジュールの読み込み: DEVOBJ.dll. デバッグ情報なし. ベースアドレス: $74690000. プロセス XXX.exe (32324)
モジュールの読み込み: CFGMGR32.dll. デバッグ情報なし. ベースアドレス: $06450000. プロセス XXX.exe (32324)
モジュールの読み込み: CFGMGR32.dll. デバッグ情報なし. ベースアドレス: $06490000. プロセス XXX.exe (32324)
モジュールの読み込み解除 : CFGMGR32.dll. プロセス XXX.exe (32324)
モジュールの読み込み: CFGMGR32.dll. デバッグ情報なし. ベースアドレス: $74650000. プロセス XXX.exe (32324)
モジュールの読み込み解除 : CFGMGR32.dll. プロセス XXX.exe (32324)
モジュールの読み込み: AudioSes.DLL. デバッグ情報なし. ベースアドレス: $5B870000. プロセス XXX.exe (32324)
スレッドの開始 : スレッド ID: 11740. プロセス XXX.exe (32324)
モジュールの読み込み: python39.dll. デバッグ情報なし. ベースアドレス: $79C60000. プロセス XXX.exe (32324)
モジュールの読み込み: WS2_32.dll. デバッグ情報なし. ベースアドレス: $76230000. プロセス XXX.exe (32324)
モジュールの読み込み: VCRUNTIME140.dll. デバッグ情報なし. ベースアドレス: $79C40000. プロセス XXX.exe (32324)
モジュールの読み込み: CRYPTSP.dll. デバッグ情報なし. ベースアドレス: $71F20000. プロセス XXX.exe (32324)
モジュールの読み込み: RSAENH.dll. デバッグ情報なし. ベースアドレス: $71EC0000. プロセス XXX.exe (32324)
モジュールの読み込み: CRYPTBASE.dll. デバッグ情報なし. ベースアドレス: $74540000. プロセス XXX.exe (32324)
モジュールの読み込み: TextInputFramework.dll. デバッグ情報なし. ベースアドレス: $6E6A0000. プロセス XXX.exe (32324)
モジュールの読み込み: CoreMessaging.dll. デバッグ情報なし. ベースアドレス: $63E10000. プロセス XXX.exe (32324)
モジュールの読み込み: CoreUIComponents.dll. デバッグ情報なし. ベースアドレス: $63B70000. プロセス XXX.exe (32324)
モジュールの読み込み: WinTypes.dll. デバッグ情報なし. ベースアドレス: $74D30000. プロセス XXX.exe (32324)
モジュールの読み込み: OLEACC.dll. デバッグ情報なし. ベースアドレス: $6ECB0000. プロセス XXX.exe (32324)
モジュールの読み込み: tophdll.dll. デバッグ情報なし. ベースアドレス: $5BE70000. プロセス XXX.exe (32324)
スレッドの開始 : スレッド ID: 14212. プロセス XXX.exe (32324)
スレッドの開始 : スレッド ID: 31268. プロセス XXX.exe (32324)

エラーは完全に消えました!

でも、長かったなー。
解決できる気は『まったくしなかった』し、
解決できたのは、ほんとに偶然だったけど・・・

もしかしたら、同じ問題で悩んでいる人が、この世界のどこかに・・・

解決策がここにあります。
届け、このメッセージ!

そう思って、これを書いています。

4.まとめ

(1)Windows11には、隠し機能があって、しかもそれは有効な状態だったりする。
(2)GitHubから入手できるViVeToolを使えば、その有効/無効を確認できる。
(3)当該機能が有効である場合は、同ツールで無効化できる。
(4)有効状態を無効化した場合は、システムの再起動が必要。

Win11の隠し機能と、Delphiのコンパイラとの関係は謎のままですが・・・。Msctf.dllを読み込む設定だと、そこで問題が起きるのかな? みたいな・・・

5.お願いとお断り

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

ながーい手続きの途中で変数を宣言したい!

これまでながーい手続きを書いていて、変数を宣言する必要がある時はいつもShift+Ctrl+1(数字は任意)でその行へすぐに戻れるように「しおり」を設定してから、手続きの最初までスクロール、Var宣言部に変数を追加、Ctrl+1(数字は任意)で編集中の行に戻ることを繰り返してきました。

いちいち、ここへ戻るのはめんどくさい!


きっと、Var宣言部に戻らなくても、その場所で変数を宣言できるショートカットキーがあるんだろうーなーって、ずうぅぅぅぅぅぅっと思っていたんだけれど、Delphi 12.0 をインストールした際に、今まで入れてなかったHelpもインストールしたので、入れた以上は使わなきゃ!と、

メニューのヘルプ ⇨ RAD Studio ヘルプ ⇨ メインヘルプ ⇨ 検索キーワード「ショートカットキー」で検索 ⇨ メインショートカット ⇨ デフォルトのキーボードショートカットとたどって(なが!)、大量にあるショートカットキーの項目を一つずつ上から下へ丹念に探します。すると・・・

あった!

Shift+Ctrl+V 
 [リファクタリング|変数の宣言...](Delphi のみ) 
 [新規変数の宣言]ウィンドウが開き、手続き内にローカル変数を宣言することができます。(上記ヘルプより引用)

これです。コレ!
やっぱり、ありました☆

【やってみた】

ある既存の手続きのいちばん最後にMessageBox関数でメッセージを表示するコードを追加します。この関数で使用する文字列型変数が strMsg ですが、まだ変数として宣言していないので、書いた途端に次のように注意の表示?が出ます。

文字列型変数として宣言したいstrMsgは、書いた途端に赤い波線と!マークが付く


構造ペインには・・・


もちろん、実行(F9)できません!

未定義の識別子エラーに加えて、「致命的エラー」という文言も登場


コンパイル結果の案内も!

Delphiのこういうところが大好き!
ここを確実に乗り越えて行けば、エラーの発生しないプログラムになる☆


とりあえず、strMsgを書いた行にカーソル(キャレット?)を置いて、Shift+Ctrl+V してみましたが・・・

(変化なし。なんにも起きない。なんでかなー?)

再挑戦。今度は str と Msg の間をクリックして、そこにカーソル(キャレット?)を表示しておいて(str|Msgみたいな)、んで、覚えたばかりの Shift+Ctrl+V を実行。すると・・・ (ここは strMsg を選択状態にしておいて Shift+Ctrl+V しても同じ)。

(なんか出た!!)

望み通り種類は「文字列」だから、迷わず「OK」をクリック
(望み通りでない場合は、ここで型を指定)


で、「OK」をクリックしたら、未定義の識別子を示す赤い波線が消えた!

Var宣言部へ行って見ると・・・

文字列型の変数として宣言されていました☆


元に戻って・・・


実行!

【まとめ】

新規変数の宣言は、その場で「Shift+Ctrl+V」

こんなイイ方法があったんだ☆

今度から、毎回使おう!!!

マークシートリーダーを教科「情報」用に設定

マークシートリーダー関連の第4回は、ゼロ始まりの選択肢への対応 です。

マークシートの選択肢には「1」始まりと「0」始まりが存在します。教科「情報」の試験で使用されるマークシートは「0」始まりが標準?のようです。そこで自作のマークシートリーダー及びマークシートを、それぞれ選択肢「ゼロ(0)」始まりにも対応できるよう改良しました。

選択肢「ゼロ(0)」始まりに対応したマークシートリーダーはこちらからダウンロードできます。

Version 1.1.1は、選択肢のゼロ始まりに対応していますが、マークシートの種類の表示に誤りがあります。
Version 1.1.2で、マークシートの種類の表示の誤りを修正しました(数学->数学/情報)

追記(20240929)

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

筆者が作成したマークシートリーダーの詳しい使い方を紹介しています。

重要 MS_Reader の使用はあくまでも自己責任でお願いします。動作保証は一切ありません!


マークシートの読み取り速度をPython環境を組み込んで高速化します。

Python環境を組み込むとマークシートリーダーをさらに高速化できます。
重要 組み込みPython環境のご使用もあくまでも自己責任でお願いします。動作保証は一切ありません!

【もくじ】

1.教科「情報」用マークシートのダウンロード
2.選択肢ゼロ始まりの設定方法
3.重要!テンプレート作成時の注意事項
4.お願いとお断り

1.教科「情報」用マークシートのダウンロード

教科「情報」用マークシートはこちらからダウンロードできます。

追記(20240929)

上記リンク先でダウンロードできる「デジタル採点 All in One !」は、ここからダウンロードできる教科「情報」用マークシートも同梱しています。「デジタル採点 All in One !」には、マークシートリーダーの他、マークの読み取りを高速化するPython環境、手書き答案の採点プログラム、受験者に採点結果を通知する個票及び成績一覧表の作成プログラム、実際の採点現場で要請に応じて作成した各種のマークシート等を同梱しています。何の保証もサポートもありませんし、「All 自己責任でお願いします」という制約はありますが、すべて無料でお使いいただけます。


選択肢はゼロ始まりで、1設問あたり16個まで設定可能です。

ゼロ始まりで15まで、選択肢数16の教科「情報」用マークシート(Version 1)。
ダウンロードできるのは、これを改良した Version 2 です。

2.選択肢ゼロ始まりの設定方法

MS_Readerを起動すると、画面上部のパラメータ設定入力用コントロールの左側に、選択肢の開始番号を指定するComboBoxがあります。教科「情報」用のマークシートを読み取る場合は、ここに「0」を指定(∨マークをクリックして選択)してください。

教科「情報」用である場合、選択肢は「0」始まりを指定

3.重要!テンプレート作成時の注意事項

マークシート情報を記録するテンプレートを作成する際には、選択肢の枠幅を間違って設定しないよう、十分注意してください。指定は、次の図のように行ってください。

列枠そのものではなく、設問番号部分は含めずに、選択肢部分のみを指定してください。
指定する際は、左上の「⊥」マークから、右下の「T」マークまでの範囲をドラッグします。


この枠の指定が何を意味するかと言うと、マーク読み取りの際、プログラムはまず、上で指定された範囲の「高さ」を行数で割って1行分を切り出し、次に「幅」を選択肢数で割ってマークを1つずつ切り出します。次の図のようなイメージです。


ですので、マークシートを作成する際は、このことを考慮して、各マークの幅と余白が均等になるように作成します。逆に言えば、各マークを上手く切り出せるように選択肢の枠幅を指定する必要があるわけです。

Wordで作成したマークシートの設問01欄の選択肢番号部分を範囲選択すると、
各マークの幅と余白が均等になっているか、どうか、確認することができます。


プログラムは、この切り出したマークの塗りつぶし面積を計算して、それが最大であるものを「マークあり」と判定しています。

各マーク間の余白も重要です。広すぎても、狭すぎても、判定に影響します。広すぎると計算が遅くなり、塗りつぶし面積の閾値の設定が難しくなります。逆に、狭すぎると大きめにマークされた場合、隣までマークしたことになり、複数マークの判定が出やすくなります。選択肢数が多くなると、どうしても各マーク間の余白は狭くなります。大きめにマークされた場合のことを考え、故意にマーク位置を変えて、どれくらいズレに強いか、試行してみました。

ちゃんと読めています!


上の図にある程度のズレであれば、プログラムは正しくマークを読み取れるようです。

もう少し、ズレ幅を大きくしてみました。

「99」は「複数マークあり」を意味します。


さすがに、ここまでズレると判定に影響します。しかし、完全な誤判定でなく、「複数マークあり」という判定なので、読み取り後のチェックで採点者の目視による判断が求められることになります(私はこの結果を見て、安心して使えると判断しました)。

マークシートの作り方については、MS_Readerの操作・設定マニュアルにも詳しい解説があります。オリジナルのマークシートを作られる際は、どうかそちらもご参照いただけますようお願い致します。

マニュアルはPDFです!

4.お願いとお断り

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