Powrót do strony głównej

Niestandardowy ListBox w WPF: drag-and-drop i skalowanie

Artykuł poświęcony jest opracowywaniu niestandardowych kontrolek WPF z obsługą skalowania, drag-and-drop i elastycznego sortowania. Omówiono dwa kluczowe komponenty: ExtendedListBox do skalowania i DragAnimatedPanel do animowanego przenoszenia elementów. Podano przykłady kodu i wyjaśniono strategie implementacji.

Tworzymy ListBox w WPF z drag-and-drop i skalowaniem
Advertisement 728x90

# Implementacja rozszerzonego ListBox w WPF: skalowanie, sortowanie i drag-and-drop

Tworzenie niestandardowych kontrolek WPF z obsługą drag-and-drop, skalowania i elastycznego sortowania — to skomplikowane, ale niezbędne zadanie dla nowoczesnych interfejsów. W tym artykule szczegółowo omówimy implementację dwóch kluczowych komponentów: rozszerzonego ListBox i panelu z animowanym przenoszeniem elementów. Rozwiązanie obejmuje separację odpowiedzialności, optymalizację wydajności i obsługę różnych strategii układania elementów.

Skalowanie: wyodrębnijmy funkcjonalność do ExtendedListBox

Dla poprawnej implementacji skalowania elementów listy wyodrębniliśmy tę funkcjonalność do osobnego kontrolu ExtendedListBox, dziedziczącego po standardowym ListBox. Taki podejście pozwoliło uniknąć komplikowania logiki panelu z animowanym przenoszeniem i zapewniło ponowne wykorzystanie komponentu. Kluczowe elementy implementacji:

  • Dependency property Scale z ograniczeniami za pomocą MinScale/MaxScale
  • Obsługa zdarzenia MouseWheel w celu zmiany skali
  • Obliczenie nowego współczynnika z płynnym ograniczeniem

Podstawowy algorytm obsługi kółka myszy:

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

Krytycznie ważne jest poprawne skonfigurowanie szablonu elementu za pomocą ItemTemplate. Do zastosowania skali użyto ScaleTransform z powiązaniem do właściwości kontrolu:

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

Ten wzorzec gwarantuje poprawne przekształcanie elementów przy dowolnej wartości skali bez dodatkowych obliczeń w kodzie.

Strategie układania: niestandardowe sortowanie w DragAnimatedPanel

Dla realizacji elastycznego układania elementów utworzyliśmy panel DragAnimatedPanel z obsługą czterech strategii sortowania. Podstawowa architektura opiera się na nadpisywaniu metod MeasureOverride i ArrangeOverride, co pozwala na pełną kontrolę procesu pomiaru i aranżacji elementów potomnych.

Google AdInline article slot

Kluczowe etapy działania:

  • Obliczenie wymaganych rozmiarów panelu w MeasureOverride
  • Określenie pozycji elementów w ArrangeOverride
  • Dynamiczne przełączanie strategii za pomocą interfejsu ILayoutStrategy

Przykład podstawowej implementacji MeasureOverride dla układu diagonalnego:

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

Obsługiwane strategie układania:

Google AdInline article slot
  • Poziomy pasek (wszystkie elementy w jednym wierszu)
  • Pionowa kolumna (wszystkie elementy w jednej kolumnie)
  • Układ tabelaryczny (stała liczba kolumn)
  • Wypełnianie po wierszach (dynamiczne obliczanie kolumn)

Każda strategia implementuje interfejs ILayoutStrategy z metodami:

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

To podejście zapewnia rozszerzalność i łatwą wymianę algorytmów sortowania bez zmiany głównej logiki panelu.

Drag-and-drop: algorytm przenoszenia elementów

Implementacja drag-and-drop obejmuje trzy krytyczne etapy: wykrycie początku przenoszenia, obsługę przemieszczania i utrwalenie wyniku. Aby zapobiec fałszywym wyzwoleniom, wprowadziliśmy sprawdzenie czasu przytrzymania i progu przesunięcia kursora.

Proces inicjalizacji przenoszenia:

  • Zapisanie współrzędnych i czasu w OnMouseLeftButtonDown
  • Sprawdzenie warunków w OnMouseMove (minimalny czas przytrzymania i przesunięcie)
  • Przechwycenie elementu i myszy w 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();
}

Kluczowa optymalizacja — sprawdzenie progu przesunięcia (10 pikseli) przed obsługą przemieszczania. To zmniejsza obciążenie przy pracy z dużą liczbą elementów:

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))
    {
        // Logika przemieszczania
    }
}

Podczas przemieszczania elementu dynamicznie przeliczamy jego pozycję i indeks w kolekcji za pomocą metody SwapElement, która poprawnie aktualizuje źródło danych za pośrednictwem interfejsu IList.

Co ważne

  • Skalowanie należy wyodrębniać do osobnego kontrolu, aby przestrzegać zasady pojedynczej odpowiedzialności
  • Używaj interfejsu ILayoutStrategy do obsługi wielu algorytmów sortowania bez zmiany głównej logiki
  • Próg przesunięcia kursora (10+ pikseli) jest krytyczny dla wydajności przy drag-and-drop z dużą liczbą elementów
  • Dla poprawnej aktualizacji UI podczas przenoszenia elementów koniecznie pracuj z ZIndex i przechwytuj mysz za pomocą CaptureMouse

— Editorial Team

Advertisement 728x90

Czytaj dalej