Automatizando las cosas
¿Qué tal si nos creamos una clase, o incluso un componente, que tras crearlo tome el dato del Formulario principal, le demos el dato del MenuItem del que queremos colgar los formularios secundarios que iremos añadiendo? Y si, además, hacemos que las respuestas al pulsar esas opciones del menú nos traiga a primer plano el formulario solicitado sin añadir código y esas que opciones aparezcan automáticamente y desaparezcan al cerrar cada formulario secundario, nos ahorrarímos mucho código.
Veamos la funcionalidad que buscamos:
1.- Añadir elementos al menú de forma automática al añadir formularios secundarios nuevos.
2.- Respuesta automática al seleccionar una opción de menú, colocando su formulario en primer plano.
3.- Eliminación automática del menú, al eliminar un formulario secundario sin escribir más código.
Con este planteamiento necesitamos controlar los mensajes que recibe el formulario principal para detectar el Mensaje WM_COMMAND que se envía al seleccionar una opción TMenuItem. También deberíamos tener un sistema para que los formularios secundarios notifiquen al principal cuando se cierran. Esto requiere detectar WM_CLOSE. ¿Como hacemos esto?
Esto requiere hacer SubClassing del formulario principal y de los secundarios. Básicamente un SubClassing realiza un cambio en la función de tratamiento de mensajes Windows de una ventana para cambiar su funcionalidad. Hacer un SubClassing a nivel API es más complejo pero con la VCL de delphi es muy sencillo, pues TWinControl dispone de un método privado y virtual denominado WndProc que equivale a la función de tratamiento de mensajes Windows a nivel API (WNDPROC), es decir, maneja mensajes del tipo WM_PAINT y no eventos como OnPaint. A su vez y para facilitar el SubClassing a alto nivel, los TWinColtrol disponen de un puntero público a un procedimiento denominado WindowProc que apunta a WndProc. Si hacemos que WindowProc apunte a nuestro procedimiento de tratamiento de mensajes, ya hemos hecho el SubClassing, con una simple asignación.
Vamos a comenzar por los formularios secundarios. Generalmente un SubClassing no pretende sustituir toda la función de tratamiento de mensajes sino añadir o cambiar alguna cosa y aprovechar la función original de esa ventana para que haga el resto del trabajo. Es por ello que para cada formulario deberemos conocer su WndProc original y escribir un procedimiento específico para su SubClassing. Esto lo vamos a hacer con una clase de la que crearemos una instancia para cada formulario. ¿Suena complicado? En realidad es sencillo.
Explicado esto y dado queremos que los formularios secundarios informen al formulario principal cuando se van a cerrar, lo primero que vamos a hacer es escribir una clase para realizar este SubClassing. Crearemos un objeto para cada formulario secundario dotándole de un nuevo método de tratamiento de mensajes que será un procedimiento que llamaremos SubClassWndProc. Éste va a añadir funcionalidad al formulario y cederá el control a su WndProc original. Necesitamos tantas versiones de SubClassWndProc como formularios secundarios y es por ello que lo encapsulamos en una clase de la que crearemos las instancias que nos hagan falta.
TMForm = class(TForm); // Clase interpuesta
TSubclasWindow = class
private
Form: TMForm;
procedure SubClassWndProc(var Message: TMessage);
public
MainForm: TForm;
constructor Create(AForm: TMForm);
destructor Destroy; override;
end;
implementation
constructor TSubclasWindow.Create(AForm: TMForm);
begin
MainForm:= nil;
Form:= TMForm(AForm);
Form.WindowProc:= SubClassWndProc; // Realizamos el SubClassing
end;
destructor TSubclasWindow.Destroy;
begin
Form.WindowProc:= Form.WndProc; // Deshacemos el SubClassing
inherited Destroy;
end;
procedure TSubclasWindow.SubClassWndProc(var Message: TMessage);
begin
if Message.Msg = WM_CLOSE then
begin
if MainForm <> nil then
PostMessage(MainForm.Handle, WM_MYCLOSE, WPARAM(Form), 0);
end;
Form.WndProc(Message); // Sigue tratando mensajes el procedimiento original del formulario
end;
Como vemos, esto nos permite tratar WM_CLOSE y enviar al Formulario principal un mensaje personalizado WM_MYCLOSE que informa en WParam cual formulario hijo se va a cerrar. Luego cede el control al Form.WndProc original que al ser privado, lo trampeamos con una clase interpuesta (TMForm). También podríamos haber usado una variable que lo almacenase.
Como necesitamos una función SubClassWndProc para cada formulario secundario, crearemos una instancia para cada uno y las controlaremos en una lista TList.
El SubClassing del formulario principal que es único, lo haremos sobre la clase núcleo de nuestro sistema, y se dedicará a manejar los mensajes WM_COMMAND enviados desde el menú. También dará respuesta a nuestro mensaje personalizado WM_MYCLOSE, enviado desde un formulario hijo que se cierra.
Esta sería la forma de tratar esos mensajes:
procedure TMultiWindowMenuControl.SubClassWndProc(var Message: TMessage);
var
Pos: integer;
begin
if Message.Msg = WM_MYCLOSE then // Un formulario secundario se cierra
Delete(TForm(Message.WParam)) // Borra de la lista el formulario que se cierra
else if Message.Msg = WM_COMMAND then // Una opción de menú se elige
begin
Pos:= GetMenuItenPos(LOWORD(Message.WParam)); // Encuentro el ItemMenu que es seleccionado por su ID en WParam
if Pos >= 0 then
TSubclasWindow(List.Items[Pos]).Form.BringToFront; // Pongo el formulario en primer plano
end;
Form.WndProc(Message); // Permitimos respuesta al resto de mensajes con el WndProc original
end;
Teniendo claro el tema del SubClassing, vamos a ver que funcionalidad necesitamos para nuestra clase principal TMultiWindowMenuControl.
1.- Un cosntructor que capture el Formulario principal, prepare su SubClassing y cree un TList para almacenar objetos TSubclasWindow.
2.- Un destructor que también deshaga los SubClassing.
3.- Asignar el MenuItem principal del que colgaremos los MenuItem para los formularios secundarios
4.- Un procedimiento Add para añadir un formulario secundario
5.- Un procedimiento Delete para eliminar un formulario secundario
6.- Un procedimiento Clear que elimine el control de todos los formularios secundarios
7.- Una forma de encontrar el MenuItem señalado por el ID que proporciona WM_COMMAND y que llamará GetMenuItenPos
8.- Un mensaje que enviará un formulario secundario al cerrarse y que el Formulario principal debe manejar liberando la opción de menú correspondiente. Lo llamaremos WM_MYCLOSE.
Con esto ya podemos automatizar la tarea que nos hemos encomendado:
type
TMultiWindowMenuControl = class(TComponent)
private
Form: TMForm;
FMenuItem: TMenuItem;
List: TList;
procedure SubClassWndProc(var Message: TMessage);
function GetMenuItenPos(MenuID: Cardinal): Integer;
public
procedure Add(NewForm: TForm);
procedure Delete(AForm: TForm);
procedure Clear;
procedure SetMenuItem(NewMenuItem: TMenuItem);
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property MenuItem: TMenuItem write SetMenuItem;
end;
implementation
constructor TMultiWindowMenuControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FMenuItem:= nil;
List:= TList.Create;
Form:= TMForm(AOwner);
if not (csDesigning in ComponentState) then
Form.WindowProc:= SubClassWndProc;
end;
destructor TMultiWindowMenuControl.Destroy;
begin
Form.WindowProc:= Form.WndProc;
Clear;
List.Free;
inherited Destroy;
end;
procedure TMultiWindowMenuControl.Clear;
var
Item: TMenuItem;
begin
While List.Count > 0 do
begin
TSubclasWindow(List.Items[0]).Free;
List.Delete(0);
Item:= FMenuItem.Items[0];
FMenuItem.Delete(0);
Item.Free;
end;
end;
procedure TMultiWindowMenuControl.SetMenuItem(NewMenuItem: TMenuItem);
var
Item: TMenuItem;
begin
// Si FMenuItem tenía Items, los traspaso al nuevo MenuItem
if FMenuItem = NewMenuItem then exit;
if NewMenuItem <> nil then
begin
while (FMenuItem <> nil) and (FMenuItem.Count > 0) do
begin
Item:= FMenuItem.Items[0];
FMenuItem.Delete(0);
NewMenuItem.Add(Item);
end;
end
else
Clear;
FMenuItem:= NewMenuItem;
end;
procedure TMultiWindowMenuControl.Add(NewForm: TForm);
var
i: integer;
SubClass: TSubclasWindow;
Item: TMenuItem;
begin
if FMenuItem <> nil then
begin
// Comprobando si existe
for i:=0 to List.Count-1 do
if TSubclasWindow(List.Items[i]).Form = NewForm then break;
if (i < List.Count) and (List.Count > 0) then exit;
SubClass:= TSubclasWindow.Create(TMForm(NewForm));
SubClass.MainForm:= Form;
List.Add(SubClass);
Item:= TMenuItem.Create(FMenuItem);
Item.Caption:= NewForm.Caption;
FMenuItem.Add(Item);
end;
end;
procedure TMultiWindowMenuControl.Delete(AForm: TForm);
var
i: integer;
Item: TMenuItem;
begin
//Buscando para borrar
i:= 0;
While (i < List.Count) and (TSubclasWindow(List.Items[i]).Form <> AForm) do inc(i);
if i < List.Count then
begin
TSubclasWindow(List.Items[i]).Free;
List.Delete(i);
Item:= FMenuItem.Items[i];
FMenuItem.Delete(i);
Item.Free;
end;
end;
procedure TMultiWindowMenuControl.SubClassWndProc(var Message: TMessage);
var
Pos: Integer;
begin
if Message.Msg = WM_MYCLOSE then
Delete(TForm(Message.WParam))
else if Message.Msg = WM_COMMAND then
begin
Pos:= GetMenuItenPos(LOWORD(Message.WParam));
if Pos <> -1 then
TSubclasWindow(List.Items[Pos]).Form.BringToFront;
end;
//OldFormWindowProc(Message);
Form.WndProc(Message);
end;
// Encuentra la posición del Item mediante su MenuID
// devuelve -1 si no aparece
function TMultiWindowMenuControl.GetMenuItenPos(MenuID: Cardinal): Integer;
begin
Result:= -1;
if FMenuItem <> nil then
begin
Result:= FMenuItem.Count -1;
while (Result>=0) and (MenuID <> GetMenuItemID(FMenuItem.Handle, Result)) do dec(Result);
end;
end;
Ambas clases se colocarán un una sola unit y la funcionalidad será tan simple como tener un formulario principal con un menú que nos permita crear formularios secundarios y con un MenuItem del que se colgarán los formularios hijos de forma automática. Además, tendrá un objeto de la clase TMultiWindowMenuControl.
// Constructor del formulario principal
procedure TForm1.FormCreate(Sender: TObject);
begin
MultiWindowMenuControl:= TMultiWindowMenuControl.Create(self);
MultiWindowMenuControl.MenuItem:= Ventanas;
end;
//Opciones de menú para crear formularios hijos
procedure TForm1.Opcion1Click(Sender: TObject);
begin
MultiWindowMenuControl.Add(Form2);
Form2.Show;
end;
procedure TForm1.Opcion2Click(Sender: TObject);
begin
MultiWindowMenuControl.Add(Form3);
Form3.Show;
end;
procedure TForm1.Opcion3Click(Sender: TObject);
var
NewForm: TForm2;
begin
// Creando dinámicamente un formulario
NewForm:= TForm2.Create(self);
NewForm.Caption:= 'Form4';
MultiWindowMenuControl.Add(NewForm);
NewForm.Show;
end;
//..............
No hay que hacer nada más para tener el control de los formularios secundarios de forma automática en un menú. La siguiente idea puede ser dotar a la clase de eventos que se disparen para WM_COMMAND y WM_MYCLOSE para que el usuario amplíe su funcionalidad, y también, porqué no, convertirlo en un componente visual.
Subo los archivos y un proyecto de ejemplo. El código está probado en delphi 7 y Berlin
Saludos.