从“双击打不开“到“管理员都服了“:用 Go 打造你的专属 .mgx 编辑器

55 阅读13分钟

当你的程序不仅能写代码,还能自己提权、注册文件类型、顺便把黑窗口藏起来——它已经比你更懂 Windows。

🧑‍💻 场景还原:一个程序员的崩溃日常

你辛辛苦苦用 Go 写了个超酷的 .mgx 文件编辑器(别问,问就是"Magic eXchange Format"),结果:

  • 双击 .mgx 文件?系统:"你想用哪个程序打开它?" 😑
  • 程序运行起来?背后还跟着个黑乎乎的命令行窗口,像个小尾巴甩不掉 🐾
  • 想自动关联文件类型?注册表写不进去,提示"权限不足"……你连自己的电脑都管不了?

别慌!今天我们就用 Go + Walk 把这些问题一锅端了。目标很明确:

✅ 双击 .mgx → 自动用我的程序打开

✅ 首次运行 → 自动注册文件关联(不够权限?那就提权!)

✅ 编译出来 → 干干净净,没有黑窗口,像个正经 Windows 软件

Ready?Let's Go!

🔧 第一步:让程序"有脑子"——检测并申请管理员权限

Windows 对注册表 HKEY_CLASSES_ROOT 的写入权限卡得很死。普通用户?门都没有。怎么办?

策略:先检测,再"装可怜"求提权。

if !isFileAssociationExists() && !isAdmin() {
    if walk.MsgBox(nil, "小请求", "我想把 .mgx 文件和我绑在一起...\n但需要管理员权限😭\n同意我以管理员身份重启吗?", 
        walk.MsgBoxYesNo|walk.MsgBoxIconQuestion) == walk.DlgCmdYes {
        restartAsAdmin() // 偷偷调用 ShellExecute("runas")
        os.Exit(0)
    }
}

这段代码翻译成人话就是:

"老板,我想干点大事,但现在权限不够。要不……您点个'是',让我穿上管理员马甲重来一次?"

而 restartAsAdmin() 的本质,就是悄悄调用 Windows 的 ShellExecuteW,动词设为 "runas" —— 这是 Windows 官方认证的"提权姿势"。

🗂️ 第二步:注册文件关联——给 .mgx 找个"家"

一旦有了管理员权限,我们就可以光明正大地写注册表了:

  • 创建类型 MGXFileEditor.MGX
  • 设置描述:"MGX 文件(由魔法驱动✨)"
  • 图标指向自己:"C:\myapp.exe,0"
  • 打开命令:"C:\myapp.exe" "%1"(%1 就是双击的文件路径)
  • 把 .mgx 后缀指向这个类型

关键代码节选:

// 关联扩展名
extensionKey, _, _ := registry.CreateKey(registry.CLASSES_ROOT, ".mgx", registry.WRITE)
extensionKey.SetStringValue("", "MGXFileEditor.MGX")

// 设置打开命令
commandKey, _, _ := registry.CreateKey(openKey, "command", registry.WRITE)
commandKey.SetStringValue("", fmt.Sprintf("\"%s\" \"%%1\"", exePath))

💡 注意:%%1 在 Go 字符串里要写成 "%%1",否则会被当成格式化参数吃掉!

搞定之后,Windows 就会记住:"哦,.mgx 是我家孩子,归那个叫 MGXFileEditor 的管。"

🖼️ 第三步:界面不能丑——用 Walk 搞个 GUI

Walk 是 Go 社区里少有的、能让你写出"看起来像 Windows 原生应用"的 GUI 库。声明式语法,清爽如诗:

MainWindow{
	Title: "MGX 文件编辑器(不是记事本!)",
	MinSize: Size{800, 600},
	MenuItems: []MenuItem{
		Menu{
			Text: "文件",
			Items: []MenuItem{
				Action{Text: "新建", OnTriggered: newFile},
				Action{Text: "打开", OnTriggered: openFile},
				Action{Text: "保存", OnTriggered: saveFile},
			},
		},
	},
	Children: []Widget{
		Composite{Layout: HBox{}, Children: []Widget{
			Label{Text: "标题:"},
			LineEdit{AssignTo: &titleEdit, StretchFactor: 1},
		}},
		TextEdit{AssignTo: &contentEdit, StretchFactor: 1},
	},
}.Create()

效果?简洁、现代、毫无"外包感"。用户甚至看不出这是 Go 写的!

