EnumChildWindowsでTLabelを取得するには?


フクロウ  2013-10-10 08:02:19  No: 45387

EnumChildWindowsでTLabelが取得できません。

http://mrxray.on.coocan.jp/Halbow/VCL06.html#VChap6-5
のList19でやっている「子ウィンドウの探索」がまさにやりたいことなのですが、
TLabelが列挙されません。

冒頭で
>TLabel や TImage 、TPaintBox 、 TSpeedButton などの TGraphicControl の派生クラスは、
>ウィンドウの特定のクライアント領域の描画機能をカプセル化したものであり、
>ウィンドウではないのでOSからは見えない。
と記載されており、
TLabelはEnumChildWindowsで取得できないのかもしれませんが、
どうにか取得する方法がありませんか?
よろしくお願いします。

//以下サンプル
//フォームにはTButtonとTListBoxとTLabelを最低1個置き、そのほかは適当に配置している状態
unit Unit1;

interface

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

type
  TForm1 = class(TForm)
    Button1: TButton;
    Label1: TLabel;
    Button2: TButton;
    ComboBox1: TComboBox;
    Panel1: TPanel;
    Label2: TLabel;
    Button3: TButton;
    ComboBox2: TComboBox;
    ListBox1: TListBox;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}
type
  PHWndArray = ^THWndArray;
  THWndArray = array[0..500] of HWND;

function EnumChildWndProc(hWindow:HWND;lData:LPARAM):BOOL;stdcall;
var
  pWA:PHWndArray;
begin
  pWA := PHWndArray(lData);
  pWA^[0] := pWA^[0]+1;
  pWA^[pWA^[0]] := hWindow;
  result := true;
end;

function AEnumChildWindows(var WA:THWndArray; hParent:HWND):integer;
begin
  WA[0] := 0;
  EnumChildWindows(hParent,@EnumChildWndProc,LPARAM(@WA));
  result := WA[0];
end;

function GetClassNameStr(hWindow:HWND):string;
var
  p:PChar;
  ret:integer;
begin
  GetMem(p,100);
  ret := GetClassName(hWindow,p,100);
  SetString(result,p,ret);
  FreeMem(p);
end;

function GetWindowTextStr(hWindow:HWND):string;
var
  p:PChar;
  ret:integer;
begin
  ret := GetWindowTextLength(hWindow);
  GetMem(p,ret+1);
  ret := GetWindowText(hWindow,p,ret+1);
  SetString(result,p,ret);
  FreeMem(p);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  SL:TStringList;
  WA:THWndArray;
  i,Num:integer;
begin
  ListBox1.clear;
  SL := TStringList.Create;
  try
    Num := AEnumChildWindows(WA,Handle);
    Label1.Caption := IntToStr(Num);
    if Num > 0 then
      for i := 1 to Num do begin
        ListBox1.Items.Add(GetClassNameStr(WA[i])+'/'+GetWindowTextStr(WA[i]));
      end;
  finally
    SL.Free;
  end;
end;

end.


Quest  2013-10-11 00:02:51  No: 45388

こういうことかな?
他のアプリの情報は取れないけど。

procedure TForm1.Button1Click(Sender: TObject);
var
  i, Num: integer;
begin
  Num := Self.ComponentCount;
  Label1.Caption := IntToStr(Num);
  for i := 0 to Num-1 do
  begin
    if Self.Components[i] is TLabel then
    begin
      ListBox1.Items.Add(Self.Components[i].Name + '/' + (Self.Components[i] as Label).Caption);
    end;
  end;
end;

Selfの部分を他のフォーム(例えばForm2)にすれば、そのフォームの情報が取れます。


Harry  2013-10-11 07:50:19  No: 45389

Questさん、asキャストがLabelになってます…。
>      ListBox1.Items.Add(Self.Components[i].Name + '/' + (Self.Components[i] as Label).Caption);

ここの as Label を as TLabelにする必要があります。 >フクロウさん

------------------------------------------------------------------------------------

TLabelはVCL内で作られた”絵”なので、「ウインドウハンドルを取得」ということ自体が成り立ちません。

