292 lines
8.6 KiB
Go
292 lines
8.6 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"runtime"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
// ── 윈도우 핸들 (진행 창에서 사용) ──────────────────────────
|
|
|
|
var (
|
|
wndProcCb uintptr
|
|
titleLabelHwnd uintptr
|
|
progressLabelHwnd uintptr
|
|
progressBarHwnd uintptr
|
|
hBrushBg uintptr
|
|
)
|
|
|
|
func init() {
|
|
wndProcCb = syscall.NewCallback(progressWndProc)
|
|
}
|
|
|
|
// ── DPI ──────────────────────────────────────────────────────
|
|
|
|
// enableDPIAwareness Per-Monitor V2 DPI 인식을 선언한다.
|
|
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 96 DPI 기준 픽셀 값을 현재 DPI에 맞게 변환한다.
|
|
func dpiScale(px int, dpi uint32) uintptr {
|
|
return uintptr(int(float64(px)*float64(dpi)/96.0 + 0.5))
|
|
}
|
|
|
|
// ── 폰트 ─────────────────────────────────────────────────────
|
|
|
|
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: weight,
|
|
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
|
|
}
|
|
|
|
// ── 메시지 박스 ──────────────────────────────────────────────
|
|
|
|
// exitWithError 오류 메시지를 표시하고 프로그램을 종료한다.
|
|
func exitWithError(msg string) {
|
|
msgBox("One of the plans 런처 - 오류", msg, mbOK|mbError)
|
|
os.Exit(1)
|
|
}
|
|
|
|
func msgBox(title, text string, flags uintptr) int {
|
|
t, err := windows.UTF16PtrFromString(title)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
m, err := windows.UTF16PtrFromString(text)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
ret, _, _ := messageBoxWProc.Call(0, uintptr(unsafe.Pointer(m)), uintptr(unsafe.Pointer(t)), flags)
|
|
return int(ret)
|
|
}
|
|
|
|
// ── 단일 인스턴스 ────────────────────────────────────────────
|
|
|
|
func acquireSingleInstance() bool {
|
|
name, _ := windows.UTF16PtrFromString("Global\\A301LauncherMutex")
|
|
_, _, err := createMutexWProc.Call(0, 0, uintptr(unsafe.Pointer(name)))
|
|
// ERROR_ALREADY_EXISTS = 183
|
|
if errno, ok := err.(syscall.Errno); ok && errno == 183 {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func activateExistingWindow() {
|
|
className, _ := windows.UTF16PtrFromString("A301Progress")
|
|
hwnd, _, _ := findWindowWProc.Call(uintptr(unsafe.Pointer(className)), 0)
|
|
if hwnd != 0 {
|
|
setForegroundWindowProc.Call(hwnd)
|
|
}
|
|
}
|
|
|
|
// ── 진행 창 (프로그레스 윈도우) ──────────────────────────────
|
|
|
|
func initCommonControls() {
|
|
icc := initCommonControlsExS{
|
|
dwSize: uint32(unsafe.Sizeof(initCommonControlsExS{})),
|
|
dwICC: iccProgressClass,
|
|
}
|
|
initCommonControlsExProc.Call(uintptr(unsafe.Pointer(&icc)))
|
|
}
|
|
|
|
// progressWndProc 진행 창의 메시지 처리 프로시저.
|
|
func progressWndProc(hwnd, uMsg, wParam, lParam uintptr) uintptr {
|
|
switch uint32(uMsg) {
|
|
case wmClose:
|
|
ret := msgBox("One of the plans 런처", "다운로드를 취소하시겠습니까?", mbYesNo|mbQ)
|
|
if ret == idYes {
|
|
downloadCancelled.Store(true)
|
|
destroyWindowProc.Call(hwnd)
|
|
}
|
|
return 0
|
|
case wmDestroy:
|
|
postQuitMsgProc.Call(0)
|
|
return 0
|
|
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
|
|
}
|
|
|
|
// setProgress 진행 창의 텍스트와 퍼센트를 갱신한다.
|
|
func setProgress(text string, pct int) {
|
|
if text != "" {
|
|
t, err := windows.UTF16PtrFromString(text)
|
|
if err == nil {
|
|
sendMessageWProc.Call(progressLabelHwnd, uintptr(wmSetText), 0, uintptr(unsafe.Pointer(t)))
|
|
}
|
|
}
|
|
if pct >= 0 {
|
|
sendMessageWProc.Call(progressBarHwnd, uintptr(pbmSetPos), uintptr(pct), 0)
|
|
}
|
|
}
|
|
|
|
// createProgressWindow 다크 테마 진행 창을 생성하고 핸들을 반환한다.
|
|
func createProgressWindow(dpi uint32) (hwnd uintptr, err error) {
|
|
s := func(px int) uintptr { return dpiScale(px, dpi) }
|
|
|
|
hBrushBg, _, _ = createSolidBrushProc.Call(colorBg)
|
|
hInstance, _, _ := getModuleHandleWProc.Call(0)
|
|
|
|
className, _ := windows.UTF16PtrFromString("A301Progress")
|
|
wc := wndClassExW{
|
|
cbSize: uint32(unsafe.Sizeof(wndClassExW{})),
|
|
lpfnWndProc: wndProcCb,
|
|
hInstance: hInstance,
|
|
lpszClassName: className,
|
|
hbrBackground: hBrushBg,
|
|
}
|
|
atom, _, _ := registerClassExWProc.Call(uintptr(unsafe.Pointer(&wc)))
|
|
if atom == 0 {
|
|
return 0, fmt.Errorf("윈도우 클래스 등록 실패")
|
|
}
|
|
|
|
// 화면 중앙에 배치
|
|
screenW, _, _ := getSystemMetricsProc.Call(smCxScreen)
|
|
screenH, _, _ := getSystemMetricsProc.Call(smCyScreen)
|
|
winW := s(440)
|
|
winH := s(152)
|
|
x := (screenW - winW) / 2
|
|
y := (screenH - winH) / 2
|
|
|
|
titleStr, _ := windows.UTF16PtrFromString("One of the plans 런처")
|
|
hwnd, _, _ = createWindowExWProc.Call(
|
|
0,
|
|
uintptr(unsafe.Pointer(className)),
|
|
uintptr(unsafe.Pointer(titleStr)),
|
|
wsPopup|wsCaption|wsSysMenu|wsVisible,
|
|
x, y, winW, winH,
|
|
0, 0, hInstance, 0,
|
|
)
|
|
if hwnd == 0 {
|
|
return 0, fmt.Errorf("다운로드 창 생성 실패")
|
|
}
|
|
|
|
// 폰트
|
|
titleFont := createUIFont(13, dpi, true)
|
|
statusFont := createUIFont(9, dpi, false)
|
|
|
|
// 타이틀 라벨
|
|
staticClass, _ := windows.UTF16PtrFromString("STATIC")
|
|
titleText, _ := windows.UTF16PtrFromString("One of the plans")
|
|
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(20), s(52), winW-s(40), s(20),
|
|
hwnd, 0, hInstance, 0,
|
|
)
|
|
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(20), s(82), winW-s(40), s(18),
|
|
hwnd, 0, hInstance, 0,
|
|
)
|
|
sendMessageWProc.Call(progressBarHwnd, uintptr(pbmSetRange32), 0, 100)
|
|
|
|
// 비주얼 스타일 비활성화 후 커스텀 색상 적용
|
|
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)
|
|
|
|
return hwnd, nil
|
|
}
|
|
|
|
// downloadWithProgress 진행 창을 표시하면서 파일을 다운로드하고 추출한다.
|
|
// Win32 메시지 루프를 사용하므로 반드시 메인 고루틴에서 호출해야 한다.
|
|
func downloadWithProgress(downloadURL, destDir string) error {
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
|
|
initCommonControls()
|
|
dpi := getSystemDPI()
|
|
|
|
hwnd, err := createProgressWindow(dpi)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer deleteObjectProc.Call(hBrushBg)
|
|
|
|
downloadCancelled.Store(false)
|
|
|
|
// 별도 고루틴에서 다운로드, 완료 시 wmAppDone 메시지로 창 닫기
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
errCh <- doDownload(downloadURL, destDir)
|
|
postMessageWProc.Call(hwnd, uintptr(wmAppDone), 0, 0)
|
|
}()
|
|
|
|
// Win32 메시지 루프
|
|
var m msgW
|
|
for {
|
|
ret, _, _ := getMessageWProc.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
|
|
if ret == 0 || ret == ^uintptr(0) {
|
|
break
|
|
}
|
|
translateMsgProc.Call(uintptr(unsafe.Pointer(&m)))
|
|
dispatchMsgWProc.Call(uintptr(unsafe.Pointer(&m)))
|
|
}
|
|
|
|
return <-errCh
|
|
}
|