Volver al inicio

ListBox personalizada en WPF: arrastrar y soltar y escalado

El artículo está dedicado al desarrollo de controles WPF personalizados con escalado, arrastrar y soltar y soporte para ordenación flexible. Se consideran dos componentes clave: ExtendedListBox para escalado y DragAnimatedPanel para transferencia animada de elementos. Se proporcionan ejemplos de código y se explican las estrategias de implementación.

Creando ListBox en WPF con arrastrar y soltar y escalado
Advertisement 728x90

# Implementación de un ListBox avanzado en WPF: escalado, ordenación y arrastrar y soltar

Desarrollar controles personalizados de WPF con arrastrar y soltar, escalado y soporte para ordenación flexible es una tarea desafiante pero esencial para interfaces modernas. En este artículo, profundizaremos en la implementación de dos componentes clave: un ListBox mejorado y un panel con arrastre animado de elementos. La solución incorpora separación de responsabilidades, optimización de rendimiento y soporte para diversas estrategias de disposición de elementos.

Escalado: Extracción de funcionalidad en ExtendedListBox

Para implementar correctamente el escalado de elementos de lista, extrajimos esta funcionalidad en un control ExtendedListBox separado, heredado del ListBox estándar. Este enfoque evitó complicar la lógica del panel de arrastre animado y permitió la reutilización del componente. Elementos clave de la implementación:

  • Propiedad de dependencia Scale con límites mediante MinScale/MaxScale
  • Manejo del evento MouseWheel para cambios de escala
  • Cálculo del nuevo factor de escala con limitación suave

El algoritmo principal de manejo de la rueda del ratón:

Google AdInline article slot
private void ProcessScale(MouseWheelEventArgs e)
{
    const double DELTA_DIVISOR = 1000d;

    double zoomScale = e.Delta / DELTA_DIVISOR;
    double newScaleFactor = Scale + zoomScale;

    if (newScaleFactor >= MinScale && newScaleFactor <= MaxScale)
    {
        Scale = newScaleFactor;
    }
    else if (newScaleFactor < MinScale)
    {
        Scale = MinScale;
    }
    else if (newScaleFactor > MaxScale)
    {
        Scale = MaxScale;
    }
}

Es crucial configurar correctamente la plantilla de elementos mediante ItemTemplate. El escalado se aplica usando ScaleTransform enlazado a la propiedad del control:

<customWpfControls:ExtendedListBox.ItemTemplate>
    <DataTemplate>
        <Border Margin="10">
            <Image Source="{Binding ImageSource}">
                <Image.LayoutTransform>
                    <TransformGroup>
                        <ScaleTransform 
                            ScaleX="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type customWpfControls:ExtendedListBox}}}" 
                            ScaleY="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type customWpfControls:ExtendedListBox}}}"/>
                    </TransformGroup>
                </Image.LayoutTransform>
            </Image>
        </Border>
    </DataTemplate>
</customWpfControls:ExtendedListBox.ItemTemplate>

Este patrón asegura la transformación correcta de elementos en cualquier valor de escala sin cálculos adicionales en code-behind.

Estrategias de disposición: Ordenación personalizada en DragAnimatedPanel

Para implementar disposiciones flexibles de elementos, creamos el DragAnimatedPanel con soporte para cuatro estrategias de ordenación. La arquitectura base sobrescribe los métodos MeasureOverride y ArrangeOverride, permitiendo control total sobre la medición y disposición de elementos hijo.

Google AdInline article slot

Etapas clave de operación:

  • Cálculo de tamaños requeridos del panel en MeasureOverride
  • Determinación de posiciones de elementos en ArrangeOverride
  • Cambio dinámico de estrategias mediante la interfaz ILayoutStrategy

Ejemplo de implementación base de MeasureOverride para disposición diagonal:

protected override Size MeasureOverride(Size availableSize)
{
    foreach (UIElement child in Children)
    {
        child.Measure(availableSize);
    }

    if (Children.Count == 0)
    {
        _calculatedSize = new Size();
    }
    else
    {
        List<Size> childSizes = new List<Size>(Children.Count);
        foreach (UIElement child in Children)
        {
            childSizes.Add(child.DesiredSize);
        }

        double width = childSizes.Sum(x => x.Width);
        double height = childSizes.Sum(x => x.Height);

        _calculatedSize = new Size(width, height);
    }

    return _calculatedSize;
}

Estrategias de disposición soportadas:

Google AdInline article slot
  • Tira horizontal (todos los elementos en una fila)
  • Columna vertical (todos los elementos en una columna)
  • Disposición en tabla (número fijo de columnas)
  • Relleno de filas (cálculo dinámico de columnas)

Cada estrategia implementa la interfaz ILayoutStrategy con métodos:

public interface ILayoutStrategy
{
    Size ResultSize { get; }
    void MeasureLayout(Size availablePanelSize, List<Size> sizes, bool isDragging);
    int GetIndex(Point position);
    ItemLayoutInfo GetLayoutInfo(int index);
}

Este enfoque asegura extensibilidad y reemplazo fácil de algoritmos de ordenación sin cambiar la lógica principal del panel.

Arrastar y soltar: Algoritmo de arrastre de elementos

La implementación de arrastrar y soltar incluye tres etapas críticas: detección del inicio del arrastre, manejo del movimiento y confirmación del resultado. Para prevenir activaciones falsas, agregamos verificaciones de duración de presión y umbral de desplazamiento del cursor.

Proceso de inicialización del arrastre:

  • Captura de coordenadas y tiempo en OnMouseLeftButtonDown
  • Verificación de condiciones en OnMouseMove (tiempo mínimo de retención y desplazamiento)
  • Captura del elemento y ratón en StartDrag
private void OnMouseLeftButtonDown(object sender, MouseEventArgs e)
{
    Point mousePos = Mouse.GetPosition(this);
    _lastMousePosX = mousePos.X;
    _lastMousePosY = mousePos.Y;
    _mouseDownTime = DateTime.Now;
    _mouseSelectedElement = GetChildThatHasMouseOver();
}

private void StartDrag(MouseEventArgs e)
{
    DraggedElement = _mouseSelectedElement;
    _draggedIndex = Children.IndexOf(DraggedElement);
    Point p = GetItemVisualPoint(DraggedElement);
    _x = p.X;
    _y = p.Y;

    SetZIndex(DraggedElement, DRAG_ELEMENT_Z_INDEX);
    CaptureMouse();
}

Una optimización clave es verificar el umbral de desplazamiento (10 píxeles) antes de procesar el movimiento. Esto reduce la carga al trabajar con un gran número de elementos:

private void OnDragOver(MouseEventArgs e)
{
    const double MOUSE_DIF = 10d;
    Point mousePos = Mouse.GetPosition(this);
    double difX = mousePos.X - _lastMousePosX;
    double difY = mousePos.Y - _lastMousePosY;
    
    if ((Math.Abs(difX) > MOUSE_DIF || Math.Abs(difY) > MOUSE_DIF))
    {
        // Movement logic
    }
}

Durante el movimiento del elemento, recalculamos dinámicamente su posición e índice en la colección mediante el método SwapElement, que actualiza correctamente la fuente de datos mediante la interfaz IList.

Puntos clave

  • Extraer el escalado a un control separado para adherirse al principio de responsabilidad única
  • Usar la interfaz ILayoutStrategy para soportar múltiples algoritmos de ordenación sin cambiar la lógica principal
  • El umbral de desplazamiento del cursor (10+ píxeles) es crítico para el rendimiento de arrastrar y soltar con un gran número de elementos
  • Para actualizaciones correctas de la interfaz de usuario durante el arrastre de elementos, siempre trabajar con ZIndex y capturar el ratón mediante CaptureMouse

— Editorial Team

Advertisement 728x90

Leer después