Upgrade UI to MahApps.Metro with runtime theming

Modernize WPF UI using MahApps.Metro: switch MainWindow to MetroWindow, add MetroTabControl/MetroTabItem, custom tab styles, and icon-based theme toggle. Move version display to window commands. Integrate MahApps resource dictionaries and ThemeManager for runtime theme switching. Update startup logic for proper theming. WinForms fallback retained.
This commit is contained in:
MaddoScientisto 2026-02-21 16:49:13 +01:00
commit 9007a27fb2
4 changed files with 207 additions and 54 deletions

View file

@ -42,6 +42,7 @@
<ItemGroup>
<PackageReference Include="AIFotoONLUS.Core" Version="0.1.1" />
<PackageReference Include="AutoMapper" Version="16.0.0" />
<PackageReference Include="MahApps.Metro" Version="2.4.11" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.3" />

View file

@ -1,15 +1,18 @@
<Window x:Class="ImageCatalog_2.MainWindow"
<controls:MetroWindow x:Class="ImageCatalog_2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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"
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:controls="http://metro.mahapps.com/winfx/xaml/controls"
mc:Ignorable="d"
Title="Image Catalog - WPF" Height="490" Width="800"
Background="{DynamicResource WindowBackgroundBrush}" Foreground="{DynamicResource ControlForegroundBrush}">
<Window.Resources>
Title="Image Catalog - WPF" Height="540" Width="800"
Background="{DynamicResource WindowBackgroundBrush}" Foreground="{DynamicResource ControlForegroundBrush}"
GlowBrush="{DynamicResource AccentBrush}">
<controls:MetroWindow.Resources>
<ResourceDictionary>
<!-- Default (Light) theme resources placed at top-level so DynamicResource lookups resolve -->
<!-- style moved later to avoid early resource lookup -->
<SolidColorBrush x:Key="WindowBackgroundBrush" Color="White" />
<SolidColorBrush x:Key="ControlBackgroundBrush" Color="White" />
<SolidColorBrush x:Key="ControlForegroundBrush" Color="Black" />
@ -38,8 +41,49 @@
<SolidColorBrush x:Key="DataGridBackgroundBrush.Dark" Color="#252526" />
<SolidColorBrush x:Key="DataGridForegroundBrush.Dark" Color="#E6E6E6" />
</ResourceDictionary>
<!-- Improve tab header visuals so selected tab and boundaries are clear -->
<Style TargetType="controls:MetroTabItem">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{DynamicResource ControlForegroundBrush}" />
<Setter Property="Margin" Value="0,0,4,0" />
<Setter Property="Padding" Value="6,4" />
<Setter Property="MinWidth" Value="56" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:MetroTabItem">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
CornerRadius="4"
BorderThickness="0"
Padding="{TemplateBinding Padding}">
<ContentPresenter ContentSource="Header" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentBrush}" />
<Setter Property="Foreground" Value="{DynamicResource ControlForegroundBrush}" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource BorderBrush}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.5" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
</Window.Resources>
</controls:MetroWindow.Resources>
<controls:MetroWindow.RightWindowCommands>
<controls:WindowCommands>
<!-- Show version in title area; theme toggle moved into window content -->
<TextBlock Name="VersionTextBlock" Text="" VerticalAlignment="Center" Margin="8,0,0,0" />
</controls:WindowCommands>
</controls:MetroWindow.RightWindowCommands>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
@ -54,14 +98,14 @@
</Grid.ColumnDefinitions>
<!-- Left: Tabs -->
<TabControl Grid.Column="0" Margin="0,0,10,0">
<TabItem>
<TabItem.Header>
<controls:MetroTabControl Grid.Column="0" Margin="0,0,10,0">
<controls:MetroTabItem>
<controls:MetroTabItem.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="CogOutline" Width="16" Height="16" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
<TextBlock Text="Generale" />
</StackPanel>
</TabItem.Header>
</controls:MetroTabItem.Header>
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="Percorsi" FontWeight="Bold" />
@ -155,15 +199,15 @@
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
</controls:MetroTabItem>
<TabItem>
<TabItem.Header>
<controls:MetroTabItem>
<controls:MetroTabItem.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="FormatLetterCase" Width="16" Height="16" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
<TextBlock Text="Testo" />
</StackPanel>
</TabItem.Header>
</controls:MetroTabItem.Header>
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="Testo Orizzontale" FontWeight="Bold" />
@ -206,15 +250,15 @@
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
</controls:MetroTabItem>
<TabItem>
<TabItem.Header>
<controls:MetroTabItem>
<controls:MetroTabItem.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="CameraFrontVariant" Width="16" Height="16" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
<TextBlock Text="Foto" />
</StackPanel>
</TabItem.Header>
</controls:MetroTabItem.Header>
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="Dimensioni foto grandi" FontWeight="Bold" />
@ -240,15 +284,15 @@
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
</controls:MetroTabItem>
<TabItem>
<TabItem.Header>
<controls:MetroTabItem>
<controls:MetroTabItem.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="Image" Width="16" Height="16" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
<TextBlock Text="Miniature" />
</StackPanel>
</TabItem.Header>
</controls:MetroTabItem.Header>
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="Miniature" FontWeight="Bold" />
@ -275,15 +319,15 @@
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
</controls:MetroTabItem>
<TabItem>
<TabItem.Header>
<controls:MetroTabItem>
<controls:MetroTabItem.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="ImageFilterCenterFocus" Width="16" Height="16" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
<TextBlock Text="Logo" />
</StackPanel>
</TabItem.Header>
</controls:MetroTabItem.Header>
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="Logo" FontWeight="Bold" />
@ -317,15 +361,15 @@
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
</controls:MetroTabItem>
<TabItem>
<TabItem.Header>
<controls:MetroTabItem>
<controls:MetroTabItem.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="Robot" Width="16" Height="16" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
<TextBlock Text="AI" />
</StackPanel>
</TabItem.Header>
</controls:MetroTabItem.Header>
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="AI / OCR" FontWeight="Bold" />
@ -388,14 +432,22 @@
</DataGrid>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
</controls:MetroTabItem>
</controls:MetroTabControl>
<!-- Right: Controls and live info -->
<Border Grid.Column="1" BorderBrush="#DDD" BorderThickness="1" Padding="8" MaxWidth="280">
<StackPanel>
<!-- Buttons stacked vertically as requested -->
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
<StackPanel Grid.Column="1" Orientation="Vertical">
<!-- Compact theme toggle panel (icon-only) aligned right -->
<StackPanel HorizontalAlignment="Stretch" Margin="0,0,0,12">
<Button Width="24" Height="24" Click="ToggleTheme_Click" ToolTip="Cambia tema" HorizontalAlignment="Right" Padding="2">
<iconPacks:PackIconMaterial Kind="ThemeLightDark" Width="12" Height="12" Foreground="{StaticResource AccentBrush}" />
</Button>
</StackPanel>
<Border BorderBrush="#DDD" BorderThickness="1" Padding="8" MaxWidth="280">
<!-- Buttons and status live info inside the bordered panel -->
<StackPanel>
<!-- Buttons stacked vertically as requested -->
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
<Button Width="120" Margin="0,0,0,8" Command="{Binding LoadSettingsCommand}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<iconPacks:PackIconMaterial Kind="FolderUploadOutline" Width="14" Height="14" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
@ -438,14 +490,10 @@
<TextBlock Text="Velocità" FontWeight="Bold" Margin="0,8,0,0" />
<TextBlock Text="{Binding SpeedCounter}" TextWrapping="Wrap" />
</StackPanel>
</Border>
</Border>
</StackPanel>
</Grid>
<!-- Status bar at the bottom showing version -->
<StatusBar Grid.Row="1">
<StatusBarItem HorizontalAlignment="Right">
<TextBlock Name="VersionTextBlock" Text="" />
</StatusBarItem>
</StatusBar>
<!-- Status bar removed; version now shown in the title commands area -->
</Grid>
</Window>
</controls:MetroWindow>

