window-frame.cpp 10 KB

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