Bug fix for non-96 dpi displays, and non-96 dpi images

This commit is contained in:
Caelan Sayler
2021-12-21 14:41:33 +00:00
parent 657d37d92c
commit 9749093c72
10 changed files with 726 additions and 389 deletions

View File

@@ -195,3 +195,6 @@ cpp_wrap_preserve_blocks = one_liners
[*.csproj] [*.csproj]
indent_size = 2 indent_size = 2
[*.manifest]
indent_size = 2

View File

@@ -11,6 +11,7 @@
</security> </security>
</trustInfo> </trustInfo>
<!-- Indicate our support for newer versions of windows, so it stops lying to us -->
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application> <application>
<!-- Windows Vista --> <!-- Windows Vista -->
@@ -30,6 +31,14 @@
</application> </application>
</compatibility> </compatibility>
<!-- Disable legacy dpi / bitmap scaling, also so windows stops lying to us -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<!-- Enable themes for Windows common controls and dialogs (Windows XP and later) --> <!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->
<dependency> <dependency>
<dependentAssembly> <dependentAssembly>

View File

@@ -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);
}
}

View File

@@ -14,7 +14,6 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squirrel.NuGet; using Squirrel.NuGet;
using Squirrel.Lib; using Squirrel.Lib;
using System.Drawing;
namespace Squirrel.Update namespace Squirrel.Update
{ {
@@ -154,41 +153,7 @@ namespace Squirrel.Update
} }
using var _t = Utility.WithTempDirectory(out var tempFolder); using var _t = Utility.WithTempDirectory(out var tempFolder);
ISplashWindow splash = new Windows.User32SplashWindow(info.AppFriendlyName, silentInstall, info.SetupIconBytes, info.SplashImageBytes);
// 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;
}
}
var missingFrameworks = info.RequiredFrameworks var missingFrameworks = info.RequiredFrameworks
.Select(f => Runtimes.GetRuntimeByName(f)) .Select(f => Runtimes.GetRuntimeByName(f))
@@ -203,7 +168,7 @@ namespace Squirrel.Update
$"Would you like to install these now?" $"Would you like to install these now?"
: $"{info.AppFriendlyName} requires {missingFrameworks.First().DisplayName} installed to continue, would you like to install it 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 return; // user cancelled install
} }
@@ -212,13 +177,13 @@ namespace Squirrel.Update
// iterate through each missing dependency and download/run the installer. // iterate through each missing dependency and download/run the installer.
foreach (var f in missingFrameworks) { foreach (var f in missingFrameworks) {
var localPath = Path.Combine(tempFolder, f.Id + ".exe"); var localPath = Path.Combine(tempFolder, f.Id + ".exe");
await f.DownloadToFile(localPath, e => splash?.SetProgress((ulong) e.BytesReceived, (ulong) e.TotalBytesToReceive)); await f.DownloadToFile(localPath, e => splash.SetProgress((ulong) e.BytesReceived, (ulong) e.TotalBytesToReceive));
splash?.SetProgressIndeterminate(); splash.SetProgressIndeterminate();
// hide splash screen while the runtime installer is running so the user can see progress // 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); var exitcode = await f.InvokeInstaller(localPath, silentInstall);
splash?.Show(); splash.Show();
if (exitcode == RuntimeInstallResult.RestartRequired) { if (exitcode == RuntimeInstallResult.RestartRequired) {
rebootRequired = true; rebootRequired = true;
@@ -230,21 +195,21 @@ namespace Squirrel.Update
RuntimeInstallResult.SystemDoesNotMeetRequirements => $"This computer does not meet the system requirements for {f.DisplayName}.", RuntimeInstallResult.SystemDoesNotMeetRequirements => $"This computer does not meet the system requirements for {f.DisplayName}.",
_ => $"{f.DisplayName} installer exited with error code '{exitcode}'.", _ => $"{f.DisplayName} installer exited with error code '{exitcode}'.",
}; };
showUserMsg(true, rtmsg, $"Error installing {f.DisplayName}"); splash.ShowErrorDialog($"Error installing {f.DisplayName}", rtmsg);
return; return;
} }
} }
if (rebootRequired) { if (rebootRequired) {
// TODO: automatic restart setup after reboot // 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; return;
} }
} }
// setup package source directory // setup package source directory
Log.Info($"Starting package install from directory " + tempFolder); Log.Info($"Starting package install from directory " + tempFolder);
splash?.SetProgressIndeterminate(); splash.SetProgressIndeterminate();
string packagePath = Path.Combine(tempFolder, info.BundledPackageName); string packagePath = Path.Combine(tempFolder, info.BundledPackageName);
File.WriteAllBytes(packagePath, info.BundledPackageBytes); File.WriteAllBytes(packagePath, info.BundledPackageBytes);
var entry = ReleaseEntry.GenerateFromFile(packagePath); var entry = ReleaseEntry.GenerateFromFile(packagePath);
@@ -253,12 +218,12 @@ namespace Squirrel.Update
var progressSource = new ProgressSource(); var progressSource = new ProgressSource();
progressSource.Progress += (e, p) => { progressSource.Progress += (e, p) => {
// post install hooks are about to be run (app will start) // post install hooks are about to be run (app will start)
if (p >= 90) splash?.Close(); if (p >= 90) splash.Hide();
else splash?.SetProgress((ulong) p, 90); else splash.SetProgress((ulong) p, 90);
}; };
await Install(silentInstall, progressSource, tempFolder); await Install(silentInstall, progressSource, tempFolder);
splash?.Close(); splash.Dispose();
} }
static async Task Install(bool silentInstall, ProgressSource progressSource, string sourceDirectory = null) static async Task Install(bool silentInstall, ProgressSource progressSource, string sourceDirectory = null)

