From efe04334b1501cf93a382b54864d56f83af1e8f9 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 20 Jun 2026 09:10:48 +0200 Subject: [PATCH] Center Windows Hello prompt without visible parent --- src/AuthProviders/UIContext.cs | 100 +++++++++++++++++++++++++- src/AuthProviders/WinHelloProvider.cs | 2 +- src/Utilities/Win32Window.cs | 13 ++++ 3 files changed, 112 insertions(+), 3 deletions(-) diff --git a/src/AuthProviders/UIContext.cs b/src/AuthProviders/UIContext.cs index 853b62b..f7effa3 100644 --- a/src/AuthProviders/UIContext.cs +++ b/src/AuthProviders/UIContext.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Drawing; using System.Windows.Forms; namespace KeePassWinHello @@ -10,6 +11,9 @@ internal sealed class UIContext : IWin32Window public string Message { get; private set; } public HWND ParentWindowHandle { get; private set; } + private readonly Control _uiThreadControl; + private Form _promptParentWindow; + IntPtr IWin32Window.Handle { get { return ParentWindowHandle.Value; } } public UIContext(string message, HWND windowHandle) @@ -17,6 +21,97 @@ public UIContext(string message, HWND windowHandle) Message = message; ParentWindowHandle = windowHandle; } + + public UIContext(string message, IWin32Window parentWindow) + : this(message, new HWND(parentWindow.Handle)) + { + _uiThreadControl = parentWindow as Control; + } + + public HWND PromptParentWindowHandle + { + get + { + if (Win32Window.IsVisibleAndNotMinimized(ParentWindowHandle)) + return ParentWindowHandle; + + return EnsurePromptParentWindowHandle(); + } + } + + public void DisposePromptParentWindow() + { + if (ShouldInvokeOnUIThread()) + { + _uiThreadControl.Invoke(new MethodInvoker(DisposePromptParentWindowOnCurrentThread)); + return; + } + + DisposePromptParentWindowOnCurrentThread(); + } + + private void DisposePromptParentWindowOnCurrentThread() + { + if (_promptParentWindow != null) + { + _promptParentWindow.Dispose(); + _promptParentWindow = null; + } + } + + private HWND EnsurePromptParentWindowHandle() + { + if (ShouldInvokeOnUIThread()) + { + return (HWND)_uiThreadControl.Invoke(new Func(EnsurePromptParentWindowHandleOnCurrentThread)); + } + + return EnsurePromptParentWindowHandleOnCurrentThread(); + } + + private HWND EnsurePromptParentWindowHandleOnCurrentThread() + { + if (_promptParentWindow == null || _promptParentWindow.IsDisposed) + _promptParentWindow = PromptParentForm.CreateCentered(); + + return new HWND(_promptParentWindow.Handle); + } + + private bool ShouldInvokeOnUIThread() + { + return _uiThreadControl != null + && !_uiThreadControl.IsDisposed + && _uiThreadControl.InvokeRequired; + } + + private sealed class PromptParentForm : Form + { + private PromptParentForm() + { + FormBorderStyle = FormBorderStyle.None; + Opacity = 0.01; + ShowInTaskbar = false; + Size = new Size(1, 1); + StartPosition = FormStartPosition.Manual; + Text = Settings.ProductName; + } + + protected override bool ShowWithoutActivation + { + get { return true; } + } + + public static PromptParentForm CreateCentered() + { + PromptParentForm form = new PromptParentForm(); + Rectangle workingArea = Screen.FromPoint(Cursor.Position).WorkingArea; + form.Location = new Point( + workingArea.Left + (workingArea.Width - form.Width) / 2, + workingArea.Top + (workingArea.Height - form.Height) / 2); + form.Show(); + return form; + } + } } internal sealed class UIContextManager @@ -46,7 +141,7 @@ public UIContext CurrentContext public IDisposable PushContext(string message, IWin32Window parentWindow) { - var context = new UIContext(message, new HWND(parentWindow.Handle)); + var context = new UIContext(message, parentWindow); _contexts.AddFirst(context); return new Disposer(this, context); } @@ -66,7 +161,8 @@ public void Dispose() { bool removed = _contextManager._contexts.Remove(_context); Debug.Assert(removed); + _context.DisposePromptParentWindow(); } } } -} \ No newline at end of file +} diff --git a/src/AuthProviders/WinHelloProvider.cs b/src/AuthProviders/WinHelloProvider.cs index 466d56c..920de5f 100644 --- a/src/AuthProviders/WinHelloProvider.cs +++ b/src/AuthProviders/WinHelloProvider.cs @@ -482,7 +482,7 @@ private void ApplyUIContext(SafeNCryptKeyHandle ngcKeyHandle, bool retryMessage var uiContext = _uiContextManager.CurrentContext; if (uiContext != null) { - IntPtr parentWindowHandle = uiContext.ParentWindowHandle.Value; + IntPtr parentWindowHandle = uiContext.PromptParentWindowHandle.Value; if (parentWindowHandle != IntPtr.Zero) { byte[] handle = BitConverter.GetBytes(IntPtr.Size == 8 ? parentWindowHandle.ToInt64() : parentWindowHandle.ToInt32()); diff --git a/src/Utilities/Win32Window.cs b/src/Utilities/Win32Window.cs index bca2bcd..a1b83a3 100644 --- a/src/Utilities/Win32Window.cs +++ b/src/Utilities/Win32Window.cs @@ -99,6 +99,13 @@ public static Win32Window GetOrNull(HWND hwnd) return null; } + public static bool IsVisibleAndNotMinimized(HWND hwnd) + { + return hwnd.IsValid + && WinAPI.IsWindowVisible(hwnd.Value).Result + && !WinAPI.IsIconic(hwnd.Value).Result; + } + #endregion Creation @@ -233,6 +240,12 @@ private static class WinAPI [DllImport(User32, SetLastError = true, CharSet = CharSet.Unicode)] public static extern HWND FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string lpClassName, string lpWindowName); + + [DllImport(User32, SetLastError = true)] + public static extern BOOL IsWindowVisible(IntPtr hWnd); + + [DllImport(User32, SetLastError = true)] + public static extern BOOL IsIconic(IntPtr hWnd); } } }