Коли дані мають ієрархічну структуру — файлова система, організаційна структура компанії, меню навігації — звичайний список не підходить. Для таких сценаріїв WPF надає TreeView — контрол, що дозволяє відображати деревоподібні структури з можливістю згортання та розгортання вузлів.
У цій статті ми також розглянемо ListView з GridView — легковісну альтернативу DataGrid для табличного відображення даних без редагування, та навчимося реалізовувати Drag and Drop між контролами.
ListBox, DataGrid) та розуміють концепції DataTemplate і HierarchicalDataTemplate. Якщо ви вже працювали з плоскими списками даних, ви готові до вивчення ієрархічних структур.TreeView — це контрол для відображення ієрархічних даних, де кожен елемент може мати дочірні елементи, які, у свою чергу, можуть мати свої дочірні елементи, і так далі.
Найпростіший спосіб створити TreeView — вручну визначити елементи в XAML:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="400">
<TreeView Margin="20">
<TreeViewItem Header="📁 Документи" IsExpanded="True">
<TreeViewItem Header="📄 Звіт.docx" />
<TreeViewItem Header="📄 Презентація.pptx" />
<TreeViewItem Header="📁 Архів" IsExpanded="True">
<TreeViewItem Header="📄 Старий_звіт.docx" />
<TreeViewItem Header="📄 Backup.zip" />
</TreeViewItem>
</TreeViewItem>
<TreeViewItem Header="📁 Зображення" IsExpanded="True">
<TreeViewItem Header="🖼️ Фото1.jpg" />
<TreeViewItem Header="🖼️ Фото2.png" />
</TreeViewItem>
<TreeViewItem Header="📁 Музика">
<TreeViewItem Header="🎵 Пісня1.mp3" />
<TreeViewItem Header="🎵 Пісня2.mp3" />
</TreeViewItem>
</TreeView>
</Window>
Для реальних застосунків дані зазвичай приходять з моделі. HierarchicalDataTemplate дозволяє визначити, як відображати кожен рівень ієрархії:
Спочатку створимо модель даних:
public class FileSystemItem : INotifyPropertyChanged
{
private bool _isExpanded;
public string Name { get; set; }
public FileSystemItemType Type { get; set; }
public ObservableCollection<FileSystemItem> Children { get; set; }
public bool IsExpanded
{
get => _isExpanded;
set
{
_isExpanded = value;
OnPropertyChanged();
}
}
public string Icon => Type switch
{
FileSystemItemType.Folder => "📁",
FileSystemItemType.Document => "📄",
FileSystemItemType.Image => "🖼️",
FileSystemItemType.Audio => "🎵",
FileSystemItemType.Video => "🎬",
_ => "📄"
};
public FileSystemItem()
{
Children = new ObservableCollection<FileSystemItem>();
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
public enum FileSystemItemType
{
Folder,
Document,
Image,
Audio,
Video
}
ViewModel:
public class FileExplorerViewModel : INotifyPropertyChanged
{
public ObservableCollection<FileSystemItem> RootItems { get; set; }
public FileExplorerViewModel()
{
RootItems = new ObservableCollection<FileSystemItem>
{
new FileSystemItem
{
Name = "Документи",
Type = FileSystemItemType.Folder,
IsExpanded = true,
Children = new ObservableCollection<FileSystemItem>
{
new FileSystemItem { Name = "Звіт.docx", Type = FileSystemItemType.Document },
new FileSystemItem { Name = "Презентація.pptx", Type = FileSystemItemType.Document },
new FileSystemItem
{
Name = "Архів",
Type = FileSystemItemType.Folder,
Children = new ObservableCollection<FileSystemItem>
{
new FileSystemItem { Name = "Старий_звіт.docx", Type = FileSystemItemType.Document }
}
}
}
},
new FileSystemItem
{
Name = "Зображення",
Type = FileSystemItemType.Folder,
Children = new ObservableCollection<FileSystemItem>
{
new FileSystemItem { Name = "Фото1.jpg", Type = FileSystemItemType.Image },
new FileSystemItem { Name = "Фото2.png", Type = FileSystemItemType.Image }
}
}
};
}
public event PropertyChangedEventHandler PropertyChanged;
}
XAML з HierarchicalDataTemplate:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="450" Height="450">
<TreeView Margin="20" ItemsSource="{Binding RootItems}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Icon}"
FontSize="16"
Margin="0,0,5,0" />
<TextBlock Text="{Binding Name}"
VerticalAlignment="Center" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
</Window>
ItemsSource — прив'язка до колекції дочірніх елементів (наприклад, {Binding Children})ItemTemplate — вкладений шаблон для дочірніх елементів (якщо структура різна на різних рівнях)Властивість IsExpanded контролює, чи розгорнутий вузол:
<TreeView.ItemContainerStyle>
<Style Selector="TreeViewItem">
<!-- TwoWay binding для синхронізації з ViewModel -->
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
Програмне керування:
// Розгорнути всі вузли
public void ExpandAll(FileSystemItem item)
{
item.IsExpanded = true;
foreach (var child in item.Children)
{
ExpandAll(child);
}
}
// Згорнути всі вузли
public void CollapseAll(FileSystemItem item)
{
item.IsExpanded = false;
foreach (var child in item.Children)
{
CollapseAll(child);
}
}
Для великих дерев (наприклад, файлова система з тисячами файлів) завантажуйте дочірні елементи тільки при розгортанні вузла:
public class FileSystemItem : INotifyPropertyChanged
{
private bool _isExpanded;
private bool _childrenLoaded;
public bool IsExpanded
{
get => _isExpanded;
set
{
_isExpanded = value;
OnPropertyChanged();
// Завантажити дочірні елементи при першому розгортанні
if (value && !_childrenLoaded && Type == FileSystemItemType.Folder)
{
LoadChildren();
}
}
}
private void LoadChildren()
{
_childrenLoaded = true;
// Видалити placeholder
Children.Clear();
// Завантажити реальні дочірні елементи
try
{
var directoryInfo = new DirectoryInfo(FullPath);
foreach (var dir in directoryInfo.GetDirectories())
{
Children.Add(new FileSystemItem
{
Name = dir.Name,
FullPath = dir.FullName,
Type = FileSystemItemType.Folder,
Children = new ObservableCollection<FileSystemItem>
{
// Placeholder для показу стрілки розгортання
new FileSystemItem { Name = "Loading..." }
}
});
}
foreach (var file in directoryInfo.GetFiles())
{
Children.Add(new FileSystemItem
{
Name = file.Name,
FullPath = file.FullName,
Type = GetFileType(file.Extension)
});
}
}
catch (UnauthorizedAccessException)
{
// Немає доступу до папки
Children.Add(new FileSystemItem { Name = "Access Denied" });
}
}
private FileSystemItemType GetFileType(string extension)
{
return extension.ToLower() switch
{
".jpg" or ".png" or ".gif" => FileSystemItemType.Image,
".mp3" or ".wav" => FileSystemItemType.Audio,
".mp4" or ".avi" => FileSystemItemType.Video,
_ => FileSystemItemType.Document
};
}
}
Children. При розгортанні замініть його реальними даними.TreeView підтримує виділення елементів:
<TreeView ItemsSource="{Binding RootItems}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<!-- ItemTemplate -->
</TreeView>
ViewModel:
public class FileExplorerViewModel : INotifyPropertyChanged
{
private FileSystemItem _selectedItem;
public FileSystemItem SelectedItem
{
get => _selectedItem;
set
{
_selectedItem = value;
OnPropertyChanged();
// Реакція на зміну виділення
if (value != null)
{
Debug.WriteLine($"Виділено: {value.Name}");
LoadDetails(value);
}
}
}
private void LoadDetails(FileSystemItem item)
{
// Завантажити деталі про файл/папку
// Показати у панелі деталей
}
}
Ви можете повністю налаштувати вигляд TreeView:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="500" Height="450">
<TreeView Margin="20" ItemsSource="{Binding RootItems}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Border Background="Transparent"
Padding="5"
CornerRadius="4">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Icon}"
FontSize="18"
Margin="0,0,8,0" />
<TextBlock Text="{Binding Name}"
VerticalAlignment="Center"
FontSize="14" />
</StackPanel>
</Border>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="Padding" Value="2" />
<Style Selector="^:selected /template/ Border">
<Setter Property="Background" Value="#3b82f6" />
</Style>
<Style Selector="^:selected /template/ ContentPresenter">
<Setter Property="TextElement.Foreground" Value="White" />
</Style>
<Style Selector="^:pointerover /template/ Border">
<Setter Property="Background" Value="#e5e7eb" />
</Style>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
</Window>
ListView з GridView — це легковісна альтернатива DataGrid для відображення табличних даних без редагування.
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="700" Height="400">
<Grid Margin="20">
<ListBox>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="200,150,*,100" Margin="0,5">
<TextBlock Grid.Column="0"
Text="{Binding Name}"
FontWeight="Bold" />
<TextBlock Grid.Column="1"
Text="{Binding Email}"
Foreground="#6b7280" />
<TextBlock Grid.Column="2"
Text="{Binding Phone}"
Foreground="#6b7280" />
<TextBlock Grid.Column="3"
Text="{Binding Department}"
Foreground="#3b82f6" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
ListView з GridView як у WPF. Замість цього використовується ListBox з кастомним DataTemplate для імітації колонок, або DataGrid для повноцінних табличних даних.| Функція | DataGrid | ListView + GridView |
|---|---|---|
| Редагування inline | ✅ Вбудоване | ❌ Немає |
| Сортування | ✅ Автоматичне | ⚠️ Ручна реалізація |
| Валідація | ✅ Вбудована | ❌ Немає |
| Продуктивність | ✅ Віртуалізація | ✅ Віртуалізація |
| Складність | ⚠️ Середня | ✅ Проста |
| Кастомізація | ⚠️ Обмежена | ✅ Повна свобода |
| Використання | Редагування даних | Тільки перегляд |
Коли використовувати ListView+GridView:
Коли використовувати DataGrid:
Drag and Drop дозволяє користувачам перетягувати елементи між контролами або всередині одного контролу.
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="700" Height="400">
<Grid Margin="20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0"
BorderBrush="#d1d5db"
BorderThickness="1"
CornerRadius="8"
Padding="10">
<StackPanel>
<TextBlock Text="📋 Доступні завдання"
FontWeight="Bold"
Margin="0,0,0,10" />
<ListBox Height="300">
<ListBoxItem Content="Завдання 1" />
<ListBoxItem Content="Завдання 2" />
<ListBoxItem Content="Завдання 3" />
<ListBoxItem Content="Завдання 4" />
</ListBox>
</StackPanel>
</Border>
<TextBlock Grid.Column="1"
Text="→"
FontSize="24"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="#6b7280" />
<Border Grid.Column="2"
BorderBrush="#d1d5db"
BorderThickness="1"
CornerRadius="8"
Padding="10">
<StackPanel>
<TextBlock Text="✅ Виконані завдання"
FontWeight="Bold"
Margin="0,0,0,10" />
<ListBox Height="300"
Background="#f9fafb">
<!-- Перетягніть завдання сюди -->
</ListBox>
</StackPanel>
</Border>
</Grid>
</Window>
Реалізація у code-behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var sourceListBox = this.FindControl<ListBox>("SourceListBox");
var targetListBox = this.FindControl<ListBox>("TargetListBox");
// Налаштування джерела (звідки перетягуємо)
sourceListBox.PointerPressed += (s, e) =>
{
if (e.GetCurrentPoint(sourceListBox).Properties.IsLeftButtonPressed)
{
var item = sourceListBox.SelectedItem;
if (item != null)
{
var data = new DataObject();
data.Set("TaskItem", item);
DragDrop.DoDragDrop(e, data, DragDropEffects.Move);
}
}
};
// Налаштування цілі (куди перетягуємо)
targetListBox.AddHandler(DragDrop.DropEvent, (s, e) =>
{
var data = e.Data.Get("TaskItem");
if (data != null)
{
// Видалити з джерела
var sourceItems = (ObservableCollection<string>)sourceListBox.ItemsSource;
sourceItems.Remove(data.ToString());
// Додати до цілі
var targetItems = (ObservableCollection<string>)targetListBox.ItemsSource;
targetItems.Add(data.ToString());
}
});
targetListBox.AddHandler(DragDrop.DragOverEvent, (s, e) =>
{
// Дозволити drop
e.DragEffects = DragDropEffects.Move;
});
}
}
Для MVVM-підходу використовуйте Attached Behaviors або Commands:
public class DragDropBehavior
{
public static readonly DependencyProperty IsDragSourceProperty =
DependencyProperty.RegisterAttached(
"IsDragSource",
typeof(bool),
typeof(DragDropBehavior),
new PropertyMetadata(false, OnIsDragSourceChanged));
public static bool GetIsDragSource(DependencyObject obj)
{
return (bool)obj.GetValue(IsDragSourceProperty);
}
public static void SetIsDragSource(DependencyObject obj, bool value)
{
obj.SetValue(IsDragSourceProperty, value);
}
private static void OnIsDragSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ListBox listBox && (bool)e.NewValue)
{
listBox.PointerPressed += ListBox_PointerPressed;
}
}
private static void ListBox_PointerPressed(object sender, PointerPressedEventArgs e)
{
var listBox = sender as ListBox;
var item = listBox?.SelectedItem;
if (item != null)
{
var data = new DataObject();
data.Set("Item", item);
DragDrop.DoDragDrop(e, data, DragDropEffects.Move);
}
}
}
Використання в XAML:
<ListBox ItemsSource="{Binding AvailableTasks}"
local:DragDropBehavior.IsDragSource="True" />
Для кращого UX додайте візуальний feedback:
// Зміна курсору
targetListBox.AddHandler(DragDrop.DragOverEvent, (s, e) =>
{
if (e.Data.Contains("TaskItem"))
{
e.DragEffects = DragDropEffects.Move;
// Курсор автоматично змінюється на "move"
}
else
{
e.DragEffects = DragDropEffects.None;
// Курсор "заборонено"
}
});
// Підсвічування цільового контролу
targetListBox.AddHandler(DragDrop.DragEnterEvent, (s, e) =>
{
targetListBox.Background = new SolidColorBrush(Color.FromRgb(229, 231, 235));
});
targetListBox.AddHandler(DragDrop.DragLeaveEvent, (s, e) =>
{
targetListBox.Background = Brushes.White;
});
Для студентів, які тільки опановують програмування, важливо зрозуміти концепцію рекурсії, яка лежить в основі роботи з деревами:
Рекурсія — це коли функція викликає саму себе. Це ідеальний підхід для роботи з деревоподібними структурами:
// Рекурсивний підрахунок всіх файлів у дереві
public int CountAllFiles(FileSystemItem item)
{
int count = 0;
// Базовий випадок: якщо це файл, повертаємо 1
if (item.Type != FileSystemItemType.Folder)
return 1;
// Рекурсивний випадок: підраховуємо файли у всіх дочірніх елементах
foreach (var child in item.Children)
{
count += CountAllFiles(child); // Виклик самої себе!
}
return count;
}
// Рекурсивний пошук файлу по імені
public FileSystemItem FindFile(FileSystemItem root, string fileName)
{
// Базовий випадок: знайшли файл
if (root.Name == fileName)
return root;
// Рекурсивний випадок: шукаємо у дочірніх елементах
foreach (var child in root.Children)
{
var result = FindFile(child, fileName);
if (result != null)
return result;
}
return null; // Не знайдено
}
Ключові концепції рекурсії:
StackOverflowExceptionМета: Навчитися створювати ієрархічні структури даних та відображати їх через TreeView.
Завдання: Створіть простий файловий браузер, що показує структуру папок.
Вимоги:
FolderItem з властивостями: Name (string), Path (string), Children (ObservableCollectionHierarchicalDataTemplate для відображенняIsExpanded binding для збереження стану розгортанняSelectedItem binding для показу шляху виділеного елементаПідказка:
var root = new FolderItem
{
Name = "C:\\",
Children = new ObservableCollection<FolderItem>
{
new FolderItem
{
Name = "Program Files",
Children = new ObservableCollection<FolderItem>
{
new FolderItem { Name = "App1" },
new FolderItem { Name = "App2" }
}
}
}
};
Мета: Опанувати створення табличного вигляду через ListView з кастомним DataTemplate.
Завдання: Створіть адресну книгу з табличним відображенням контактів.
Вимоги:
Contact з властивостями:
Name (string)Email (string)Phone (string)Company (string)IsFavorite (bool)ListBox з DataTemplate для імітації колонокДодаткові виклики:
Мета: Реалізувати складну взаємодію з Drag and Drop між різними контролами.
Завдання: Створіть систему управління проєктами з можливістю перетягування завдань.
Вимоги:
TreeView з проєктами та їх категоріями (ієрархія)ListBox з завданнями поточної категоріїСтруктура даних:
public class Project
{
public string Name { get; set; }
public ObservableCollection<Category> Categories { get; set; }
}
public class Category
{
public string Name { get; set; }
public ObservableCollection<Task> Tasks { get; set; }
}
public class Task
{
public string Title { get; set; }
public string Description { get; set; }
public TaskPriority Priority { get; set; }
public bool IsCompleted { get; set; }
}
Додаткові виклики:
У цій статті ми детально розібрали роботу з ієрархічними та табличними контролами:
TreeView:
TreeViewItem для ручного визначення структуриHierarchicalDataTemplate для прив'язки до данихItemsSource вказує на колекцію дочірніх елементівIsExpanded для контролю згортання/розгортанняSelectedItem для роботи з виділеннямItemContainerStyleListView + GridView:
DataGrid для перегляду данихDataTemplateDataGrid: простіший, але без редагуванняDrag and Drop:
DragDrop.DoDragDrop() для початку перетягуванняDragEnter, DragOver, Drop події для обробкиDragEffects для контролю типу операції (Move, Copy, None)Рекурсія:
Наступні кроки: У наступній статті ми розглянемо систему меню, панелей інструментів та контекстних меню для створення повноцінного інтерфейсу застосунку.
DataGrid — сортування, фільтрація, редагування
Просунуті можливості DataGrid для роботи з великими наборами даних — сортування, фільтрація, групування, inline-редагування та валідація
Меню, Toolbar, ContextMenu, StatusBar
Побудова повної системи меню та панелей інструментів для професійних desktop-застосунків