window-frame.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. // Keeps the DWM frame and shadow while the NSIS page owns the entire client area.
  2. #define WIN32_LEAN_AND_MEAN
  3. #define NOMINMAX
  4. #define UNICODE
  5. #include <windows.h>
  6. #include <tlhelp32.h>
  7. #include <objidl.h>
  8. #include <commctrl.h>
  9. #include <dwmapi.h>
  10. #include <gdiplus.h>
  11. #include <new>
  12. #include <algorithm>
  13. #include "progress.h"
  14. #include "extract.h"
  15. using namespace Gdiplus;
  16. // Match the affected executable, not another user's or directory's same-named application.
  17. // Returns 0 while running, 1 when absent, and -1 if the process list cannot be read.
  18. extern "C" __declspec(dllexport) int __cdecl InstallerFindProcess(LPCWSTR executable) {
  19. WCHAR target[32768];
  20. DWORD length = GetLongPathNameW(executable, target, ARRAYSIZE(target));
  21. LPCWSTR expected = length > 0 && length < ARRAYSIZE(target) ? target : executable;
  22. LPCWSTR filename = wcsrchr(expected, L'\\');
  23. filename = filename ? filename + 1 : expected;
  24. HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  25. if (snapshot == INVALID_HANDLE_VALUE) return -1;
  26. PROCESSENTRY32W entry = {};
  27. entry.dwSize = sizeof(entry);
  28. int result = 1;
  29. BOOL present = Process32FirstW(snapshot, &entry);
  30. while (present) {
  31. if (_wcsicmp(entry.szExeFile, filename) == 0) {
  32. HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, entry.th32ProcessID);
  33. if (process) {
  34. WCHAR path[32768];
  35. DWORD count = ARRAYSIZE(path);
  36. if (QueryFullProcessImageNameW(process, 0, path, &count) && _wcsicmp(path, expected) == 0) result = 0;
  37. CloseHandle(process);
  38. if (result == 0) break;
  39. }
  40. }
  41. present = Process32NextW(snapshot, &entry);
  42. }
  43. if (!present && GetLastError() != ERROR_NO_MORE_FILES) result = -1;
  44. CloseHandle(snapshot);
  45. return result;
  46. }
  47. struct ProgressPage {
  48. InstallProgress progress{GetTickCount64()};
  49. bool dark;
  50. UINT dpi;
  51. ULONG_PTR gdiplus;
  52. Image* brand;
  53. WCHAR captions[5][128];
  54. };
  55. // NSIS shows its page after MUI's SHOW callback; keep its controls off screen.
  56. static LRESULT CALLBACK HiddenPageProc(HWND window, UINT message, WPARAM wparam,
  57. LPARAM lparam, UINT_PTR id, DWORD_PTR) {
  58. if (message == WM_WINDOWPOSCHANGING) {
  59. auto* position = reinterpret_cast<WINDOWPOS*>(lparam);
  60. position->flags = (position->flags & ~SWP_SHOWWINDOW) | SWP_HIDEWINDOW;
  61. }
  62. if (message == WM_NCDESTROY) RemoveWindowSubclass(window, HiddenPageProc, id);
  63. return DefSubclassProc(window, message, wparam, lparam);
  64. }
  65. static void FillProgress(Graphics& graphics, Brush& brush, REAL width) {
  66. if (width <= 0) return;
  67. if (width < 4) { graphics.FillRectangle(&brush, 64.0f, 482.0f, width, 6.0f); return; }
  68. GraphicsPath path;
  69. path.AddArc(64.0f, 482.0f, 4.0f, 4.0f, 180.0f, 90.0f);
  70. path.AddArc(64.0f + width - 4, 482.0f, 4.0f, 4.0f, 270.0f, 90.0f);
  71. path.AddArc(64.0f + width - 4, 484.0f, 4.0f, 4.0f, 0.0f, 90.0f);
  72. path.AddArc(64.0f, 484.0f, 4.0f, 4.0f, 90.0f, 90.0f);
  73. path.CloseFigure();
  74. graphics.FillPath(&brush, &path);
  75. }
  76. static LRESULT CALLBACK ProgressProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) {
  77. auto* page = reinterpret_cast<ProgressPage*>(GetWindowLongPtrW(window, GWLP_USERDATA));
  78. if (message == WM_CREATE) {
  79. page = static_cast<ProgressPage*>(reinterpret_cast<CREATESTRUCTW*>(lparam)->lpCreateParams);
  80. SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(page));
  81. SetTimer(window, 1, 16, nullptr);
  82. }
  83. if (message == WM_ERASEBKGND) return 1;
  84. if (message == WM_TIMER) { InvalidateRect(window, nullptr, FALSE); return 0; }
  85. if (message == WM_LBUTTONDOWN && page) {
  86. const int x = LOWORD(lparam) * 96 / page->dpi;
  87. const int y = HIWORD(lparam) * 96 / page->dpi;
  88. if (y < 48) {
  89. HWND parent = GetParent(window);
  90. if (x >= 548) PostMessageW(parent, WM_CLOSE, 0, 0);
  91. else if (x >= 504) ShowWindow(parent, SW_MINIMIZE);
  92. else { ReleaseCapture(); SendMessageW(parent, WM_NCLBUTTONDOWN, HTCAPTION, 0); }
  93. }
  94. return 0;
  95. }
  96. if (message == WM_PAINT && page) {
  97. PAINTSTRUCT paint;
  98. HDC dc = BeginPaint(window, &paint);
  99. {
  100. Bitmap buffer(MulDiv(600, page->dpi, 96), MulDiv(600, page->dpi, 96), PixelFormat32bppPARGB);
  101. Graphics graphics(&buffer);
  102. graphics.ScaleTransform(page->dpi / 96.0f, page->dpi / 96.0f);
  103. graphics.Clear(page->dark ? Color(255, 21, 21, 23) : Color(255, 255, 255, 255));
  104. graphics.SetSmoothingMode(SmoothingModeAntiAlias);
  105. graphics.DrawImage(page->brand, Rect(0, 174, 600, 196));
  106. const int stage = static_cast<int>(reinterpret_cast<INT_PTR>(GetPropW(GetParent(window), L"HarnessInstaller.Stage")));
  107. const double fraction = reinterpret_cast<UINT_PTR>(GetPropW(GetParent(window), L"HarnessInstaller.ExtractProgress")) / 100.0;
  108. page->progress.Advance(stage, fraction, GetTickCount64());
  109. const int percent = static_cast<int>(page->progress.value);
  110. SolidBrush track(page->dark ? Color(255, 97, 102, 107) : Color(255, 233, 236, 242));
  111. SolidBrush ink(page->dark ? Color(255, 255, 255, 255) : Color(255, 15, 17, 21));
  112. FillProgress(graphics, track, 472.0f);
  113. FillProgress(graphics, ink, 472.0f * static_cast<REAL>(page->progress.value) / 100);
  114. FontFamily family(L"Microsoft YaHei UI");
  115. Font font(&family, 14, FontStyleRegular, UnitPixel);
  116. StringFormat centered;
  117. centered.SetAlignment(StringAlignmentCenter);
  118. centered.SetLineAlignment(StringAlignmentCenter);
  119. WCHAR caption[160];
  120. wsprintfW(caption, page->captions[page->progress.CaptionStage()], percent);
  121. SetWindowTextW(window, caption);
  122. graphics.DrawString(caption, -1, &font, RectF(48, 512, 504, 22), &centered, &ink);
  123. Font controls(&family, 16, FontStyleRegular, UnitPixel);
  124. graphics.DrawString(L"\x2212", -1, &controls, RectF(504, 8, 40, 32), &centered, &ink);
  125. graphics.DrawString(L"\x00d7", -1, &controls, RectF(548, 8, 40, 32), &centered, &ink);
  126. Graphics screen(dc);
  127. screen.DrawImage(&buffer, 0, 0);
  128. }
  129. EndPaint(window, &paint);
  130. if (page->progress.value == 100) SetPropW(GetParent(window), L"HarnessInstaller.CompletedPercent", reinterpret_cast<HANDLE>(100));
  131. return 0;
  132. }
  133. if (message == WM_NCDESTROY && page) {
  134. KillTimer(window, 1);
  135. delete page->brand;
  136. GdiplusShutdown(page->gdiplus);
  137. delete page;
  138. }
  139. return DefWindowProcW(window, message, wparam, lparam);
  140. }
  141. // Runs on the NSIS UI thread; the stock installation section runs on its worker.
  142. extern "C" __declspec(dllexport) HWND __cdecl InstallerShowProgress(HWND parent, HWND source,
  143. BOOL dark, UINT dpi, const WCHAR* brand, const WCHAR* preparing, const WCHAR* extracting,
  144. const WCHAR* copying, const WCHAR* registering, const WCHAR* cleaning) {
  145. HINSTANCE module = GetModuleHandleW(nullptr);
  146. WNDCLASSW type = {};
  147. type.lpfnWndProc = ProgressProc;
  148. type.hInstance = module;
  149. type.lpszClassName = L"HarnessInstallerProgress";
  150. type.hCursor = LoadCursorW(nullptr, IDC_ARROW);
  151. RegisterClassW(&type);
  152. auto* page = new (std::nothrow) ProgressPage{};
  153. if (!page) return nullptr;
  154. GdiplusStartupInput startup;
  155. if (GdiplusStartup(&page->gdiplus, &startup, nullptr) != Ok) { delete page; return nullptr; }
  156. HWND stockPage = GetParent(source);
  157. if (!SetWindowSubclass(stockPage, HiddenPageProc, 1, 0)) {
  158. GdiplusShutdown(page->gdiplus); delete page; return nullptr;
  159. }
  160. ShowWindow(stockPage, SW_HIDE);
  161. page->dark = dark != FALSE;
  162. page->dpi = dpi;
  163. const WCHAR* captions[] = {preparing, extracting, copying, registering, cleaning};
  164. for (int i = 0; i < 5; ++i) lstrcpynW(page->captions[i], captions[i], 128);
  165. page->brand = new Image(brand);
  166. if (page->brand->GetLastStatus() != Ok) {
  167. delete page->brand; GdiplusShutdown(page->gdiplus); delete page; return nullptr;
  168. }
  169. HWND window = CreateWindowExW(0, type.lpszClassName, L"", WS_CHILD | WS_VISIBLE,
  170. 0, 0, MulDiv(600, dpi, 96), MulDiv(600, dpi, 96), parent, nullptr, module, page);
  171. if (!window) { delete page->brand; GdiplusShutdown(page->gdiplus); delete page; }
  172. return window;
  173. }
  174. // NSIS has already reported success. Pump the UI for the bounded final animation,
  175. // including a painted 100% frame, before constructing the interactive finish page.
  176. extern "C" __declspec(dllexport) BOOL __cdecl InstallerFinishProgress(HWND window) {
  177. auto* page = reinterpret_cast<ProgressPage*>(GetWindowLongPtrW(window, GWLP_USERDATA));
  178. if (!page) return FALSE;
  179. const ULONGLONG started = GetTickCount64();
  180. SetPropW(GetParent(window), L"HarnessInstaller.Succeeded", reinterpret_cast<HANDLE>(1));
  181. page->progress.Complete(started);
  182. while (IsWindow(window) && GetTickCount64() - started < 750) {
  183. MSG message;
  184. if (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
  185. if (message.message == WM_QUIT) { PostQuitMessage(static_cast<int>(message.wParam)); return FALSE; }
  186. TranslateMessage(&message);
  187. DispatchMessageW(&message);
  188. } else {
  189. MsgWaitForMultipleObjectsEx(0, nullptr, 16, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
  190. }
  191. }
  192. if (!IsWindow(window)) return FALSE;
  193. InvalidateRect(window, nullptr, FALSE);
  194. UpdateWindow(window);
  195. return TRUE;
  196. }
  197. static LRESULT CALLBACK FrameProc(HWND window, UINT message, WPARAM wparam,
  198. LPARAM lparam, UINT_PTR id, DWORD_PTR) {
  199. if (message == WM_NCCALCSIZE && wparam) return 0;
  200. if (message == WM_NCHITTEST) {
  201. const LRESULT hit = DefSubclassProc(window, message, wparam, lparam);
  202. // The installer has a fixed size; its page provides the drag area.
  203. return hit >= HTLEFT && hit <= HTBOTTOMRIGHT ? HTCLIENT : hit;
  204. }
  205. if (message == WM_NCDESTROY) RemoveWindowSubclass(window, FrameProc, id);
  206. return DefSubclassProc(window, message, wparam, lparam);
  207. }
  208. extern "C" __declspec(dllexport) HRESULT __cdecl InstallerApplyFrame(HWND window) {
  209. // NSIS may release its DLL reference before the window receives WM_NCDESTROY.
  210. HMODULE module = nullptr;
  211. if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_PIN | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
  212. reinterpret_cast<LPCWSTR>(&FrameProc), &module)) return E_FAIL;
  213. if (!SetWindowSubclass(window, FrameProc, 1, 0)) return E_FAIL;
  214. const int stockControls[] = {1, 2, 3, 1028, 1256, 1034, 1035, 1036, 1037, 1038, 1039};
  215. for (int id : stockControls) {
  216. HWND child = GetDlgItem(window, id);
  217. if (child) {
  218. if (!SetWindowSubclass(child, HiddenPageProc, 1, 0)) return E_FAIL;
  219. ShowWindow(child, SW_HIDE);
  220. }
  221. }
  222. SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) | WS_THICKFRAME);
  223. const DWMNCRENDERINGPOLICY policy = DWMNCRP_ENABLED;
  224. HRESULT result = DwmSetWindowAttribute(window, DWMWA_NCRENDERING_POLICY, &policy, sizeof(policy));
  225. if (FAILED(result)) return result;
  226. const DWORD rounded = 2;
  227. // Windows 10 does not support the Windows 11 corner preference.
  228. DwmSetWindowAttribute(window, 33, &rounded, sizeof(rounded));
  229. const MARGINS margins = {1, 1, 1, 1};
  230. result = DwmExtendFrameIntoClientArea(window, &margins);
  231. SetWindowPos(window, nullptr, 0, 0, 0, 0,
  232. SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
  233. return result;
  234. }