WinForms e WPF para o Avalonia com a mesma API do sgcUI

O sgcUI traz controles nativos para WinForms, WPF e Avalonia que compartilham um modelo por produto. As colunas da grade, o gráfico, os dados do planejador e o tema são escritos uma só vez e compilam sem alterações para cada host, de modo que levar uma tela para o Avalonia, e com ele para macOS e Linux, significa mudar o código da janela e os tipos dos controles, não a lógica da tela. Este kit mostra como, com código de um exemplo que compila e passa no seu autoteste nos três hosts.

Uma aplicação desktop Windows que precisa rodar no macOS e no Linux

Telas de sistemas corporativos com grades, gráficos e agendadores, uma camada de dados que você quer manter e nenhuma vontade de reescrever a lógica das telas em um novo framework de interface.

A partir do WinForms

O WinForms roda somente no Windows. Com o sgcUI, os controles TsgcWinForms* que você usa hoje têm equivalentes TsgcAvalonia* com os mesmos membros. O código de configuração passa para uma biblioteca de classes compartilhada, o formulário vira uma janela Avalonia e DataSource vira ItemsSource.

A partir do WPF

O WPF também roda somente no Windows. Os seus controles do sgcUI são os mais próximos do Avalonia: ambos se ligam com ItemsSource, ambos são declarados em XAML com o mesmo URI de namespace sgc, e a chamada da thread de interface é a principal mudança de código.

O caminho habitual para uma aplicação WPF existente é o Avalonia XPF, que o seu fornecedor descreve como "a binary-compatible WPF runtime" e "a fork of WPF that maintains API and binary compatibility across the managed surface area". No momento em que escrevemos (20/10/2026), o XPF é vendido em licenças perpétuas por aplicação com doze meses de atualizações e suporte: Internal por EUR 9.500 por aplicação (distribuição somente para os seus próprios funcionários), Business por EUR 29.500 por aplicação (distribuição comercial para macOS e Linux desktop, com o Linux embarcado como complemento de EUR 21.250) e Enterprise a partir de EUR 124.500 por aplicação para dispositivos móveis ou o navegador. Pacotes WPF de terceiros podem rodar no XPF com condições: as notas do XPF sobre bibliotecas de terceiros dizem que os shims da API Win32 são "required for DevExpress controls", que alguns controles do DevExpress dependem do System.Drawing.Common (GDI+), "which is deprecated on non-Windows platforms", que os controles do DevExpress no Linux exigem a libgdiplus, e que "Issues most commonly arise with controls that use GDI or WinForms components". O próprio Avalonia é um framework de interface gratuito com licença MIT. O sgcUI para .NET é licenciado por desenvolvedor, a partir de €380 para a edição .NET Standard, com o código-fonte C# completo para WinForms, WPF e Avalonia, redistribuição binária gratuita e nenhuma taxa por aplicação. Há pacotes Community gratuitos no NuGet.

Um modelo, três hosts finos

Cada produto do sgcUI tem um modelo que não sabe nada do host. Os controles WinForms, WPF e Avalonia são visões finas sobre ele.

Modelos compartilhados

TsgcGridColumns, TsgcGridData, TsgcChart, TsgcPlannerData, TsgcPlannerSchedulerOptions, TsgcPivotEngine e TsgcDiagramDocument são os mesmos tipos nos três hosts. O código escrito contra eles compila sem alterações para cada um.

Os mesmos nomes de membro

Columns, Data, ShowFooter, GroupSummaries, GroupBy, SaveLayoutToString, LoadLayoutFromString, Chart, Options, View, Date, PrintSettings, CreatePrintSource, Print, ShowPrintPreview e ExportToPDF, além dos eventos da grade como OnCellClick, OnSelectionChange e OnSortChange.

O mesmo layout salvo

Um layout de grade salvo com SaveLayoutToString é o mesmo texto JSON em todos os hosts. No exemplo, os três hosts gravam o seu layout em um arquivo e os três arquivos têm um único SHA-256. Os temas (TsgcUITheme, sgcUIApplication.Theme, sgcUIApplication.AccentColor, alto contraste) também são compartilhados.

O controle de host de cada produto

A regra prática: troque o prefixo TsgcWinForms ou TsgcWpf por TsgcAvalonia. As exceções estão marcadas na tabela.

