//go:build windows package main import ( "fmt" "io" "runtime" "strconv" "strings" "sync" "syscall" "unsafe" ) const ( windowTitle = "D-InSAR 数据包恢复工具" wmDestroy = 0x0002 wmCommand = 0x0111 wmClose = 0x0010 wmSetFont = 0x0030 bnClicked = 0 wsOverlappedWindow = 0x00CF0000 wsVisible = 0x10000000 wsChild = 0x40000000 wsTabStop = 0x00010000 wsBorder = 0x00800000 wsVScroll = 0x00200000 wsHScroll = 0x00100000 esLeft = 0x0000 esMultiline = 0x0004 esAutoVScroll = 0x0040 esAutoHScroll = 0x0080 esReadOnly = 0x0800 bsPushButton = 0x00000000 bsAutoCheckBox = 0x00000003 swShow = 5 cwUseDefault = 0x80000000 gwlUserData = -21 idInputEdit = 1001 idInputBrowse = 1002 idOutputEdit = 1003 idOutputBrowse = 1004 idDryRun = 1005 idOverwrite = 1006 idLimitEdit = 1007 idStart = 1008 idLogEdit = 1009 idCheck = 1010 bmGetCheck = 0x00F0 bstChecked = 1 maxPathBuffer = 4096 ) var ( user32 = syscall.NewLazyDLL("user32.dll") kernel32 = syscall.NewLazyDLL("kernel32.dll") gdi32 = syscall.NewLazyDLL("gdi32.dll") shell32 = syscall.NewLazyDLL("shell32.dll") ole32 = syscall.NewLazyDLL("ole32.dll") procRegisterClassExW = user32.NewProc("RegisterClassExW") procCreateWindowExW = user32.NewProc("CreateWindowExW") procDefWindowProcW = user32.NewProc("DefWindowProcW") procDestroyWindow = user32.NewProc("DestroyWindow") procDispatchMessageW = user32.NewProc("DispatchMessageW") procGetMessageW = user32.NewProc("GetMessageW") procPostQuitMessage = user32.NewProc("PostQuitMessage") procSendMessageW = user32.NewProc("SendMessageW") procShowWindow = user32.NewProc("ShowWindow") procUpdateWindow = user32.NewProc("UpdateWindow") procSetWindowTextW = user32.NewProc("SetWindowTextW") procGetWindowTextW = user32.NewProc("GetWindowTextW") procGetWindowTextLenW = user32.NewProc("GetWindowTextLengthW") procEnableWindow = user32.NewProc("EnableWindow") procSetWindowLongPtrW = user32.NewProc("SetWindowLongPtrW") procGetWindowLongPtrW = user32.NewProc("GetWindowLongPtrW") procMessageBoxW = user32.NewProc("MessageBoxW") procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW") procCreateFontW = gdi32.NewProc("CreateFontW") procSHBrowseForFolderW = shell32.NewProc("SHBrowseForFolderW") procSHGetPathFromIDListW = shell32.NewProc("SHGetPathFromIDListW") procCoTaskMemFree = ole32.NewProc("CoTaskMemFree") procOleInitialize = ole32.NewProc("OleInitialize") procOleUninitialize = ole32.NewProc("OleUninitialize") ) type point struct { x int32 y int32 } type msg struct { hwnd uintptr message uint32 wParam uintptr lParam uintptr time uint32 pt point } type wndClassEx struct { cbSize uint32 style uint32 lpfnWndProc uintptr cbClsExtra int32 cbWndExtra int32 hInstance uintptr hIcon uintptr hCursor uintptr hbrBackground uintptr lpszMenuName *uint16 lpszClassName *uint16 hIconSm uintptr } type browseInfo struct { hwndOwner uintptr pidlRoot uintptr pszDisplayName *uint16 lpszTitle *uint16 ulFlags uint32 lpfn uintptr lParam uintptr iImage int32 } type guiApp struct { hwnd uintptr font uintptr inputEdit uintptr outputEdit uintptr dryRun uintptr overwrite uintptr limitEdit uintptr startButton uintptr checkButton uintptr logEdit uintptr mu sync.Mutex logText string running bool } func runGUI() error { runtime.LockOSThread() defer runtime.UnlockOSThread() procOleInitialize.Call(0) defer procOleUninitialize.Call() instance, _, _ := procGetModuleHandleW.Call(0) className, _ := syscall.UTF16PtrFromString("DinsarRestoreWindow") title, _ := syscall.UTF16PtrFromString(windowTitle) wc := wndClassEx{ cbSize: uint32(unsafe.Sizeof(wndClassEx{})), lpfnWndProc: syscall.NewCallback(windowProc), hInstance: instance, hbrBackground: 5 + 1, lpszClassName: className, } if atom, _, err := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc))); atom == 0 { return fmt.Errorf("RegisterClassExW failed: %v", err) } app := &guiApp{} hwnd, _, err := procCreateWindowExW.Call( 0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(title)), wsOverlappedWindow|wsVisible, cwUseDefault, cwUseDefault, 1040, 720, 0, 0, instance, uintptr(unsafe.Pointer(app)), ) if hwnd == 0 { return fmt.Errorf("CreateWindowExW failed: %v", err) } app.hwnd = hwnd procShowWindow.Call(hwnd, swShow) procUpdateWindow.Call(hwnd) var m msg for { ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0) if int32(ret) <= 0 { break } procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m))) } return nil } func windowProc(hwnd uintptr, msg uint32, wParam uintptr, lParam uintptr) uintptr { app := appFromWindow(hwnd) switch msg { case wmClose: if app != nil && app.isRunning() { messageBox(hwnd, "任务正在运行,请等待结束。", windowTitle) return 0 } procDestroyWindow.Call(hwnd) return 0 case wmDestroy: procPostQuitMessage.Call(0) return 0 case wmCommand: id := int(wParam & 0xffff) notify := int((wParam >> 16) & 0xffff) if notify == bnClicked && app != nil { switch id { case idInputBrowse: if selected := chooseDirectory(hwnd, "选择 BundleRoot 输入目录"); selected != "" { setWindowText(app.inputEdit, selected) } case idOutputBrowse: if selected := chooseDirectory(hwnd, "选择 OutputRoot 输出目录"); selected != "" { setWindowText(app.outputEdit, selected) } case idStart: app.startRestore() case idCheck: app.startCheck() } } return 0 case 0x0081: // WM_NCCREATE cs := (*createStruct)(unsafe.Pointer(lParam)) procSetWindowLongPtrW.Call(hwnd, winIndex(gwlUserData), cs.createParams) return 1 case 0x0001: // WM_CREATE app = appFromWindow(hwnd) if app != nil { app.hwnd = hwnd app.createControls() } return 0 } ret, _, _ := procDefWindowProcW.Call(hwnd, uintptr(msg), wParam, lParam) return ret } type createStruct struct { createParams uintptr instance uintptr menu uintptr parent uintptr cy int32 cx int32 y int32 x int32 style int32 name *uint16 class *uint16 exStyle uint32 } func appFromWindow(hwnd uintptr) *guiApp { if hwnd == 0 { return nil } ptr, _, _ := procGetWindowLongPtrW.Call(hwnd, winIndex(gwlUserData)) if ptr == 0 { return nil } return (*guiApp)(unsafe.Pointer(ptr)) } func (a *guiApp) createControls() { a.font = createFont("Microsoft YaHei UI", 20) createLabel(a.hwnd, "输入目录 BundleRoot", 24, 28, 210, 30, a.font) a.inputEdit = createEdit(a.hwnd, "", idInputEdit, 240, 24, 640, 34, false, a.font) createButton(a.hwnd, "选择...", idInputBrowse, 895, 24, 100, 34, a.font) createLabel(a.hwnd, "输出目录 OutputRoot", 24, 78, 210, 30, a.font) a.outputEdit = createEdit(a.hwnd, "", idOutputEdit, 240, 74, 640, 34, false, a.font) createButton(a.hwnd, "选择...", idOutputBrowse, 895, 74, 100, 34, a.font) a.dryRun = createCheckBox(a.hwnd, "Dry run 只打印计划", idDryRun, 240, 128, 230, 32, a.font) a.overwrite = createCheckBox(a.hwnd, "覆盖已有 Task", idOverwrite, 500, 128, 190, 32, a.font) createLabel(a.hwnd, "Limit", 720, 130, 60, 30, a.font) a.limitEdit = createEdit(a.hwnd, "", idLimitEdit, 780, 126, 100, 34, false, a.font) a.checkButton = createButton(a.hwnd, "数据自检", idCheck, 760, 174, 105, 38, a.font) a.startButton = createButton(a.hwnd, "开始恢复", idStart, 890, 174, 105, 38, a.font) createLabel(a.hwnd, "运行日志", 24, 226, 140, 30, a.font) a.logEdit = createEdit(a.hwnd, "", idLogEdit, 24, 262, 970, 365, true, a.font) a.appendLog("请选择输入目录后可先数据自检;选择输出目录后可开始恢复。\r\n") } func (a *guiApp) startRestore() { if a.isRunning() { return } input := strings.TrimSpace(getWindowText(a.inputEdit)) output := strings.TrimSpace(getWindowText(a.outputEdit)) if input == "" { messageBox(a.hwnd, "请选择输入目录 BundleRoot。", windowTitle) return } if output == "" { messageBox(a.hwnd, "请选择输出目录 OutputRoot。", windowTitle) return } limit, ok := a.parseLimit() if !ok { return } cfg := config{ inputRoot: input, outputRoot: output, skipExisting: true, overwrite: isChecked(a.overwrite), limit: limit, dryRun: isChecked(a.dryRun), logWriter: guiLogWriter{app: a}, } if cfg.overwrite { cfg.skipExisting = false } a.setRunning(true) a.setLog("") a.appendLog("开始执行...\r\n") go func() { result, err := run(cfg) if err != nil { a.appendLog("错误: " + err.Error() + "\r\n") messageBox(a.hwnd, "执行失败:\r\n"+err.Error(), windowTitle) } else { a.appendLog(fmt.Sprintf("完成: restored=%d skipped=%d failed=%d warnings=%d\r\n", result.report.Restored, result.report.Skipped, result.report.Failed, len(result.report.Warnings), )) messageBox(a.hwnd, "执行完成。", windowTitle) } a.setRunning(false) }() } func (a *guiApp) startCheck() { if a.isRunning() { return } input := strings.TrimSpace(getWindowText(a.inputEdit)) if input == "" { messageBox(a.hwnd, "请选择输入目录 BundleRoot。", windowTitle) return } limit, ok := a.parseLimit() if !ok { return } cfg := config{ inputRoot: input, limit: limit, checkOnly: true, logWriter: guiLogWriter{app: a}, } a.setRunning(true) a.setLog("") a.appendLog("开始数据自检...\r\n") go func() { result, err := runCheck(cfg) if err != nil { a.appendLog("错误: " + err.Error() + "\r\n") messageBox(a.hwnd, "自检失败:\r\n"+err.Error(), windowTitle) } else if result.report.Failed > 0 { a.appendLog(fmt.Sprintf("自检完成: total=%d checked=%d valid=%d failed=%d warnings=%d\r\n", result.report.TotalPairs, result.report.Checked, result.report.Valid, result.report.Failed, len(result.report.Warnings), )) messageBox(a.hwnd, fmt.Sprintf("自检完成,但发现 %d 个失败项。请查看日志。", result.report.Failed), windowTitle) } else { a.appendLog(fmt.Sprintf("自检通过: total=%d checked=%d valid=%d warnings=%d\r\n", result.report.TotalPairs, result.report.Checked, result.report.Valid, len(result.report.Warnings), )) messageBox(a.hwnd, "自检通过。", windowTitle) } a.setRunning(false) }() } func (a *guiApp) parseLimit() (int, bool) { limitText := strings.TrimSpace(getWindowText(a.limitEdit)) if limitText == "" { return 0, true } parsed, err := strconv.Atoi(limitText) if err != nil || parsed < 0 { messageBox(a.hwnd, "Limit 必须是大于等于 0 的整数。", windowTitle) return 0, false } return parsed, true } func (a *guiApp) isRunning() bool { a.mu.Lock() defer a.mu.Unlock() return a.running } func (a *guiApp) setRunning(running bool) { a.mu.Lock() a.running = running a.mu.Unlock() enable := uintptr(1) if running { enable = 0 } procEnableWindow.Call(a.startButton, enable) procEnableWindow.Call(a.checkButton, enable) procEnableWindow.Call(a.inputEdit, enable) procEnableWindow.Call(a.outputEdit, enable) procEnableWindow.Call(a.dryRun, enable) procEnableWindow.Call(a.overwrite, enable) procEnableWindow.Call(a.limitEdit, enable) } func (a *guiApp) setLog(text string) { a.mu.Lock() a.logText = text a.mu.Unlock() setWindowText(a.logEdit, text) } func (a *guiApp) appendLog(text string) { a.mu.Lock() a.logText += text fullText := a.logText a.mu.Unlock() setWindowText(a.logEdit, fullText) } type guiLogWriter struct { app *guiApp } func (w guiLogWriter) Write(p []byte) (int, error) { if w.app != nil { w.app.appendLog(strings.ReplaceAll(string(p), "\n", "\r\n")) } return len(p), nil } func createLabel(parent uintptr, text string, x, y, width, height int32, font uintptr) uintptr { return createControl("STATIC", text, wsChild|wsVisible, 0, parent, 0, x, y, width, height, font) } func createEdit(parent uintptr, text string, id int, x, y, width, height int32, multiline bool, font uintptr) uintptr { style := uint32(wsChild | wsVisible | wsBorder | wsTabStop | esLeft | esAutoHScroll) if multiline { style = wsChild | wsVisible | wsBorder | wsVScroll | wsHScroll | esLeft | esMultiline | esAutoVScroll | esAutoHScroll | esReadOnly } return createControl("EDIT", text, style, 0, parent, id, x, y, width, height, font) } func createButton(parent uintptr, text string, id int, x, y, width, height int32, font uintptr) uintptr { return createControl("BUTTON", text, wsChild|wsVisible|wsTabStop|bsPushButton, 0, parent, id, x, y, width, height, font) } func createCheckBox(parent uintptr, text string, id int, x, y, width, height int32, font uintptr) uintptr { return createControl("BUTTON", text, wsChild|wsVisible|wsTabStop|bsAutoCheckBox, 0, parent, id, x, y, width, height, font) } func createControl(className string, text string, style uint32, exStyle uint32, parent uintptr, id int, x, y, width, height int32, font uintptr) uintptr { classPtr, _ := syscall.UTF16PtrFromString(className) textPtr, _ := syscall.UTF16PtrFromString(text) hwnd, _, _ := procCreateWindowExW.Call( uintptr(exStyle), uintptr(unsafe.Pointer(classPtr)), uintptr(unsafe.Pointer(textPtr)), uintptr(style), uintptr(x), uintptr(y), uintptr(width), uintptr(height), parent, uintptr(id), 0, 0, ) if hwnd != 0 && font != 0 { procSendMessageW.Call(hwnd, wmSetFont, font, 1) } return hwnd } func createFont(name string, size int32) uintptr { namePtr, _ := syscall.UTF16PtrFromString(name) font, _, _ := procCreateFontW.Call( uintptr(-size), 0, 0, 0, 400, 0, 0, 0, 1, 0, 0, 0, 0, uintptr(unsafe.Pointer(namePtr)), ) return font } func setWindowText(hwnd uintptr, text string) { ptr, _ := syscall.UTF16PtrFromString(text) procSetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(ptr))) } func getWindowText(hwnd uintptr) string { length, _, _ := procGetWindowTextLenW.Call(hwnd) buffer := make([]uint16, int(length)+1) procGetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(&buffer[0])), uintptr(len(buffer))) return syscall.UTF16ToString(buffer) } func isChecked(hwnd uintptr) bool { ret, _, _ := procSendMessageW.Call(hwnd, bmGetCheck, 0, 0) return ret == bstChecked } func messageBox(hwnd uintptr, text string, title string) { textPtr, _ := syscall.UTF16PtrFromString(text) titlePtr, _ := syscall.UTF16PtrFromString(title) procMessageBoxW.Call(hwnd, uintptr(unsafe.Pointer(textPtr)), uintptr(unsafe.Pointer(titlePtr)), 0) } func chooseDirectory(hwnd uintptr, title string) string { titlePtr, _ := syscall.UTF16PtrFromString(title) displayName := make([]uint16, maxPathBuffer) bi := browseInfo{ hwndOwner: hwnd, pszDisplayName: &displayName[0], lpszTitle: titlePtr, ulFlags: 0x00000001 | 0x00000010 | 0x00000040, } pidl, _, _ := procSHBrowseForFolderW.Call(uintptr(unsafe.Pointer(&bi))) if pidl == 0 { return "" } defer procCoTaskMemFree.Call(pidl) pathBuffer := make([]uint16, maxPathBuffer) ret, _, _ := procSHGetPathFromIDListW.Call(pidl, uintptr(unsafe.Pointer(&pathBuffer[0]))) if ret == 0 { return "" } return syscall.UTF16ToString(pathBuffer) } func winIndex(value int32) uintptr { return uintptr(value) } var _ io.Writer = guiLogWriter{}