From 28c1b377df786b48e2f1ed4b9ec9d3bc27445aa4 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Wed, 25 Feb 2026 00:14:39 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B0=BD=20=EB=8B=A4=ED=81=AC=20=ED=85=8C=EB=A7=88=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 배경 #2E2C2F, 타이틀 #BACDB0(강조색), 상태 텍스트 밝은 회색 - WM_CTLCOLORSTATIC으로 STATIC 컨트롤 색상 제어 - SetWindowTheme + PBM_SETBARCOLOR로 진행 막대 색상 변경 - "A301" 타이틀 레이블(13pt bold) + 상태 레이블(9pt) 분리 - 웹사이트 색상 팔레트와 통일 Co-Authored-By: Claude Sonnet 4.6 --- main.go | 118 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 93 insertions(+), 25 deletions(-) diff --git a/main.go b/main.go index b4bfe6a..c3c8c8e 100644 --- a/main.go +++ b/main.go @@ -30,11 +30,12 @@ const ( // Win32 constants const ( - wmDestroy uint32 = 0x0002 - wmClose uint32 = 0x0010 - wmSetFont uint32 = 0x0030 - wmSetText uint32 = 0x000C - wmAppDone uint32 = 0x8001 + wmDestroy uint32 = 0x0002 + wmClose uint32 = 0x0010 + wmSetFont uint32 = 0x0030 + wmSetText uint32 = 0x000C + wmCtlColorStatic uint32 = 0x0138 + wmAppDone uint32 = 0x8001 wsPopup uintptr = 0x80000000 wsCaption uintptr = 0x00C00000 @@ -43,9 +44,13 @@ const ( wsVisible uintptr = 0x10000000 ssCenter uintptr = 0x00000001 - pbsSmooth uintptr = 0x01 - pbmSetRange32 uint32 = 0x0406 - pbmSetPos uint32 = 0x0402 + pbsSmooth uintptr = 0x01 + pbmSetRange32 uint32 = 0x0406 + pbmSetPos uint32 = 0x0402 + pbmSetBarColor uint32 = 0x0409 + pbmSetBkColor uint32 = 0x2001 + + setBkModeTransparent = 1 swShow = 5 smCxScreen = 0 @@ -59,7 +64,19 @@ const ( idYes = 6 iccProgressClass uint32 = 0x00000020 - logpixelsX = 88 +) + +// rgb builds a COLORREF from R, G, B components. +func rgb(r, g, b uint8) uintptr { + return uintptr(r) | (uintptr(g) << 8) | (uintptr(b) << 16) +} + +// 웹사이트 색상과 동일한 팔레트 +var ( + colorBg = rgb(46, 44, 47) // #2E2C2F + colorText = rgb(200, 200, 200) // 밝은 회색 + colorAccent = rgb(186, 205, 176) // #BACDB0 + colorProgressBg = rgb(65, 63, 67) // bg보다 약간 밝은 색 ) var ( @@ -68,6 +85,7 @@ var ( gdi32 = windows.NewLazySystemDLL("gdi32.dll") shell32 = windows.NewLazySystemDLL("shell32.dll") comctl32 = windows.NewLazySystemDLL("comctl32.dll") + uxtheme = windows.NewLazySystemDLL("uxtheme.dll") messageBoxWProc = user32.NewProc("MessageBoxW") registerClassExWProc = user32.NewProc("RegisterClassExW") @@ -88,12 +106,18 @@ var ( shellExecuteWProc = shell32.NewProc("ShellExecuteW") getModuleHandleWProc = kernel32.NewProc("GetModuleHandleW") createFontIndirectWProc = gdi32.NewProc("CreateFontIndirectW") + createSolidBrushProc = gdi32.NewProc("CreateSolidBrush") + setTextColorProc = gdi32.NewProc("SetTextColor") + setBkModeProc = gdi32.NewProc("SetBkMode") deleteObjectProc = gdi32.NewProc("DeleteObject") initCommonControlsExProc = comctl32.NewProc("InitCommonControlsEx") + setWindowThemeProc = uxtheme.NewProc("SetWindowTheme") wndProcCb uintptr + titleLabelHwnd uintptr progressLabelHwnd uintptr progressBarHwnd uintptr + hBrushBg uintptr ) type wndClassExW struct { @@ -169,12 +193,16 @@ func dpiScale(px int, dpi uint32) uintptr { // ── Font helpers ───────────────────────────────────────────────────────────── -func createUIFont(pointSize int, dpi uint32) uintptr { +func createUIFont(pointSize int, dpi uint32, bold bool) uintptr { + weight := int32(400) // FW_NORMAL + if bold { + weight = 700 // FW_BOLD + } lf := logFontW{ lfHeight: -int32(pointSize) * int32(dpi) / 72, - lfWeight: 400, // FW_NORMAL - lfCharSet: 1, // DEFAULT_CHARSET - lfQuality: 5, // CLEARTYPE_QUALITY + lfWeight: weight, + lfCharSet: 1, // DEFAULT_CHARSET + lfQuality: 5, // CLEARTYPE_QUALITY } face, _ := windows.UTF16FromString("Segoe UI") copy(lf.lfFaceName[:], face) @@ -219,6 +247,16 @@ func progressWndProc(hwnd, uMsg, wParam, lParam uintptr) uintptr { case wmAppDone: destroyWindowProc.Call(hwnd) return 0 + case wmCtlColorStatic: + // 다크 테마: 배경 브러시 + 텍스트 색 지정 + hdc := wParam + setBkModeProc.Call(hdc, setBkModeTransparent) + if lParam == titleLabelHwnd { + setTextColorProc.Call(hdc, colorAccent) + } else { + setTextColorProc.Call(hdc, colorText) + } + return hBrushBg } ret, _, _ := defWindowProcWProc.Call(hwnd, uMsg, wParam, lParam) return ret @@ -234,7 +272,7 @@ func setProgress(text string, pct int) { } } -// downloadWithProgress shows a DPI-aware progress window and downloads+extracts the zip. +// downloadWithProgress shows a DPI-aware dark-themed progress window and downloads+extracts the zip. // Must be called from the main goroutine (Win32 message loop requirement). func downloadWithProgress(downloadURL, destDir string) error { runtime.LockOSThread() @@ -244,6 +282,10 @@ func downloadWithProgress(downloadURL, destDir string) error { dpi := getSystemDPI() s := func(px int) uintptr { return dpiScale(px, dpi) } + // 배경 브러시 생성 (window proc에서도 사용) + hBrushBg, _, _ = createSolidBrushProc.Call(colorBg) + defer deleteObjectProc.Call(hBrushBg) + hInstance, _, _ := getModuleHandleWProc.Call(0) className, _ := windows.UTF16PtrFromString("A301Progress") @@ -252,55 +294,81 @@ func downloadWithProgress(downloadURL, destDir string) error { lpfnWndProc: wndProcCb, hInstance: hInstance, lpszClassName: className, - hbrBackground: 16, // COLOR_BTNFACE+1 + hbrBackground: hBrushBg, } registerClassExWProc.Call(uintptr(unsafe.Pointer(&wc))) screenW, _, _ := getSystemMetricsProc.Call(smCxScreen) screenH, _, _ := getSystemMetricsProc.Call(smCyScreen) winW := s(440) - winH := s(130) + winH := s(152) x := (screenW - winW) / 2 y := (screenH - winH) / 2 - title, _ := windows.UTF16PtrFromString("A301 - 게임 설치") + // 창 타이틀은 비워서 타이틀바를 최소화 + titleStr, _ := windows.UTF16PtrFromString("A301 런처") hwnd, _, _ := createWindowExWProc.Call( 0, uintptr(unsafe.Pointer(className)), - uintptr(unsafe.Pointer(title)), + uintptr(unsafe.Pointer(titleStr)), wsPopup|wsCaption|wsSysMenu|wsVisible, x, y, winW, winH, 0, 0, hInstance, 0, ) - font := createUIFont(9, dpi) - defer deleteObjectProc.Call(font) + titleFont := createUIFont(13, dpi, true) + defer deleteObjectProc.Call(titleFont) + statusFont := createUIFont(9, dpi, false) + defer deleteObjectProc.Call(statusFont) - // 상태 레이블 (텍스트) staticClass, _ := windows.UTF16PtrFromString("STATIC") + + // ── "A301" 타이틀 레이블 ── + // 클라이언트 영역 레이아웃 (base 96 DPI): + // y=14 h=28 → "A301" 타이틀 (13pt bold, 강조색) + // y=52 h=20 → 상태 텍스트 (9pt, 밝은 회색) + // y=82 h=18 → 진행 막대 + titleText, _ := windows.UTF16PtrFromString("A301") + titleLabelHwnd, _, _ = createWindowExWProc.Call( + 0, + uintptr(unsafe.Pointer(staticClass)), + uintptr(unsafe.Pointer(titleText)), + wsChild|wsVisible|ssCenter, + s(20), s(14), winW-s(40), s(28), + hwnd, 0, hInstance, 0, + ) + sendMessageWProc.Call(titleLabelHwnd, uintptr(wmSetFont), titleFont, 1) + + // ── 상태 레이블 ── initText, _ := windows.UTF16PtrFromString("게임 파일을 다운로드하는 중...") progressLabelHwnd, _, _ = createWindowExWProc.Call( 0, uintptr(unsafe.Pointer(staticClass)), uintptr(unsafe.Pointer(initText)), wsChild|wsVisible|ssCenter, - s(16), s(20), winW-s(32), s(22), + s(20), s(52), winW-s(40), s(20), hwnd, 0, hInstance, 0, ) - sendMessageWProc.Call(progressLabelHwnd, uintptr(wmSetFont), font, 1) + sendMessageWProc.Call(progressLabelHwnd, uintptr(wmSetFont), statusFont, 1) - // 진행 막대 + // ── 진행 막대 ── progressClass, _ := windows.UTF16PtrFromString("msctls_progress32") progressBarHwnd, _, _ = createWindowExWProc.Call( 0, uintptr(unsafe.Pointer(progressClass)), 0, wsChild|wsVisible|pbsSmooth, - s(16), s(52), winW-s(32), s(22), + s(20), s(82), winW-s(40), s(18), hwnd, 0, hInstance, 0, ) sendMessageWProc.Call(progressBarHwnd, uintptr(pbmSetRange32), 0, 100) + // 비주얼 스타일 비활성화 → PBM_SETBARCOLOR/PBM_SETBKCOLOR 적용 가능 + empty, _ := windows.UTF16PtrFromString("") + setWindowThemeProc.Call(progressBarHwnd, uintptr(unsafe.Pointer(empty)), uintptr(unsafe.Pointer(empty))) + sendMessageWProc.Call(progressBarHwnd, uintptr(pbmSetBarColor), 0, colorAccent) + sendMessageWProc.Call(progressBarHwnd, uintptr(pbmSetBkColor), 0, colorProgressBg) + showWindowProc.Call(hwnd, swShow) updateWindowProc.Call(hwnd)