Starward项目抽卡记录页面显示异常问题分析与修复

Starward项目抽卡记录页面显示异常问题分析与修复

【免费下载链接】Starward Game Launcher for miHoYo - 米家游戏启动器 【免费下载链接】Starward 项目地址: 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 { } // 静默捕获异常,导致问题难以排查
}

技术原理深度解析

抽卡记录数据处理流程

mermaid

核心数据结构分析

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();
}

总结与最佳实践

通过本次抽卡记录页面显示异常问题的分析与修复,我们总结了以下最佳实践:

  1. 异步操作时序控制:确保异步操作的完整性和时序正确性
  2. 数据绑定优化:避免频繁的Clear操作,采用集合替换策略
  3. 异常处理完善:提供有意义的错误信息和日志记录
  4. 性能监控:对关键操作进行性能分析和优化
  5. 测试覆盖:建立完整的单元测试和集成测试体系

这些解决方案不仅修复了当前的显示异常问题,也为Starward项目的其他功能模块提供了可借鉴的技术实践。通过系统性的问题分析和针对性的修复,显著提升了抽卡记录页面的稳定性和用户体验。

【免费下载链接】Starward Game Launcher for miHoYo - 米家游戏启动器 【免费下载链接】Starward 项目地址: https://gitcode.com/gh_mirrors/st/Starward

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值