window-frame.cpp 12 KB

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