Add Avalonia UI frontend alongside WinForms and WPF

Introduce Avalonia as a new cross-platform UI option, including new XAML and code-behind files for the application and main window. Update Program.cs to support a --avalonia launch argument and add corresponding launch profile. Integrate Avalonia NuGet packages and ensure DataModel supports UI-thread invocation for all frontends. All business logic and state are shared via DI, enabling consistent behavior across WinForms, WPF, and Avalonia.
This commit is contained in:
MaddoScientisto 2026-02-26 18:43:07 +01:00
commit 311b3e76f0
8 changed files with 535 additions and 9 deletions

View file

@ -0,0 +1,8 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ImageCatalog_2.AvaloniaApp">
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
</Application.Styles>
</Application>

View file

@ -0,0 +1,20 @@
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
namespace ImageCatalog_2;
public partial class AvaloniaApp : Avalonia.Application
{
public override void Initialize() => AvaloniaXamlLoader.Load(this);
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var model = Program.ServiceProvider.GetRequiredService<DataModel>();
desktop.MainWindow = new AvaloniaMainWindow(model);
}
base.OnFrameworkInitializationCompleted();
}
}

View file

@ -0,0 +1,317 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Class="ImageCatalog_2.AvaloniaMainWindow"
mc:Ignorable="d"
Title="Image Catalog - Avalonia" Height="540" Width="800">
<Grid RowDefinitions="*,Auto">
<Grid Grid.Row="0" Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="0.8*" />
</Grid.ColumnDefinitions>
<!-- Left: Tabs -->
<TabControl Grid.Column="0" Margin="0,0,10,0">
<!-- Tab 1: Generale -->
<TabItem Header="Generale">
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="Percorsi" FontWeight="Bold" />
<StackPanel Margin="0,6,0,0">
<!-- Source -->
<Grid Margin="0,0,0,6" ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock Text="Sorgente:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding SourcePath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectSourceFolderCommand}" Grid.Column="2" Content="Scegli..." />
<Button Width="56" Margin="8,0,0,0" Grid.Column="3" Click="OpenSourceFolder_Click" Content="Apri" />
</Grid>
<!-- Destination -->
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock Text="Destinazione:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding DestinationPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectDestinationFolderCommand}" Grid.Column="2" Content="Scegli..." />
<Button Width="56" Margin="8,0,0,0" Grid.Column="3" Click="OpenDestinationFolder_Click" Content="Apri" />
</Grid>
</StackPanel>
<TextBlock Text="Opzioni" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Margin="0,6,0,0">
<CheckBox Content="Forza JPEG" IsChecked="{Binding ForceJpeg}" />
<CheckBox Content="Aggiorna sottodirectory" IsChecked="{Binding UpdateSubdirectories}" />
<CheckBox Content="Crea sottocartelle" IsChecked="{Binding CreateSubfolders}" />
<CheckBox Content="Sovrascrivi immagini" IsChecked="{Binding OverwriteImages}" />
</StackPanel>
<TextBlock Text="Elaborazione" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="Threads:" VerticalAlignment="Center" />
<TextBox Text="{Binding ThreadsCount, Mode=TwoWay}" Width="60" Margin="8,0,0,0" />
<TextBlock Text="Chunk:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding ChunkSize, Mode=TwoWay}" Width="60" Margin="8,0,0,0" />
</StackPanel>
<TextBlock Text="Divisione cartelle" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="File per cartella:" VerticalAlignment="Center" />
<TextBox Text="{Binding FilesPerFolder}" Width="60" Margin="8,0,0,0" />
<TextBlock Text="Suffisso:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding FolderSuffix}" Width="120" Margin="8,0,0,0" />
</StackPanel>
<TextBlock Text="Numerazione" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<RadioButton Content="Progressiva" IsChecked="{Binding UseProgressiveNumbering}" GroupName="Num" />
<RadioButton Content="Per file" IsChecked="{Binding UseFileNumbering}" GroupName="Num" Margin="8,0,0,0" />
<TextBlock Text="Cifre:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding CounterDigits}" Width="40" Margin="8,0,0,0" />
</StackPanel>
<TextBlock Text="Libreria Immagini" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<RadioButton Content="System.Graphics" IsChecked="{Binding UseSystemGraphics}" GroupName="Lib" />
<RadioButton Content="ImageSharp" IsChecked="{Binding UseImageSharp}" GroupName="Lib" Margin="8,0,0,0" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- Tab 2: Testo -->
<TabItem Header="Testo">
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="Testo Orizzontale" FontWeight="Bold" />
<TextBox Text="{Binding HorizontalText, Mode=TwoWay}" />
<TextBlock Text="Testo Verticale" FontWeight="Bold" Margin="0,8,0,0" />
<TextBox Text="{Binding VerticalText, Mode=TwoWay}" AcceptsReturn="True"
TextWrapping="Wrap" MinHeight="80" />
<TextBlock Text="Font" FontWeight="Bold" Margin="0,8,0,0" />
<StackPanel Orientation="Horizontal">
<ComboBox ItemsSource="{Binding AvailableFonts}" SelectedItem="{Binding FontName}" Width="250" />
<TextBox Text="{Binding FontSize}" Width="60" Margin="8,0,0,0" />
<CheckBox Content="Grassetto" IsChecked="{Binding FontBold}" Margin="8,0,0,0" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="Colore testo (hex)" FontWeight="Bold" Margin="0,8,0,0" />
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding TextColorRGB}" Width="120" />
<Button Content="Seleziona colore" Command="{Binding SelectColorCommand}" Margin="8,0,0,0" />
</StackPanel>
<TextBlock Text="Dimensioni verticale" FontWeight="Bold" Margin="0,8,0,0" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="Size:" VerticalAlignment="Center" />
<TextBox Text="{Binding VerticalTextSize}" Width="60" Margin="8,0,0,0" />
<TextBlock Text="Margin:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding VerticalTextMargin}" Width="60" Margin="8,0,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="Trasparenza testo:" VerticalAlignment="Center" />
<TextBox Text="{Binding TextTransparency}" Width="60" Margin="8,0,0,0" />
<TextBlock Text="Margine testo:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding TextMargin}" Width="60" Margin="8,0,0,0" />
</StackPanel>
<TextBlock Text="Tempo Gara" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<CheckBox Content="Aggiungi Orario" IsChecked="{Binding AddTime}" />
<CheckBox Content="Aggiungi tempo gara" IsChecked="{Binding AddRaceTime}" Margin="12,0,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="Partenza:" VerticalAlignment="Center" />
<CalendarDatePicker SelectedDate="{Binding RaceStartDate}"
IsEnabled="{Binding AddRaceTime}"
Margin="8,0,0,0" Width="200" />
<TextBox Text="{Binding TimeLabel}" Width="220" Margin="12,0,0,0" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- Tab 3: Foto -->
<TabItem Header="Foto">
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="Dimensioni foto grandi" FontWeight="Bold" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBox Text="{Binding PhotoBigWidth}" Width="80" />
<TextBox Text="{Binding PhotoBigHeight}" Width="80" Margin="8,0,0,0" />
</StackPanel>
<TextBlock Text="Opzioni foto" FontWeight="Bold" Margin="0,8,0,0" />
<StackPanel Margin="0,6,0,0">
<CheckBox Content="Mantieni dimensioni originali" IsChecked="{Binding KeepOriginalDimensions}" />
<CheckBox Content="Rotazione automatica" IsChecked="{Binding AutomaticRotation}" />
</StackPanel>
<TextBlock Text="JPEG" FontWeight="Bold" Margin="0,8,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="Qualità:" VerticalAlignment="Center" />
<TextBox Text="{Binding JpegQuality}" Width="60" Margin="8,0,0,0" />
<TextBlock Text="Miniature Qualità:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding JpegQualityThumbnail}" Width="60" Margin="8,0,0,0" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- Tab 4: Miniature -->
<TabItem Header="Miniature">
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="Miniature" FontWeight="Bold" />
<CheckBox Content="Crea miniature" IsChecked="{Binding CreateThumbnails}" Margin="0,6,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="Prefisso:" VerticalAlignment="Center" />
<TextBox Text="{Binding ThumbnailPrefix}" Width="120" Margin="8,0,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBox Text="{Binding ThumbnailWidth}" Width="80" />
<TextBox Text="{Binding ThumbnailHeight}" Width="80" Margin="8,0,0,0" />
</StackPanel>
<StackPanel Margin="0,8,0,0">
<TextBlock Text="Modalità miniature:" VerticalAlignment="Center" />
<ComboBox SelectedIndex="{Binding ThumbnailOptionIndex, Mode=TwoWay}" Width="220" Margin="0,6,0,0">
<ComboBoxItem>Nessuna</ComboBoxItem>
<ComboBoxItem>Aggiungi scritta</ComboBoxItem>
<ComboBoxItem>Nome file</ComboBoxItem>
<ComboBoxItem>Aggiungi orario</ComboBoxItem>
<ComboBoxItem>Nome+Orario</ComboBoxItem>
<ComboBoxItem>Tempo gara</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- Tab 5: Logo -->
<TabItem Header="Logo">
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="Logo" FontWeight="Bold" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<Button Command="{Binding SelectLogoFileCommand}" Content="Seleziona logo" />
<TextBlock Text="{Binding LogoFile}" Margin="8,0,0,0" VerticalAlignment="Center"
Width="250" TextTrimming="CharacterEllipsis" />
</StackPanel>
<StackPanel Margin="0,8,0,0">
<Image Name="LogoPreview" Width="160" Height="160" Stretch="Uniform" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<TextBox Text="{Binding LogoWidth}" Width="80" />
<TextBox Text="{Binding LogoHeight}" Width="80" Margin="8,0,0,0" />
</StackPanel>
<CheckBox Content="Aggiungi logo" IsChecked="{Binding AddLogo}" Margin="0,8,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<TextBlock Text="Margine:" VerticalAlignment="Center" />
<TextBox Text="{Binding LogoMargin}" Width="80" Margin="8,0,0,0" />
<TextBlock Text="Trasparenza:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding LogoTransparency}" Width="60" Margin="8,0,0,0" />
<Button Command="{Binding SelectTransparentColorCommand}" Margin="8,0,0,0"
Content="Seleziona trasparente" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<TextBlock Text="Posizione:" VerticalAlignment="Center" />
<ComboBox ItemsSource="{Binding HorizontalAlignments}"
SelectedItem="{Binding LogoHorizontalPosition}"
Width="120" Margin="8,0,0,0" />
<ComboBox ItemsSource="{Binding VerticalPositions}"
SelectedItem="{Binding LogoVerticalPosition}"
Width="120" Margin="8,0,0,0" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- Tab 6: AI -->
<TabItem Header="AI">
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="AI / OCR" FontWeight="Bold" />
<CheckBox Content="Estrai numeri dalle immagini" IsChecked="{Binding ExtractNumbers}" Margin="0,8,0,0" />
<TextBlock Text="Modelli" FontWeight="Bold" Margin="0,12,0,0" />
<Grid Margin="0,6,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock Text="Cartella modelli:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding ModelsFolderPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectModelsFolderCommand}"
Grid.Column="2" Content="Scegli..." />
<Button Width="56" Margin="8,0,0,0" Grid.Column="3"
Click="OpenModelsFolder_Click" Content="Apri" />
</Grid>
<TextBlock Text="Output CSV" FontWeight="Bold" Margin="0,12,0,0" />
<Grid Margin="0,6,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock Text="Percorso CSV:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding CsvOutputPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectCsvOutputCommand}"
Grid.Column="2" Content="Scegli..." />
<Button Width="56" Margin="8,0,0,0" Grid.Column="3"
Click="OpenCsvOutputFolder_Click" Content="Apri" />
</Grid>
<TextBlock Text="Anteprima risultati" FontWeight="Bold" Margin="0,12,0,0" />
<DataGrid ItemsSource="{Binding PreviewResults}" IsReadOnly="True"
AutoGenerateColumns="False" Height="200" Margin="0,6,0,0">
<DataGrid.Columns>
<DataGridTextColumn Header="Path" Binding="{Binding Path}" Width="*" />
<DataGridTextColumn Header="Text" Binding="{Binding Text}" Width="2*" />
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
<!-- Right: Controls and live info -->
<StackPanel Grid.Column="1">
<StackPanel HorizontalAlignment="Right" Margin="0,0,0,12">
<Button Width="28" Height="28" Click="ToggleTheme_Click" ToolTip.Tip="Cambia tema"
HorizontalAlignment="Right" Padding="2" Content="🌙" />
</StackPanel>
<Border BorderBrush="#DDD" BorderThickness="1" Padding="8" MaxWidth="280">
<StackPanel>
<StackPanel>
<Button Width="120" Margin="0,0,0,8" Command="{Binding LoadSettingsCommand}" Content="Carica" />
<Button Width="120" Margin="0,0,0,8" Command="{Binding SaveSettingsCommand}" Content="Salva" />
<Button Width="120" Height="36" Margin="0,6,0,8"
Command="{Binding ProcessImagesCommand}"
IsEnabled="{Binding UiEnabled}" Content="Avvia" />
<Button Width="120" Height="36"
Command="{Binding AsyncCancelOperationCommand}" Content="Stop" />
</StackPanel>
<Separator Margin="0,12,0,12" />
<TextBlock Text="Stato" FontWeight="Bold" />
<TextBlock Text="{Binding ProcessingStatus}" TextWrapping="Wrap" />
<TextBlock Text="Progresso" FontWeight="Bold" Margin="0,8,0,0" />
<ProgressBar Minimum="0" Maximum="{Binding ProgressBarMaximum}"
Value="{Binding ProgressBarValue}" Height="20" />
<TextBlock Margin="0,6,0,0">
<Run Text="{Binding ProcessedImagesCount}" />
<Run Text=" / " />
<Run Text="{Binding TotalImagesCount}" />
</TextBlock>
<TextBlock Text="Velocità" FontWeight="Bold" Margin="0,8,0,0" />
<TextBlock Text="{Binding SpeedCounter}" TextWrapping="Wrap" />
<TextBlock Text="{Binding AppVersion}" Margin="0,8,0,0" Opacity="0.6" />
</StackPanel>
</Border>
</StackPanel>
</Grid>
</Grid>
</Window>

