Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2020年06月25日] [更新 2020年07月01日] Delphi 一般・その他
   
Lazarus のフォームのウィントウのクラス名設定
動作確認等 Windows 7 U64(SP1) + lazarus-2.0.8
[整理番号 - 1500]







Lazarus のフォームのウィントウのクラス名設定


Lazarus の,ウィンドウとしてのフォームのクラス名を設定する方法の記事です.
自アプリ内のフォームは,コントロールとしてのフォームのクラス名を使用して操作できます.ウィンドウハンドルが必要であれば,Handle プロパティが利用できます.TWinControl の派生クラスには Handle プロパティがあります.
ウィンドウのクラス名が必要になるのは,FindWindow 関数などで,Lazarus で作成したアプリを検出して操作する場合ではないかと思われます.






01_Lazarus のフォームのウィントウのクラス名は常に Window


Lazarus で作成したアブリを,FindWindow('TForm1', nil) のように,フォームのクラス名で検束することはできません.Lazarus で作成したフォームのウィントウのクラス名は常に Window となります.このことを確認するサンプルです.
フォームが 2 つのプロジェクトを作成して,画の下にあるコードを実行します.



図1
フォームが 2 つのプロジェクト
  • Button1 クリックで Form2 を表示

Button1 をクリックして Form2 をモーダル表示します.この時の,このアプリのウィンドウ
階層構造は下図のようになります.
Form1, Form2 のウィンドウのクラス名がどちらも同じ Window になっています.



図2
Lazarusで作成したアプリのウィンドウの階層構造
  • ウィンドウのクラス名は全て Window
  • 最初の Windows は Lazarus のアプリケーションウィンドウのクラス名

Form2 を閉じて Button2 をクリックします.
Button2 クリックのコードは,Form1 のクラス名を引数にして,FindWindow 関数で  Form1 のウインドウハンドルを取得し,そのハンドルを使用して Form1 を閉じます.しかし,TForm1 といクラス名のウィンドウはありません.FindWindow 関数は Form1 のウィンドウハンドルを取得できないため Form1 を閉じることはできません.



図3
ウィンドウハンドルの値
  • 最初の値は FindWindow による値
  • 次は,フォームの Handle プロパティの値
  • 2 つの値は違っている

リスト1
フォームのウィントウのクラス名が常に Window であることを確認するテスト
unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls,
  Windows, LCLType;

  { TForm1 }
type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Edit1: TEdit;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private

  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

uses
  Unit2;

{ TForm1 }

//=============================================================================
//  Form2 をモーダル表示
//  uses に Unit2 が必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  Form2.ShowModal;
end;

//=============================================================================
//  FindWindow 関数でフォームのウィンドウを検索してそのウィンドウを閉じる
//  閉じるフォームの Handle プロパティの値とウィンドウハンドルの値を確認表示
//
//  uses に Windows が必要
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LhWnd : HWND;
begin
  LhWnd:= FindWindow('TForm1', nil);
  if LhWnd <> 0 then begin
    ShowMessage(IntToStr(LhWnd) + sLineBreak + IntToStr(Form1.Handle));
    PostMessage(LhWnd, WM_CLOSE, 0,  0);
  end;
end;

end.


[備考]
ネット上に,Lazarus では FindWindow 関数が機能しないという記述かあります.
FindWindow 関数の第 1 引数は,検索対象のウィンドウのクラス名です.そのため,上のサンプルでは,フォームのウィンドウとしてのクラス名を確認しています.





02_Win32WSForms.pp にクラス名登録用のコードを追加


Delphi のフォームのウィンドウのクラス名は,フォームのクラス名と同じになっています.Lazarus も同じ仕様にしてみました.
Lazarus でフォームのウィンドウのクラス名がフォームのクラス名にならないのは,フォームのクラス名がウィンドウのクラス名として登録されていないためと思われます.そこで,以下のページの記事を参考にして,Win32WSForms.pp にクラス名登録用のコードを追加しました.