而且,当用户双击 .mgx 文件时,路径会作为 os.Args[1] 传进来,我们直接加载即可:

if len(os.Args) > 1 && strings.HasSuffix(os.Args[1], ".mgx") {
	loadFile(os.Args[1], ...)
}

完美闭环!

🚫 第四步:消灭黑窗口——让它像个"正经软件"

默认 go build 出来的程序,是个"控制台应用",所以总会带个黑窗口。这在 GUI 程序里简直灾难。

解决方案两步走:

1️⃣ 加一个 main.manifest

告诉 Windows:"我是 GUI 程序,别给我开控制台!"

<!-- main.manifest -->
<assembly ...>
  <trustInfo>
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel level="asInvoker" uiAccess="false"/>
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>

📌 asInvoker 表示"按当前权限运行",不强制提权——因为我们已经实现了按需提权!

2️⃣ 用 rsrc 把 manifest 和图标打包进 exe

# 安装 rsrc(只需一次)
go install github.com/akavel/rsrc@latest

# 生成资源文件(放在 main.go 同目录!)
rsrc -manifest main.manifest -ico app.ico -o rsrc.syso

# 编译时指定 GUI 子系统
go build -ldflags="-H windowsgui" -o mgx-editor.exe

现在,双击 mgx-editor.exe ——

✨ 无黑窗!

✨ 有图标!

✨ 像极了 Visual Studio 编出来的正经软件!

🛠️ 完整使用指南(手把手版)

1. 准备工作

mkdir mgx-editor && cd mgx-editor
go mod init mgx-editor
go get github.com/lxn/walk
go get golang.org/x/sys/windows

2. 放入你的 main.go(就是你写的那段完整代码)

3. 创建 main.manifest(内容见上文)

4. 准备一个 app.ico 图标(可选,但强烈建议)

5. 生成资源文件

rsrc -manifest main.manifest -ico app.ico -o rsrc.syso

6. 编译!

go build -ldflags="-H windowsgui" -o mgx-editor.exe

7. 运行!

首次运行 → 弹窗问是否提权 → 点"是" → 自动注册 .mgx 关联 之后随便创建个 test.mgx,双击 → 自动用你的编辑器打开! 界面清爽,无黑窗,同事看了直呼内行 👨‍💻

🤓 彩蛋:为什么不用 Electron?

因为——

  • 你的程序只有 5MB,而不是 150MB
  • 启动速度 0.2 秒,而不是"等我喝完这杯咖啡"
  • 内存占用 20MB,而不是"把 Chrome 当后台跑"

Go + Walk,轻量、原生、高效。这才是 Windows GUI 的正确打开方式。

🎁 结语:你的程序,值得被双击

从今天起,你的 Go 程序不再只是命令行里的"工具人"。它可以:

  • 自己申请权限
  • 自动绑定文件类型
  • 拥有漂亮的界面
  • 静静地运行,不打扰用户

这,才是一个成熟 Windows 应用该有的样子。

快去试试吧!说不定下一个被用户"双击打开"的,就是你的作品 ❤️

完整源码

package main

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"
	"syscall"
	"unsafe"

	"golang.org/x/sys/windows"
	"golang.org/x/sys/windows/registry"

	"github.com/lxn/walk"
	. "github.com/lxn/walk/declarative"
)

// MGXFile 定义文件内容结构
type MGXFile struct {
	Title      string `json:"title"`
	Content    string `json:"content"`
	CreatedAt  string `json:"created_at"`
	ModifiedAt string `json:"modified_at"`
}

// 当前打开的文件路径
var currentFilePath string



// isAdmin 检查当前进程是否拥有管理员权限
func isAdmin() bool {
	// 获取当前进程令牌
	var token windows.Token
	err := windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_QUERY, &token)
	if err != nil {
		return false
	}
	defer token.Close()

	// 检查是否是管理员 - 使用TOKEN_ELEVATION结构
	tokenElevation := struct {
		TokenIsElevated uint32
	}{}

	size := uint32(unsafe.Sizeof(tokenElevation))
	err = windows.GetTokenInformation(
		token,
		windows.TokenElevation,
		(*byte)(unsafe.Pointer(&tokenElevation)),
		size,
		&size,
	)

	if err != nil {
		return false
	}

	return tokenElevation.TokenIsElevated != 0
}