View file

@ -1,4 +1,6 @@
using System.Windows;
using MahApps.Metro.Controls;
using ControlzEx.Theming;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.IO;
@ -9,9 +11,10 @@ using System.Windows.Forms;
namespace ImageCatalog_2
{
public partial class MainWindow : Window
public partial class MainWindow : MetroWindow
{
private readonly DataModel _model;
private bool _isDarkTheme = false;
public MainWindow(DataModel model)
{
InitializeComponent();
@ -38,6 +41,9 @@ namespace ImageCatalog_2
VersionTextBlock.Text = string.IsNullOrWhiteSpace(version) ? string.Empty : $"v{version}";
}
catch { }
// Ensure MahApps resource dictionaries are loaded so chrome/styles are available
EnsureMahAppsResourcesLoaded();
// Apply theme based on user preference or system setting (default to light)
ApplyTheme(isDark: false);
// Subscribe to DataModel events that require UI dialogs
@ -62,7 +68,15 @@ namespace ImageCatalog_2
var rd = isDark ? (ResourceDictionary)Resources["DarkTheme"] : (ResourceDictionary)Resources["LightTheme"];
foreach (var key in rd.Keys)
{
Resources[key] = rd[key];
// If the theme dictionary uses suffixed keys (e.g. "WindowBackgroundBrush.Dark"),
// map them to the base key ("WindowBackgroundBrush") so existing DynamicResource lookups update.
string outKey = key?.ToString() ?? string.Empty;
if (outKey.EndsWith(".Light", StringComparison.OrdinalIgnoreCase))
outKey = outKey.Substring(0, outKey.Length - ".Light".Length);
else if (outKey.EndsWith(".Dark", StringComparison.OrdinalIgnoreCase))
outKey = outKey.Substring(0, outKey.Length - ".Dark".Length);
Resources[outKey] = rd[key];
}
}
catch
@ -274,6 +288,71 @@ namespace ImageCatalog_2
}
}
private void ToggleTheme_Click(object? sender, RoutedEventArgs e)
{
ToggleTheme();
}
private void ToggleTheme()
{
try
{
_isDarkTheme = !_isDarkTheme;
// Use MahApps ThemeManager to change the application theme (handles chrome and brushes)
try
{
var themeName = _isDarkTheme ? "Dark.Blue" : "Light.Blue";
ThemeManager.Current.ChangeTheme(System.Windows.Application.Current, themeName);
}
catch
{
// Fall back silently if ThemeManager isn't available
}
// Still apply local resource overrides so any app-specific keys update
ApplyTheme(_isDarkTheme);
}
catch
{
// ignore toggle failures
}
}
private void EnsureMahAppsResourcesLoaded()
{
try
{
var app = System.Windows.Application.Current;
if (app is null)
return;
var mds = app.Resources.MergedDictionaries;
// Helper to add if missing
void AddIfMissing(string uriString)
{
if (!mds.Any(d => d.Source is not null && d.Source.OriginalString.Equals(uriString, StringComparison.OrdinalIgnoreCase)))
{
mds.Add(new ResourceDictionary { Source = new Uri(uriString) });
}
}
AddIfMissing("pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml");
AddIfMissing("pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml");
// Ensure a default theme is present
if (!mds.Any(d => d.Source is not null && d.Source.OriginalString.IndexOf("/MahApps.Metro;component/Styles/Themes/", StringComparison.OrdinalIgnoreCase) >= 0))
{
AddIfMissing("pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml");
_isDarkTheme = false;
}
}
catch
{
// ignore; styling will fallback to local resources
}
}
private void UpdateLogoPreview(string? path)
{
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))

