XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEToolBox

【WPF】XFE工具箱

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFEToolBox

支持软件图标异步加载与ComboBox优化

- SoftwareCatalogRow 增加 IconSource 属性及异步加载方法,防止重复加载图标 - SoftwareManagementPage 实现异步图标加载,支持 base64 与网络图片,优先显示内置资源 - XAML 软件卡片优先显示 IconSource,无则显示首字母 - ComboBox 增加 TextSearch.TextPath,提升筛选与搜索体验 - FilterOption 和 ChannelModeOption 重写 ToString,优化下拉框显示 - 其他细节优化如事件绑定和资源路径适配

a669569
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

6 个文件 +156 -10
Modified XFEToolBox.Core/Downloads/SoftwareCatalogContracts.cs +2 -0
@@ -27,6 +27,8 @@ public sealed class SoftwareDownloadChannel
27 27
28 28 public bool Enabled { get; set; } = true;
29 29 public int SortOrder { get; set; }
30
31 public override string ToString() => string.IsNullOrWhiteSpace(Name) ? Id : Name;
30 32 }
31 33
32 34 /// <summary>
Modified XFEToolBox/Resources/Style/StandardControlsStyle.xaml +2 -0
@@ -252,6 +252,8 @@
252 252 <ContentPresenter Margin="{Binding Padding, RelativeSource={RelativeSource AncestorType=ComboBox}}" VerticalAlignment="Center"
253 253 Content="{Binding SelectionBoxItem, RelativeSource={RelativeSource AncestorType=ComboBox}}"
254 254 ContentTemplate="{Binding SelectionBoxItemTemplate, RelativeSource={RelativeSource AncestorType=ComboBox}}"
255 ContentTemplateSelector="{Binding ItemTemplateSelector, RelativeSource={RelativeSource AncestorType=ComboBox}}"
256 ContentStringFormat="{Binding SelectionBoxItemStringFormat, RelativeSource={RelativeSource AncestorType=ComboBox}}"
255 257 TextElement.Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ComboBox}}"/>
256 258 <Path Grid.Column="1" Width="8" Height="5" Data="M 0,0 L 4,4 L 8,0 Z" Stretch="Fill"
257 259 Fill="{DynamicResource MainColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
Modified XFEToolBox/Views/Pages/Admin/SoftwareManagementPage.xaml +32 -5
@@ -105,7 +105,8 @@
105 105 TextChanged="CatalogSearchTextBox_TextChanged"/>
106 106 <ComboBox x:Name="CategoryFilterBox" Grid.Column="2" Height="36" SelectionChanged="CatalogFilterBox_SelectionChanged"/>
107 107 <ComboBox x:Name="StatusFilterBox" Grid.Column="4" Height="36"
108 DisplayMemberPath="Text" SelectionChanged="CatalogFilterBox_SelectionChanged"/>
108 DisplayMemberPath="Text" TextSearch.TextPath="Text"
109 SelectionChanged="CatalogFilterBox_SelectionChanged"/>
109 110 <Button Grid.Column="6" Content="清除筛选" MinWidth="84" Click="ClearFiltersButton_Click"/>
110 111 </Grid>
111 112
@@ -123,7 +124,8 @@
123 124 <DataTemplate>
124 125 <Border Padding="14,12" Background="#FCFCFF" BorderBrush="#E3E3EF"
125 126 BorderThickness="1" CornerRadius="13" Cursor="Hand"
126 Tag="{Binding}" MouseLeftButtonUp="SoftwareCard_MouseLeftButtonUp">
127 Tag="{Binding}" Loaded="SoftwareCard_Loaded"
128 MouseLeftButtonUp="SoftwareCard_MouseLeftButtonUp">
127 129 <Grid>
128 130 <Grid.ColumnDefinitions>
129 131 <ColumnDefinition Width="46"/><ColumnDefinition Width="14"/>
@@ -133,9 +135,34 @@
133 135 </Grid.ColumnDefinitions>
134 136 <Border Width="46" Height="46" CornerRadius="14" Background="#EEEEFC"
135 137 BorderBrush="#E0E0F2" BorderThickness="1">
136 <TextBlock Text="{Binding Initial}" Foreground="{DynamicResource MainColor}"
137 FontSize="16" FontWeight="SemiBold"
138 HorizontalAlignment="Center" VerticalAlignment="Center"/>
138 <Grid>
139 <TextBlock Text="{Binding Initial}" Foreground="{DynamicResource MainColor}"
140 FontSize="16" FontWeight="SemiBold"
141 HorizontalAlignment="Center" VerticalAlignment="Center">
142 <TextBlock.Style>
143 <Style TargetType="TextBlock">
144 <Setter Property="Visibility" Value="Collapsed"/>
145 <Style.Triggers>
146 <DataTrigger Binding="{Binding IconSource}" Value="{x:Null}">
147 <Setter Property="Visibility" Value="Visible"/>
148 </DataTrigger>
149 </Style.Triggers>
150 </Style>
151 </TextBlock.Style>
152 </TextBlock>
153 <Image Source="{Binding IconSource}" Width="31" Height="31" Stretch="Uniform">
154 <Image.Style>
155 <Style TargetType="Image">
156 <Setter Property="Visibility" Value="Visible"/>
157 <Style.Triggers>
158 <DataTrigger Binding="{Binding IconSource}" Value="{x:Null}">
159 <Setter Property="Visibility" Value="Collapsed"/>
160 </DataTrigger>
161 </Style.Triggers>
162 </Style>
163 </Image.Style>
164 </Image>
165 </Grid>
139 166 </Border>
140 167 <StackPanel Grid.Column="2" VerticalAlignment="Center">
141 168 <StackPanel Orientation="Horizontal">
Modified XFEToolBox/Views/Pages/Admin/SoftwareManagementPage.xaml.cs +116 -4
@@ -2,11 +2,14 @@ using System.Collections.ObjectModel;
2 2 using System.ComponentModel;
3 3 using System.IO;
4 4 using System.Net;
5 using System.Net.Http;
5 6 using System.Runtime.CompilerServices;
6 7 using System.Windows;
7 8 using System.Windows.Controls;
8 9 using System.Windows.Data;
9 10 using System.Windows.Input;
11 using System.Windows.Media;
12 using System.Windows.Media.Imaging;
10 13 using Microsoft.Win32;
11 14 using XFEToolBox.Client.Utilities;
12 15 using XFEToolBox.Client.Utilities.Server;
@@ -18,6 +21,8 @@ public partial class SoftwareManagementPage : Page
18 21 {
19 22 public static SoftwareManagementPage Current { get; } = new();
20 23
24 private static readonly HttpClient IconClient = CreateIconClient();
25 private static readonly SemaphoreSlim IconLoadGate = new(4);
21 26 private readonly ObservableCollection<SoftwareCatalogItem> _software = [];
22 27 private readonly ObservableCollection<SoftwareCatalogRow> _softwareRows = [];
23 28 private readonly ObservableCollection<EditableChannel> _channels = [];
@@ -95,7 +100,7 @@ public partial class SoftwareManagementPage : Page
95 100 foreach (var item in response.Result.OrderBy(item => item.Name, StringComparer.CurrentCultureIgnoreCase))
96 101 {
97 102 _software.Add(item);
98 _softwareRows.Add(new SoftwareCatalogRow(item));
103 _softwareRows.Add(new SoftwareCatalogRow(item, GetBundledIcon(item.Id)));
99 104 }
100 105
101 106 RebuildSuggestionSources();
@@ -237,6 +242,87 @@ public partial class SoftwareManagementPage : Page
237 242 }
238 243 }
239 244
245 private async void SoftwareCard_Loaded(object sender, RoutedEventArgs e)
246 {
247 if (sender is Border { DataContext: SoftwareCatalogRow row } && row.TryBeginIconLoad())
248 await LoadConfiguredIconAsync(row);
249 }
250
251 private static async Task LoadConfiguredIconAsync(SoftwareCatalogRow row)
252 {
253 await IconLoadGate.WaitAsync();
254 try
255 {
256 var icon = await ReadIconAsync(row.Item.IconUrl);
257 if (icon is not null)
258 row.IconSource = icon;
259 }
260 catch
261 {
262 // 配置图标不可用时继续显示内置图标或首字母占位。
263 }
264 finally
265 {
266 IconLoadGate.Release();
267 }
268 }
269
270 private static async Task<ImageSource?> ReadIconAsync(string value)
271 {
272 byte[] bytes;
273 if (value.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase))
274 {
275 var separator = value.IndexOf(',');
276 if (separator < 0)
277 return null;
278 bytes = Convert.FromBase64String(value[(separator + 1)..]);
279 }
280 else
281 {
282 if (!Uri.TryCreate(value, UriKind.Absolute, out var uri) ||
283 (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
284 return null;
285 bytes = await IconClient.GetByteArrayAsync(uri);
286 }
287
288 if (bytes.Length == 0 || bytes.Length > 1024 * 1024)
289 return null;
290
291 using var stream = new MemoryStream(bytes);
292 var image = new BitmapImage();
293 image.BeginInit();
294 image.CacheOption = BitmapCacheOption.OnLoad;
295 image.StreamSource = stream;
296 image.EndInit();
297 image.Freeze();
298 return image;
299 }
300
301 private static ImageSource? GetBundledIcon(string id)
302 {
303 var resource = id.ToLowerInvariant() switch
304 {
305 "steam" => "/Resources/Image/DownloadImage/steam_logo.png",
306 "watt-toolkit" or "steampp" => "/Resources/Image/DownloadImage/steampp.png",
307 "visual-studio" => "/Resources/Image/DownloadImage/visual_studio.png",
308 "cheat-engine" => "/Resources/Image/DownloadImage/cheat_engine.png",
309 _ => null
310 };
311 if (resource is null)
312 return null;
313
314 var image = new BitmapImage(new Uri($"pack://application:,,,{resource}", UriKind.Absolute));
315 image.Freeze();
316 return image;
317 }
318
319 private static HttpClient CreateIconClient()
320 {
321 var client = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
322 client.DefaultRequestHeaders.UserAgent.ParseAdd("XFEToolBox/1.0");
323 return client;
324 }
325
240 326 private void NewButton_Click(object sender, RoutedEventArgs e)
241 327 {
242 328 _isCreating = true;
@@ -704,12 +790,32 @@ public partial class SoftwareManagementPage : Page
704 790 Channels = _channels.Select((channel, index) => channel.ToContract(index)).ToArray()
705 791 };
706 792
707 private sealed class SoftwareCatalogRow(SoftwareCatalogItem item)
793 private sealed class SoftwareCatalogRow(SoftwareCatalogItem item, ImageSource? iconSource) : INotifyPropertyChanged
708 794 {
795 private int _iconLoadStarted;
796 private ImageSource? _iconSource = iconSource;
797
709 798 public SoftwareCatalogItem Item { get; } = item;
710 799 public string Initial => string.IsNullOrWhiteSpace(Item.Name) ? "?" : Item.Name.Trim()[..1].ToUpperInvariant();
711 800 public string ChannelSummary => $"{Item.GetEffectiveChannels().Length} 个渠道";
712 801 public string FeaturedText => Item.Featured ? "精选" : "普通";
802
803 public ImageSource? IconSource
804 {
805 get => _iconSource;
806 set
807 {
808 if (ReferenceEquals(_iconSource, value))
809 return;
810 _iconSource = value;
811 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IconSource)));
812 }
813 }
814
815 public bool TryBeginIconLoad() =>
816 !string.IsNullOrWhiteSpace(Item.IconUrl) && Interlocked.Exchange(ref _iconLoadStarted, 1) == 0;
817
818 public event PropertyChangedEventHandler? PropertyChanged;
713 819 }
714 820
715 821 private sealed class EditableChannel : INotifyPropertyChanged
@@ -798,6 +904,12 @@ public partial class SoftwareManagementPage : Page
798 904 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
799 905 }
800 906
801 private sealed record FilterOption(string Text, string Value);
802 private sealed record ChannelModeOption(string Name, SoftwareDownloadMode Value);
907 private sealed record FilterOption(string Text, string Value)
908 {
909 public override string ToString() => Text;
910 }
911 private sealed record ChannelModeOption(string Name, SoftwareDownloadMode Value)
912 {
913 public override string ToString() => Name;
914 }
803 915 }
Modified XFEToolBox/Views/Pages/DownloadInfoPage.xaml +1 -1
@@ -45,7 +45,7 @@
45 45 <TextBlock Text="可选择不同来源" Foreground="#A0A0B1" FontSize="8.5" Margin="0,2,0,0"/>
46 46 </StackPanel>
47 47 <ComboBox Grid.Column="1" Height="38" ItemsSource="{Binding Channels}" SelectedItem="{Binding SelectedChannel}"
48 DisplayMemberPath="Name"/>
48 DisplayMemberPath="Name" TextSearch.TextPath="Name"/>
49 49 </Grid>
50 50 </StackPanel>
51 51 </Border>
Modified XFEToolBox/Views/Pages/DownloadPage.xaml +3 -0
@@ -72,6 +72,9 @@
72 72 <Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="24"/></Grid.ColumnDefinitions>
73 73 <ContentPresenter Margin="{TemplateBinding Padding}" VerticalAlignment="Center" HorizontalAlignment="Left"
74 74 Content="{Binding SelectionBoxItem, RelativeSource={RelativeSource AncestorType=ComboBox}}"
75 ContentTemplate="{Binding SelectionBoxItemTemplate, RelativeSource={RelativeSource AncestorType=ComboBox}}"
76 ContentTemplateSelector="{Binding ItemTemplateSelector, RelativeSource={RelativeSource AncestorType=ComboBox}}"
77 ContentStringFormat="{Binding SelectionBoxItemStringFormat, RelativeSource={RelativeSource AncestorType=ComboBox}}"
75 78 TextElement.Foreground="{TemplateBinding Foreground}"/>
76 79 <Path x:Name="Arrow" Grid.Column="1" Width="8" Height="5" Stretch="Fill" Fill="#F4F4FF"
77 80 HorizontalAlignment="Center" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5"