2012年12月24日月曜日

コンソールアプリケーション4-コンソールをデバッグに使う(Delphi Advent Calendar 2012-12-24)


Delphi Advent Calendar 2012 12/24 の記事です。

前回までで、コンソールの基礎的な事柄を述べました
今回 GUI アプリケーションで、コンソールを使う方法を紹介します。
(ずっとコンソールについて書いてきましたが、結局これがやりたかった!)

GUI アプリでも、デバッグ時に現在の状態を表示するためにログを出したりする事が多々あります。
その場合、TMemo をアプリに配置して、そこに Lines.Add('メッセージ')などとしている事が多いのでは無いでしょうか?
しかし、Lines.Add だと数字は IntToStr() で文字列に変換しないと表示できないですし、そもそもデバッグの為だけに TMemo を置くのも馬鹿らしいです。

コンソールが使えれば、それらの悩みも一挙解決です。
コンソールを表示するために、何かインスタンスを作る必要も無いですし、Writeln を使えば文字列も数値も一緒くたに表示できるからです。
しかも、コンソールは表示するだけではなく読み取ることもできます。
コンソールから文字列を受け取って、それに応じてアプリの状態を変更したりできます(デバッグ時に非常に役に立つでしょう)。

GUI アプリでコンソールを使うには2つ方法があります。
1つは CreateProcess の引数に CREATE_NEW_CONSOLE を付ける方法、
もう1つは、AllocConsole を使う方法です。

CREATE_NEW_CONSOLE を使う方法は、アプリの起動時(CreateProcess を呼び出した時)に指定する必要があるため、今回は使えません(自分で自分を呼び出すことはできないため)。
そこで、今回は AllocConsole を使います。
FormCreate でコンソールを割り当て、FormDestroy でコンソールの割り当てを解除しています。

procedure TForm1.Button1Click(Sender: TObject);
begin
// コンソールに文字列を出力
Writeln('Hello, Console !');
end;
 
procedure TForm1.FormCreate(Sender: TObject);
begin
// コンソールを割り当て
AllocConsole;
end;
 
procedure TForm1.FormDestroy(Sender: TObject);
begin
// 割り当てたコンソールを開放
FreeConsole;
end;

Button1 を押すとコンソールに文字列が表示されました!



なお、コンソールが無い状態で Writeln や Readln を呼び出すと「I/O エラー」が発生します。


では、この仕組みを使いやすくライブラリ化し、簡単にデバッグメッセージを出力できるようにしてみます。
それぞれ詳細はコード中のコメントを参照してください。

まず先に使い方です。
コンソールアプリケーションと同様に Writeln/Readln が普通に使えます。
ここでは Readln を使う代わりに、ライブラリ化した関数 ReadConsole を使っています。
procedure TForm1.Button1Click(Sender: TObject);
begin
// 文字列の表示
Writeln('Hello, World !');
 
// 数字や Boolean、文字列などを混在して表示できる
Writeln(123456789, ' ', True);
end;
 
procedure TForm1.Button2Click(Sender: TObject);
var
Str: String;
begin
// 命令をコンソールから読み取る
Str := ReadConsole('Input Command: ', True);
 
// exit なら終了
if (Str = 'exit') then
Close
// notepad なら「メモ帳」を起動
else if (Str = 'notepad') then
WinExec('notepad.exe', SW_SHOW)
// それ以外なら不明と表示
else
Writeln('Unknown command:', Str);
end;

uConsole.pas
unit uConsole;
 
interface
 
type
// コンソールイベント
TConsoleEventType = (
ceC, // CTRL + C が押された
ceBreak, // CTRL + BREAK が押された
ceClose, // コンソールウィンドウが閉じられた
ceLogOff, // ログオフされた
ceShutdown // シャットダウンされた
);
 
// コンソールイベント型
TConsoleEvent = procedure(const iType: TConsoleEventType) of object;
 
// コンソールイベントを受け取るリスナーを追加・解除
procedure AddConsoleEventListener(const iListener: TConsoleEvent);
procedure RemoveConsoleEventListener(const iListener: TConsoleEvent);
 
// コンソールから文字列を読み取る
function ReadConsole(
const iPrompt: String = '';
const iToLower: Boolean = False): String;
 
implementation
 
uses
Winapi.Windows, Generics.Collections, System.SysUtils;
 
var
// コンソールウィンドウのハンドル
GWnd: HWND;
// イベントリスナを管理するリスト
GListeners: TList<TConsoleEvent>;
 
// コンソールイベントリスナを追加
procedure AddConsoleEventListener(const iListener: TConsoleEvent);
begin
if (GListeners.IndexOf(iListener) < 0) then
GListeners.Add(iListener);
end;
 
// コンソールイベントリスナを削除
procedure RemoveConsoleEventListener(const iListener: TConsoleEvent);
begin
if (GListeners.IndexOf(iListener) > -1) then
GListeners.Remove(iListener);
end;
 
// コンソールから文字列を読み取る
// iPrompt 読み取り前に表示する文字列(ex. 'Please input your name: ')
// iToLower 読み取った文字列を小文字にするなら True
function ReadConsole(
const iPrompt: String = '';
const iToLower: Boolean = False): String;
begin
// プロンプトの表示
if (iPrompt <> '') then
Write(iPrompt);
 
// コンソールに入力フォーカスを与える
ShowWindow(GWnd, SW_SHOW);
SetForegroundWindow(GWnd);
 
// 読み込む
Readln(Result);
 
// 小文字化
if (iToLower) then
Result := LowerCase(Result);
end;
 
// コンソールイベントが起きたときに呼ばれる関数
function HandlerRoutine(dwCtrlType: DWORD): BOOL; stdcall;
var
Listener: TConsoleEvent;
begin
Result := True; // False の場合、イベントは OS が適切に処理する
//(ex. CTRL + C が押されたらアプリケーションを終了させる)
// True の場合、OS は何もしない
 
for Listener in GListeners do
Listener(TConsoleEventType(dwCtrlType));
end;
 
// コンソールの初期設定
// ・コンソールの Window Handle の特定
// ・コンソールのタイトルの設定
procedure InitConsole;
var
Cap: String;
begin
// Window Caption に GUID を設定する
Cap := TGUID.NewGuid.ToString;
SetConsoleTitle(PWideChar(Cap));
 
Sleep(40); // Caption が確実に設定されるために 40[msec] 待つ
 
// GUID でウィンドウを探す
GWnd := FindWindow(nil, PChar(Cap));
 
if (GWnd <> 0) then
// 見つけたらスタイルから System Menu を外す
//(コンソールを勝手に閉じられないようにするため)
SetWindowLong(
GWnd,
GWL_STYLE,
GetWindowLong(GWnd, GWL_STYLE) and not WS_SYSMENU);
 
// コンソールのタイトルをアプリケーションのパスにする
SetConsoleTitle(PWideChar(ParamStr(0)));
end;
 
// 初期化
initialization
begin
// イベントハンドラ管理用リストの生成
GListeners := TList<TConsoleEvent>.Create;
 
// アプリケーションにコンソールを割り当てる
AllocConsole;
 
// コンソールイベントのハンドラを設定する
SetConsoleCtrlHandler(@HandlerRoutine, True);
 
// コンソールの初期設定
InitConsole;
end;
 
// 終了処理
finalization
begin
// コンソールイベントのハンドラを解除
SetConsoleCtrlHandler(@HandlerRoutine, False);
 
// 割り当て済みのコンソールを解除
FreeConsole;
 
// イベントハンドラ管理用リストの破棄
GListeners.Free;
end;
 
end.

実行すると、こんな風になります。


0 件のコメント:

コメントを投稿