// restartAsAdmin 以管理员权限重启当前程序
func restartAsAdmin() bool {
	exePath, err := os.Executable()
	if err != nil {
		return false
	}

	// 使用syscall包的ShellExecuteW函数,这个函数更符合Windows API的原始行为
	exePath16, _ := syscall.UTF16PtrFromString(exePath)
	verb16, _ := syscall.UTF16PtrFromString("runas")

	// 构建命令行参数
	var params *uint16
	if len(os.Args) > 1 {
		params, _ = syscall.UTF16PtrFromString(strings.Join(os.Args[1:], " "))
	}

	dir16, _ := syscall.UTF16PtrFromString("")

	// 调用Windows API的ShellExecuteW函数
	hInstance, _, err := syscall.Syscall6(
		syscall.NewLazyDLL("shell32.dll").NewProc("ShellExecuteW").Addr(),
		6,
		0, // hWnd
		uintptr(unsafe.Pointer(verb16)),
		uintptr(unsafe.Pointer(exePath16)),
		uintptr(unsafe.Pointer(params)),
		uintptr(unsafe.Pointer(dir16)),
		windows.SW_SHOWNORMAL,
	)

	// ShellExecute返回大于32的HINSTANCE表示成功
	return hInstance > 32
}

// setFileAssociation 设置.mgx文件与程序的关联
func setFileAssociation() error {
	// 获取当前可执行文件路径
	exePath, err := os.Executable()
	if err != nil {
		return fmt.Errorf("获取可执行文件路径失败: %v", err)
	}

	// 文件类型ID和扩展名
	fileTypeID := "MGXFileEditor.MGX"
	fileExtension := ".mgx"

	// 尝试以不同的访问权限打开注册表
	// 首先创建文件类型项
	fileTypeKey, _, err := registry.CreateKey(registry.CLASSES_ROOT, fileTypeID, registry.WRITE)
	if err != nil {
		return fmt.Errorf("创建/打开文件类型注册表项失败: %v (通常需要管理员权限)", err)
	}
	defer fileTypeKey.Close()

	// 不再使用不存在的CREATED_NEW_KEY常量
	fmt.Println("已获取文件类型注册表项访问权限")

	// 设置文件类型描述
	if err := fileTypeKey.SetStringValue("", "MGX 文件"); err != nil {
		return fmt.Errorf("设置文件类型描述失败: %v", err)
	}

	// 创建或打开默认图标项
	defaultIconKey, _, err := registry.CreateKey(fileTypeKey, "DefaultIcon", registry.WRITE)
	if err != nil {
		return fmt.Errorf("创建默认图标注册表项失败: %v", err)
	}
	defer defaultIconKey.Close()

	// 设置图标路径
	iconPath := fmt.Sprintf("%s,0", exePath)
	if err := defaultIconKey.SetStringValue("", iconPath); err != nil {
		return fmt.Errorf("设置图标路径失败: %v", err)
	}

	// 创建或打开shell项
	shellKey, _, err := registry.CreateKey(fileTypeKey, "shell", registry.WRITE)
	if err != nil {
		return fmt.Errorf("创建shell注册表项失败: %v", err)
	}
	defer shellKey.Close()

	// 创建或打开open项
	openKey, _, err := registry.CreateKey(shellKey, "open", registry.WRITE)
	if err != nil {
		return fmt.Errorf("创建open注册表项失败: %v", err)
	}
	defer openKey.Close()

	// 创建或打开command项
	commandKey, _, err := registry.CreateKey(openKey, "command", registry.WRITE)
	if err != nil {
		return fmt.Errorf("创建command注册表项失败: %v", err)
	}
	defer commandKey.Close()

	// 设置命令行,使用双引号包裹路径以支持空格
	command := fmt.Sprintf("\"%s\" \"%%1\"", exePath)
	if err := commandKey.SetStringValue("", command); err != nil {
		return fmt.Errorf("设置命令行失败: %v", err)
	}

	// 关联文件扩展名
	extensionKey, _, err := registry.CreateKey(registry.CLASSES_ROOT, fileExtension, registry.WRITE)
	if err != nil {
		return fmt.Errorf("创建/打开文件扩展名注册表项失败: %v (通常需要管理员权限)", err)
	}
	defer extensionKey.Close()

	// 不再使用不存在的CREATED_NEW_KEY常量
	fmt.Println("已获取文件扩展名注册表项访问权限")

	// 设置文件类型ID
	if err := extensionKey.SetStringValue("", fileTypeID); err != nil {
		return fmt.Errorf("设置文件类型ID失败: %v", err)
	}

	// 通知Windows刷新文件类型关联
	fmt.Println(".mgx 文件关联设置成功完成!请重启文件资源管理器以确保图标正确显示")
	return nil
}

