跳至內容
Bluebean 的筆記
Go back

來翻新老專案 (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 的程式內新增套用篩選的程式:

public class WzTreeNode : INotifyPropertyChanged
{
    /* ... */
    private bool _isVisible = true;
    public bool IsVisible {
        get => _isVisible;
        set {
            _isVisible = value;
            OnPropertyChanged();
        }
    }
    /* ... */
}
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 綁定

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

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

    /* ... */
}
<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 轉換成 Visible , false 轉換成 Hidden,但是 Hidden 只是把 UI 隱藏,但依然會佔用空間 所以這次的目標是將 false 轉成 Collapsed ,它可以讓 UI 不佔位,使被篩選的 TreeViewItem 直接消失

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

[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

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

最後再將 IsVisible 套用到 TreeViewItem 即可

<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 等大語言模型的幫助,多少能降低一些學習門檻
儘管大語言模型的輸出並非每次都很可靠,但也提供不錯的靈感去尋找資源或實作想要的效果

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


分享文章:

Previous
來翻新老專案 (6) - MapleLooker - TreeNode ContextMenu 重構
Next
來聊點 AI