mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 01:36:31 +02:00
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request With this skills, we can easily enable AI to complete most of the tasks involved in migrating from WPF to WinUI3. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #46464 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed --------- Co-authored-by: Yu Leng <yuleng@microsoft.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9.0 KiB
9.0 KiB
Threading and Window Management Migration
Based on patterns from the ImageResizer migration.
Dispatcher → DispatcherQueue
API Mapping
| WPF | WinUI 3 |
|---|---|
Dispatcher.Invoke(Action) |
DispatcherQueue.TryEnqueue(Action) |
Dispatcher.BeginInvoke(Action) |
DispatcherQueue.TryEnqueue(Action) |
Dispatcher.Invoke(DispatcherPriority, Action) |
DispatcherQueue.TryEnqueue(DispatcherQueuePriority, Action) |
Dispatcher.CheckAccess() |
DispatcherQueue.HasThreadAccess |
Dispatcher.VerifyAccess() |
Check DispatcherQueue.HasThreadAccess (no exception-throwing method) |
Priority Mapping
WinUI 3 has only 3 levels: High, Normal, Low.
WPF DispatcherPriority |
WinUI 3 DispatcherQueuePriority |
|---|---|
Send |
High |
Normal / Input / Loaded / Render / DataBind |
Normal |
Background / ContextIdle / ApplicationIdle / SystemIdle |
Low |
Pattern: Global DispatcherQueue Access (from ImageResizer)
WPF provided Application.Current.Dispatcher globally. WinUI 3 requires explicit storage:
// Store DispatcherQueue at app startup
private static DispatcherQueue _uiDispatcherQueue;
public static void InitializeDispatcher()
{
_uiDispatcherQueue = DispatcherQueue.GetForCurrentThread();
}
Usage with thread-check pattern (from Settings.Reload()):
var currentDispatcher = DispatcherQueue.GetForCurrentThread();
if (currentDispatcher != null)
{
// Already on UI thread
ReloadCore(jsonSettings);
}
else if (_uiDispatcherQueue != null)
{
// Dispatch to UI thread
_uiDispatcherQueue.TryEnqueue(() => ReloadCore(jsonSettings));
}
else
{
// Fallback (e.g., CLI mode, no UI)
ReloadCore(jsonSettings);
}
Pattern: DispatcherQueue in ViewModels (from ProgressViewModel)
public class ProgressViewModel
{
private readonly DispatcherQueue _dispatcherQueue;
public ProgressViewModel()
{
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
}
private void OnProgressChanged(double progress)
{
_dispatcherQueue.TryEnqueue(() =>
{
Progress = progress;
// other UI updates...
});
}
}
Pattern: Async Dispatch (await)
// WPF
await this.Dispatcher.InvokeAsync(() => { /* UI work */ });
// WinUI 3 (using TaskCompletionSource)
var tcs = new TaskCompletionSource();
this.DispatcherQueue.TryEnqueue(() =>
{
try { /* UI work */ tcs.SetResult(); }
catch (Exception ex) { tcs.SetException(ex); }
});
await tcs.Task;
C++/WinRT Threading
| Old API | New API |
|---|---|
winrt::resume_foreground(CoreDispatcher) |
wil::resume_foreground(DispatcherQueue) |
CoreDispatcher.RunAsync() |
DispatcherQueue.TryEnqueue() |
Add Microsoft.Windows.ImplementationLibrary NuGet for wil::resume_foreground.
Window Management
WPF Window vs WinUI 3 Window
| Feature | WPF Window |
WinUI 3 Window |
|---|---|---|
| Base class | ContentControl → DependencyObject |
NOT a control, NOT a DependencyObject |
Resources property |
Yes | No — use root container's Resources |
DataContext property |
Yes | No — use root Page/UserControl |
VisualStateManager |
Yes | No — use inside child controls |
Load/Unload events |
Yes | No |
SizeToContent |
Yes (Height/Width/WidthAndHeight) |
No — must implement manually |
WindowState (min/max/normal) |
Yes | No — use AppWindow.Presenter |
WindowStyle |
Yes | No — use AppWindow title bar APIs |
ResizeMode |
Yes | No — use AppWindow.Presenter |
WindowStartupLocation |
Yes | No — calculate manually |
Icon |
Window.Icon |
AppWindow.SetIcon() |
Title |
Window.Title |
AppWindow.Title (or Window.Title) |
| Size (Width/Height) | Yes | No — use AppWindow.Resize() |
| Position (Left/Top) | Yes | No — use AppWindow.Move() |
IsDefault/IsCancel on buttons |
Yes | No — handle Enter/Escape in code-behind |
Getting AppWindow from Window
using Microsoft.UI;
using Microsoft.UI.Windowing;
using WinRT.Interop;
IntPtr hwnd = WindowNative.GetWindowHandle(window);
WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
AppWindow appWindow = AppWindow.GetFromWindowId(windowId);
Pattern: SizeToContent Replacement (from ImageResizer)
WinUI 3 has no SizeToContent. ImageResizer implemented a manual equivalent:
private void SizeToContent()
{
if (Content is not FrameworkElement content)
return;
// Measure desired content size
content.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
var desiredHeight = content.DesiredSize.Height + WindowChromeHeight + Padding;
// Account for DPI scaling
var scaleFactor = Content.XamlRoot.RasterizationScale;
var pixelHeight = (int)(desiredHeight * scaleFactor);
var pixelWidth = (int)(WindowWidth * scaleFactor);
// Resize via AppWindow
var hwnd = WindowNative.GetWindowHandle(this);
var windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
var appWindow = AppWindow.GetFromWindowId(windowId);
appWindow.Resize(new Windows.Graphics.SizeInt32(pixelWidth, pixelHeight));
}
Key details:
WindowChromeHeight≈ 32px for the title bar- Must multiply by
RasterizationScalefor DPI-aware sizing - Call
SizeToContent()after page navigation or content changes - Unsubscribe previous event handlers before subscribing new ones to avoid memory leaks
Window Positioning (Center Screen)
var displayArea = DisplayArea.GetFromWindowId(windowId, DisplayAreaFallback.Nearest);
var centerX = (displayArea.WorkArea.Width - appWindow.Size.Width) / 2;
var centerY = (displayArea.WorkArea.Height - appWindow.Size.Height) / 2;
appWindow.Move(new Windows.Graphics.PointInt32(centerX, centerY));
Window State (Minimize/Maximize)
(appWindow.Presenter as OverlappedPresenter)?.Maximize();
(appWindow.Presenter as OverlappedPresenter)?.Minimize();
(appWindow.Presenter as OverlappedPresenter)?.Restore();
Title Bar Customization
// Extend content into title bar
this.ExtendsContentIntoTitleBar = true;
this.SetTitleBar(AppTitleBar); // AppTitleBar is a XAML element
// Or via AppWindow API
if (AppWindowTitleBar.IsCustomizationSupported())
{
var titleBar = appWindow.TitleBar;
titleBar.ExtendsContentIntoTitleBar = true;
titleBar.ButtonBackgroundColor = Colors.Transparent;
}
Tracking the Main Window
public partial class App : Application
{
public static Window MainWindow { get; private set; }
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
MainWindow = new MainWindow();
MainWindow.Activate();
}
}
ContentDialog Requires XamlRoot
var dialog = new ContentDialog
{
Title = "Confirm",
Content = "Are you sure?",
PrimaryButtonText = "Yes",
CloseButtonText = "No",
XamlRoot = this.Content.XamlRoot // REQUIRED
};
var result = await dialog.ShowAsync();
File Pickers Require HWND
var picker = new FileOpenPicker();
picker.FileTypeFilter.Add(".jpg");
// REQUIRED for desktop apps
var hwnd = WindowNative.GetWindowHandle(App.MainWindow);
WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);
var file = await picker.PickSingleFileAsync();
Window Close Handling
// WPF
protected override void OnClosing(CancelEventArgs e) { e.Cancel = true; this.Hide(); }
// WinUI 3
this.AppWindow.Closing += (s, e) => { e.Cancel = true; this.AppWindow.Hide(); };
Custom Entry Point (DISABLE_XAML_GENERATED_MAIN)
ImageResizer uses a custom Program.cs entry point instead of the WinUI 3 auto-generated Main. This is needed for:
- CLI mode (process files without showing UI)
- Custom initialization before the WinUI 3 App starts
- Single-instance enforcement
Setup
In .csproj:
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
Create Program.cs:
public static class Program
{
[STAThread]
public static int Main(string[] args)
{
if (args.Length > 0)
{
// CLI mode — no UI
return RunCli(args);
}
// GUI mode
WinRT.ComWrappersSupport.InitializeComWrappers();
Application.Start((p) =>
{
var context = new DispatcherQueueSynchronizationContext(
DispatcherQueue.GetForCurrentThread());
SynchronizationContext.SetSynchronizationContext(context);
_ = new App();
});
return 0;
}
}
WPF App Constructor Removal
WPF modules often created new App() to initialize the WPF Application and get Application.Current.Dispatcher. This is no longer needed — the WinUI 3 Application.Start() handles this.
// DELETE (WPF pattern):
_imageResizerApp = new App();
// REPLACE with: Store DispatcherQueue explicitly (see Global DispatcherQueue Access above)