WinUIEx Documentation

repository·main·Indexed 21 days ago

https://github.com/dotmorten/winuiex

A collection of extension methods and classes designed to fill functional gaps in WinUI 3. WinUIEx provides enhancements for window management (WindowEx, WindowManager, HWND extensions), UI elements (Splash Screens, Tray Icons, Custom Backdrops), generic numeric controls (NumberBox<T>), OAuth Web Authentication, and a ported MediaPlayerElement. It also includes specific integration guidance and helpers for .NET MAUI Windows targets.

Tokens
10.3K
Snippets
30
Records
37
Agent score
73%

What's inside WinUIEx

  1. Overview of WinUIEx features

    main

    WinUIEx provides several capabilities to enhance WinUI 3 development:

    • Windowing: Includes Window extension methods, HWND (Window Handle) extensions, an extended WindowEx class, and a Window Manager.
    • UI Enhancements: Support for Splash Screens, Custom Window Backdrops, and Tray Icons (including WinUI 3 based context menus).
    • Controls: Generic number input controls (NumberBoxInt32, NumberBoxDecimal, and NumberBox<T>) that support expressions.
    • Authentication: OAuth Web Authentication support.
    • Developer Tools: Code analyzers for Windows App SDK APIs to provide guidance during development.

    Note: TitleBar control is deprecated in favor of WinAppSDK 1.7 TitleBar.

  2. Features of WinUIEx.WindowEx

    main

    The WinUIEx.WindowEx class provides several enhancements over the standard WinUI Window class, specifically targeting window management and DPI awareness:

    • XAML Integration: Set Title, Width, and Height directly in XAML.
    • DPI Awareness: Width and Height properties automatically adjust for system DPI. The class also handles DPI changes and window resizing correctly.
    • Sizing Constraints: Provides MinWidth and MinHeight get/set properties.
    • Taskbar & System Integration: Ability to set TaskBarIcon and control whether the window shows in the taskbar or app switchers.
    • Window State & Behavior: Convenience properties to control if a window can be maximized, minimized, or resized, change the presenter, or set the window to be 'Always on Top'.
    • Dialogs: Provides easy shortcuts for displaying Message Dialogs.
  3. Use the generic NumberBox<T> control

    main

    The NumberBox<T> is a port of the WinUI NumberBox control that supports generic numeric types. It provides validation, increment stepping, and support for inline calculations (addition, subtraction, multiplication, and division).

    Out of the box, the library provides:

    • NumberBoxDecimal for decimal types.
    • NumberBoxInt32 for 32-bit integers.

    You can use these controls in XAML by registering the WinUIEx namespace. Most functional behavior follows the standard Microsoft NumberBox documentation.

    <ex:NumberBoxInt32 Header="NumberBoxInt32" AllowNull="True"
                   AcceptsExpression="True" 
                   Value="{x:Bind VM.IntValue, Mode=TwoWay}"
                   Minimum="-10"
                   Maximum="10"
                   IsWrapEnabled="True"
                   Description="32bit Integer"
                   PlaceholderText="Enter a whole number" />
  4. Use the TrayIcon class for fine-grained control

    main

    For scenarios requiring more control than the WindowManager provides, use the TrayIcon class directly. Unlike the WindowManager approach, TrayIcon does not have default behaviors and is not automatically tied to a window.

    Key capabilities:

    • Create tray icons without an associated window (useful for window-less background apps).
    • Manage multiple icons for a single process.
    • Manually update icons and tooltips.
    • Handle specific events like Selected and ContextMenu.

    CRITICAL: You must call .Dispose() on all TrayIcon instances when your application closes. If you do not, the icon will remain in the system tray and the application process will not exit.

    // Example of a window-less app using TrayIcon
    public partial class App : Application
    {
        private TrayIcon icon;
        private Window? _window;
    
        protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
        {
            // Create icon without a window
            icon = new TrayIcon(1, "Images/StatusOK.ico", "Test");
            icon.IsVisible = true;
            
            icon.Selected += (s, e) => GetMainWindow().Activate();
            
            icon.ContextMenu += (w, e) =>
            {
                var flyout = new MenuFlyout();
                flyout.Items.Add(new MenuFlyoutItem() { Text = "Open" });
                ((MenuFlyoutItem)flyout.Items[0]).Click += (s, e) => GetMainWindow().Activate();
                flyout.Items.Add(new MenuFlyoutItem() { Text = "Quit App" });
                ((MenuFlyoutItem)flyout.Items[1]).Click += (s, e) =>
                {
                    _window?.Close();
                    icon.Dispose(); // Required to exit the process
                };
                e.Flyout = flyout;
            };
        }
    }
  5. Create custom backdrops with CompositionBrushBackdrop

    main

    For advanced background effects like animations or blurring, inherit from CompositionBrushBackdrop. You must override the CreateBrush method, which provides a Windows.UI.Composition.Compositor to create the brush used for the window backdrop.

    Custom animated backdrop

    By creating a brush and applying a CompositionColorKeyFrameAnimation within CreateBrush, you can create continuously animating backgrounds.

    Blurred backdrop

    You can create a blurred effect by returning a host backdrop brush using compositor.CreateHostBackdropBrush() inside the CreateBrush override.

    // Example: Custom animated composition-brush backdrop
    public class ColorAnimatedBackdrop : CompositionBrushBackdrop
    {
        protected override Windows.UI.Composition.CompositionBrush CreateBrush(Windows.UI.Composition.Compositor compositor)
        {
            var brush = compositor.CreateColorBrush(Windows.UI.Color.FromArgb(255,255,0,0));
            var animation = compositor.CreateColorKeyFrameAnimation();
            var easing = compositor.CreateLinearEasingFunction();
            animation.InsertKeyFrame(0, Colors.Red, easing);
            animation.InsertKeyFrame(.333f, Colors.Green, easing);
            animation.InsertKeyFrame(.667f, Colors.Blue, easing);
            animation.InsertKeyFrame(1, Colors.Red, easing);
            animation.InterpolationColorSpace = Windows.UI.Composition.CompositionColorSpace.Hsl;
            animation.Duration = TimeSpan.FromSeconds(15);
            animation.IterationBehavior = Windows.UI.Composition.AnimationIterationBehavior.Forever;
            brush.StartAnimation("Color", animation);
            return brush;
        }
    }
    
    // Example: Blurred composition-brush backdrop
    public class BlurredBackdrop : CompositionBrushBackdrop
    {
        protected override Windows.UI.Composition.CompositionBrush CreateBrush(Windows.UI.Composition.Compositor compositor)
            => compositor.CreateHostBackdropBrush();
    }
  6. How SimpleSplashScreen works and how to optimize it

    main

    The SimpleSplashScreen is a lightweight way to show an image during startup.

    For the fastest possible startup, you can bypass the XAML-generated Main method by defining the DISABLE_XAML_GENERATED_MAIN preprocessor directive. This allows you to show the splash screen as the very first operation in a custom static void Main method, even before the Microsoft.UI.Xaml.Application is fully initialized.

    #if DISABLE_XAML_GENERATED_MAIN
      public static class Program
      {
        [System.STAThreadAttribute]
        static void Main(string[] args)
        {
          // 1. Handle any necessary redirection (e.g. WebAuthenticator)
          if (WebAuthenticator.CheckOAuthRedirectionActivation(true))
            return;
    
          // 2. Show the splash screen immediately
          var fss = SimpleSplashScreen.ShowDefaultSplashScreen();
    
          WinRT.ComWrappersSupport.InitializeComWrappers();
    
          // 3. Start the XAML application
          Microsoft.UI.Xaml.Application.Start((p) => {
            var context = new Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext(Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread());
            System.Threading.SynchronizationContext.SetSynchronizationContext(context);
            new App(fss); // Pass the splash screen instance to your App class
          });
        }
      }
    #endif
  7. Use WindowManager to manage window properties

    main

    The WindowManager is a helper utility used to manage a window's size, position, and backdrop. While these features are built into the WindowEx class, you can use WindowManager to apply the same functionality to any existing window class, such as custom window implementations or .NET MAUI windows.

    To interact with a window, retrieve its manager using the WinUIEx.WindowManager.Get(window) method.

    // Get the manager for a specific window instance
    var manager = WinUIEx.WindowManager.Get(window);
    
    // Configure window properties
    manager.PersistenceId = "MainWindowPersistanceId";
    manager.MinWidth = 640;
    manager.MinHeight = 480;
    manager.Backdrop = new WinUIEx.MicaSystemBackdrop();
  8. Use SplashScreen for advanced XAML-based splash screens

    main

    The SplashScreen class allows you to create a custom splash screen using any XAML layout, enabling features like progress bars and status text.

    To implement this:

    1. Create a new Window and change its base class from Window to WinUIEx.SplashScreen in both XAML and code-behind.
    2. The SplashScreen constructor requires the Type of the window you want to launch after loading is complete.
    3. In App.xaml.cs, instantiate the SplashScreen and subscribe to its Completed event to capture the main window instance.
    4. Override the OnLoading method in your splash screen class to perform asynchronous setup work. The splash screen will automatically close once OnLoading completes.
    // 1. Define the SplashScreen class
    public sealed partial class MySplashScreen : WinUIEx.SplashScreen
    {
      public MySplashScreen(Type window) : base(window)
      {
        this.InitializeComponent();
      }
    
      // 2. Perform setup work here
      protected override async Task OnLoading()
      {
        for (int i = 0; i <= 100; i += 10)
        {
          statusText.Text = $"Loading {i}%...";
          progressBar.Value = i;
          await Task.Delay(100);
        }
        // Once this method finishes, the splash screen closes and MainWindow opens
      }
    }
    
    // 3. Launch it in App.xaml.cs
    protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
    {
      var splash = new MySplashScreen(typeof(MainWindow));
      splash.Completed += (s, e) => m_window = e;
    }
  9. Handle null values in NumberBox<T> bindings

    main

    The NumberBox<T>.Value property is of type T? (nullable) to support cases where a user clears the text field.

    Important: If you are binding the Value property to a non-nullable property in your ViewModel, you must set AllowNull="False" on the control to prevent binding errors.

  10. Replace Dispatcher with DispatcherQueue

    main

    In WinUI, the Dispatcher API is deprecated and will always return null because it was part of the UWP API surface area which is not supported in desktop apps. You must replace all usages of Dispatcher.RunAsync with DispatcherQueue.TryEnqueue using the Microsoft.UI.Dispatching namespace.

    Key Changes:

    • Old API: Windows.UI.Core.CoreDispatcher via Dispatcher.RunAsync.
    • New API: Microsoft.UI.Dispatching.DispatcherQueue via DispatcherQueue.TryEnqueue.
    • Priority Type: Change CoreDispatcherPriority to DispatcherQueuePriority.
    // Replace this:
    Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
    {
       // your code
    });
    
    // With this:
    DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
    {
       // your code
    });
  11. Implement Minimize-To-Tray or Launch-To-Tray

    main

    To create a 'Minimize-to-Tray' feature, react to the WindowManager.WindowStateChanged event. When the state becomes WindowState.Minimized, set AppWindow.IsShownInSwitchers to false to hide the app from task switchers (like Alt-Tab).

    To implement 'Launch-to-Tray', check your application settings during OnLaunched and set the WindowManager.WindowState to WindowState.Minimized immediately, rather than calling Activate() on the window.

    protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
    {
        _window = new MainWindow();
        var wm = WindowManager.Get(_window);
        wm.IsVisibleInTray = true; // Show app in tray
        
        // Minimize to tray logic:
        wm.WindowStateChanged += (s, state) =>
            wm.AppWindow.IsShownInSwitchers = state != WindowState.Minimized;
    
        if (MyAppSettings.LaunchToTray) // Launch minimized
            wm.WindowState = WindowState.Minimized;
        else
            _window.Activate();
    }
  12. Use SimpleSplashScreen for a basic image splash screen

    main

    Use SimpleSplashScreen to display a single image while your application loads. You can show the default splash screen defined in your app manifest or a custom image via a full file path.

    To use it, instantiate it in your App constructor. Once your main window is activated, you must manually hide or dispose of the splash screen to remove it from view.

    private SimpleSplashScreen fss { get; set; }
    
    public App()
    {
      // Use the default splash screen from the app manifest
      fss = SimpleSplashScreen.ShowDefaultSplashScreen();
      
      // OR use a custom image (must be a full path, no relative paths)
      // fss = SimpleSplashScreen.ShowSplashScreenImage(@"C:\path\to\image.png");
    
      this.InitializeComponent();
    }
    
    // In your Window activation logic, hide the splash screen
    private void Window_Activated(object sender, WindowActivatedEventArgs args)
    {
      ((Window)sender).Activated -= Window_Activated;
      fss?.Hide();
      fss = null;
    }