Ir al contenido



Foto

Control de ventanas no MDI


  • Por favor identifícate para responder
8 respuestas en este tema

#1 escafandra

escafandra

    Advanced Member

  • Moderadores
  • PipPipPip
  • 3.852 mensajes
  • LocationMadrid - España

Escrito 19 septiembre 2019 - 01:49

Me pareció interesante resolver de forma automática una cuestión que preguntaron en CD. Se trata de llevar un control en un menú de los formularios creados o mostrados como secundarios de un Form principal. En realidad es útil cuando hay varias y no están en primer plano y en un número no determinado con anterioridad. Lógicamente estamos en el caso de una aplicación no MDI lo que es bastante habitual.

Voy a desarrollar el asunto como un tutorial sencillo en el que doy por sabidas algunas cosas y profundizo en las menos populares.

La idea más sencilla es hacerlo manualmente, de forma que se cree un MemuItem por cada formulario secundario y que cada Item tenga una forma de conocer su formulario asignado. Al mismo tiempo cada formulario debe conocer su MenuItem para proceder a su destrucción al cerrarse. La manera más sencilla de guardar estas cosas  es en el tag de cada componente mediante un puntero. De esta forma un OnClicMenuItem traería fácilmente a primer plano el formulario solicitado y si éste se cierra, podría destruir su MenuItem puesto que lo tiene guardado en su Tag.
 


delphi
  1. // Este procedimiento se ejecutará cada vez que creemos o mostremos un formulario
  2. procedure TForm1.Opcion(Form: TForm);
  3. var
  4. Item: TMenuItem;
  5. begin
  6. Form.Show;
  7. Item:= TMenuItem.Create(MainMenu1);
  8. Item.Caption:= Form.Caption;
  9. Item.Tag:= integer(Form);
  10. Item.OnClick:= VentanaOpcion;
  11. Form.Tag:= integer(Item);
  12. Ventanas.Add(Item);
  13. end;
  14.  
  15. procedure TForm1.Opcion1Click(Sender: TObject);
  16. begin
  17. Opcion(Form2);
  18. end;
  19.  
  20. procedure TForm1.Opcion21Click(Sender: TObject);
  21. begin
  22. Opcion(Form3);
  23. end;
  24.  
  25. // Este procedimiento traerá al primer plano el formulario solicitado en el menú.
  26. procedure TForm1.VentanaOpcion(Sender: TObject);
  27. begin
  28. TForm((Sender as TMenuItem).Tag).BringToFront;
  29. end;

Esto es muy sencillo pero para cada nuevo formulario que implementemos debe ser ajustado manualmente para controlar lo que pasa con el menú tras el cierre de un formulario.

En el formulario secundario pondremos el siguiente código en el evento OnClose:


delphi
  1. procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction);
  2. begin
  3. with Form1.Ventanas do
  4. Delete(IndexOf(TMenuItem(self.tag)));
  5. TMenuItem(self.tag).Free;
  6. // Action:= caFree;
  7. end;

Perfecto, esto es sencillo y funciona bastante bien pero no es automático, cada nuevo formulario que implementemos debe ser ajustado manualmente. Una primera aproximación sería crear una clase base de formulario secundario del que derivamos éstos, así controlaríamos bien su cierre.

 

Pero el objetivo de este hilo es más ambicioso, se trata de automatizar esta funcionalidad en la medida de lo posible y para ello hay que complicarse un poco la vida y adentrarse en un concepto que para algunos puede resultar engorroso. Se trata del SubClasing. Esto lo desarrollaremos en el siguiente post,

 

Subo un ejemplo del código hasta aquí expuesto. El código está probado en delphi 7 y Berlin

 

Saludos.

Archivos adjuntos


  • 1

#2 escafandra

escafandra

    Advanced Member

  • Moderadores
  • PipPipPip
  • 3.852 mensajes
  • LocationMadrid - España

Escrito 19 septiembre 2019 - 02:18

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.
 


