使用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, "更新库存计数失败"); } }
关键优化点说明
- 异步一致性:SQLite异步操作改用
FirstOrDefaultAsync(),同步方法通过.Wait()确保初始化完成,避免连接异常。 - 分组统计:用LINQ的
GroupBy实现分类分组,同时计算分类合计与总合计,满足报表需求。 - 分页处理:自动检测页面容量,内容超出时新建页面,避免内容截断。
- 样式区分:分类标题、商品行、合计行使用不同画笔样式,提升报表可读性。
内容的提问来源于stack exchange,提问作者Conrad Budde
相关产品推荐
相关产品推荐

