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