perf: 干员识别与仓库识别支持虚拟化,大幅提高首次加载速度 (#16486)

https://github.com/user-attachments/assets/3a0722fb-b4c0-4864-9141-7d0200c117b8

## Summary by Sourcery

优化仓库和运营商卡片列表的展示方式,支持基于行的虚拟化以及共享的卡片宽度约束,从而实现更快的初始加载和更一致的布局。

新功能:
- 为虚拟化卡片列表添加共享卡片宽度行为,根据可用空间和列数保持一致的卡片宽度。

增强内容:
- 将仓库和运营商集合重构为基于行的可观察集合,并计算列数,以更好地支持虚拟化网格布局。
- 连接集合变更处理程序,以在仓库和运营商数据更新时保持行展示和列数的同步。

<details>
<summary>Original summary in English</summary>

## Summary by Sourcery

Optimize depot and operator card list presentation to support row-based
virtualization and shared card width constraints for faster initial
loading and more consistent layouts.

New Features:
- Add a shared card width behavior for virtualized card lists to
maintain consistent widths based on available space and column count.

Enhancements:
- Refactor depot and operator collections into row-based observable
collections with computed column counts to better support virtualized
grid layouts.
- Wire collection change handlers to keep row presentations and column
counts in sync when depot and operator data updates.

</details>
This commit is contained in:
uye
2026-05-12 18:28:20 +08:00
committed by GitHub
parent 60470006c5
commit 2d5b2110f5
3 changed files with 956 additions and 172 deletions

View File

@@ -0,0 +1,575 @@
// <copyright file="SharedCardWidthBehavior.cs" company="MaaAssistantArknights">
// Part of the MaaWpfGui project, maintained by the MaaAssistantArknights team (Maa Team)
// Copyright (C) 2021-2025 MaaAssistantArknights Contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License v3.0 only as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
namespace MaaWpfGui.Extensions.UIBehaviors;
/// <summary>
/// 为按行虚拟化的卡片列表维护统一的共享宽度,并根据父容器可用宽度动态约束单卡上限。
/// </summary>
public static class SharedCardWidthBehavior
{
#region ColumnCount
/// <summary>
/// 指定共享宽度作用域当前使用的列数,用于按父容器宽度推导单卡的最大允许宽度。
/// </summary>
public static readonly DependencyProperty ColumnCountProperty =
DependencyProperty.RegisterAttached(
"ColumnCount",
typeof(int),
typeof(SharedCardWidthBehavior),
new PropertyMetadata(1, OnScopeLayoutParameterChanged));
/// <summary>
/// 设置共享宽度作用域的列数。
/// </summary>
/// <param name="obj">承载共享宽度作用域的元素。</param>
/// <param name="value">当前布局的列数。</param>
public static void SetColumnCount(DependencyObject obj, int value) => obj.SetValue(ColumnCountProperty, value);
/// <summary>
/// 获取共享宽度作用域的列数。
/// </summary>
/// <param name="obj">承载共享宽度作用域的元素。</param>
/// <returns>当前布局的列数。</returns>
public static int GetColumnCount(DependencyObject obj) => (int)obj.GetValue(ColumnCountProperty);
#endregion
#region WidthPadding
/// <summary>
/// 指定单卡宽度计算时需要预留的水平占位,用来抵消外边距、边框等额外宽度。
/// </summary>
public static readonly DependencyProperty WidthPaddingProperty =
DependencyProperty.RegisterAttached(
"WidthPadding",
typeof(double),
typeof(SharedCardWidthBehavior),
new PropertyMetadata(10d, OnScopeLayoutParameterChanged));
/// <summary>
/// 设置单卡宽度计算时的水平预留值。
/// </summary>
/// <param name="obj">承载共享宽度作用域的元素。</param>
/// <param name="value">需要从每列可用宽度中扣除的水平占位。</param>
public static void SetWidthPadding(DependencyObject obj, double value) => obj.SetValue(WidthPaddingProperty, value);
/// <summary>
/// 获取单卡宽度计算时的水平预留值。
/// </summary>
/// <param name="obj">承载共享宽度作用域的元素。</param>
/// <returns>每列需要扣除的水平占位。</returns>
public static double GetWidthPadding(DependencyObject obj) => (double)obj.GetValue(WidthPaddingProperty);
#endregion
#region EnableScope
/// <summary>
/// 标记某个 ItemsControl 是否作为共享宽度作用域的根节点。
/// </summary>
public static readonly DependencyProperty EnableScopeProperty =
DependencyProperty.RegisterAttached(
"EnableScope",
typeof(bool),
typeof(SharedCardWidthBehavior),
new PropertyMetadata(false, OnEnableScopeChanged));
/// <summary>
/// 设置元素是否启用共享宽度作用域。
/// </summary>
/// <param name="obj">候选作用域根节点。</param>
/// <param name="value">为 true 时启用共享宽度管理。</param>
public static void SetEnableScope(DependencyObject obj, bool value) => obj.SetValue(EnableScopeProperty, value);
/// <summary>
/// 获取元素是否启用了共享宽度作用域。
/// </summary>
/// <param name="obj">候选作用域根节点。</param>
/// <returns>为 true 表示该元素负责维护整组卡片的共享宽度。</returns>
public static bool GetEnableScope(DependencyObject obj) => (bool)obj.GetValue(EnableScopeProperty);
#endregion
#region TrackWidth
/// <summary>
/// 标记某个卡片元素是否参与共享宽度测量。
/// </summary>
public static readonly DependencyProperty TrackWidthProperty =
DependencyProperty.RegisterAttached(
"TrackWidth",
typeof(bool),
typeof(SharedCardWidthBehavior),
new PropertyMetadata(false, OnTrackWidthChanged));
/// <summary>
/// 设置元素是否参与共享宽度跟踪。
/// </summary>
/// <param name="obj">需要参与测量的卡片元素。</param>
/// <param name="value">为 true 时会把该元素的实际宽度纳入共享宽度计算。</param>
public static void SetTrackWidth(DependencyObject obj, bool value) => obj.SetValue(TrackWidthProperty, value);
/// <summary>
/// 获取元素是否参与共享宽度跟踪。
/// </summary>
/// <param name="obj">需要参与测量的卡片元素。</param>
/// <returns>为 true 表示该元素会推动共享宽度增大。</returns>
public static bool GetTrackWidth(DependencyObject obj) => (bool)obj.GetValue(TrackWidthProperty);
#endregion
#region SharedMinWidth
/// <summary>
/// 保存当前作用域内所有卡片共享的最小宽度,供每个卡片绑定统一值。
/// </summary>
public static readonly DependencyProperty SharedMinWidthProperty =
DependencyProperty.RegisterAttached(
"SharedMinWidth",
typeof(double),
typeof(SharedCardWidthBehavior),
new PropertyMetadata(0d));
/// <summary>
/// 设置当前作用域的共享最小宽度。
/// </summary>
/// <param name="obj">共享宽度作用域根节点。</param>
/// <param name="value">所有卡片共同使用的最小宽度。</param>
public static void SetSharedMinWidth(DependencyObject obj, double value) => obj.SetValue(SharedMinWidthProperty, value);
/// <summary>
/// 获取当前作用域的共享最小宽度。
/// </summary>
/// <param name="obj">共享宽度作用域根节点。</param>
/// <returns>当前缓存的统一卡片宽度。</returns>
public static double GetSharedMinWidth(DependencyObject obj) => (double)obj.GetValue(SharedMinWidthProperty);
#endregion
#region InitialWidth
/// <summary>
/// 指定作用域初始化或整体重算时使用的基准宽度,避免首次布局时过窄。
/// </summary>
public static readonly DependencyProperty InitialWidthProperty =
DependencyProperty.RegisterAttached(
"InitialWidth",
typeof(double),
typeof(SharedCardWidthBehavior),
new PropertyMetadata(0d));
/// <summary>
/// 设置共享宽度的初始基准值。
/// </summary>
/// <param name="obj">共享宽度作用域根节点。</param>
/// <param name="value">整体重算前先采用的基准宽度。</param>
public static void SetInitialWidth(DependencyObject obj, double value) => obj.SetValue(InitialWidthProperty, value);
/// <summary>
/// 获取共享宽度的初始基准值。
/// </summary>
/// <param name="obj">共享宽度作用域根节点。</param>
/// <returns>初始化或重算时使用的基准宽度。</returns>
public static double GetInitialWidth(DependencyObject obj) => (double)obj.GetValue(InitialWidthProperty);
#endregion
#region WidthLimit
/// <summary>
/// 保存当前作用域内单个卡片允许使用的硬上限宽度。
/// </summary>
public static readonly DependencyProperty WidthLimitProperty =
DependencyProperty.RegisterAttached(
"WidthLimit",
typeof(double),
typeof(SharedCardWidthBehavior),
new PropertyMetadata(double.PositiveInfinity, OnWidthLimitChanged));
/// <summary>
/// 设置当前作用域内单卡的宽度上限。
/// </summary>
/// <param name="obj">共享宽度作用域根节点。</param>
/// <param name="value">单卡允许使用的最大宽度。</param>
public static void SetWidthLimit(DependencyObject obj, double value) => obj.SetValue(WidthLimitProperty, value);
/// <summary>
/// 获取当前作用域内单卡的宽度上限。
/// </summary>
/// <param name="obj">共享宽度作用域根节点。</param>
/// <returns>单卡允许使用的最大宽度。</returns>
public static double GetWidthLimit(DependencyObject obj) => (double)obj.GetValue(WidthLimitProperty);
#endregion
// 监听 ItemsSource 变更,确保整表数据切换后重新扫描一次共享宽度。
private static readonly DependencyPropertyDescriptor ItemsSourceDescriptor =
DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, typeof(ItemsControl));
// 标记当前作用域是否已经排队等待重算,避免同一帧重复投递刷新请求。
private static readonly DependencyProperty IsRecalculationQueuedProperty =
DependencyProperty.RegisterAttached(
"IsRecalculationQueued",
typeof(bool),
typeof(SharedCardWidthBehavior),
new PropertyMetadata(false));
// 标记当前作用域是否正在执行整体重算,避免测量过程中的回调再次干扰共享宽度。
private static readonly DependencyProperty IsRecalculatingProperty =
DependencyProperty.RegisterAttached(
"IsRecalculating",
typeof(bool),
typeof(SharedCardWidthBehavior),
new PropertyMetadata(false));
private static void OnScopeLayoutParameterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ItemsControl itemsControl && GetEnableScope(itemsControl))
{
// 列数或间距变化后,需要重新按父容器宽度计算单卡上限。
QueueScopeRecalculation(itemsControl);
}
}
private static void OnWidthLimitChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ItemsControl itemsControl)
{
CoerceSharedMinWidth(itemsControl);
}
}
private static void OnEnableScopeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ItemsControl itemsControl)
{
return;
}
if ((bool)e.NewValue)
{
itemsControl.Loaded += OnScopeLoaded;
itemsControl.SizeChanged += OnScopeSizeChanged;
ItemsSourceDescriptor?.AddValueChanged(itemsControl, OnScopeItemsSourceChanged);
QueueScopeRecalculation(itemsControl);
}
else
{
itemsControl.Loaded -= OnScopeLoaded;
itemsControl.SizeChanged -= OnScopeSizeChanged;
ItemsSourceDescriptor?.RemoveValueChanged(itemsControl, OnScopeItemsSourceChanged);
ResetScope(itemsControl);
}
}
private static void OnTrackWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not FrameworkElement element)
{
return;
}
if ((bool)e.NewValue)
{
element.Loaded += OnTrackedElementLoaded;
element.SizeChanged += OnTrackedElementSizeChanged;
UpdateSharedMinWidth(element);
}
else
{
element.Loaded -= OnTrackedElementLoaded;
element.SizeChanged -= OnTrackedElementSizeChanged;
}
}
private static void OnScopeLoaded(object sender, RoutedEventArgs e)
{
if (sender is ItemsControl itemsControl)
{
QueueScopeRecalculation(itemsControl);
}
}
private static void OnScopeItemsSourceChanged(object? sender, EventArgs e)
{
if (sender is ItemsControl itemsControl)
{
QueueScopeRecalculation(itemsControl);
}
}
private static void OnScopeSizeChanged(object sender, SizeChangedEventArgs e)
{
if (!e.WidthChanged)
{
return;
}
if (sender is ItemsControl itemsControl)
{
// 共享宽度只在父容器宽度变化时允许收缩,卡片自身变化只允许把它撑大。
QueueScopeRecalculation(itemsControl);
}
}
private static void OnTrackedElementLoaded(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement element)
{
UpdateSharedMinWidth(element);
}
}
private static void OnTrackedElementSizeChanged(object sender, SizeChangedEventArgs e)
{
if (sender is FrameworkElement element)
{
UpdateSharedMinWidth(element);
}
}
private static void UpdateSharedMinWidth(FrameworkElement element)
{
var trackedWidth = GetTrackedWidth(element);
if (trackedWidth <= 0)
{
return;
}
var scope = FindScope(element);
if (scope == null)
{
return;
}
if ((bool)scope.GetValue(IsRecalculatingProperty))
{
return;
}
// 运行时上推共享宽度时,仍然必须先服从当前的单卡硬上限。
trackedWidth = ClampWidth(scope, trackedWidth);
if (trackedWidth <= 0)
{
return;
}
var currentWidth = GetSharedMinWidth(scope);
if (trackedWidth > currentWidth + 0.5)
{
SetSharedMinWidth(scope, trackedWidth);
}
}
private static void ResetScope(ItemsControl itemsControl)
{
SetSharedMinWidth(itemsControl, ClampWidth(itemsControl, GetInitialWidth(itemsControl)));
}
private static void QueueScopeRecalculation(ItemsControl itemsControl)
{
if ((bool)itemsControl.GetValue(IsRecalculationQueuedProperty) || itemsControl.Dispatcher.HasShutdownStarted)
{
return;
}
itemsControl.SetValue(IsRecalculationQueuedProperty, true);
_ = itemsControl.Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() =>
{
itemsControl.SetValue(IsRecalculationQueuedProperty, false);
if (!GetEnableScope(itemsControl))
{
return;
}
RecalculateScopeWidth(itemsControl);
}));
}
private static void RecalculateScopeWidth(ItemsControl itemsControl)
{
itemsControl.SetValue(IsRecalculatingProperty, true);
try
{
// 布局前后各刷新一次上限,确保依赖可视区的值都是最新的。
UpdateWidthLimit(itemsControl);
ResetScope(itemsControl);
itemsControl.UpdateLayout();
UpdateWidthLimit(itemsControl);
var maxWidth = GetInitialWidth(itemsControl);
var widthLimit = GetWidthLimit(itemsControl);
foreach (var trackedElement in EnumerateTrackedElements(itemsControl))
{
var trackedWidth = GetTrackedWidth(trackedElement);
if (trackedWidth > maxWidth + 0.5)
{
maxWidth = trackedWidth;
}
}
SetSharedMinWidth(itemsControl, ClampWidth(itemsControl, maxWidth));
}
finally
{
itemsControl.SetValue(IsRecalculatingProperty, false);
}
}
private static ItemsControl? FindScope(DependencyObject? current)
{
while (current != null)
{
if (current is ItemsControl itemsControl && GetEnableScope(itemsControl))
{
return itemsControl;
}
current = VisualTreeHelper.GetParent(current);
}
return null;
}
private static void UpdateWidthLimit(ItemsControl itemsControl)
{
var availableWidth = GetAvailableWidth(itemsControl);
var columnCount = Math.Max(1, GetColumnCount(itemsControl));
var widthPadding = GetWidthPadding(itemsControl);
if (availableWidth <= 0)
{
SetWidthLimit(itemsControl, double.PositiveInfinity);
return;
}
var widthLimit = Math.Max(0, (availableWidth / columnCount) - widthPadding);
SetWidthLimit(itemsControl, widthLimit);
}
private static void CoerceSharedMinWidth(ItemsControl itemsControl)
{
// SharedMinWidth 是缓存值,所以硬上限变化后要再做一次钳制。
var currentWidth = GetSharedMinWidth(itemsControl);
var clampedWidth = ClampWidth(itemsControl, currentWidth);
if (Math.Abs(clampedWidth - currentWidth) > 0.5)
{
SetSharedMinWidth(itemsControl, clampedWidth);
}
}
private static double GetAvailableWidth(ItemsControl itemsControl)
{
var scrollViewer = FindDescendant<ScrollViewer>(itemsControl);
if (scrollViewer != null)
{
// 出现滚动条时ViewportWidth 比 ActualWidth 更接近真实可用宽度。
var viewportWidth = scrollViewer.ViewportWidth;
if (!double.IsNaN(viewportWidth) && !double.IsInfinity(viewportWidth) && viewportWidth > 0)
{
return viewportWidth;
}
}
return itemsControl.ActualWidth;
}
private static IEnumerable<FrameworkElement> EnumerateTrackedElements(DependencyObject root)
{
var childCount = VisualTreeHelper.GetChildrenCount(root);
for (var index = 0; index < childCount; index++)
{
var child = VisualTreeHelper.GetChild(root, index);
if (child is FrameworkElement element && GetTrackWidth(element))
{
yield return element;
}
foreach (var descendant in EnumerateTrackedElements(child))
{
yield return descendant;
}
}
}
private static double GetTrackedWidth(FrameworkElement element)
{
var width = element.ActualWidth;
if (width <= 0)
{
return 0;
}
var maxWidth = element.MaxWidth;
if (!double.IsNaN(maxWidth) && !double.IsInfinity(maxWidth) && maxWidth > 0)
{
width = Math.Min(width, maxWidth);
}
return width;
}
private static double ClampWidth(ItemsControl itemsControl, double width)
{
if (width <= 0)
{
return 0;
}
// 共享宽度任何时候都不允许超过当前的单卡上限。
var widthLimit = GetWidthLimit(itemsControl);
if (!double.IsNaN(widthLimit) && !double.IsInfinity(widthLimit) && widthLimit > 0)
{
width = Math.Min(width, widthLimit);
}
return Math.Max(0, width);
}
private static T? FindDescendant<T>(DependencyObject root)
where T : DependencyObject
{
var childCount = VisualTreeHelper.GetChildrenCount(root);
for (var index = 0; index < childCount; index++)
{
var child = VisualTreeHelper.GetChild(root, index);
if (child is T result)
{
return result;
}
var descendant = FindDescendant<T>(child);
if (descendant != null)
{
return descendant;
}
}
return null;
}
}