// isFileAssociationExists 检查.mgx文件关联是否已经存在
func isFileAssociationExists() bool {
	// 使用标准库的registry包,这更可靠
	key, err := registry.OpenKey(registry.CLASSES_ROOT, ".mgx", registry.QUERY_VALUE)
	if err != nil {
		fmt.Println("检查文件关联时无法打开注册表项:", err)
		return false
	}
	defer key.Close()

	value, _, err := key.GetStringValue("")
	if err != nil {
		fmt.Println("检查文件关联时无法读取注册表值:", err)
		return false
	}

	result := value == "MGXFileEditor.MGX"
	fmt.Printf("文件关联检查结果: %v (当前值: '%s')\n", result, value)
	return result
}



func main() {
	// 定义UI组件引用
	var titleEdit *walk.LineEdit
	var contentEdit *walk.TextEdit
	var statusLabel *walk.Label
	var mainWindow *walk.MainWindow

	// 检查.mgx文件关联是否已设置
	fileAssocExists := isFileAssociationExists()

	// 如果文件关联不存在且当前不是管理员权限,则提示并询问是否以管理员权限重启
	if !fileAssocExists && !isAdmin() {
		fmt.Println("检测到.mgx文件关联尚未设置,需要管理员权限")
		fmt.Println("将提示您以管理员身份重启程序以设置文件关联")

		// 使用walk.MsgBox创建图形界面提示,因为窗口还未创建
		result := walk.MsgBox(nil, "需要管理员权限", "设置文件关联需要管理员权限。\n\n"+
			"是否以管理员身份重启程序来设置文件关联?\n\n"+
			"(点击'是'将请求管理员权限重启,点击'否'将以普通权限继续运行)",
			walk.MsgBoxYesNo|walk.MsgBoxIconQuestion)

		if result == walk.DlgCmdYes {
			// 重启程序获取管理员权限
			fmt.Println("正在请求管理员权限重启程序...")
			if restartAsAdmin() {
				// 重启成功,退出当前进程
				fmt.Println("程序正在以管理员权限重启...")
				os.Exit(0)
			} else {
				fmt.Fprintf(os.Stderr, "无法以管理员权限重启程序,请手动以管理员身份运行\n")
			}
		} else {
			fmt.Println("用户选择不以管理员权限运行,将继续以普通权限运行程序")
		}
	}

	// 尝试设置文件关联
	if !fileAssocExists {
		fmt.Println("尝试设置文件关联...")
		if err := setFileAssociation(); err != nil {
			fmt.Fprintf(os.Stderr, "\n=== 重要提示 ===\n")
			fmt.Fprintf(os.Stderr, "设置文件关联失败: %v\n", err)
			fmt.Fprintf(os.Stderr, "即使不设置文件关联,程序仍可正常编辑.mgx文件\n")
			fmt.Fprintf(os.Stderr, "=================\n\n")
		} else {
			fmt.Println(".mgx 文件关联已成功设置!现在您可以双击.mgx文件直接打开")
		}
	} else {
		fmt.Println(".mgx文件关联已存在,无需重新设置")
	}

	// 创建窗口
	err := MainWindow{
		AssignTo: &mainWindow,
		Title:    "MGX文件编辑器",
		MinSize:  Size{Width: 800, Height: 600},
		Layout:   VBox{},
		MenuItems: []MenuItem{
			Menu{
				Text: "文件",
				Items: []MenuItem{
					Action{
						Text: "新建",
						OnTriggered: func() {
							newFile(titleEdit, contentEdit, &currentFilePath)
						},
					},
					Action{
						Text: "打开",
						OnTriggered: func() {
							openFile(titleEdit, contentEdit, &currentFilePath, mainWindow)
						},
					},
					Separator{},
					Action{
						Text: "保存",
						OnTriggered: func() {
							saveFile(titleEdit, contentEdit, &currentFilePath)
						},
					},
					Action{
						Text: "另存为...",
						OnTriggered: func() {
							saveFileAs(titleEdit, contentEdit, &currentFilePath)
						},
					},
				},
			},
		},
		Children: []Widget{
			// 标题输入区域
			Composite{
				Layout: HBox{},
				Children: []Widget{
					Label{Text: "标题:"},
					LineEdit{
						AssignTo:      &titleEdit,
						StretchFactor: 1,
					},
				},
			},

			// 内容编辑区
			TextEdit{
				AssignTo:      &contentEdit,
				StretchFactor: 1,
				VScroll:       true,
				HScroll:       true,
			},

			// 状态栏
			Composite{
				Layout: HBox{},
				Children: []Widget{
					Label{
						AssignTo:      &statusLabel,
						Text:          "欢迎使用MGX文件编辑器",
						StretchFactor: 1,
					},
				},
			},
		},
	}.Create()

	if err != nil {
		fmt.Fprintf(os.Stderr, "创建窗口失败: %v\n", err)
		os.Exit(1)
	}

	// 处理命令行参数(双击打开文件)
	if len(os.Args) > 1 && strings.HasSuffix(strings.ToLower(os.Args[1]), ".mgx") {
		if err := loadFile(os.Args[1], titleEdit, contentEdit, &currentFilePath, mainWindow); err != nil {
			statusLabel.SetText(fmt.Sprintf("错误: %v", err))
		} else {
			statusLabel.SetText(fmt.Sprintf("已打开: %s", filepath.Base(os.Args[1])))
		}
	}

	// 运行主循环
	mainWindow.Run()
}

