este espacio está libre ¿deseas utilizarlo? haz clic aquí.

   Secciones

   Inicio
   Documentos
   Programas
   Links
   Proyectos
   Libros

   Actualizaciones & Foro

   Avisar al actualizar
   Foro Delphiladero

   Otras

   Contacto
   Agregar a favoritos
   Vota por nosotros

 

 

 

inicio » documentos » intro a las mdi - parte 1


indice

Aplicaciones MDI

      ¿Qué son las aplicaciones MDI?
      ¿Cómo crear una aplicación MDI en Delphi?
            Eliminar la creación automática de la ventana hija
            Al presionar la X de la ventana hija debe cerrarse, no minimizarse
            Caption ventana madre "-" caption ventana hija
            Solucionar el problema de flickering al maximizar ventanas hijas
            Empezar a implementar el menú de frmChild
            Al intentar cerrar un documento modificado debe salir un mensaje
            Crear el menú Ventana
            Solucionar algunos problemas al abrir documentos
            Solucionar el problema visual al abrir varios documentos a la vez
            Solucionar el problema de los 2 toolbars


« aplicaciones mdi »


¿qué son las aplicaciones mdi?

Una aplicación con interfaz gráfica de usuario o GUI (Graphical User Interface) es aquella diseñada utilizando ventanas, menús, cuadros de diálogo, y demás características que hacen a una aplicación fácil de usar. Delphi ofrece 2 tipos de interfaces de usuario: interface de un documento o SDI (Single Document Interface) e interface de múltiples documentos o MDI (Multiple Document Interface).

En una aplicación MDI, como su nombre lo indica, se puede abrir más de una ventana hija (child window) dentro del espacio de una ventana madre (parent window). Esto quiere decir, que si por ejemplo nos encontramos creando un editor de texto, y lo hacemos MDI el usuario va a poder abrir y modificar varios documentos a la vez. En cambio, si decidimos hacer una aplicación SDI, entonces el usuario se verá limitado a modificar un documento a la vez. Un claro ejemplo de este tipo de aplicación es el Bloc de Notas o el WordPad que vienen con Windows.

Antes de comenzar a develar el misterio de cómo crear una aplicación MDI en Delphi y contarles algunos secretos aprendidos a fuerza de horas culo (sentado frente a la máquina) sería recomendable que vieras con tus propios ojos a una aplicación MDI en acción para comprender mejor su estructura y funcionamiento. Para ello no tienes más que abrir Delphi ir a File -> New... ir a la lengüeta Projects y seleccionar MDI Application. Luego de elegir el directorio en donde guardar los archivos presiona F9 para compilar y correr la aplicación. Ponte a jugar un rato creando, abriendo y cerrando documentos hasta que ya te sientas cómodo con la interfaz.


¿cómo crear una aplicación mdi en delphi?

Una aplicación MDI está compuesta por:

  • Ventana de marco (Frame Window): La ventana principal de la aplicación. El espacio vacio entre las ventanas MDI hijas es conocido como area cliente y es en realidad la ventana de clientes.

  • Ventana de clientes (Client Window): El administrador de las aplicaciones MDI. La ventana de clientes se encarga de manejar todos los comandos específicos del MDI y de manejar a las ventanas hijas que residen en su superficie -incluyendo el dibujo de las ventanas MDI hijas. La ventana de clientes es creada automáticamente por la VCL cuando se crea una ventana de marco.

  • Ventanas MDI hijas (MDI Child Windows): Son los documentos mismos. Las ventanas hijas no pueden dibujarse por fuera del area de clientes de la ventana de marco.

Medio complicado, ¿no?. Bueno, lo de arriba vamos a pasarlo a un lenguaje bien coloquial, podemos decir que la ventana de marco es la ventana madre, la ventana cliente no la mencionaremos y sólo hablaremos del area de clientes y a las ventanas MDI hijas las llamaremos  simplemente ventanas hijas.

Entonces, crear una aplicación MDI puede resumirse en la creación de una ventana madre y al menos una ventana hija.

En una aplicación MDI puede haber solamente una ventana madre. Para crear esta ventana madre o principal en Delphi debes crear un nuevo proyecto, guardarlo, seleccionar el form creado y cambiar su propiedad FormStyle a fsMDIForm. Listo.

Ahora bien, cada ventana madre necesita al menos una ventana hija. No conozco ninguna madre que lo sea sin tener un hijo, verdad. Bueno, el asunto es que para ello debemos crear un nuevo form y cambiar su propiedad FormStyle a fsMDIChild.

Nota: Es muy común nombrar a la ventana madre frmMain y a la ventana hija frmChild, y en caso de que sean varias podrían nombrarse frmRTFChild, frmImageChild, etc.

Eso es todo el misterio de crear aplicaciones MDI. Una verdadera pavada. Sin embargo, a medida que le vayas agregando cosas a tu aplicación irán surgiendo todo tipo de problemas que debemos solucionar de manera diferente de como lo veníamos haciendo.

