lunes, 21 de febrero de 2022

Arrastrar y Soltar: un caso práctico

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.

TMiDragObject

La aplicación utiliza su propia clase descendiente de TDragControlObject llamada TMiDragObject e implementada en la unidad UDragDrop.pas.

Propiedades nuevas

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.

Imagen al arrastrar

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.

TDragControlObjectEx

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.

Aceptar el objeto arrastrado

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.

DoAgregar

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 := TPanel.Create(Self);
  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];
 
  DoAgregarSeparador(aIndex + 1);
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.

Otros eventos

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.

Conclusiones

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.

No hay comentarios:

Publicar un comentario