AvaloniaUI TreeViewの使い方(発展編)
下記の続き
やりたいこと
winformsのTreeViewを代替する機能を作りたい。
Nodeにアイコンを追加し、Node Object側からアイコンを変更できるようにしたい。
各種イベント/プロパティの処理をNode Objectに追加したい。
使用環境
Windows 11 : Home 22H2 + Visual Studio Community 2022(64bit)
Linuxテスト環境 : 上記Windows上のWSL2 Ubuntsu 22.04.2 LTS
Visual Studio2022, AvaloniaUI 11.0.2
TreeNode実装
TreeNodeクラスは下記のような実装にする
ImageアイコンとText文字列を持ち、Select/Expand/Collapse/Click/DoubleClickなどの動作に応じて各methodが呼ばれるようにする。双方向Bindingによって、Propertyの変更がViewに反映されるようにする。
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace AvaloniaExample.Models
{
internal class TreeNode : INotifyPropertyChanged
{
public TreeNode(string text)
{
Text = text;
}
// 初期アイコン (Assets内のpngファイル)
static Bitmap defaultBitmap = new Bitmap(AssetLoader.Open(new Uri("avares://AvaloniaExample/Assets/paper.png")));
// アイコン画像
private IImage bitmap = defaultBitmap;
public IImage Image
{
get
{
return bitmap;
}
set
{
bitmap = value;
}
}
// ノード展開状態
private bool _IsExpanded = false;
public bool IsExpanded
{
get { return _IsExpanded; }
set
{
bool prev = _IsExpanded;
_IsExpanded = value;
NotifyPropertyChanged();
if (!prev & _IsExpanded)
{
OnExpand();
}
if (prev & !_IsExpanded)
{
OnCollapse();
}
}
}
// ノード展開時に呼ばれる
public virtual void OnExpand()
{
Text = "Expanded";
}
// ノードを閉じたときに呼ばれる
public virtual void OnCollapse()
{
Text = "Collapsed";
}
// ノードが選択されたときに呼ばれる
public virtual void OnSelected()
{
Text = "Selected";
}
// ノードがクリックされたときに呼ばれる
public virtual void OnClicked()
{
Text = "Clicked";
}
// ノードがダブルクリックされたときに呼ばれる
public virtual void OnDoubleClicked()
{
Text = "DoubleClicked";
}
// ノードテキスト
private string _Text = "";
public string Text
{
get { return _Text; }
set { _Text = value; NotifyPropertyChanged(); }
}
// 子ノード
private ObservableCollection<TreeNode> nodes = new ObservableCollection<TreeNode>();
public ObservableCollection<TreeNode> Nodes
{
get { return nodes; }
set { nodes = value; NotifyPropertyChanged(); }
}
// 双方向BIndingのためのViewModelへのProperty変更通知
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
ViewModel実装
ここではRoot Nodesの実装と、ノードダミーデータの初期化のみ。
using AvaloniaExample.Models;
using System.Collections.ObjectModel;
namespace AvaloniaExample.ViewModels
{
internal class TreeViewModel : ViewModelBase
{
// TreeViewのRoot Nodes
public ObservableCollection<TreeNode> Nodes { get; } = new ObservableCollection<TreeNode>();
public TreeViewModel()
{
// Nodeの追加
Models.TreeNode node = new TreeNode("AAA");
Nodes.Add(node);
Models.TreeNode node2 = new TreeNode("BBB");
node.Nodes.Add(node2);
Models.TreeNode node3 = new TreeNode("CCC");
node.Nodes.Add(node3);
}
}
}
axaml実装
Tapped,DoubleTapped,SelectionChangedのイベント実装とIsExpandedプロパティのBininding実装。TreeViewのItemはStackedPanelでImage(アイコン)とTextBlockを左右に並べたものにする。
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:AvaloniaExample.ViewModels;assembly=AvaloniaExample"
xmlns:views="clr-namespace:AvaloniaExample.Views;assembly=AvaloniaExample"
xmlns:models="clr-namespace:AvaloniaExample.Models;assembly=AvaloniaExample"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="450"
x:Class="AvaloniaExample.Views.TreeControl"
>
<Design.DataContext>
<vm:MainViewModel />
</Design.DataContext>
<TreeView
Name="Tree"
x:DataType="vm:TreeViewModel"
ItemsSource="{Binding Nodes}"
Tapped="TreeView_Tapped"
DoubleTapped="TreeView_DoubleTapped"
SelectionChanged="TreeView_SelectionChanged"
>
<!-- Tap,DoubleTap,SelectionChangedイベントの割り当て -->
<TreeView.ItemTemplate>
<TreeDataTemplate
x:DataType="models:TreeNode"
ItemsSource="{Binding Nodes}"
>
<!-- ItemはImageとTextBlockを横に並べた構造にする -->
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Image}" Width="16" Height="16"/>
<TextBlock Text="{Binding Text}" Margin="8 0 0 0" />
</StackPanel>
</TreeDataTemplate>
</TreeView.ItemTemplate>
<!-- IsExpanded Propertyを更新する -->
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded,Mode=TwoWay}"/>
</Style>
</TreeView.Styles>
</TreeView>
</UserControl>
※ Solution作成時にCompiled Binindingをtrueにしていると、現状のAvaloniaの問題でIsExpand PropertyのStyle設定部でエラーが発生する。(TreeNodeではなくTreeViewModelにBindしようとする。) 現状ではCompiled Bindingは外しておくべきなよう。
TreeContolのコード
ハンドラで取得できるオブジェクトからgetTreeNodeでTreeNodeを取得している。
オブジェクトの親オブジェクトを探索してTreeViewItemを探し、そのDataContextからTreeNodeを取得している。
using Avalonia.Controls;
using AvaloniaExample.Models;
namespace AvaloniaExample.Views
{
public partial class TreeControl : UserControl
{
public TreeControl()
{
InitializeComponent();
// Binding先の設定
Tree.DataContext = new ViewModels.TreeControlModel();
}
// ノードクリック時の処理
private void TreeView_Tapped(object? sender, Avalonia.Input.TappedEventArgs e)
{
TreeNode node = getTreeNode(e.Source);
if (node == null) return;
node.OnClicked();
}
// ノード選択時の処理
private void TreeView_SelectionChanged(object? sender, Avalonia.Controls.SelectionChangedEventArgs e)
{
TreeNode node = getTreeNode(Tree.SelectedItem);
if (node == null) return;
node.OnSelected();
}
// ノードダブルクリック時の処理
private void TreeView_DoubleTapped(object? sender, Avalonia.Input.TappedEventArgs e)
{
TreeNode node = getTreeNode(e.Source);
if (node == null) return;
node.OnDoubleClicked();
}
// 取得したオブジェクトからTreeNodeを探しにいく
// TextBlock/StackPanelのParentからTreeViewItemを探し、
// TreeViewItemのDataContextからTreeNodeを取得する
private TreeNode getTreeNode(object? target)
{
if (target == null) return null;
if (target is TreeNode)
{
return target as TreeNode;
}
if (target is TextBlock)
{
TextBlock textBlock = target as TextBlock;
return getTreeNode(textBlock.Parent);
}
else if (target is StackPanel)
{
StackPanel stackPanel = target as StackPanel;
return getTreeNode(stackPanel.Parent);
}
else if (target is TreeViewItem)
{
TreeViewItem treeViewItem = target as TreeViewItem;
return treeViewItem.DataContext as TreeNode;
}
else
{
return null;
}
}
}
}
さらに以下に続く
参考
WPFでのtreeVieewのBind方法
プロパティ変更通知の方法
Discussion