feat: DPI 인식 및 진행 막대 UI 추가
- SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2)로 고DPI 디스플레이 지원 - GetDpiForSystem()으로 시스템 DPI 조회 후 모든 크기 동적 스케일링 - 다운로드 진행창에 msctls_progress32 진행 막대 추가 (0~100%) - Segoe UI 9pt 폰트를 DPI에 맞게 CreateFontIndirectW로 생성 - 4K(200% / 192 DPI)와 FHD(100% / 96 DPI) 모두 동일한 시각적 크기 보장 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
147
main.go
147
main.go
@@ -32,15 +32,21 @@ const (
|
|||||||
const (
|
const (
|
||||||
wmDestroy uint32 = 0x0002
|
wmDestroy uint32 = 0x0002
|
||||||
wmClose uint32 = 0x0010
|
wmClose uint32 = 0x0010
|
||||||
|
wmSetFont uint32 = 0x0030
|
||||||
wmSetText uint32 = 0x000C
|
wmSetText uint32 = 0x000C
|
||||||
wmAppDone uint32 = 0x8001
|
wmAppDone uint32 = 0x8001
|
||||||
|
|
||||||
wsPopup uintptr = 0x80000000
|
wsPopup uintptr = 0x80000000
|
||||||
wsCaption uintptr = 0x00C00000
|
wsCaption uintptr = 0x00C00000
|
||||||
|
wsSysMenu uintptr = 0x00080000
|
||||||
wsChild uintptr = 0x40000000
|
wsChild uintptr = 0x40000000
|
||||||
wsVisible uintptr = 0x10000000
|
wsVisible uintptr = 0x10000000
|
||||||
ssCenter uintptr = 0x00000001
|
ssCenter uintptr = 0x00000001
|
||||||
|
|
||||||
|
pbsSmooth uintptr = 0x01
|
||||||
|
pbmSetRange32 uint32 = 0x0406
|
||||||
|
pbmSetPos uint32 = 0x0402
|
||||||
|
|
||||||
swShow = 5
|
swShow = 5
|
||||||
smCxScreen = 0
|
smCxScreen = 0
|
||||||
smCyScreen = 1
|
smCyScreen = 1
|
||||||
@@ -51,12 +57,17 @@ const (
|
|||||||
mbYesNo uintptr = 0x00000004
|
mbYesNo uintptr = 0x00000004
|
||||||
mbQ uintptr = 0x00000020
|
mbQ uintptr = 0x00000020
|
||||||
idYes = 6
|
idYes = 6
|
||||||
|
|
||||||
|
iccProgressClass uint32 = 0x00000020
|
||||||
|
logpixelsX = 88
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
user32 = windows.NewLazySystemDLL("user32.dll")
|
user32 = windows.NewLazySystemDLL("user32.dll")
|
||||||
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||||
|
gdi32 = windows.NewLazySystemDLL("gdi32.dll")
|
||||||
shell32 = windows.NewLazySystemDLL("shell32.dll")
|
shell32 = windows.NewLazySystemDLL("shell32.dll")
|
||||||
|
comctl32 = windows.NewLazySystemDLL("comctl32.dll")
|
||||||
|
|
||||||
messageBoxWProc = user32.NewProc("MessageBoxW")
|
messageBoxWProc = user32.NewProc("MessageBoxW")
|
||||||
registerClassExWProc = user32.NewProc("RegisterClassExW")
|
registerClassExWProc = user32.NewProc("RegisterClassExW")
|
||||||
@@ -72,11 +83,17 @@ var (
|
|||||||
destroyWindowProc = user32.NewProc("DestroyWindow")
|
destroyWindowProc = user32.NewProc("DestroyWindow")
|
||||||
postQuitMsgProc = user32.NewProc("PostQuitMessage")
|
postQuitMsgProc = user32.NewProc("PostQuitMessage")
|
||||||
getSystemMetricsProc = user32.NewProc("GetSystemMetrics")
|
getSystemMetricsProc = user32.NewProc("GetSystemMetrics")
|
||||||
|
getDpiForSystemProc = user32.NewProc("GetDpiForSystem")
|
||||||
|
setProcessDpiAwarenessContextProc = user32.NewProc("SetProcessDpiAwarenessContext")
|
||||||
shellExecuteWProc = shell32.NewProc("ShellExecuteW")
|
shellExecuteWProc = shell32.NewProc("ShellExecuteW")
|
||||||
getModuleHandleWProc = kernel32.NewProc("GetModuleHandleW")
|
getModuleHandleWProc = kernel32.NewProc("GetModuleHandleW")
|
||||||
|
createFontIndirectWProc = gdi32.NewProc("CreateFontIndirectW")
|
||||||
|
deleteObjectProc = gdi32.NewProc("DeleteObject")
|
||||||
|
initCommonControlsExProc = comctl32.NewProc("InitCommonControlsEx")
|
||||||
|
|
||||||
wndProcCb uintptr
|
wndProcCb uintptr
|
||||||
progressLabelHwnd uintptr
|
progressLabelHwnd uintptr
|
||||||
|
progressBarHwnd uintptr
|
||||||
)
|
)
|
||||||
|
|
||||||
type wndClassExW struct {
|
type wndClassExW struct {
|
||||||
@@ -104,11 +121,78 @@ type msgW struct {
|
|||||||
ptY int32
|
ptY int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type initCommonControlsExS struct {
|
||||||
|
dwSize uint32
|
||||||
|
dwICC uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type logFontW struct {
|
||||||
|
lfHeight int32
|
||||||
|
lfWidth int32
|
||||||
|
lfEscapement int32
|
||||||
|
lfOrientation int32
|
||||||
|
lfWeight int32
|
||||||
|
lfItalic byte
|
||||||
|
lfUnderline byte
|
||||||
|
lfStrikeOut byte
|
||||||
|
lfCharSet byte
|
||||||
|
lfOutPrecision byte
|
||||||
|
lfClipPrecision byte
|
||||||
|
lfQuality byte
|
||||||
|
lfPitchAndFamily byte
|
||||||
|
lfFaceName [32]uint16
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
wndProcCb = syscall.NewCallback(progressWndProc)
|
wndProcCb = syscall.NewCallback(progressWndProc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Win32 helpers ──────────────────────────────────────────────────────────
|
// ── DPI helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func enableDPIAwareness() {
|
||||||
|
// DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4 (Windows 10 1703+)
|
||||||
|
setProcessDpiAwarenessContextProc.Call(^uintptr(3))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSystemDPI() uint32 {
|
||||||
|
dpi, _, _ := getDpiForSystemProc.Call()
|
||||||
|
if dpi == 0 {
|
||||||
|
return 96
|
||||||
|
}
|
||||||
|
return uint32(dpi)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dpiScale scales a base-96-DPI pixel value to the system DPI.
|
||||||
|
func dpiScale(px int, dpi uint32) uintptr {
|
||||||
|
return uintptr(px * int(dpi) / 96)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Font helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func createUIFont(pointSize int, dpi uint32) uintptr {
|
||||||
|
lf := logFontW{
|
||||||
|
lfHeight: -int32(pointSize) * int32(dpi) / 72,
|
||||||
|
lfWeight: 400, // FW_NORMAL
|
||||||
|
lfCharSet: 1, // DEFAULT_CHARSET
|
||||||
|
lfQuality: 5, // CLEARTYPE_QUALITY
|
||||||
|
}
|
||||||
|
face, _ := windows.UTF16FromString("Segoe UI")
|
||||||
|
copy(lf.lfFaceName[:], face)
|
||||||
|
font, _, _ := createFontIndirectWProc.Call(uintptr(unsafe.Pointer(&lf)))
|
||||||
|
return font
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Common controls ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func initCommonControls() {
|
||||||
|
icc := initCommonControlsExS{
|
||||||
|
dwSize: uint32(unsafe.Sizeof(initCommonControlsExS{})),
|
||||||
|
dwICC: iccProgressClass,
|
||||||
|
}
|
||||||
|
initCommonControlsExProc.Call(uintptr(unsafe.Pointer(&icc)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Win32 helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func msgBox(title, text string, flags uintptr) int {
|
func msgBox(title, text string, flags uintptr) int {
|
||||||
t, _ := windows.UTF16PtrFromString(title)
|
t, _ := windows.UTF16PtrFromString(title)
|
||||||
@@ -123,7 +207,7 @@ func openBrowser(rawURL string) {
|
|||||||
shellExecuteWProc.Call(0, uintptr(unsafe.Pointer(op)), uintptr(unsafe.Pointer(u)), 0, 0, 1)
|
shellExecuteWProc.Call(0, uintptr(unsafe.Pointer(op)), uintptr(unsafe.Pointer(u)), 0, 0, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Progress window ────────────────────────────────────────────────────────
|
// ── Progress window ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func progressWndProc(hwnd, uMsg, wParam, lParam uintptr) uintptr {
|
func progressWndProc(hwnd, uMsg, wParam, lParam uintptr) uintptr {
|
||||||
switch uint32(uMsg) {
|
switch uint32(uMsg) {
|
||||||
@@ -140,17 +224,26 @@ func progressWndProc(hwnd, uMsg, wParam, lParam uintptr) uintptr {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func setProgressText(text string) {
|
func setProgress(text string, pct int) {
|
||||||
|
if text != "" {
|
||||||
t, _ := windows.UTF16PtrFromString(text)
|
t, _ := windows.UTF16PtrFromString(text)
|
||||||
sendMessageWProc.Call(progressLabelHwnd, uintptr(wmSetText), 0, uintptr(unsafe.Pointer(t)))
|
sendMessageWProc.Call(progressLabelHwnd, uintptr(wmSetText), 0, uintptr(unsafe.Pointer(t)))
|
||||||
|
}
|
||||||
|
if pct >= 0 {
|
||||||
|
sendMessageWProc.Call(progressBarHwnd, uintptr(pbmSetPos), uintptr(pct), 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// downloadWithProgress shows a progress window and downloads+extracts the zip.
|
// downloadWithProgress shows a DPI-aware progress window and downloads+extracts the zip.
|
||||||
// Must be called from the main goroutine (Win32 message loop requirement).
|
// Must be called from the main goroutine (Win32 message loop requirement).
|
||||||
func downloadWithProgress(downloadURL, destDir string) error {
|
func downloadWithProgress(downloadURL, destDir string) error {
|
||||||
runtime.LockOSThread()
|
runtime.LockOSThread()
|
||||||
defer runtime.UnlockOSThread()
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
|
initCommonControls()
|
||||||
|
dpi := getSystemDPI()
|
||||||
|
s := func(px int) uintptr { return dpiScale(px, dpi) }
|
||||||
|
|
||||||
hInstance, _, _ := getModuleHandleWProc.Call(0)
|
hInstance, _, _ := getModuleHandleWProc.Call(0)
|
||||||
|
|
||||||
className, _ := windows.UTF16PtrFromString("A301Progress")
|
className, _ := windows.UTF16PtrFromString("A301Progress")
|
||||||
@@ -159,13 +252,14 @@ func downloadWithProgress(downloadURL, destDir string) error {
|
|||||||
lpfnWndProc: wndProcCb,
|
lpfnWndProc: wndProcCb,
|
||||||
hInstance: hInstance,
|
hInstance: hInstance,
|
||||||
lpszClassName: className,
|
lpszClassName: className,
|
||||||
hbrBackground: 16, // COLOR_BTNFACE + 1
|
hbrBackground: 16, // COLOR_BTNFACE+1
|
||||||
}
|
}
|
||||||
registerClassExWProc.Call(uintptr(unsafe.Pointer(&wc)))
|
registerClassExWProc.Call(uintptr(unsafe.Pointer(&wc)))
|
||||||
|
|
||||||
screenW, _, _ := getSystemMetricsProc.Call(smCxScreen)
|
screenW, _, _ := getSystemMetricsProc.Call(smCxScreen)
|
||||||
screenH, _, _ := getSystemMetricsProc.Call(smCyScreen)
|
screenH, _, _ := getSystemMetricsProc.Call(smCyScreen)
|
||||||
const winW, winH = 420, 110
|
winW := s(440)
|
||||||
|
winH := s(130)
|
||||||
x := (screenW - winW) / 2
|
x := (screenW - winW) / 2
|
||||||
y := (screenH - winH) / 2
|
y := (screenH - winH) / 2
|
||||||
|
|
||||||
@@ -174,11 +268,15 @@ func downloadWithProgress(downloadURL, destDir string) error {
|
|||||||
0,
|
0,
|
||||||
uintptr(unsafe.Pointer(className)),
|
uintptr(unsafe.Pointer(className)),
|
||||||
uintptr(unsafe.Pointer(title)),
|
uintptr(unsafe.Pointer(title)),
|
||||||
wsPopup|wsCaption|wsVisible,
|
wsPopup|wsCaption|wsSysMenu|wsVisible,
|
||||||
x, y, winW, winH,
|
x, y, winW, winH,
|
||||||
0, 0, hInstance, 0,
|
0, 0, hInstance, 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
font := createUIFont(9, dpi)
|
||||||
|
defer deleteObjectProc.Call(font)
|
||||||
|
|
||||||
|
// 상태 레이블 (텍스트)
|
||||||
staticClass, _ := windows.UTF16PtrFromString("STATIC")
|
staticClass, _ := windows.UTF16PtrFromString("STATIC")
|
||||||
initText, _ := windows.UTF16PtrFromString("게임 파일을 다운로드하는 중...")
|
initText, _ := windows.UTF16PtrFromString("게임 파일을 다운로드하는 중...")
|
||||||
progressLabelHwnd, _, _ = createWindowExWProc.Call(
|
progressLabelHwnd, _, _ = createWindowExWProc.Call(
|
||||||
@@ -186,9 +284,22 @@ func downloadWithProgress(downloadURL, destDir string) error {
|
|||||||
uintptr(unsafe.Pointer(staticClass)),
|
uintptr(unsafe.Pointer(staticClass)),
|
||||||
uintptr(unsafe.Pointer(initText)),
|
uintptr(unsafe.Pointer(initText)),
|
||||||
wsChild|wsVisible|ssCenter,
|
wsChild|wsVisible|ssCenter,
|
||||||
10, 35, 400, 30,
|
s(16), s(20), winW-s(32), s(22),
|
||||||
hwnd, 0, hInstance, 0,
|
hwnd, 0, hInstance, 0,
|
||||||
)
|
)
|
||||||
|
sendMessageWProc.Call(progressLabelHwnd, uintptr(wmSetFont), font, 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),
|
||||||
|
hwnd, 0, hInstance, 0,
|
||||||
|
)
|
||||||
|
sendMessageWProc.Call(progressBarHwnd, uintptr(pbmSetRange32), 0, 100)
|
||||||
|
|
||||||
showWindowProc.Call(hwnd, swShow)
|
showWindowProc.Call(hwnd, swShow)
|
||||||
updateWindowProc.Call(hwnd)
|
updateWindowProc.Call(hwnd)
|
||||||
@@ -239,8 +350,8 @@ func doDownload(downloadURL, destDir string) error {
|
|||||||
}
|
}
|
||||||
downloaded += int64(n)
|
downloaded += int64(n)
|
||||||
if total > 0 {
|
if total > 0 {
|
||||||
pct := downloaded * 100 / total
|
pct := int(downloaded * 100 / total)
|
||||||
setProgressText(fmt.Sprintf("다운로드 중... %d%%", pct))
|
setProgress(fmt.Sprintf("다운로드 중... %d%%", pct), pct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
@@ -255,7 +366,7 @@ func doDownload(downloadURL, destDir string) error {
|
|||||||
tmpFile.Close()
|
tmpFile.Close()
|
||||||
defer os.Remove(tmpPath)
|
defer os.Remove(tmpPath)
|
||||||
|
|
||||||
setProgressText("압축을 해제하는 중...")
|
setProgress("압축을 해제하는 중...", -1)
|
||||||
return extractZip(tmpPath, destDir)
|
return extractZip(tmpPath, destDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +428,7 @@ func extractZip(zipPath, destDir string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Server info ────────────────────────────────────────────────────────────
|
// ── Server info ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type downloadInfo struct {
|
type downloadInfo struct {
|
||||||
FileHash string `json:"fileHash"`
|
FileHash string `json:"fileHash"`
|
||||||
@@ -358,7 +469,7 @@ func hashFile(path string) (string, error) {
|
|||||||
return hex.EncodeToString(h.Sum(nil)), nil
|
return hex.EncodeToString(h.Sum(nil)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Launcher path ──────────────────────────────────────────────────────────
|
// ── Launcher path ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func launcherPath() (string, error) {
|
func launcherPath() (string, error) {
|
||||||
exe, err := os.Executable()
|
exe, err := os.Executable()
|
||||||
@@ -368,7 +479,7 @@ func launcherPath() (string, error) {
|
|||||||
return filepath.Abs(exe)
|
return filepath.Abs(exe)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Protocol install / uninstall ───────────────────────────────────────────
|
// ── Protocol install / uninstall ─────────────────────────────────────────────
|
||||||
|
|
||||||
func install() error {
|
func install() error {
|
||||||
exePath, err := launcherPath()
|
exePath, err := launcherPath()
|
||||||
@@ -407,7 +518,7 @@ func uninstall() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Game update check + download ───────────────────────────────────────────
|
// ── Game update check + download ─────────────────────────────────────────────
|
||||||
|
|
||||||
func ensureGame(gameDir, gamePath string, serverInfo *downloadInfo) error {
|
func ensureGame(gameDir, gamePath string, serverInfo *downloadInfo) error {
|
||||||
needsDownload := false
|
needsDownload := false
|
||||||
@@ -436,7 +547,7 @@ func ensureGame(gameDir, gamePath string, serverInfo *downloadInfo) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── URI handler ────────────────────────────────────────────────────────────
|
// ── URI handler ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func handleURI(rawURI string) error {
|
func handleURI(rawURI string) error {
|
||||||
parsed, err := url.Parse(rawURI)
|
parsed, err := url.Parse(rawURI)
|
||||||
@@ -473,9 +584,11 @@ func handleURI(rawURI string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Entry point ────────────────────────────────────────────────────────────
|
// ── Entry point ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
enableDPIAwareness()
|
||||||
|
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
ret := msgBox("A301 런처", "게임 실행을 위해 프로토콜을 등록합니다.\n계속하시겠습니까?", mbYesNo|mbQ)
|
ret := msgBox("A301 런처", "게임 실행을 위해 프로토콜을 등록합니다.\n계속하시겠습니까?", mbYesNo|mbQ)
|
||||||
if ret != idYes {
|
if ret != idYes {
|
||||||
|
|||||||
Reference in New Issue
Block a user