// newFile 创建新文件,会提示保存当前未保存的内容
func newFile(titleEdit *walk.LineEdit, contentEdit *walk.TextEdit, filePath *string) {
	// 确认是否保存当前文件
	if *filePath == "" && (titleEdit.Text() != "" || contentEdit.Text() != "") {
		result := walk.MsgBox(nil, "保存", "是否保存当前文件?", walk.MsgBoxYesNo|walk.MsgBoxIconQuestion)
		switch result {
		case walk.DlgCmdYes:
			saveFileAs(titleEdit, contentEdit, filePath)
			return
		case walk.DlgCmdNo:
			// 继续创建新文件
		default:
			return // 取消操作
		}
	}

	// 清空编辑区
	titleEdit.SetText("")
	contentEdit.SetText("")
	*filePath = ""
}

// openFile 打开文件对话框选择并加载文件
func openFile(titleEdit *walk.LineEdit, contentEdit *walk.TextEdit, filePath *string, mainWindow *walk.MainWindow) {
	// 确认是否保存当前文件
	if *filePath == "" && (titleEdit.Text() != "" || contentEdit.Text() != "") {
		result := walk.MsgBox(mainWindow, "保存", "是否保存当前文件?", walk.MsgBoxYesNo|walk.MsgBoxIconQuestion)
		switch result {
		case walk.DlgCmdYes:
			saveFileAs(titleEdit, contentEdit, filePath)
		case walk.DlgCmdNo:
			// 继续打开文件
		default:
			return // 取消操作
		}
	}

	// 打开文件对话框
	dlg := walk.FileDialog{
		Title:    "打开MGX文件",
		Filter:   "MGX文件 (*.mgx)|*.mgx|所有文件 (*.*)|*.*",
		FilePath: *filePath,
	}

	if ok, err := dlg.ShowOpen(mainWindow); err != nil {
		walk.MsgBox(mainWindow, "错误", fmt.Sprintf("打开文件对话框失败: %v", err), walk.MsgBoxIconError)
	} else if ok {
		if err := loadFile(dlg.FilePath, titleEdit, contentEdit, filePath, mainWindow); err != nil {
			walk.MsgBox(mainWindow, "错误", fmt.Sprintf("打开文件失败: %v", err), walk.MsgBoxIconError)
		}
	}
}

// loadFile 从指定路径加载MGX文件内容到编辑器界面
func loadFile(filePath string, titleEdit *walk.LineEdit, contentEdit *walk.TextEdit, currentPath *string, mainWindow *walk.MainWindow) error {
	// 读取文件
	data, err := os.ReadFile(filePath)
	if err != nil {
		return err
	}

	// 解析JSON
	var mgxFile MGXFile
	if err := json.Unmarshal(data, &mgxFile); err != nil {
		return err
	}

	// 更新界面
	titleEdit.SetText(mgxFile.Title)
	contentEdit.SetText(mgxFile.Content)
	*currentPath = filePath

	// 更新窗口标题
	if mainWindow != nil {
		mainWindow.SetTitle(fmt.Sprintf("MGX文件编辑器 - %s", filepath.Base(filePath)))
	}

	return nil
}

