449 lines
22 KiB
C++
449 lines
22 KiB
C++
// Keeps the DWM frame and shadow while the NSIS page owns the entire client area.
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#define NOMINMAX
|
|
#define UNICODE
|
|
#include <windows.h>
|
|
#include <tlhelp32.h>
|
|
#include <objidl.h>
|
|
#include <commctrl.h>
|
|
#include <dwmapi.h>
|
|
#include <gdiplus.h>
|
|
#include <new>
|
|
#include <algorithm>
|
|
#include "progress.h"
|
|
#include "extract.h"
|
|
#include "extract-report.h"
|
|
#include "directory-report.h"
|
|
#include "uninstall-data.h"
|
|
|
|
using namespace Gdiplus;
|
|
|
|
// Returns whether the UTF-8 report was saved; failure still presents the original error unless silent.
|
|
extern "C" __declspec(dllexport) int __cdecl InstallerReportDirectoryFailure(
|
|
HWND parent, DWORD error, LPCWSTR source, LPCWSTR destination, DWORD restoreError, LPCWSTR backup,
|
|
LPCWSTR log, LPCWSTR reportPath, int show, LPCWSTR title, LPCWSTR heading, LPCWSTR hint, LPCWSTR copy,
|
|
LPCWSTR expand, LPCWSTR collapse, LPCWSTR savedFormat, LPCWSTR unsaved, LPCWSTR copied) {
|
|
try {
|
|
const std::wstring report = directory_report::Compose(error, source, destination, restoreError, backup, extract_report::ReadOutput(log));
|
|
const bool saved = extract_report::WriteUtf8(reportPath, report);
|
|
if (show) {
|
|
std::wstring footer = saved ? savedFormat : unsaved;
|
|
const auto placeholder = footer.find(L"%s");
|
|
if (saved && placeholder != std::wstring::npos) footer.replace(placeholder, 2, reportPath);
|
|
const extract_report::DialogStrings strings{title, heading, copy, expand, collapse, footer.c_str(), copied};
|
|
const std::wstring content = directory_report::Content(error, hint, restoreError, backup);
|
|
extract_report::Show(parent, content, extract_report::BoundedExcerpt(extract_report::Lines(extract_report::Trim(report))), report, strings);
|
|
}
|
|
return saved ? 1 : 0;
|
|
} catch (const std::bad_alloc&) {
|
|
if (show) MessageBoxW(parent, hint, heading, MB_OK | MB_ICONEXCLAMATION);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Saves the extraction failure report and, unless silent, presents its headline with a copy action.
|
|
// Returns 1 when the report file was written and 0 otherwise; the dialog outcome never fails the caller.
|
|
extern "C" __declspec(dllexport) int __cdecl InstallerReportExtractFailure(
|
|
HWND parent, int code, LPCWSTR archive, LPCWSTR destination, LPCWSTR log, LPCWSTR reportPath, int show,
|
|
LPCWSTR title, LPCWSTR heading, LPCWSTR hint, LPCWSTR copy, LPCWSTR expand, LPCWSTR collapse, LPCWSTR savedFormat, LPCWSTR unsaved, LPCWSTR copied) {
|
|
try {
|
|
const std::wstring output = extract_report::ReadOutput(log);
|
|
const std::wstring report = extract_report::Compose(code, archive, destination, output,
|
|
extract_report::Timestamp(), extract_report::WindowsVersion());
|
|
const bool saved = extract_report::WriteUtf8(reportPath, report);
|
|
if (show) {
|
|
std::wstring footer = saved ? savedFormat : unsaved;
|
|
const auto placeholder = footer.find(L"%s");
|
|
if (saved && placeholder != std::wstring::npos) footer.replace(placeholder, 2, reportPath);
|
|
const extract_report::DialogStrings strings{title, heading, copy, expand, collapse, footer.c_str(), copied};
|
|
const std::wstring content = extract_report::Headline(code, output, archive) + L"\r\n\r\n" + hint;
|
|
extract_report::Show(parent, content, extract_report::Excerpt(report), report, strings);
|
|
}
|
|
return saved ? 1 : 0;
|
|
} catch (const std::bad_alloc&) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Returns a Win32 error code; no cleanup request may traverse a linked root or touch the protected root.
|
|
extern "C" __declspec(dllexport) DWORD __cdecl UninstallRemoveData(LPCWSTR path, LPCWSTR installation, LPCWSTR protectedRoot) {
|
|
return uninstall_data::Remove(path, installation, protectedRoot);
|
|
}
|
|
|
|
// Returns a Win32 error code; only ordinary empty directories strictly below the stop directory are removed.
|
|
extern "C" __declspec(dllexport) DWORD __cdecl UninstallRemoveEmptyParents(LPCWSTR path, LPCWSTR stop) {
|
|
return uninstall_data::RemoveEmptyParents(path, stop);
|
|
}
|
|
|
|
// Match the affected executable, not another user's or directory's same-named application.
|
|
// Returns 0 while running, 1 when absent, and -1 if the process list cannot be read.
|
|
extern "C" __declspec(dllexport) int __cdecl InstallerFindProcess(LPCWSTR executable) {
|
|
WCHAR target[32768];
|
|
DWORD length = GetLongPathNameW(executable, target, ARRAYSIZE(target));
|
|
LPCWSTR expected = length > 0 && length < ARRAYSIZE(target) ? target : executable;
|
|
LPCWSTR filename = wcsrchr(expected, L'\\');
|
|
filename = filename ? filename + 1 : expected;
|
|
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
|
if (snapshot == INVALID_HANDLE_VALUE) return -1;
|
|
PROCESSENTRY32W entry = {};
|
|
entry.dwSize = sizeof(entry);
|
|
int result = 1;
|
|
BOOL present = Process32FirstW(snapshot, &entry);
|
|
while (present) {
|
|
if (_wcsicmp(entry.szExeFile, filename) == 0) {
|
|
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, entry.th32ProcessID);
|
|
if (process) {
|
|
WCHAR path[32768];
|
|
DWORD count = ARRAYSIZE(path);
|
|
if (QueryFullProcessImageNameW(process, 0, path, &count) && _wcsicmp(path, expected) == 0) result = 0;
|
|
CloseHandle(process);
|
|
if (result == 0) break;
|
|
}
|
|
}
|
|
present = Process32NextW(snapshot, &entry);
|
|
}
|
|
if (!present && GetLastError() != ERROR_NO_MORE_FILES) result = -1;
|
|
CloseHandle(snapshot);
|
|
return result;
|
|
}
|
|
|
|
struct ProgressPage {
|
|
InstallProgress progress{GetTickCount64()};
|
|
bool dark;
|
|
UINT dpi;
|
|
ULONG_PTR gdiplus;
|
|
Image* brand;
|
|
Bitmap* buffer;
|
|
HDC bufferDc;
|
|
HBITMAP bufferBitmap;
|
|
HGDIOBJ previousBitmap;
|
|
UINT pressedCommand;
|
|
UINT hoveredCommand;
|
|
ARGB controlHover;
|
|
ARGB controlPressed;
|
|
WCHAR captions[5][128];
|
|
|
|
~ProgressPage() {
|
|
delete buffer;
|
|
delete brand;
|
|
if (previousBitmap) SelectObject(bufferDc, previousBitmap);
|
|
if (bufferBitmap) DeleteObject(bufferBitmap);
|
|
if (bufferDc) DeleteDC(bufferDc);
|
|
if (gdiplus) GdiplusShutdown(gdiplus);
|
|
}
|
|
};
|
|
|
|
// NSIS shows its page after MUI's SHOW callback; keep its controls off screen.
|
|
static LRESULT CALLBACK HiddenPageProc(HWND window, UINT message, WPARAM wparam,
|
|
LPARAM lparam, UINT_PTR id, DWORD_PTR) {
|
|
if (message == WM_WINDOWPOSCHANGING) {
|
|
auto* position = reinterpret_cast<WINDOWPOS*>(lparam);
|
|
position->flags = (position->flags & ~SWP_SHOWWINDOW) | SWP_HIDEWINDOW;
|
|
}
|
|
if (message == WM_NCDESTROY) RemoveWindowSubclass(window, HiddenPageProc, id);
|
|
return DefSubclassProc(window, message, wparam, lparam);
|
|
}
|
|
|
|
static void FillProgress(Graphics& graphics, Brush& brush, REAL width) {
|
|
if (width <= 0) return;
|
|
if (width < 4) { graphics.FillRectangle(&brush, 64.0f, 482.0f, width, 6.0f); return; }
|
|
GraphicsPath path;
|
|
path.AddArc(64.0f, 482.0f, 4.0f, 4.0f, 180.0f, 90.0f);
|
|
path.AddArc(64.0f + width - 4, 482.0f, 4.0f, 4.0f, 270.0f, 90.0f);
|
|
path.AddArc(64.0f + width - 4, 484.0f, 4.0f, 4.0f, 0.0f, 90.0f);
|
|
path.AddArc(64.0f, 484.0f, 4.0f, 4.0f, 90.0f, 90.0f);
|
|
path.CloseFigure();
|
|
graphics.FillPath(&brush, &path);
|
|
}
|
|
|
|
static UINT CaptionCommand(const ProgressPage& page, LPARAM position) {
|
|
const int x = static_cast<short>(LOWORD(position));
|
|
const int y = static_cast<short>(HIWORD(position));
|
|
if (y < 0 || y >= MulDiv(48, page.dpi, 96) || x < MulDiv(504, page.dpi, 96) || x >= MulDiv(600, page.dpi, 96)) return 0;
|
|
return x >= MulDiv(548, page.dpi, 96) ? SC_CLOSE : SC_MINIMIZE;
|
|
}
|
|
|
|
static void PaintCaptionButtons(Graphics& graphics, const ProgressPage& page) {
|
|
SolidBrush background(page.dark ? Color(255, 21, 21, 23) : Color(255, 255, 255, 255));
|
|
graphics.FillRectangle(&background, 504, 0, 96, 48);
|
|
SolidBrush ink(page.dark ? Color(255, 255, 255, 255) : Color(255, 15, 17, 21));
|
|
FontFamily family(L"Microsoft YaHei UI");
|
|
Font font(&family, 16, FontStyleRegular, UnitPixel);
|
|
StringFormat centered;
|
|
centered.SetAlignment(StringAlignmentCenter);
|
|
centered.SetLineAlignment(StringAlignmentCenter);
|
|
for (UINT command : {SC_MINIMIZE, SC_CLOSE}) {
|
|
const int x = command == SC_MINIMIZE ? 504 : 548;
|
|
if (page.hoveredCommand == command && (!page.pressedCommand || page.pressedCommand == command)) {
|
|
const bool pressed = page.pressedCommand == command;
|
|
SolidBrush fill(Color(pressed ? page.controlPressed : page.controlHover));
|
|
GraphicsPath path;
|
|
path.AddArc(x, 8, 20, 20, 180, 90);
|
|
path.AddArc(x + 20, 8, 20, 20, 270, 90);
|
|
path.AddArc(x + 20, 20, 20, 20, 0, 90);
|
|
path.AddArc(x, 20, 20, 20, 90, 90);
|
|
path.CloseFigure();
|
|
graphics.FillPath(&fill, &path);
|
|
}
|
|
graphics.DrawString(command == SC_MINIMIZE ? L"\x2212" : L"\x00d7", -1,
|
|
&font, RectF(static_cast<REAL>(x), 8, 40, 32), ¢ered, &ink);
|
|
}
|
|
}
|
|
|
|
static void InvalidateCaptionButtons(HWND window, const ProgressPage& page) {
|
|
RECT bounds{MulDiv(504, page.dpi, 96), 0, MulDiv(600, page.dpi, 96), MulDiv(48, page.dpi, 96)};
|
|
InvalidateRect(window, &bounds, FALSE);
|
|
}
|
|
|
|
static LRESULT CALLBACK ProgressProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) {
|
|
auto* page = reinterpret_cast<ProgressPage*>(GetWindowLongPtrW(window, GWLP_USERDATA));
|
|
if (message == WM_CREATE) {
|
|
page = static_cast<ProgressPage*>(reinterpret_cast<CREATESTRUCTW*>(lparam)->lpCreateParams);
|
|
SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(page));
|
|
SetTimer(window, 1, 16, nullptr);
|
|
}
|
|
if (message == WM_ERASEBKGND) return 1;
|
|
if (message == WM_TIMER) { InvalidateRect(window, nullptr, FALSE); return 0; }
|
|
if (message == WM_MOUSEMOVE && page) {
|
|
TRACKMOUSEEVENT tracking{sizeof(tracking), TME_LEAVE, window, 0};
|
|
TrackMouseEvent(&tracking);
|
|
const UINT hovered = CaptionCommand(*page, lparam);
|
|
if (page->hoveredCommand != hovered) {
|
|
page->hoveredCommand = hovered;
|
|
InvalidateCaptionButtons(window, *page);
|
|
}
|
|
return 0;
|
|
}
|
|
if (message == WM_MOUSELEAVE && page) {
|
|
page->hoveredCommand = 0;
|
|
InvalidateCaptionButtons(window, *page);
|
|
return 0;
|
|
}
|
|
if (message == WM_LBUTTONDOWN && page) {
|
|
const int x = static_cast<short>(LOWORD(lparam));
|
|
const int y = static_cast<short>(HIWORD(lparam));
|
|
page->pressedCommand = CaptionCommand(*page, lparam);
|
|
if (page->pressedCommand) {
|
|
page->hoveredCommand = page->pressedCommand;
|
|
SetCapture(window);
|
|
InvalidateCaptionButtons(window, *page);
|
|
}
|
|
else if (x >= 0 && x < MulDiv(504, page->dpi, 96) && y >= 0 && y < MulDiv(48, page->dpi, 96)) {
|
|
HWND parent = GetParent(window);
|
|
ReleaseCapture(); SendMessageW(parent, WM_NCLBUTTONDOWN, HTCAPTION, 0);
|
|
}
|
|
return 0;
|
|
}
|
|
if (message == WM_LBUTTONUP && page) {
|
|
const UINT command = page->pressedCommand;
|
|
page->pressedCommand = 0;
|
|
page->hoveredCommand = CaptionCommand(*page, lparam);
|
|
InvalidateCaptionButtons(window, *page);
|
|
const bool activate = command && GetCapture() == window && CaptionCommand(*page, lparam) == command;
|
|
if (GetCapture() == window) ReleaseCapture();
|
|
if (activate) {
|
|
if (command == SC_CLOSE) PostMessageW(GetParent(window), WM_CLOSE, 0, 0);
|
|
else ShowWindow(GetParent(window), SW_MINIMIZE);
|
|
}
|
|
return 0;
|
|
}
|
|
if ((message == WM_CAPTURECHANGED || message == WM_CANCELMODE) && page) {
|
|
page->pressedCommand = 0;
|
|
InvalidateCaptionButtons(window, *page);
|
|
if (message == WM_CANCELMODE && GetCapture() == window) ReleaseCapture();
|
|
return 0;
|
|
}
|
|
if ((message == WM_PAINT || message == WM_PRINTCLIENT) && page) {
|
|
PAINTSTRUCT paint = {};
|
|
const bool printing = message == WM_PRINTCLIENT;
|
|
HDC dc = printing ? reinterpret_cast<HDC>(wparam) : BeginPaint(window, &paint);
|
|
if (printing) GetClientRect(window, &paint.rcPaint);
|
|
{
|
|
Graphics graphics(page->buffer);
|
|
graphics.ScaleTransform(page->dpi / 96.0f, page->dpi / 96.0f);
|
|
graphics.SetSmoothingMode(SmoothingModeAntiAlias);
|
|
graphics.SetPixelOffsetMode(PixelOffsetModeHalf);
|
|
PaintCaptionButtons(graphics, *page);
|
|
SolidBrush background(page->dark ? Color(255, 21, 21, 23) : Color(255, 255, 255, 255));
|
|
graphics.FillRectangle(&background, 48, 480, 504, 56);
|
|
const int stage = static_cast<int>(reinterpret_cast<INT_PTR>(GetPropW(GetParent(window), L"HarnessInstaller.Stage")));
|
|
const double fraction = reinterpret_cast<UINT_PTR>(GetPropW(GetParent(window), L"HarnessInstaller.ExtractProgress")) / 100.0;
|
|
page->progress.Advance(stage, fraction, GetTickCount64());
|
|
const int percent = static_cast<int>(page->progress.value);
|
|
SolidBrush track(page->dark ? Color(255, 97, 102, 107) : Color(255, 233, 236, 242));
|
|
SolidBrush ink(page->dark ? Color(255, 255, 255, 255) : Color(255, 15, 17, 21));
|
|
FillProgress(graphics, track, 472.0f);
|
|
FillProgress(graphics, ink, 472.0f * static_cast<REAL>(page->progress.value) / 100);
|
|
FontFamily family(L"Microsoft YaHei UI");
|
|
Font font(&family, 14, FontStyleRegular, UnitPixel);
|
|
StringFormat centered;
|
|
centered.SetAlignment(StringAlignmentCenter);
|
|
centered.SetLineAlignment(StringAlignmentCenter);
|
|
WCHAR caption[160];
|
|
wsprintfW(caption, page->captions[page->progress.CaptionStage()], percent);
|
|
SetWindowTextW(window, caption);
|
|
graphics.DrawString(caption, -1, &font, RectF(48, 512, 504, 22), ¢ered, &ink);
|
|
}
|
|
BitBlt(dc, paint.rcPaint.left, paint.rcPaint.top,
|
|
paint.rcPaint.right - paint.rcPaint.left, paint.rcPaint.bottom - paint.rcPaint.top,
|
|
page->bufferDc, paint.rcPaint.left, paint.rcPaint.top, SRCCOPY);
|
|
if (!printing) EndPaint(window, &paint);
|
|
if (page->progress.value == 100) SetPropW(GetParent(window), L"HarnessInstaller.CompletedPercent", reinterpret_cast<HANDLE>(100));
|
|
return 0;
|
|
}
|
|
if (message == WM_NCDESTROY && page) {
|
|
KillTimer(window, 1);
|
|
delete page;
|
|
}
|
|
return DefWindowProcW(window, message, wparam, lparam);
|
|
}
|
|
|
|
// Runs on the NSIS UI thread; the stock installation section runs on its worker.
|
|
// Caption colors are opaque ARGB from NSIS; failure preserves the stock page's visibility.
|
|
extern "C" __declspec(dllexport) HWND __cdecl InstallerShowProgress(HWND parent, HWND source,
|
|
BOOL dark, UINT dpi, ARGB controlHover, ARGB controlPressed, const WCHAR* brand, const WCHAR* preparing, const WCHAR* extracting,
|
|
const WCHAR* copying, const WCHAR* registering, const WCHAR* cleaning) {
|
|
HINSTANCE module = GetModuleHandleW(nullptr);
|
|
WNDCLASSW type = {};
|
|
type.lpfnWndProc = ProgressProc;
|
|
type.hInstance = module;
|
|
type.lpszClassName = L"HarnessInstallerProgress";
|
|
type.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
|
RegisterClassW(&type);
|
|
auto* page = new (std::nothrow) ProgressPage{};
|
|
if (!page) return nullptr;
|
|
GdiplusStartupInput startup;
|
|
if (GdiplusStartup(&page->gdiplus, &startup, nullptr) != Ok) { delete page; return nullptr; }
|
|
page->dark = dark != FALSE;
|
|
page->dpi = dpi;
|
|
page->controlHover = controlHover;
|
|
page->controlPressed = controlPressed;
|
|
const WCHAR* captions[] = {preparing, extracting, copying, registering, cleaning};
|
|
for (int i = 0; i < 5; ++i) lstrcpynW(page->captions[i], captions[i], 128);
|
|
page->brand = new Image(brand);
|
|
if (!page->brand || page->brand->GetLastStatus() != Ok) {
|
|
delete page; return nullptr;
|
|
}
|
|
const int size = MulDiv(600, dpi, 96);
|
|
BITMAPINFO info = {};
|
|
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
|
info.bmiHeader.biWidth = size;
|
|
info.bmiHeader.biHeight = -size;
|
|
info.bmiHeader.biPlanes = 1;
|
|
info.bmiHeader.biBitCount = 32;
|
|
info.bmiHeader.biCompression = BI_RGB;
|
|
void* pixels = nullptr;
|
|
page->bufferDc = CreateCompatibleDC(nullptr);
|
|
page->bufferBitmap = CreateDIBSection(nullptr, &info, DIB_RGB_COLORS, &pixels, nullptr, 0);
|
|
if (!page->bufferDc || !page->bufferBitmap) { delete page; return nullptr; }
|
|
page->previousBitmap = SelectObject(page->bufferDc, page->bufferBitmap);
|
|
if (!page->previousBitmap) { delete page; return nullptr; }
|
|
// Opaque pixels are shared by GDI+ drawing and the window's GDI blit.
|
|
page->buffer = new Bitmap(size, size, size * 4, PixelFormat32bppPARGB, static_cast<BYTE*>(pixels));
|
|
if (!page->buffer || page->buffer->GetLastStatus() != Ok) { delete page; return nullptr; }
|
|
{
|
|
// Static artwork stays in the buffer for this page's lifetime.
|
|
Graphics graphics(page->buffer);
|
|
graphics.ScaleTransform(dpi / 96.0f, dpi / 96.0f);
|
|
graphics.Clear(page->dark ? Color(255, 21, 21, 23) : Color(255, 255, 255, 255));
|
|
graphics.SetSmoothingMode(SmoothingModeAntiAlias);
|
|
graphics.SetInterpolationMode(InterpolationModeHighQualityBicubic);
|
|
graphics.SetPixelOffsetMode(PixelOffsetModeHalf);
|
|
graphics.DrawImage(page->brand, Rect(0, 174, 600, 196));
|
|
PaintCaptionButtons(graphics, *page);
|
|
}
|
|
delete page->brand;
|
|
page->brand = nullptr;
|
|
HWND window = CreateWindowExW(0, type.lpszClassName, L"", WS_CHILD,
|
|
0, 0, MulDiv(600, dpi, 96), MulDiv(600, dpi, 96), parent, nullptr, module, page);
|
|
if (!window) { delete page; return nullptr; }
|
|
// Keep the stock page available until the replacement is fully constructed.
|
|
HWND stockPage = GetParent(source);
|
|
if (!SetWindowSubclass(stockPage, HiddenPageProc, 1, 0)) {
|
|
DestroyWindow(window); return nullptr;
|
|
}
|
|
ShowWindow(stockPage, SW_HIDE);
|
|
ShowWindow(window, SW_SHOW);
|
|
return window;
|
|
}
|
|
|
|
// NSIS has already reported success. Pump the UI for the bounded final animation,
|
|
// including a painted 100% frame, before constructing the interactive finish page.
|
|
extern "C" __declspec(dllexport) BOOL __cdecl InstallerFinishProgress(HWND window) {
|
|
auto* page = reinterpret_cast<ProgressPage*>(GetWindowLongPtrW(window, GWLP_USERDATA));
|
|
if (!page) return FALSE;
|
|
const ULONGLONG started = GetTickCount64();
|
|
SetPropW(GetParent(window), L"HarnessInstaller.Succeeded", reinterpret_cast<HANDLE>(1));
|
|
page->progress.Complete(started);
|
|
while (IsWindow(window) && GetTickCount64() - started < 750) {
|
|
MSG message;
|
|
if (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
|
if (message.message == WM_QUIT) { PostQuitMessage(static_cast<int>(message.wParam)); return FALSE; }
|
|
TranslateMessage(&message);
|
|
DispatchMessageW(&message);
|
|
} else {
|
|
MsgWaitForMultipleObjectsEx(0, nullptr, 16, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
|
|
}
|
|
}
|
|
if (!IsWindow(window)) return FALSE;
|
|
InvalidateRect(window, nullptr, FALSE);
|
|
UpdateWindow(window);
|
|
return TRUE;
|
|
}
|
|
|
|
static LRESULT CALLBACK FrameProc(HWND window, UINT message, WPARAM wparam,
|
|
LPARAM lparam, UINT_PTR id, DWORD_PTR) {
|
|
if (message == WM_NCCALCSIZE && wparam) return 0;
|
|
// Activation must not repaint the standard frame over the custom client area.
|
|
if (message == WM_NCACTIVATE) return DefSubclassProc(window, message, wparam, -1);
|
|
if (message == WM_NCHITTEST) {
|
|
const LRESULT hit = DefSubclassProc(window, message, wparam, lparam);
|
|
// The installer has a fixed size; its page provides the drag area.
|
|
return hit >= HTLEFT && hit <= HTBOTTOMRIGHT ? HTCLIENT : hit;
|
|
}
|
|
if (message == WM_NCDESTROY) RemoveWindowSubclass(window, FrameProc, id);
|
|
return DefSubclassProc(window, message, wparam, lparam);
|
|
}
|
|
|
|
extern "C" __declspec(dllexport) HRESULT __cdecl InstallerApplyFrame(HWND window) {
|
|
// NSIS may release its DLL reference before the window receives WM_NCDESTROY.
|
|
HMODULE module = nullptr;
|
|
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_PIN | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
|
|
reinterpret_cast<LPCWSTR>(&FrameProc), &module)) return E_FAIL;
|
|
if (!SetWindowSubclass(window, FrameProc, 1, 0)) return E_FAIL;
|
|
const int stockControls[] = {1, 2, 3, 1028, 1256, 1034, 1035, 1036, 1037, 1038, 1039};
|
|
for (int id : stockControls) {
|
|
HWND child = GetDlgItem(window, id);
|
|
if (child) {
|
|
if (!SetWindowSubclass(child, HiddenPageProc, 1, 0)) return E_FAIL;
|
|
ShowWindow(child, SW_HIDE);
|
|
}
|
|
}
|
|
// DWM retains native transitions; WM_NCCALCSIZE removes the visible caption.
|
|
SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) | WS_THICKFRAME | WS_CAPTION);
|
|
const DWMNCRENDERINGPOLICY policy = DWMNCRP_ENABLED;
|
|
HRESULT result = DwmSetWindowAttribute(window, DWMWA_NCRENDERING_POLICY, &policy, sizeof(policy));
|
|
if (FAILED(result)) return result;
|
|
const DWORD rounded = 2;
|
|
// Windows 10 does not support the Windows 11 corner preference.
|
|
DwmSetWindowAttribute(window, 33, &rounded, sizeof(rounded));
|
|
const MARGINS margins = {1, 1, 1, 1};
|
|
result = DwmExtendFrameIntoClientArea(window, &margins);
|
|
SetWindowPos(window, nullptr, 0, 0, 0, 0,
|
|
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
|
|
return result;
|
|
}
|
|
|
|
// Present the first interactive page after resource preparation without keeping the installer topmost.
|
|
extern "C" __declspec(dllexport) BOOL __cdecl InstallerPresentWelcome(HWND window) {
|
|
if (!IsWindowVisible(window)) return FALSE;
|
|
if (!SetWindowPos(window, HWND_TOP, 0, 0, 0, 0,
|
|
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE)) return FALSE;
|
|
if (GetForegroundWindow() != window) {
|
|
FLASHWINFO flash = {sizeof(flash), window, FLASHW_TRAY | FLASHW_TIMERNOFG, 0, 0};
|
|
FlashWindowEx(&flash);
|
|
}
|
|
SetPropW(window, L"HarnessInstaller.Presented", reinterpret_cast<HANDLE>(1));
|
|
return TRUE;
|
|
}
|