delphi
  1. TMForm = class(TForm); // Clase interpuesta
  2.  
  3.   TSubclasWindow = class
  4.   private
  5.     Form: TMForm;
  6.     procedure SubClassWndProc(var Message: TMessage);
  7.   public
  8.     MainForm: TForm;
  9.     constructor Create(AForm: TMForm);
  10.     destructor Destroy; override;  
  11.   end;
  12.  
  13. implementation
  14.  
  15. constructor TSubclasWindow.Create(AForm: TMForm);
  16. begin
  17.   MainForm:= nil;
  18.   Form:= TMForm(AForm);
  19.   Form.WindowProc:= SubClassWndProc; // Realizamos el SubClassing
  20. end;
  21.  
  22. destructor TSubclasWindow.Destroy;
  23. begin
  24.   Form.WindowProc:= Form.WndProc; // Deshacemos el SubClassing
  25.   inherited Destroy;
  26. end;
  27.  
  28. procedure TSubclasWindow.SubClassWndProc(var Message: TMessage);
  29. begin
  30.   if Message.Msg = WM_CLOSE then
  31.   begin
  32.     if MainForm <> nil then
  33.       PostMessage(MainForm.Handle, WM_MYCLOSE, WPARAM(Form), 0);
  34.   end;
  35.   Form.WndProc(Message); // Sigue tratando mensajes el procedimiento original del formulario
  36. 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: 


delphi
  1. procedure TMultiWindowMenuControl.SubClassWndProc(var Message: TMessage);
  2. var
  3.   Pos: integer;
  4. begin
  5.   if Message.Msg = WM_MYCLOSE then // Un formulario secundario se cierra
  6.     Delete(TForm(Message.WParam)) // Borra de la lista el formulario que se cierra
  7.   else if Message.Msg = WM_COMMAND then // Una opción de menú se elige
  8.   begin
  9.     Pos:= GetMenuItenPos(LOWORD(Message.WParam));   // Encuentro el ItemMenu que es seleccionado por su ID en WParam
  10.     if Pos >= 0 then
  11.       TSubclasWindow(List.Items[Pos]).Form.BringToFront; // Pongo el formulario en primer plano
  12.   end;
  13.   Form.WndProc(Message); // Permitimos respuesta al resto de mensajes con el WndProc original 
  14. 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:


delphi
  1. type
  2.  
  3. TMultiWindowMenuControl = class(TComponent)
  4.   private
  5.     Form: TMForm;
  6.     FMenuItem: TMenuItem;
  7.     List: TList;
  8.     procedure SubClassWndProc(var Message: TMessage);
  9.     function  GetMenuItenPos(MenuID: Cardinal): Integer;
  10.   public
  11.     procedure Add(NewForm: TForm);
  12.     procedure Delete(AForm: TForm);
  13.     procedure Clear;
  14.     procedure SetMenuItem(NewMenuItem: TMenuItem);
  15.     constructor Create(AOwner: TComponent); override;
  16.     destructor Destroy; override;
  17.   published
  18.     property MenuItem: TMenuItem write SetMenuItem;
  19.   end;
  20.  
  21. implementation
  22.  
  23. constructor TMultiWindowMenuControl.Create(AOwner: TComponent);
  24. begin
  25.   inherited Create(AOwner);
  26.   FMenuItem:= nil;
  27.   List:= TList.Create;
  28.   Form:= TMForm(AOwner);
  29.   if not (csDesigning in ComponentState) then
  30.     Form.WindowProc:= SubClassWndProc;
  31. end;
  32.  
  33. destructor TMultiWindowMenuControl.Destroy;
  34. begin
  35.   Form.WindowProc:= Form.WndProc;
  36.   Clear;
  37.   List.Free;
  38.   inherited Destroy;
  39. end;
  40.  
  41. procedure TMultiWindowMenuControl.Clear;
  42. var
  43.   Item: TMenuItem;
  44. begin
  45.   While List.Count > 0 do
  46.   begin
  47.     TSubclasWindow(List.Items[0]).Free;
  48.     List.Delete(0);
  49.     Item:= FMenuItem.Items[0];
  50.     FMenuItem.Delete(0);
  51.     Item.Free;
  52.   end;
  53. end;
  54.  
  55. procedure TMultiWindowMenuControl.SetMenuItem(NewMenuItem: TMenuItem);
  56. var
  57. Item: TMenuItem;
  58. begin
  59. // Si FMenuItem tenía Items, los traspaso al nuevo MenuItem
  60. if FMenuItem = NewMenuItem then exit;
  61. if NewMenuItem <> nil then
  62. begin
  63. while (FMenuItem <> nil) and (FMenuItem.Count > 0) do
  64. begin
  65. Item:= FMenuItem.Items[0];
  66. FMenuItem.Delete(0);
  67. NewMenuItem.Add(Item);
  68. end;
  69. end
  70. else
  71. Clear;
  72. FMenuItem:= NewMenuItem;
  73. end;
  74.  
  75. procedure TMultiWindowMenuControl.Add(NewForm: TForm);
  76. var
  77.   i: integer;
  78.   SubClass: TSubclasWindow;
  79.   Item: TMenuItem;
  80. begin
  81.   if FMenuItem <> nil then
  82.   begin
  83.     // Comprobando si existe
  84.     for i:=0 to List.Count-1 do
  85.       if TSubclasWindow(List.Items[i]).Form = NewForm then break;
  86.     if (i < List.Count) and (List.Count > 0) then exit;
  87.  
  88.     SubClass:= TSubclasWindow.Create(TMForm(NewForm));
  89.     SubClass.MainForm:= Form;
  90.     List.Add(SubClass);
  91.     Item:= TMenuItem.Create(FMenuItem);
  92.     Item.Caption:= NewForm.Caption;
  93.     FMenuItem.Add(Item);
  94.   end;
  95. end;
  96.  
  97. procedure TMultiWindowMenuControl.Delete(AForm: TForm);
  98. var
  99.   i: integer;
  100.   Item: TMenuItem;
  101. begin
  102.   //Buscando para borrar
  103.   i:= 0;
  104.   While (i < List.Count) and (TSubclasWindow(List.Items[i]).Form <> AForm) do inc(i);
  105.   if i < List.Count then
  106.   begin
  107.     TSubclasWindow(List.Items[i]).Free;
  108.     List.Delete(i);
  109.     Item:= FMenuItem.Items[i];
  110.     FMenuItem.Delete(i);
  111.     Item.Free;
  112.   end;
  113. end;
  114.  
  115. procedure TMultiWindowMenuControl.SubClassWndProc(var Message: TMessage);
  116. var
  117.   Pos: Integer;
  118. begin
  119.   if Message.Msg = WM_MYCLOSE then
  120.     Delete(TForm(Message.WParam))
  121.   else if Message.Msg = WM_COMMAND then
  122.   begin
  123.     Pos:= GetMenuItenPos(LOWORD(Message.WParam));
  124.     if Pos <> -1 then
  125.       TSubclasWindow(List.Items[Pos]).Form.BringToFront;
  126.   end;
  127.   //OldFormWindowProc(Message);
  128.   Form.WndProc(Message);
  129. end;
  130.  
  131. // Encuentra la posición del Item mediante su MenuID
  132. // devuelve -1 si no aparece
  133. function TMultiWindowMenuControl.GetMenuItenPos(MenuID: Cardinal): Integer;
  134. begin
  135.   Result:= -1;
  136.   if FMenuItem <> nil then
  137.   begin
  138.     Result:= FMenuItem.Count -1;
  139.     while (Result>=0) and  (MenuID <> GetMenuItemID(FMenuItem.Handle, Result)) do dec(Result);
  140.   end;
  141. 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. 
 


delphi
  1. // Constructor del formulario principal
  2. procedure TForm1.FormCreate(Sender: TObject);
  3. begin
  4.   MultiWindowMenuControl:= TMultiWindowMenuControl.Create(self);
  5.   MultiWindowMenuControl.MenuItem:= Ventanas;
  6. end;
  7.  
  8. //Opciones de menú para crear formularios hijos
  9. procedure TForm1.Opcion1Click(Sender: TObject);
  10. begin
  11.   MultiWindowMenuControl.Add(Form2);
  12.   Form2.Show;
  13. end;
  14.  
  15. procedure TForm1.Opcion2Click(Sender: TObject);
  16. begin
  17.   MultiWindowMenuControl.Add(Form3);
  18.   Form3.Show;
  19. end;
  20.  
  21. procedure TForm1.Opcion3Click(Sender: TObject);
  22. var
  23.   NewForm: TForm2;
  24. begin
  25.   // Creando dinámicamente un formulario
  26.   NewForm:= TForm2.Create(self);
  27.   NewForm.Caption:= 'Form4';
  28.   MultiWindowMenuControl.Add(NewForm);
  29.   NewForm.Show;
  30. end;
  31. //..............

  
 
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.

Archivos adjuntos


  • 1

#3 egostar

egostar

    missing my father, I love my mother.

  • Administrador
  • 13.979 mensajes
  • LocationMéxico

Escrito 19 septiembre 2019 - 10:01

Muy interesante forma de automatizar amigo, Ya me lo descargo (y)

 

Saludos


  • 0

#4 egostar

egostar

    missing my father, I love my mother.

  • Administrador
  • 13.979 mensajes
  • LocationMéxico