Win32WSForms.pp ファイルをテキストエディタ,あるいは IDE で表示します.Win32WSForms.ppファイルは,Lazarus のインストール先の \lcl\interfaces\win32 フォルダ内にあります.
このファイルのコード内の TWin32WSCustomForm の.CreateHandle メソッドの変数宣言に TWNDCLASSW 型の変数を追加します.


リスト2
TWin32WSCustomForm.CreateHandle 関数内に変数を 1 つ追加
class function TWin32WSCustomForm.CreateHandle(const AWinControl: TWinControl;
  const AParams: TCreateParams): HWND;
var
  Params: TCreateWindowExParams;
  lForm: TCustomForm absolute AWinControl;
  Bounds: TRect;
  SystemMenu: HMenu;
  LWndClassW: TWNDCLASSW;  // 追加
begin 


次に,以下の pClassName := @ClsName[0];  の後に 10 行程度のコードを追加します.筆者の環境では 430 行目になります.
下のコードには空白行がありますが,実際の pp ファイルのコードには空白行はありません.
コードを追加したら [Ctrl] + [S} で保存します.

下に示したコード内の ClsName は,Win32Int ユニットに Window という文字列定数として定義されています.Lazarus は,この文字列をフォームのウィンドウのクラス名として使用しています.下に示すコードは,そのクラス情報を取得して,ウィンドウのクラス名だけを変更して登録します.
コントロールの登録関係コードは,Win32WSControls ユニット内にあります.



リスト3
TWin32WSCustomForm.CreateHandle 関数内に,ウィンドウのクラス名登録用のコードを追加
    CalcFormWindowFlags(lForm, Flags, FlagsEx);
    pClassName := @ClsName[0];

    // 以下のコードを追加
    if AParams.WinClassName[0] <> #0 then begin
      pClassName := @AParams.WinClassName[0];
    end else begin
      pClassName := PChar(String(AWinControl.ClassName));
    end;
    if GetClassInfoW(HINSTANCE, PWideChar(WideString(ClsName)), @LWndClassW) then begin
      LWndClassW.lpszClassName := PWideChar(WideString(pClassName));
      if Windows.RegisterClassW(@LWndClassW) = 0 then begin
        // error
      end;
    end;
    // 追加するコードはここまで     

    WindowTitle := StrCaption;




03_変更後のフォームのウィントウのクラス名を確認


