๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
C#

WPF TreeView Drag & Drop ๊ตฌํ˜„

by samie 2025. 4. 10.
WPF TreeView Drag & Drop ๊ตฌํ˜„

๐Ÿ–ฑ๏ธ WPF TreeView์—์„œ Drag & Drop์œผ๋กœ ๋…ธ๋“œ ์ด๋™ํ•˜๊ธฐ

์ด๋ฒˆ ํฌ์ŠคํŠธ์—์„œ๋Š” WPF TreeView์—์„œ ๋…ธ๋“œ๋ฅผ ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ(Drag & Drop)์œผ๋กœ ์ด๋™ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.
๋‹จ์ˆœํžˆ UI ์ด๋ฒคํŠธ๋งŒ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ MVVM ๊ตฌ์กฐ ์•ˆ์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ํŒ๋„ ํ•จ๊ป˜ ์•Œ๋ ค๋“œ๋ฆฝ๋‹ˆ๋‹ค.

1. Category ๋ชจ๋ธ ํด๋ž˜์Šค


public class Category
{
    public string Name { get; set; }
    public ObservableCollection<Category> Children { get; set; } = new();
}

2. ViewModel ๊ตฌ์„ฑ


public class MainViewModel
{
    public ObservableCollection<Category> Categories { get; set; }

    public MainViewModel()
    {
        Categories = new ObservableCollection<Category>
        {
            new Category
            {
                Name = "๋ฐฑ์—”๋“œ",
                Children = new ObservableCollection<Category>
                {
                    new Category { Name = "ASP.NET" },
                    new Category { Name = "Node.js" }
                }
            },
            new Category
            {
                Name = "ํ”„๋ก ํŠธ์—”๋“œ",
                Children = new ObservableCollection<Category>
                {
                    new Category { Name = "React" },
                    new Category { Name = "Vue" }
                }
            }
        };
    }
}

3. XAML์—์„œ Drag & Drop ๊ตฌํ˜„


<TreeView ItemsSource="{Binding Categories}" AllowDrop="True"
          PreviewMouseLeftButtonDown="TreeView_PreviewMouseLeftButtonDown"
          DragOver="TreeView_DragOver"
          Drop="TreeView_Drop" Margin="10">

  <TreeView.ItemTemplate>
    <HierarchicalDataTemplate ItemsSource="{Binding Children}">
      <TextBlock Text="{Binding Name}" />
    </HierarchicalDataTemplate>
  </TreeView.ItemTemplate>
</TreeView>

๐Ÿ“Œ Code-behind ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ


private Category? _draggedItem;

private void TreeView_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    TreeViewItem item = FindAncestor<TreeViewItem>((DependencyObject)e.OriginalSource);
    if (item?.DataContext is Category category)
    {
        _draggedItem = category;
        DragDrop.DoDragDrop(item, category, DragDropEffects.Move);
    }
}

private void TreeView_DragOver(object sender, DragEventArgs e)
{
    e.Effects = DragDropEffects.Move;
    e.Handled = true;
}

private void TreeView_Drop(object sender, DragEventArgs e)
{
    if (_draggedItem == null)
        return;

    TreeViewItem item = FindAncestor<TreeViewItem>((DependencyObject)e.OriginalSource);
    if (item?.DataContext is Category targetCategory && targetCategory != _draggedItem)
    {
        // ๋ถ€๋ชจ ๋…ธ๋“œ์—์„œ ์ œ๊ฑฐ
        RemoveFromParent(DataContext as MainViewModel, _draggedItem);
        // ๋Œ€์ƒ ๋…ธ๋“œ์˜ ์ž์‹์œผ๋กœ ์ถ”๊ฐ€
        targetCategory.Children.Add(_draggedItem);
    }
    _draggedItem = null;
}

private void RemoveFromParent(MainViewModel vm, Category child)
{
    foreach (var parent in vm.Categories.SelectMany(GetAllCategories))
    {
        if (parent.Children.Contains(child))
        {
            parent.Children.Remove(child);
            return;
        }
    }
}

private IEnumerable<Category> GetAllCategories(Category cat)
{
    yield return cat;
    foreach (var child in cat.Children.SelectMany(GetAllCategories))
        yield return child;
}

// Visual Tree Helper
private static T? FindAncestor<T>(DependencyObject current) where T : DependencyObject
{
    while (current != null)
    {
        if (current is T desired)
            return desired;
        current = VisualTreeHelper.GetParent(current);
    }
    return null;
}

4. ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ๋™์ž‘ ์„ค๋ช…

  • ๋…ธ๋“œ๋ฅผ ํด๋ฆญ ํ›„ ๋“œ๋ž˜๊ทธํ•˜๋ฉด DragDrop ์‹œ์ž‘
  • ๋“œ๋กญ ๋Œ€์ƒ ๋…ธ๋“œ์˜ ์ž์‹์œผ๋กœ ์ด๋™
  • ๊ธฐ์กด ๋ถ€๋ชจ์—์„œ ํ•ด๋‹น ๋…ธ๋“œ๋ฅผ ์ฐพ์•„ ์ œ๊ฑฐ ํ›„ ์ด๋™
  • MVVM ๊ตฌ์กฐ์— ๋งž์ถฐ ViewModel ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ •

์™„์ „ํ•œ MVVM ๊ตฌ์กฐ๋กœ ํ•˜๋ ค๋ฉด Behavior ๋˜๋Š” Attached Property๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
ํ•˜์ง€๋งŒ ๊ธฐ๋ณธ ๊ตฌ์กฐ์—์„œ๋Š” code-behind๋„ ์ถฉ๋ถ„ํžˆ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

5. ๋งˆ๋ฌด๋ฆฌ ๋ฐ ๋‹ค์Œ ํŽธ ์˜ˆ๊ณ 

TreeView์—์„œ ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ํ˜•ํƒœ์˜ ๋…ธ๋“œ ์ด๋™์„ ๊ตฌํ˜„ํ•ด๋ดค์Šต๋‹ˆ๋‹ค.

  • ViewModel์˜ ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ DragDrop ๊ตฌํ˜„
  • TreeViewItem ์ฐพ๊ธฐ ๋ฐ VisualTree ํƒ์ƒ‰
  • ์ž์‹ ์ด๋™ ๋ฐ ์ œ๊ฑฐ ๋กœ์ง ์บก์Аํ™”

๋‹ค์Œ ์‹œ๋ฆฌ์ฆˆ์—์„œ๋Š” TreeView์˜ ์ฒดํฌ๋ฐ•์Šค(Checkbox) ๋…ธ๋“œ ์ฒ˜๋ฆฌ์— ๋Œ€ํ•ด ๋‹ค๋ค„๋ณผ ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.
๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค! ๐Ÿ˜„ ๋” ๋‹ค๋ค„๋ณด๊ณ  ์‹ถ์€ TreeView ๊ธฐ๋Šฅ์ด ์žˆ๋‹ค๋ฉด ๋Œ“๊ธ€๋กœ ์•Œ๋ ค์ฃผ์„ธ์š”!