En primer lugar, si presionas F9 para compilar y correr la aplicación verás que aparece nuestra ventana principal con una ventana hija dentro de ella. Esta aplicación está muy pelada, hay que agregarle un menú, un toolbar, un menú que liste todas las ventanas hijas abiertas, que en el caption aparezca el nombre de la aplicación "-" nombre del documento abierto, que no cree la ventana hija automaticamente al iniciarse la aplicación, etc, etc.

 

Eliminar la creación automatica de la ventana hija

Empecemos de atrás para adelante. Si recuerdas lo que viste al correr hace instantes la aplicación recordarás que la ventana hija se creó automaticamente, pero en el futuro no queremos que esto suceda ya que para eso tendremos un item Nuevo para crear ventanas hijas en blanco o Abrir para crear ventanas hijas mostrando el archivo seleccionado. La solución es muy fácil, debes ir a el menú Project -> Options luego seleccionar la lengüeta Forms. Una vez allí veremos 2 listas: auto-created forms y available forms. Como vemos, nuestros 2 forms (frmMain y frmChild) se encuentran en la lista de auto-created forms. Por supuesto, lo que debemos hacer es pasar nuestro form hijo (frmChild) a la lista de available forms.

Nota: Delphi, en forma predeterminada, agrega a la lista de auto-created forms todos los forms que vayamos agregando en tiempo de diseño. Esto significa que la aplicación al iniciarse crea automaticamente todos los forms en esa lista. Quitar algunos forms de esa lista resulta conveniente hoy para nosotros para que no se cree automaticamente y la muestre al iniciar nuestro programa, pero en general, cuando trabajes en aplicaciones SDI, es decir, cuando trabajes en las aplicaciones que venias haciendo hasta ahora no se van a mostrar todos los Forms de la lista auto-created forms al iniciarse tu aplicación, sólo el primero. Los demás van a quedar ocultos en memoria para que luego puedas acceder a ellos más rápido. Sin embargo, esto no siempre es una ventaja, ya que muchas veces esto hace que la carga del programa sea un tanto lenta y por eso muchas veces resulta conveniente sacar algunos forms de esa lista y crearlos en tiempo de ejecución cuando se los necesite. Además puede ocurrir que el usuario nunca abra muchos de los forms que están en memoria, entonces, ¿para qué cargarlos al iniciar?. Es un desperdicio de memoria y de velocidad al iniciarse la aplicación.

 

Seguimos y hacemos que al presionar la X de la ventana hija se cierre y deje de minimizarse

Primer problema solucionado. Si corremos la aplicación nuevamente veremos que solamente aparece la ventana madre sin ninguna ventana hija adentro. Bien, ahora llegó la hora de empezar a hacer que la aplicación sea util. Entonces, como nuestra aplicación va a ser un editor de textos comun y corriente, vamos a la ventana hija y agregamos un control richedit. Le cambiamos las siguientes propiedades: Align=alCliente, Scrollbars=ssBoth, PlainText=true, Name=reMain. Volvemos a la ventana madre y agregamos un menú. Creamos un menú Archivo con los siguientes items: Nuevo, Abrir y Salir.

Bien, ahora llegó la hora de sentarse y escribir un poco de código. Empecemos por el evento OnClick del item Nuevo.

procedure TfrmMain.mniArNuevoClick(Sender: TObject);
var MyChildForm: TfrmChild;
begin
  MyChildForm := TfrmChild.Create(Self);
  // podría haberse escrito MyChildForm := TfrmChild.Create(frmMain);
  MyChildForm.Caption := 'Sin Nombre ' + IntToStr(MDIChildCount);
  { idem MyChildForm.Caption := 'Sin Nombre ' + IntToStr(frmMain.MDIChildCount); }
end;

Como se que eres una persona audaz y que le gusta comprender cada detalle de lo que se escribe, desglozarlo, estudiarlo y siempre preguntar seguramente te estarás preguntando 3 cosas:

1) ¿Self? Self hace referencia a la instancia de la clase en la que nos encontramos escribiendo código. En este caso la clase es TfrmMain y su instancia es simplemente frmMain. Entonces, esa línea también podría haberse escrito como se describe en la línea siguiente. ¿Por qué la clase es TFrmMain? Simplemente porque nos encontramos escribiendo código dentro de un procedure perteneciente a esta clase, en este caso el TFrmMain.mniNuevoClick.

2) ¿MDIChildCount? Bueno, todo form tiene 3 propiedades que cuando te encuentres creando aplicaciones MDI utilizarás mucho. Estas son: MDIChildCount, MDIChildren y ActiveMDIChild. Pasemos a explicar la utilidad de cada una de ellas.

  • MDIChildCount: nos indica la cantidad de forms MDI hijos abiertos.

  • MDIChildren: Se utiliza para hacer referencia a algún form MDI hijo en particular. Así si escribo MDIChildren[0] me estaré refiriendo al primer form MDI hijo y si escribo MDIChildren[MDIChildCount - 1] me estaré refiriendo al último. En general, lo utilizarás para cuando tengas que hacer algo con todos los forms hijos abiertos. Por ejemplo, Cerrar todo o Guardar todo, etc.

  • ActiveMDIChild: Se utiliza para obtener el form MDI hijo que actualmente tiene el foco.