View File

@@ -16,6 +16,7 @@ using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Threading;
@@ -74,6 +75,8 @@ public class ToolboxViewModel : Screen
_gachaTimer.Tick += RefreshGachaTip;
LoadDepotDetails();
LoadOperBoxDetails();
InitializeDepotRowPresentation();
InitializeOperBoxRowPresentation();
OperBoxSelectedIndex = OperBoxNotHaveList.Count > 0 ? 0 : 1;
UpdateMiniGameTaskList();
@@ -497,6 +500,8 @@ public class ToolboxViewModel : Screen
}
}
private const int DepotRowSize = 5;
private ObservableCollection<DepotResultDate> _depotResult = [];
/// <summary>
@@ -506,11 +511,31 @@ public class ToolboxViewModel : Screen
{
get => _depotResult;
set {
if (ReferenceEquals(_depotResult, value))
{
RefreshDepotRows();
InvalidateDepotCache();
return;
}
_depotResult.CollectionChanged -= DepotResultCollectionChanged;
SetAndNotify(ref _depotResult, value);
_depotResult.CollectionChanged += DepotResultCollectionChanged;
RefreshDepotRows();
InvalidateDepotCache();
}
}
private ObservableCollection<ObservableCollection<DepotResultDate>> _depotRows = [];
public ObservableCollection<ObservableCollection<DepotResultDate>> DepotRows
{
get => _depotRows;
private set => SetAndNotify(ref _depotRows, value);
}
public int DepotColumnCount => GetColumnCount(DepotResult.Count, DepotRowSize);
// 缓存相关字段
private bool _depotCacheInvalid = true;
private string? _cachedArkPlannerResult;
@@ -554,6 +579,23 @@ public class ToolboxViewModel : Screen
public string? DisplayCount => Count >= 0 ? Count.FormatNumber(false) : null;
}
private void InitializeDepotRowPresentation()
{
_depotResult.CollectionChanged += DepotResultCollectionChanged;
RefreshDepotRows();
}
private void DepotResultCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RefreshDepotRows();
}
private void RefreshDepotRows()
{
DepotRows = BuildRows(DepotResult, DepotRowSize);
NotifyOfPropertyChange(nameof(DepotColumnCount));
}
/// <summary>
/// 保存仓库详情数据
/// </summary>
@@ -1142,20 +1184,111 @@ public class ToolboxViewModel : Screen
}
}
private const int OperBoxRowSize = 5;
private ObservableCollection<Operator> _operBoxHaveList = [];
public ObservableCollection<Operator> OperBoxHaveList
{
get => _operBoxHaveList;
set => SetAndNotify(ref _operBoxHaveList, value);
set {
if (ReferenceEquals(_operBoxHaveList, value))
{
RefreshOperBoxHaveRows();
return;
}
_operBoxHaveList.CollectionChanged -= OperBoxHaveListCollectionChanged;
SetAndNotify(ref _operBoxHaveList, value);
_operBoxHaveList.CollectionChanged += OperBoxHaveListCollectionChanged;
RefreshOperBoxHaveRows();
}
}
private ObservableCollection<ObservableCollection<Operator>> _operBoxHaveRows = [];
public ObservableCollection<ObservableCollection<Operator>> OperBoxHaveRows
{
get => _operBoxHaveRows;
private set => SetAndNotify(ref _operBoxHaveRows, value);
}
public int OperBoxHaveColumnCount => GetColumnCount(OperBoxHaveList.Count, OperBoxRowSize);
private ObservableCollection<Operator> _operBoxNotHaveList = [];
public ObservableCollection<Operator> OperBoxNotHaveList
{
get => _operBoxNotHaveList;
set => SetAndNotify(ref _operBoxNotHaveList, value);
set {
if (ReferenceEquals(_operBoxNotHaveList, value))
{
RefreshOperBoxNotHaveRows();
return;
}
_operBoxNotHaveList.CollectionChanged -= OperBoxNotHaveListCollectionChanged;
SetAndNotify(ref _operBoxNotHaveList, value);
_operBoxNotHaveList.CollectionChanged += OperBoxNotHaveListCollectionChanged;
RefreshOperBoxNotHaveRows();
}
}
private ObservableCollection<ObservableCollection<Operator>> _operBoxNotHaveRows = [];
public ObservableCollection<ObservableCollection<Operator>> OperBoxNotHaveRows
{
get => _operBoxNotHaveRows;
private set => SetAndNotify(ref _operBoxNotHaveRows, value);
}
public int OperBoxNotHaveColumnCount => GetColumnCount(OperBoxNotHaveList.Count, OperBoxRowSize);
private void InitializeOperBoxRowPresentation()
{
_operBoxHaveList.CollectionChanged += OperBoxHaveListCollectionChanged;
_operBoxNotHaveList.CollectionChanged += OperBoxNotHaveListCollectionChanged;
RefreshOperBoxHaveRows();
RefreshOperBoxNotHaveRows();
}
private void OperBoxHaveListCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RefreshOperBoxHaveRows();
}
private void OperBoxNotHaveListCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RefreshOperBoxNotHaveRows();
}
private void RefreshOperBoxHaveRows()
{
OperBoxHaveRows = BuildRows(OperBoxHaveList, OperBoxRowSize);
NotifyOfPropertyChange(nameof(OperBoxHaveColumnCount));
}
private void RefreshOperBoxNotHaveRows()
{
OperBoxNotHaveRows = BuildRows(OperBoxNotHaveList, OperBoxRowSize);
NotifyOfPropertyChange(nameof(OperBoxNotHaveColumnCount));
}
private static ObservableCollection<ObservableCollection<T>> BuildRows<T>(IEnumerable<T> items, int rowSize)
{
ObservableCollection<ObservableCollection<T>> rows = [];
foreach (var row in items.Chunk(rowSize))
{
rows.Add(new ObservableCollection<T>(row));
}
return rows;
}
private static int GetColumnCount(int count, int rowSize)
{
return count <= 0 ? 1 : Math.Min(count, rowSize);
}
private void SaveOperBoxDetails(List<OperBoxData.OperData> details)