・ 自分自身(Delphi製アプリ)のTLabel → 自分の中ですからどうにでも出来ます。
・ 自作のDelphi製アプリのラベルを他のアプリから → TStaticTextにすれば出来るはずです。
・ 他のDelphi製アプリのTLabel → 無理です。
・ 他のDelphi製でないアプリのラベル → スタティックコントロールであれば出来るはずです。

最終的に何をしたいのでしょう?


Quest  2013-10-11 11:49:03  No: 45390

Harryさん、フォローどうもm(__)m
うーん、実際動かしたヤツをコピペしたんだけどなぁ。
あっ、そうか。コピペした時に途中で改行されたんで
それを削除するときに消しすぎたのか。


フクロウ  2013-10-15 23:55:00  No: 45391

返事遅れてすいません。

>>Questさん
サンプルありがとうございます。
自EXE内でしたら取得できるのですが、他EXEでラベルを取得したいのです。
私の出したサンプルが悪かったですね。申し訳ないです・・・。

>>Harryさん
やりたいことは、マニュアルの雛形作成です。
delphiでコンパイル済のEXEの指定フォームから
ラベルのCaptionとHintの一覧を取得するイメージです。
(※できればTop,Left順に)

ウィンドウの取得は今のところ、
以下のようにTimerとGetForeGroundWindowを利用して取得する予定です。

--------------------------------------------
ThisWin : HWND;
TgtWin  : HWND;

procedure TForm1.Button4Click(Sender: TObject);
begin
  ThisWin := GetForeGroundWindow;
  Timer1.Enabled :=True;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
var
  SL:TStringList;
  WA:THWndArray;
  i,Num,Res:integer;
begin
  TgtWin := GetForeGroundWindow;
  if TgtWin <> ThisWin then begin
    Timer1.Enabled:=False;
    Res := MessageBox(0,
                      PChar('['+ GetWindowTextStr(TgtWin) + '] この画面でいいですか?'),
                      '確認',
                      MB_YESNOCANCEL or MB_TOPMOST);
    if Res=IDYES then begin
      (TgtWinのラベル一覧を表示)
    end;
  end;
end;
--------------------------------------------

Harryさんの選択肢で言うと、
「自作のDelphi製アプリのラベルを他のアプリから 」
「他のDelphi製アプリのTLabel」
のどちらかになるかと思います。
ちょっとこの選択肢の違いがわからないのですが、
TStaticTextを利用すれば実現可能でしょうか?


フクロウ  2013-10-15 23:57:33  No: 45392

すいません。
マニュアルとは操作マニュアルという意味のマニュアルです。


Quest  2013-10-16 07:10:21  No: 45393

直接の回答ではないですが、ヒントになれば。

実行中のEXEからLabelの情報を取り出すのは無理そうですが
XN Resourece EditorというフリーのツールでDelphi製EXEを開くと
リソースとしてRC Dataという項目に各フォームの情報(*.dfmの内容)が
保存されているようです。
たぶん、フォームをテキスト形式で保存しないとダメだとは思いますが
各パラメータなどは記載されているので、これを解析すれば位置や大きさ
キャプションなどはわかると思います。

Delphi2007で作ったEXEで確認しました。


Quest  2013-10-16 07:12:49  No: 45394

