用相同的 sgcUI API 从 WinForms 和 WPF 迁移到 Avalonia

sgcUI 提供适用于 WinForms、WPF 和 Avalonia 的原生控件,它们在每个产品上共用同一个模型。网格列、图表、计划器数据和主题只需编写一次,就能原样编译到每个宿主上,因此把界面迁移到 Avalonia,也就是迁移到 macOS 和 Linux,意味着改变窗口代码和控件类型,而不是界面逻辑。本套件展示具体做法,代码来自一个可以编译并在三个宿主上都通过自检的示例。

必须在 macOS 和 Linux 上运行的 Windows 桌面应用

带有网格、图表和计划器的业务界面,一个您想保留的数据层,以及不想在新的 UI 框架中重写界面逻辑。

从 WinForms 迁移

WinForms 只能在 Windows 上运行。使用 sgcUI 时,您现在使用的 TsgcWinForms* 控件有成员相同的 TsgcAvalonia* 对应项。配置代码移到共享类库,窗体变成 Avalonia 窗口,DataSource 变成 ItemsSource。

从 WPF 迁移

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
sgcGridTsgcWinFormsGridTsgcWpfGridTsgcAvaloniaGrid
sgcGrid 声明的列在代码中,Columns.AddTsgcWpfGridColumn(XAML)TsgcAvaloniaGridColumn(XAML)
sgcChartsTsgcWinFormsChart, TsgcWinFormsChartEditorTsgcWpfChart, TsgcWpfChartEditorTsgcAvaloniaChart, TsgcAvaloniaChartEditor
sgcPivotGridTsgcWinFormsPivotGrid, TsgcWinFormsPivotFieldListTsgcWpfPivotGrid, TsgcWpfPivotFieldListTsgcAvaloniaPivotGrid, TsgcAvaloniaPivotFieldList
sgcPlannerTsgcWinFormsScheduler, TsgcWinFormsGantt, TsgcWinFormsKanbanTsgcWpfScheduler, TsgcWpfGantt, TsgcWpfKanbanTsgcAvaloniaScheduler, TsgcAvaloniaGantt, TsgcAvaloniaKanban
sgcDiagramTsgcWinFormsDiagramView, TsgcWinFormsDiagramPalette, TsgcWinFormsDiagramMinimapTsgcWpfDiagramView, TsgcWpfDiagramPalette, TsgcWpfDiagramMinimapTsgcAvaloniaDiagramView, TsgcAvaloniaDiagramPalette, TsgcAvaloniaDiagramMinimap
sgcGaugesTsgcWinForms<Kind>GaugeTsgcWpf<Kind>GaugeTsgcAvalonia<Kind>Gauge
Kind 为 Radial、Linear、Digital、Compass、Clock、LED、LEDBar、ProgressRing、VUMeter 或 Annunciator。
sgcDashboardTsgcWinFormsDashboardTsgcWpfDashboardTsgcAvaloniaDashboard
sgcDashboard 磁贴TsgcWinFormsDashboardTile、...ChartTile、...GridTile、...GaugeTile、...PivotTile、TsgcWinFormsKPICard 和 TsgcWinFormsDashboardMapTileTsgcWpfDashboardTile、...ChartTile、...GridTile、...GaugeTile、...PivotTile、TsgcWpfKPICard。没有地图磁贴。TsgcAvaloniaDashboardTile、...ChartTile、...GridTile、...GaugeTile、...PivotTile、TsgcAvaloniaKPICard。没有地图磁贴。
sgcMapsSgcMapControl(命名空间 esegece.sgcMaps.WinForms)TsgcWpfMapTsgcAvaloniaMap
sgcEditorsTsgcWinForms<Editor>TsgcWpf<Editor>TsgcAvalonia<Editor>
Editor 为 ButtonEdit、CurrencyEdit、MaskEdit、SpinEdit、DatePicker、DateTimePicker、TimePicker、LookupEdit、SearchEdit、TokenEdit、Memo、ColorPicker、MonthCalendar、RadioGroup、CheckGroup、Rating 或 RangeSlider。
sgcNavigationTsgcWinFormsNavigationBar, ...NavigationTabs, ...NavigationBreadcrumb, ...DockManagerTsgcWpfNavigationBar, ...NavigationTabs, ...NavigationBreadcrumb, ...DockManagerTsgcAvaloniaNavigationBar, ...NavigationTabs, ...NavigationBreadcrumb, ...DockManager
sgcToolbarTsgcWinFormsRibbon, TsgcWinFormsToolbar, TsgcWinFormsStatusBarTsgcWpfRibbon, TsgcWpfToolbar, TsgcWpfStatusBarTsgcAvaloniaRibbon, TsgcAvaloniaToolbar, TsgcAvaloniaStatusBar
sgcWizardTsgcWinFormsWizard, TsgcWinFormsStepperTsgcWpfWizard, TsgcWpfStepperTsgcAvaloniaWizard, TsgcAvaloniaStepper
sgcRichEditTsgcWinFormsRichEditTsgcWpfRichEditTsgcAvaloniaRichEdit
sgcPDF 查看器TsgcPDFViewerControlTsgcWpfPDFViewerTsgcAvaloniaPDFViewer
sgcReports 设计器TsgcWinFormsReportDesignerTsgcWpfReportDesignerTsgcAvaloniaReportDesigner
sgcReports 预览TsgcReportPreviewVCL(WinForms Panel)TsgcWpfReportPreviewTsgcAvaloniaReportPreview
sgcUICoreTsgcWinFormsAIQueryBar, TsgcWinFormsValidationSummaryTsgcWpfAIQueryBar, TsgcWpfValidationSummaryTsgcAvaloniaAIQueryBar, 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.BeginInvokeDispatcher.BeginInvokeDispatcher.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

