🐥

AvaloniaUI TreeViewの使い方(発展編2)

2024/03/06に公開

やりたいこと

下記の続き
https://zenn.dev/ajkfds/articles/bdbc6e074b4f52
AvaloniaUIを使ってwinformsのtreeviewの代替するControlを使いたい
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という名称でプロパティ定義している。

TreeNode.cs
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のために持つ。

TreeControlViewModel.cs
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のイベントハンドラ定義。

TreeControl.axaml
<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オブジェクトのメソッドを呼びに行く構成。

TreeControl.axaml.cs
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