あ、キャプションやヒントが実行時に動的に設定される場合は
当然取得できないです(^^ゞ


Harry  2013-10-16 07:55:07  No: 45395

フクロウさん
もう少し細かく書きますとこういうことです。

(1) フクロウさん自作するDelphi製アプリのラベルを他のアプリから → TLabelをTStaticTextに変更すれば出来るはずです。
(2) フクロウさんが制作に関与できない、どこかのDelphi製アプリのTLabel → 無理です。TLabelはウインドウではありませんので。

今回は(1)のパターンですよね?
ならばTStaticTextを試してみてはいかがでしょうか。変更できるなら、ですが。

しかし自作のアプリから取得するなら、TLabelのままにして「マニュアル作成モード」でも仕込む方が楽なように思えます。
リリースでは除去してもいいですし。つまり↓この状態にするのです。
>・ 自分自身(Delphi製アプリ)のTLabel → 自分の中ですからどうにでも出来ます。

------------------------------------------------------------------------------------

Questさん
それ自分も思いました(笑)
ただし Resource Hackerですが。ちなみにこれもDelphi製。
でもこの用途だとチョー面倒に思えてしまいます。だって、自作アプリなら手元に*.dfmありますから…。


フクロウ  2013-10-17 01:35:32  No: 45396

>>Questさん
ほー。このようなツールがあるのですね。
ただ、dfmがバイナリ形式で正常に読込できませんでした。

>>Harryさん
それで言うなら、(1)ですね。
ただ、EXEが300個以上あり、担当者も複数に渡ります。
ですので、TStaticTextへの変更や、マニュアルモードの仕込みは
結構難しいです。

Resource Hackerはバイナリ形式のdfmでも確認できましたが、
しかし、delphiで見れる、dfmがツリービュー形式で見れるというだけで、
TLabelのリストはちょっと難しいかなと思っています。。。

ちょっと、ツール化は難しいそうなので、コピペで泥臭くがんばろうかなと今は思っています。


D  2013-10-18 05:22:51  No: 45397

>ただ、EXEが300個以上あり、担当者も複数に渡ります。
>ですので、TStaticTextへの変更や、マニュアルモードの仕込みは
>結構難しいです。

>ちょっと、ツール化は難しいそうなので、コピペで泥臭くがんばろうかなと今は思っています。

  目的のEXEのソースコードにフクロウさんがアクセスできるのならばツール化は可能と思います。
(書き込みできなくても読み込みのみできればOK)

・まずアプリ中のフォームのLabelを列挙するツールを作ります。
・そのツールのプロジェクトに目的のEXEのフォームを追加します。
・「プロジェクト」の「オプション」で追加したフォームを「自動作成の対象」にします。
・ツールを実行。
場合によっては必要なdcuがないと怒られることもあるのでその場合はそのdcuの元になっているソースコードをプロジェクトに追加します。

↓こんな感じかなと。

procedure TForm1.Button1Click(Sender: TObject);
  procedure LabelGet(AForm: TForm);
  var
    i : Integer;
    l_Label : TLabel;
  begin
    for i := 0 to AForm.ComponentCount -1 do
    begin
      if (AForm.Components[i] is TLabel) then
      begin
        l_Label := TLabel(AForm.Components[i]);
        //Top,Leftの順に並べ替えるため4桁の幅の数値にしてしまう
        ListBox1.Items.Add(Format('%s %.4d %.4d %s "%s"/%s', [AForm.Name, l_Label.Top, l_Label.Left, l_Label.Name, l_Label.Caption, l_Label.Hint]));
      end;
    end;
  end;
var
  i : Integer;
  l_Form : TForm;
begin
  ListBox1.Sorted := True;
  ListBox1.Items.Clear;

  for i := 0 to Screen.FormCount -1 do
  begin
    l_Form := Screen.Forms[i];
    if (l_Form <> Self) then
    begin
      LabelGet(l_Form);
    end;
  end;
end;


Harry  2013-10-18 09:53:20  No: 45398

仕込みすればいいのに…と思ってましたが、
>ただ、EXEが300個以上あり、担当者も複数に渡ります。
(^_^;;;;;;;;;;;;;;;;
それでは仕込みは厳しいですねー。

>ちょっと、ツール化は難しいそうなので、コピペで泥臭くがんばろうかなと今は思っています。
フクロウさん、あきらめ早過ぎっすよ!
可能性がありそうな方法を分類すると、以下のようになると思います。

(a) 仕込み型 … 当該のプロジェクトにTLabelの情報を取得するコードを仕込んでコンパイル。(Questさん、私)
(b) フォーム拝借型 … 当該のプロジェクトからフォームだけ取り出し、TLabelの情報を取得するコードと合わせてコンパイル。(Dさん)
(c) ソース解析型 … 当該のプロジェクトからフォームの*.dfmを抜き出し、解析してTLabelの情報を取得。(一番現実的か?)
(d) EXEから取得型 … 当該のEXEのリソースからフォーム情報を取り出し、解析してTLabelの情報を取得。(かなり面倒?)

(a)と(b)が既出ですが、(c)のソース解析型が一番真っ当な気がします。
で、(d)の方法も出来そうな気がしてきたので、ちょっと研究してみます。うまくいったらお伝えします。
あれ、開発環境はどうなってますか? dfmがバイナリどうたらということはかなり古い?

------------------------------------------------------------------------------------

あと、自分も(a)の仕込み型のサンプルを作ってたので、せっかくですからここに置いておきます。
TLabelだけじゃ面白くないので、TControlなら取得するようにしてみました。(しかしかなり適当)

仕込むプロジェクトでフォームを新規作成、フォームをダブルクリック、フォームのNameプロパティをGetTCtrlFormに変更、
TMemoを配置、下記のコードをコピー&貼り付けすればOkです。

type
  TGetTCtrlForm = class(TForm)
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private 宣言 }
    procedure ScreenActiveFormChange(Sender: TObject);
    procedure GetTControlData(AForm: TForm; AStrings: TStrings);
  public
    { Public 宣言 }
  end;

var
  GetTCtrlForm: TGetTCtrlForm;

implementation

{$R *.dfm}

uses
  ClipBrd;

type
  TControlEx = class(TControl); // protectedなプロパティを取り出すための仕掛け

procedure TGetTCtrlForm.FormCreate(Sender: TObject);
begin
  Memo1.Font.Name:='MS ゴシック';
  Self.FormStyle:=fsStayOnTop;
  Self.Show;
  Screen.OnActiveFormChange:=Self.ScreenActiveFormChange;
end;

procedure TGetTCtrlForm.FormDestroy(Sender: TObject);
begin
  Screen.OnActiveFormChange:=nil;
end;

procedure TGetTCtrlForm.ScreenActiveFormChange(Sender: TObject);
var
  SL: TStringList;
begin
  SL:=TStringList.Create;
  try
    Self.GetTControlData(Screen.ActiveForm, SL);
    SL.Sort; // ソートすれば Top、Left順 になる
    SL.CommaText:=StringReplace(SL.Text, sLineBreak, ',', [rfReplaceAll]); // ゴニョゴニョして整形
    Memo1.Lines.Assign(SL);    // TMemoに出力
    ClipBoard.AsText:=SL.Text; // クリップボードにも出力
  finally
    SL.Free;
  end;
end;

procedure TGetTCtrlForm.GetTControlData(AForm: TForm; AStrings: TStrings);
var
  TempSL: TStringList;
  I: Integer;
  AControl: TControl;
begin
  AStrings.Add('------------------------------------------');
  TempSL:=TStringList.Create;
  try
    for I:=0 to AForm.ComponentCount-1 do begin
      if not (AForm.Components[I] is TControl) then Continue; // TLabelのみにしても良い

      AControl:=AForm.Components[I] as TControl;
      TempSL.Clear;
      TempSL.Add(Format('(%4d,%4d) %d/%d', [AControl.Top, AControl.Left, I+1, AForm.ComponentCount]));
      TempSL.Add(AControl.Name);
      if not (AControl is TCustomMemo) then begin
        TempSL.Add(TControlEx(AControl).Caption); // Captionは無理やりほじくり出す
      end else begin
        TempSL.Add('* Caption N/A *'); // TMemoとかのCaptionにはText全部が入ってるからナシで
      end;
      TempSL.Add(AControl.Hint);
      TempSL.Add('------------------------------------------');
      AStrings.Add(TempSL.CommaText);
    end;
  finally
    TempSL.Free;
  end;
end;


Quest  2013-10-18 22:41:23  No: 45399

では、私は「(c)ソース解析型」の取っ掛かりの部分だけ。
フォームにメモ、ボタン、オープンダイアログを置いて
ボタンクリックイベントに
procedure TForm1.Button1Click(Sender: TObject);
var
  dfmName: string;
  fs: TFileStream;
  ms: TMemoryStream;
begin
  if OpenDialog1.Execute then
  begin
    dfmName := OpenDialog1.FileName;
    fs := TFileStream.Create(dfmName, fmOpenRead);
    ms := TMemoryStream.Create;
    try
      try
        ObjectResourceToText(fs, ms);
        ms.Position := 0;
        Memo1.Lines.LoadFromStream(ms);
      except
        Memo1.Lines.LoadFromStream(fs);
      end;
    finally
      ms.Free;
      fs.Free;
    end;
  end;
end;
を記述。
核の部分のtry〜exceptの部分はいい加減で
バイナリでないdfmを読むとエラーになるので
エラーになったらテキストだろうと限定しています。
なので、他のエラーの場合は考えていません。

あと問題は、Delphi6(多分?)以降は全角文字が
"#12345"のような表記になっているのでこれの変換が必要です。
例)
Font.Name = #65325#65331' '#26126#26397
これは「MS 明朝」です。
IDE自身がフォームを読み込むんだから、内部にそれらしい変換関数が
あっても良さそうですけど。
この辺はDEKOさんが詳しいかも。

