mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Bug fix for non-96 dpi displays, and non-96 dpi images
This commit is contained in:
@@ -195,3 +195,6 @@ cpp_wrap_preserve_blocks = one_liners
|
||||
|
||||
[*.csproj]
|
||||
indent_size = 2
|
||||
|
||||
[*.manifest]
|
||||
indent_size = 2
|
||||
@@ -11,6 +11,7 @@
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<!-- Indicate our support for newer versions of windows, so it stops lying to us -->
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows Vista -->
|
||||
@@ -30,6 +31,14 @@
|
||||
</application>
|
||||
</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) -->
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
|
||||
16
src/Update/ISplashWindow.cs
Normal file
16
src/Update/ISplashWindow.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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.SHCore" Version="3.3.14" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
223
src/Update/Windows/ThreadDpiScalingContext.cs
Normal file
223
src/Update/Windows/ThreadDpiScalingContext.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
450
src/Update/Windows/User32SplashWindow.cs
Normal file
450
src/Update/Windows/User32SplashWindow.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<!-- Indicate our support for newer versions of windows, so it stops lying to us -->
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows Vista -->
|
||||
@@ -30,6 +31,14 @@
|
||||
</application>
|
||||
</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) -->
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
|
||||
Reference in New Issue
Block a user