|
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:
-
caNone: se le permite a la ventana cerrarse y
no pasa nada más.
-
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.
-
caFree: la ventana se cierra y se libera toda
la memoria asignada al form.
-
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.
|