あとは(一番面倒な?)語句解析を何とかすれば
全フォームを一気に解析も夢ではないかと(^^ゞ
Label限定ならそれほど難しくはないかも。


Quest  2013-10-18 23:49:38  No: 45400

さらに、LabelのHintとCaptionだけ羅列するようにしてみました。

procedure TForm1.Button1Click(Sender: TObject);
var
  dfmName: string;
  fs: TFileStream;
  ms: TMemoryStream;
begin
  if OpenDialog1.Execute then
  begin
    dfmName := OpenDialog1.FileName;
    fs := TFileStream.Create(dfmName, fmOpenRead);
    ms := TMemoryStream.Create;
    try
      try
        ObjectResourceToText(fs, ms);
        Memo1.Lines.Text := ExtractLabelParam(ms);
      except
        Memo1.Lines.Text := ExtractLabelParam(fs);
      end;
    finally
      ms.Free;
      fs.Free;
    end;
  end;
end;

function TForm1.ExtractLabelParam(st: TStream): string;
var
  s: string;
  sLine: TStringList;
  LabelFlag: boolean;
begin
  Result := '';
  LabelFlag := False;
  sLine := TStringList.Create;
  try
    st.Position := 0;
    sLine.LoadFromStream(st);
    for s in sLine do
    begin
      if (Pos('object', s) > 0) and (Pos(': TLabel', s) > 0) then
      begin
        Result := Result + 'Name : '+Copy(s, Pos('object', s)+7, Pos(': TLabel', s)-(Pos('object', s)+7)) + #13;
        LabelFlag := True;
      end;
      if LabelFlag then
      begin
        if Pos('Caption =', s) > 0 then
          Result := Result + '  ' + Trim(s) + #13;
        if Pos('Hint =', s) > 0 then
          Result := Result + '  ' + Trim(s) + #13;
        if Trim(s) = 'end' then
        begin
          Result := Result + #13;
          LabelFlag := False;
        end;
      end;
    end;
  finally
    sLine.Free;
  end;
end;

あとは"#12345"の全角文字をどうにかするだけ(^_^;)
あ、これはDelphi2007でテストしてます。


Quest  2013-10-19 02:47:35  No: 45401

一応、力技でコード変換してみました。
Delphi2009以降では動かない(コンパイルできない)と思いますが(^_^;)

function TForm1.ExtractLabelParam(st: TStream): string;
var
  s: string;
  sLine: TStringList;
  LabelFlag: boolean;
  function CodeToString(const Str: string): string;
  var
    ix: integer;
    c: char;
    cnt: integer;
    buf: string;
    wc: WideChar;
  begin
    Result := '';
    ix := 1;
    while Length(Str) >= ix do
    begin
      c := Str[ix];
      if c <> '''' then
      begin
        if c = '#' then
        begin
          cnt := 0;
          buf := '';
          Inc(ix);
          c := Str[ix];
          while c in ['0'..'9'] do
          begin
            buf := buf + c;
            Inc(cnt);
            Inc(ix);
            c := Str[ix];
          end;
          Dec(ix);
          if cnt = 5 then
          begin
            wc := WideChar(StrToInt(buf));
            Result := Result + wc;
          end
          else
            Result := Result + '#' + buf;
        end
        else
          Result := Result + c;
      end;
      Inc(ix);
    end;
  end;
begin
  Result := '';
  LabelFlag := False;
  sLine := TStringList.Create;
  try
    st.Position := 0;
    sLine.LoadFromStream(st);
    for s in sLine do
    begin
      if (Pos('object', s) > 0) and (Pos(': TLabel', s) > 0) then
      begin
        Result := Result + 'Name : '+Copy(s, Pos('object', s)+7, Pos(': TLabel', s)-(Pos('object', s)+7)) + #13;
        LabelFlag := True;
      end;
      if LabelFlag then
      begin
        if Pos('Caption =', s) > 0 then
          Result := Result + '  Caption = ''' + CodeToString(Copy(s, Pos('Caption =', s)+10, Length(s))) + ''''#13;
        if Pos(' Hint =', s) > 0 then
          Result := Result + '  Hint = ''' + CodeToString(Copy(s, Pos('Hint =', s)+7, Length(s))) + ''''#13;
        if Trim(s) = 'end' then
        begin
          Result := Result + #13;
          LabelFlag := False;
        end;
      end;
    end;
  finally
    sLine.Free;
  end;
end;


Harry  2013-10-19 08:37:00  No: 45402

Questさんすごいです。
CodeToString関数だけ抜き出して試しましたが、完璧ですね!

…で、力技のところ誠に恐縮なんですが、、、
>IDE自身がフォームを読み込むんだから、内部にそれらしい変換関数があっても良さそう
それ、ClassesにTParserがありました \(^o^)/

// dfm内部形式の文字列をWideStringに変換
// 例1:#65325#65331' '#65328#12468#12471#12483#12463 → MS Pゴシック
// 例2:'Label1' → Label1
// 例3:'Button2'#12384#12424 → Button2だよ
// 例4:Delphi → Delphi
function TokenToWideString(Token: String): WideString;
var
  Stream: TStringStream;
  Parser: TParser;
begin
  Stream:=TStringStream.Create(Token);
  try
    Parser:=TParser.Create(Stream);
    try
      Result:=Parser.TokenWideString;
      if Result='' then Result:=Parser.TokenString;
    finally
      Parser.Free;
    end;
  finally
    Stream.Free;
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
const
  aStr: array[0..3] of String =(
    '#65325#65331'' ''#65328#12468#12471#12483#12463',
    '''Label1''',
    '''Button2''#12384#12424',
    'Delphi'
  );