Produto WinForms WPF Avalonia
sgcGridTsgcWinFormsGridTsgcWpfGridTsgcAvaloniaGrid
Coluna declarada do sgcGridem código, 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 ou Annunciator.
sgcDashboardTsgcWinFormsDashboardTsgcWpfDashboardTsgcAvaloniaDashboard
Blocos do sgcDashboardTsgcWinFormsDashboardTile, ...ChartTile, ...GridTile, ...GaugeTile, ...PivotTile, TsgcWinFormsKPICard e TsgcWinFormsDashboardMapTileTsgcWpfDashboardTile, ...ChartTile, ...GridTile, ...GaugeTile, ...PivotTile, TsgcWpfKPICard. Sem bloco de mapa.TsgcAvaloniaDashboardTile, ...ChartTile, ...GridTile, ...GaugeTile, ...PivotTile, TsgcAvaloniaKPICard. Sem bloco de mapa.
sgcMapsSgcMapControl (namespace 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 ou RangeSlider.
sgcNavigationTsgcWinFormsNavigationBar, ...NavigationTabs, ...NavigationBreadcrumb, ...DockManagerTsgcWpfNavigationBar, ...NavigationTabs, ...NavigationBreadcrumb, ...DockManagerTsgcAvaloniaNavigationBar, ...NavigationTabs, ...NavigationBreadcrumb, ...DockManager
sgcToolbarTsgcWinFormsRibbon, TsgcWinFormsToolbar, TsgcWinFormsStatusBarTsgcWpfRibbon, TsgcWpfToolbar, TsgcWpfStatusBarTsgcAvaloniaRibbon, TsgcAvaloniaToolbar, TsgcAvaloniaStatusBar
sgcWizardTsgcWinFormsWizard, TsgcWinFormsStepperTsgcWpfWizard, TsgcWpfStepperTsgcAvaloniaWizard, TsgcAvaloniaStepper
sgcRichEditTsgcWinFormsRichEditTsgcWpfRichEditTsgcAvaloniaRichEdit
Visualizador de PDF do sgcPDFTsgcPDFViewerControlTsgcWpfPDFViewerTsgcAvaloniaPDFViewer
Designer do sgcReportsTsgcWinFormsReportDesignerTsgcWpfReportDesignerTsgcAvaloniaReportDesigner
Visualização do sgcReportsTsgcReportPreviewVCL (um Panel do WinForms)TsgcWpfReportPreviewTsgcAvaloniaReportPreview
sgcUICoreTsgcWinFormsAIQueryBar, TsgcWinFormsValidationSummaryTsgcWpfAIQueryBar, TsgcWpfValidationSummaryTsgcAvaloniaAIQueryBar, TsgcAvaloniaValidationSummary

O namespace é um por produto (esegece.sgcGrid, esegece.sgcCharts, ...) e o mesmo para os três hosts, exceto o mapa do WinForms, que está em esegece.sgcMaps.WinForms. Os controles de host vêm nos pacotes esegece.<Product>.WinForms, .Wpf e .Avalonia; o bloco de mapa do dashboard para WinForms está em esegece.sgcDashboard.Maps.WinForms. No WPF e no Avalonia, todo produto exceto o sgcReports é declarado em XAML com um único URI de namespace, xmlns:sgc="https://www.esegece.com/sgcui"; para o sgcReports use um mapeamento clr-namespace.

Os membros que mudam em um porte

Todo o resto nos controles mantém o nome. Estes são os pontos em que o host aparece.

Assunto WinForms WPF Avalonia
Ligar uma lista à gradeDataSource (e DataMember)ItemsSource (DependencyProperty)ItemsSource (StyledProperty)
Membros de ligação da gradeAutoGenerateColumns, KeyMember, SelectedItem, ItemsBinding e ItemsWriteBack (Registrada), iguais nos três hosts
Sincronização do item atualnenhumaIsSynchronizedWithCurrentItemnenhuma
Colunas e dados da gradeColumns (TsgcGridColumns), Data (TsgcGridData)
Modelo do gráficoChart (TsgcChart)
Modelo do planejadorData (TsgcPlannerData), Options (TsgcPlannerSchedulerOptions)
TemaTheme do controle (TsgcUITheme; utDefault segue sgcUIApplication.Theme)o mesmo, mais sgcUICore_Theme_Avalonia.sgcUIApplyThemeToWindow
Thread de interfaceControl.BeginInvokeDispatcher.BeginInvokeDispatcher.UIThread.Post
Início da aplicaçãoApplication.Run(form)new Application().Run(window)uma classe Application com um tema, e StartWithClassicDesktopLifetime
Target framework da aplicaçãonet8.0-windows (também net472)net8.0-windows (também net472)net8.0, net9.0, net10.0

Linhas HOST e linhas SAME

Cada trecho é copiado do exemplo MigrateToAvalonia, que compila em Release para as edições Registrada e Community com 0 avisos e passa no seu autoteste em WinForms, WPF e Avalonia. Nos arquivos de host, uma linha marcada HOST difere entre os três hosts e uma linha marcada SAME é idêntica em todos eles.

Escrito uma vez: a camada compartilhada

A biblioteca de classes compartilhada referencia apenas os pacotes de modelo esegece.sgcGrid, esegece.sgcCharts, esegece.sgcPlanner e esegece.sgcUICore, nunca um pacote de host. As colunas da grade, com agrupamento e totais, são configuradas contra o 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();
    }
}

O gráfico é um modelo TsgcChart, com a série de pizza alimentada ao vivo por uma ObservableCollection através do TsgcChartDataSource_Items:

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;

