You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用SkiaSharp .NET MAUI生成SQLite库存PDF的报错问题

解决SkiaSharp库存PDF报表的两个核心问题

问题背景

需要用SkiaSharp生成专业库存计数PDF报表,从SQLite读取数据,按商品分类分组展示并统计分类合计,但找不到合适的代码示例。同时当前代码调用pdfDocument.ToArray()时出现**SKDocument doesn't contain a definition for 'ToArray'**错误。


一、修复ToArray()报错问题

错误原因

SKDocument本身没有ToArray()方法,PDF的二进制数据是写入到创建SKDocument时传入的MemoryStream中的,不能直接返回SKDocument对象,应该返回Stream或者byte数组。

修正后的核心代码

1. 修改PDF生成方法,返回byte数组

public async Task<byte[]> GenerateInventoryItemCountPdfAsync(DateTime date, List<Item> items, string pdfLogoPath, string pdfTitle) 
{ 
    using var stream = new MemoryStream(); 
    using var document = SKDocument.CreatePdf(stream); 

    var pageSize = new SKSize(595, 842); // A4尺寸,单位:点
    using var canvas = document.BeginPage((int)pageSize.Width, (int)pageSize.Height); 

    // 绘制Logo(取消注释即可启用)
    //using (var logoBitmap = SKBitmap.Decode(pdfLogoPath))
    //{
    //    var logoRect = SKRect.Create(pageSize.Width / 2 - logoBitmap.Width / 2, 50, logoBitmap.Width, logoBitmap.Height);
    //    canvas.DrawBitmap(logoBitmap, logoRect);
    //}

    // 绘制标题
    var titlePaint = new SKPaint 
    { 
        Typeface = SKTypeface.FromFamilyName("Arial", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright), 
        TextSize = 24, 
        IsAntialias = true, 
        Color = SKColors.Black 
    }; 
    var titleBounds = new SKRect(); 
    titlePaint.MeasureText(pdfTitle, ref titleBounds); 
    canvas.DrawText(pdfTitle, pageSize.Width / 2 - titleBounds.Width / 2, 150, titlePaint); 

    // 按商品分类分组(假设Item类有CategoryName属性,根据实际字段调整)
    var groupedItems = items.GroupBy(item => item.CategoryName).OrderBy(g => g.Key);

    var countPaint = new SKPaint 
    { 
        Typeface = SKTypeface.FromFamilyName("Arial", SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright), 
        TextSize = 18, 
        IsAntialias = true, 
        Color = SKColors.Black 
    }; 
    var categoryPaint = new SKPaint 
    { 
        Typeface = SKTypeface.FromFamilyName("Arial", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright), 
        TextSize = 20, 
        IsAntialias = true, 
        Color = new SKColor(0x2C, 0x3E, 0x50) // 深色标题
    };
    var totalPaint = new SKPaint 
    { 
        Typeface = SKTypeface.FromFamilyName("Arial", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright), 
        TextSize = 18, 
        IsAntialias = true, 
        Color = SKColors.Red 
    };

    float y = 200;
    float pageMargin = 40; // 左右边距
    float totalOverall = 0; // 总合计数量

    foreach (var categoryGroup in groupedItems)
    {
        // 绘制分类标题
        var categoryText = $"分类:{categoryGroup.Key}";
        var categoryBounds = new SKRect();
        categoryPaint.MeasureText(categoryText, ref categoryBounds);
        canvas.DrawText(categoryText, pageMargin, y, categoryPaint);
        y += 35;

        // 统计分类合计
        float categoryTotal = 0;

        // 绘制分类下的商品
        foreach (var item in categoryGroup) 
        { 
            var itemCount = await GetInventoryItemCountForDateAsync(date, item.Id); 
            string qtyText = itemCount?.InventoryItemUnitQty.ToString() ?? "N/A";
            float qtyValue = itemCount?.InventoryItemUnitQty ?? 0;
            if (qtyValue > 0) categoryTotal += qtyValue;

            var countText = $"- {item.ItemFullName}:{qtyText}"; 
            var countBounds = new SKRect(); 
            countPaint.MeasureText(countText, ref countBounds); 
            canvas.DrawText(countText, pageMargin + 20, y, countPaint); 
            y += 30; 

            // 检查是否需要分页
            if (y > pageSize.Height - 80)
            {
                document.EndPage();
                canvas.Dispose();
                canvas = document.BeginPage((int)pageSize.Width, (int)pageSize.Height);
                y = 50; // 新页起始位置
            }
        }

        // 绘制分类合计
        var totalText = $"分类合计:{categoryTotal:F2}";
        var totalBounds = new SKRect();
        totalPaint.MeasureText(totalText, ref totalBounds);
        canvas.DrawText(totalText, pageSize.Width - pageMargin - totalBounds.Width, y, totalPaint);
        totalOverall += categoryTotal;
        y += 40;

        // 检查分页
        if (y > pageSize.Height - 80)
        {
            document.EndPage();
            canvas.Dispose();
            canvas = document.BeginPage((int)pageSize.Width, (int)pageSize.Height);
            y = 50;
        }
    }

    // 绘制总合计
    var overallTotalText = $"库存总合计:{totalOverall:F2}";
    var overallTotalBounds = new SKRect();
    totalPaint.MeasureText(overallTotalText, ref overallTotalBounds);
    canvas.DrawText(overallTotalText, pageSize.Width / 2 - overallTotalBounds.Width / 2, y, totalPaint);

    document.EndPage(); 
    document.Close(); 
    stream.Position = 0; // 重置流位置
    return stream.ToArray(); // 返回PDF的byte数组
} 

