Zpět na domů

Vlastní ListBox v WPF: drag-and-drop a škálování

Článek se věnuje vývoji vlastních WPF ovládacích prvků s podporou škálování, drag-and-drop a flexibilního třídění. Jsou zváženy dva klíčové komponenty: ExtendedListBox pro škálování a DragAnimatedPanel pro animovaný přenos prvků. Uvedeny příklady kódu a vysvětleny strategie implementace.

Vytváříme ListBox v WPF s drag-and-drop a škálováním
Advertisement 728x90

# Implementace rozšířeného ListBox v WPF: škálování, řazení a drag-and-drop

Vývoj vlastních WPF ovládacích prvků s podporou drag-and-drop, škálování a flexibilního řazení je složitá, ale nezbytná úloha pro moderní uživatelská rozhraní. V tomto článku podrobně rozebereme implementaci dvou klíčových komponent: rozšířeného ListBox a panelu s animovaným přenášením prvků. Řešení zahrnuje oddělení odpovědností, optimalizaci výkonu a podporu různých strategií umístění prvků.

Škálování: vyčlenění funkce do ExtendedListBox

Pro správnou implementaci škálování prvků seznamu jsme tuto funkcionalitu vyčlenili do samostatného ovládacího prvku ExtendedListBox, který dědí od standardního ListBox. Tento přístup umožnil vyhnout se zbytečnému složení logiky panelu s animovaným přenášením a zajistil opakované použití komponenty. Klíčové prvky implementace:

  • Dependency property Scale s omezeními přes MinScale/MaxScale
  • Zpracování události MouseWheel pro změnu škály
  • Výpočet nového koeficientu s plynulým omezením

Hlavní algoritmus zpracování kolečka myši:

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

Kriticky důležité je správně nastavit šablonu prvku přes ItemTemplate. Pro aplikaci škály se používá ScaleTransform s vazbou na vlastnost ovládacího prvku:

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

Tento vzor zaručuje správnou transformaci prvků při jakékoli hodnotě škály bez dalších výpočtů v kódu.

Strategie umístění: vlastní řazení v DragAnimatedPanel

Pro implementaci flexibilního umístění prvků jsme vytvořili panel DragAnimatedPanel s podporou čtyř strategií řazení. Základní architektura je postavena na přepsání metod MeasureOverride a ArrangeOverride, což umožňuje plnou kontrolu nad procesem měření a umístění podřízených prvků.

Google AdInline article slot

Klíčové etapy práce:

  • Výpočet požadovaných velikostí panelu v MeasureOverride
  • Určení pozic prvků v ArrangeOverride
  • Dynamické přepínání strategií přes rozhraní ILayoutStrategy

Příklad základní implementace MeasureOverride pro diagonální umístění:

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

Podporované strategie umístění:

Google AdInline article slot
  • Horizontální řada (všechny prvky v jednom řádku)
  • Vertikální sloupec (všechny prvky v jednom sloupci)
  • Tabulkové umístění (fixní počet sloupců)
  • Vyplňování po řádcích (dynamický výpočet sloupců)

Každá strategie implementuje rozhraní ILayoutStrategy s metodami:

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

Tento přístup zajišťuje rozšiřitelnost a snadnou výměnu algoritmů řazení bez změny hlavní logiky panelu.

Drag-and-drop: algoritmus přenášení prvků

Implementace drag-and-drop zahrnuje tři kritické etapy: detekci začátku přenášení, zpracování pohybu a fixaci výsledku. Pro prevenci falešných spuštění jsme implementovali kontrolu délky stisknutí a prahu posunu kurzoru.

Proces inicializace přenášení:

  • Fixace souřadnic a času v OnMouseLeftButtonDown
  • Kontrola podmínek v OnMouseMove (minimální čas držení a posun)
  • Zachycení prvku a myši v 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();
}

Klíčová optimalizace — kontrola prahu posunu (10 pixelů) před zpracováním pohybu. To snižuje zátěž při práci s velkým množstvím prvků:

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

Při pohybu prvku dynamicky přepočítáváme jeho pozici a index v kolekci přes metodu SwapElement, která správně aktualizuje zdroj dat přes rozhraní IList.

Co je důležité

  • Škálování je třeba vyčlenit do samostatného ovládacího prvku pro dodržení principu jediné odpovědnosti
  • Používejte rozhraní ILayoutStrategy pro podporu mnoha algoritmů řazení bez změny hlavní logiky
  • Prah posunu kurzoru (10+ pixelů) je kritický pro výkon při drag-and-drop s velkým počtem prvků
  • Pro správnou aktualizaci UI při přenášení prvků je nezbytné pracovat s ZIndex a zachytit myš přes CaptureMouse

— Editorial Team

Advertisement 728x90

Číst dál