用相同的 sgcUI API 从 WinForms 和 WPF 迁移到 Avalonia
sgcUI 提供适用于 WinForms、WPF 和 Avalonia 的原生控件,它们在每个产品上共用同一个模型。网格列、图表、计划器数据和主题只需编写一次,就能原样编译到每个宿主上,因此把界面迁移到 Avalonia,也就是迁移到 macOS 和 Linux,意味着改变窗口代码和控件类型,而不是界面逻辑。本套件展示具体做法,代码来自一个可以编译并在三个宿主上都通过自检的示例。
sgcUI 提供适用于 WinForms、WPF 和 Avalonia 的原生控件,它们在每个产品上共用同一个模型。网格列、图表、计划器数据和主题只需编写一次,就能原样编译到每个宿主上,因此把界面迁移到 Avalonia,也就是迁移到 macOS 和 Linux,意味着改变窗口代码和控件类型,而不是界面逻辑。本套件展示具体做法,代码来自一个可以编译并在三个宿主上都通过自检的示例。
带有网格、图表和计划器的业务界面,一个您想保留的数据层,以及不想在新的 UI 框架中重写界面逻辑。
WinForms 只能在 Windows 上运行。使用 sgcUI 时,您现在使用的 TsgcWinForms* 控件有成员相同的 TsgcAvalonia* 对应项。配置代码移到共享类库,窗体变成 Avalonia 窗口,DataSource 变成 ItemsSource。
WPF 也只能在 Windows 上运行。它的 sgcUI 控件与 Avalonia 最为接近:两者都用 ItemsSource 绑定,都在 XAML 中用同一个 sgc 命名空间 URI 声明,主要的代码变化是 UI 线程调用。
现有 WPF 应用的常规路线是 Avalonia XPF,其厂商将其描述为“二进制兼容的 WPF 运行时”和“在托管接口层面保持 API 与二进制兼容的 WPF 分支”。撰写本文时(2026-10-20),XPF 按应用销售永久许可证,含十二个月的更新和支持:Internal 为每个应用 9,500 欧元(仅部署给您自己的员工),Business 为每个应用 29,500 欧元(商业部署到 macOS 和桌面 Linux,嵌入式 Linux 另加 21,250 欧元),Enterprise 面向移动端或浏览器,每个应用 124,500 欧元起。第三方 WPF 套件可以在附带条件的情况下运行于 XPF:XPF 第三方库说明指出,Win32 API shim“DevExpress 控件需要”,部分 DevExpress 控件依赖 System.Drawing.Common(GDI+),“在非 Windows 平台上已弃用”,Linux 上的 DevExpress 控件需要 libgdiplus,并且“问题最常出现在使用 GDI 或 WinForms 组件的控件上”。Avalonia 本身是免费的、MIT 许可的 UI 框架。.NET 版 sgcUI 按开发者授权,.NET Standard 版价格从 €380 起,提供 WinForms、WPF 和 Avalonia 的完整 C# 源代码,二进制可免费再分发,不收取按应用计算的费用。免费的社区版软件包可在 NuGet 上获取。
每个 sgcUI 产品都有一个对宿主一无所知的模型。WinForms、WPF 和 Avalonia 控件是它之上的轻薄视图。
TsgcGridColumns、TsgcGridData、TsgcChart、TsgcPlannerData、TsgcPlannerSchedulerOptions、TsgcPivotEngine 和 TsgcDiagramDocument 在三个宿主上是相同的类型。针对它们编写的代码可以原样编译到每个宿主上。
Columns、Data、ShowFooter、GroupSummaries、GroupBy、SaveLayoutToString、LoadLayoutFromString、Chart、Options、View、Date、PrintSettings、CreatePrintSource、Print、ShowPrintPreview 和 ExportToPDF,以及 OnCellClick、OnSelectionChange 和 OnSortChange 等网格事件。
用 SaveLayoutToString 保存的网格布局在每个宿主上都是相同的 JSON 文本。在示例中,三个宿主把各自的布局写入文件,这三个文件的 SHA-256 完全相同。主题(TsgcUITheme、sgcUIApplication.Theme、sgcUIApplication.AccentColor、高对比度)也是共享的。
经验法则:把 TsgcWinForms 或 TsgcWpf 前缀换成 TsgcAvalonia。例外情况已在表中标出。
| 产品 | WinForms | WPF | Avalonia |
|---|---|---|---|
| sgcGrid | TsgcWinFormsGrid | TsgcWpfGrid | TsgcAvaloniaGrid |
| sgcGrid 声明的列 | 在代码中,Columns.Add | TsgcWpfGridColumn(XAML) | TsgcAvaloniaGridColumn(XAML) |
| sgcCharts | TsgcWinFormsChart, TsgcWinFormsChartEditor | TsgcWpfChart, TsgcWpfChartEditor | TsgcAvaloniaChart, TsgcAvaloniaChartEditor |
| sgcPivotGrid | TsgcWinFormsPivotGrid, TsgcWinFormsPivotFieldList | TsgcWpfPivotGrid, TsgcWpfPivotFieldList | TsgcAvaloniaPivotGrid, TsgcAvaloniaPivotFieldList |
| sgcPlanner | TsgcWinFormsScheduler, TsgcWinFormsGantt, TsgcWinFormsKanban | TsgcWpfScheduler, TsgcWpfGantt, TsgcWpfKanban | TsgcAvaloniaScheduler, TsgcAvaloniaGantt, TsgcAvaloniaKanban |
| sgcDiagram | TsgcWinFormsDiagramView, TsgcWinFormsDiagramPalette, TsgcWinFormsDiagramMinimap | TsgcWpfDiagramView, TsgcWpfDiagramPalette, TsgcWpfDiagramMinimap | TsgcAvaloniaDiagramView, TsgcAvaloniaDiagramPalette, TsgcAvaloniaDiagramMinimap |
| sgcGauges | TsgcWinForms<Kind>Gauge | TsgcWpf<Kind>Gauge | TsgcAvalonia<Kind>Gauge |
| Kind 为 Radial、Linear、Digital、Compass、Clock、LED、LEDBar、ProgressRing、VUMeter 或 Annunciator。 | |||
| sgcDashboard | TsgcWinFormsDashboard | TsgcWpfDashboard | TsgcAvaloniaDashboard |
| sgcDashboard 磁贴 | TsgcWinFormsDashboardTile、...ChartTile、...GridTile、...GaugeTile、...PivotTile、TsgcWinFormsKPICard 和 TsgcWinFormsDashboardMapTile | TsgcWpfDashboardTile、...ChartTile、...GridTile、...GaugeTile、...PivotTile、TsgcWpfKPICard。没有地图磁贴。 | TsgcAvaloniaDashboardTile、...ChartTile、...GridTile、...GaugeTile、...PivotTile、TsgcAvaloniaKPICard。没有地图磁贴。 |
| sgcMaps | SgcMapControl(命名空间 esegece.sgcMaps.WinForms) | TsgcWpfMap | TsgcAvaloniaMap |
| sgcEditors | TsgcWinForms<Editor> | TsgcWpf<Editor> | TsgcAvalonia<Editor> |
| Editor 为 ButtonEdit、CurrencyEdit、MaskEdit、SpinEdit、DatePicker、DateTimePicker、TimePicker、LookupEdit、SearchEdit、TokenEdit、Memo、ColorPicker、MonthCalendar、RadioGroup、CheckGroup、Rating 或 RangeSlider。 | |||
| sgcNavigation | TsgcWinFormsNavigationBar, ...NavigationTabs, ...NavigationBreadcrumb, ...DockManager | TsgcWpfNavigationBar, ...NavigationTabs, ...NavigationBreadcrumb, ...DockManager | TsgcAvaloniaNavigationBar, ...NavigationTabs, ...NavigationBreadcrumb, ...DockManager |
| sgcToolbar | TsgcWinFormsRibbon, TsgcWinFormsToolbar, TsgcWinFormsStatusBar | TsgcWpfRibbon, TsgcWpfToolbar, TsgcWpfStatusBar | TsgcAvaloniaRibbon, TsgcAvaloniaToolbar, TsgcAvaloniaStatusBar |
| sgcWizard | TsgcWinFormsWizard, TsgcWinFormsStepper | TsgcWpfWizard, TsgcWpfStepper | TsgcAvaloniaWizard, TsgcAvaloniaStepper |
| sgcRichEdit | TsgcWinFormsRichEdit | TsgcWpfRichEdit | TsgcAvaloniaRichEdit |
| sgcPDF 查看器 | TsgcPDFViewerControl | TsgcWpfPDFViewer | TsgcAvaloniaPDFViewer |
| sgcReports 设计器 | TsgcWinFormsReportDesigner | TsgcWpfReportDesigner | TsgcAvaloniaReportDesigner |
| sgcReports 预览 | TsgcReportPreviewVCL(WinForms Panel) | TsgcWpfReportPreview | TsgcAvaloniaReportPreview |
| sgcUICore | TsgcWinFormsAIQueryBar, TsgcWinFormsValidationSummary | TsgcWpfAIQueryBar, TsgcWpfValidationSummary | TsgcAvaloniaAIQueryBar, TsgcAvaloniaValidationSummary |
每个产品一个命名空间(esegece.sgcGrid、esegece.sgcCharts 等),三个宿主相同,只有 WinForms 地图例外,它位于 esegece.sgcMaps.WinForms。宿主控件位于软件包 esegece.<Product>.WinForms、.Wpf 和 .Avalonia 中;仪表板的 WinForms 地图磁贴位于 esegece.sgcDashboard.Maps.WinForms。在 WPF 和 Avalonia 上,除 sgcReports 外的每个产品都用同一个命名空间 URI 在 XAML 中声明,即 xmlns:sgc="https://www.esegece.com/sgcui";sgcReports 请使用 clr-namespace 映射。
控件上的其他一切都保持原名。下面是宿主会显现出来的地方。
| 事项 | WinForms | WPF | Avalonia |
|---|---|---|---|
| 把列表绑定到网格 | DataSource(以及 DataMember) | ItemsSource(DependencyProperty) | ItemsSource(StyledProperty) |
| 网格绑定成员 | AutoGenerateColumns、KeyMember、SelectedItem、ItemsBinding 和 ItemsWriteBack(注册版),三个宿主相同 | ||
| 当前项同步 | 无 | IsSynchronizedWithCurrentItem | 无 |
| 网格列和数据 | Columns (TsgcGridColumns), Data (TsgcGridData) | ||
| 图表模型 | Chart (TsgcChart) | ||
| 计划器模型 | Data (TsgcPlannerData), Options (TsgcPlannerSchedulerOptions) | ||
| 主题 | 控件的 Theme(TsgcUITheme;utDefault 跟随 sgcUIApplication.Theme) | 相同,另外还有 sgcUICore_Theme_Avalonia.sgcUIApplyThemeToWindow | |
| UI 线程 | Control.BeginInvoke | Dispatcher.BeginInvoke | Dispatcher.UIThread.Post |
| 应用启动 | Application.Run(form) | new Application().Run(window) | 带主题的 Application 类,以及 StartWithClassicDesktopLifetime |
| 应用的目标框架 | net8.0-windows(也可以是 net472) | net8.0-windows(也可以是 net472) | net8.0, net9.0, net10.0 |
每个片段都复制自 MigrateToAvalonia 示例,该示例可在 Release 下为注册版和社区版编译,0 个警告,并在 WinForms、WPF 和 Avalonia 上通过自检。在宿主文件中,标有 HOST 的行在三个宿主之间不同,标有 SAME 的行在所有宿主中都相同。
共享类库只引用模型包 esegece.sgcGrid、esegece.sgcCharts、esegece.sgcPlanner 和 esegece.sgcUICore,从不引用宿主包。带分组和汇总的网格列针对 TsgcGridColumns 配置:
// the grid columns, grouped by Region, with a sum of Amount in the
// footer and in every group row; TsgcGridColumns is the Columns of
// TsgcWinFormsGrid, TsgcWpfGrid and TsgcAvaloniaGrid
public static void ConfigureGrid(TsgcGridColumns aColumns)
{
aColumns.BeginUpdate();
try
{
aColumns.Clear();
aColumns.Add(nameof(TOrder.Id), "Order", TsgcGridDataType.gdtInteger).Width = 80;
aColumns.Add(nameof(TOrder.Customer), "Customer").Width = 180;
aColumns.Add(nameof(TOrder.Region), "Region").GroupIndex = 0;
TsgcGridColumn oDate = aColumns.Add(nameof(TOrder.Date), "Date", TsgcGridDataType.gdtDateTime);
oDate.Width = 110;
oDate.DisplayFormat = "yyyy-mm-dd";
TsgcGridColumn oAmount = aColumns.Add(nameof(TOrder.Amount), "Amount", TsgcGridDataType.gdtFloat);
oAmount.Width = 120;
oAmount.DisplayFormat = "#,##0.00";
oAmount.SummaryType = TsgcGridSummaryType.gsuSum;
oAmount.SummaryFormat = "\"Total: \"#,##0.00";
aColumns.Add(nameof(TOrder.Status), "Status").Width = 100;
}
finally
{
aColumns.EndUpdate();
}
}图表是一个 TsgcChart 模型,它的饼图系列通过 TsgcChartDataSource_Items 由 ObservableCollection 实时提供数据:
// the chart model: one pie series fed by RegionTotals through the
// items data source (live: a changed Amount updates its slice)
Chart = new TsgcChart();
Chart.Title.Text = "Amount by region";
Chart.Title.Visible = true;
Chart.Legend.Position = TsgcChartLegendPosition.clpTop;
Pie = new TsgcChartSeries_Pie();
Pie.Title = "Amount";
Pie.Chart = Chart;
FChartSource = new TsgcChartDataSource_Items();
FChartSource.Series = Pie;
FChartSource.LabelMember = nameof(TRegionTotal.Region);
FChartSource.YMember = nameof(TRegionTotal.Amount);
FChartSource.Items = RegionTotals;应用中所有 sgcUI 控件共用一个主题:
// the theme of every sgcUI control of the application (their Theme is
// utDefault, so they follow sgcUIApplication.Theme), and one accent
public static void ApplyTheme()
{
sgcUIApplication.Theme = TsgcUITheme.utLight;
sgcUIApplication.AccentColor = 0xFF2563EB;
}绑定的集合在 UI 线程上变化。共享层并不了解宿主,因此它把投递调用作为委托接收:
// aPost runs an action on the UI thread; it is the only host specific
// service of the layer (WinForms Control.BeginInvoke, WPF
// Dispatcher.BeginInvoke, Avalonia Dispatcher.UIThread.Post)
public TMigrateModel(Action<Action> aPost)// a feed on a worker thread: the order is built there and posted to the
// UI thread with aPost, because bound collections change on the UI thread
public Task AddOrderFromWorker()
{
return Task.Run(() =>
{
TOrder oOrder = DoNewOrder();
FPost(() => DoAdd(oOrder));
});
}迁移前:WinForms 或 WPF 的类型。迁移后:Avalonia 的类型。
// HOST: the WinForms control types
private readonly TsgcWinFormsGrid FGrid;
private readonly TsgcWinFormsChart FChart;
private readonly TsgcWinFormsScheduler FScheduler;// HOST: the WPF control types
private readonly TsgcWpfGrid FGrid;
private readonly TsgcWpfChart FChart;
private readonly TsgcWpfScheduler FScheduler;// HOST: the Avalonia control types
private readonly TsgcAvaloniaGrid FGrid;
private readonly TsgcAvaloniaChart FChart;
private readonly TsgcAvaloniaScheduler FScheduler;有两行标有 HOST:UI 线程调用和列表绑定。其余部分在三个文件中是相同的文本。
// HOST: WinForms reaches the UI thread with Control.BeginInvoke
FModel = new TMigrateModel(aAction => BeginInvoke(aAction));
// SAME on every host: the shared columns, footer and group sums
TMigrateModel.ConfigureGrid(FGrid.Columns);
FGrid.ShowFooter = true;
FGrid.GroupSummaries = true;
// HOST: WinForms binds with DataSource (WPF and Avalonia: ItemsSource)
FGrid.DataSource = FModel.Orders;
// SAME on every host: the shared chart model and planner data
FChart.Chart = FModel.Chart;
TMigrateModel.ConfigureScheduler(FScheduler.Options);
FScheduler.Data = FModel.Planner;
FScheduler.View = TsgcPlannerSchedulerViewKind.psvWorkWeek;
FScheduler.Date = TMigrateModel.CS_WEEK;// HOST: WPF reaches the UI thread with Dispatcher.BeginInvoke
FModel = new TMigrateModel(aAction => Dispatcher.BeginInvoke(aAction));
// SAME on every host: the shared columns, footer and group sums
TMigrateModel.ConfigureGrid(FGrid.Columns);
FGrid.ShowFooter = true;
FGrid.GroupSummaries = true;
// HOST: WPF binds with ItemsSource (WinForms: DataSource)
FGrid.ItemsSource = FModel.Orders;
// SAME on every host: the shared chart model and planner data
FChart.Chart = FModel.Chart;
TMigrateModel.ConfigureScheduler(FScheduler.Options);
FScheduler.Data = FModel.Planner;
FScheduler.View = TsgcPlannerSchedulerViewKind.psvWorkWeek;
FScheduler.Date = TMigrateModel.CS_WEEK;// HOST: Avalonia reaches the UI thread with Dispatcher.UIThread.Post
FModel = new TMigrateModel(aAction => Dispatcher.UIThread.Post(aAction));
// SAME on every host: the shared columns, footer and group sums
TMigrateModel.ConfigureGrid(FGrid.Columns);
FGrid.ShowFooter = true;
FGrid.GroupSummaries = true;
// HOST: Avalonia binds with ItemsSource, as WPF (WinForms: DataSource)
FGrid.ItemsSource = FModel.Orders;
// SAME on every host: the shared chart model and planner data
FChart.Chart = FModel.Chart;
TMigrateModel.ConfigureScheduler(FScheduler.Options);
FScheduler.Data = FModel.Planner;
FScheduler.View = TsgcPlannerSchedulerViewKind.psvWorkWeek;
FScheduler.Date = TMigrateModel.CS_WEEK;启动代码天然属于宿主代码。主题调用在三个宿主上相同。
// HOST: the WinForms application start
Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// SAME on every host: one theme for every sgcUI control
TMigrateModel.ApplyTheme();
TMigrateForm oForm = new TMigrateForm(vTest ? vFolder : null);
Application.Run(oForm);
return oForm.ExitCode;// HOST: the WPF application start
Application oApp = new Application { ShutdownMode = ShutdownMode.OnMainWindowClose };
// SAME on every host: one theme for every sgcUI control
TMigrateModel.ApplyTheme();
TMigrateWindow oWindow = new TMigrateWindow(vTest ? vFolder : null);
oApp.Run(oWindow);
return oWindow.ExitCode;// HOST: Avalonia needs an Application class with a theme
internal sealed class TMigrateApp : Application
{
public static string TestFolder;
public override void Initialize()
{
Styles.Add(new FluentTheme());
RequestedThemeVariant = ThemeVariant.Light;
}
public override void OnFrameworkInitializationCompleted()
{
// SAME on every host: one theme for every sgcUI control
TMigrateModel.ApplyTheme();
IClassicDesktopStyleApplicationLifetime oDesktop =
ApplicationLifetime as IClassicDesktopStyleApplicationLifetime;
if (oDesktop != null)
oDesktop.MainWindow = new TMigrateWindow(TestFolder);
base.OnFrameworkInitializationCompleted();
}
}每一步之后,Windows 应用都能继续工作,因此可以一次迁移一个界面。
把数据和控件配置移到只引用 sgcUI 模型包(esegece.sgcGrid、esegece.sgcCharts、esegece.sgcPlanner、esegece.sgcUICore)的类库中。针对 TsgcGridColumns、TsgcChart、TsgcPlannerData 和 TsgcPlannerSchedulerOptions 编写。
让现有的 WinForms 或 WPF 界面调用该库。它在 Windows 上照常工作。
一个包含 Avalonia.Desktop、Avalonia.Themes.Fluent 以及所需的 esegece.<Product>.Avalonia 软件包的 net8.0 项目。
TsgcWinForms* 和 TsgcWpf* 变成 TsgcAvalonia*,映射表中列出的例外除外。
带 TableLayoutPanel 的 Form,或带 Grid 的 WPF Window,变成带 Grid 的 Avalonia Window。WPF 的 XAML 移到 .axaml,并保留 sgc 命名空间 URI。
把 DataSource 改为 ItemsSource。WPF 代码已经在使用 ItemsSource。
BeginInvoke 或 Dispatcher.BeginInvoke 变成 Dispatcher.UIThread.Post。
在每个宿主上运行相同的检查,就像示例的 /selftest 所做的那样,并比较保存的网格布局 JSON:它必须是相同的文本。
规划迁移之前请先阅读这些内容。
TsgcWinFormsDashboardMapTile)。WPF 和 Avalonia 仪表板没有地图磁贴。SgcMapControl,PDF 查看器是 TsgcPDFViewerControl,报表预览是 TsgcReportPreviewVCL。IsSynchronizedWithCurrentItem 在 Avalonia 网格上没有对应项。{Binding Orders} 这样的 XAML 绑定在两者上都有效。WinForms 使用普通属性和 DataSource。lp。打印成员(PrintSettings、CreatePrintSource、Print、ShowPrintPreview、ExportToPDF)是相同的。ItemsWriteBack,以及图表设计器的 NodesSource 和 LinksSource。一个界面,包含按区域分组的 40 个订单的网格、按区域显示金额的饼图,以及工作周计划器,由三个宿主基于同一个共享层显示。
在 sgcUI .NET 演示中,位于 sgcUI_Demos.sln 解决方案的 Suite\MigrateToAvalonia 文件夹:Shared、WinForms、Wpf、Avalonia 和一个 readme.html。在演示下载包中,项目引用 sgcUI NuGet 软件包。
dotnet build Avalonia\sgcUIMigrate.Avalonia.csproj -c Release,WinForms\sgcUIMigrate.WinForms.csproj 和 Wpf\sgcUIMigrate.Wpf.csproj 同理。社区版请添加 -p:SgcCommunity=true。Avalonia 宿主也可以在 macOS 和 Linux 上运行。
sgcUIMigrate.<Host>.exe /selftest [folder] 检查行、分组、饼图、计划器、在工作线程上添加的订单和被删除的订单,写出 migrate_selftest_<host>.txt 和 migrate_layout_<host>.json,并以失败检查的数量退出。三个宿主上全部 13 项检查都通过,三个布局文件逐字节相同。
公平地说,两条路线各有所长:XPF 旨在让现有 WPF 应用在 macOS 和 Linux 上基本保持原有代码和控件运行,而 sgcUI 不做这件事。使用 sgcUI 时,您要把每个界面的窗口代码迁移到 Avalonia,界面中 sgcUI 的部分原样沿用。使用其他厂商控件构建的界面,需要先在 Windows 上替换这些控件,然后才能迁移。如果您的界面正在使用或可以迁移到 sgcUI 的网格、图表、计划器、透视表、图表设计器和上面的其他产品,并且希望获得带完整源代码、中间没有运行时层的原生 Avalonia 控件,那么 sgcUI 是合适的选择。产品和版本请参阅 sgcUI 产品页面。