using Microsoft.Win32; namespace MatrixImageEditor; public sealed class MainForm : Form { private readonly TextBox path = new() { ReadOnly = true, Dock = DockStyle.Fill }; private readonly Label version = new() { AutoSize = true, Text = "尚未打开镜像" }; private readonly TextBox username = new(); private readonly TextBox accountPassword = new() { UseSystemPasswordChar = true }; private readonly TextBox ssid = new(); private readonly TextBox wifiPassword = new() { UseSystemPasswordChar = true }; private readonly ComboBox mode = new() { DropDownStyle = ComboBoxStyle.DropDownList }; private readonly TextBox address = new(); private readonly NumericUpDown prefix = new() { Minimum = 1, Maximum = 32, Value = 24 }; private readonly TextBox gateway = new(); private readonly TextBox dns = new(); private readonly Button open = new() { Text = "打开镜像…", AutoSize = true, MinimumSize = new Size(130, 38) }; private readonly Button save = new() { Text = "修改原镜像", Enabled = false, AutoSize = true }; private readonly Button saveAs = new() { Text = "另存为…", Enabled = false, AutoSize = true }; private readonly ProgressBar progress = new() { Dock = DockStyle.Fill, Visible = false }; private string? imagePath; private ImageConfig? loaded; private bool isBusy; private bool watchingDisplaySettings; private static readonly Size PreferredLogicalSize = new(960, 760); private static readonly Size MinimumLogicalSize = new(760, 600); public MainForm() { var assemblyVersion = typeof(MainForm).Assembly.GetName().Version; Text = $"核桃派镜像配置编辑器 {assemblyVersion?.Major}.{assemblyVersion?.Minor}.{assemblyVersion?.Build}"; AutoScaleMode = AutoScaleMode.Dpi; StartPosition = FormStartPosition.Manual; Font = new Font("Microsoft YaHei UI", 10F); AllowDrop = true; mode.Items.AddRange(["自动获取(DHCP)", "静态 IPv4"]); mode.SelectedIndexChanged += (_, _) => UpdateNetworkFields(); save.Click += async (_, _) => await SaveOriginalAsync(); saveAs.Click += async (_, _) => await SaveAsAsync(); open.Click += (_, _) => OpenImageFromDialog(); DragEnter += (_, eventArgs) => { if (!isBusy && eventArgs.Data?.GetDataPresent(DataFormats.FileDrop) == true) eventArgs.Effect = DragDropEffects.Copy; }; DragDrop += (_, eventArgs) => { if (!isBusy && eventArgs.Data?.GetData(DataFormats.FileDrop) is string[] files && files.Length == 1) OpenImage(files[0]); }; Controls.Add(BuildLayout()); } private Control BuildLayout() { var outer = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(24), ColumnCount = 1, RowCount = 6, AutoScroll = true }; for (var index = 0; index < 6; index++) outer.RowStyles.Add(new RowStyle(SizeType.AutoSize)); var header = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, ColumnCount = 2, Margin = new Padding(0, 0, 0, 12) }; header.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); header.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); path.Margin = new Padding(0, 4, 12, 4); open.Margin = new Padding(0); header.Controls.Add(path, 0, 0); header.Controls.Add(open, 1, 0); outer.Controls.Add(header); version.Margin = new Padding(0, 0, 0, 12); outer.Controls.Add(version); outer.Controls.Add(Group("登录账户", [("用户名", username), ("密码", PasswordPanel(accountPassword))])); outer.Controls.Add(Group("Wi-Fi", [("SSID", ssid), ("密码", PasswordPanel(wifiPassword))])); outer.Controls.Add(Group("IPv4", [("方式", mode), ("地址", address), ("前缀", prefix), ("网关", gateway), ("DNS(逗号分隔)", dns)])); var footer = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, ColumnCount = 3, Margin = new Padding(0, 16, 0, 0) }; footer.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); footer.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); footer.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); progress.MinimumSize = new Size(200, 28); save.MinimumSize = new Size(150, 40); save.Margin = new Padding(12, 0, 0, 0); saveAs.MinimumSize = new Size(130, 40); saveAs.Margin = new Padding(12, 0, 0, 0); footer.Controls.Add(progress, 0, 0); footer.Controls.Add(save, 1, 0); footer.Controls.Add(saveAs, 2, 0); outer.Controls.Add(footer); return outer; } private static GroupBox Group(string title, (string Label, Control Control)[] rows) { var box = new GroupBox { Text = title, Dock = DockStyle.Top, AutoSize = true, Padding = new Padding(16), Margin = new Padding(0, 0, 0, 12) }; var table = new TableLayoutPanel { Dock = DockStyle.Fill, AutoSize = true, ColumnCount = 2, GrowStyle = TableLayoutPanelGrowStyle.AddRows }; table.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); foreach (var row in rows) { row.Control.Dock = DockStyle.Fill; row.Control.MinimumSize = new Size(320, 0); row.Control.Margin = new Padding(12, 5, 0, 5); table.Controls.Add(new Label { Text = row.Label, AutoSize = true, Anchor = AnchorStyles.Left, Margin = new Padding(0, 7, 12, 7) }, 0, table.RowCount); table.Controls.Add(row.Control, 1, table.RowCount++); } box.Controls.Add(table); return box; } private static Control PasswordPanel(TextBox password) { var panel = new TableLayoutPanel { Dock = DockStyle.Fill, AutoSize = true, ColumnCount = 2 }; panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); var show = new CheckBox { Text = "显示", AutoSize = true }; show.CheckedChanged += (_, _) => password.UseSystemPasswordChar = !show.Checked; password.Dock = DockStyle.Fill; password.MinimumSize = new Size(260, 0); password.Margin = new Padding(0, 0, 12, 0); show.Anchor = AnchorStyles.Left; show.Margin = new Padding(0); panel.Controls.Add(password); panel.Controls.Add(show); return panel; } private void OpenImageFromDialog() { using var dialog = new OpenFileDialog { Filter = "核桃派镜像 (*.img)|*.img", CheckFileExists = true, CheckPathExists = true, Multiselect = false, ValidateNames = true, }; if (dialog.ShowDialog(this) == DialogResult.OK) OpenImage(dialog.FileName); } private void OpenImage(string filename) { try { var config = ImageOperations.ReadConfig(filename); ApplyLoadedConfig(Path.GetFullPath(filename), config); } catch (Exception exception) { AppDialog.Error(this, exception.Message, "无法打开镜像"); } } private void ApplyLoadedConfig(string filename, ImageConfig config) { imagePath = filename; loaded = config; path.Text = imagePath; version.Text = $"软件版本:{config.SoftwareVersion} 配置校验:正常"; username.Text = config.Account.Username; accountPassword.Text = config.Account.Password; ssid.Text = config.Wifi.Ssid; wifiPassword.Text = config.Wifi.Password; mode.SelectedIndex = config.Ipv4.Mode == "static" ? 1 : 0; address.Text = config.Ipv4.Address; prefix.Value = config.Ipv4.Prefix == 0 ? 24 : config.Ipv4.Prefix; gateway.Text = config.Ipv4.Gateway; dns.Text = string.Join(",", config.Ipv4.Dns); save.Enabled = saveAs.Enabled = true; UpdateNetworkFields(); } private ImageConfig Collect() { if (loaded is null) throw new InvalidOperationException("尚未打开镜像"); var config = new ImageConfig { SchemaVersion = loaded.SchemaVersion, Product = loaded.Product, SoftwareVersion = loaded.SoftwareVersion, Account = new AccountConfig { Username = username.Text.Trim(), Password = accountPassword.Text }, Wifi = new WifiConfig { Ssid = ssid.Text, Password = wifiPassword.Text }, Ipv4 = mode.SelectedIndex == 0 ? new Ipv4Config { Mode = "dhcp" } : new Ipv4Config { Mode = "static", Address = address.Text.Trim(), Prefix = (int)prefix.Value, Gateway = gateway.Text.Trim(), Dns = dns.Text.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList() }, }; ImageConfigCodec.Validate(config); return config; } private async Task SaveOriginalAsync() { if (imagePath is null) return; try { var config = Collect(); if (AppDialog.Confirm(this, "这会直接修改当前镜像。建议日常使用“另存为”保留原始包。\n\n确定继续吗?", "确认修改原镜像") != DialogResult.Yes) return; var verified = await WriteAndVerifyAsync(imagePath, config); ApplyLoadedConfig(imagePath, verified); AppDialog.Information(this, "配置已保存,镜像与 SHA-256 摘要均已完成写后校验。", "保存成功"); } catch (Exception exception) { AppDialog.Error(this, exception.Message, "保存失败"); } } private async Task SaveAsAsync() { if (imagePath is null) return; using var dialog = new SaveFileDialog { Filter = "核桃派镜像 (*.img)|*.img", DefaultExt = "img", AddExtension = true, ValidateNames = true, CheckPathExists = true, OverwritePrompt = false, FileName = Path.GetFileNameWithoutExtension(imagePath) + "-自定义.img", InitialDirectory = Path.GetDirectoryName(imagePath), Title = "请重命名,以便区分设备或使用地点", }; if (dialog.ShowDialog(this) != DialogResult.OK) return; string? destination = null; try { var config = Collect(); destination = ImagePathRules.ValidateNewDestination(imagePath, dialog.FileName); SetBusy(true); await Task.Run(() => File.Copy(imagePath, destination, overwrite: false)); var verified = await WriteAndVerifyAsync(destination, config, alreadyBusy: true); ApplyLoadedConfig(destination, verified); AppDialog.Information(this, "自定义镜像已保存并重新打开,镜像与 SHA-256 摘要均已完成写后校验。", "另存为成功"); } catch (Exception exception) { var cleanupError = destination is null ? null : ImagePathRules.CleanupNewDestination(destination); var message = cleanupError is null ? exception.Message : $"{exception.Message}\n\n清理未完成:{cleanupError}"; AppDialog.Error(this, message, "另存为失败"); } finally { SetBusy(false); } } private async Task WriteAndVerifyAsync(string target, ImageConfig config, bool alreadyBusy = false) { if (!alreadyBusy) SetBusy(true); try { var reporter = new Progress(value => progress.Value = Math.Clamp(value, 0, 100)); return await Task.Run(() => { ImageOperations.WriteConfig(target, config); ImageOperations.WriteSha256(target, reporter); return ImageOperations.ReadConfig(target); }); } finally { if (!alreadyBusy) SetBusy(false); } } private void SetBusy(bool value) { isBusy = value; open.Enabled = !value; save.Enabled = saveAs.Enabled = !value && loaded is not null; progress.Visible = value; progress.Value = 0; UseWaitCursor = value; } private void UpdateNetworkFields() { var enabled = mode.SelectedIndex == 1; address.Enabled = prefix.Enabled = gateway.Enabled = dns.Enabled = enabled; } protected override void OnShown(EventArgs eventArgs) { base.OnShown(eventArgs); ResponsiveLayout.FitAndCenter(this, PreferredLogicalSize, MinimumLogicalSize, 0.9); if (!watchingDisplaySettings) { SystemEvents.DisplaySettingsChanged += DisplaySettingsChanged; watchingDisplaySettings = true; } } protected override void OnDpiChanged(DpiChangedEventArgs eventArgs) { base.OnDpiChanged(eventArgs); BeginInvoke(() => ResponsiveLayout.FitAndCenter(this, PreferredLogicalSize, MinimumLogicalSize, 0.9)); } protected override void OnFormClosed(FormClosedEventArgs eventArgs) { if (watchingDisplaySettings) { SystemEvents.DisplaySettingsChanged -= DisplaySettingsChanged; watchingDisplaySettings = false; } base.OnFormClosed(eventArgs); } private void DisplaySettingsChanged(object? sender, EventArgs eventArgs) { if (!IsDisposed && IsHandleCreated) BeginInvoke(() => ResponsiveLayout.FitAndCenter(this, PreferredLogicalSize, MinimumLogicalSize, 0.9)); } }