循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(7) -- 图标列表展示和选择处理

发布时间 2023-10-13 18:11:30作者: 伍华聪

我们在WPF应用端的界面中,使用lepoco/wpfui 来做主要的入口框架,这个项目它的菜单内置了不少图标,我们需要在动态菜单的配置中,使用它作为图标的展示处理,本篇随笔介绍如何基于图标枚举集合进行图标的展示和选择处理。并扩展到Font-Awesome-WPF的处理进行展示和选择。

1、lepoco/wpfui 项目的图标库

lepoco/wpfui 项目的图标库来源于Fluent System Icons,项目地址是:https://github.com/microsoft/fluentui-system-icons

这些图标映射到枚举对象 SymbolRegular 和 SymbolFilled,一个是常规的,一个是填充的图标,如下枚举对象所示。

 图标主要通过前面的名称来区分展示的,图标列表主要展示效果如下所示。

我们可以通过代码把这些枚举内容全部加载到列表中进行使用。

var iconList = EnumHelper.GetMemberKeyValue<SymbolRegular>();
foreach (var icon in iconList)
{
    this.AllItems.Add(new CListItem(icon.Key, icon.Value.ToString()));
}

我们为了处理这些图标内容,需要按照MVVM的设计模式,设计相关的视图模型和视图界面,由于图标比较多,测试一次性展示的时候太过耗时,因此把它们分页处理,实际运行的界面效果如下所示。

1)图标列表选择界面

 2)图标选择后的展示界面

