Files
AmiReel/MainWindow.xaml.cs
klevze f1f1be778b Fix missing taskbar/Alt+Tab icon
AppWindow.SetIcon(executablePath) was passing the .exe's own path, but that
API requires an actual .ico file path — passing an .exe silently does
nothing, so the window fell back to a generic placeholder icon in the
taskbar and Alt+Tab (the custom title bar icon fixed earlier is a separate,
XAML-drawn Image and was unaffected).

Embed branding/AmiReel.ico as a resource, extract it once to
%LOCALAPPDATA%\AmiReel\app-icon.ico on startup, and call
AppWindow.SetIcon() with that file path instead. Verified via WM_GETICON
against the running window's HWND that both the big and small icons now
resolve to the real AmiReel logo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 16:38:12 +02:00

174 lines
6.3 KiB
C#

using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Graphics;
using Windows.UI;
using WinRT.Interop;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace AmiReel;
/// <summary>
/// The application window. This hosts a Frame that displays pages. Add your
/// UI and logic to MainPage.xaml / MainPage.xaml.cs instead of here so you
/// can use Page features such as navigation events and the Loaded lifecycle.
/// </summary>
public sealed partial class MainWindow : Window
{
private bool _exitConfirmed;
public MainWindow()
{
InitializeComponent();
App.MainWindowInstance = this;
ExtendsContentIntoTitleBar = true;
SetTitleBar(AppTitleBar);
SetWindowIcon();
SetDefaultSizeAndPosition();
LoadTitleBarIcon();
// Navigate the root frame to the main page on startup.
RootFrame.Navigate(typeof(MainPage));
AppWindow.Closing += AppWindow_Closing;
}
/// <summary>
/// Sets the window's taskbar/Alt+Tab icon. <see cref="AppWindow.SetIcon(string)"/> requires an
/// actual .ico file path (an .exe path is silently ignored), so the embedded .ico is extracted
/// once to a stable file under %LOCALAPPDATA% and that path is used instead.
/// </summary>
private void SetWindowIcon()
{
try
{
string iconPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"AmiReel", "app-icon.ico");
if (!File.Exists(iconPath))
{
Directory.CreateDirectory(Path.GetDirectoryName(iconPath)!);
Assembly assembly = Assembly.GetExecutingAssembly();
using Stream? resource = assembly.GetManifestResourceStream("AmiReel.Assets.AppIcon.ico");
if (resource is null) return;
using FileStream file = File.Create(iconPath);
resource.CopyTo(file);
}
AppWindow.SetIcon(iconPath);
}
catch
{
// Non-critical: the window simply keeps whatever default icon Windows assigned.
}
}
/// <summary>
/// Loads the title bar icon from an embedded resource instead of an "ms-appx:///Assets/..."
/// URI. A single-file self-contained publish bundles Content/Assets items into the exe in a
/// way the ms-appx resource pipeline can no longer resolve at runtime, leaving the title bar
/// blank; a plain embedded resource stream works the same in every publish mode.
/// </summary>
private async void LoadTitleBarIcon()
{
try
{
Assembly assembly = Assembly.GetExecutingAssembly();
using Stream? stream = assembly.GetManifestResourceStream("AmiReel.Assets.TitleBarIcon.png");
if (stream is null) return;
BitmapImage bitmap = new();
await bitmap.SetSourceAsync(stream.AsRandomAccessStream());
TitleBarIcon.Source = bitmap;
}
catch
{
// Non-critical: the title bar simply shows no icon if this fails.
}
}
private void SetDefaultSizeAndPosition()
{
const int DefaultWidth = 1280;
const int DefaultHeight = 860;
const int MinWidth = 1100;
const int MinHeight = 780;
IntPtr hwnd = WindowNative.GetWindowHandle(this);
double scale = GetDpiForWindow(hwnd) / 96.0;
int width = (int)(DefaultWidth * scale);
int height = (int)(DefaultHeight * scale);
AppWindow.Resize(new SizeInt32(width, height));
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.PreferredMinimumWidth = (int)(MinWidth * scale);
presenter.PreferredMinimumHeight = (int)(MinHeight * scale);
}
DisplayArea? displayArea = DisplayArea.GetFromWindowId(Win32Interop.GetWindowIdFromWindow(hwnd), DisplayAreaFallback.Primary);
if (displayArea is not null)
{
int centerX = displayArea.WorkArea.X + (displayArea.WorkArea.Width - width) / 2;
int centerY = displayArea.WorkArea.Y + (displayArea.WorkArea.Height - height) / 2;
AppWindow.Move(new PointInt32(centerX, centerY));
}
}
[DllImport("user32.dll")]
private static extern int GetDpiForWindow(IntPtr hwnd);
public void ApplyTheme(bool isDark)
{
if (Content is FrameworkElement root)
root.RequestedTheme = isDark ? ElementTheme.Dark : ElementTheme.Light;
AppWindowTitleBar titleBar = AppWindow.TitleBar;
Color foreground = isDark ? Color.FromArgb(255, 0xF5, 0xF7, 0xFB) : Color.FromArgb(255, 0x12, 0x20, 0x33);
Color hoverBackground = isDark ? Color.FromArgb(255, 0x2E, 0x44, 0x6C) : Color.FromArgb(255, 0xD7, 0xE4, 0xF4);
titleBar.ButtonBackgroundColor = Colors.Transparent;
titleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
titleBar.ButtonForegroundColor = foreground;
titleBar.ButtonInactiveForegroundColor = foreground;
titleBar.ButtonHoverBackgroundColor = hoverBackground;
titleBar.ButtonHoverForegroundColor = foreground;
titleBar.ButtonPressedBackgroundColor = hoverBackground;
titleBar.ButtonPressedForegroundColor = foreground;
}
private async void AppWindow_Closing(AppWindow sender, AppWindowClosingEventArgs args)
{
if (_exitConfirmed) return;
args.Cancel = true;
FrameworkElement root = (FrameworkElement)Content;
ContentDialog dialog = DialogHelper.CreateStyled(RootFrame.XamlRoot, root.RequestedTheme);
dialog.Title = "Exit AmiReel?";
dialog.Content = "Are you sure you want to close the application?";
dialog.PrimaryButtonText = "Exit";
dialog.CloseButtonText = "Cancel";
dialog.DefaultButton = ContentDialogButton.Primary;
ContentDialogResult result = await dialog.ShowAsync();
if (result != ContentDialogResult.Primary) return;
_exitConfirmed = true;
Close();
}
}