Nota: El form desde el cual hacemos referencia a cualquiera de estas 3 propiedades debe ser un form MDI madre, es decir debe tener su propiedad FormStyle=fsMDIForm. De lo contrario, MDIChildCount y MDIChildren no tendrán sentido y ActiveMDIChild devolverá nil. Esto quiere decir que no nos sirve hacer referencia a frmChild.MDIChildCount porque es una ventana MDI hija, tampoco nos serviría hacer referencia a Form1.MDIChildCount pensando a Form1 como a un form con su propiedad FormStyle=fsNormal. Sólo tiene sentido hacer referencia a frmMain.MDIChildCount o frmMain.MDIChildren[x] o frmMain.ActiveMDIChild si frmMain es una ventana MDI madre.

3) ¿Create sin Free, ummm...? Si has pensado eso realmente debo felicitarte quiere decir que lo que escribí en el artículo de la OOP ha servido y tu has sido un atento lector. Si no, no te aflijas es cuestión de práctica y estar mas canchero. Muy bien, pero ¿donde esta el Free?. Es decir, hemos creado una ventana en tiempo de ejecución, pero debemos liberar esa memoria ... al cerrarla. Muy bien, entonces vamos a frmChild y creamos un evento OnClose.

procedure TfrmChild.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Action := caFree;
end;

Hay 4 opciones para Action:

  1. caNone: se le permite a la ventana cerrarse y no pasa nada más.

  2. caHide: la ventana no se cierra, se esconde y queda residente en memoria para poder acceder a ella más rápido en el futuro.

  3. caFree: la ventana se cierra y se libera toda la memoria asignada al form.

  4. caMinimize: la ventana en vez de cerrarse se minimiza. Esta es la predeterminada.

Bien, segundo problema solucionado, basta de presionar la X para cerrar la ventana y que me la minimizara. Ahora la cerrará y liberará toda la memoria asignada a esa ventana.

 

Seguimos y hacemos que el caption quede caption ventana madre "-" caption ventana hija

Si recuerdas toda esta larga explicación vino porque nos atrevimos a escribir 3 lineas locas en el evento frmMain.mniNuevoClick. Ahora bien, eso crea una ventana en blanco nueva, pero ¿cómo hacemos para que la ventana nueva muestre un archivo que seleccione el usuario? Es decir, ¿cómo hacer el clásico Abrir?. Primero que nada agreguemos un OpenDialog y modifiquemos sus propiedades: Filter=Archivos de texto puro (*.txt)|*.txt y Name=OpenDialog. Ahora sí, pasemos al código.

procedure TfrmMain.mniArAbrirClick(Sender: TObject);
var MyChildForm: TFrmChild;
begin
  MyChildForm := TFrmChild.Create(Self);
  if OpenDialog.Execute then
    MyChildForm.Caption := OpenDialog.FileName;
end;

Como ves el código es muy parecido al escrito en mniNuevoClick. Debemos hacer algo al respecto, ¿no te parece?. Siempre que haya 2 porciones de código iguales o muy parecidas debes pensar en crear un nuevo procedure o function. Eso es exactamente lo que haremos nosotros. Crearemos un nuevo procedure llamado CrearVentanaMDIHija que tomará como parámetro un string llamado Nombre.

procedure TfrmMain.CrearVentanaMDIHija(Nombre: string);
var MyChildForm: TFrmChild;
begin
  MyChildForm := TFrmChild.Create(Self);
  MyChildForm.Caption := Nombre;
  if FileExists(Nombre) then
    MyChildForm.reMain.Lines.LoadFromFile(Nombre);
end;

Lo de arriba, esto va para los más experimentados también podría haberse escrito de la siguiente manera:

procedure TfrmMain.CrearVentanaMDIHija(Nombre: string);
begin
  with TFrmChild.Create(Self) do
  begin
    Caption := Nombre;
    if FileExists(Nombre) then
      reMain.Lines.LoadFromFile(Nombre);
  end;
end;

La forma que elijas es sólo cuestión de gustos, pero siempre es interesante conocer diferentes maneras de hacer lo mismo para que cuando veamos código que no fue escrito por nosotros no nos resulte raro lo que escribieron. Bueno, muy bien, ahora veamos cómo quedarán nuestros eventos OnClick de los items Nuevo y Abrir.

procedure TfrmMain.mniNuevoClick(Sender: TObject);
begin
  CrearVentanaMDIHija('Sin Nombre ' + IntToStr(MDIChildCount + 1));
end;

procedure TfrmMain.mniAbrirClick(Sender: TObject);
begin
  if OpenDialog.Execute then
    CrearVentanaMDIHija(OpenDialog.FileName);
end;

Mejor, ¿no?. Ahora, ¿por qué MDIChildCount + 1 y no sólo MDIChildCount como hacíamos antes (ver más arriba)?. Simplemente porque estamos haciendo referencia a MDIChildCount antes de crear la nueva ventana. Es decir, antes hacíamos referencia a MDIChildCount después de MyChildForm := TfrmChild.Create(Self), ahora lo estamos haciendo antes porque le estamos mandando el dato como parámetro a la función que crea la nueva ventana (CrearVentanaHija).