HOST 行和 SAME 行

每个片段都复制自 MigrateToAvalonia 示例,该示例可在 Release 下为注册版和社区版编译,0 个警告,并在 WinForms、WPF 和 Avalonia 上通过自检。在宿主文件中,标有 HOST 的行在三个宿主之间不同,标有 SAME 的行在所有宿主中都相同。

只写一次:共享层

共享类库只引用模型包 esegece.sgcGrid、esegece.sgcCharts、esegece.sgcPlanner 和 esegece.sgcUICore,从不引用宿主包。带分组和汇总的网格列针对 TsgcGridColumns 配置:

Shared\TMigrateModel.cs
// 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 实时提供数据:

Shared\TMigrateModel.cs
// 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 控件共用一个主题:

Shared\TMigrateModel.cs
// 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 线程

绑定的集合在 UI 线程上变化。共享层并不了解宿主,因此它把投递调用作为委托接收:

Shared\TMigrateModel.cs
// 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)
Shared\TMigrateModel.cs
// 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 的类型。

WinForms\TMigrateForm.cs
// HOST: the WinForms control types
private readonly TsgcWinFormsGrid FGrid;
private readonly TsgcWinFormsChart FChart;
private readonly TsgcWinFormsScheduler FScheduler;
Wpf\TMigrateWindow.cs
// HOST: the WPF control types
private readonly TsgcWpfGrid FGrid;
private readonly TsgcWpfChart FChart;
private readonly TsgcWpfScheduler FScheduler;
Avalonia\TMigrateWindow.cs
// HOST: the Avalonia control types
private readonly TsgcAvaloniaGrid FGrid;
private readonly TsgcAvaloniaChart FChart;
private readonly TsgcAvaloniaScheduler FScheduler;

绑定界面:每个宿主上是同一段代码

有两行标有 HOST:UI 线程调用和列表绑定。其余部分在三个文件中是相同的文本。

WinForms\TMigrateForm.cs
// 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;
Wpf\TMigrateWindow.cs
// 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;
Avalonia\TMigrateWindow.cs
// 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;

应用启动

启动代码天然属于宿主代码。主题调用在三个宿主上相同。

WinForms\Program.cs
// 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;
Wpf\Program.cs
// 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;
Avalonia\Program.cs
// 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 应用都能继续工作,因此可以一次迁移一个界面。

1

提取共享层

把数据和控件配置移到只引用 sgcUI 模型包(esegece.sgcGrid、esegece.sgcCharts、esegece.sgcPlanner、esegece.sgcUICore)的类库中。针对 TsgcGridColumns、TsgcChart、TsgcPlannerData 和 TsgcPlannerSchedulerOptions 编写。

2

从 Windows 调用它

让现有的 WinForms 或 WPF 界面调用该库。它在 Windows 上照常工作。

3

创建 Avalonia 应用

一个包含 Avalonia.Desktop、Avalonia.Themes.Fluent 以及所需的 esegece.<Product>.Avalonia 软件包的 net8.0 项目。

4

替换控件类型

TsgcWinForms* 和 TsgcWpf* 变成 TsgcAvalonia*,映射表中列出的例外除外。

5

迁移窗口

带 TableLayoutPanel 的 Form,或带 Grid 的 WPF Window,变成带 Grid 的 Avalonia Window。WPF 的 XAML 移到 .axaml,并保留 sgc 命名空间 URI。

6

仅限 WinForms:绑定

把 DataSource 改为 ItemsSource。WPF 代码已经在使用 ItemsSource。

7

UI 线程调用

BeginInvoke 或 Dispatcher.BeginInvoke 变成 Dispatcher.UIThread.Post。

8

检查每个宿主

在每个宿主上运行相同的检查,就像示例的 /selftest 所做的那样,并比较保存的网格布局 JSON:它必须是相同的文本。

哪些内容无法一一对应

规划迁移之前请先阅读这些内容。

MigrateToAvalonia

一个界面,包含按区域分组的 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 产品页面。

超值之选:All-AccesseSeGeCe 全部产品,含高级支持,每年 €1,059 起。
查看 All-Access 价格

今天就迁移您的第一个界面

从 NuGet 添加免费的社区版软件包,或添加提供全部功能和完整源代码的注册版软件包,并对照 MigrateToAvalonia 示例迁移您的第一个界面。