View file

@ -86,17 +86,42 @@ static class Program
//Application.Run(mainForm);
// -----------------------------------------------------------------------------
if (serviceProvider.GetService(typeof(ImageCatalog_2.MainWindow)) is ImageCatalog_2.MainWindow wpfMain)
// Create the WPF Application and merge MahApps resources BEFORE constructing the MainWindow
// so InitializeComponent sees the theme resources on first render.
var wpfApp = new System.Windows.Application();
try
{
wpfApp.Resources.MergedDictionaries.Add(new System.Windows.ResourceDictionary { Source = new Uri("pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml") });
wpfApp.Resources.MergedDictionaries.Add(new System.Windows.ResourceDictionary { Source = new Uri("pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml") });
// Default Light theme (can be replaced at runtime)
wpfApp.Resources.MergedDictionaries.Add(new System.Windows.ResourceDictionary { Source = new Uri("pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml") });
// Also notify ThemeManager about initial theme so chrome and MahApps brushes are applied
try
{
ControlzEx.Theming.ThemeManager.Current.ChangeTheme(wpfApp, "Light.Blue");
}
catch
{
// ignore if ThemeManager API isn't present
}
}
catch
{
// If resources fail to load (package not present at runtime), continue silently
}
// Now resolve the WPF MainWindow so its constructor runs with the application resources available
var wpfMain = serviceProvider.GetService(typeof(ImageCatalog_2.MainWindow)) as ImageCatalog_2.MainWindow;
if (wpfMain is not null)
{
// Start WPF app
var app = new System.Windows.Application();
app.Run(wpfMain);
}
else
{
var mainForm = serviceProvider.GetRequiredService<MainForm>();
Application.Run(mainForm);
wpfApp.Run(wpfMain);
return;
}
// Fallback to WinForms UI
var mainForm = serviceProvider.GetRequiredService<MainForm>();
System.Windows.Forms.Application.Run(mainForm);
}
private static void ConfigureServices(ServiceCollection services)