Zurück zur Startseite

Benutzerdefinierte ListBox in WPF: Drag-and-Drop und Skalierung

Der Artikel widmet sich der Entwicklung benutzerdefinierter WPF-Steuerelemente mit Skalierung, Drag-and-Drop und flexibler Sortierungsunterstützung. Zwei Schlüsseldkomponenten werden betrachtet: ExtendedListBox für Skalierung und DragAnimatedPanel für animierten Elemententransfer. Code-Beispiele werden bereitgestellt und Implementierungsstrategien erklärt.

Erstellen einer ListBox in WPF mit Drag-and-Drop und Skalierung
Advertisement 728x90

# Implementierung einer erweiterten ListBox in WPF: Skalierung, Sortierung und Drag-and-Drop

Die Entwicklung benutzerdefinierter WPF-Steuerelemente mit Drag-and-Drop, Skalierung und flexibler Sortierungsunterstützung ist eine anspruchsvolle, aber essenzielle Aufgabe für moderne Benutzeroberflächen. In diesem Artikel tauchen wir tief in die Implementierung zweier Schlüsselfunktionen ein: eine erweiterte ListBox und ein Panel mit animiertem Element-Dragging. Die Lösung berücksichtigt Trennung der Verantwortlichkeiten, Leistungsoptimierung und Unterstützung für verschiedene Element-Layout-Strategien.

Skalierung: Auslagerung der Funktionalität in ExtendedListBox

Um die Skalierung von Listenelementen richtig zu implementieren, haben wir diese Funktionalität in eine separate ExtendedListBox-Steuerelement extrahiert, die von der standardmäßigen ListBox erbt. Dieser Ansatz vermeidet die Komplizierung der Logik des animierten Drag-Panels und ermöglicht die Wiederverwendung der Komponente. Wichtige Implementierungslemente:

  • Abhängigkeitseigenschaft Scale mit Limits über MinScale/MaxScale
  • Behandlung des MouseWheel-Ereignisses für Skalierungsänderungen
  • Berechnung des neuen Skalierungsfaktors mit sanfter Begrenzung

Der Hauptalgorithmus zur Behandlung des Mausrads:

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 ist entscheidend, die Elementvorlage über ItemTemplate richtig einzurichten. Die Skalierung wird mit ScaleTransform angewendet, das an die Eigenschaft des Steuerelements gebunden ist:

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

Dieses Muster gewährleistet die korrekte Elementtransformation bei jedem Skalierungswert ohne zusätzliche Code-Behind-Berechnungen.

Layout-Strategien: Benutzerdefinierte Sortierung in DragAnimatedPanel

Um flexible Elementlayouts zu implementieren, haben wir das DragAnimatedPanel mit Unterstützung für vier Sortierungsstrategien erstellt. Die Basisarchitektur überschreibt die Methoden MeasureOverride und ArrangeOverride, was volle Kontrolle über das Messen und Anordnen der Kindelemente ermöglicht.

Google AdInline article slot

Schlüsselphasen des Betriebs:

  • Berechnung der erforderlichen Panelgrößen in MeasureOverride
  • Bestimmung der Elementpositionen in ArrangeOverride
  • Dynamischer Wechsel der Strategien über die ILayoutStrategy-Schnittstelle

Beispielhafte Basisimplementierung von MeasureOverride für diagonales 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;
}

Unterstützte Layout-Strategien:

Google AdInline article slot
  • Horizontales Band (alle Elemente in einer Zeile)
  • Vertikale Spalte (alle Elemente in einer Spalte)
  • Tabellenlayout (feste Anzahl von Spalten)
  • Zeilenfüllung (dynamische Spaltenberechnung)

Jede Strategie implementiert die ILayoutStrategy-Schnittstelle mit Methoden:

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

Dieser Ansatz gewährleistet Erweiterbarkeit und einfachen Austausch von Sortierungsalgorithmen ohne Änderung der Hauptpanel-Logik.

Drag-and-Drop: Element-Dragging-Algorithmus

Die Drag-and-Drop-Implementierung umfasst drei kritische Phasen: Erkennen des Drag-Starts, Behandlung der Bewegung und Abschließen des Ergebnisses. Um Fehltrigger zu verhindern, haben wir Überprüfungen für die Drückdauer und den Cursor-Verdrängungsschwellenwert hinzugefügt.

Drag-Initialisierungsprozess:

  • Erfassen von Koordinaten und Zeit in OnMouseLeftButtonDown
  • Überprüfung der Bedingungen in OnMouseMove (minimale Haltezeit und Verdrängung)
  • Erfassen des Elements und der Maus 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();
}

Eine wichtige Optimierung ist die Überprüfung des Verdrängungsschwellenwerts (10 Pixel), bevor die Bewegung verarbeitet wird. Dies reduziert die Belastung bei der Arbeit mit einer großen Anzahl von Elementen:

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

Während der Elementbewegung berechnen wir dynamisch seine Position und seinen Index in der Sammlung über die SwapElement-Methode neu, die die Datenquelle über die IList-Schnittstelle korrekt aktualisiert.

Wichtige Erkenntnisse

  • Skalierung in eine separate Steuerelement extrahieren, um dem Prinzip der Einzelverantwortung zu folgen
  • Die ILayoutStrategy-Schnittstelle nutzen, um mehrere Sortierungsalgorithmen ohne Änderung der Hauptlogik zu unterstützen
  • Cursor-Verdrängungsschwellenwert (10+ Pixel) ist entscheidend für die Drag-and-Drop-Leistung bei einer großen Anzahl von Elementen
  • Für korrekte UI-Updates während des Element-Draggings immer mit ZIndex arbeiten und die Maus über CaptureMouse erfassen

— Editorial Team

Advertisement 728x90

Weiterlesen