Ok, es el momento de correr la aplicación y ver cuál es el resultado de todo el código que acabamos de escribir. Si maximizas una de las ventanas hijas verás que el caption pasa a: Caption de la ventana madre "-" Caption de la ventana hija abierta. Así nuestro tercer problema está solucionado y la VCL hizo el trabajo por nosotros.

Pero, paremos la pelota un segundo y miremos detenidamente el código que escribimos hasta ahora. Hagamos un pequeño experimento y pongamos un Breakpoint (F5) en la única línea del evento OnClose de frmChild. Corremos la aplicación, abrimos varias ventanas y luego presionamos en la X que cierra el form madre. Como vemos la aplicación se cerró sin pasar por los eventos OnClose de cada ventana hija, y por lo tanto sin liberar la memoria asignada a cada una de ellas. Bueno, eso no es del todo correcto. ¿Por qué?. Porque cada vez que creábamos una ventana hija le pasábamos como parámetro Owner a frmMain (MyChildForm := TFrmChild.Create(Self)). Esto quiere decir que frmMain se hace responsable de liberar la memoria de cada form hijo al liberarse la memoria de frmMain, es decir, al cerrarse frmMain.

Nota: Hablando de cerrar, la barra de título de los forms MDI hijos (Minimizar, Maximizar, Cerrar, etc.) desaparece al maximizarse si no hay un menú. Si creas un proyecto nuevo con 2 forms, uno fsMDIForm y otro fsMDIChild, corres la aplicación y maximizas la ventana hija comprenderas de qué estoy hablando. Ahora agregale un menú cualquiera y vuelve a correr la aplicación. ¿Ves la diferencia?.

Cuarto problema solucionado. Bien, como ves hemos solucionados varios problemas a los que alguien acostumbrado a crear aplicaciones SDI no hubiera podido resolver ni detectar al primer intento. Pero, basta de chachara y pasemos a resolver otro problema.

 

Seguimos y solucionamos el problema de flickering al maximizar ventanas hijas

Es muy común en este tipo de aplicaciones que las ventanas hijas aparezcan ya maximizadas al crearlas. Es decir, que cuando el usuario va a Nuevo o Abrir se le abre una nueva ventana (en blanco o con texto) maximizada. Muy bien, para empezar a resolver ese problema no tenemos más que hacer lo mismo que hacemos cuando queremos que cualquier ventana se maximize. Debemos modificar la propiedad WindowState=wsMaximized. Ahora sí, corremos la aplicación y vemos los resultados. Ummm... ¿qué pasó?. Cuando vamos a Nuevo o Abrir se abre la ventana pero es como si se abriera en su tamaño normal y luego se maximizara, no aparece maximizada de entrada. Ese movimiento de la ventana realmente es horrible. ¿Cómo solucionarlo? Veamos...

procedure TfrmMain.CrearVentanaMDIHija(Nombre: string);
begin
  LockWindowUpdate(Handle);
  with TFrmChild.Create(Self) do
  begin
    Caption := Nombre;
    if FileExists(Nombre) then
      reMain.Lines.LoadFromFile(Nombre);
  end;
  LockWindowUpdate(0);
end;

¿Y eso? Las API de Windows siempre al rescate han venido a salvarnos nuevamente de un aprieto. El problema es que la nueva ventana nace con el tamaño que le indiquemos en sus propiedades Height y Width y luego se maximiza en caso de que su propiedad WindowState=wsMaximized. LockWindowUpdate, entonces, lo que hace es hacer que la ventana no se dibuje hasta que esté completamente maximizada. Es decir, impide que la ventana se repinte a medida que se va agrandando y le da ese efecto tan interesante de que se está maximizando. Da la sensación de movimiento, pero en este caso no nos sirve, no nos interesa y además queda horrible, asi que le quitamos todo esa exquisitez.

Quinto problema solucionado. Ahora sólo nos queda escribir el código para el evento OnClick de mniSalir.

procedure TfrmMain.mniArSalirClick(Sender: TObject);
begin
  Close;
end;

 

Seguimos y empezamos a implementar al menú de frmChild

Listo. Acabamos de finalizar la implementación de nuestro mini-menú. Ahora, si me voy a manejar con varios documentos necesito más opciones como Guardar, Guardar como..., Guardar todo, Cerrar, Cerrar todo. Más aún, necesito un menú Edición en donde pueda Cortar, Copiar, Pegar, Seleccionar todo, Buscar, Reemplazar, etc, etc.

Bueno, llegó la hora de complicarnos un poco más. Sí, no porque crear menús sea muy complicado, sino más bien porque al crearlos estamos definiendo la estructura del código que escribiremos posteriormente. ¿Qué quiere decir eso? Bueno donde y qué tipo de código escribiremos depende de donde coloquemos los menús. Los menús, en este caso, pueden ir en la ventana madre o en la ventana hija. ¿Da lo mismo ponerlo en un lugar o en otro?, ¿habrá que crear más de 1 menu?, todas estas son preguntas que debemos contestarnos antes de proceder y escribir código.

