홈으로 돌아가기

WPF의 사용자 지정 ListBox: 드래그 앤 드롭 및 스케일링

이 기사는 스케일링, 드래그 앤 드롭 및 유연한 정렬 지원이 있는 custom WPF controls 개발에 전념합니다. 두 가지 주요 구성 요소를 고려합니다: 스케일링을 위한 ExtendedListBox와 애니메이션 요소 전송을 위한 DragAnimatedPanel. 코드 예제가 제공되며 구현 전략이 설명됩니다.

드래그 앤 드롭 및 스케일링이 있는 WPF ListBox 만들기
Advertisement 728x90

WPF에서 고급 ListBox 구현: 스케일링, 정렬 및 드래그 앤 드롭

드래그 앤 드롭, 스케일링, 유연한 정렬 기능을 갖춘 사용자 지정 WPF 컨트롤을 개발하는 것은 도전적이지만 현대 인터페이스에 필수적인 작업입니다. 이 글에서는 향상된 ListBox와 애니메이션 요소 드래깅을 지원하는 패널 두 가지 핵심 구성 요소 구현 방법을 깊이 있게 살펴보겠습니다. 이 솔루션은 관심사 분리, 성능 최적화, 다양한 요소 레이아웃 전략 지원을 포함합니다.

스케일링: ExtendedListBox로 기능 추출

리스트 항목 스케일링을 제대로 구현하기 위해 이 기능을 표준 ListBox에서 상속된 별도의 ExtendedListBox 컨트롤로 추출했습니다. 이 접근 방식은 애니메이션 드래그 패널의 로직을 복잡하게 만드는 것을 피하고 구성 요소 재사용을 가능하게 했습니다. 주요 구현 요소:

  • 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

다음 읽기