Escrito 19 septiembre 2019 - 10:16

Hola amigo,
 
Descargué el archivo adjunto y ejecuté el EXE y se ha quedado congelado.
 
Hice un debug en el código y veo que se queda dando vueltas en la siguiente función:
 


delphi
  1. procedure TMultiWindowMenuControl.SubClassWndProc(var Message: TMessage);
  2. var
  3.   Pos: integer;
  4. begin
  5.   if Message.Msg = WM_MYCLOSE then
  6.     Delete(TForm(Message.WParam))
  7.   else if Message.Msg = WM_COMMAND then
  8.   begin
  9.     Pos:= GetMenuItenPos(LOWORD(Message.WParam));
  10.     if Pos >= 0 then
  11.       TSubclasWindow(List.Items[Pos]).Form.BringToFront;
  12.   end;
  13.   //OldFormWindowProc(Message);
  14.   Form.WndProc(Message);
  15. end;

Probado en Berlin

 

Saludos (y)


  • 0

#5 escafandra

escafandra

    Advanced Member

  • Moderadores
  • PipPipPip
  • 3.852 mensajes
  • LocationMadrid - España

Escrito 19 septiembre 2019 - 12:30

Caramba, Lo he probado en Berlin Win10 y no falla. :o
Si realizas un debug paso a paso es normal que se quede en los procedimientos de tratamiento de mensajes pues es el código aue más veces se ejecuta y al que el flujo irá cuando tengas un mensaje Windows, Es decir, un simple movimiento de ratón...
 
4a5b2cc67861b0e9c5876c678dc371a2o.gif




Saludos.
  • 0

#6 egostar

egostar

    missing my father, I love my mother.

  • Administrador
  • 13.979 mensajes
  • LocationMéxico

Escrito 19 septiembre 2019 - 03:12

Perdón amigo, olvide lo importante, es cuando presionas el botón [Cambiar Menú]:embarrassed:

 

Saludos


  • 1

#7 escafandra

escafandra

    Advanced Member

  • Moderadores
  • PipPipPip
  • 3.852 mensajes
  • LocationMadrid - España

Escrito 19 septiembre 2019 - 04:17

Perdón amigo, olvide lo importante, es cuando presionas el botón [Cambiar Menú]:embarrassed:
 
Saludos

 
 
Ok, ya lo he visto. Es al cambiar menú dos veces. El bug ya está resuelto con este cambio y permite la asignación nil:
 

delphi
  1. procedure TMultiWindowMenuControl.SetMenuItem(NewMenuItem: TMenuItem);
  2. var
  3. Item: TMenuItem;
  4. begin
  5. // Si FMenuItem tenía Items, los traspaso al nuevo MenuItem
  6. if FMenuItem = NewMenuItem then exit;
  7. if NewMenuItem <> nil then
  8. begin
  9. while (FMenuItem <> nil) and (FMenuItem.Count > 0) do
  10. begin
  11. Item:= FMenuItem.Items[0];
  12. FMenuItem.Delete(0);
  13. NewMenuItem.Add(Item);
  14. end;
  15. end
  16. else
  17. Clear;
  18. FMenuItem:= NewMenuItem;
  19. end;

 
Y como no me convencía mucho la función GetMenuItenPos, aunque funcionaba bien, la he modificado así:

delphi
  1. function TMultiWindowMenuControl.GetMenuItenPos(MenuID: Cardinal): Integer;
  2. begin
  3. Result:= -1;
  4. if FMenuItem <> nil then
  5. begin
  6. Result:= FMenuItem.Count -1;
  7. while (Result>=0) and (MenuID <> GetMenuItemID(FMenuItem.Handle, Result)) do dec(Result);
  8. end;
  9. end;

He subido el código nuevamente y editado los cambios en el código del post.

Saludos.
  • 1

#8 egostar

egostar

    missing my father, I love my mother.

  • Administrador
  • 13.979 mensajes
  • LocationMéxico

Escrito 19 septiembre 2019 - 04:46

Perfecto. (y)

 

Saludos :)


  • 0

#9 escafandra

escafandra

    Advanced Member

  • Moderadores
  • PipPipPip
  • 3.852 mensajes
  • LocationMadrid - España

Escrito 23 septiembre 2019 - 02:51

Me he animado a completar el trabajo convirtiendo esta idea en un componente visual que encontrareis aquí: TMultiWindowMenuControl
 
El código prácticamente no varía y es una adaptación para convertirse en componente visual.
 
 
Saludos,
  • 1