#ifdef _WIN32 #define NOMINMAX #include #include #include #include "config.hpp" #include "connector_service.hpp" #include "platform.hpp" #include 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; HWND path_label = nullptr; HWND browse_button = nullptr; 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 {}; const int size = MultiByteToWideChar(CP_UTF8, 0, value.data(), static_cast(value.size()), nullptr, 0); std::wstring result(size, L'\0'); MultiByteToWideChar(CP_UTF8, 0, value.data(), static_cast(value.size()), result.data(), size); return result; } std::string utf8(const std::wstring& value) { if (value.empty()) return {}; const int size = WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast(value.size()), nullptr, 0, nullptr, nullptr); std::string result(size, '\0'); WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast(value.size()), result.data(), size, nullptr, nullptr); return result; } std::wstring field_text(HWND field) { const int length = GetWindowTextLengthW(field); std::wstring value(static_cast(length) + 1, L'\0'); GetWindowTextW(field, value.data(), length + 1); value.resize(static_cast(length)); return value; } void refresh_status() { std::wstring status; 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}) { if (control) ShowWindow(control, show_path ? SW_SHOW : SW_HIDE); } } void save_path(HWND window) { const std::string path = utf8(field_text(path_field)); if (!path.empty() && !connector::valid_spine_executable(path)) { MessageBoxW(window, L"请选择实际存在的 Spine.com 文件。", L"Spine 路径无效", MB_OK | MB_ICONWARNING); return; } try { auto config = connector::load_config(); config.spine_executable = path; connector::save_config(config); refresh_status(); } catch (const std::exception& error) { MessageBoxW(window, wide(error.what()).c_str(), L"保存失败", MB_OK | MB_ICONERROR); } } LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) { (void)lparam; try { switch (message) { case WM_CREATE: { 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 = 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: { wchar_t filename[MAX_PATH] = L""; OPENFILENAMEW dialog{}; dialog.lStructSize = sizeof(dialog); dialog.hwndOwner = window; dialog.lpstrFilter = L"Spine.com\0Spine.com\0所有文件\0*.*\0"; dialog.lpstrFile = filename; dialog.nMaxFile = MAX_PATH; dialog.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST; if (GetOpenFileNameW(&dialog)) SetWindowTextW(path_field, filename); return 0; } case 102: SetWindowTextW(path_field, L""); save_path(window); return 0; case 103: save_path(window); return 0; case 104: if (service.running()) { service.stop(); startup_error.clear(); } else { startup_error.clear(); service.start(startup_error); } refresh_status(); 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(); 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 (...) { return DefWindowProcW(window, message, wparam, lparam); } } } // namespace int WINAPI wWinMain(HINSTANCE instance, HINSTANCE previous, PWSTR command_line, int show) { (void)previous; (void)command_line; try { service.start(startup_error); } 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 = 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, 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{}; while (GetMessageW(&message, nullptr, 0, 0) > 0) { TranslateMessage(&message); DispatchMessageW(&message); } service.stop(); return static_cast(message.wParam); } #endif