來翻新老專案 (7) - MapleLooker - TreeView 查詢

從第一篇文章開始到現在,已經過了三個半月
MapleLooker 從最初的 .NET Framework 4.8 + WinForm 變成 .NET 8.0 + WPF
透過這個專案,學到一些 WPF 的皮毛,現在 MapleLooker 已經沒什麼東西可以翻新了
因此這篇將會是 MapleLooker 翻新的最後一篇文章 🙂

這次主要是實現針對 TreeView 節點的查詢功能
在原始 WinForm 版本中,這個功能並沒有實現,這次特別將它做出來,為這個專案劃下一個句點

TreeView 篩選的主要邏輯

這次的 TreeView 篩選是一個相當簡單的功能
不知道為什麼以前沒去實做這個功能

這次主要會透過 Visibility 這個屬性去過濾 TreeViewItem
由於 TreeViewItem 元件的組成相當複雜,因此篩選主要針對 Model 而不是 TreeViewItem 本身

一開始先為 TreeNode 建立 IsVisible 選項,並在 MainForm 的程式內新增套用篩選的程式:

WzTreeNode.cs
public class WzTreeNode : INotifyPropertyChanged
{
/* ... */
private bool _isVisible = true;
public bool IsVisible {
get => _isVisible;
set {
_isVisible = value;
OnPropertyChanged();
}
}
/* ... */
}

MainForm.xaml.cs
public partial class MainForm : WpfUi.FluentWindow, INotifyPropertyChanged
{
/* ... */

private void ApplyFilter(string filterText) {
foreach (WzTreeNode node in RootNodes) {
this.ApplyFilterForTreeNode(node, filterText);
}
}

private bool ApplyFilterForTreeNode(WzTreeNode node, string filterText, bool parentVisible = false) {
bool visible = parentVisible || string.IsNullOrEmpty(filterText) || node.Name.Contains(filterText);
bool finalVisible = visible;
foreach (var child in node.Children) {
bool childVisible = this.ApplyFilterForTreeNode(child, filterText, visible);
finalVisible = finalVisible || childVisible;
}
node.IsVisible = finalVisible;
return finalVisible;
}

/* ... */
}

邏輯其實很單純,當我要尋找的節點與文字不匹配的時候,就把它隱藏起來
比較特別的是,我要找的節點如果在深處,那他的父節點也要顯示
而要尋找的節點底下還有子節點,則他底下所有的子節點全部都要顯示

當輸入為空字串時,則全部節點的 IsVisible 都會是 true

綁定 UI 並呼叫 ApplyFilter

方法有了,再來就是要綁定 UI 並觸發 ApplyFilter 方法
先建立 FilterText 屬性,然後再跟之前已經建立的 TextBox 做 TwoWay 綁定

MainForm.xaml.cs
public partial class MainForm : WpfUi.FluentWindow, INotifyPropertyChanged
{
/* ... */

private string _filterText = "";
public string FilterText {
get => _filterText;
set {
this._filterText = value;
OnPropertyChanged();
this.ApplyFilter(this._filterText);
}
}

/* ... */
}
MainForm.xaml
<ui:AutoSuggestBox
Grid.Row="1"
Margin="10,10,10,0"
PlaceholderText="搜尋..."
Text="{Binding FilterText, ElementName=_parent, Mode=TwoWay}"/>

當 AutoSuggestBox 輸入之後,會改變 FilterText
改變當下便會呼叫 ApplyFilter 對 TreeView 進行隱藏

IsVisible 寫回 TreeViewItem

現在輸入文字後會控制 TreeNode 的顯示狀態
最後只要再建立 Convertor 將 IsVisible 轉成 Visibility,並設定到 TreeViewItem 即可

這邊我直接沿用之前寫好的 BoolVisibilityConvertor
之前的邏輯是 true 轉換成 Visiblefalse 轉換成 Hidden,但是 Hidden 只是把 UI 隱藏,但依然會佔用空間
所以這次的目標是將 false 轉成 Collapsed ,它可以讓 UI 不佔位,使被篩選的 TreeViewItem 直接消失

我為 BoolVisibilityConvertor 新增一個 Collapsed 屬性來決定這個 Convertor 在 false 的情況該如何轉換 Visibility

BoolVisibilityConvertor.cs
[ValueConversion(typeof(bool), typeof(Visibility))]
public class BoolVisibilityConvertor : IValueConverter
{
public bool Collapsed { get; init; } = false;

public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
bool b = (bool)value;
return b ? Visibility.Visible : (Collapsed ? Visibility.Collapsed : Visibility.Hidden);
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}

這樣的寫法可以避免重複建立類似功能的 Convertor,並且能夠依照需求選擇該如何建立 Convertor
我只要在 Resource 建立時指定 Collapsed 即可建立將 false 轉換成 Collapsed 的 Convertor

MainForm.xaml
<ui:FluentWindow.Resources>
<convert:BoolVisibilityConvertor x:Key="boolVisibility" Collapsed="True"/>
</ui:FluentWindow.Resources>

最後再將 IsVisible 套用到 TreeViewItem 即可

MainForm.xaml
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem" BasedOn="{StaticResource DefaultTreeViewItemStyle}">
<Setter Property="Visibility" Value="{Binding IsVisible, Converter={StaticResource boolVisibility}, Mode=TwoWay}"/>
</Style>
</TreeView.ItemContainerStyle>

篩選成果

最後的成品如下:

maple-looker-tree-view-filter-20250429.gif

其他優化

除了 TreeView 篩選以外,這次還有對 UI 稍微做一點點小優化

Image 節點文字會根據解析狀態變化

由於 Image 節點我是採用 LazyLoading 的方式來載入子節點
為了能夠更清楚的知道哪些解析過哪些還沒解析
所以我就把沒有解析的節點設定為灰色,讓我能夠一眼辨識

maple-looker-image-node-text-color-hint-20250429.png

主題切換

專案本身因為個人偏好,預設是深色主題
但考慮到並非每個人都會用深色主題,所以做了一個開關來切換淺色主題跟深色主題
算是一個非常簡單的功能

maple-looker-light-theme-switch-20250429.png

結語

本來翻新 MapleLooker 這個專案主要是想寫一些程式重構的東西
但寫到後面才發現,這個專案的程式好像都跟 UI 有關 🙃
最後整個系列就變得像 WPF 學習紀錄了

會想要在這一篇結束 MapleLooker 這個系列,主要是因為大部分的功能以及介面都已經翻新完成
雖然不是每個部份都寫得很完美,但該有的大多都有了,也運作得很好
如果再繼續往下去寫,那就不是翻新了,而是繼續堆疊新功能,而且我也想不到該翻新什麼了
所以做完篩選框之後,就打算結束這個系列

WPF 我覺得是一個蠻有挑戰性的東西,它跟 WinForm 相比完全是不同層級的東西
前面需要學習的概念還蠻多的,但是學會以後,就能夠快速建立好看的 Desktop App
現在有 ChatGPT、Claude 等大語言模型的幫助,多少能降低一些學習門檻
儘管大語言模型的輸出並非每次都很可靠,但也提供不錯的靈感去尋找資源或實作想要的效果

不知道怎麼搞的,文章的產出時間漸漸變成以兩週為一個週期
後面也是盡可能在第一個週末投入時間想方向跟寫程式,並在第二個週末撰寫文章
雖然翻新系列到這篇就結束了,但我還是會繼續想一些題材來寫,紀錄一些個人思維與看法,看能不能提昇文字方面的表達能力