以上の変更で,FindWindow(''TForm1', nil) のように,FindWindow の引数に,フォームのクラス名を指定して, フォームのウィンドウハンドルが取得できるようになりました.
最初のサンプルで結果を確認してみます.[Shift] + [F9] でプロジェクトを再構築します.再構築しないと結果が反映しないことがあります.
[F9] で実行して Button1 をクリックして Form2 をモーダル表示します.下図は,この時のウィンドウの階層構造です.



図4
Lazarusで作成したアプリのウィンドウの階層構造
  • 最初の Windows は Lazarus のアプリケーションウィンドウのクラス名
  • その下にメインフォームの TForm1 がある
  • その階層下に子フォームの TForm2 がある

プロジェクトのソースコードに Application.MainFormOnTaskbar := True; を追加すると,ウィンドウの階層構造は下図のようになります.メインフォームのウィンドウがトップレベルのウィンドウとなります.Delphi 2007 以降の Delphi では,これがデフォルトの設定となっています.



図5
Application.MainFormOnTaskbar の値が True の場合

Form2 を閉じて Button2 をクリックします.今度は FindWindow 関数で取得した値と Form1.Handle の値が同じになります.ダイアログを閉じると,メインフォームである Form1 が閉じて,テストプログラムが終了します.



図6
ウィンドウハンドルの値
  • 最初の値は FindWindow による値
  • 次は,フォームの Handle プロパティの値
  • 2 つの値が同じ

プロジェクトのソースコードに Application.MainFormOnTaskBar がない場合,MainFormOnTaskBar プロパティの値は False と同じです.
MainFormOnTaskBar プロパティの値が False の時は,タスクバーにはアプリケーションのアイコンが表示され,キャプションは Application.Title の値になります.Application.Title の値が未設定の場合はプロジェクト名となります.
MainFormOnTaskBar プロパティの値が True の時は,タスクバーにはメインフォームのアイコンが表示され,キャプションは TForm.Capion の値になります.
下図は,アイコンもタイトルもデフォルトの設定での実行結果です.



図7
タスクバーのアイコンとキャプション
  • 左は MainFormOnTaskBar プロパティの値が False の場合
  • 右は MainFormOnTaskBar プロパティの値が True の場合




04_Lazarus で作成したアプリのウィンドウをクラス名で検索
Lazarus_WindowClassName.zip [2.1 MB] 2020年07月01日版 (EXE 同梱)


今回のユニットファイルの更新で,Lazarus で作成したアプリのウィンドウを,フォームのクラス名を使用して検索できるようになりました.そのことを確認するためのテストプログラムです.
EnumWindows 関数を利用して,起動しているアプリのウィンドウを検索します.自分と同じクラス名のウィンドウを見つけたら,そのウィントウを閉じます.


リスト4
フォームのクラス名を使用して.Lazarus で作成したアプリのウィンドウを検索
var
  FTargetWnd : HWND = 0;

  //-----------------------------------------------------------------------------
  //  ウィンドウのクラス名を取得する関数
  //  FillChar を実行しないと結果にゴミが入ることがある
  //  小規模のプログラムの場合はゴミが入る可能性は低い
  //-----------------------------------------------------------------------------
  function GetWindowClassNameStr(hWindow: HWND): String;
  var
    LBuffer : array[0..MAX_PATH-1] of Char;
    LLen    : Integer;
    LLL : PWideChar;
  begin
    FillChar(LBuffer, SizeOf(LBuffer), #0);
    LLen := GetClassName(hWindow, LBuffer, Length(LBuffer));
    if LLen > 0 then Result := LBuffer else Result := '';
  end;

  //-----------------------------------------------------------------------------
  //  EnumWindows のコールバック関数
  //  EnumWindows を実行すると,そのバック (裏) でコール (呼び出し) されて実行する関数
  //  コールバック関数の名前は任意
  //  現在起動中のウィンドウの列挙が終了するまで EnumWindows 関数は終了しない
  //  Result := false にすると強制的に列挙終了となる
  //-----------------------------------------------------------------------------
function EnumWindowHogeProc(hWindow: HWND; lData: LPARAM):WINBOOL; stdcall;
begin
  Result:= True;
  if not IsWindowVisible(hWindow) then Exit;

  if GetWindowClassNameStr(hWindow) = PString(lData)^ then begin
    if hWindow <> Application.MainForm.Handle then begin;
      FTargetWnd:= hWindow;
      if PostMessage(FTargetWnd, WM_CLOSE, 0, 0) then begin
        // 目的のウィンドウを閉じることができる
        // 管理者権限で起動してたりすると終了不可の場合もある
      end;
      // 以下の 2 行を削除すると自分以外の TForm1 を全て閉じる
      Result := False;
      Exit;
    end;
  end;
end;

//=============================================================================
//  クラス名を指定してウィンドウの列挙関数を実行
//  自分と同じクラス名のウィンドウを強制的に閉じる
//  このプログラムを複数起動してテストする
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LClassName : string;
begin
  LClassName:= Self.Classname;
  FTargetWnd:= 0;
  EnumWindows(@EnumWindowHogeProc, LPARAM(@LClassName));
  // ウィンドウの列挙が終了しないとここは実行されない
end;




05_CreateParams メソッドによるウィンドウのクラス名変更


今回の変更では,Delphi と同じように,CreateParams メソッドでウィンドウのクラス名が変更できるようにしています.
この場合,そのフォームのクラス名と,フォームのウィンドウのクラス名は別の値となります.


リスト5
TCreateParams.WinClassName でフォームのウィンドウのクラス名を設定
unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, LCLType;

type

  { TForm1 }

  TForm1 = class(TForm)
  private

  protected
    procedure CreateParams(var Params: TCreateParams); override;
  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

//=============================================================================
//  CreateParams メソッド処理
//  uses に LCLType が必要
//=============================================================================
procedure TForm1.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);

  // このフォームのウィンドウのクラス名を変更
  Params.WinClassName := 'TMyMainWindow';
end;

end.