Um tema para todos os controles sgcUI da aplicação:

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;
}

O único serviço do host: postar na thread de interface

As coleções ligadas mudam na thread de interface. A camada compartilhada não conhece o host, então recebe a chamada de postagem como um delegate:

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));
    });
}

Os campos de controle diferem só no tipo

Antes: os tipos do WinForms ou do WPF. Depois: os tipos do 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;

Ligando a tela: o mesmo bloco em todos os hosts

Duas linhas estão marcadas HOST: a chamada da thread de interface e a ligação da lista. O resto é o mesmo texto nos três arquivos.

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;

O início da aplicação

O código de início é código de host por natureza. A chamada do tema é a mesma nos três hosts.

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();
    }
}

Oito passos por tela

A aplicação Windows continua funcionando depois de cada passo, então a mudança pode ser feita uma tela por vez.

1

Extraia a camada compartilhada

Mova os dados e a configuração dos controles para uma biblioteca de classes que referencie apenas os pacotes de modelo do sgcUI (esegece.sgcGrid, esegece.sgcCharts, esegece.sgcPlanner, esegece.sgcUICore). Escreva-a contra TsgcGridColumns, TsgcChart, TsgcPlannerData e TsgcPlannerSchedulerOptions.

2

Chame-a a partir do Windows

Faça a tela WinForms ou WPF existente chamar essa biblioteca. Ela continua funcionando no Windows como antes.

3

Crie a aplicação Avalonia

Um projeto net8.0 com Avalonia.Desktop, Avalonia.Themes.Fluent e os pacotes esegece.<Product>.Avalonia de que você precisa.

4

Troque os tipos de controle

TsgcWinForms* e TsgcWpf* viram TsgcAvalonia*, com as exceções listadas na tabela de mapeamento.

5

Porte a janela

Um Form com um TableLayoutPanel, ou uma Window WPF com um Grid, vira uma Window Avalonia com um Grid. O XAML do WPF passa para .axaml e mantém o URI de namespace sgc.

6

Só no WinForms: a ligação

Troque DataSource por ItemsSource. O código WPF já usa ItemsSource.

7

A chamada da thread de interface

BeginInvoke ou Dispatcher.BeginInvoke vira Dispatcher.UIThread.Post.

8

Verifique cada host

Execute as mesmas verificações em cada host, como faz o /selftest do exemplo, e compare o JSON do layout da grade salvo: deve ser o mesmo texto.

O que não tem correspondência um a um

Leia isto antes de planejar a mudança.

MigrateToAvalonia

Uma tela com uma grade de 40 pedidos agrupados por região, um gráfico de pizza do valor por região e um agendador de semana útil, mostrada por três hosts a partir de uma camada compartilhada.

Onde está

Nas demos .NET do sgcUI, na pasta Suite\MigrateToAvalonia da solução sgcUI_Demos.sln: Shared, WinForms, Wpf, Avalonia e um readme.html. No download das demos, os projetos referenciam os pacotes NuGet do sgcUI.

Como executá-la

dotnet build Avalonia\sgcUIMigrate.Avalonia.csproj -c Release, e o mesmo para WinForms\sgcUIMigrate.WinForms.csproj e Wpf\sgcUIMigrate.Wpf.csproj. Acrescente -p:SgcCommunity=true para a edição Community. O host Avalonia também roda no macOS e no Linux.

Autoteste

sgcUIMigrate.<Host>.exe /selftest [folder] verifica as linhas, o agrupamento, a pizza, o agendador, um pedido adicionado em uma thread de trabalho e um pedido removido, grava migrate_selftest_<host>.txt e migrate_layout_<host>.json, e termina com o número de verificações que falharam. Todas as 13 verificações passam nos três hosts, e os três arquivos de layout são idênticos byte a byte.

Para ser justo com os dois caminhos: o XPF pretende rodar uma aplicação WPF existente no macOS e no Linux com o seu código e os seus controles em grande parte como estão, e o sgcUI não faz isso. Com o sgcUI você porta o código de janela de cada tela para o Avalonia, e as partes da tela feitas com o sgcUI passam sem alterações. As telas construídas com controles de outros fornecedores precisam que esses controles sejam substituídos primeiro, no Windows, antes da mudança. O sgcUI é o caminho quando as suas telas usam, ou podem passar a usar, a grade, os gráficos, o planejador, a tabela dinâmica, o diagrama e os outros produtos do sgcUI acima, e você quer controles Avalonia nativos com o código-fonte completo e nenhuma camada de execução no meio. Para os produtos e as edições veja a página do produto sgcUI.

Melhor custo-benefício: All-AccessTodos os produtos da eSeGeCe, com Suporte Premium incluído, a partir de €1,059/ano.
Ver preços do All-Access

Migre a sua primeira tela hoje

Adicione os pacotes Community gratuitos do NuGet, ou os pacotes Registrados para ter todos os recursos e o código-fonte completo, e porte a sua primeira tela com o exemplo MigrateToAvalonia ao lado.