Este paso, debo admitirlo, puede resultar una estupidez pero no lo es. Yo mismo me he envuelto en complicaciones espectaculares por no haber pensado detenidamente la estructura de mis menús. Sin embargo, después de horas de idas y venidas, he llegado a desarrollar un método para construir mis menús que no es el único y quizá no sea el mejor pero es el que yo entiendo más claramente y me parece el más adecuado para la mayoría de mis aplicaciones MDI.

Ahora bien, como te habrás dado cuenta nuestro mini-menú en frmMain, la ventana madre, sólo tiene 3 opciones: Nuevo, Abrir y Salir. Eso no es casual, hay un razonamiento detrás. En primer lugar, déjame aclarar que el menú que diseñemos debes pensarlo como el menú que verá el usuario cuando no haya ninguna ventana hija abierta. Por lo tanto, agregar en nuestro menú opciones de Guardar o Cerrar sería inutil y mucho más lo sería agregar un menú Edición. Listo el pollo. Ya tenemos el menú de frmMain y toda su implementación.

El segundo paso es crear el menú que el usuario verá cuando haya al menos 1 ventana hija abierta. Ese menú lo crearemos en frmChild y es el que además de Nuevo, Abrir y Salir, tendrá Guardar, Guardar como, Guardar todo, Cerrar, Cerrar todo. Veamos cuál será su implementación.

Comencemos por los 3 items que se repiten: Nuevo, Abrir y Salir.

procedure TfrmChild.mniArNuevoClick(Sender: TObject);
begin
  frmMain.mniArNuevoClick(Sender);
end;

procedure TfrmChild.mniArAbrirClick(Sender: TObject);
begin
  frmMain.mniArAbrirClick(Sender);
end;

procedure TfrmChild.mniArSalirClick(Sender: TObject);
begin
  frmMain.mniArSalirClick(Sender);
end;

Como ves, en los elementos que se repiten solamente nos limitamos a llamar a los OnClick de frmMain que habíamos implementado hace unos minutos. Bien, el siguiente paso será escribir el código para Guardar, Guardar como y Guardar todo.

procedure TfrmChild.mniArGuardarComoClick(Sender: TObject);
begin
  if SaveDialog.Execute then
  begin
    Guardar(SaveDialog.FileName);
    Caption := SaveDialog.FileName;
  end;
end;

procedure TfrmChild.mniArGuardarClick(Sender: TObject);
begin
  if DocNuevo = False then
    Guardar(Caption)
  else
    mniArGuardarComoClick(Sender);
end;

procedure TfrmChild.Guardar(Nombre: string);
begin
  reMain.Lines.SaveToFile(Nombre);
  reMain.Modified := False;
  DocNuevo := False;
end;

procedure TfrmChild.mniArGuardarTodoClick(Sender: TObject);
var i: integer;
begin
  for i := frmMain.MDIChildCount - 1 downto 0 do
  TfrmChild(frmMain.MDIChildren[i]).mniArGuardarClick(Sender);
end;

Varias cosas para aclarar. En primer lugar dejame decir que Guardar es un procedure declarado en la sección private de frmChild y DocNuevo es una variable boolean declarada en la sección public. ¿Por qué cree este procedure Guardar? Simplemente porque me ahorraba un par de líneas. ¿Por qué cree una variable boolean llamada NuevoDoc?. Bueno, para comprenderlo debemos entender primero cómo funciona nuestro método de guardado.

Básicamente hay 2 preguntas que la máquina se debe hacer para saber qué método de guardado debe utilizar, si es que tiene que utilizar alguno después de todo.

1) ¿se le han realizado modificaciones al documento?
2) ¿es un documento nuevo o ya esta guardado en el disco?

Si el archivo no ha sufrido ninguna modificación no lo guardaremos. De lo contrario pasaremos a hacernos la segunda pregunta.

Si es un documento nuevo, no existe en nuestro disco duro, por lo tanto cuando intentemos guardarlo debe aparecer un cuadro de diálogo preguntándole al usuario donde desea guardar el archivo y que le ponga un nombre. De lo contrario, es decir, si el documento ya existe en el disco duro entonces lo guardamos y listo.

DocNuevo, entonces, lo que hace es contestarnos nuestra segunda pregunta. Pero, ¿qué pasa con la primera, cómo sabemos si el documento ha sido modificado?. Bueno, muchos comenten el error de crear una nueva variable de tipo boolean como NuevoDoc llamada por ejemplo Modificado y la van modificando según sea necesario. Modificado=False cuando se crea/abre/guarda un documento y Modificado=True cuando se produce el evento OnChange del RichEdit, en nuestro caso, reMain. Ahora bien, los de Borland, conocedores de nuestros problemas diarios ya incluyeron esta variable dentro del RichEdit. Es la propiedad Modified. Esta propiedad nos ahorra 2 cosas: tener que declarar una nueva variable y tener que modificar la propiedad en el evento OnChange del RichEdit. Automaticamente, Modified=True cada vez que se da un evento OnChange. Sin embargo, el RichEdit no tiene forma de saber cuando queremos que Modified=False, así que eso todavía sigue en nuestras manos. Como ya dije eso debe ocurrir cuando se crea/abre/guarda un documento, pero nosotros en el ejemplo anterior vimos cómo hacerlo al guardar, nos falta modificar los procedures para crear/abrir documentos implementados en frmMain quedando estos de la siguiente manera:

procedure TfrmMain.mniArNuevoClick(Sender: TObject);
begin
  CrearVentanaMDIHija('Sin Nombre ' + IntToStr(MDIChildCount + 1));
  TfrmChild(ActiveMDIChild).DocNuevo := True;
end;

procedure TfrmMain.mniArAbrirClick(Sender: TObject);
begin
  if OpenDialog.Execute then
    CrearVentanaMDIHija(OpenDialog.FileName);

  TfrmChild(ActiveMDIChild).DocNuevo := False;
end;

Bueno, por primera vez, estamos haciendo uso de la propiedad ActiveMDIChild de la ventana madre. Como ves, esta propiedad nos devuelve un valor TForm, por lo que debemos convertirla a TFrmChild para poder acceder a nuestra variable pública DocNuevo. Para ello hacemos lo que se llama un casting (conversión de un tipo de datos en otro). Sin embargo, esa no es la única manera de hacerlo, veamos otras alternativas:

procedure TfrmMain.mniArNuevoClick(Sender: TObject);
begin
  CrearVentanaMDIHija('Sin Nombre ' + IntToStr(MDIChildCount + 1));
  (ActiveMDIChild as TfrmChild).DocNuevo := True;
end;

otra forma de lograr el mismo efecto sería:

procedure TfrmMain.mniArNuevoClick(Sender: TObject);
var MyChildForm: TfrmChild;
begin
  MyChildForm := ActiveMDIChild as TFrmChild;
  CrearVentanaMDIHija('Sin Nombre ' + IntToStr(MDIChildCount + 1));
  MyChildForm.DocNuevo := True;
end;

A la hora de implementar Cerrar y Cerrar todo la cosa se torna bastante más sencilla. Veamos...

procedure TfrmChild.mniArCerrarClick(Sender: TObject);
begin
  Close;
end;

procedure TfrmChild.mniArCerrarTodoClick(Sender: TObject);
var i: integer;
begin
  for i := frmMain.MDIChildCount - 1 downto 0 do
    frmMain.MDIChildren[i].Close;
end;

Si hay algo que seguramente te ha llamado la atención eso debe ser ... ¿por qué el conteo hacia abajo?. Fíjate bien que el for va de mayor a menor y no de menor a mayor como generalmente solemos escribirlo. ¿Por qué? Imagínate la siguiente lista y pongámosle un índice a cada item de esa lista:

[0] Form0
[1] Form1
[2] Form2
[3] Form3

Si borramos de arriba hacia abajo, por ejemplo, si borramos el item 0 quedarán 3 items [0] Form1, [1] Form2, [2] Form3. Luego el for pasa a borrar el item 1 (se está salteando uno -error) y queda una lista con los items [0] Form1 y [1] Form3. Al querer borrar el item 2 no lo va a encontrar y va a salir error. Lo mismo ocurrira cuando itente acceder al item 3. Un verdadero desastre. En cambio, si se empieza a borrar desde el final accederá al item [3] Form3, lo borrará, luego pasará al [2] Form2 y así hasta llegar al primero sin problemas.

Nota: El primer form MDI hijo tiene índice 0 y el último MDIChildCount - 1.

 

Seguimos y hacemos que al intentar cerrar un documento modificado salga un cuadro de diálogo

Ahora pasemos a ver cómo debemos hacer para que si se intenta cerrar un documento con modificaciones aparezca un cuadro de diálogo preguntando si se quiere guardar o no.

procedure TfrmChild.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
var Resp: word;
begin
  CanClose := True;

  if reMain.Modified then
  begin
    Resp := MessageDlg('Se han realizado modificaciones en ' +
              Caption + '"' + #13#10 + '¿Desea guardarlas?',
              mtConfirmation, [mbYes, mbNo, mbCancel], 0);
    case Resp of
      mrYes: mniArGuardarClick(Sender);
      mrCancel: CanClose := False;
    end;
  end;
end;

Como ves, este no es el evento OnClose sino el evento OnCloseQuery. ¿Por qué escribimos nuestro código aquí y no en OnClose?. Simplemente porque aquí se nos permite cancelar el cierre de la ventana en caso de que sea necesario modificando la propiedad CanClose.

Nota: El cuadro de diálogo que mostramos con MessageDlg nos avisa que se han realizado modificaciones y nos indica cuál es el documento en cuestión, punto que resulta de extrema importancia ya que ahora nuestro programa puede abrir varios documentos a la vez.

Bien, terminamos con la parte dificil, ahora viene la parte más fácil: cosntruir nuestro menú Edición. Como la misión de este artículo se limita a hablar sobre las aplicaciones MDI seré lo más breve posible. Veamos entonces cómo implementar nuestro nuevo menú.

procedure TfrmChild.mniEdCortarClick(Sender: TObject);
begin
  reMain.CutToClipboard;
end;

