Generic

Win32アプリケーションの基本構造

ここでは、すべてのWin32アプリケーションの基本となるプロジェクトを作成します。

Hello World!アプリケーションと同様にプロジェクトを作成し、以下のコードを入力します。
Generic.dprというファイル名で保存すれば完成です。

program Generic;


{$WARN SYMBOL_PLATFORM OFF} //CmdShowの警告回避


uses
  Windows, Messages;


////////////////////////////////////////////////////////////////////////////////
// Main Window Procedure


function MainWndProc(wnd: HWND; msg: UINT; wp: WPARAM;
  lp: LPARAM): LRESULT; stdcall;
begin
  Result := 0;
  case msg of
    WM_DESTROY: PostQuitMessage(0);
  else
    Result := DefWindowProc(wnd, msg, wp, lp);

  end;
end;


////////////////////////////////////////////////////////////////////////////////
// Main


const
  CW_USEDEFAULT = Integer($80000000); //Windowsユニットの宣言では警告が出る
  WC_MAIN = 'GenericMain';


var
  wc: TWndClass;
  wnd: HWND;
  msg: TMsg;


begin
  wc.style          := CS_VREDRAW or CS_HREDRAW;
  wc.lpfnWndProc    := @MainWndProc;
  wc.cbClsExtra     := 0;
  wc.cbWndExtra     := 0;
  wc.hInstance      := HInstance;
  wc.hIcon          := LoadIcon(0, IDI_APPLICATION);
  wc.hCursor        := LoadCursor(0, IDC_ARROW);
  wc.hbrBackground  := COLOR_BTNFACE + 1;
  wc.lpszMenuName   := nil;
  wc.lpszClassName  := WC_MAIN;


  RegisterClass(wc);


  wnd := CreateWindow(
    WC_MAIN,
    'Generic',
    WS_OVERLAPPEDWINDOW or WS_CLIPCHILDREN,
    CW_USEDEFAULT, CW_USEDEFAULT,
    CW_USEDEFAULT, CW_USEDEFAULT,
    HWND_DESKTOP,
    HMENU(0),
    HInstance,
    nil
  );


  ShowWindow(wnd, CmdShow);


  while GetMessage(msg, 0, 0, 0) do begin
    TranslateMessage(msg);
    DispatchMessage(msg);
  end;


  ExitCode := msg.wParam;
end.

完成したらコンパイルして実行してみましょう。何も乗せていないTFormのようなウィンドウが表示されたはずです。サイズ変更など、基本的なウィンドウ操作ができることを確認してください。

TFormの場合は400KBを超えましたが、この実行バイナリは管理人の環境では8.5KBでした。KOL systemユニットとUPXを使えば4KBまで小さくできました。

コードの内容を簡単に解説すると、

  1. MainWndProc関数をウィンドウプロシージャとしてWNDCLASS構造体を初期化
  2. RegisterClass関数に渡してウィンドウクラスを登録
  3. 登録したウィンドウクラスを使ってCreateWindow関数でウィンドウを作成
  4. ShowWindowで作成したウィンドウを表示
  5. GetMessageでメッセージキューからメッセージを取り出し、メッセージがWM_QUITでない限り、

という流れになります。Win32APIについてより詳細な情報を知りたければ関連リンクを参照してください。

Delphi特有の注意事項としては次のものがあります。

子ウィンドウを追加する

もう少しアプリケーションらしくするためにエディットコントロールを子ウィンドウとして追加します。

Genericプロジェクトを次のように編集してください。

program Generic;


{$WARN SYMBOL_PLATFORM OFF}


uses
  Windows, Messages;


var
  hEdit: HWND;


////////////////////////////////////////////////////////////////////////////////
// Message Handler


procedure MainCreate(wnd: HWND);
const
  EDIT_STYLE =
    WS_CHILD or WS_VISIBLE or WS_HSCROLL or WS_VSCROLL or
    ES_AUTOHSCROLL or ES_AUTOVSCROLL or
    ES_LEFT or ES_MULTILINE or ES_NOHIDESEL;
begin
  hEdit := CreateWindowEx(
    WS_EX_CLIENTEDGE, 'EDIT', '', EDIT_STYLE,
    0, 0, 100, 100, wnd, HMENU(0), HInstance, nil
  );
end;


////////////////////////////////////////////////////////////////////////////////
// Main Window Procedure


function MainWndProc(wnd: HWND; msg: UINT; wp: WPARAM;
  lp: LPARAM): LRESULT; stdcall;
begin
  Result := 0;
  case msg of
    WM_DESTROY: PostQuitMessage(0);
    WM_CREATE: MainCreate(wnd);
    WM_SIZE: MoveWindow(hEdit, 0, 0, LOWORD(lp), HiWord(lp), True);
  else
    Result := DefWindowProc(wnd, msg, wp, lp);
  end;
end;


////////////////////////////////////////////////////////////////////////////////
// Main


(同じなので省略)


WM_CREATEメッセージが送られた(メインウィンドウが作成された)ときにエディットコントロールを作成し、hEditグローバル変数にハンドルを代入しています。WM_SIZEメッセージが送られたとき(ウィンドウサイズが変更されたとき)エディットコントロールのサイズをメインウィンドウのクライアント領域いっぱいになるように変更しています。