View File

@@ -16,6 +16,54 @@
d:DesignHeight="600"
d:DesignWidth="800"
mc:Ignorable="d">
<UserControl.Resources>
<Style x:Key="OperBoxVirtualizedRowListBoxItemStyle" TargetType="{x:Type ListBoxItem}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<ContentPresenter
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="OperBoxVirtualizedRowListBoxStyle" TargetType="{x:Type ListBox}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="ItemContainerStyle" Value="{StaticResource OperBoxVirtualizedRowListBoxItemStyle}" />
<Setter Property="ScrollViewer.CanContentScroll" Value="True" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="True" />
<Setter Property="VirtualizingPanel.ScrollUnit" Value="Pixel" />
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBox}">
<hc:ScrollViewer
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
CanContentScroll="{TemplateBinding ScrollViewer.CanContentScroll}"
Focusable="False"
HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</hc:ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<TabControl
Margin="0,10"
Background="{DynamicResource MouseOverRegionBrushOpacity75}"
@@ -248,129 +296,149 @@
Header="{c:Binding OperBoxNotHaveList.Count}"
HeaderStringFormat="{StaticResource OperBoxNotHaveList}"
Visibility="{c:Binding 'OperBoxNotHaveList.Count != 0'}">
<hc:ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ItemsControl
HorizontalAlignment="Center"
VerticalAlignment="Center"
d:ItemsSource="{d:SampleData ItemCount=1}"
ItemsSource="{Binding OperBoxNotHaveList}"
ScrollViewer.CanContentScroll="True"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{c:Binding 'OperBoxNotHaveList.Count >= 5 ? 5 : OperBoxNotHaveList.Count'}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
Margin="5"
Padding="10,15"
Background="{DynamicResource RegionBrushOpacity50}"
BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1"
CornerRadius="4">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<controls:TextBlock
HorizontalAlignment="Center"
FontSize="13"
Text="{Binding Name}"
TextAlignment="Center"
TextWrapping="Wrap" />
<controls:TextBlock
HorizontalAlignment="Center"
uiBehaviors:ResourceReferenceHelper.ForegroundResourceKey="{Binding RarityColorResourceKey}"
FontSize="11"
Text="{Binding RarityStars}"
TextAlignment="Center" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</hc:ScrollViewer>
<ListBox
d:ItemsSource="{d:SampleData ItemCount=3}"
uiBehaviors:SharedCardWidthBehavior.ColumnCount="{Binding OperBoxNotHaveColumnCount}"
uiBehaviors:SharedCardWidthBehavior.EnableScope="True"
ItemsSource="{Binding OperBoxNotHaveRows}"
Style="{StaticResource OperBoxVirtualizedRowListBoxStyle}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<ItemsControl HorizontalAlignment="Center" ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding DataContext.OperBoxNotHaveColumnCount, RelativeSource={RelativeSource AncestorType=ListBox}}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
MinWidth="{Binding Path=(uiBehaviors:SharedCardWidthBehavior.SharedMinWidth), RelativeSource={RelativeSource AncestorType=ListBox}}"
MaxWidth="{Binding Path=(uiBehaviors:SharedCardWidthBehavior.WidthLimit), RelativeSource={RelativeSource AncestorType=ListBox}}"
Margin="5"
Padding="10,15"
uiBehaviors:SharedCardWidthBehavior.TrackWidth="True"
Background="{DynamicResource RegionBrushOpacity50}"
BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1"
CornerRadius="4">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<controls:TextBlock
HorizontalAlignment="Center"
FontSize="13"
Text="{Binding Name}"
TextAlignment="Center"
TextWrapping="Wrap" />
<controls:TextBlock
HorizontalAlignment="Center"
uiBehaviors:ResourceReferenceHelper.ForegroundResourceKey="{Binding RarityColorResourceKey}"
FontSize="11"
Text="{Binding RarityStars}"
TextAlignment="Center" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</TabItem>
<TabItem Header="{c:Binding OperBoxHaveList.Count}" HeaderStringFormat="{StaticResource OperBoxHaveList}">
<hc:ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ItemsControl
HorizontalAlignment="Center"
VerticalAlignment="Center"
d:ItemsSource="{d:SampleData ItemCount=1}"
ItemsSource="{Binding OperBoxHaveList}"
ScrollViewer.CanContentScroll="True"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{c:Binding 'OperBoxHaveList.Count >= 5 ? 5 : OperBoxHaveList.Count'}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
Margin="5"
Padding="10,15"
Background="{DynamicResource RegionBrushOpacity25}"
BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1"
CornerRadius="4">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<controls:TextBlock
HorizontalAlignment="Center"
FontSize="13"
Text="{Binding Name}"
TextAlignment="Center"
TextWrapping="Wrap" />
<controls:TextBlock
HorizontalAlignment="Center"
uiBehaviors:ResourceReferenceHelper.ForegroundResourceKey="{Binding RarityColorResourceKey}"
FontSize="11"
Text="{Binding RarityStars}"
TextAlignment="Center" />
<StackPanel
Margin="0,0,0,0"
HorizontalAlignment="Center"
Orientation="Horizontal">
<Image
Width="18"
Height="18"
Source="{Binding EliteIconPath}"
Stretch="Uniform"
ToolTip="{Binding Elite}">
<Image.Effect>
<DropShadowEffect
BlurRadius="8"
Opacity="0.5"
ShadowDepth="2"
Color="Black" />
</Image.Effect>
</Image>
<controls:TextBlock
Margin="2.5,0"
VerticalAlignment="Center"
FontSize="11"
Text="{Binding Level, StringFormat={}Lv.{0}}" />
<Image
Width="18"
Height="18"
Source="{Binding PotentialIconPath}"
ToolTip="{Binding Potential}">
<Image.Effect>
<DropShadowEffect
BlurRadius="8"
Opacity="0.5"
ShadowDepth="2"
Color="Black" />
</Image.Effect>
</Image>
</StackPanel>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</hc:ScrollViewer>
<ListBox
d:ItemsSource="{d:SampleData ItemCount=3}"
uiBehaviors:SharedCardWidthBehavior.ColumnCount="{Binding OperBoxHaveColumnCount}"
uiBehaviors:SharedCardWidthBehavior.EnableScope="True"
ItemsSource="{Binding OperBoxHaveRows}"
Style="{StaticResource OperBoxVirtualizedRowListBoxStyle}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<ItemsControl HorizontalAlignment="Center" ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding DataContext.OperBoxHaveColumnCount, RelativeSource={RelativeSource AncestorType=ListBox}}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
MinWidth="{Binding Path=(uiBehaviors:SharedCardWidthBehavior.SharedMinWidth), RelativeSource={RelativeSource AncestorType=ListBox}}"
MaxWidth="{Binding Path=(uiBehaviors:SharedCardWidthBehavior.WidthLimit), RelativeSource={RelativeSource AncestorType=ListBox}}"
Margin="5"
Padding="10,15"
uiBehaviors:SharedCardWidthBehavior.TrackWidth="True"
Background="{DynamicResource RegionBrushOpacity25}"
BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1"
CornerRadius="4">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<controls:TextBlock
HorizontalAlignment="Center"
FontSize="13"
Text="{Binding Name}"
TextAlignment="Center"
TextWrapping="Wrap" />
<controls:TextBlock
HorizontalAlignment="Center"
uiBehaviors:ResourceReferenceHelper.ForegroundResourceKey="{Binding RarityColorResourceKey}"
FontSize="11"
Text="{Binding RarityStars}"
TextAlignment="Center" />
<StackPanel
Margin="0,0,0,0"
HorizontalAlignment="Center"
Orientation="Horizontal">
<Image
Width="18"
Height="18"
Source="{Binding EliteIconPath}"
Stretch="Uniform"
ToolTip="{Binding Elite}">
<Image.Effect>
<DropShadowEffect
BlurRadius="8"
Opacity="0.5"
ShadowDepth="2"
Color="Black" />
</Image.Effect>
</Image>
<controls:TextBlock
Margin="2.5,0"
VerticalAlignment="Center"
FontSize="11"
Text="{Binding Level, StringFormat={}Lv.{0}}" />
<Image
Width="18"
Height="18"
Source="{Binding PotentialIconPath}"
ToolTip="{Binding Potential}">
<Image.Effect>
<DropShadowEffect
BlurRadius="8"
Opacity="0.5"
ShadowDepth="2"
Color="Black" />
</Image.Effect>
</Image>
</StackPanel>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</TabItem>
</TabControl>
<StackPanel
@@ -427,56 +495,64 @@
TextWrapping="Wrap"
Visibility="{c:Binding 'DepotInfo.Length > 0'}" />
</StackPanel>
<hc:ScrollViewer
<ListBox
Grid.Row="1"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsControl
HorizontalAlignment="Center"
VerticalAlignment="Center"
d:ItemsSource="{d:SampleData ItemCount=1}"
ItemsSource="{Binding DepotResult}"
ScrollViewer.CanContentScroll="True"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{c:Binding 'DepotResult.Count >= 5 ? 5 : DepotResult.Count'}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
MinWidth="125"
Margin="5"
Padding="2"
Background="{DynamicResource RegionBrushOpacity25}"
BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1"
CornerRadius="4">
<StackPanel HorizontalAlignment="Center" Orientation="Vertical">
<Image
Width="50"
Height="50"
Source="{Binding Image}" />
<controls:TextBlock
HorizontalAlignment="Center"
FontSize="12"
Text="{Binding Name}"
TextAlignment="Center"
TextWrapping="Wrap" />
<controls:TextBlock
HorizontalAlignment="Center"
FontSize="12"
FontWeight="Bold"
Text="{Binding DisplayCount}"
TextAlignment="Center" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</hc:ScrollViewer>
d:ItemsSource="{d:SampleData ItemCount=3}"
uiBehaviors:SharedCardWidthBehavior.ColumnCount="{Binding DepotColumnCount}"
uiBehaviors:SharedCardWidthBehavior.EnableScope="True"
uiBehaviors:SharedCardWidthBehavior.InitialWidth="125"
ItemsSource="{Binding DepotRows}"
Style="{StaticResource OperBoxVirtualizedRowListBoxStyle}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<ItemsControl HorizontalAlignment="Center" ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding DataContext.DepotColumnCount, RelativeSource={RelativeSource AncestorType=ListBox}}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
MinWidth="{Binding Path=(uiBehaviors:SharedCardWidthBehavior.SharedMinWidth), RelativeSource={RelativeSource AncestorType=ListBox}}"
MaxWidth="{Binding Path=(uiBehaviors:SharedCardWidthBehavior.WidthLimit), RelativeSource={RelativeSource AncestorType=ListBox}}"
Margin="5"
Padding="2"
uiBehaviors:SharedCardWidthBehavior.TrackWidth="True"
Background="{DynamicResource RegionBrushOpacity25}"
BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1"
CornerRadius="4">
<StackPanel HorizontalAlignment="Center" Orientation="Vertical">
<Image
Width="50"
Height="50"
Source="{Binding Image}" />
<controls:TextBlock
HorizontalAlignment="Center"
FontSize="12"
Text="{Binding Name}"
TextAlignment="Center"
TextWrapping="Wrap" />
<controls:TextBlock
HorizontalAlignment="Center"
FontSize="12"
FontWeight="Bold"
Text="{Binding DisplayCount}"
TextAlignment="Center" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel
Grid.Row="2"
Margin="0,10,0,0"