ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Avalonia跨平台报表审核工具开发实战

2026/9/14 6:21:39 拓冰建站 浏览量
Avalonia跨平台报表审核工具开发实战 简介本资源是一套基于Avalonia框架开发的C#畜牧业报表审核系统源码面向.NET开发者、农业信息化系统实施人员及高校计算机专业师生旨在解决传统畜牧业报表人工审核效率低、易出错等痛点提供跨平台、可快速部署的审核工具原型。压缩包共224个文件大小46.7MB涵盖51个核心C#逻辑文件实现数据处理、审核规则与接口交互、15个Avalonia XAML界面文件构建多页面审核工作流、4个数据库文件SQLite持久化存储报表数据以及配置、图标、字体等配套资源结构完整开箱即用。已有136人学习下载资源中包含国产麒麟系统适配脚本如执行_畜牧业审核程序国产.sh.7z及多页面AXAML界面MainWindow.axaml、Page1–7.axaml等直观展现跨平台UI组织方式与畜牧业务流程映射逻辑适合学习Avalonia实战开发、行业软件架构设计及.NET桌面应用国产化适配实践。1. 为什么畜牧业报表审核要选 Avalonia 而不是 WinForms 或 WPF在基层畜牧站、养殖合作社和县级动监所一线人员常需离线核对免疫记录、饲料投喂日志、出栏检疫单等结构化报表——这些数据往往来自 Excel 导入、SQLite 本地库或轻量级 Web API但操作环境高度碎片化既有 Windows 10 专业版的台式机也有预装 LinuxUbuntu Server XFCE的老旧工控机甚至部分移动巡检终端运行 ARM64 架构的 Debian。若用 WinFormsLinux 端直接不可用WPF 仅限 Windows 且 .NET Framework 依赖重而 Avalonia 提供真正跨平台的原生渲染能力不依赖系统 UI 框架同一套 C# 代码可编译为 Windows x64/x86、Linux x64/ARM64、macOS x64/ARM64 四类二进制且内存占用比 Electron 低 60% 以上。本程序聚焦「报表审核」这一具体动作支持按耳标号快速定位单条记录、高亮显示字段级差异如疫苗批次号不一致、导出带数字签名的 PDF 审核报告、离线缓存最近 30 天数据——所有功能均基于 Avalonia 的 MVVM 模式与 ReactiveUI 集成实现避免 WinForms 中常见的 UI 线程阻塞导致的卡顿问题。适合 C# 开发者快速交付可部署到异构终端的审核工具而非仅限于开发演示原型。2. 用 Avalonia 在 Linux 和 Windows 上跑通畜牧业报表审核的最小命令集2.1 初始化项目并确认跨平台目标框架Avalonia 默认使用 .NET 6但畜牧业现场设备常受限于旧内核如 CentOS 7.9 内核 3.10需避开 .NET 8 的某些新 syscall。实际项目中我们锁定net6.0并显式指定linux-x64和win-x64RIDRuntime Identifierdotnet new avalonia.app -n LivestockAuditApp --framework net6.0 cd LivestockAuditApp提示不要用dotnet new avalonia.mvvm模板它默认引入 ReactiveUI 18而该版本在 ARM64 Linux 上存在System.Drawing.Common加载失败问题。应手动安装兼容版本dotnet add package ReactiveUI.Fody --version 17.1.35。接着修改.csproj文件显式声明目标平台并禁用不必要特性PropertyGroup TargetFrameworknet6.0/TargetFramework Nullableenable/Nullable BuiltInComInteropSupportfalse/BuiltInComInteropSupport EnableDefaultCompileItemsfalse/EnableDefaultCompileItems /PropertyGroup ItemGroup PackageReference IncludeAvalonia Version11.1.1 / PackageReference IncludeAvalonia.Desktop Version11.1.1 / PackageReference IncludeReactiveUI.Fody Version17.1.35 / PackageReference IncludeSQLitePCLRaw.bundle_green Version2.1.12 / /ItemGroup !-- 关键为 Linux 添加原生依赖 -- ItemGroup Condition$(RuntimeIdentifier) linux-x64 PackageReference IncludeAvalonia.Native.Linux Version11.1.1 / /ItemGroup2.2 编译时区分平台并生成可执行文件Avalonia 不同于传统 .NET 桌面应用其资源AXAML、字体、图标需在编译时嵌入或随发布目录部署。以下命令分别生成 Windows 和 Linux 可执行包# Windows 发布含 Avalonia 渲染器 DLL dotnet publish -c Release -r win-x64 --self-contained true /p:PublishTrimmedtrue /p:TrimModepartial # Linux 发布注意必须指定 --self-contained否则依赖系统 libSkiaSharp dotnet publish -c Release -r linux-x64 --self-contained true /p:PublishTrimmedtrue /p:TrimModepartial生成路径为bin/Release/net6.0/win-x64/publish/和bin/Release/net6.0/linux-x64/publish/。Linux 包需额外验证libSkiaSharp.so是否存在ls bin/Release/net6.0/linux-x64/publish/libSkiaSharp.so # 若缺失说明未正确引用 Avalonia.Native.Linux —— 此时需检查 .csproj 中 Condition 判断是否生效2.3 运行时验证 Avalonia 渲染器初始化状态在Program.cs中不能直接调用BuildAvaloniaApp().StartWithMainWindow()而应捕获初始化异常并输出平台诊断信息public static void Main(string[] args) { try { var app BuildAvaloniaApp(); // 关键在 Start 前检查 Skia 渲染后端 if (OperatingSystem.IsLinux()) { Console.WriteLine($Linux detected: {Environment.OSVersion.VersionString}); Console.WriteLine($Avalonia backend: {Avalonia.Controls.Platforms.PlatformManager.Current?.RenderInterface?.GetType().Name ?? Unknown}); } app.StartWithMainWindow(); } catch (Exception ex) when (ex is DllNotFoundException or InvalidOperationException) { Console.Error.WriteLine($Avalonia init failed: {ex.Message}); Console.Error.WriteLine(Check: 1) libSkiaSharp.so exists in publish dir; 2) glibc 2.17 on target Linux); Environment.Exit(1); } }注意glibc版本是 Linux 端最常见失败点。CentOS 7 默认为glibc 2.17而 Avalonia 11.1.1 编译依赖glibc 2.18。若现场机器无法升级需降级至 Avalonia 10.1.1 并使用libSkiaSharp 2.88.0已验证兼容glibc 2.17。3. 实现「查找一条记录字段数据」的核心逻辑与 AXAML 绑定技巧3.1 定义可查询的报表数据模型与 Repository 层畜牧业报表核心字段包括EarTagId耳标号主键、VaccinationDate免疫日期、VaccineBatch疫苗批次、InspectorName检疫员姓名、IsApproved是否审核通过。为支持离线查询采用 SQLite 本地存储并封装为IReportRepositorypublic interface IReportRepository { TaskReportRecord? FindByEarTagAsync(string earTag); TaskIEnumerableReportRecord SearchAsync(string keyword, DateTime? from null, DateTime? to null); } public class ReportRecord { public string EarTagId { get; set; } null!; public DateTime VaccinationDate { get; set; } public string VaccineBatch { get; set; } null!; public string InspectorName { get; set; } null!; public bool IsApproved { get; set; } public DateTime LastModified { get; set; } }关键点在于FindByEarTagAsync必须支持模糊匹配因耳标号可能录入带空格或横线且查询响应时间需控制在 50ms 内public async TaskReportRecord? FindByEarTagAsync(string earTag) { // 去除输入中的空格和横线适配不同录入习惯 var cleanTag Regex.Replace(earTag.Trim(), [\s\-], ); using var conn new SqliteConnection(_connectionString); await conn.OpenAsync(); using var cmd conn.CreateCommand(); cmd.CommandText SELECT * FROM Reports WHERE REPLACE(REPLACE(EarTagId, , ), -, ) earTag ORDER BY LastModified DESC LIMIT 1; cmd.Parameters.AddWithValue(earTag, cleanTag); using var reader await cmd.ExecuteReaderAsync(); if (await reader.ReadAsync()) { return new ReportRecord { EarTagId reader.GetString(0), VaccinationDate reader.GetDateTime(1), VaccineBatch reader.GetString(2), InspectorName reader.GetString(3), IsApproved reader.GetBoolean(4), LastModified reader.GetDateTime(5) }; } return null; }3.2 在 AXAML 中绑定搜索框并响应字段级高亮Avalonia 的TextBox支持TextChanging事件比TextChanged更适合防抖查询。在MainView.axaml中定义搜索区域StackPanel Margin10 TextBlock Text输入耳标号查找 FontSize14/ TextBox x:NameSearchBox Width300 Height32 Classessearch-input TextChangingOnSearchTextChanged/ Button Content清空结果 Command{Binding ClearResultCommand} Margin0,5,0,0/ /StackPanel !-- 结果显示区使用 DataGrid 绑定单条记录 -- DataGrid x:NameResultGrid AutoGenerateColumnsFalse CanUserSortFalse IsReadOnlyTrue Items{Binding SearchResult} DataGrid.Columns DataGridTextColumn Header耳标号 Binding{Binding EarTagId} Width150/ DataGridTextColumn Header免疫日期 Binding{Binding VaccinationDate, StringFormatyyyy-MM-dd} Width120/ DataGridTextColumn Header疫苗批次 Binding{Binding VaccineBatch} Width180/ DataGridTextColumn Header检疫员 Binding{Binding InspectorName} Width120/ DataGridCheckBoxColumn Header已审核 Binding{Binding IsApproved} Width80/ /DataGrid.Columns /DataGrid对应 ViewModel 中实现防抖逻辑使用Observable.AsTask()避免 UI 线程阻塞private readonly TimeSpan _debounceDelay TimeSpan.FromMilliseconds(300); private IDisposable? _searchSubscription; public void OnSearchTextChanged(object sender, TextChangedEventArgs e) { _searchSubscription?.Dispose(); // 取消上一次未完成的查询 var text e.Source as TextBox; if (string.IsNullOrWhiteSpace(text?.Text)) return; _searchSubscription Observable .Timer(_debounceDelay) .Take(1) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ ExecuteSearch(text.Text)); } private async void ExecuteSearch(string keyword) { try { IsLoading true; SearchResult await _repository.FindByEarTagAsync(keyword); OnPropertyChanged(nameof(SearchResult)); } finally { IsLoading false; } }3.3 字段级差异高亮用 Style 触发器动态着色当用户输入耳标号后程序需对比当前记录与数据库中最新历史记录如有将变更字段标红。此功能不依赖第三方库纯 AXAML 实现!-- 在 DataGrid 内部为 VaccineBatch 列添加样式 -- DataGridTextColumn Header疫苗批次 Width180 DataGridTextColumn.CellTemplate DataTemplate TextBlock Text{Binding VaccineBatch} Foreground{Binding IsVaccineBatchChanged, Converter{StaticResource BoolToBrushConverter}, ConverterParameterRed}/ /DataTemplate /DataGridTextColumn.CellTemplate /DataGridTextColumn对应 ViewModel 中计算差异public bool IsVaccineBatchChanged OriginalRecord ! null !string.Equals(OriginalRecord.VaccineBatch, CurrentRecord.VaccineBatch, StringComparison.Ordinal);提示BoolToBrushConverter需继承IValueConverter返回new SolidColorBrush(Colors.Red)。避免在 Converter 中创建新 Brush 实例引发内存泄漏应复用静态资源。4. 解决「Avalonia 每次打开界面内存都会上涨」的三个必调参数4.1 禁用 Avalonia 默认的资源缓存策略Avalonia 为提升 UI 渲染性能默认启用BitmapCache和GlyphTypefaceCache但在频繁打开/关闭审核窗口如逐条查耳标时这些缓存不会自动释放导致内存持续增长。需在App.xaml.cs中全局关闭public override void Initialize() { AvaloniaXamlLoader.Load(this); // 关键禁用位图与字形缓存 AvaloniaLocator.CurrentMutable .BindIResourceProvider().ToConstant(new DefaultResourceProvider()); // 强制使用无缓存的 Skia 渲染器 AvaloniaLocator.CurrentMutable .BindIRendererFactory().ToSingletonSkiaRendererFactory(); }并在SkiaRendererFactory中注入自定义配置public class SkiaRendererFactory : IRendererFactory { public IRenderer CreateRenderer(IWindowBase window) { var skiaOptions new SkiaOptions { // 关键参数1禁用 GPU 缓存Linux ARM64 显存有限 UseGpu false, // 关键参数2限制位图缓存大小默认 100MB设为 5MB BitmapCacheSize 5 * 1024 * 1024, // 关键参数3禁用字形缓存中文报表无需高频字形重绘 GlyphCacheSize 0 }; return new SkiaRenderer(window, skiaOptions); } }4.2 控制窗口生命周期避免隐式引用导致 GC 失效Avalonia 窗口关闭时若 ViewModel 中持有IDisposable资源如数据库连接、定时器且未显式释放会导致整个窗口对象无法被 GC 回收。必须在 ViewModel 中实现IDisposable并在窗口关闭时触发public class AuditViewModel : ReactiveObject, IDisposable { private readonly IDisposable _disposable; private bool _disposed false; public AuditViewModel() { // 使用 Observable.Timer 创建定时器时必须用 CompositeDisposable 管理 _disposable new CompositeDisposable { this.WhenAnyValue(x x.SearchText) .Throttle(TimeSpan.FromMilliseconds(300)) .Subscribe(_ ExecuteSearch()), // 其他订阅... }; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_disposed) { if (disposing) { _disposable?.Dispose(); // 关键释放所有 Observable 订阅 _repository?.Dispose(); // 释放数据库连接 } _disposed true; } } }在MainView.axaml.cs中确保窗口关闭时调用 ViewModel 的Disposepublic partial class MainView : Window { public MainView() { InitializeComponent(); this.Closing OnWindowClosing; } private void OnWindowClosing(object sender, WindowClosingEventArgs e) { if (DataContext is IDisposable disposable) { disposable.Dispose(); } } }4.3 监控内存泄漏用 dotnet-dump 分析 Linux 端真实占用Windows 端可用 Visual Studio 诊断工具但 Linux 端需命令行验证。在目标机器上运行发布版程序后获取进程 ID 并抓取堆快照# 启动程序并记录 PID ./LivestockAuditApp echo $! /tmp/audit.pid # 等待 5 分钟模拟多次开窗操作 sleep 300 # 抓取 dump dotnet-dump collect -p $(cat /tmp/audit.pid) -o /tmp/audit_dump_$(date %s).dump # 分析对象计数重点关注 Avalonia.Controls.Control 和 SkiaSharp.SKImage dotnet-dump analyze /tmp/audit_dump_*.dump --command dumpheap -stat | grep -E (Control|SKImage|AXAML)若输出中Avalonia.Controls.Control实例数持续增长如从 120 增至 350说明控件未被释放此时应检查DataGrid是否绑定了未取消的INotifyCollectionChanged事件或UserControl是否在InitializeComponent()中重复注册了Loaded事件。5. 将文本和图片分层输出到 PDF用 iText7 生成带电子签名的审核报告5.1 在 Avalonia 中集成 iText7 并规避字体嵌入问题畜牧业报表需导出 PDF 供签字归档但 iText7 默认嵌入完整字体如 Noto Sans CJK单个 PDF 增大 10MB。实际只需支持简体中文常用字GB2312 范围应使用子集嵌入// 创建 PDF writer 时指定字体提供器 var writer new PdfWriter(outputPath); var pdfDoc new PdfDocument(writer); var doc new Document(pdfDoc); // 关键使用子集化字体仅嵌入实际用到的汉字 var fontProgram FontProgramFactory.CreateFont( /usr/share/fonts/truetype/wqy/wqy-microhei.ttc, // Linux 系统字体路径 PdfFontConstants.IDENTITY_H, true); // true 表示启用子集嵌入 var font PdfFontFactory.CreateFont(fontProgram, GBK); doc.SetFont(font);注意Windows 下路径为C:\Windows\Fonts\simhei.ttf需在代码中判断OperatingSystem.IsWindows()动态切换。若目标系统无中文字体需将wqy-microhei.ttc随程序发布并在publish目录中指定相对路径。5.2 分层绘制先背景水印再表格最后电子签名矩形框审核报告需包含三层次内容浅灰「审核专用」水印、居中报表表格、右下角签名区域。iText7 支持Canvas分层操作var pageSize PageSize.A4; var pdfDoc new PdfDocument(new PdfWriter(outputPath)); var doc new Document(pdfDoc, pageSize); // 第一层添加水印置于底层 var canvas new Canvas(doc.GetPdfDocument().GetFirstPage(), pageSize); canvas.SetFillColor(ColorConstants.LIGHT_GRAY); canvas.SetFontSize(60); canvas.ShowTextAligned(审核专用, pageSize.GetWidth() / 2, pageSize.GetHeight() / 2, 1, TextAlignment.CENTER, VerticalAlignment.MIDDLE, 45); // 第二层插入报表表格居中 var table new Table(UnitValue.CreatePercentArray(new float[] { 1, 1, 1, 1 })).UseAllAvailableWidth(); table.AddCell(new Cell().Add(new Paragraph(耳标号).SetBold())); table.AddCell(new Cell().Add(new Paragraph(免疫日期))); table.AddCell(new Cell().Add(new Paragraph(疫苗批次))); table.AddCell(new Cell().Add(new Paragraph(审核状态))); // 第三层签名区域绝对定位不随表格滚动 var signatureRect new Rectangle(400, 50, 180, 60); // x, y, width, height var signatureCanvas new Canvas(pdfDoc.GetFirstPage(), signatureRect); signatureCanvas.Add(new Paragraph(审核人签字).SetFontSize(12)); signatureCanvas.StrokeColor ColorConstants.BLACK; signatureCanvas.Rectangle(0, 0, 180, 60); signatureCanvas.Stroke(); doc.Close();5.3 添加数字签名用 BouncyCastle 签署 PDF 并验证签名有效性畜牧业数据需满足《电子签名法》要求iText7 支持 PAdES-LT长期有效签名。私钥应存储于 HSM 或加密文件此处以 PEM 格式为例// 读取私钥生产环境应从安全模块加载 var keyBytes File.ReadAllBytes(/etc/audit/private_key.pem); var keyPair GetKeyPairFromPem(keyBytes); // 创建签名外观 var appearance new PdfSignatureAppearance(pdfDoc.GetFirstPage()); appearance.SetReason(畜牧业报表审核); appearance.SetLocation(中国某省某县); appearance.SetContact(auditlivestock.gov.cn); // 执行签名 var signer new PdfSigner(pdfDoc, new FileStream(outputPath .signed.pdf, FileMode.Create), new StampingProperties()); signer.SignDetached(appearance, keyPair.Private, GetCertificateChain(), null, null, null, 0, CryptoStandard.CMS);验证签名有效性时不依赖 Adobe Reader而是用 iText7 自检using var signedPdf new PdfReader(outputPath .signed.pdf); var signedDoc new PdfDocument(signedPdf); var verification new PdfPKCS7(signedDoc.GetAcroForm().GetField(Signature1).GetValue().GetAsString().GetValue()); Console.WriteLine($签名时间: {verification.GetSignDate()}); Console.WriteLine($证书颁发者: {verification.GetCertificate().GetIssuerDN()}); Console.WriteLine($签名有效: {verification.VerifySignature()});提示VerifySignature()返回true仅表示签名数学有效还需调用verification.GetCRLs()检查证书吊销状态畜牧业系统通常离线运行故采用 OCSP Stapling 预加载方式在签名前缓存 OCSP 响应并嵌入 PDF。本文还有配套的精品资源点击获取