# 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
Scalez ograniczeniami za pomocąMinScale/MaxScale - Obsługa zdarzenia
MouseWheelw celu zmiany skali - Obliczenie nowego współczynnika z płynnym ograniczeniem
Podstawowy algorytm obsługi kółka myszy:
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.
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:
- 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
ILayoutStrategydo 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
ZIndexi przechwytuj mysz za pomocąCaptureMouse
— Editorial Team
Brak komentarzy.