procedure TfrmChild.mniEdCopiarClick(Sender: TObject);
begin
  reMain.CopyToClipboard;
end;

procedure TfrmChild.mniEdPegarClick(Sender: TObject);
begin
  reMain.PasteFromClipboard;
end;

procedure TfrmChild.mniEdSelTodoClick(Sender: TObject);
begin
  reMain.SelectAll;
end;

 

Seguimos y creamos el menú Ventana

Antes de terminar con este tema de los menús, nos falta un último punto. Como bien dije al comienzo, la mayoría de las aplicaciones MDI son identificables por el menú, generalmente Ventana, en el que se listan todas las ventanas MDI hijas abiertas en ese momento. Muy bien, ¿cómo hacemos eso en Delphi?. Bueno, comencemos por crear un menú Ventana y agregarle: Mosaico Horizontal, Mosaico Vertical, Cascada, Ordenar íconos y un separador. Veamos su implementación.

procedure TfrmMain.mniVeMosaicoHClick(Sender: TObject);
begin
  TileMode := tbHorizontal;
  Tile;
end;

procedure TfrmMain.mniVeMosaicoVClick(Sender: TObject);
begin
  frmMain.TileMode := tbVertical; { puse frmMain para que se vea claramente que TileMode }
  frmMain.Tile;   // Tile, Cascade y ArrangeIcons pertenecen a frmMain.
end;

procedure TfrmMain.mniVeCascadaClick(Sender: TObject);
begin
  Cascade;
end;

procedure TfrmMain.mniVeOrganizarIconosClick(Sender: TObject);
begin
  ArrangeIcons;
end;

Nota: para insertar un separador, apreta Ins en el lugar en donde desees agregar un nuevo item de menú y ponle "-" como Caption.

Bien, esto nos sirve para que el usuario de nuestro programa pueda distribuir visualmente las ventanas hijas de la forma más adecuada a cada situación, pero no resolvió el problema que habíamos planteado. Para decirle a Delphi que muestre un listado de las ventanas MDI hijas abiertas en el menú Ventana todo lo que tenemos que hacer es modificar la propiedad WindowMenu de la ventana MDI madre. En nuestro caso, vamos a frmMain y cambiamos WindowMenu=mniVentana.

¡Cuidado!: De no agregar el separador en el menú que se asigne como WindowMenu la lista no aparecerá.

Nota: modificar WindowMenu y llamar a Tile, TileMode, Cascade y ArrangeIcons sólo tiene sentido si corresponden a una ventana MDI madre.

Sin embargo, si corremos la aplicación veremos que al abrir/crear un documento nuestro menú Ventana desaparece y sólo quedan los 2 menús que habíamos creado en frmChild: Archivo y Edición. Para solucionar este problema debemos combinar ambos menús, el definido en frmMain y el definido en frmChild. Para ello debemos modificar una simple propiedad de cada uno de los titulados de nuestros menús (Archivo y Ventana en frmMain y Archivo y Edición en frmChild). Esa propiedad es GroupIndex y lo que hace es fusionar los menús según el número que allí le indiquemos. Por ejemplo:

En frmMain...
Archivo [GroupIndex=0]
Ventana [GroupIndex=2]

En frmChild...
Archivo [GroupIndex=0]
Edición [GroupIndex=1]

Cuando abramos cualquier documento, el menú fusionado tendrá esta forma:

Archivo - Edición - Ventana

Los elementos del menú de la ventana hija reemplazan a los elementos del menú de la ventana madre que tengan el mismo GroupIndex, en nuestro caso los menús Archivo, y además inserta a los demás menús de la ventana hija en el lugar que corresponda según la propiedad GroupIndex de los demás menús de la ventana madre. Es decir, que inserta a Edición (de frmChild) entre Archivo (de frmChild, porque reemplaza a Archivo de frmMain) y Ventana (de frmMain). Se complica, ¿no?. Bueno, en realidad no tanto, en caso de que no hayas comprendido lo aquí explicado es recomendable leer la ayuda de Delphi con respecto a la propiedad GroupIndex de los items de los menús.

 

Seguimos y solucionamos unos problemas al abrir documentos

En primer lugar, ya que se pueden ver varios documentos a la vez, sería entonces una buena idea poder permitirle a usuario abrir varios documentos a la vez. Para ello, debemos ir a frmMain, seleccionar el OpenDialog y modificar su propiedad Options habilitándole la opción ofAllowMultiSelect. Con esto logramos que en el cuadro de diálogo se puedan elegir varios documentos, pero no hacemos que se abran todos, recordemos que al abrir un archivo estábamos haciendo referencia a OpenDialog.FileName (ver frmMain.mniArAbrirClick). Ahora debemos hacer uso de la propiedad Files de OpenDialog. En segundo lugar, debemos solucionar un pequeño "error" de nuestro programa que no verifica si el documento que se está intentando abrir ya está abierto. Todo esto, se soluciona realizando unas pequeñas modificaciones y agregados a nuestro frmMain.mniArAbrirClick. Veamos...

