JS的设置UI添加级联选择 (#2667)

This commit is contained in:
DarkFlameMaster
2026-01-21 15:23:02 +08:00
committed by GitHub
parent 6e9da06f08
commit ea3f653d95
3 changed files with 413 additions and 1 deletions

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
@@ -6,6 +6,7 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using BetterGenshinImpact.View.Controls;
namespace BetterGenshinImpact.Model;
@@ -18,6 +19,8 @@ public class SettingItem
public List<string>? Options { get; set; }
public Dictionary<string, List<string>>? CascadeOptions { get; set; }
public object? Default { get; set; }
public List<UIElement> ToControl(dynamic context)
@@ -171,6 +174,35 @@ public class SettingItem
break;
}
case "cascade-select":
{
if (CascadeOptions == null || CascadeOptions.Count == 0)
{
break;
}
var cascadeSelector = new CascadeSelector
{
CascadeOptions = CascadeOptions,
DefaultValue = Default?.ToString(),
Margin = new Thickness(0, 0, 0, 10)
};
if (Default != null)
{
if (context is IDictionary<string, object?> ctx)
{
ctx.TryAdd(Name, Default.ToString());
}
}
BindingOperations.SetBinding(cascadeSelector, CascadeSelector.SelectedValueProperty,
new Binding(Name) { Source = context, Mode = BindingMode.TwoWay });
list.Add(cascadeSelector);
break;
}
default:
throw new Exception($"Unknown setting type: {Type}");
}

View File

@@ -0,0 +1,100 @@
<UserControl x:Class="BetterGenshinImpact.View.Controls.CascadeSelector"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
mc:Ignorable="d"
d:DesignHeight="40" d:DesignWidth="300"
Name="Root">
<Grid>
<ToggleButton x:Name="MainToggle"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Padding="12,6"
Height="34"
Background="{DynamicResource ControlFillColorDefaultBrush}"
BorderBrush="{DynamicResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="1">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding SelectedValue, ElementName=Root, TargetNullValue='请选择', FallbackValue='请选择'}"
VerticalAlignment="Center"
Margin="5,0,0,0"
TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Column="1" Text="▼" FontSize="10"
VerticalAlignment="Center" Margin="0,0,10,0"/>
</Grid>
</Border>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<Popup x:Name="MainPopup"
IsOpen="{Binding IsChecked, ElementName=MainToggle, Mode=TwoWay}"
PlacementTarget="{Binding ElementName=MainToggle}"
StaysOpen="False"
AllowsTransparency="True"
PopupAnimation="Slide">
<Border x:Name="PopupBorder"
Background="{DynamicResource ApplicationBackgroundBrush}"
BorderBrush="{DynamicResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Margin="0,4,0,0"
Padding="4"
MinHeight="100"
MaxHeight="300"
MinWidth="120"
MaxWidth="600">
<Border.Effect>
<DropShadowEffect BlurRadius="10" ShadowDepth="2" Direction="270" Color="Black" Opacity="0.2"/>
</Border.Effect>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="100"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto" MinWidth="100"/>
</Grid.ColumnDefinitions>
<ui:ListView x:Name="FirstLevelListView"
ItemsSource="{Binding FirstLevelOptions, ElementName=Root}"
BorderThickness="0"
SelectionChanged="FirstLevelListView_SelectionChanged"
ScrollViewer.VerticalScrollBarVisibility="Auto">
<ui:ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Margin="8,4"/>
</DataTemplate>
</ui:ListView.ItemTemplate>
</ui:ListView>
<Rectangle Grid.Column="1" Width="1"
Fill="{DynamicResource SurfaceStrokeColorDefaultBrush}"
Margin="2,0"/>
<ui:ListView Grid.Column="2" x:Name="SecondLevelListView"
ItemsSource="{Binding SecondLevelOptions, ElementName=Root}"
BorderThickness="0"
SelectionChanged="SecondLevelListView_SelectionChanged"
ScrollViewer.VerticalScrollBarVisibility="Auto">
<ui:ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Margin="8,4"/>
</DataTemplate>
</ui:ListView.ItemTemplate>
</ui:ListView>
</Grid>
</Border>
</Popup>
</Grid>
</UserControl>