これだけでファイルを開く・保存などは一切できませんが右クリックメニューを備えたメモ帳らしきものが出来上がります。

メニューを追加する

さらにもう少しアプリケーションらしくするためにメニューを追加します。

Genericプロジェクトを次のように編集してください。

program Generic;


{$WARN SYMBOL_PLATFORM OFF}


uses
  Windows, Messages;


const
  IDM_UNDO = 100;
  IDM_CUT = 101;
  IDM_COPY = 102;
  IDM_PASTE = 103;
  IDM_SELECTALL = 104;


var
  hEdit: HWND;


////////////////////////////////////////////////////////////////////////////////
// Message Handler


procedure MainNCCreate(wnd: HWND);
var
  popup: HMENU;
begin
  SetMenu(wnd, CreateMenu());
  popup := CreatePopupMenu();
  AppendMenu(GetMenu(wnd), MF_STRING or MF_POPUP, popup, '編集(&E)');
  AppendMenu(popup, MF_STRING, IDM_UNDO, '元に戻す(&U)' + #$9 + 'Ctrl+Z');
  AppendMenu(popup, MF_SEPARATOR, 0, nil);
  AppendMenu(popup, MF_STRING, IDM_CUT, '切り取り(&T)' + #$9 + 'Ctrl+X');
  AppendMenu(popup, MF_STRING, IDM_COPY, 'コピー(&C)' + #$9 + 'Ctrl+C');
  AppendMenu(popup, MF_STRING, IDM_PASTE, '貼り付け(&P)' + #$9 + 'Ctrl+V');
  AppendMenu(popup, MF_SEPARATOR, 0, nil);
  AppendMenu(popup, MF_STRING, IDM_SELECTALL, 'すべて選択(&A)' + #$9 + 'Ctrl+A');
end;


procedure MainCreate(wnd: HWND);
const
  EDIT_STYLE =
    WS_CHILD or WS_VISIBLE or WS_HSCROLL or WS_VSCROLL or
    ES_AUTOHSCROLL or ES_AUTOVSCROLL or
    ES_LEFT or ES_MULTILINE or ES_NOHIDESEL;
begin
  hEdit := CreateWindowEx(
    WS_EX_CLIENTEDGE, 'EDIT', '', EDIT_STYLE,
    0, 0, 100, 100, wnd, HMENU(0), HInstance, nil
  );
end;


////////////////////////////////////////////////////////////////////////////////
// Main Window Procedure


function MainWndProc(wnd: HWND; msg: UINT; wp: WPARAM;
  lp: LPARAM): LRESULT; stdcall;
begin
  Result := 0;
  case msg of
    WM_DESTROY: PostQuitMessage(0);
    WM_DESTROY: PostQuitMessage(0);
    WM_NCCreate: begin
      MainNCCreate(wnd);
      Result := DefWindowProc(wnd, msg, wp, lp);
    end;
    WM_CREATE: MainCreate(wnd);
    WM_SIZE: MoveWindow(hEdit, 0, 0, LOWORD(lp), HiWord(lp), True);
    WM_COMMAND:
      case LOWORD(wp) of
        IDM_UNDO: SendMessage(hEdit, WM_UNDO, 0, 0);
        IDM_CUT: SendMessage(hEdit, WM_CUT, 0, 0);
        IDM_COPY: SendMessage(hEdit, WM_COPY, 0, 0);
        IDM_PASTE: SendMessage(hEdit, WM_PASTE, 0, 0);
        IDM_SELECTALL: SendMessage(hEdit, EM_SETSEL, 0, -1);
      end;
  else

    Result := DefWindowProc(wnd, msg, wp, lp);
  end;
end;


////////////////////////////////////////////////////////////////////////////////
// Main


(同じなので省略)


WM_NCCREATEメッセージが送られたときにCreateMenu関数でメインメニューを作成し、SetMenu関数でメインウィンドウと関連付けています。次にCreatePopupMenu関数でサブメニューを作成し、GetMenu関数で取得したメインメニューにAppendMenu関数を使ってサブメニューを追加しています。その後同じくAppendMenu関数を使って個々のメニュー項目を追加しています。

WM_CREATEメッセージが送られたときにメニューを作成・追加すると、起動したときにメニューがエディットコントロールに隠されてしまいます。これはおそらくWM_NCCREATEメッセージがWM_NCCALCSIZEメッセージが送られる(クライアント領域が計算される)前に送られるのに対し、WM_CREATEメッセージはWM_NCCALCSIZEメッセージより後に送られるためと思われます。

通常、ウィンドウプロシージャでメッセージを処理するとき(DefWindowProc関数に渡さないとき)は戻り値として0を返すのですが、WM_NCCREATEメッセージの場合、0を返すとウィンドウを破棄してしまいます。他にもタイトルバー文字列を初期化するなどの動作をこのメッセージで行うので、WM_NCCREATEメッセージが送られたときはメッセージをDefWindowProc関数に渡して戻り値を返しています。

メニューが選択されたときはメニューの親ウィンドウにWM_COMMANDメッセージが送られるので、ここでメニューが選択されたときの動作を記述します。LOWORD(wp)にメニューを作成したときに指定したID値が格納されているので、どのメニューが選択されたのかわかります。このID値はどんな値でもかまいませんが、固有であること、特にIDOKやIDCANCELなどの定義済みID値と重複しないことに気をつけてください。これらの定義済みID値は1〜11程度の値を割り振られているようですので、管理人は通常100から順番にID値を割り振るようにしています。

エディットコントロールに「元に戻す」「コピー」などの動作をさせるのは、特定のメッセージをエディットコントロールに送るだけで済みます。

実行イメージは次のようになります。(ウィンドウサイズを変更しています。)

アクセラレータテーブルを追加する

メニューにショートカットキーを表示するのにキャプションに#$9(タブ文字)をはさんで"Ctrl+Z"や"Ctrl+C"などを指定しましたが、これだけでショートカットキーがはたらくようになるわけではありません。Ctrl+ZやCtrl+Cを押すと「元に戻す」や「コピー」の動作が行われるのは、エディットコントロールの標準の動作によるものです。Ctrl+Aを押しても「すべて選択」の動作が行われないことを確認してください。

このようなショートカットキーの動作、つまりある組み合わせのキーを押すと対応する機能を実行するものをアクセラレータと呼び、このキーの組み合わせと機能の対応表をアクセラレータテーブルと呼びます。

ここでは「すべて選択」機能のCtrl+Aのショートカットキーをアクセラレータテーブルを使って実装してみます。
GenricプロジェクトのMainの部分のみを以下のように編集してください。

program Generic;


(同じなので省略)


////////////////////////////////////////////////////////////////////////////////
// Main


const
  CW_USEDEFAULT = Integer($80000000);
  WC_MAIN = 'GenericMain';
  ACCEL_COUNT = 1;


var
  wc: TWndClass;
  wnd: HWND;
  msg: TMsg;
  table: array[0..ACCEL_COUNT - 1] of TAccel;
  accel: HACCEL;


begin
  wc.style          := CS_VREDRAW or CS_HREDRAW;
  wc.lpfnWndProc    := @MainWndProc;
  wc.cbClsExtra     := 0;
  wc.cbWndExtra     := 0;
  wc.hInstance      := HInstance;
  wc.hIcon          := LoadIcon(0, IDI_APPLICATION);
  wc.hCursor        := LoadCursor(0, IDC_ARROW);
  wc.hbrBackground  := COLOR_BTNFACE + 1;
  wc.lpszMenuName   := nil;
  wc.lpszClassName  := WC_MAIN;


  RegisterClass(wc);


  wnd := CreateWindow(
    WC_MAIN,
    'Generic',
    WS_OVERLAPPEDWINDOW or WS_CLIPCHILDREN,
    CW_USEDEFAULT, CW_USEDEFAULT,
    CW_USEDEFAULT, CW_USEDEFAULT,
    HWND_DESKTOP,
    HMENU(0),
    HInstance,
    nil
  );


  table[0].fVirt := FCONTROL or FVIRTKEY;
  table[0].key := Ord('A');
  table[0].cmd := IDM_SELECTALL;


  accel := CreateAcceleratorTable(table, ACCEL_COUNT);


  ShowWindow(wnd, CmdShow);


  while GetMessage(msg, 0, 0, 0) do begin
    if TranslateAccelerator(wnd, accel, msg) = 0 then begin
      TranslateMessage(msg);
      DispatchMessage(msg);
    end;
  end;


  DestroyAcceleratorTable(accel);


  ExitCode := msg.wParam;
end.

アクセラレータテーブルの使い方は以下のようなものです。

  1. ACCEL構造体配列(TAccelレコード配列)を初期化し、CreateAcceleratorTable関数に渡してアクセラレータテーブルを作成する
  2. メッセージループの中で、TranslateAccelerator関数によってキーボードメッセージをアクセラレータテーブルに基づいてWM_COMMANDメッセージに変換し、直接ウィンドウプロシージャに送る
  3. メッセージを二重処理することをさけるため、TranslateAccelerator関数が0を返した(対応するショートカットキーがアクセラレータテーブルにない)ときのみTranslateMessage関数やDispatchMessage関数を呼び出すようにする
  4. メッセージループを抜けた後にDestroyAccelerator関数を使ってアクセラレータテーブルを破棄する

最後に

今回、ウィンドウ・メニュー・アクセラレータテーブルの作成方法について解説しました。この方法だと、たとえば子ウィンドウを大量に作成するときなどはかなり面倒なことになります。実はWindowsはリソースという仕組みを使って、簡単にウィンドウ・メニュー・アクセラレータを作る方法を用意しており、むしろこちらの方法が一般的です。

しかし、リソースを使う方法でもWindowsの内部では今回解説した方法と同様な処理が行われているはずです。リソースの仕組みを理解するうえでも役立つと思い、あえてリソースを使わない方法を紹介しました。

Win32APIについて概要を知りたければ、次のページが参考になります。

個々のAPIについて知りたければ、MSDNのページで検索をかけるのがよいでしょう。