From 9749093c720f18d1da72eab7388bdbcba5b3d8ba Mon Sep 17 00:00:00 2001 From: Caelan Sayler Date: Tue, 21 Dec 2021 14:41:33 +0000 Subject: [PATCH] Bug fix for non-96 dpi displays, and non-96 dpi images --- .editorconfig | 3 + src/SquirrelCli/app.manifest | 11 +- src/Update/ISplashWindow.cs | 16 + src/Update/Program.cs | 59 +-- src/Update/SplashWindow.cs | 338 ------------- src/Update/Update.csproj | 2 +- src/Update/Windows/ThreadDpiScalingContext.cs | 223 +++++++++ src/Update/{ => Windows}/User32MessageBox.cs | 2 +- src/Update/Windows/User32SplashWindow.cs | 450 ++++++++++++++++++ src/Update/app.manifest | 11 +- 10 files changed, 726 insertions(+), 389 deletions(-) create mode 100644 src/Update/ISplashWindow.cs delete mode 100644 src/Update/SplashWindow.cs create mode 100644 src/Update/Windows/ThreadDpiScalingContext.cs rename src/Update/{ => Windows}/User32MessageBox.cs (99%) create mode 100644 src/Update/Windows/User32SplashWindow.cs diff --git a/.editorconfig b/.editorconfig index 8bde9ec1..de42338a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -194,4 +194,7 @@ cpp_space_around_ternary_operator = insert cpp_wrap_preserve_blocks = one_liners [*.csproj] +indent_size = 2 + +[*.manifest] indent_size = 2 \ No newline at end of file diff --git a/src/SquirrelCli/app.manifest b/src/SquirrelCli/app.manifest index c2cd0336..8e583b26 100644 --- a/src/SquirrelCli/app.manifest +++ b/src/SquirrelCli/app.manifest @@ -11,6 +11,7 @@ + @@ -30,6 +31,14 @@ + + + + True + PerMonitorV2 + + + @@ -44,4 +53,4 @@ - + \ No newline at end of file diff --git a/src/Update/ISplashWindow.cs b/src/Update/ISplashWindow.cs new file mode 100644 index 00000000..4d01ea56 --- /dev/null +++ b/src/Update/ISplashWindow.cs @@ -0,0 +1,16 @@ +using System; + +namespace Squirrel.Update +{ + internal interface ISplashWindow : IDisposable + { + void Show(); + void Hide(); + void SetNoProgress(); + void SetProgressIndeterminate(); + void SetProgress(ulong completed, ulong total); + void ShowErrorDialog(string title, string message); + void ShowInfoDialog(string title, string message); + bool ShowQuestionDialog(string title, string message); + } +} diff --git a/src/Update/Program.cs b/src/Update/Program.cs index 8f7ceb08..013cdab8 100644 --- a/src/Update/Program.cs +++ b/src/Update/Program.cs @@ -14,7 +14,6 @@ using System.Threading; using System.Threading.Tasks; using Squirrel.NuGet; using Squirrel.Lib; -using System.Drawing; namespace Squirrel.Update { @@ -154,41 +153,7 @@ namespace Squirrel.Update } using var _t = Utility.WithTempDirectory(out var tempFolder); - - // show splash screen - SplashWindow splash = null; - if (!silentInstall && info.SplashImageBytes?.Length > 0) { - Log.Info($"Showing splash window"); - splash = new SplashWindow( - info.SetupIconBytes?.Length > 0 ? new Icon(new MemoryStream(info.SetupIconBytes)) : null, - (Bitmap) Image.FromStream(new MemoryStream(info.SplashImageBytes))); - splash.Show(); - splash.SetProgressIndeterminate(); - } - - void showUserMsg(bool error, string message, string title) - { - if (!silentInstall) { - Log.Info("User shown message: " + message); - User32MessageBox.Show(splash?.Handle ?? IntPtr.Zero, message, info.AppFriendlyName + " - " + title, User32MessageBox.MessageBoxButtons.OK, - error ? User32MessageBox.MessageBoxIcon.Error : User32MessageBox.MessageBoxIcon.Information); - } else { - Log.Info("User message suppressed (updater in silent mode): " + message); - } - } - - bool askUserQuestion(string message, string title) - { - if (!silentInstall) { - var result = User32MessageBox.Show(splash?.Handle ?? IntPtr.Zero, message, info.AppFriendlyName + " - " + title, User32MessageBox.MessageBoxButtons.OKCancel, - User32MessageBox.MessageBoxIcon.Question, User32MessageBox.MessageBoxResult.Cancel); - Log.Info("User prompted: '" + message + "' -- User answered " + result.ToString()); - return User32MessageBox.MessageBoxResult.OK == result; - } else { - Log.Info("User prompt suppressed (updater in silent mode): '" + message + "' -- Automatically answering Cancel."); - return false; - } - } + ISplashWindow splash = new Windows.User32SplashWindow(info.AppFriendlyName, silentInstall, info.SetupIconBytes, info.SplashImageBytes); var missingFrameworks = info.RequiredFrameworks .Select(f => Runtimes.GetRuntimeByName(f)) @@ -203,7 +168,7 @@ namespace Squirrel.Update $"Would you like to install these now?" : $"{info.AppFriendlyName} requires {missingFrameworks.First().DisplayName} installed to continue, would you like to install it now?"; - if (!askUserQuestion(message, "Missing System Components")) { + if (!splash.ShowQuestionDialog("Missing System Components", message)) { return; // user cancelled install } @@ -212,13 +177,13 @@ namespace Squirrel.Update // iterate through each missing dependency and download/run the installer. foreach (var f in missingFrameworks) { var localPath = Path.Combine(tempFolder, f.Id + ".exe"); - await f.DownloadToFile(localPath, e => splash?.SetProgress((ulong) e.BytesReceived, (ulong) e.TotalBytesToReceive)); - splash?.SetProgressIndeterminate(); + await f.DownloadToFile(localPath, e => splash.SetProgress((ulong) e.BytesReceived, (ulong) e.TotalBytesToReceive)); + splash.SetProgressIndeterminate(); // hide splash screen while the runtime installer is running so the user can see progress - splash?.Hide(); + splash.Hide(); var exitcode = await f.InvokeInstaller(localPath, silentInstall); - splash?.Show(); + splash.Show(); if (exitcode == RuntimeInstallResult.RestartRequired) { rebootRequired = true; @@ -230,21 +195,21 @@ namespace Squirrel.Update RuntimeInstallResult.SystemDoesNotMeetRequirements => $"This computer does not meet the system requirements for {f.DisplayName}.", _ => $"{f.DisplayName} installer exited with error code '{exitcode}'.", }; - showUserMsg(true, rtmsg, $"Error installing {f.DisplayName}"); + splash.ShowErrorDialog($"Error installing {f.DisplayName}", rtmsg); return; } } if (rebootRequired) { // TODO: automatic restart setup after reboot - showUserMsg(false, $"A restart is required before Setup can continue.", "Restart required"); + splash.ShowInfoDialog("Restart required", $"A restart is required before Setup can continue."); return; } } // setup package source directory Log.Info($"Starting package install from directory " + tempFolder); - splash?.SetProgressIndeterminate(); + splash.SetProgressIndeterminate(); string packagePath = Path.Combine(tempFolder, info.BundledPackageName); File.WriteAllBytes(packagePath, info.BundledPackageBytes); var entry = ReleaseEntry.GenerateFromFile(packagePath); @@ -253,12 +218,12 @@ namespace Squirrel.Update var progressSource = new ProgressSource(); progressSource.Progress += (e, p) => { // post install hooks are about to be run (app will start) - if (p >= 90) splash?.Close(); - else splash?.SetProgress((ulong) p, 90); + if (p >= 90) splash.Hide(); + else splash.SetProgress((ulong) p, 90); }; await Install(silentInstall, progressSource, tempFolder); - splash?.Close(); + splash.Dispose(); } static async Task Install(bool silentInstall, ProgressSource progressSource, string sourceDirectory = null) diff --git a/src/Update/SplashWindow.cs b/src/Update/SplashWindow.cs deleted file mode 100644 index c170a3c2..00000000 --- a/src/Update/SplashWindow.cs +++ /dev/null @@ -1,338 +0,0 @@ -using System; -using System.ComponentModel; -using System.Drawing; -using System.Drawing.Imaging; -using System.Runtime.InteropServices; -using System.Threading; -using Vanara.PInvoke; -using static Vanara.PInvoke.Kernel32; -using static Vanara.PInvoke.ShowWindowCommand; -using static Vanara.PInvoke.User32; -using static Vanara.PInvoke.User32.MonitorFlags; -using static Vanara.PInvoke.User32.SetWindowPosFlags; -using static Vanara.PInvoke.User32.SPI; -using static Vanara.PInvoke.User32.WindowClassStyles; -using static Vanara.PInvoke.User32.WindowMessage; -using static Vanara.PInvoke.User32.WindowStyles; -using static Vanara.PInvoke.User32.WindowStylesEx; -using POINT = System.Drawing.Point; - -namespace Squirrel.Update -{ - internal unsafe class SplashWindow - { - public IntPtr Handle => _hwnd != null ? _hwnd.DangerousGetHandle() : IntPtr.Zero; - - private SafeHWND _hwnd; - private Exception _error; - private Thread _thread; - private uint _threadId; - private POINT _ptMouseDown; - private ITaskbarList3 _taskbarList; - private double _progress; - - private readonly ManualResetEvent _signal; - private readonly Bitmap _img; - private readonly Icon _icon; - - private const int OPERATION_TIMEOUT = 5000; - private const string WINDOW_CLASS_NAME = "SquirrelSplashWindow"; - - public SplashWindow(Icon icon, Bitmap splashImg) - { - _icon = icon; - _img = splashImg; - _signal = new ManualResetEvent(false); - _taskbarList = (ITaskbarList3) new CTaskbarList(); - _taskbarList.HrInit(); - } - - public void Show() - { - if (_thread == null) { - _error = null; - _signal.Reset(); - _thread = new Thread(ThreadProc); - _thread.IsBackground = true; - _thread.Start(); - if (!_signal.WaitOne(OPERATION_TIMEOUT)) { - if (_error != null) throw _error; - else throw new Exception("Timeout waiting for splash window to open"); - } - if (_error != null) throw _error; - } else { - ShowWindow(_hwnd, SW_SHOW); - } - } - - public void Hide() - { - if (_thread == null) return; - ShowWindow(_hwnd, SW_HIDE); - } - - public void SetNoProgress() - { - if (_thread == null) return; - var h = _hwnd.DangerousGetHandle(); - _taskbarList.SetProgressState(h, ThumbnailProgressState.NoProgress); - _progress = 0; - InvalidateRect(_hwnd, null, false); - } - - public void SetProgressIndeterminate() - { - if (_thread == null) return; - var h = _hwnd.DangerousGetHandle(); - _taskbarList.SetProgressState(h, ThumbnailProgressState.Indeterminate); - _progress = 0; - InvalidateRect(_hwnd, null, false); - } - - public void SetProgress(ulong completed, ulong total) - { - if (_thread == null) return; - var h = _hwnd.DangerousGetHandle(); - _taskbarList.SetProgressState(h, ThumbnailProgressState.Normal); - _taskbarList.SetProgressValue(h, completed, total); - _progress = completed / (double) total; - InvalidateRect(_hwnd, null, false); - } - - public void Close() - { - if (_thread == null) return; - PostThreadMessage(_threadId, (uint) WM_QUIT, IntPtr.Zero, IntPtr.Zero); - _thread.Join(OPERATION_TIMEOUT); - _thread = null; - _error = null; - _threadId = 0; - _hwnd = null; - _signal.Reset(); - } - - private void ThreadProc() - { - try { - _threadId = GetCurrentThreadId(); - CreateWindow(); - } catch (Exception ex) { - _error = ex; - _signal.Set(); - } - } - - private void CreateWindow() - { - int imgWidth = _img.Width; - int imgHeight = _img.Height; - - var instance = GetModuleHandle(null); - - WNDCLASS wndClass = new WNDCLASS { - style = CS_HREDRAW | CS_VREDRAW, - lpfnWndProc = WndProc, - hInstance = instance, - //hbrBackground = COLOR_WINDOW - hCursor = LoadCursor(HINSTANCE.NULL, IDC_APPSTARTING), - lpszClassName = WINDOW_CLASS_NAME, - hIcon = _icon != null ? new HICON(_icon.Handle) : LoadIcon(instance, IDI_APPLICATION), - }; - - if (RegisterClass(wndClass) == 0) { - var clhr = GetLastError(); - if (clhr != 0x00000582) // already registered - throw clhr.GetException("Unable to register splash window class"); - } - - // try to find monitor where mouse is - GetCursorPos(out var point); - var hMonitor = MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST); - MONITORINFO mi = new MONITORINFO { cbSize = 40 /*sizeof(MONITORINFO)*/ }; - RECT rcArea = default; - - if (GetMonitorInfo(hMonitor, ref mi)) { - rcArea.left = (mi.rcMonitor.right + mi.rcMonitor.left - imgWidth) / 2; - rcArea.top = (mi.rcMonitor.top + mi.rcMonitor.bottom - imgHeight) / 2; - } else { - SystemParametersInfo(SPI_GETWORKAREA, 0, new IntPtr(&rcArea), 0); - rcArea.left = (rcArea.right + rcArea.left - imgWidth) / 2; - rcArea.top = (rcArea.top + rcArea.bottom - imgHeight) / 2; - } - - _hwnd = CreateWindowEx( - /*WS_EX_TOOLWINDOW |*/ WS_EX_TOPMOST, - WINDOW_CLASS_NAME, - "Setup", - WS_CLIPCHILDREN | WS_POPUP, - rcArea.left, rcArea.top, imgWidth, imgHeight, - HWND.NULL, - HMENU.NULL, - instance, - IntPtr.Zero); - - if (_hwnd.IsInvalid) { - throw new Win32Exception(); - } - - ShowWindow(_hwnd, SW_SHOWNOACTIVATE); - - // check for animation properties - var pDimensionIDs = _img.FrameDimensionsList; - var frameDimension = new FrameDimension(pDimensionIDs[0]); - var frameCount = _img.GetFrameCount(frameDimension); - var delayProperty = _img.GetPropertyItem(0x5100 /*PropertyTagFrameDelay*/); - - ManualResetEvent exitGif = new ManualResetEvent(false); - Thread gif = new Thread(() => { - fixed (byte* frameDelayBytes = delayProperty.Value) { - int framePosition = 0; - int* frameDelays = (int*) frameDelayBytes; - while (true) { - - lock (_img) _img.SelectActiveFrame(frameDimension, framePosition++); - InvalidateRect(_hwnd, null, false); - - if (framePosition == frameCount) - framePosition = 0; - - int lPause = frameDelays[framePosition] * 10; - if (exitGif.WaitOne(lPause)) - return; - } - } - }); - - // start gif animation - if (frameCount > 1 && delayProperty?.Value != null && (delayProperty.Value.Length / 4) >= frameCount) { - gif.IsBackground = true; - gif.Start(); - } - - MSG msg; - PeekMessage(out msg, _hwnd, 0, 0, 0); // invoke creating message queue - - _signal.Set(); // signal to calling thread that the window has been created - - bool bRet; - while ((bRet = GetMessage(out msg, HWND.NULL, 0, 0)) != false) { - if (msg.message == (uint) WM_QUIT) - break; - - TranslateMessage(msg); - DispatchMessage(msg); - } - - exitGif.Set(); - gif.Join(1000); - DestroyWindow(_hwnd); - } - - private nint WndProc(HWND hwnd, uint uMsg, nint wParam, nint lParam) - { - switch (uMsg) { - - case (uint) WM_PAINT: - GetWindowRect(hwnd, out var r); - using (var buffer = new Bitmap(r.Width, r.Height)) - using (var brush = new SolidBrush(Color.FromArgb(160, Color.LimeGreen))) - using (var g = Graphics.FromImage(buffer)) - using (var wnd = Graphics.FromHwnd(hwnd.DangerousGetHandle())) { - lock (_img) g.DrawImage(_img, 0, 0); - if (_progress > 0) { - g.FillRectangle(brush, new Rectangle(0, r.Height - 10, (int) (r.Width * _progress), 10)); - } - wnd.DrawImage(buffer, 0, 0); - } - - ValidateRect(hwnd, null); - return 0; - - case (uint) WM_LBUTTONDOWN: - GetCursorPos(out _ptMouseDown); - SetCapture(hwnd); - return 0; - - case (uint) WM_MOUSEMOVE: - if (GetCapture() == hwnd) { - GetWindowRect(hwnd, out var rcWnd); - GetCursorPos(out var pt); - - POINT ptDown = _ptMouseDown; - var xdiff = ptDown.X - pt.X; - var ydiff = ptDown.Y - pt.Y; - - SetWindowPos(hwnd, HWND.HWND_TOP, rcWnd.left - xdiff, rcWnd.top - ydiff, 0, 0, - SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER); - - _ptMouseDown = pt; - } - return 0; - - case (uint) WM_LBUTTONUP: - ReleaseCapture(); - return 0; - - } - - return DefWindowProc(hwnd, uMsg, wParam, lParam); - } - - [ComImportAttribute()] - [GuidAttribute("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf")] - [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)] - internal interface ITaskbarList3 - { - // ITaskbarList - [PreserveSig] - void HrInit(); - [PreserveSig] - void AddTab(IntPtr hwnd); - [PreserveSig] - void DeleteTab(IntPtr hwnd); - [PreserveSig] - void ActivateTab(IntPtr hwnd); - [PreserveSig] - void SetActiveAlt(IntPtr hwnd); - - // ITaskbarList2 - [PreserveSig] - void MarkFullscreenWindow( - IntPtr hwnd, - [MarshalAs(UnmanagedType.Bool)] bool fFullscreen); - - // ITaskbarList3 - void SetProgressValue(IntPtr hwnd, UInt64 ullCompleted, UInt64 ullTotal); - void SetProgressState(IntPtr hwnd, ThumbnailProgressState tbpFlags); - } - - [GuidAttribute("56FDF344-FD6D-11d0-958A-006097C9A090")] - [ClassInterfaceAttribute(ClassInterfaceType.None)] - [ComImportAttribute()] - internal class CTaskbarList { } - - public enum ThumbnailProgressState - { - /// - /// No progress is displayed. - /// - NoProgress = 0, - /// - /// The progress is indeterminate (marquee). - /// - Indeterminate = 0x1, - /// - /// Normal progress is displayed. - /// - Normal = 0x2, - /// - /// An error occurred (red). - /// - Error = 0x4, - /// - /// The operation is paused (yellow). - /// - Paused = 0x8 - } - } -} diff --git a/src/Update/Update.csproj b/src/Update/Update.csproj index a1fa4bf8..38484656 100644 --- a/src/Update/Update.csproj +++ b/src/Update/Update.csproj @@ -16,8 +16,8 @@ - + diff --git a/src/Update/Windows/ThreadDpiScalingContext.cs b/src/Update/Windows/ThreadDpiScalingContext.cs new file mode 100644 index 00000000..e58b3ec3 --- /dev/null +++ b/src/Update/Windows/ThreadDpiScalingContext.cs @@ -0,0 +1,223 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using static Vanara.PInvoke.User32; +using static Vanara.PInvoke.SHCore; + +// from clowd-windows/Clowd.PlatformUtil/Windows/ThreadDpiScalingContext.cs + +namespace Squirrel.Update.Windows +{ + public enum ThreadScalingMode + { + Unaware, + SystemAware, + PerMonitorAware, + PerMonitorV2Aware, + UnawareGdiScaled, + } + + public static class ThreadDpiScalingContext + { + private static readonly DPI_AWARENESS_CONTEXT DPI_AWARENESS_CONTEXT_UNAWARE = new DPI_AWARENESS_CONTEXT((IntPtr)(-1)); + private static readonly DPI_AWARENESS_CONTEXT DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = new DPI_AWARENESS_CONTEXT((IntPtr)(-2)); + private static readonly DPI_AWARENESS_CONTEXT DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = new DPI_AWARENESS_CONTEXT((IntPtr)(-3)); + private static readonly DPI_AWARENESS_CONTEXT DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = new DPI_AWARENESS_CONTEXT((IntPtr)(-4)); + private static readonly DPI_AWARENESS_CONTEXT DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED = new DPI_AWARENESS_CONTEXT((IntPtr)(-5)); + + private static ThreadScalingMode Get1607ThreadAwarenessContext() + { + var context = GetThreadDpiAwarenessContext(); + + if (AreDpiAwarenessContextsEqual(context, DPI_AWARENESS_CONTEXT_UNAWARE)) + { + return ThreadScalingMode.Unaware; + } + else if (AreDpiAwarenessContextsEqual(context, DPI_AWARENESS_CONTEXT_SYSTEM_AWARE)) + { + return ThreadScalingMode.SystemAware; + } + else if (AreDpiAwarenessContextsEqual(context, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) + { + return ThreadScalingMode.PerMonitorAware; + } + else if (AreDpiAwarenessContextsEqual(context, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) + { + return ThreadScalingMode.PerMonitorV2Aware; + } + else if (AreDpiAwarenessContextsEqual(context, DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED)) + { + return ThreadScalingMode.UnawareGdiScaled; + } + else + { + throw new ArgumentOutOfRangeException("DPI_AWARENESS_CONTEXT"); + } + } + + private static void Set1607ThreadAwarenessContext(ThreadScalingMode mode) + { + var ctx = mode switch + { + ThreadScalingMode.Unaware => DPI_AWARENESS_CONTEXT_UNAWARE, + ThreadScalingMode.SystemAware => DPI_AWARENESS_CONTEXT_SYSTEM_AWARE, + ThreadScalingMode.PerMonitorAware => DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, + ThreadScalingMode.PerMonitorV2Aware => DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, + ThreadScalingMode.UnawareGdiScaled => DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED, + _ => throw new ArgumentOutOfRangeException("mode"), + }; + + var ctxOld = SetThreadDpiAwarenessContext(ctx); + if (((IntPtr)ctxOld) == IntPtr.Zero) + throw new Exception("Failed to update thread awareness context"); + } + + private static void SetShcoreAwareness(ThreadScalingMode mode) + { + var aw = mode switch + { + ThreadScalingMode.Unaware => PROCESS_DPI_AWARENESS.PROCESS_DPI_UNAWARE, + ThreadScalingMode.SystemAware => PROCESS_DPI_AWARENESS.PROCESS_SYSTEM_DPI_AWARE, + ThreadScalingMode.PerMonitorAware => PROCESS_DPI_AWARENESS.PROCESS_PER_MONITOR_DPI_AWARE, + ThreadScalingMode.PerMonitorV2Aware => PROCESS_DPI_AWARENESS.PROCESS_PER_MONITOR_DPI_AWARE, + ThreadScalingMode.UnawareGdiScaled => PROCESS_DPI_AWARENESS.PROCESS_DPI_UNAWARE, + _ => throw new ArgumentOutOfRangeException("mode"), + }; + + Marshal.ThrowExceptionForHR((int)SetProcessDpiAwareness(aw)); + } + + private static ThreadScalingMode GetShcoreAwareness() + { + Marshal.ThrowExceptionForHR((int)GetProcessDpiAwareness(default, out var aw)); + return aw switch + { + PROCESS_DPI_AWARENESS.PROCESS_DPI_UNAWARE => ThreadScalingMode.Unaware, + PROCESS_DPI_AWARENESS.PROCESS_SYSTEM_DPI_AWARE => ThreadScalingMode.SystemAware, + PROCESS_DPI_AWARENESS.PROCESS_PER_MONITOR_DPI_AWARE => ThreadScalingMode.PerMonitorAware, + _ => throw new ArgumentOutOfRangeException("DPI_AWARENESS"), + }; + } + + /// + /// Gets the current thread scaling / dpi awareness. + /// + public static ThreadScalingMode GetCurrentThreadScalingMode() + { + try + { + return Get1607ThreadAwarenessContext(); + } + catch { } + + try + { + return GetShcoreAwareness(); + } + catch { } + + return ThreadScalingMode.Unaware; + } + + /// + /// Sets the current thread scaling / dpi awareness. This will only succeed if the OS is windows 8.1 or above, + /// and if no winapi functions which perform scaling have been called on this thread. + /// + public static bool SetCurrentThreadScalingMode(ThreadScalingMode mode) + { + try + { + Set1607ThreadAwarenessContext(mode); + return true; + } + catch { } + + try + { + // technically, on older versions of windows, this will update the dpi awareness for the whole process. + // if an exe manifest is present, or on subsequent calls, this will fail. + // in newer versions of windows, dpi is thread-specific, so this logic is equivilant to SetThreadDpiAwarenessContext + SetShcoreAwareness(mode); + return true; + } + catch { } + + // unable to set awareness, this could be because a UI has been created already or because the + // api's we need do not yet exist (older windows SDK's) + return false; + } + + public static void RunScalingAware(ThreadScalingMode mode, Action task) + { + RunScalingAware(mode, () => { task(); return true; }); + } + + public static T RunScalingAware(ThreadScalingMode mode, Func task) + { + var thread = new AwareThread(mode, task); + return thread.GetResult(); + } + + public static Task RunScalingAwareAsync(ThreadScalingMode mode, Action task) + { + return RunScalingAwareAsync(mode, () => { task(); return true; }); + } + + public static Task RunScalingAwareAsync(ThreadScalingMode mode, Func task) + { + var thread = new AwareThread(mode, task); + return thread.Wait(); + } + + private class AwareThread + { + ThreadScalingMode scaling; + Func job; + TaskCompletionSource source; + Thread thread; + + public AwareThread(ThreadScalingMode mode, Func task) + { + scaling = mode; + job = task; + source = new TaskCompletionSource(); + thread = new Thread(Run); + thread.IsBackground = true; + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + } + + public Task Wait() + { + return this.source.Task; + } + + public T GetResult() + { + thread.Join(); + return source.Task.Result; + } + + void Run() + { + try + { + // we won't try shcore here, since between 8.1-10 this is global and not thread specific. + // we also only catch DllNotFoundException in the case it's not supported by the OS we want to complete the task anyway + try + { + Set1607ThreadAwarenessContext(scaling); + } + catch (DllNotFoundException) { } + + this.source.SetResult(job()); + } + catch (Exception ex) + { + this.source.SetException(ex); + } + } + } + } +} diff --git a/src/Update/User32MessageBox.cs b/src/Update/Windows/User32MessageBox.cs similarity index 99% rename from src/Update/User32MessageBox.cs rename to src/Update/Windows/User32MessageBox.cs index 1ae290fa..21042a6b 100644 --- a/src/Update/User32MessageBox.cs +++ b/src/Update/Windows/User32MessageBox.cs @@ -4,7 +4,7 @@ using System.Text; // from clowd-windows/Clowd.PlatformUtil/Windows/User32MessageBox.cs -namespace Squirrel.Update +namespace Squirrel.Update.Windows { internal static class User32MessageBox { diff --git a/src/Update/Windows/User32SplashWindow.cs b/src/Update/Windows/User32SplashWindow.cs new file mode 100644 index 00000000..21f334cf --- /dev/null +++ b/src/Update/Windows/User32SplashWindow.cs @@ -0,0 +1,450 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using System.Threading; +using Squirrel.SimpleSplat; +using Vanara.PInvoke; +using static Vanara.PInvoke.Kernel32; +using static Vanara.PInvoke.ShowWindowCommand; +using static Vanara.PInvoke.User32; +using static Vanara.PInvoke.User32.MonitorFlags; +using static Vanara.PInvoke.User32.SetWindowPosFlags; +using static Vanara.PInvoke.User32.SPI; +using static Vanara.PInvoke.User32.WindowClassStyles; +using static Vanara.PInvoke.User32.WindowMessage; +using static Vanara.PInvoke.User32.WindowStyles; +using static Vanara.PInvoke.User32.WindowStylesEx; +using static Vanara.PInvoke.SHCore; +using POINT = System.Drawing.Point; +using System.IO; + +namespace Squirrel.Update.Windows +{ + + internal unsafe class User32SplashWindow : ISplashWindow + { + public IntPtr Handle => _hwnd != null ? _hwnd.DangerousGetHandle() : IntPtr.Zero; + + static IFullLogger Log = SquirrelLocator.Current.GetService().GetLogger(typeof(User32SplashWindow)); + + private SafeHWND _hwnd; + private Exception _error; + private Thread _thread; + private uint _threadId; + private POINT _ptMouseDown; + private ITaskbarList3 _taskbarList3; + private double _progress; + + private readonly ManualResetEvent _signal; + private readonly Bitmap _img; + private readonly string _appName; + private readonly bool _silent; + private readonly Icon _icon; + + private const int OPERATION_TIMEOUT = 5000; + private const string WINDOW_CLASS_NAME = "SquirrelSplashWindow"; + + public User32SplashWindow(string appName, bool silent, byte[] iconBytes, byte[] splashBytes) + { + _appName = appName; + _silent = silent; + _signal = new ManualResetEvent(false); + + try { + if (iconBytes?.Length > 0) _icon = new Icon(new MemoryStream(iconBytes)); + if (splashBytes?.Length > 0) _img = (Bitmap) Bitmap.FromStream(new MemoryStream(splashBytes)); + } catch (Exception ex) { + Log.WarnException("Unable to load splash image", ex); + } + + try { + var tbl = (ITaskbarList3) new CTaskbarList(); + tbl.HrInit(); + _taskbarList3 = tbl; + } catch (Exception ex) { + // failure to load the COM taskbar progress feature should not break this entire window + Log.WarnException("Unable to load ITaskbarList3, progress will not be shown in taskbar", ex); + } + + if (_silent || _img == null) { + // can not show splash window without an image + return; + } + + _thread = new Thread(ThreadProc); + _thread.IsBackground = true; + _thread.Start(); + if (!_signal.WaitOne()) { + if (_error != null) throw _error; + else throw new Exception("Timeout waiting for splash window to open"); + } + if (_error != null) throw _error; + + SetProgressIndeterminate(); + } + + public void Show() + { + if (_thread == null) return; + ShowWindow(_hwnd, SW_SHOW); + } + + public void Hide() + { + if (_thread == null) return; + ShowWindow(_hwnd, SW_HIDE); + } + + public void SetNoProgress() + { + if (_thread == null) return; + var h = _hwnd.DangerousGetHandle(); + _taskbarList3?.SetProgressState(h, ThumbnailProgressState.NoProgress); + _progress = 0; + InvalidateRect(_hwnd, null, false); + } + + public void SetProgressIndeterminate() + { + if (_thread == null) return; + var h = _hwnd.DangerousGetHandle(); + _taskbarList3?.SetProgressState(h, ThumbnailProgressState.Indeterminate); + _progress = 0; + InvalidateRect(_hwnd, null, false); + } + + public void SetProgress(ulong completed, ulong total) + { + if (_thread == null) return; + var h = _hwnd.DangerousGetHandle(); + _taskbarList3?.SetProgressState(h, ThumbnailProgressState.Normal); + _taskbarList3?.SetProgressValue(h, completed, total); + _progress = completed / (double) total; + InvalidateRect(_hwnd, null, false); + } + + public void ShowErrorDialog(string title, string message) + { + if (_silent) { + Log.Info("User err suppressed (updater in silent mode): " + message); + } else { + Log.Info("User shown err: " + message); + User32MessageBox.Show( + Handle, + message, + _appName + " - " + title, + User32MessageBox.MessageBoxButtons.OK, + User32MessageBox.MessageBoxIcon.Error); + } + } + + public void ShowInfoDialog(string title, string message) + { + if (_silent) { + Log.Info("User message suppressed (updater in silent mode): " + message); + } else { + Log.Info("User shown message: " + message); + User32MessageBox.Show( + Handle, + message, + _appName + " - " + title, + User32MessageBox.MessageBoxButtons.OK, + User32MessageBox.MessageBoxIcon.Information); + } + } + + public bool ShowQuestionDialog(string title, string message) + { + if (_silent) { + Log.Info("User prompt suppressed (updater in silent mode): '" + message + "' -- Automatically answering Cancel."); + return false; + } else { + var result = User32MessageBox.Show( + Handle, + message, + _appName + " - " + title, + User32MessageBox.MessageBoxButtons.OKCancel, + User32MessageBox.MessageBoxIcon.Question, + User32MessageBox.MessageBoxResult.Cancel); + Log.Info("User prompted: '" + message + "' -- User answered " + result.ToString()); + return User32MessageBox.MessageBoxResult.OK == result; + } + } + + public void Dispose() + { + if (_thread == null) return; + PostThreadMessage(_threadId, (uint) WM_QUIT, IntPtr.Zero, IntPtr.Zero); + _thread.Join(OPERATION_TIMEOUT); + _thread = null; + _error = null; + _threadId = 0; + _hwnd = null; + _signal.Reset(); + } + + private IDisposable StartGifAnimation() + { + // check for animation properties + ManualResetEvent exitGif = new ManualResetEvent(false); + Thread gif = null; + + try { + var pDimensionIDs = _img.FrameDimensionsList; + var frameDimension = new FrameDimension(pDimensionIDs[0]); + var frameCount = _img.GetFrameCount(frameDimension); + Log.Info($"There were {frameCount} frames detected in the splash image ({(frameCount > 1 ? "it's animated" : "it's not animated")})."); + if (frameCount > 1) { + var delayProperty = _img.GetPropertyItem(0x5100 /*PropertyTagFrameDelay*/); + gif = new Thread(() => { + fixed (byte* frameDelayBytes = delayProperty.Value) { + int framePosition = 0; + int* frameDelays = (int*) frameDelayBytes; + while (true) { + + lock (_img) _img.SelectActiveFrame(frameDimension, framePosition++); + InvalidateRect(_hwnd, null, false); + + if (framePosition == frameCount) + framePosition = 0; + + int lPause = frameDelays[framePosition] * 10; + if (exitGif.WaitOne(lPause)) + return; + } + } + }); + + // start gif animation + if (frameCount > 1 && delayProperty?.Value != null && (delayProperty.Value.Length / 4) >= frameCount) { + gif.IsBackground = true; + gif.Start(); + } + } + } catch (Exception e) { + // errors starting a gif should not break the splash window + Log.ErrorException("Failed to start GIF animation.", e); + } + + return Disposable.Create(() => { + exitGif.Set(); + gif?.Join(1000); + }); + } + + private void ThreadProc() + { + try { + // this is also set in the manifest, but this won't hurt anything and can help if the manifest got replaced with something else. + ThreadDpiScalingContext.SetCurrentThreadScalingMode(ThreadScalingMode.PerMonitorV2Aware); + + _threadId = GetCurrentThreadId(); + CreateWindow(); + } catch (Exception ex) { + _error = ex; + _signal.Set(); + } + } + + private void CreateWindow() + { + var instance = GetModuleHandle(null); + + WNDCLASS wndClass = new WNDCLASS { + style = CS_HREDRAW | CS_VREDRAW, + lpfnWndProc = WndProc, + hInstance = instance, + //hbrBackground = COLOR_WINDOW + hCursor = LoadCursor(HINSTANCE.NULL, IDC_APPSTARTING), + lpszClassName = WINDOW_CLASS_NAME, + hIcon = _icon != null ? new HICON(_icon.Handle) : LoadIcon(instance, IDI_APPLICATION), + }; + + if (RegisterClass(wndClass) == 0) { + var clhr = GetLastError(); + if (clhr != 0x00000582) // already registered + throw clhr.GetException("Unable to register splash window class"); + } + + // try to find monitor where mouse is + GetCursorPos(out var point); + var hMonitor = MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST); + MONITORINFO mi = new MONITORINFO { cbSize = 40 /*sizeof(MONITORINFO)*/ }; + + // calculate ideal window position, and adjust for image and screen DPI + int x, y, w, h; + try { + if (!GetMonitorInfo(hMonitor, ref mi)) throw new Win32Exception(); + GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_DEFAULT, out var dpiX, out var dpiY).ThrowIfFailed(); + var dpiRatioX = _img.HorizontalResolution / dpiX; + var dpiRatioY = _img.VerticalResolution / dpiY; + w = (int) Math.Round(_img.Width / dpiRatioX); + h = (int) Math.Round(_img.Height / dpiRatioY); + x = (mi.rcWork.Width - w) / 2; + y = (mi.rcWork.Height - h) / 2; + } catch { + RECT rcArea = default; + SystemParametersInfo(SPI_GETWORKAREA, 0, new IntPtr(&rcArea), 0); + w = _img.Width; + h = _img.Height; + x = (rcArea.Width - w) / 2; + y = (rcArea.Height - h) / 2; + } + + _hwnd = CreateWindowEx( + /*WS_EX_TOOLWINDOW |*/ WS_EX_TOPMOST, + WINDOW_CLASS_NAME, + _appName + " Setup", + WS_CLIPCHILDREN | WS_POPUP, + x, y, w, h, + HWND.NULL, + HMENU.NULL, + instance, + IntPtr.Zero); + + if (_hwnd.IsInvalid) { + throw new Win32Exception(); + } + + ShowWindow(_hwnd, SW_SHOWNOACTIVATE); + + MSG msg; + PeekMessage(out msg, _hwnd, 0, 0, 0); // invoke creating message queue + + _signal.Set(); // signal to calling thread that the window has been created + + using (StartGifAnimation()) { + bool bRet; + while ((bRet = GetMessage(out msg, HWND.NULL, 0, 0)) != false) { + if (msg.message == (uint) WM_QUIT) + break; + + TranslateMessage(msg); + DispatchMessage(msg); + } + } + + DestroyWindow(_hwnd); + } + + private nint WndProc(HWND hwnd, uint uMsg, nint wParam, nint lParam) + { + switch (uMsg) { + + case (uint) WM_DPICHANGED: + var suggestedRect = Marshal.PtrToStructure(lParam); + SetWindowPos(hwnd, HWND.HWND_TOP, + suggestedRect.X, suggestedRect.Y, suggestedRect.Width, suggestedRect.Height, + SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER); + return 0; + + case (uint) WM_PAINT: + GetWindowRect(hwnd, out var r); + using (var buffer = new Bitmap(r.Width, r.Height)) + using (var brush = new SolidBrush(Color.FromArgb(160, Color.LimeGreen))) + using (var g = Graphics.FromImage(buffer)) + using (var wnd = Graphics.FromHwnd(hwnd.DangerousGetHandle())) { + // draw to back buffer + g.FillRectangle(Brushes.Black, 0, 0, r.Width, r.Height); + lock (_img) g.DrawImage(_img, 0, 0, r.Width, r.Height); + if (_progress > 0) { + g.FillRectangle(brush, new Rectangle(0, r.Height - 10, (int) (r.Width * _progress), 10)); + } + + // only should do a single draw operation to the window front buffer to prevent flickering + wnd.DrawImage(buffer, 0, 0, r.Width, r.Height); + } + + ValidateRect(hwnd, null); + return 0; + + case (uint) WM_LBUTTONDOWN: + GetCursorPos(out _ptMouseDown); + SetCapture(hwnd); + return 0; + + case (uint) WM_MOUSEMOVE: + if (GetCapture() == hwnd) { + GetWindowRect(hwnd, out var rcWnd); + GetCursorPos(out var pt); + + POINT ptDown = _ptMouseDown; + var xdiff = ptDown.X - pt.X; + var ydiff = ptDown.Y - pt.Y; + + SetWindowPos(hwnd, HWND.HWND_TOP, rcWnd.left - xdiff, rcWnd.top - ydiff, 0, 0, + SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER); + + _ptMouseDown = pt; + } + return 0; + + case (uint) WM_LBUTTONUP: + ReleaseCapture(); + return 0; + + } + + return DefWindowProc(hwnd, uMsg, wParam, lParam); + } + + [ComImport()] + [Guid("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ITaskbarList3 + { + // ITaskbarList + [PreserveSig] + void HrInit(); + [PreserveSig] + void AddTab(IntPtr hwnd); + [PreserveSig] + void DeleteTab(IntPtr hwnd); + [PreserveSig] + void ActivateTab(IntPtr hwnd); + [PreserveSig] + void SetActiveAlt(IntPtr hwnd); + + // ITaskbarList2 + [PreserveSig] + void MarkFullscreenWindow( + IntPtr hwnd, + [MarshalAs(UnmanagedType.Bool)] bool fFullscreen); + + // ITaskbarList3 + void SetProgressValue(IntPtr hwnd, UInt64 ullCompleted, UInt64 ullTotal); + void SetProgressState(IntPtr hwnd, ThumbnailProgressState tbpFlags); + } + + [Guid("56FDF344-FD6D-11d0-958A-006097C9A090")] + [ClassInterface(ClassInterfaceType.None)] + [ComImport()] + internal class CTaskbarList { } + + public enum ThumbnailProgressState + { + /// + /// No progress is displayed. + /// + NoProgress = 0, + /// + /// The progress is indeterminate (marquee). + /// + Indeterminate = 0x1, + /// + /// Normal progress is displayed. + /// + Normal = 0x2, + /// + /// An error occurred (red). + /// + Error = 0x4, + /// + /// The operation is paused (yellow). + /// + Paused = 0x8 + } + } +} diff --git a/src/Update/app.manifest b/src/Update/app.manifest index c2cd0336..8e583b26 100644 --- a/src/Update/app.manifest +++ b/src/Update/app.manifest @@ -11,6 +11,7 @@ + @@ -30,6 +31,14 @@ + + + + True + PerMonitorV2 + + + @@ -44,4 +53,4 @@ - + \ No newline at end of file