Back to Home

Custom ListBox in WPF: drag-and-drop and scaling

The article is devoted to the development of custom WPF controls with scaling, drag-and-drop, and flexible sorting support. Two key components are considered: ExtendedListBox for scaling and DragAnimatedPanel for animated element transfer. Code examples are provided and implementation strategies are explained.

Creating ListBox in WPF with drag-and-drop and scaling
Advertisement 728x90

Implementing an Advanced ListBox in WPF: Scaling, Sorting, and Drag-and-Drop

Developing custom WPF controls with drag-and-drop, scaling, and flexible sorting support is a challenging but essential task for modern interfaces. In this article, we'll dive deep into implementing two key components: an enhanced ListBox and a panel with animated element dragging. The solution incorporates separation of concerns, performance optimization, and support for various element layout strategies.

Scaling: Extracting Functionality into ExtendedListBox

To properly implement list item scaling, we extracted this functionality into a separate ExtendedListBox control, inherited from the standard ListBox. This approach avoided complicating the logic of the animated drag panel and enabled component reuse. Key implementation elements:

  • Dependency property Scale with limits via MinScale/MaxScale
  • Handling the MouseWheel event for scale changes
  • Calculating the new scale factor with smooth clamping

The main mouse wheel handling algorithm:

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;
    }
}

It's crucial to properly set up the item template via ItemTemplate. Scaling is applied using ScaleTransform bound to the control's property:

<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>

This pattern ensures correct element transformation at any scale value without additional code-behind calculations.

Layout Strategies: Custom Sorting in DragAnimatedPanel

To implement flexible element layouts, we created the DragAnimatedPanel with support for four sorting strategies. The base architecture overrides MeasureOverride and ArrangeOverride methods, allowing full control over measuring and arranging child elements.

Google AdInline article slot

Key stages of operation:

  • Calculating required panel sizes in MeasureOverride
  • Determining element positions in ArrangeOverride
  • Dynamically switching strategies via the ILayoutStrategy interface

Example base implementation of MeasureOverride for diagonal layout:

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;
}

Supported layout strategies:

Google AdInline article slot
  • Horizontal strip (all elements in one row)
  • Vertical column (all elements in one column)
  • Table layout (fixed number of columns)
  • Row filling (dynamic column calculation)

Each strategy implements the ILayoutStrategy interface with methods:

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

This approach ensures extensibility and easy replacement of sorting algorithms without changing the main panel logic.

Drag-and-Drop: Element Dragging Algorithm

Drag-and-drop implementation includes three critical stages: detecting the start of dragging, handling movement, and committing the result. To prevent false triggers, we added checks for press duration and cursor displacement threshold.

Dragging initialization process:

  • Capturing coordinates and time in OnMouseLeftButtonDown
  • Checking conditions in OnMouseMove (minimum hold time and displacement)
  • Capturing the element and mouse in 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();
}

A key optimization is checking the displacement threshold (10 pixels) before processing movement. This reduces load when working with a large number of elements:

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
    }
}

During element movement, we dynamically recalculate its position and index in the collection via the SwapElement method, which correctly updates the data source via the IList interface.

Key Takeaways

  • Extract scaling into a separate control to adhere to the single responsibility principle
  • Use the ILayoutStrategy interface to support multiple sorting algorithms without changing the main logic
  • Cursor displacement threshold (10+ pixels) is critical for drag-and-drop performance with a large number of elements
  • For correct UI updates during element dragging, always work with ZIndex and capture the mouse via CaptureMouse

— Editorial Team

Advertisement 728x90

Read Next