Starward项目抽卡记录页面显示异常问题分析与修复
【免费下载链接】Starward Game Launcher for miHoYo - 米家游戏启动器 项目地址: https://gitcode.com/gh_mirrors/st/Starward
引言:抽卡记录显示异常的痛点
作为米哈游游戏启动器Starward的核心功能之一,抽卡记录页面承载着玩家查看历史抽卡数据、分析抽卡概率的重要任务。然而,在实际使用过程中,用户经常遇到页面显示异常的问题:统计数据不更新、卡池选择失效、UI元素错位等。这些问题严重影响了用户体验,甚至可能导致数据丢失的风险。
本文将深入分析Starward项目中抽卡记录页面的常见显示异常问题,并提供完整的解决方案和技术实现细节。
问题现象与分类
1. 统计数据不更新异常
用户反馈最多的异常现象是抽卡统计数据无法正常更新,主要表现为:
- 卡池统计数据停留在初始状态
- 5星/4星物品计数不准确
- 保底计数显示错误
2. 卡池选择功能失效
ListView控件选择状态异常:
// 问题代码示例
ListView_GachaBanners.SelectionChanged -= ListView_GachaBanners_SelectionChanged;
// ... 其他操作
ListView_GachaBanners.SelectionChanged += ListView_GachaBanners_SelectionChanged;
3. UI布局计算错误
统计卡片宽度计算异常:
private void UpdateGachaStatsCardLayout()
{
try
{
if (ItemsControl_GachaStats != null)
{
int count = ItemsControl_GachaStats.Items.Count;
if (count > 0)
{
double width = (ScrollViewer_GachaStats.ActualWidth - 40 - (count - 1) * 12) / count;
width = Math.Clamp(width, 262, double.MaxValue);
// 布局计算可能因异步加载而失效
}
}
}
catch { } // 静默捕获异常,导致问题难以排查
}
技术原理深度解析
抽卡记录数据处理流程
核心数据结构分析
public class GachaTypeStats
{
public int GachaType { get; set; }
public string GachaTypeText { get; set; }
public int Count { get; set; }
public int Pity_5 { get; set; }
public int Pity_4 { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public int Count_5 { get; set; }
public int Count_5_Up { get; set; }
public int Count_4 { get; set; }
public int Count_3 { get; set; }
public double Ratio_5 { get; set; }
public double Ratio_4 { get; set; }
public double Ratio_3 { get; set; }
public double Average_5 { get; set; }
public double Average_5_Up { get; set; }
public List<GachaLogItemEx> List_5 { get; set; }
public List<GachaLogItemEx> List_4 { get; set; }
}
根本原因分析
1. 异步操作时序问题
抽卡记录更新涉及多个异步操作,时序控制不当会导致状态不一致:
protected override async void OnLoaded()
{
await Task.Delay(16); // 延迟加载可能错过关键事件
WeakReferenceMessenger.Default.Register<UpdateGachaLogMessage>(this, (s, m) =>
{
if (m.GameBiz == CurrentGameBiz)
{
User32.SetForegroundWindow((nint)this.XamlRoot.ContentIslandEnvironment.AppWindowId.Value);
_ = UpdateGachaLogInternalAsync(m.Url); // 未等待完成
}
});
Initialize(); // 可能在与异步操作竞争
}
2. 数据绑定机制失效
ObservableCollection的Clear操作可能导致绑定失效:
private void UpdateDisplayGachaTypeStats()
{
if (gachaTypeStats is null) return;
DisplayGachaTypeStatsCollection ??= [];
DisplayGachaTypeStatsCollection.Clear(); // 清除操作可能中断绑定
var list = ListView_GachaBanners.SelectedItems.Cast<GachaBanner>().ToList();
foreach (var item in list)
{
if (gachaTypeStats.FirstOrDefault(x => x.GachaType == item.Value) is GachaTypeStats stats)
{
DisplayGachaTypeStatsCollection.Add(stats); // 重新添加可能不触发UI更新
}
}
}
3. 异常处理不完善
静默捕获异常导致问题难以排查:
private void ListView_GachaBanners_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
try
{
string value = string.Join(',', ListView_GachaBanners.SelectedItems.Cast<GachaBanner>().Select(x => x.Value));
AppConfig.SetDisplayGachaBanners(CurrentGameBiz.Game, value);
UpdateDisplayGachaTypeStats();
}
catch { } // 静默捕获,问题被隐藏
}
完整解决方案
1. 修复异步时序问题
// 修复后的异步控制
protected override async void OnLoaded()
{
try
{
// 确保UI完全加载后再注册消息
await this.DispatcherQueue.EnqueueAsync(() =>
{
WeakReferenceMessenger.Default.Register<UpdateGachaLogMessage>(this, OnGachaLogUpdateMessage);
Initialize();
});
await UpdateWikiDataAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "OnLoaded initialization failed");
InAppToast.MainWindow?.Error(ex);
}
}
private async void OnGachaLogUpdateMessage(object recipient, UpdateGachaLogMessage message)
{
if (message.GameBiz == CurrentGameBiz)
{
await this.DispatcherQueue.EnqueueAsync(() =>
{
User32.SetForegroundWindow((nint)this.XamlRoot.ContentIslandEnvironment.AppWindowId.Value);
});
// 等待更新操作完成
await UpdateGachaLogInternalAsync(message.Url);
}
}
2. 优化数据绑定机制
private void UpdateDisplayGachaTypeStats()
{
if (gachaTypeStats is null)
{
DisplayGachaTypeStatsCollection = new ObservableCollection<GachaTypeStats>();
return;
}
var selectedBanners = ListView_GachaBanners.SelectedItems?
.Cast<GachaBanner>()
.ToList() ?? GachaBanners;
var newCollection = new ObservableCollection<GachaTypeStats>();
foreach (var banner in selectedBanners)
{
var stats = gachaTypeStats.FirstOrDefault(x => x.GachaType == banner.Value);
if (stats != null)
{
newCollection.Add(stats);
}
}
// 直接替换整个集合,避免Clear导致的绑定问题
DisplayGachaTypeStatsCollection = newCollection;
// 触发布局更新
DispatcherQueue.TryEnqueue(() =>
{
UpdateGachaStatsCardLayout();
});
}
3. 完善异常处理与日志
private void ListView_GachaBanners_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
try
{
if (ListView_GachaBanners.SelectedItems == null) return;
var selectedValues = ListView_GachaBanners.SelectedItems
.Cast<GachaBanner>()
.Select(x => x.Value.ToString())
.Where(x => !string.IsNullOrEmpty(x));
if (!selectedValues.Any()) return;
string value = string.Join(',', selectedValues);
AppConfig.SetDisplayGachaBanners(CurrentGameBiz.Game, value);
if (CurrentGameBiz.Game is GameBiz.hkrpg)
{
AppConfig.SetValue(true, "SavedStarRailBannersAfterCollaborationStarting");
}
UpdateDisplayGachaTypeStats();
}
catch (Exception ex)
{
_logger.LogError(ex, "ListView_GachaBanners_SelectionChanged failed");
// 提供用户可见的错误反馈
InAppToast.MainWindow?.Warning("Selection Error", "Failed to update banner selection");
}
}
4. 布局计算优化
private void UpdateGachaStatsCardLayout()
{
try
{
// 确保在UI线程执行布局计算
DispatcherQueue.TryEnqueue(() =>
{
double availableWidth = ScrollViewer_GachaStats.ActualWidth - 40;
if (availableWidth <= 0) return;
UpdateGachaStatsCardsLayout(ItemsControl_GachaStats, availableWidth);
UpdateGachaStatsCardsLayout(ItemsControl_ZZZGachaStats, availableWidth);
});
}
catch (Exception ex)
{
_logger.LogWarning(ex, "UpdateGachaStatsCardLayout failed");
}
}
private void UpdateGachaStatsCardsLayout(ItemsControl itemsControl, double availableWidth)
{
if (itemsControl?.Items == null) return;
int itemCount = itemsControl.Items.Count;
if (itemCount == 0) return;
// 计算每个项目的宽度,考虑间距
double spacing = 12 * (itemCount - 1);
double itemWidth = (availableWidth - spacing) / itemCount;
itemWidth = Math.Max(Math.Min(itemWidth, 400), 262); // 合理的宽度范围
for (int i = 0; i < itemCount; i++)
{
if (itemsControl.ContainerFromIndex(i) is ContentPresenter presenter)
{
presenter.Width = itemWidth;
}
}
}
测试验证方案
单元测试用例
[Test]
public async Task TestGachaLogPage_DataBinding()
{
// 模拟数据
var mockService = new Mock<IGachaLogService>();
var testStats = new List<GachaTypeStats>
{
new GachaTypeStats { GachaType = 1, Count = 100, Count_5 = 5 },
new GachaTypeStats { GachaType = 2, Count = 200, Count_5 = 10 }
};
mockService.Setup(s => s.GetGachaTypeStats(It.IsAny<long>()))
.Returns((testStats, new List<GachaLogItemEx>()));
// 创建页面实例
var page = new GachaLogPage();
page.SetGachaLogService(mockService.Object);
// 测试数据绑定
await page.TestUpdateGachaTypeStats(12345);
// 验证数据是否正确显示
Assert.AreEqual(2, page.DisplayGachaTypeStatsCollection.Count);
Assert.AreEqual(100, page.DisplayGachaTypeStatsCollection[0].Count);
}
集成测试场景
| 测试场景 | 预期结果 | 验证方法 |
|---|---|---|
| 卡池选择变化 | 统计数据实时更新 | UI自动化测试 |
| 异步数据加载 | 页面不卡顿,数据显示正确 | 性能监控 |
| 异常情况处理 | 友好错误提示,不崩溃 | 异常注入测试 |
性能优化建议
1. 数据查询优化
public virtual (List<GachaTypeStats> GachaStats, List<GachaLogItemEx> ItemStats) GetGachaTypeStats(long uid)
{
// 使用批量查询代替多次数据库访问
var allItems = _gachaLogService.QueryGachaLogItems(uid).ToList();
var statsList = new List<GachaTypeStats>();
var itemStats = new List<GachaLogItemEx>();
// 分组处理提高效率
var groupedItems = allItems.GroupBy(x => x.GachaType);
foreach (var group in groupedItems)
{
var stats = CalculateStatsForGroup(group.Key, group.ToList());
statsList.Add(stats);
itemStats.AddRange(GetItemStats(group.ToList()));
}
return (statsList, itemStats);
}
2. 内存管理优化
protected override void OnUnloaded()
{
// 显式释放资源
WeakReferenceMessenger.Default.UnregisterAll(this);
if (DisplayGachaTypeStatsCollection != null)
{
DisplayGachaTypeStatsCollection.Clear();
DisplayGachaTypeStatsCollection = null;
}
// 清理事件订阅
ListView_GachaBanners.SelectionChanged -= ListView_GachaBanners_SelectionChanged;
base.OnUnloaded();
}
总结与最佳实践
通过本次抽卡记录页面显示异常问题的分析与修复,我们总结了以下最佳实践:
- 异步操作时序控制:确保异步操作的完整性和时序正确性
- 数据绑定优化:避免频繁的Clear操作,采用集合替换策略
- 异常处理完善:提供有意义的错误信息和日志记录
- 性能监控:对关键操作进行性能分析和优化
- 测试覆盖:建立完整的单元测试和集成测试体系
这些解决方案不仅修复了当前的显示异常问题,也为Starward项目的其他功能模块提供了可借鉴的技术实践。通过系统性的问题分析和针对性的修复,显著提升了抽卡记录页面的稳定性和用户体验。
【免费下载链接】Starward Game Launcher for miHoYo - 米家游戏启动器 项目地址: https://gitcode.com/gh_mirrors/st/Starward
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