procedure TfrmMain.mniArAbrirClick(Sender: TObject);
var i, x: integer;
begin
  if OpenDialog.Execute then
    for i := 0 to OpenDialog.Files.Count - 1 do
    begin
      {verificar si el doc ya está abierto}
      for x := 0 to MDIChildCount - 1 do
        if OpenDialog.Files[i] = MDIChildren[x].Caption then
        begin
          MDIChildren[x].SetFocus;
          Exit;
        end;

      CrearVentanaMDIHija(OpenDialog.Files[i]);

      TfrmChild(ActiveMDIChild).DocNuevo := False;
    end;
end;

 

Seguimos y solucionamos el problema de visualización al abrir varios documentos a la vez

Aparentemente no hay ningún problema con el código que escribimos hace unos segundos. Sin embargo, si corres la aplicación y abres varios documentos de un sólo intento verás que hay un problema en la visualización de las ventanas al abrirse cada una de ellas. ¿Como se soluciona?. Fácil, ¿recuerdas que alguna vez al comienzo de este artículo te había dicho que debías cambiar la propiedad WindowState de frmMain a wsMaximized?. Bueno, demos marcha atrás con ese asunto y volvamos a asignarle wsNormal. Lo que haremos entonces será modificar esa propiedad en tiempo de ejecución como veremos a continuación.

procedure TfrmMain.CrearVentanaMDIHija(Nombre: string);
begin
  LockWindowUpdate(Handle);
  with TFrmChild.Create(Self) do
  begin
    Caption := Nombre;
    if FileExists(Nombre) then
      reMain.Lines.LoadFromFile(Nombre);

    reMain.Modified := False;
    WindowState := wsMaximized;
  end;
  LockWindowUpdate(0);
end;

Bien, ya casi estamos al final, pero nos falta algo fundamental ... un toolbar.

 

Seguimos y solucionamos el problema de los 2 toolbars

No es necesario que les explique cómo hacer un toolbar, pero el caso lo merece. Primero arrastramos un toolbar a frmMain, lo llamamos tlbMain y le agregamos 3 botones: nuevo, abrir y salir. Seleccionamos el botón de nuevo, vamos al Object Editor la lengüeta Events, y seleccionamos mniArNuevoClick como su evento OnClick. Como verás es el mismo procedimiento utilizado por el menú. Sigue con el resto de los botones según corresponda. Ahora vamos a frmChild y hacemos exactamente lo mismo pero esta vez agregando más botones: nuevo, abrir, guardar, cortar, copiar, pegar y salir. El toolbar se llamará tblMain, como su homónimo en frmMain y los botones tendrán como evento OnClick su homónimo en el menú, obviamente el menú de frmChild.

Nota: Por supuesto, en ambos casos también hay que agregar un ImageList que almacene las imágenes de los botones.

Si corremos la aplicación tal y como está veremos que al abrir/crear nuevos documentos aparecen 2 toolbars en vez de 1. ¿Cómo lo solucionamos? Bueno, para empezar creemos un procedimiento en frmMain de tipo public. Veamos su implementación.

procedure TfrmMain.DeterminarToolbar(AToolbar: TToolBar);
begin
  if (MDIChildCount = 1) and (AToolBar = nil) then
    tlbMain.Parent := self
  else
  begin
    tlbMain.Parent := nil;
    if not (AToolBar = nil) then
      AToolBar.Parent := self;
  end;
end;

Las llamadas a DeterminarToolbar son las siguientes.

procedure TfrmChild.FormActivate(Sender: TObject);
begin
  frmMain.DeterminarToolbar(tlbMain);
end;

procedure TfrmChild.FormDeactivate(Sender: TObject);
begin
  tlbMain.Parent := nil;
end;

procedure TfrmChild.FormDestroy(Sender: TObject);
begin
  frmMain.DeterminarToolbar(nil);
end;

El razonamiento que hay detrás es el siguiente: si no hay documentos abiertos el toolbar que se debe mostrar es el de frmMain, si hay al menos 1 abierto, entonces el toolbar que se debe mostrar es el de frmChild. Ahora bien, para comprender exactamente lo que este código implica debemos recordar primero el significado de la propiedad Parent. El componente considerado Padre o Parent de otro es el componente en donde ese "hijo" se va a dibujar. Es así que todos los componentes dentro de Form1 lo tienen como Parent, los componentes dentro de un panel tienen al panel como Parent y así.

Entonces si a DeterminarToolbar le pasan como parámetro algo diferente de nil, muestra ese toolbar que le pasan (AToolbar), de lo contrario muestra el toolbar de frmMain.

Nota: tlbMain.Parent := nil tiene el efecto de "esconder" el toolbar. El de frmMain se esconde en DeterminarToolbar cuando se considere necesario y el de frmChild se esconde al ejecutarse el evento OnDeactivate de frmChild.


Recomienda este documento a un amigo.
Recuerda enviarme tus comentarios sobre el artículo.


«anterior - Indice - siguiente»


 Copyright © Pablo Castagnino 2000-2002. Todos los derechos reservados.


Puedes ayudarnos

¿Conoces algún documento enteramente en español que pueda resultar interesante para nuestra comunidad delphiadicta?

Coméntanos de él aquí.


Vota por nosotros