mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-09 12:46:47 +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>
315 lines
9.0 KiB
Markdown
315 lines
9.0 KiB
Markdown
# 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:
|
|
|
|
```csharp
|
|
// Store DispatcherQueue at app startup
|
|
private static DispatcherQueue _uiDispatcherQueue;
|
|
|
|
public static void InitializeDispatcher()
|
|
{
|
|
_uiDispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
|
}
|
|
```
|
|
|
|
Usage with thread-check pattern (from `Settings.Reload()`):
|
|
```csharp
|
|
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)
|
|
|
|
```csharp
|
|
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)
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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 `RasterizationScale` for 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)
|
|
|
|
```csharp
|
|
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)
|
|
|
|
```csharp
|
|
(appWindow.Presenter as OverlappedPresenter)?.Maximize();
|
|
(appWindow.Presenter as OverlappedPresenter)?.Minimize();
|
|
(appWindow.Presenter as OverlappedPresenter)?.Restore();
|
|
```
|
|
|
|
### Title Bar Customization
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
// 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`:
|
|
```xml
|
|
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
|
|
```
|
|
|
|
Create `Program.cs`:
|
|
```csharp
|
|
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.
|
|
|
|
```csharp
|
|
// DELETE (WPF pattern):
|
|
_imageResizerApp = new App();
|
|
// REPLACE with: Store DispatcherQueue explicitly (see Global DispatcherQueue Access above)
|
|
```
|