// saveFile 保存当前文件,如未指定路径则调用另存为
func saveFile(titleEdit *walk.LineEdit, contentEdit *walk.TextEdit, filePath *string) {
	if *filePath != "" {
		doSaveFile(*filePath, titleEdit, contentEdit)
	} else {
		saveFileAs(titleEdit, contentEdit, filePath)
	}
}

// saveFileAs 打开另存为对话框选择保存位置
func saveFileAs(titleEdit *walk.LineEdit, contentEdit *walk.TextEdit, filePath *string) {
	dlg := walk.FileDialog{
		Title:    "保存MGX文件",
		Filter:   "MGX文件 (*.mgx)|*.mgx|所有文件 (*.*)|*.*",
		FilePath: "未命名.mgx",
	}

	// 使用当前窗口作为父窗口
	var mainWindow *walk.MainWindow
	if titleEdit != nil && titleEdit.Form() != nil {
		if mw, ok := titleEdit.Form().(*walk.MainWindow); ok {
			mainWindow = mw
		}
	}

	if ok, err := dlg.ShowSave(mainWindow); err != nil {
		walk.MsgBox(mainWindow, "错误", fmt.Sprintf("保存文件对话框失败: %v", err), walk.MsgBoxIconError)
	} else if ok {
		// 确保文件扩展名正确
		if !strings.HasSuffix(strings.ToLower(dlg.FilePath), ".mgx") {
			dlg.FilePath += ".mgx"
		}
		*filePath = dlg.FilePath
		doSaveFile(*filePath, titleEdit, contentEdit)
	}
}

// doSaveFile 将编辑器内容保存到指定文件路径
func doSaveFile(filePath string, titleEdit *walk.LineEdit, contentEdit *walk.TextEdit) {
	now := time.Now().Format("2006-01-02 15:04:05")

	// 检查是否为新文件
	isNewFile := !fileExists(filePath)

	// 创建MGXFile对象
	var createdAt string
	if isNewFile {
		createdAt = now
	} else {
		// 保留原始创建时间
		data, err := os.ReadFile(filePath)
		if err == nil {
			var existingFile MGXFile
			if json.Unmarshal(data, &existingFile) == nil {
				createdAt = existingFile.CreatedAt
			} else {
				createdAt = now
			}
		} else {
			createdAt = now
		}
	}

	mgxFile := MGXFile{
		Title:      titleEdit.Text(),
		Content:    contentEdit.Text(),
		CreatedAt:  createdAt,
		ModifiedAt: now,
	}

	// 序列化到JSON
	data, err := json.MarshalIndent(mgxFile, "", "  ")
	if err != nil {
		// 尝试获取主窗口引用
		var mainWindow *walk.MainWindow
		if titleEdit != nil && titleEdit.Form() != nil {
			if mw, ok := titleEdit.Form().(*walk.MainWindow); ok {
				mainWindow = mw
			}
		}
		walk.MsgBox(mainWindow, "错误", fmt.Sprintf("序列化文件失败: %v", err), walk.MsgBoxIconError)
		return
	}

	// 写入文件
	if err := os.WriteFile(filePath, data, 0644); err != nil {
		// 尝试获取主窗口引用
		var mainWindow *walk.MainWindow
		if titleEdit != nil && titleEdit.Form() != nil {
			if mw, ok := titleEdit.Form().(*walk.MainWindow); ok {
				mainWindow = mw
			}
		}
		walk.MsgBox(mainWindow, "错误", fmt.Sprintf("写入文件失败: %v", err), walk.MsgBoxIconError)
		return
	}

	// 保存成功后更新窗口标题
	if titleEdit != nil && titleEdit.Form() != nil {
		if mw, ok := titleEdit.Form().(*walk.MainWindow); ok {
			mw.SetTitle(fmt.Sprintf("MGX文件编辑器 - %s", filepath.Base(filePath)))
		}
	}
}

// fileExists 检查指定路径的文件是否存在
func fileExists(filePath string) bool {
	_, err := os.Stat(filePath)
	return !os.IsNotExist(err)
}

友情提示:测试完记得去注册表删掉 .mgx 项,不然你的桌面会充满魔法 ✨

往期部分文章列表