AvaloniaUI TreeViewの使い方(発展編2)
やりたいこと
下記の続き
MVVMとしては正しくないかもしれないが、とりあえずwinformsのコードを流用できるように
同じような挙動をする実装をしたい。
使用環境
Windows 11 : Home 22H2 + Visual Studio Community 2022(64bit)
Linuxテスト環境 : 上記Windows上のWSL2 Ubuntsu 22.04.2 LTS
Visual Studio2022, AvaloniaUI 11.0.2
使い方
axamlに以下を記載し、
xmlns:controls="clr-namespace:AjkAvaloniaLibs.Contorls;assembly=AjkAvaloniaLibs"
...
<controls:TreeControl Name="Tree1"/>
下記のようなコードを実装すると
TreeNode node1 = new TreeNode("node1");
TreeNode node2 = new TreeNode("node2");
TreeNode node3 = new TreeNode("node3");
TreeNode node4 = new TreeNode("node4");
Tree1.Nodes.Add(node1);
Tree1.Nodes.Add(node2);
node1.Nodes.Add(node3);
node3.Nodes.Add(node4);
このような動作になる。
winformsのTreeViewと同じようにNodesに対するAddでNode追加できる実装にしている。
各nodeはParentプロパティを持ち、親ノード情報を保持している。
TreeNode実装
TreeNodeを下記に定義する。このTreeNodeを継承していろいろなnodeを定義する想定。
親ノード情報を保持するためには単純に子ノード情報をObservableCollection<TreeNode>で持つことができない。そのためNodesを別のクラスで保持する機構にする。
NodesはTreeNodesクラスで定義し、AddしたときのParentの設定をここで行っている。
子ノードのBindingのためにReadOnlyObservableCollection<TreeNode>を_nodesという名称でプロパティ定義している。
using AjkAvaloniaLibs.Libs;
using Avalonia.Media;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace AjkAvaloniaLibs.Contorls
{
public class TreeNode : INotifyPropertyChanged
{
public TreeNode()
{
Nodes = new TreeNodes(this);
}
public TreeNode(string text) : this()
{
Text = text;
}
// 子ノード
public TreeNodes Nodes;
// アイコン画像
private IImage? bitmap = null;
public virtual 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();
}
}
}
// 親ノード WeakReferenceで保持する
private System.WeakReference<TreeNode>? parent;
public TreeNode? Parent
{
get
{
TreeNode ret;
if (parent == null) return null;
if (!parent.TryGetTarget(out ret)) return null;
return ret;
}
set
{
if (value == null)
{
parent = null;
}
else
{
parent = new WeakReference<TreeNode>(value);
}
}
}
// ノード展開時に呼ばれる
public virtual void OnExpand() { }
// ノードを閉じたときに呼ばれる
public virtual void OnCollapse() { }
// ノードが選択されたときに呼ばれる
public virtual void OnSelected() { }
// ノードがクリックされたときに呼ばれる
public virtual void OnClicked() { }
// ノードがダブルクリックされたときに呼ばれる
public virtual void OnDoubleClicked() { }
// ノードテキスト
private string _Text = "";
public virtual string Text
{
get { return _Text; }
set { _Text = value; NotifyPropertyChanged(); }
}
// Viewからの参照のために保持する
public ReadOnlyObservableCollection<TreeNode> _nodes
{
get { return Nodes.ReadOnlyNodes; }
}
// 双方向BIndingのためのViewModelへのProperty変更通知
public event PropertyChangedEventHandler? PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
// 子ノードを保持するクラス
// Parenetを保持するためにクラスを実装するs
public class TreeNodes : IEnumerable<TreeNode>
{
private ObservableCollection<TreeNode> nodes;
private ReadOnlyObservableCollection<TreeNode> rNodes;
private TreeNode? parent;
public TreeNodes()
{
nodes = new ObservableCollection<TreeNode>();
rNodes = new ReadOnlyObservableCollection<TreeNode>(nodes);
}
public TreeNodes(TreeNode? parent) : this()
{
this.parent = parent;
}
public ReadOnlyObservableCollection<TreeNode> ReadOnlyNodes
{
get { return rNodes; }
}
// Parentを保持する
public void Add(TreeNode treeNode)
{
nodes.Add(treeNode);
treeNode.Parent = parent;
}
public void Remove(TreeNode treeNode)
{
nodes.Remove(treeNode);
treeNode.Parent = null;
}
public IEnumerator<TreeNode> GetEnumerator()
{
return ((IEnumerable<TreeNode>)nodes).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)nodes).GetEnumerator();
}
}
}
}
TreeControlのViewModel
Root Nodeを定義している。TreeNodeと同じく_nodesプロパティをBindingのために持つ。
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using static AjkAvaloniaLibs.Contorls.TreeNode;
namespace AjkAvaloniaLibs.Contorls
{
public class TreeControlViewModel
{
public TreeControlViewModel()
{
Nodes = new TreeNodes(null);
}
// 子ノード
public TreeNodes Nodes;
public ReadOnlyObservableCollection<TreeNode> _nodes
{
get { return Nodes.ReadOnlyNodes; }
}
}
}
TreeControlのaxaml定義。
_nodes,IsExpandedをbinding。
Tapped,DoubleTapped,SelectionChangedのイベントハンドラ定義。
<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:ctrl="clr-namespace:AjkAvaloniaLibs.Contorls;assembly=AjkAvaloniaLibs"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="450"
x:Class="AjkAvaloniaLibs.Contorls.TreeControl">
<TreeView
Name="TreeView"
x:DataType="ctrl:TreeControlViewModel"
ItemsSource="{Binding _nodes}"
Tapped="TreeView_Tapped"
DoubleTapped="TreeView_DoubleTapped"
SelectionChanged="TreeView_SelectionChanged"
>
<TreeView.ItemTemplate>
<TreeDataTemplate
x:DataType="ctrl:TreeNode"
ItemsSource="{Binding _nodes}"
>
<!-- ItemはImageとTextBlockを横に並べた構造にする -->
<StackPanel Orientation="Horizontal" Height ="16">
<Image Source="{Binding Image}" Width="16" Height="16"/>
<TextBlock Text="{Binding Text}" Margin="4 0 0 0" FontSize="16" />
</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>
各種イベントの処理。
イベント発生時にTreeNodeオブジェクトのメソッドを呼びに行く構成。
using Avalonia.Controls;
using Avalonia.Controls.Generators;
namespace AjkAvaloniaLibs.Contorls
{
public partial class TreeControl : UserControl
{
public TreeControl()
{
InitializeComponent();
// DataContextのViewModelの設定
TreeView.DataContext = new TreeControlViewModel();
}
public TreeNode.TreeNodes Nodes
{
get
{
TreeControlViewModel? viewModel = TreeView.DataContext as TreeControlViewModel;
if (viewModel == null) throw new System.Exception();
return viewModel.Nodes;
}
}
// クリックハンドラ
private void TreeView_Tapped(object? sender, Avalonia.Input.TappedEventArgs e)
{
TreeNode? node = getTreeNode(e.Source);
if (node == null)
{
System.Diagnostics.Debug.Print(e.ToString());
}
if (node == null) return;
node.OnClicked();
}
// 選択アイテム変更ハンドラ
private void TreeView_SelectionChanged(object? sender, Avalonia.Controls.SelectionChangedEventArgs e)
{
TreeNode? node = getTreeNode(TreeView.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を取得する
private TreeNode? getTreeNode(object? target)
{
if (target == null) return null;
if (target is TreeNode)
{
return target as TreeNode;
}
if (target is TextBlock)
{ // targetがTextBockの場合は親objectを探索する
TextBlock? textBlock = target as TextBlock;
if (textBlock == null) return null;
return getTreeNode(textBlock.Parent);
}
else if (target is StackPanel)
{ // targetがStackPanelの場合は親objectを探索する
StackPanel? stackPanel = target as StackPanel;
if (stackPanel == null) return null;
return getTreeNode(stackPanel.Parent);
}
else if (target is TreeViewItem)
{ // targetがTreeViewItemの場合はBindされているTreeNodeを取得する
TreeViewItem? treeViewItem = target as TreeViewItem;
if (treeViewItem == null) return null;
return treeViewItem.DataContext as TreeNode;
}
else
{ // ほしいobjectが見つからなかった場合
return null;
}
}
}
}
Discussion