View File

@@ -0,0 +1,280 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace BetterGenshinImpact.View.Controls;
public partial class CascadeSelector : UserControl
{
private const double HorizontalMargin = 40;
private const double MinFirstLevelWidth = 100;
private const double MaxFirstLevelWidth = 300;
private const double MinSecondLevelWidth = 100;
private const double MaxSecondLevelWidth = 300;
private const double MinPopupWidth = 120;
private const double MaxPopupWidth = 600;
public CascadeSelector()
{
InitializeComponent();
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
UpdateFirstLevelOptions();
UpdatePopupWidth();
}
public Dictionary<string, List<string>>? CascadeOptions
{
get { return (Dictionary<string, List<string>>?)GetValue(CascadeOptionsProperty); }
set { SetValue(CascadeOptionsProperty, value); }
}
public static readonly DependencyProperty CascadeOptionsProperty =
DependencyProperty.Register("CascadeOptions", typeof(Dictionary<string, List<string>>), typeof(CascadeSelector),
new PropertyMetadata(null, OnCascadeOptionsChanged));
private static void OnCascadeOptionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (CascadeSelector)d;
control.UpdateFirstLevelOptions();
}
public List<string> FirstLevelOptions
{
get { return (List<string>)GetValue(FirstLevelOptionsProperty); }
set { SetValue(FirstLevelOptionsProperty, value); }
}
public static readonly DependencyProperty FirstLevelOptionsProperty =
DependencyProperty.Register("FirstLevelOptions", typeof(List<string>), typeof(CascadeSelector), new PropertyMetadata(null));
public List<string> SecondLevelOptions
{
get { return (List<string>)GetValue(SecondLevelOptionsProperty); }
set { SetValue(SecondLevelOptionsProperty, value); }
}
public static readonly DependencyProperty SecondLevelOptionsProperty =
DependencyProperty.Register("SecondLevelOptions", typeof(List<string>), typeof(CascadeSelector),
new PropertyMetadata(null, OnSecondLevelOptionsChanged));
private static void OnSecondLevelOptionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (CascadeSelector)d;
control.Dispatcher.BeginInvoke(() =>
{
control.AdjustSecondLevelListWidth();
}, System.Windows.Threading.DispatcherPriority.Render);
}
public string? SelectedValue
{
get { return (string?)GetValue(SelectedValueProperty); }
set { SetValue(SelectedValueProperty, value); }
}
public static readonly DependencyProperty SelectedValueProperty =
DependencyProperty.Register("SelectedValue", typeof(string), typeof(CascadeSelector),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedValueChanged));
private static void OnSelectedValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (CascadeSelector)d;
control.HandleSelectedValueChanged((string?)e.NewValue);
}
public string? DefaultValue
{
get { return (string?)GetValue(DefaultValueProperty); }
set { SetValue(DefaultValueProperty, value); }
}
public static readonly DependencyProperty DefaultValueProperty =
DependencyProperty.Register("DefaultValue", typeof(string), typeof(CascadeSelector), new PropertyMetadata(null));
private double MeasureTextWidth(string text, double fontSize)
{
if (string.IsNullOrEmpty(text))
{
return 0;
}
var formattedText = new FormattedText(
text,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
fontSize,
Brushes.Black,
new NumberSubstitution(),
TextFormattingMode.Display,
VisualTreeHelper.GetDpi(this).PixelsPerDip);
return formattedText.WidthIncludingTrailingWhitespace;
}
private void AdjustFirstLevelListWidth()
{
if (FirstLevelOptions == null || FirstLevelOptions.Count == 0)
{
return;
}
double maxWidth = MinFirstLevelWidth;
foreach (var option in FirstLevelOptions)
{
double textWidth = MeasureTextWidth(option, FontSize) + HorizontalMargin;
if (textWidth > maxWidth)
{
maxWidth = textWidth;
}
}
if (maxWidth > MaxFirstLevelWidth)
{
maxWidth = MaxFirstLevelWidth;
}
var grid = PopupBorder?.Child as Grid;
var firstColumn = grid?.ColumnDefinitions[0];
if (firstColumn != null)
{
firstColumn.Width = new GridLength(maxWidth);
}
}
private void AdjustSecondLevelListWidth()
{
if (SecondLevelOptions == null || SecondLevelOptions.Count == 0)
{
UpdatePopupWidth();
return;
}
double maxWidth = MinSecondLevelWidth;
foreach (var option in SecondLevelOptions)
{
double textWidth = MeasureTextWidth(option, FontSize) + HorizontalMargin;
if (textWidth > maxWidth)
{
maxWidth = textWidth;
}
}
if (maxWidth > MaxSecondLevelWidth)
{
maxWidth = MaxSecondLevelWidth;
}
var grid = PopupBorder?.Child as Grid;
var thirdColumn = grid?.ColumnDefinitions[2];
if (thirdColumn != null)
{
thirdColumn.Width = new GridLength(maxWidth);
}
Dispatcher.BeginInvoke(() => UpdatePopupWidth(), System.Windows.Threading.DispatcherPriority.Render);
}
private void UpdatePopupWidth()
{
var grid = PopupBorder?.Child as Grid;
if (grid == null || grid.ColumnDefinitions.Count < 3)
{
return;
}
double totalWidth = 0;
var firstColumn = grid.ColumnDefinitions[0];
var secondColumn = grid.ColumnDefinitions[1];
var thirdColumn = grid.ColumnDefinitions[2];
if (firstColumn.Width.IsAuto)
{
firstColumn.Width = new GridLength(MinFirstLevelWidth);
}
totalWidth += firstColumn.Width.Value;
totalWidth += secondColumn.Width.Value;
totalWidth += thirdColumn.Width.Value;
totalWidth += 8;
if (totalWidth < MinPopupWidth)
{
totalWidth = MinPopupWidth;
}
if (totalWidth > MaxPopupWidth)
{
totalWidth = MaxPopupWidth;
}
PopupBorder.Width = totalWidth;
}
private void UpdateFirstLevelOptions()
{
if (CascadeOptions != null)
{
FirstLevelOptions = CascadeOptions.Keys.ToList();
Dispatcher.BeginInvoke(() =>
{
AdjustFirstLevelListWidth();
UpdatePopupWidth();
}, System.Windows.Threading.DispatcherPriority.Render);
}
else
{
FirstLevelOptions = new List<string>();
}
}
private void HandleSelectedValueChanged(string? newValue)
{
if (string.IsNullOrEmpty(newValue) || CascadeOptions == null)
{
return;
}
foreach (var kvp in CascadeOptions)
{
if (kvp.Value.Contains(newValue))
{
FirstLevelListView.SelectedItem = kvp.Key;
SecondLevelOptions = kvp.Value;
SecondLevelListView.SelectedItem = newValue;
break;
}
}
}
private void FirstLevelListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (FirstLevelListView.SelectedItem is string selectedFirstLevel)
{
if (CascadeOptions != null && CascadeOptions.TryGetValue(selectedFirstLevel, out var secondLevelOptions))
{
SecondLevelOptions = secondLevelOptions;
SecondLevelListView.SelectedItem = null;
}
}
}
private void SecondLevelListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (SecondLevelListView.SelectedItem is string selectedSecondLevel)
{
SelectedValue = selectedSecondLevel;
if (MainToggle.IsChecked == true)
{
MainToggle.IsChecked = false;
}
}
}
}