2012. november 9., péntek

Use a TPanel as a host for child windows (MDI simulation)


Problem/Question/Abstract:

I was wondering if someone can offer assistance with this application. Basically the application is for configuring our system. At present it is a MDI where child windows are various functions (security, report options, etc.). The number of functions are growing, currently around 15, which means an increase in different child forms and, overall, a growing exe. I would like the child forms to be standalone programs or dlls which can appear in the control program as child windows and also execute by themselves. Only one child form is displayed at a time and always maximised within the parent window. I did see some code about that provided for a dll as a child form, but this would not help as a standalone execution.

Answer:

This is an interesting problem. As it happens it is possible in Win32 to make another processes window appear like a child window in ones own windows. It does not work quite as well as a true child in your own process but takes care about moving the pseudo-child with your menu app.

The general design is this: the main/menu app has a form with menu, perhaps tool and status bars, and a client-aligned panel that will serve as the host for the child windows. It reads the available child apps from INI file or registry key and builds a menu or selection list from this info. On user request it launches the appropriate child app and passes the panels window handle on the commandline. The child app checks the command line, if there are no parameters it rans as designed, if there is a parameter it reads it, removes its border and bordericon, parents itself to the passed window handle and sizes itself to its client area. It also sends a message with *its* window handle to the panels parent (the main app form) to register itself. The main app can close the child with this handle and also resize it when the user resizes the main app.

Main app: has a menu with two entries (OpenMenu, CloseMenu), a toolbar with two buttons attached to the same events as the two menus, a statusbar, a client-aliged panel.

unit MenuApp;

interface

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

const
  UM_CHILDREGISTER = WM_USER + 111;
  UM_CHILDUNREGISTER = WM_USER + 112;

type
  TUmChildRegister = packed record
    msg: Cardinal;
    childwnd: HWND;
    unused: Integer;
    result: Integer;
  end;
  TUmChildUnregister = TUmChildregister;

  TForm1 = class(TForm)
    MainMenu1: TMainMenu;
    OpenMenu: TMenuItem;
    StatusBar1: TStatusBar;
    ToolBar1: TToolBar;
    ToolButton1: TToolButton;
    CloseMenu: TMenuItem;
    ToolButton2: TToolButton;
    Panel1: TPanel;
    procedure OpenMenuClick(Sender: TObject);
    procedure CloseMenuClick(Sender: TObject);
    procedure Panel1Resize(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
    { Private declarations }
    FChildAppHandle: HWND;
    procedure UMChildRegister(var msg: TUmChildRegister);
      message UM_CHILDREGISTER;
    procedure UMChildUnRegister(var msg: TUmChildUnRegister);
      message UM_CHILDUNREGISTER;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

uses
  shellapi;

{$R *.DFM}

procedure TForm1.OpenMenuClick(Sender: TObject);
var
  path, param: string;
begin
  if FChildAppHandle = 0 then
  begin
    path := ExtractFilePath(Application.Exename) + 'childAppProj.exe';
    param := '$' + IntTohex(panel1.handle, 8);
    ShellExecute(handle, 'open', pchar(path), pchar(param), nil, SW_SHOWNORMAL);
  end
  else
    ShowMessage('Child already loaded');
end;

procedure TForm1.CloseMenuClick(Sender: TObject);
begin
  if FChildAppHandle <> 0 then
    SendMessage(FchildApphandle, WM_CLOSE, 0, 0);
end;

procedure TForm1.Panel1Resize(Sender: TObject);
begin
  if FChildAppHandle <> 0 then
    MoveWindow(FchildAppHandle, 0, 0, Panel1.ClientWidth, Panel1.ClientHeight, true);
end;

procedure TForm1.UMChildRegister(var msg: TUmChildRegister);
begin
  FChildAppHandle := msg.childwnd;
end;

procedure TForm1.UMChildUnRegister(var msg: TUmChildUnRegister);
begin
  if FChildAppHandle = msg.childwnd then
    FChildAppHandle := 0;
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  if FChildAppHandle <> 0 then
    SendMessage(FchildApphandle, WM_CLOSE, 0, 0);
end;

end.

Child app has a couple of edits, two buttons, a memo.

unit ChildApp;

interface

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

type
  TForm2 = class(TForm)
    Edit1: TEdit;
    Edit2: TEdit;
    Edit3: TEdit;
    Button1: TButton;
    Memo1: TMemo;
    Button2: TButton;
    ApplicationEvents1: TApplicationEvents;
    procedure Button1Click(Sender: TObject);
    procedure ApplicationEvents1Activate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormResize(Sender: TObject);
  private
    { Private declarations }
    FMenuAppWnd: HWND;
    FParentPanelWnd: HWND;
  public
    { Public declarations }
    constructor Create(aOwner: TComponent); override;
    procedure CreateWnd; override;
    procedure DestroyWnd; override;
  end;

var
  Form2: TForm2;

implementation

{$R *.DFM}

const
  UM_CHILDREGISTER = WM_USER + 111;
  UM_CHILDUNREGISTER = WM_USER + 112;

procedure TForm2.Button1Click(Sender: TObject);
begin
  close;
end;

procedure TForm2.ApplicationEvents1Activate(Sender: TObject);
begin
  if FMenuAppWnd <> 0 then
    SendMessage(FMenuAppWnd, WM_NCACTIVATE, 1, 0);
  memo1.lines.add('Activated');
end;

constructor TForm2.Create(aOwner: TComponent);
begin
  if ParamCount > 0 then
  begin
    FParentPanelWnd := StrToInt(ParamStr(1));
    FMenuAppWnd := Windows.GetParent(FParentPanelWnd);
  end;
  inherited;
  if FParentPanelWnd <> 0 then
  begin
    Borderstyle := bsNone;
    BorderIcons := [];
    {remove taskbar button for the child app}
    SetWindowLong(Application.Handle, GWL_EXSTYLE,
      GetWindowLong(Application.Handle, GWL_EXSTYLE)
      and not WS_EX_APPWINDOW or WS_EX_TOOLWINDOW);
  end;
end;

procedure TForm2.CreateWnd;
var
  r: Trect;
begin
  inherited;
  if FMenuAppWnd <> 0 then
  begin
    SendMessage(FMenuAppWnd, UM_CHILDREGISTER, handle, 0);
    Windows.SetPArent(handle, FParentPanelWnd);
    Windows.GetClientRect(FParentPanelWnd, r);
    SetBounds(r.left, r.top, r.right - r.left, r.bottom - r.top);
  end;
end;

procedure TForm2.DestroyWnd;
begin
  if FMenuAppWnd <> 0 then
    SendMessage(FMenuAppWnd, UM_CHILDUNREGISTER, handle, 0);
  inherited;
end;

procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  {Closing the main form does not fire DestroyWnd for some reason}
  if FMenuAppWnd <> 0 then
    SendMessage(FMenuAppWnd, UM_CHILDUNREGISTER, handle, 0);
end;

procedure TForm2.FormResize(Sender: TObject);
begin
  memo1.width := clientwidth - memo1.Left - 10;
  memo1.height := clientheight - memo1.Top - 10;
end;

end.

One problem I noted is that sometimes the main applications caption will loose the active look when switching between main and child despite the action taken in the childs Application.OnActivate handler.

Nincsenek megjegyzések:

Megjegyzés küldése