View file

@ -0,0 +1,151 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using Avalonia.Threading;
using System;
using System.IO;
namespace ImageCatalog_2;
public partial class AvaloniaMainWindow : Window
{
private readonly DataModel _model;
private bool _isDarkTheme = false;
public AvaloniaMainWindow(DataModel model)
{
InitializeComponent();
_model = model;
DataContext = _model;
// Provide Avalonia dispatcher so DataModel can marshal UI updates
_model.UiInvoker = action => Dispatcher.UIThread.Invoke(action);
// Wire dialog events
_model.SelectSourceFolderRequested += async (_, _) =>
{
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = "Seleziona cartella sorgente" });
if (folders.Count > 0) _model.SourcePath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
};
_model.SelectDestinationFolderRequested += async (_, _) =>
{
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = "Seleziona cartella destinazione" });
if (folders.Count > 0) _model.DestinationPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
};
_model.SelectLogoFileRequested += async (_, _) =>
{
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Seleziona logo",
FileTypeFilter = new[] { new FilePickerFileType("Immagini") { Patterns = new[] { "*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif" } } }
});
if (files.Count > 0)
{
_model.LogoFile = files[0].Path.LocalPath;
UpdateLogoPreview(_model.LogoFile);
}
};
_model.SelectModelsFolderRequested += async (_, _) =>
{
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = "Seleziona cartella modelli" });
if (folders.Count > 0) _model.ModelsFolderPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
};
_model.SelectCsvOutputRequested += async (_, _) =>
{
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Salva CSV",
DefaultExtension = "csv",
FileTypeChoices = new[] { new FilePickerFileType("CSV") { Patterns = new[] { "*.csv" } } }
});
if (file != null) _model.CsvOutputPath = file.Path.LocalPath;
};
_model.SaveSettingsRequested += async (_, _) =>
{
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Salva impostazioni",
DefaultExtension = "xml",
FileTypeChoices = new[] { new FilePickerFileType("Setup") { Patterns = new[] { "*.xml" } } }
});
if (file != null) await _model.SaveSettingsToFileAsync(file.Path.LocalPath);
};
_model.LoadSettingsRequested += async (_, _) =>
{
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Carica impostazioni",
FileTypeFilter = new[] { new FilePickerFileType("Setup") { Patterns = new[] { "*.xml" } } }
});
if (files.Count > 0) await _model.LoadSettingsFromFileAsync(files[0].Path.LocalPath);
};
_model.SelectColorRequested += (_, _) =>
{
// Color is set by typing hex directly in the TextBox
};
_model.SelectTransparentColorRequested += (_, _) =>
{
// Color is set by typing hex directly in the TextBox
};
_model.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(_model.LogoFile))
UpdateLogoPreview(_model.LogoFile);
};
}
private void ToggleTheme_Click(object? sender, RoutedEventArgs e)
{
_isDarkTheme = !_isDarkTheme;
if (Avalonia.Application.Current != null)
Avalonia.Application.Current.RequestedThemeVariant = _isDarkTheme ? ThemeVariant.Dark : ThemeVariant.Light;
}
private void OpenSourceFolder_Click(object? sender, RoutedEventArgs e) => OpenInExplorer(_model.SourcePath);
private void OpenDestinationFolder_Click(object? sender, RoutedEventArgs e) => OpenInExplorer(_model.DestinationPath);
private void OpenModelsFolder_Click(object? sender, RoutedEventArgs e) => OpenInExplorer(_model.ModelsFolderPath);
private void OpenCsvOutputFolder_Click(object? sender, RoutedEventArgs e)
{
var dir = Path.GetDirectoryName(_model.CsvOutputPath);
OpenInExplorer(string.IsNullOrWhiteSpace(dir) ? _model.CsvOutputPath : dir);
}
private static void OpenInExplorer(string? path)
{
if (string.IsNullOrWhiteSpace(path)) return;
path = path.Trim().Trim('"');
try
{
if (File.Exists(path))
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\"");
else if (Directory.Exists(path))
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true });
}
catch { }
}
private void UpdateLogoPreview(string? path)
{
var preview = this.FindControl<Avalonia.Controls.Image>("LogoPreview");
if (preview == null) return;
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
preview.Source = null;
return;
}
try { preview.Source = new Avalonia.Media.Imaging.Bitmap(path); }
catch { preview.Source = null; }
}
}

