diff --git a/connector/native/app_macos.mm b/connector/native/app_macos.mm index 985236a..0df8937 100644 --- a/connector/native/app_macos.mm +++ b/connector/native/app_macos.mm @@ -170,6 +170,7 @@ int main(int argc, char* argv[]) { service.start(startup_error); NSApplication* application = [NSApplication sharedApplication]; application.activationPolicy = NSApplicationActivationPolicyRegular; + application.appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; ConnectorAppDelegate* delegate = [[ConnectorAppDelegate alloc] init]; delegate.service = &service; delegate.startupError = [NSString stringWithUTF8String:startup_error.c_str()]; diff --git a/connector/native/app_windows.cpp b/connector/native/app_windows.cpp index 7a0927a..44ec0ed 100644 --- a/connector/native/app_windows.cpp +++ b/connector/native/app_windows.cpp @@ -3,6 +3,7 @@ #define NOMINMAX #include #include +#include #include "config.hpp" #include "connector_service.hpp" @@ -12,6 +13,16 @@ namespace { +constexpr COLORREF kBackground = RGB(38, 37, 36); +constexpr COLORREF kControlBackground = RGB(56, 55, 54); +constexpr COLORREF kControlPressed = RGB(70, 69, 68); +constexpr COLORREF kBorder = RGB(76, 75, 74); +constexpr COLORREF kPrimaryText = RGB(238, 238, 238); +constexpr COLORREF kSecondaryText = RGB(166, 164, 162); +constexpr COLORREF kSuccessText = RGB(48, 209, 88); +constexpr COLORREF kWarningText = RGB(255, 159, 10); +constexpr COLORREF kErrorText = RGB(255, 69, 58); + connector::ConnectorService service; HWND path_field = nullptr; HWND status_label = nullptr; @@ -21,6 +32,89 @@ HWND automatic_button = nullptr; HWND save_button = nullptr; HWND connection_button = nullptr; std::string startup_error; +UINT ui_dpi = 96; +HFONT title_font = nullptr; +HFONT body_font = nullptr; +HBRUSH background_brush = nullptr; +HBRUSH control_brush = nullptr; +COLORREF status_color = kSuccessText; + +int scaled(int value) { + return MulDiv(value, static_cast(ui_dpi), 96); +} + +void enable_dpi_awareness() { + HMODULE user32 = GetModuleHandleW(L"user32.dll"); + using SetAwareness = BOOL(WINAPI*)(HANDLE); + const auto set_awareness = reinterpret_cast(GetProcAddress(user32, "SetProcessDpiAwarenessContext")); + if (set_awareness) set_awareness(reinterpret_cast(static_cast(-4))); // PER_MONITOR_AWARE_V2 + using GetDpi = UINT(WINAPI*)(); + const auto get_dpi = reinterpret_cast(GetProcAddress(user32, "GetDpiForSystem")); + if (get_dpi) ui_dpi = get_dpi(); +} + +HFONT make_font(int point_size, int weight) { + return CreateFontW(-MulDiv(point_size, static_cast(ui_dpi), 72), 0, 0, 0, weight, FALSE, FALSE, FALSE, + DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, + DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI"); +} + +void use_dark_title_bar(HWND window) { + HMODULE dwm = LoadLibraryW(L"dwmapi.dll"); + if (!dwm) return; + using SetAttribute = HRESULT(WINAPI*)(HWND, DWORD, LPCVOID, DWORD); + const auto set_attribute = reinterpret_cast(GetProcAddress(dwm, "DwmSetWindowAttribute")); + if (set_attribute) { + const BOOL enabled = TRUE; + // Windows 10 20H1+ uses 20; older Windows 10 builds use 19. Both calls are safe to probe. + if (FAILED(set_attribute(window, 20, &enabled, sizeof(enabled)))) { + set_attribute(window, 19, &enabled, sizeof(enabled)); + } + } + FreeLibrary(dwm); +} + +HWND make_label(HWND parent, const wchar_t* text, int id, int x, int y, int width, int height, HFONT font) { + HWND label = CreateWindowW(L"STATIC", text, WS_CHILD | WS_VISIBLE | SS_LEFT, + scaled(x), scaled(y), scaled(width), scaled(height), parent, + reinterpret_cast(static_cast(id)), nullptr, nullptr); + SendMessageW(label, WM_SETFONT, reinterpret_cast(font), TRUE); + return label; +} + +HWND make_button(HWND parent, const wchar_t* text, int id, int x, int y, int width, int height) { + HWND button = CreateWindowW(L"BUTTON", text, WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_OWNERDRAW, + scaled(x), scaled(y), scaled(width), scaled(height), parent, + reinterpret_cast(static_cast(id)), nullptr, nullptr); + SendMessageW(button, WM_SETFONT, reinterpret_cast(body_font), TRUE); + return button; +} + +void draw_dark_button(const DRAWITEMSTRUCT& item) { + RECT bounds = item.rcItem; + HBRUSH fill = CreateSolidBrush((item.itemState & ODS_SELECTED) ? kControlPressed : kControlBackground); + HPEN border = CreatePen(PS_SOLID, scaled(1), kBorder); + const HGDIOBJ old_brush = SelectObject(item.hDC, fill); + const HGDIOBJ old_pen = SelectObject(item.hDC, border); + RoundRect(item.hDC, bounds.left, bounds.top, bounds.right, bounds.bottom, scaled(7), scaled(7)); + SelectObject(item.hDC, old_pen); + SelectObject(item.hDC, old_brush); + DeleteObject(border); + DeleteObject(fill); + + const int length = GetWindowTextLengthW(item.hwndItem); + std::wstring text(static_cast(length) + 1, L'\0'); + GetWindowTextW(item.hwndItem, text.data(), length + 1); + text.resize(static_cast(length)); + SetBkMode(item.hDC, TRANSPARENT); + SetTextColor(item.hDC, kPrimaryText); + SelectObject(item.hDC, body_font); + DrawTextW(item.hDC, text.c_str(), length, &bounds, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + if (item.itemState & ODS_FOCUS) { + InflateRect(&bounds, -scaled(4), -scaled(4)); + DrawFocusRect(item.hDC, &bounds); + } +} std::wstring wide(const std::string& value) { if (value.empty()) return {}; @@ -48,10 +142,21 @@ std::wstring field_text(HWND field) { void refresh_status() { std::wstring status; - if (!startup_error.empty()) status = L"连接器未启动 · " + wide(startup_error); - else if (!service.running()) status = L"连接器已断开"; - else status = connector::find_spine_executable().empty() ? L"连接器运行中 · 未找到 Spine" : L"连接器运行中 · Spine 已就绪"; + if (!startup_error.empty()) { + status = L"● 连接器未启动 · " + wide(startup_error); + status_color = kErrorText; + } else if (!service.running()) { + status = L"● 连接器已断开"; + status_color = kSecondaryText; + } else if (connector::find_spine_executable().empty()) { + status = L"● 连接器运行中 · 未找到 Spine"; + status_color = kWarningText; + } else { + status = L"● 连接器运行中 · Spine 已就绪"; + status_color = kSuccessText; + } SetWindowTextW(status_label, status.c_str()); + InvalidateRect(status_label, nullptr, TRUE); SetWindowTextW(connection_button, service.running() ? L"断开连接" : L"启动连接"); const bool show_path = connector::find_spine_executable().empty(); for (HWND control : {path_label, path_field, browse_button, automatic_button, save_button}) { @@ -80,30 +185,47 @@ LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wparam, LPARAM lp try { switch (message) { case WM_CREATE: { - HFONT font = static_cast(GetStockObject(DEFAULT_GUI_FONT)); - HWND title = CreateWindowW(L"STATIC", L"Spine粒子连接器", WS_CHILD | WS_VISIBLE, 24, 20, 540, 34, window, nullptr, nullptr, nullptr); - SendMessageW(title, WM_SETFONT, reinterpret_cast(font), TRUE); - HWND help = CreateWindowW(L"STATIC", L"应用打开期间,网页可调用本机 Spine 生成工程文件。", WS_CHILD | WS_VISIBLE, 24, 58, 540, 24, window, nullptr, nullptr, nullptr); - SendMessageW(help, WM_SETFONT, reinterpret_cast(font), TRUE); - status_label = CreateWindowW(L"STATIC", L"", WS_CHILD | WS_VISIBLE, 24, 91, 540, 24, window, nullptr, nullptr, nullptr); - SendMessageW(status_label, WM_SETFONT, reinterpret_cast(font), TRUE); - path_label = CreateWindowW(L"STATIC", L"Spine.com 路径", WS_CHILD | WS_VISIBLE, 24, 128, 160, 24, window, nullptr, nullptr, nullptr); - SendMessageW(path_label, WM_SETFONT, reinterpret_cast(font), TRUE); - path_field = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"", WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, 24, 153, 390, 30, window, reinterpret_cast(100), nullptr, nullptr); - SendMessageW(path_field, WM_SETFONT, reinterpret_cast(font), TRUE); + make_label(window, L"Spine粒子连接器", 200, 28, 24, 560, 34, title_font); + make_label(window, L"应用打开且连接已启动时,网页可调用本机 Spine 生成工程文件。", 201, 29, 65, 560, 24, body_font); + status_label = make_label(window, L"", 202, 29, 101, 560, 24, body_font); + path_label = make_label(window, L"Spine.com 路径", 203, 29, 140, 160, 24, body_font); + path_field = CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL, + scaled(29), scaled(168), scaled(390), scaled(32), window, + reinterpret_cast(100), nullptr, nullptr); + SendMessageW(path_field, WM_SETFONT, reinterpret_cast(body_font), TRUE); + SendMessageW(path_field, EM_SETMARGINS, EC_LEFTMARGIN | EC_RIGHTMARGIN, MAKELPARAM(scaled(8), scaled(8))); const auto config = connector::load_config(); SetWindowTextW(path_field, wide(config.spine_executable).c_str()); - browse_button = CreateWindowW(L"BUTTON", L"选择 Spine.com", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 425, 151, 145, 34, window, reinterpret_cast(101), nullptr, nullptr); - SendMessageW(browse_button, WM_SETFONT, reinterpret_cast(font), TRUE); - automatic_button = CreateWindowW(L"BUTTON", L"恢复自动检测", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 24, 202, 135, 34, window, reinterpret_cast(102), nullptr, nullptr); - SendMessageW(automatic_button, WM_SETFONT, reinterpret_cast(font), TRUE); - save_button = CreateWindowW(L"BUTTON", L"保存路径", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, 435, 202, 135, 34, window, reinterpret_cast(103), nullptr, nullptr); - SendMessageW(save_button, WM_SETFONT, reinterpret_cast(font), TRUE); - connection_button = CreateWindowW(L"BUTTON", L"断开连接", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 435, 250, 135, 34, window, reinterpret_cast(104), nullptr, nullptr); - SendMessageW(connection_button, WM_SETFONT, reinterpret_cast(font), TRUE); + browse_button = make_button(window, L"选择 Spine.com", 101, 430, 166, 160, 36); + automatic_button = make_button(window, L"恢复自动检测", 102, 29, 212, 130, 36); + save_button = make_button(window, L"保存路径", 103, 460, 212, 130, 36); + connection_button = make_button(window, L"断开连接", 104, 460, 265, 130, 36); refresh_status(); return 0; } + case WM_CTLCOLORSTATIC: { + HDC context = reinterpret_cast(wparam); + const int id = GetDlgCtrlID(reinterpret_cast(lparam)); + SetBkMode(context, TRANSPARENT); + if (id == 202) SetTextColor(context, status_color); + else if (id == 201) SetTextColor(context, kSecondaryText); + else SetTextColor(context, kPrimaryText); + return reinterpret_cast(background_brush); + } + case WM_CTLCOLOREDIT: { + HDC context = reinterpret_cast(wparam); + SetBkColor(context, kControlBackground); + SetTextColor(context, kPrimaryText); + return reinterpret_cast(control_brush); + } + case WM_DRAWITEM: { + const auto* item = reinterpret_cast(lparam); + if (item && item->CtlType == ODT_BUTTON) { + draw_dark_button(*item); + return TRUE; + } + break; + } case WM_COMMAND: switch (LOWORD(wparam)) { case 101: { @@ -132,8 +254,21 @@ LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wparam, LPARAM lp return 0; } break; + case WM_ERASEBKGND: { + RECT area{}; + GetClientRect(window, &area); + FillRect(reinterpret_cast(wparam), &area, background_brush); + return TRUE; + } case WM_CLOSE: DestroyWindow(window); return 0; - case WM_DESTROY: service.stop(); PostQuitMessage(0); return 0; + case WM_DESTROY: + service.stop(); + if (title_font) DeleteObject(title_font); + if (body_font) DeleteObject(body_font); + if (background_brush) DeleteObject(background_brush); + if (control_brush) DeleteObject(control_brush); + PostQuitMessage(0); + return 0; } return DefWindowProcW(window, message, wparam, lparam); } catch (...) { @@ -151,17 +286,26 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE previous, PWSTR command_line, } catch (...) { startup_error = "连接器启动失败"; } + enable_dpi_awareness(); + title_font = make_font(20, FW_SEMIBOLD); + body_font = make_font(13, FW_NORMAL); + background_brush = CreateSolidBrush(kBackground); + control_brush = CreateSolidBrush(kControlBackground); const wchar_t* class_name = L"SpineParticleConnectorWindow"; WNDCLASSW window_class{}; window_class.lpfnWndProc = window_proc; window_class.hInstance = instance; window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); - window_class.hbrBackground = reinterpret_cast(COLOR_WINDOW + 1); + window_class.hbrBackground = background_brush; window_class.lpszClassName = class_name; RegisterClassW(&window_class); + RECT window_bounds{0, 0, scaled(620), scaled(326)}; + AdjustWindowRect(&window_bounds, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, FALSE); HWND window = CreateWindowW(class_name, L"Spine粒子连接器", WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, - CW_USEDEFAULT, CW_USEDEFAULT, 620, 350, nullptr, nullptr, instance, nullptr); + CW_USEDEFAULT, CW_USEDEFAULT, window_bounds.right - window_bounds.left, + window_bounds.bottom - window_bounds.top, nullptr, nullptr, instance, nullptr); if (!window) return 1; + use_dark_title_bar(window); ShowWindow(window, show); UpdateWindow(window); MSG message{}; diff --git a/release/connector/macos-arm64/Spine粒子连接器.app/Contents/MacOS/SpineParticleConnector b/release/connector/macos-arm64/Spine粒子连接器.app/Contents/MacOS/SpineParticleConnector index d9a1403..6894cee 100755 Binary files a/release/connector/macos-arm64/Spine粒子连接器.app/Contents/MacOS/SpineParticleConnector and b/release/connector/macos-arm64/Spine粒子连接器.app/Contents/MacOS/SpineParticleConnector differ