Descripción del caso práctico
La funcionalidad
de la aplicación es bastante simple. En el formulario principal hay un
TFlowPanel, dos TPanel (uno rojo y otro azul) y un TImage (con la imagen de una
papelera)
El formulario principal de la
aplicación.
El usuario
puede arrastrar cualquiera de los dos TPanel y soltarlos en el TFlowPanel. Al
hacerlo se creará un TPanel (en realidad dos ya que además del arrastrado y
soltado se crea otro que cumple la función de separador) dentro del TFlowPanel
del mismo color del TPanel arrastrado y soltado. Los TPanel que se vayan
creando dentro del TFlowPanel también podrán ser arrastrados y soltados. Si son
soltados sobre el mismo TFlowPanel cambiarán su posición. Si son soltados sobre
el TImage serán eliminados. Para mejorar la experiencia del usuario la
aplicación provee respuesta visual para las operaciones de arrastrar y soltar.
Al arrastrar se muestra el valor de la propiedad Caption del TPanel arrastrado
(en un caso el nombre del color y en otro la posición del TPanel dentro del
TFlowPanel) y al arrastrar sobre el TFlowPanel (o cualquiera de los TPanel que
hay dentro de él) se resalta el sitio donde será creado el TPanel si es soltado
en ese momento.
La
aplicación utiliza su propia clase descendiente de TDragControlObject llamada
TMiDragObject e implementada en la unidad UDragDrop.pas.
En primer
lugar se agregan unas propiedades para poder personalizar las operaciones de
arrastrar y soltar.
private
FColor: TColor;
FDragText: string;
FExterno: boolean;
public
property Color: TColor read FColor write FColor;
property DragText: string read FDragText write FDragText;
property Externo: boolean read FExterno write FExterno;
La propiedad
Color es para determinar el color del TPanel arrastrado (que será el color del
TPanel que se agregará al TFlowPanel).
La propiedad
DragText es para especificar el texto que se mostrará junto con el icono de
arrastrar.
La propiedad
Externo es para determinar si el TPanel arrastrado es uno de los que está fuera
del TFlowPanel o uno de los agregados al TFlowPanel. En el primer caso hay que
agregar un TPanel al TFlowPanel y en el segundo caso sólo hay que cambiar la
posición del TPanel arrastrado y soltado.
Es posible
mostrar una imagen al arrastrar junto al puntero del ratón. En este caso se
utiliza esa imagen para mostrar un texto. Para ello se deben sobrescribir tres
métodos:
protected
function GetDragImages: TDragImageList; override;
public
procedure HideDragImage; override;
procedure ShowDragImage; override;
El método
GetDragImages permite proveer una instancia especial de TImageList
(TDragImageList) con la imagen correspondiente. La implementación de este
método no hace más que crear una instancia de TDragImageList y agregarle la
imagen luego de dibujar sobre ella el valor de la propiedad Caption del TPanel
arrastrado. Hay un par de líneas que merecen una explicación:
vIndex := FDragImageList.AddMasked(oBitmap, clYellow);
FDragImageList.SetDragImage(vIndex, Round(oBitmap.Width / 2),
oBitmap.Height);
Con
SetDragImage se establece lo que en inglés se llama HotSpot que son las
coordenadas de un punto de la imagen que se convierte en el punto sobre el que
se posiciona el puntero del ratón al arrastrar. En este caso ese punto es el
centro para el eje X y el borde inferior para el eje Y.
Finalmente
HideDragImage y ShowDragImage que se encargan de ocultar y mostrar la imagen.
La implementación por defecto hace uso de la instancia de TDragImageList que
provee el control arrastrado. En este caso el control arrastrado es un TPanel
que no provee ninguna instancia de TDragImageList por lo que modificamos estos
dos métodos para hacer uso de la instancia TDragImageList de la propia
instancia de TMiDragObject.
Delphi
provee la clase TDragControlObjectEx que se diferencia de TDragControlObject en
que se destruye automáticamente cuando la operación arrastrar y soltar
finaliza. En este caso la aplicación se encarga de destruir dicha instancia.
Iniciar una operación arrastrar y soltar
En este caso
la aplicación utiliza los eventos OnMouseDown y OnMouseMove para iniciar las
operaciones de arrastrar y soltar.
procedure
TfrmPrincipal.ExPanelMouseDown(aSender: TObject;
aButton: TMouseButton; aShift: TShiftState; aX, aY: integer);
begin
if aButton = mbLeft then
FPunto := Point(aX, aY);
end;
En el evento
OnMouseDown se obtiene el punto en el cual el usuario hizo clic y se almacena
en el campo FPunto declarado como TPoint.
procedure
TfrmPrincipal.ExPanelMouseMove(aSender: TObject; aShift: TShiftState;
aX, aY: integer);
begin
if not FArrastrando and (ssLeft in aShift)
and ( (Abs(FPunto.X - aX) > 5) or (Abs(FPunto.Y - aY) > 5) )
then
TControl(aSender).BeginDrag(True);
end;
En el evento
OnMouseMove se detecta el momento en el cual el puntero del ratón se ha
desplazado al menos cinco puntos para iniciar la operación arrastrar y soltar.
El campo FArrastrando sirve para evitar problema al mover el puntero del ratón
sobre un control una vez se ha iniciado una operación de arrastrar y soltar.
El método
BeginDrag(True) inicia inmediatamente una operación arrastrar y soltar y
dispara el evento OnStartDrag.
procedure
TfrmPrincipal.ExPanelStartDrag(aSender: TObject;
var aDragObject: TDragObject);
begin
FDragObject := TMiDragObject.Create(TPanel(aSender));
TMiDragObject(FDragObject).Color := TPanel(aSender).Color;
TMiDragObject(FDragObject).DragText := TPanel(aSender).Caption;
TMiDragObject(FDragObject).Externo := True;
aDragObject := FDragObject;
FArrastrando := True;
end;
En este
evento se crea una instancia de TMiDragObject pasando como parámetro del
constructor el objeto que lo disparó (en este caso un TPanel externo). Luego se
asigna el color, el texto para la imagen y se indica que se trata de un control
externo. Se asigna la instancia de TMiDragObject al parámetro aDragObject y
finalmente se indica que se ha iniciado una operación arrastrar y soltar.
Con estos
tres evento se ha iniciado la operación arrastrar y soltar.
Durante una
operación arrastrar y soltar se dispara el evento OnDragOver en los controles
sobre los que pasa el puntero del ratón. En este evento se debe indicar si se
acepta el objeto arrastrado.
procedure
TfrmPrincipal.FlowPanelDragOver(aSender, aSource: TObject;
aX, aY: integer; aState: TDragState; var aAccept: boolean);
var
oSeparador: TPanel;
begin
aAccept := (aSource is TMiDragObject) and
(TMiDragObject(aSource).Externo);
if aAccept then
begin
if FlowPanel.ControlCount > 0 then
begin
oSeparador := TPanel(GetControlAtIndex((FlowPanel.ControlCount - 1)));
TMiDragObject(aSource).HideDragImage;
if aState = dsDragEnter then
oSeparador.Color := clBlack
else if aState = dsDragLeave then
oSeparador.ParentColor := True;
oSeparador.Update;
TMiDragObject(aSource).ShowDragImage;
end;
end;
end;
En primer
lugar se determina si se acepta el objeto arrastrado. Si el objeto arrastrado
es aceptado entonces se muestra una respuesta visual que le indica al usuario
dónde se colocará el objeto arrastrado si lo suelta en ese momento. Para la
respuesta visual se utiliza el separador. El parámetro aState indica si el
puntero del ratón acaba de entrar en el control (dsDragEnter), si acaba de
salir (dsDragLeave) o si se ha movido luego de haber entrado y antes de haber
salido (dsDragMove)
Esta
implementación del evento OnDragOver corresponde al TFlowPanel. En este caso la
respuesta visual consiste en cambiar el color del último separador. Para
obtenerlo se utiliza el método GetControlAtIndex ya que el mismo TFlowPanel no
tiene ningún método para hacerlo. Luego se oculta la imagen de arrastrar y
soltar, se cambia el color del separador para mostrar la respuesta visual, se
actualiza el separador (para que se vuelva a pintar sin esperar el mensaje
correspondiente) y finalmente se vuelve a mostrar la imagen de arrastrar y
soltar.
La
implementación del evento OnDragOver varía dependiendo del control. Más
adelante veremos las otras implementaciones.
El objeto arrastrado fue soltado
Cuando el
usuario suelta el objeto arrastrado sobre un control se dispara el evento
OnDragDrop para el control sobre el cual fue soltado el objeto arrastrado y el
evento OnEndDrag para el control que inició la operación arrastrar y soltar.
En el evento
OnDragDrop se agregar un TPanel al TFlowPanel.
procedure
TfrmPrincipal.FlowPanelDragDrop(aSender, aSource: TObject;
aX, aY: integer);
begin
DoAgregar(TMiDragObject(aSource).Color, FlowPanel.ControlCount + 1);
end;
Se utiliza
el método DoAgregar para agregar el panel con el color y en la posición
indicada. En este caso la posición es luego del último control dentro del TFlowPanel.
En el evento
OnEndDrag es poco lo que se hace.
procedure
TfrmPrincipal.ExPanelEndDrag(aSender, aTarget: TObject;
aX, aY: integer);
begin
FArrastrando := False;
FreeAndNil(FDragObject);
end;
Se indica
que ya no hay una operación arrastrar y soltar en curso y se destruye la
instancia de TMiDragObject (que fue creada en el evento OnStartDrag). Esto
último no sería necesario si se utilizara la clase TDragControlObjectEx.
Este método
se encarga de agregar un TPanel (y un separador) al TFlowPanel.
procedure
TfrmPrincipal.DoAgregar(aColor: TColor; aIndex: integer);
var
oPanel: TPanel;
begin
if FlowPanel.ControlCount = 0 then
DoAgregarSeparador(0);
oPanel.BevelInner := bvNone;
oPanel.BevelOuter := bvNone;
oPanel.Width := 32;
oPanel.Height := 32;
oPanel.ParentBackground := False;
oPanel.Color := aColor;
if FlowPanel.ControlCount = 1 then
oPanel.Caption := '1'
else
oPanel.Caption := IntToStr(Round((FlowPanel.ControlCount + 1) / 2));
oPanel.Parent := FlowPanel;
FlowPanel.SetControlIndex(oPanel, aIndex);
oPanel.OnDragOver := InPanelDragOver;
oPanel.OnDragDrop := InPanelDragDrop;
oPanel.OnStartDrag := InPanelStartDrag;
oPanel.OnEndDrag := InPanelEndDrag;
oPanel.OnMouseDown := InPanelMouseDown;
oPanel.OnMouseMove :=
InPanelMouseMove;
oPanel.ControlStyle := oPanel.ControlStyle + [csDisplayDragImage];
end;
Si es el
primer control agregado al TFlowPanel entonces se agrega un separador al
principio. Luego se crear una instancia de TPanel, se configura, se agrega al
TFlowPanel y se asignan manejadores para los eventos involucrados en
operaciones arrastrar y soltar. Finalmente se crea un separador.
Algunas
líneas merecen explicación.
oPanel.ControlStyle := oPanel.ControlStyle + [csDisplayDragImage];
Esta línea
habilita el control (el TPanel) para que muestre la imagen de arrastrar y
soltar cuando el puntero del ratón pase sobre él. Algo similar se hace en la
clase TMiDragObject para obtener el mismo resultado sobre otros controles.
Hasta aquí
se ha explicado una operación arrastrar y soltar completa. Sin embargo la
funcionalidad de la aplicación requiere de más código que no se explica
detalladamente por razones de espacio. Sin embargo ese código resuelve
particularidades que no son fundamentales para comprender las operaciones
arrastrar y soltar sino más bien aspectos específicos de la funcionalidad de la
aplicación.
Las
operaciones arrastrar y soltar pueden dotar a una aplicación de una facilidad
de uso a prueba de usuarios principiantes. De hecho es una de las
funcionalidades más intuitivas de cara al usuario. La implementación básica es
sencilla y Delphi nos da casi todo resuelto. Sin embargo para darle un toque
más profesional es necesario recurrir a pequeños trucos que no siempre están
bien documentados.
En este caso
se han explicado sólo algunos aspectos de las operaciones arrastrar y soltar.
Espero poder abordar otros aspectos en futuros artículos.