返回首页

WPF 中的自定义 ListBox:拖放和缩放

本文致力于开发带有缩放、拖放和灵活排序支持的自定义 WPF 控件。考虑了两个关键组件:用于缩放的 ExtendedListBox 和用于动画元素传输的 DragAnimatedPanel。提供了代码示例并解释了实现策略。

在 WPF 中创建带有拖放和缩放的 ListBox
Advertisement 728x90

# 在 WPF 中实现高级 ListBox:缩放、排序和拖放

开发带有拖放、缩放和灵活排序支持的自定义 WPF 控件,是现代界面中一项富有挑战性但至关重要的任务。本文将深入探讨实现两个核心组件:增强型 ListBox 和支持动画元素拖拽的面板。该解决方案体现了关注点分离、性能优化,并支持多种元素布局策略。

缩放:将功能提取到 ExtendedListBox 中

为了正确实现列表项缩放,我们将此功能提取到一个独立的 ExtendedListBox 控件中,该控件继承自标准的 ListBox。这种方法避免了动画拖拽面板逻辑的复杂化,并实现了组件复用。主要实现要素包括:

  • 通过 MinScale/MaxScale 设置限制的依赖属性 Scale
  • 处理 MouseWheel 事件以实现缩放变化
  • 使用平滑限制计算新的缩放因子

主要鼠标滚轮处理算法:

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

正确设置项模板通过 ItemTemplate 至关重要。缩放使用绑定到控件属性的 ScaleTransform 应用:

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

这种模式确保在任何缩放值下都能正确转换元素,而无需额外的代码后置计算。

布局策略:DragAnimatedPanel 中的自定义排序

为了实现灵活的元素布局,我们创建了 DragAnimatedPanel,支持四种排序策略。基础架构重写了 MeasureOverrideArrangeOverride 方法,从而完全控制子元素的测量和排列。

Google AdInline article slot

关键操作阶段:

  • MeasureOverride 中计算所需面板尺寸
  • ArrangeOverride 中确定元素位置
  • 通过 ILayoutStrategy 接口动态切换策略

对角布局的 MeasureOverride 示例基础实现:

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

支持的布局策略:

Google AdInline article slot
  • 水平条带(所有元素在一行)
  • 垂直列(所有元素在一列)
  • 表格布局(固定列数)
  • 行填充(动态列计算)

每种策略都实现了 ILayoutStrategy 接口,包含以下方法:

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

这种方法确保了可扩展性,并能轻松替换排序算法,而无需更改主面板逻辑。

拖放:元素拖拽算法

拖放实现包括三个关键阶段:检测拖拽开始、处理移动以及提交结果。为了防止误触发,我们添加了按压持续时间和光标位移阈值的检查。

拖拽初始化过程:

  • OnMouseLeftButtonDown 中捕获坐标和时间
  • OnMouseMove 中检查条件(最小保持时间和位移)
  • 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();
}

一项关键优化是在处理移动前检查位移阈值(10 像素)。这能减少处理大量元素时的负载:

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

在元素移动过程中,我们通过 SwapElement 方法动态重新计算其位置和集合中的索引,该方法通过 IList 接口正确更新数据源。

关键要点

  • 将缩放提取到单独控件中,以遵循单一职责原则
  • 使用 ILayoutStrategy 接口支持多种排序算法,而无需更改主逻辑
  • 光标位移阈值(10 像素以上)对大量元素的拖放性能至关重要
  • 为了在元素拖拽期间正确更新 UI,始终使用 ZIndex 并通过 CaptureMouse 捕获鼠标

— Editorial Team

Advertisement 728x90

继续阅读