View File

@@ -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
{
/// <summary>
/// No progress is displayed.
/// </summary>
NoProgress = 0,
/// <summary>
/// The progress is indeterminate (marquee).
/// </summary>
Indeterminate = 0x1,
/// <summary>
/// Normal progress is displayed.
/// </summary>
Normal = 0x2,
/// <summary>
/// An error occurred (red).
/// </summary>
Error = 0x4,
/// <summary>
/// The operation is paused (yellow).
/// </summary>
Paused = 0x8
}
}
}

View File

@@ -16,8 +16,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="NLog" Version="5.0.0-preview.3" /> <PackageReference Include="NLog" Version="5.0.0-preview.3" />
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
<PackageReference Include="Vanara.PInvoke.User32" Version="3.3.14" /> <PackageReference Include="Vanara.PInvoke.User32" Version="3.3.14" />
<PackageReference Include="Vanara.PInvoke.SHCore" Version="3.3.14" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -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"),
};
}
/// <summary>
/// Gets the current thread scaling / dpi awareness.
/// </summary>
public static ThreadScalingMode GetCurrentThreadScalingMode()
{
try
{
return Get1607ThreadAwarenessContext();
}
catch { }
try
{
return GetShcoreAwareness();
}
catch { }
return ThreadScalingMode.Unaware;
}
/// <summary>
/// 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.
/// </summary>
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<T>(ThreadScalingMode mode, Func<T> task)
{
var thread = new AwareThread<T>(mode, task);
return thread.GetResult();
}
public static Task RunScalingAwareAsync(ThreadScalingMode mode, Action task)
{
return RunScalingAwareAsync(mode, () => { task(); return true; });
}
public static Task<T> RunScalingAwareAsync<T>(ThreadScalingMode mode, Func<T> task)
{
var thread = new AwareThread<T>(mode, task);
return thread.Wait();
}
private class AwareThread<T>
{
ThreadScalingMode scaling;
Func<T> job;
TaskCompletionSource<T> source;
Thread thread;
public AwareThread(ThreadScalingMode mode, Func<T> task)
{
scaling = mode;
job = task;
source = new TaskCompletionSource<T>();
thread = new Thread(Run);
thread.IsBackground = true;
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
}
public Task<T> 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);
}
}
}
}
}

View File

@@ -4,7 +4,7 @@ using System.Text;
// from clowd-windows/Clowd.PlatformUtil/Windows/User32MessageBox.cs // from clowd-windows/Clowd.PlatformUtil/Windows/User32MessageBox.cs
namespace Squirrel.Update namespace Squirrel.Update.Windows
{ {
internal static class User32MessageBox internal static class User32MessageBox
{ {

View File

@@ -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<ILogManager>().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<RECT>(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
{
/// <summary>
/// No progress is displayed.
/// </summary>
NoProgress = 0,
/// <summary>
/// The progress is indeterminate (marquee).
/// </summary>
Indeterminate = 0x1,
/// <summary>
/// Normal progress is displayed.
/// </summary>
Normal = 0x2,
/// <summary>
/// An error occurred (red).
/// </summary>
Error = 0x4,
/// <summary>
/// The operation is paused (yellow).
/// </summary>
Paused = 0x8
}
}
}

View File

@@ -11,6 +11,7 @@
</security> </security>
</trustInfo> </trustInfo>
<!-- Indicate our support for newer versions of windows, so it stops lying to us -->
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application> <application>
<!-- Windows Vista --> <!-- Windows Vista -->
@@ -30,6 +31,14 @@
</application> </application>
</compatibility> </compatibility>
<!-- Disable legacy dpi / bitmap scaling, also so windows stops lying to us -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<!-- Enable themes for Windows common controls and dialogs (Windows XP and later) --> <!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->
<dependency> <dependency>
<dependentAssembly> <dependentAssembly>