我们一般在动态菜单设置页面中用到图标的选择处理,如下界面所示。

 为了有效的对图标进行分页展示,视图模型需要包含一些分页所需的对象信息,如下代码所示。

    /// <summary>
    /// SymbolRegular 的图标查询视图模型
    /// </summary>
    public partial class SymbolRegularListViewModel : BaseViewModel
    {
        /// <summary>
        /// 选择的图表项目
        /// </summary>
        [ObservableProperty]
        private CListItem selectedItem = new();

        /// <summary>
        /// 符合条件的图标列表
        /// </summary>
        [ObservableProperty]
        private List<CListItem> iconItems = new();

        /// <summary>
        /// 所有图标
        /// </summary>
        [ObservableProperty]
        private List<CListItem> allItems = new();

        /// <summary>
        /// 分页对象
        /// </summary>
        [ObservableProperty]
        private PagingData pagerInfo = new PagingData() { CurrentPageIndex = 1, PageSize =60 };

由于图标不用访问数据库,因此在枚举读取初始化后存储所有的图标集合,并以集合为基础进行图标名称的检索及排序处理。

在查询处理的时候,我们需要把分页的页码和页面大小等信息转换为记录数的跳转及获取变量,如下所示。

/// <summary>
/// 转换下分页信息,为查询对象的属性
/// </summary>
protected virtual void ConvertPagingInfo()
{
    //根据传入的分页信息构建查询记录数和位置
    this.SkipCount = (this.PagerInfo.CurrentPageIndex - 1) * this.PagerInfo.PageSize;
    this.MaxResultCount = this.PagerInfo.PageSize;
}

查询处理的代码如下所示(在视图模型上处理代码)

/// <summary>
/// 触发查询处理命令
/// </summary>
/// <returns></returns>
[RelayCommand]
public virtual void Search()
{
    //切换第一页
    this.PagerInfo.CurrentPageIndex = 1;
    //查询更新
    GetData();
}

/// <summary>
/// 根据分页和查询条件查询,请求数据
/// </summary>
/// <returns></returns>
public virtual void GetData()
{
    //转换下分页信息
    ConvertPagingInfo();
    if (this.Filter.IsNullOrEmpty())
    {
        this.IconItems = AllItems.Skip(this.SkipCount).Take(this.MaxResultCount).ToList();
        this.PagerInfo.RecordCount = this.AllItems.Count;
    }
    else
    {
        this.IconItems = AllItems.Where(s => s.Text.Contains(this.Filter, StringComparison.OrdinalIgnoreCase)).Skip(this.SkipCount).Take(this.MaxResultCount).ToList();
        this.PagerInfo.RecordCount = AllItems.Where(s => s.Text.Contains(this.Filter, StringComparison.OrdinalIgnoreCase)).Count();
    }
}

选择图标的列表展示界面,遵循MVVM的视图界面代码规范,标准化处理即可,对查询搜索框的处理响应进行查询处理。

/// <summary>
/// SymbolRegularSelectPage.xaml 交互逻辑
/// </summary>
public partial class SymbolRegularSelectPage : INavigableView<SymbolRegularListViewModel>
{
    /// <summary>
    /// 视图模型对象
    /// </summary>
    public SymbolRegularListViewModel ViewModel { get; }  

    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="viewModel">视图模型对象</param>
    public SymbolRegularSelectPage(SymbolRegularListViewModel viewModel)
    {
        ViewModel = viewModel;
        DataContext = this;
        InitializeComponent();
    }

    /// <summary>
    /// 过滤查询事件
    /// </summary>
    private void SearchBar_OnSearchStarted(object sender, HandyControl.Data.FunctionEventArgs<string> e)
    {
        this.ViewModel.Search();
    }

2、对图标使用ItemsControl控件进行控制输出

图标是一个集合对象,因此我们如果需要按照我们格式进行展示,可以使用ItemsControl来进行处理。

ItemsControl可以通过控制 ItemsControl.ItemsPanel 的ItemsPanelTemplate模板进行控制布局面板,可以通过控制 ItemsControl.Template 的ControlTemplate来控制输出格式的总模板,可以通过控制 ItemsControl.ItemTemplate的DataTemplate来控制输出每个项目的模板,这个控件ItemsControl提供了很灵活的控制模板处理。如下XAML界面代码所示。

<ItemsControl
    x:Name="chkIcons"
    Height="580"
    HorizontalContentAlignment="Left"
    ItemsSource="{Binding ViewModel.IconItems}"
    ScrollViewer.CanContentScroll="True"
    VirtualizingStackPanel.IsVirtualizing="true"
    VirtualizingStackPanel.VirtualizationMode="Standard">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <UniformGrid
                HorizontalAlignment="Left"
                VerticalAlignment="Top"
                Columns="10" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button
                Width="80"
                Height="80"
                Margin="8,8"
                Padding="0"
                HorizontalAlignment="Stretch"
                VerticalAlignment="Stretch"
                Click="Button_Click"
                FontSize="32"
                MouseDoubleClick="Button_MouseDoubleClick"
                Tag="{Binding}"
                ToolTip="{Binding Text, Mode=OneTime}"
                ToolTipService.InitialShowDelay="240">
                <ui:SymbolIcon
                    FontSize="48"
                    Foreground="CornflowerBlue"
                    Symbol="{Binding Text}"
                    Tag="{Binding}"
                    ToolTip="{Binding Text}" />
            </Button>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.Template>
        <ControlTemplate TargetType="ItemsControl">
            <ScrollViewer
                Width="Auto"
                CanContentScroll="True"
                VerticalScrollBarVisibility="Visible">
                <ItemsPresenter />
            </ScrollViewer>
        </ControlTemplate>
    </ItemsControl.Template>
</ItemsControl>

展示界面效果如下所示

当然这里面还有分页的界面代码,使用的是HandyControl的分页控件处理。

<hc:Pagination
    DataCountPerPage="{Binding ViewModel.PagerInfo.PageSize}"
    IsJumpEnabled="True"
    MaxPageCount="{Binding ViewModel.PagerInfo.MaxPageCount}"
    MaxPageInterval="5"
    PageIndex="{Binding ViewModel.PagerInfo.CurrentPageIndex, UpdateSourceTrigger=PropertyChanged}">
    <hc:Interaction.Triggers>
        <hc:EventTrigger EventName="PageUpdated">
            <hc:EventToCommand Command="{Binding ViewModel.PageUpdatedCommand}" PassEventArgsToCommand="True" />
        </hc:EventTrigger>
    </hc:Interaction.Triggers>
</hc:Pagination>

而对每项的选择,我们单击选中,双击选中并返回处理,代码逻辑如下所示。

private void Button_Click(object sender, RoutedEventArgs e)
{
    var button = (Button)sender;
    if (button != null)
    {
        if (button.Tag is CListItem item)
        {
            this.ViewModel.SelectedItem = item;
        }
    }
}

private void Button_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    var button = (Button)sender;
    if (button != null)
    {
        if (button.Tag is CListItem item)
        {
            this.ViewModel.SelectedItem = item;
            this.DialogResult = true;
        }
    }
}

而选择的信息展示,我们可以通过一个面板来组合展示相关图标名称和图标效果即可。

<WrapPanel
    Grid.Column="0"
    Margin="10,0"
    VerticalAlignment="Center"
    Orientation="Horizontal">
    <TextBlock
        HorizontalAlignment="Left"
        VerticalAlignment="Center"
        Text="当前选择:" />
    <ui:SymbolIcon
        FontSize="32"
        Foreground="CornflowerBlue"
        Symbol="{Binding ViewModel.SelectedItem.Text}"
        ToolTip="{Binding ViewModel.SelectedItem.Text}" />
    <TextBlock
        Margin="10,0"
        HorizontalAlignment="Left"
        VerticalAlignment="Center"
        Foreground="Blue"
        Text="{Binding ViewModel.SelectedItem.Text}" />
</WrapPanel>

确认选择后返回的内容展示,我们也是使用类似的方式处理界面的

红色框的界面XAML代码如下所示。

<!--  组合多个控件显示  -->
<hc:ElementGroup
    Width="350"
    Height="32"
    Margin="5"
    Layout="Stack"
    Orientation="Horizontal">
    <TextBox
        x:Name="txtIcon"
        Width="230"
        hc:TitleElement.Title="图标"
        hc:TitleElement.TitlePlacement="Left"
        IsReadOnly="True"
        Text="{Binding ViewModel.Item.Icon, UpdateSourceTrigger=PropertyChanged}" />
    <Border Padding="1,0" Style="{StaticResource BorderRegion}">
        <ui:SymbolIcon
            Width="50"
            FontSize="32"
            Symbol="{Binding ViewModel.Item.Icon, UpdateSourceTrigger=PropertyChanged}" />
    </Border>
    <Button
        Command="{Binding SelectIconCommand}"
        Content="选择图标"
        Style="{StaticResource ButtonPrimary}" />
</hc:ElementGroup>

后面就是存储处理,按照窗口界面弹出,并存储对象属性即可。

3、扩展到Font-Awesome-WPF的处理进行展示和选择

在WPF中使用Font-Awesome-WPF 图标组件的很多,它的项目地址:https://github.com/charri/Font-Awesome-WPF/blob/master/README-WPF.md

我们也可以用类似的方式来整合这个图标组件到项目中进行使用。

首先在项目的Nugget上添加安装FontAwesome.WPF组件。

 或者通过命令进行安装

PM> Install-Package FontAwesome.WPF

在使用的XAML中添加对应的命名空间。

xmlns:fa="http://schemas.fontawesome.io/icons/"

这个图标的组件使用比较简单如下代码所示。

<fa:FontAwesome Icon="Flag" />

和前面的图标组件处理类似,同样需要处理图标枚举到具体图标列表展示的处理过程,图标选择界面运行效果如下所示,由于图标不是很多,所以一次性加载了。

 我们先创建MVVM的视图模型对象,如下所示代码。

/// <summary>
/// FontAwesome.WPF的图标查询视图模型
/// </summary>
public partial class FontAwesomeListViewModel : BaseViewModel
{
    [ObservableProperty]
    private CListItem selectedItem = new();

    /// <summary>
    /// 符合条件的图标列表
    /// </summary>
    [ObservableProperty]
    private List<CListItem> iconItems = new();

    /// <summary>
    /// 所有图标
    /// </summary>
    [ObservableProperty]
    private List<CListItem> allItems = new();

    /// <summary>
    /// 构造函数
    /// </summary>
    public FontAwesomeListViewModel()
    {
        this.Title = "FontAwesome.WPF的图标";

        //FontAwesomeIcon.Book = 0xf02d
        var iconList = EnumHelper.GetMemberKeyValue<FontAwesomeIcon>();
        foreach (var icon in iconList)
        {
            this.AllItems.Add(new CListItem(icon.Key, icon.Value.ToString()));
        }

        ResetData();
    }

ItemsControl的对象展示类似,如下所示。

<ItemsControl
    x:Name="chkIcons"
    Height="900"
    HorizontalContentAlignment="Left"
    ItemsSource="{Binding ViewModel.IconItems}"
    ScrollViewer.CanContentScroll="True"
    VirtualizingStackPanel.IsVirtualizing="true"
    VirtualizingStackPanel.VirtualizationMode="Standard">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <UniformGrid
                HorizontalAlignment="Left"
                VerticalAlignment="Top"
                Columns="9" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button
                Width="80"
                Height="80"
                Margin="5"
                HorizontalAlignment="Stretch"
                VerticalAlignment="Stretch"
                Click="Button_Click"
                FontSize="32"
                MouseDoubleClick="Button_MouseDoubleClick"
                Tag="{Binding}"
                ToolTip="{Binding Text, Mode=OneTime}"
                ToolTipService.InitialShowDelay="240">
            <fa:ImageAwesome
                    Width="32"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Foreground="CornflowerBlue"
                    Icon="{Binding Text}"
                    Tag="{Binding}" />
            </Button>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.Template>
        <ControlTemplate TargetType="ItemsControl">
            <ScrollViewer
                Width="Auto"
                CanContentScroll="True"
                VerticalScrollBarVisibility="Visible">
                <ItemsPresenter />
            </ScrollViewer>
        </ControlTemplate>
    </ItemsControl.Template>
</ItemsControl>

其实处理的逻辑类似了。在调用的父页面中展示也是使用相应的图标代码即可。

<fa:FontAwesome
    Width="50"
    FontSize="32"
    Icon="{Binding ViewModel.Item.Icon, UpdateSourceTrigger=PropertyChanged}"
    ToolTip="{Binding ViewModel.Item.Icon}" />

这样通过动态配置的菜单,我们就可以让它在系统运行的时候动态加载对应的菜单图标了。

菜单模块配置界面列表效果。

系统运行,动态从后端获取菜单及图标展示如下所示。