View file

@ -207,12 +207,20 @@ namespace ImageCatalog_2
return digits;
}
/// <summary>
/// Optional UI-thread invoker set by the active UI layer (WPF, Avalonia, etc.).
/// When set, <see cref="InvokeOnUiThreadAsync"/> uses this delegate instead of the WPF dispatcher.
/// </summary>
public Action<Action>? UiInvoker { get; set; }
private Task InvokeOnUiThreadAsync(Action action)
{
// Use SynchronizationContext via Task to ensure UI thread update
return Task.Run(() =>
{
System.Windows.Application.Current?.Dispatcher.Invoke(action);
if (UiInvoker != null)
UiInvoker(action);
else
System.Windows.Application.Current?.Dispatcher.Invoke(action);
});
}

View file

@ -48,6 +48,10 @@
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.3" />
<PackageReference Include="MahApps.Metro.IconPacks" Version="6.2.1" />
<PackageReference Include="MinVer" Version="7.0.0" PrivateAssets="all" />
<PackageReference Include="Avalonia" Version="11.3.0" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.0" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.0" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.0" />
<PackageReference Include="Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers" Version="0.4.421302">
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View file

@ -9,6 +9,8 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
using System.IO;
using Microsoft.Extensions.Options;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
namespace ImageCatalog_2;
@ -56,12 +58,18 @@ static class Program
}
public static IServiceProvider ServiceProvider { get; private set; }
public static Avalonia.AppBuilder BuildAvaloniaApp()
=> Avalonia.AppBuilder.Configure<AvaloniaApp>()
.UsePlatformDetect()
.LogToTrace();
[STAThread]
static void Main(string[] args)
{
Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
System.Windows.Forms.Application.SetHighDpiMode(HighDpiMode.SystemAware);
System.Windows.Forms.Application.EnableVisualStyles();
System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false);
AllocConsole();
RedirectConsoleOutput();
@ -74,8 +82,15 @@ static class Program
// Resolve WPF MainWindow when available, otherwise fall back to WinForms MainForm
var serviceProvider = ServiceProvider;
// Determine UI based on command line. Default: WinForms. Use --wpf to explicitly request WPF.
// Determine UI based on command line. Default: WinForms. Use --wpf for WPF, --avalonia for Avalonia.
bool useWpf = args is not null && Array.Exists(args, a => string.Equals(a, "--wpf", StringComparison.OrdinalIgnoreCase));
bool useAvalonia = args is not null && Array.Exists(args, a => string.Equals(a, "--avalonia", StringComparison.OrdinalIgnoreCase));
if (useAvalonia)
{
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args ?? Array.Empty<string>());
return;
}
if (useWpf)
{

View file

@ -1,12 +1,15 @@
{
"profiles": {
"ImageCatalog (WinForms)": {
"commandName": "Project",
"commandLineArgs": ""
"commandName": "Project"
},
"ImageCatalog (WPF)": {
"commandName": "Project",
"commandLineArgs": "--wpf"
},
"ImageCatalog (Avalonia)": {
"commandName": "Project",
"commandLineArgs": "--avalonia"
}
}
}
}