初始化奇妙小屏幕控制器项目
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
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<ImageConfig> WriteAndVerifyAsync(string target, ImageConfig config, bool alreadyBusy = false)
|
||||
{
|
||||
if (!alreadyBusy) SetBusy(true);
|
||||
try
|
||||
{
|
||||
var reporter = new Progress<int>(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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user