var
  I: Integer;
begin
  for I:=Low(aStr) to High(aStr) do begin
    Memo1.Lines.add(CodeToString(aStr[I]));
    Memo1.Lines.add(TokenToWideString(aStr[I]));
  end;
end;

それと、EXEのリソースをObjectBinaryToTextで変換し、それを使ってdfmの解析も出来ました。
もう少しパーツを作ってつなぎ合わせれば(d)が作れそうな感じです。


Quest  2013-10-19 22:33:57  No: 45403

Harryさん、こんにちは。

>それ、ClassesにTParserがありました \(^o^)/
ぬぅ〜、やっぱりありましたか(笑)
古い人間なんで、無ければ作る(しかもベタに)タチなんで(^^ゞ

スレ主さんそっちのけで、なんかすんませんm(__)m


フクロウ  2013-10-22 08:23:19  No: 45404

>>レスをいただいたみなさま
諦めかけてスレを見ていなかった間に
こんなにもレスを。
ありがとうございます。

>>Dさん
ソースへの読み書きは可能ですが、
ソース量はもちろん、ソースが画一的でないため、
手動での書き込みは難しいです。
dcuの前に、すべてのEXEが複雑に継承構成をとっているため、
継承元がないと怒られることが多数あり、
>>・そのツールのプロジェクトに目的のEXEのフォームを追加します。
が現実的に難しかったです。

>>Harryさん
>>フクロウさん、あきらめ早過ぎっすよ!
す、すいません!!
できるところまでがんばります!

開発環境はDelphi3です。
(c)案は当初より考えてはいましたが、は個人的に難しいのでは?と思っています。
というのも継承構成が複雑で、継承先のdfmには継承元のdfmの情報は直接持っていないため、
継承構成を懸案しながら、dfmの解析が必要だからです。

ですので、(d)案で動いてTlabelの問題にぶつかったという経緯です。

>>Questさん
バイナリでも読込はできましたよ。
「それらしい変換関数」とやらいるんでしょうかね。

色々ソースを検討いただき本当にありがとうございます。
ただ、(c)案の方向でソースを考えて頂いているように感じています。
Questさんの
>>それと、EXEのリソースをObjectBinaryToTextで変換し、それを使ってdfmの解析も出来ました。
>>もう少しパーツを作ってつなぎ合わせれば(d)が作れそうな感じです。
これはどのような、方法で解析されたのでしょうか?


Quest  2013-10-22 20:06:36  No: 45405

> Questさんの
> >>それと、EXEのリソースをObjectBinaryToTextで変換し、それを使ってdfmの解析も出来ました。
> >>もう少しパーツを作ってつなぎ合わせれば(d)が作れそうな感じです。
> これはどのような、方法で解析されたのでしょうか?
これはHarryさんのレスですね。

ただ、XN Resourece EditorがEXEからリソースを抜き出せているので
「なんらかの方法」でリソースを抽出できれば、あとはその中の
"RC Data"をdfmファイルから読むように解析すればいいんじゃないでしょうか。

肝心の「なんらかの方法」はHarryさんにパス(^^ゞ


Harry  2013-10-23 07:58:03  No: 45406

※下記は前のレスのセルフフォローに過ぎません。なぜならば、これですべての問題が解決するわけではないので。
-----------------------------------------------------------------------------------------------
dfmの簡易的解析は十分達成されたと思ってたのですが、基本的な所で落とし穴がありました。
どうもdfmファイル内で一行を255文字以内に納めるため、適当な所でちょん切って改行する仕様みたいです。

模擬的な例を示しますと、以下のような形です。
#12487+#12523 + ' '+#9734 +
#12501+ #12449 +#12452
定数として表現するとこのようになります。
'#12487+#12523 + '' ''+#9734 +'#$0D#$0A'#12501+ #12449 +#12452'
これをデコードすると、「デル ☆ファイ」になります。

これに対応するには、先に提示したTokenToWideString関数の一番内側のブロックを以下に差し替えればOkです。
      if Parser.Token in [toString, toWString] then begin
        Result:=Parser.TokenWideString;
        while Parser.NextToken='+' do begin
          Parser.NextToken;
          if not (Parser.Token in [toString, toWString]) then Parser.CheckToken(toString);
          Result:=Result+Parser.TokenWideString;
        end;
      end else begin
        Result:=Parser.TokenString;
      end;
ちなみに、(OpenCLXの)ClassesにあるObjectTextToBinaryの関数内関数内関数をパク…もとい、参考にしました。
-----------------------------------------------------------------------------------------------

これで万事Ok…ではないです。この関数に渡す前に複数トークンの切り出しと連結を行わないといけませんし、
そもそも「1行=1プロパティ」となってない以上、簡易的解析では無理だと悟りました。(後でもっと無理と判明。)

で、Classesをいろいろ眺めていたら、TReaderを使えば出来そうだと気付きました。
TParserがトークン(最小単位)の切り出ししか行えないのに対し、TReaderならdfmの構造をチェックしつつ、意味の
あるデータとして取り出してくれます。
自力であーだこーだしてみたんですが無理っぽいので、使用例を検索。ぜんぜん無い…かと思ったらありました!

TXMLDocument - Delphi@WCIMH(いつも心に工事中!) - YTさんのサイト
http://hp.vector.co.jp/authors/VA028375/delphi/sample_xml.html

↑こちらはXMLに変換するというサンプルですが、出力を変えればどうにでも出来ます。

これで目途が立った…と思ったんですが、そうは甘くなかったですね。
ということで次に続きます。


Harry  2013-10-23 08:12:33  No: 45407

フクロウさん
>開発環境はDelphi3です。
え、、、それは未知領域です。。。。 少なくとも動的配列は禁止ですね。

>(d)案で動いてTlabelの問題にぶつかったという経緯です。
念のため。いいえ、(d)案はウインドウを取得するのではなく、EXEのリソースからdfmのデータを取り出します。

>(c)案は当初より考えてはいましたが、は個人的に難しいのでは?と思っています。
>というのも継承構成が複雑で、継承先のdfmには継承元のdfmの情報は直接持っていないため、
>継承構成を懸案しながら、dfmの解析が必要だからです。
その通りです!!
これは(d)案にもまったく同じことが言えます。ただし、(c)・(d)案ともに継承元(dfmファイル/リソース)は取得できます
ので、やる気次第ですね。(理屈は簡単ですが、プログラミング的に難しい。)

ちなみに、以下はサンプルのTAboutBoxフォームを継承した場合のTAboutBox2のdfmです。
TLabelの位置だけを変更してみたので、Left = 136 が追加されています。
inherited AboutBox2: TAboutBox2
  Caption = 'AboutBox2'
  PixelsPerInch = 96
  TextHeight = 12
  inherited Panel1: TPanel
    inherited ProductName: TLabel
      Left = 136
    end
  end
end
1つのフォーム中では名前の衝突は無いはずだからツリー的管理は必要なく、先に継承元を読み込んで単純に
オーバーライドしていけば可能…と思ってます。(言うは易し。)
継承のinheritedの他に、inlineてのもある模様…これはフレームか!
名前の衝突が無いという前提がいきなり崩れました…。どうすれば良いのか…。

>これはどのような、方法で解析されたのでしょうか?
Questさんとは違ったアプローチですが、Name、Top、Left、Caption、Hintだけ取得する簡易的なものです。
上からプロパティを読んでいき、objectかendがあったら取捨選択して記録、再帰。ソートして出力…みたいな。
今となっては徒労であったことが判明したので、具体的なコードは割愛します。

Questさん
>「なんらかの方法」でリソースを抽出できれば
ここはとても簡単です。普通にリソースを読むだけなので。↓こんな感じで。

// EXEのリソースからdfmのテキストを得る関数、EXENameには'TFORM1'とかを指定する
//
// ヘルプのObjectBinaryToTextの例より作成
// 検索順序:ReadComponentResFile→ObjectTextToResource→ObjectResourceToText→ObjectBinaryToText
function CompoResourceToString(const EXEName, ResName: String): String;
var
  AInstance: HMODULE;
  ResStream: TResourceStream;
  StrStream: TStringStream;
begin
  AInstance:=LoadLibrary(PChar(EXEName));
  if AInstance=0 then Exit;
  try
    ResStream:=TResourceStream.Create(AInstance, ResName, RT_RCDATA);
    try
      StrStream:=TStringStream.Create('');
      try
        ObjectBinaryToText(ResStream, StrStream);
        Result:=StrStream.DataString;
      finally
        StrStream.Free;
      end;
    finally
      ResStream.Free
    end;
  finally
    FreeLibrary(AInstance);
  end;
end;

↑これはObjectBinaryToTextでテキスト化してますが、TReaderだとバイナリのストリームのまま放り込むことになります。
以下は余談。
TReaderで扱えるのはリソースヘッダ無しのバイナリdfmデータしか扱えませんが、変換すればテキストも大丈夫。
dfmデータは以下の3種類あると思います。若干ややこしいです。。。
  ・ テキストの*.dfm … ObjectTextToBinaryで変換すればOk。
  ・ バイナリの*.dfm … リソースヘッダが付いてるので、TStream.ReadResHeaderでリソースヘッダを読み飛ばせばOk。
  ・ EXEリソース内のバイナリdfmデータ … リソースヘッダは付いてないので、そのままでOk。

ということで、TReaderを使ったアプローチもまだ考え中です。
フクロウさんにお聞きしておきたいことがいくつかあります。
  ・ 制作に使用されたバージョンとは別に、現在使用できるDelphiでもっと新しいのはないのでしょうか?
  ・ 動的に生成、または変化するCaptionとか無いですよね? あったら前提が崩れます。
  ・ 出力はTop、Left順の一覧テキストで良いのでしょうか。


Harry  2013-10-23 08:28:25  No: 45408

↑いま見直したら、関数の冒頭の肝心のコメントが間違ってました。正解はこちら↓
// EXEのリソースからdfmのテキストを得る関数、ResNameには'TFORM1'とかを指定する

関数名もイマイチでした…。↓こうしましょう。
function EXEResourceToString(const EXEName, ResName: String): String;


※返信する前に利用規約をご確認ください。

※Google reCAPTCHA認証からCloudflare Turnstile認証へ変更しました。






  このエントリーをはてなブックマークに追加