2. 修改分享方法,使用byte数组生成文件

public async Task ShareInventoryItemCountPdfAsync(DateTime date, List<Item> items, string pdfLogoPath, string pdfTitle) 
{ 
    var pdfBytes = await GenerateInventoryItemCountPdfAsync(date, items, pdfLogoPath, pdfTitle).ConfigureAwait(false); 
    
    // 将byte数组写入临时文件
    var tempPath = Path.Combine(Path.GetTempPath(), "InventoryItemCount.pdf");
    await File.WriteAllBytesAsync(tempPath, pdfBytes).ConfigureAwait(false);

    var pdfFile = new ShareFile(tempPath, "application/pdf"); 
    await Share.RequestAsync(new ShareFileRequest { Title = "分享库存计数PDF", File = pdfFile }).ConfigureAwait(false); 

    // 可选:删除临时文件
    // File.Delete(tempPath);
} 

二、优化后的SQLite操作方法

public List<InventoryActivityReport> GetAllInventoryActivityReports() 
{ 
    InitAsync().Wait(); // 同步等待初始化完成
    return conn.Table<InventoryActivityReport>().ToList(); 
} 

public List<InventoryCount> GetAllInventoryCounts() 
{ 
    InitAsync().Wait();
    return conn.Table<InventoryCount>().ToList(); 
} 

public InventoryActivityReport GetInventoryActivityReport(int id) 
{ 
    InitAsync().Wait();
    return conn.Table<InventoryActivityReport>().Where(i => i.Id == id).FirstOrDefault(); 
} 

public InventoryCount GetInventoryCount(int id) 
{ 
    InitAsync().Wait();
    return conn.Table<InventoryCount>().Where(i => i.Id == id).FirstOrDefault(); 
} 

public List<InventoryCount> GetInventoryCountsByInventoryActivityReportId(int id) 
{ 
    InitAsync().Wait();
    return conn.Table<InventoryCount>().Where(i => i.InventoryActivityReportID == id).ToList(); 
} 

public List<InventoryCount> GetInventoryCountsByInventoryReport(InventoryActivityReport inventoryActivityReport) 
{ 
    InitAsync().Wait();
    return conn.Table<InventoryCount>().Where(i => i.InventoryActivityReportID == inventoryActivityReport.Id).ToList(); 
} 

public async Task<InventoryCount> GetInventoryItemCountForDateAsync(DateTime date, int id) 
{ 
    await InitAsync().ConfigureAwait(false);
    return await conn.Table<InventoryCount>() 
          .Where(ic => ic.CountedDate.Date == date.Date && ic.ItemId == id) 
          .FirstOrDefaultAsync().ConfigureAwait(false); 
} 

public InventoryCount GetInventoryItemCountForDate(DateTime date, int id) 
{ 
    InitAsync().Wait();
    var count = conn.Table<InventoryCount>() 
        .Where(ic => ic.CountedDate.Date == date.Date && ic.ItemId == id) 
        .FirstOrDefault(); 
    return count ?? new InventoryCount(); 
} 

public void UpdateInventoryCount(InventoryCount inventoryCount) 
{ 
    try 
    { 
        InitAsync().Wait();
        if (inventoryCount != null) 
        { 
            conn.Update(inventoryCount); 
        } 
    } 
    catch (Exception ex) 
    { 
        // 建议添加日志记录
        // Logger.Error(ex, "更新库存计数失败");
    } 
}

关键优化点说明

  1. 异步一致性:SQLite异步操作改用FirstOrDefaultAsync(),同步方法通过.Wait()确保初始化完成,避免连接异常。
  2. 分组统计:用LINQ的GroupBy实现分类分组,同时计算分类合计与总合计,满足报表需求。
  3. 分页处理:自动检测页面容量,内容超出时新建页面,避免内容截断。
  4. 样式区分:分类标题、商品行、合计行使用不同画笔样式,提升报表可读性。

内容的提问来源于stack exchange,提问作者Conrad Budde

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 01:14:59