refactor: 프로젝트명 DB720Flasher → FirmwareFlasher
폴더·csproj·어셈블리명·RootNamespace·코드 namespace·x:Class·app.manifest 전부 변경. 빌드 결과가 네이티브로 FirmwareFlasher.exe 로 나온다(dist 수동 리네임 불필요). README 빌드/구조 경로도 갱신. 모델명 "DB720"은 그대로 유지. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
476
FirmwareFlasher/MainWindow.xaml.cs
Normal file
476
FirmwareFlasher/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,476 @@
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Threading;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace FirmwareFlasher;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
string _baseDir = "";
|
||||
string _esptoolPath = "esptool";
|
||||
string? _fwFolder;
|
||||
List<FirmwareItem> _apps = new();
|
||||
List<FlashProfile> _builtins = new();
|
||||
List<FlashProfile> _models = new();
|
||||
FlashProfile? _profile;
|
||||
List<ComPortInfo> _ports = new();
|
||||
CancellationTokenSource? _cts;
|
||||
bool _busy, _ready, _refreshing;
|
||||
readonly DispatcherTimer _debounce;
|
||||
readonly DispatcherTimer _watch;
|
||||
string[] _portSnapshot = Array.Empty<string>();
|
||||
string? _shotPath;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_debounce = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(450) };
|
||||
_debounce.Tick += async (_, __) => { _debounce.Stop(); if (!_busy) await RefreshPortsAsync(); };
|
||||
|
||||
_watch = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
_watch.Tick += OnWatchTick;
|
||||
|
||||
var args = Environment.GetCommandLineArgs();
|
||||
int si = Array.IndexOf(args, "--shot");
|
||||
if (si >= 0 && si + 1 < args.Length) _shotPath = args[si + 1];
|
||||
|
||||
Loaded += OnLoaded;
|
||||
}
|
||||
|
||||
async void OnLoaded(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_baseDir = AppContext.BaseDirectory;
|
||||
_esptoolPath = EsptoolRunner.Locate(_baseDir, null);
|
||||
_builtins = ProfileStore.BuiltIns();
|
||||
|
||||
cboModel.SelectionChanged += (_, __) => OnModelChanged();
|
||||
cboApps.SelectionChanged += (_, __) => UpdateFlashEnabled();
|
||||
cboPorts.SelectionChanged += (_, __) => { UpdatePortHint(); UpdateConsoleWarn(); UpdateFlashEnabled(); };
|
||||
|
||||
var fwFolder = FirmwareCatalog.AutoLocate(_baseDir) ?? FirmwareCatalog.AutoLocate(Directory.GetCurrentDirectory());
|
||||
if (fwFolder is not null) LoadFirmware(fwFolder);
|
||||
else
|
||||
{
|
||||
txtFwFolder.Text = "(firmware 폴더를 찾지 못함 — '폴더…' 로 지정)";
|
||||
cboModel.ItemsSource = _builtins;
|
||||
cboModel.SelectedItem = _builtins.FirstOrDefault();
|
||||
}
|
||||
|
||||
_ready = true;
|
||||
OnModeChanged(this, new RoutedEventArgs());
|
||||
await RefreshPortsAsync();
|
||||
_ = ProbeEsptoolAsync();
|
||||
|
||||
_portSnapshot = CurrentPortNames();
|
||||
_watch.Start();
|
||||
|
||||
if (_shotPath is not null) await CaptureAndExitAsync(_shotPath);
|
||||
}
|
||||
|
||||
protected override void OnSourceInitialized(EventArgs e)
|
||||
{
|
||||
base.OnSourceInitialized(e);
|
||||
(PresentationSource.FromVisual(this) as HwndSource)?.AddHook(WndProc);
|
||||
}
|
||||
|
||||
const int WM_DEVICECHANGE = 0x0219;
|
||||
IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||
{
|
||||
if (msg == WM_DEVICECHANGE) { _debounce.Stop(); _debounce.Start(); }
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
async void OnWatchTick(object? sender, EventArgs e)
|
||||
{
|
||||
var now = CurrentPortNames();
|
||||
if (now.SequenceEqual(_portSnapshot)) return;
|
||||
_portSnapshot = now;
|
||||
if (!_busy) await RefreshPortsAsync();
|
||||
}
|
||||
|
||||
static string[] CurrentPortNames()
|
||||
{
|
||||
try { var n = System.IO.Ports.SerialPort.GetPortNames(); Array.Sort(n); return n; }
|
||||
catch { return Array.Empty<string>(); }
|
||||
}
|
||||
|
||||
// ===== 펌웨어 폴더 + 모델 =====
|
||||
void LoadFirmware(string folder)
|
||||
{
|
||||
_fwFolder = folder;
|
||||
txtFwFolder.Text = folder;
|
||||
_apps = FirmwareCatalog.LoadApps(folder);
|
||||
cboApps.ItemsSource = _apps;
|
||||
cboApps.SelectedItem = _apps.FirstOrDefault();
|
||||
|
||||
var models = new List<FlashProfile>(_builtins);
|
||||
FlashProfile? pick;
|
||||
var fileProf = ProfileStore.LoadFromFolder(folder);
|
||||
if (fileProf is not null)
|
||||
{
|
||||
fileProf.Name = $"{fileProf.Name} (폴더 flash.json)";
|
||||
models.Insert(0, fileProf);
|
||||
pick = fileProf;
|
||||
}
|
||||
else
|
||||
{
|
||||
pick = ProfileStore.MatchByApp(_apps.Select(a => a.FileName), _builtins);
|
||||
}
|
||||
_models = models;
|
||||
cboModel.ItemsSource = _models;
|
||||
cboModel.SelectedItem = pick ?? _models.FirstOrDefault();
|
||||
}
|
||||
|
||||
void OnModelChanged()
|
||||
{
|
||||
_profile = cboModel.SelectedItem as FlashProfile ?? _builtins.FirstOrDefault();
|
||||
if (_profile is null) return;
|
||||
txtProfile.Text = "설정: " + _profile.Summary + (_profile.FromFile ? " · 폴더 flash.json" : " · 빌트인");
|
||||
txtDlMode.Text = _profile.DownloadModeHint ?? "(안내 없음)";
|
||||
SelectPreferredPort((cboPorts.SelectedItem as ComPortInfo)?.PortName);
|
||||
UpdatePortHint();
|
||||
UpdateConsoleWarn();
|
||||
UpdateFlashEnabled();
|
||||
}
|
||||
|
||||
void OnBrowseClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new OpenFolderDialog { Title = "펌웨어 폴더 선택" };
|
||||
if (_fwFolder is not null && Directory.Exists(_fwFolder)) dlg.InitialDirectory = _fwFolder;
|
||||
if (dlg.ShowDialog(this) == true) LoadFirmware(dlg.FolderName);
|
||||
}
|
||||
|
||||
// ===== 포트 =====
|
||||
async Task RefreshPortsAsync()
|
||||
{
|
||||
if (_refreshing) return;
|
||||
_refreshing = true;
|
||||
try
|
||||
{
|
||||
string? prev = (cboPorts.SelectedItem as ComPortInfo)?.PortName;
|
||||
List<ComPortInfo> ports;
|
||||
try { ports = await Task.Run(PortScanner.Scan); } catch { ports = new(); }
|
||||
_ports = ports;
|
||||
cboPorts.ItemsSource = ports; // 전부 표시(제외 없음)
|
||||
SelectPreferredPort(prev);
|
||||
UpdatePortHint();
|
||||
UpdateConsoleWarn();
|
||||
UpdateFlashEnabled();
|
||||
}
|
||||
finally { _refreshing = false; }
|
||||
}
|
||||
|
||||
void SelectPreferredPort(string? prev)
|
||||
{
|
||||
var list = (cboPorts.ItemsSource as IEnumerable<ComPortInfo>)?.ToList() ?? new();
|
||||
// JTAG/권장 포트 중 가장 높은 번호(재열거된 최신)를 우선 선택 — 유령(옛 번호) 회피
|
||||
var pref = list
|
||||
.Where(p => p.IsJtag || (_profile is not null && p.Matches(_profile.FlashPortHint)))
|
||||
.OrderByDescending(p => p.Number)
|
||||
.FirstOrDefault();
|
||||
cboPorts.SelectedItem = pref
|
||||
?? list.FirstOrDefault(p => p.PortName == prev)
|
||||
?? list.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>선택 포트가 유령이면(열기 실패) 살아있는 JTAG/권장 포트로 자동 전환해 돌려준다.</summary>
|
||||
async Task<ComPortInfo?> ResolveLivePortAsync(ComPortInfo sel)
|
||||
{
|
||||
if (await Task.Run(() => PortScanner.CanOpen(sel.PortName))) return sel;
|
||||
|
||||
AppendLog($"[포트] {sel.PortName} 를 열 수 없음 — 보드 리셋으로 번호가 바뀐 듯. 살아있는 포트 탐색…");
|
||||
var ports = await Task.Run(PortScanner.Scan);
|
||||
foreach (var p in ports
|
||||
.Where(p => p.IsJtag || (_profile is not null && p.Matches(_profile.FlashPortHint)))
|
||||
.OrderByDescending(p => p.Number))
|
||||
{
|
||||
if (string.Equals(p.PortName, sel.PortName, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
if (await Task.Run(() => PortScanner.CanOpen(p.PortName)))
|
||||
{
|
||||
AppendLog($"[포트] → {p.PortName} 로 자동 전환");
|
||||
_ports = ports;
|
||||
cboPorts.ItemsSource = ports;
|
||||
cboPorts.SelectedItem = ports.FirstOrDefault(x => x.PortName == p.PortName);
|
||||
return cboPorts.SelectedItem as ComPortInfo;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void UpdatePortHint()
|
||||
{
|
||||
var sel = cboPorts.SelectedItem as ComPortInfo;
|
||||
var jtags = _ports.Where(p => p.IsJtag).ToList();
|
||||
string multi = jtags.Count > 1
|
||||
? $" · JTAG {jtags.Count}개(리셋으로 번호 바뀜) — 안 되면 자동으로 다른 번호 시도"
|
||||
: "";
|
||||
|
||||
if (sel is null)
|
||||
{
|
||||
txtPortStatus.Foreground = GetBrush("Danger");
|
||||
txtPortStatus.Text = "포트 없음 — 케이블을 연결하세요 (자동 감지 대기 중)";
|
||||
}
|
||||
else if (sel.IsJtag)
|
||||
{
|
||||
txtPortStatus.Foreground = GetBrush("Success");
|
||||
txtPortStatus.Text = $"✓ {sel.PortName} 선택됨 — USB-JTAG (303A:1001) 자동 감지{multi}";
|
||||
}
|
||||
else if (_profile is not null && sel.Matches(_profile.FlashPortHint))
|
||||
{
|
||||
txtPortStatus.Foreground = GetBrush("Success");
|
||||
txtPortStatus.Text = $"✓ {sel.PortName} 선택됨 — {sel.Kind} (모델 권장 포트){multi}";
|
||||
}
|
||||
else
|
||||
{
|
||||
var liveJtag = jtags.OrderByDescending(p => p.Number).FirstOrDefault();
|
||||
txtPortStatus.Foreground = GetBrush("Warn");
|
||||
txtPortStatus.Text = $"{sel.PortName} 선택됨 — {sel.Kind}. 모델에 맞는 포트인지 확인하세요"
|
||||
+ (liveJtag is not null ? $" (JTAG {liveJtag.PortName}도 있음)" : "");
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateConsoleWarn()
|
||||
{
|
||||
var sel = cboPorts.SelectedItem as ComPortInfo;
|
||||
// FlashPortHint 가 있는(=JTAG로 굽는) 모델에서만, 콘솔 포트를 고르면 경고. 막지는 않음.
|
||||
if (_profile?.FlashPortHint is not null && _profile.ConsolePortHint is not null
|
||||
&& sel is not null && sel.Matches(_profile.ConsolePortHint))
|
||||
{
|
||||
txtConsoleWarn.Visibility = Visibility.Visible;
|
||||
txtConsoleWarn.Text = $"※ {sel.PortName}({sel.Kind})는 이 모델에선 디버그 콘솔일 수 있습니다. 플래시는 보통 JTAG로 합니다 — 의도한 거면 그대로 진행하세요.";
|
||||
}
|
||||
else txtConsoleWarn.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
async void OnRefreshClick(object sender, RoutedEventArgs e) => await RefreshPortsAsync();
|
||||
|
||||
// ===== 모드 =====
|
||||
FlashMode CurrentMode() =>
|
||||
rbFull.IsChecked == true ? FlashMode.Full
|
||||
: rbStandard.IsChecked == true ? FlashMode.Standard
|
||||
: FlashMode.AppOnly;
|
||||
|
||||
void OnModeChanged(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!_ready) return;
|
||||
pnlEfuse.Visibility = CurrentMode() == FlashMode.Full ? Visibility.Visible : Visibility.Collapsed;
|
||||
UpdateFlashEnabled();
|
||||
}
|
||||
|
||||
void UpdateFlashEnabled()
|
||||
{
|
||||
if (!_ready) return;
|
||||
bool ok = !_busy
|
||||
&& cboPorts.SelectedItem is ComPortInfo
|
||||
&& cboApps.SelectedItem is FirmwareItem
|
||||
&& _profile is not null
|
||||
&& _fwFolder is not null
|
||||
&& FirmwareCatalog.MissingFor(_fwFolder, _profile, CurrentMode()) is null;
|
||||
if (CurrentMode() == FlashMode.Full && chkEfuseAck.IsChecked != true) ok = false;
|
||||
btnFlash.IsEnabled = ok;
|
||||
}
|
||||
|
||||
// ===== 실행 =====
|
||||
async void OnFlashClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_profile is null || _fwFolder is null) return;
|
||||
if (cboPorts.SelectedItem is not ComPortInfo port) { Info("포트를 선택하세요."); return; }
|
||||
if (cboApps.SelectedItem is not FirmwareItem app) { Info("올릴 앱을 선택하세요."); return; }
|
||||
var mode = CurrentMode();
|
||||
if (FirmwareCatalog.MissingFor(_fwFolder, _profile, mode) is string miss) { Info($"이 모드에 필요한 파일이 없습니다: {miss}"); return; }
|
||||
|
||||
// 로그/진행 초기화 후 '살아있는' 포트 확인 — 유령 COM(번호 바뀜) 자동 회피
|
||||
HideResult();
|
||||
txtLog.Clear();
|
||||
barProgress.Value = 0; txtPercent.Text = "0%"; txtStep.Text = "";
|
||||
|
||||
var live = await ResolveLivePortAsync(port);
|
||||
if (live is null)
|
||||
{
|
||||
ShowResultBox(false, "포트를 열 수 없습니다. 보드 리셋으로 COM 번호가 바뀌었거나(유령 포트) 다른 프로그램이 포트를 잡고 있을 수 있습니다 — USB 케이블을 다시 꽂고 '↻ 새로고침' 후 다시 시도하세요.");
|
||||
return;
|
||||
}
|
||||
port = live;
|
||||
|
||||
string modeName = mode switch { FlashMode.AppOnly => "앱만", FlashMode.Standard => "표준", _ => "전체" };
|
||||
string msg = $"모델: {_profile.Name}\n포트: {port.PortName} ({port.Kind})\n앱: {app.FileName}\n모드: {modeName} 플래시{(chkForce.IsChecked == true ? " (--force)" : "")}";
|
||||
if (mode == FlashMode.Full)
|
||||
msg += "\n\n★ 전체 플래시는 부트로더를 다시 씁니다. eFuse 미굽힘 보드는 첫 부팅에 영구로 구워집니다(비가역).";
|
||||
if (MessageBox.Show(this, msg + "\n\n진행할까요?", "플래시 확인",
|
||||
MessageBoxButton.YesNo,
|
||||
mode == FlashMode.Full ? MessageBoxImage.Warning : MessageBoxImage.Question) != MessageBoxResult.Yes)
|
||||
return;
|
||||
|
||||
var req = new FlashRequest
|
||||
{
|
||||
Port = port.PortName,
|
||||
Mode = mode,
|
||||
AppPath = app.FullPath,
|
||||
FirmwareFolder = _fwFolder,
|
||||
Profile = _profile,
|
||||
Force = chkForce.IsChecked == true,
|
||||
};
|
||||
|
||||
SetBusy(true);
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
var runner = MakeRunner();
|
||||
try
|
||||
{
|
||||
var res = await runner.RunAsync(req, _cts.Token);
|
||||
ShowResult(res);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
AppendLog("\n■ 중지됨");
|
||||
ShowResultBox(false, "중지됨 — 플래시가 중간에 멈췄습니다. 보드 상태가 불완전할 수 있으니 다시 하세요.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppendLog("\n✗ 오류: " + ex.Message);
|
||||
ShowResultBox(false, "오류: " + ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cts?.Dispose(); _cts = null;
|
||||
SetBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async void OnTestClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_profile is null) { Info("모델을 선택하세요."); return; }
|
||||
if (cboPorts.SelectedItem is not ComPortInfo port) { Info("점검할 포트를 선택하세요."); return; }
|
||||
HideResult();
|
||||
txtLog.Clear();
|
||||
var live = await ResolveLivePortAsync(port);
|
||||
if (live is null) { ShowResultBox(false, "포트를 열 수 없습니다 — USB 케이블을 다시 꽂고 '↻ 새로고침' 후 다시 시도하세요."); return; }
|
||||
port = live;
|
||||
SetBusy(true);
|
||||
txtStep.Text = "연결 점검";
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
var runner = MakeRunner();
|
||||
try
|
||||
{
|
||||
bool ok = await runner.TestConnectionAsync(_profile, port.PortName, _cts.Token);
|
||||
ShowResultBox(ok, ok
|
||||
? $"✓ {port.PortName} 연결 OK — 플래시 준비됨."
|
||||
: $"✗ {port.PortName} 연결 실패 — 케이블/포트·다운로드 모드 진입을 확인하세요. 다른 프로그램이 포트를 잡고 있을 수 있습니다.");
|
||||
}
|
||||
catch (OperationCanceledException) { AppendLog("\n■ 중지됨"); }
|
||||
catch (Exception ex) { ShowResultBox(false, "오류: " + ex.Message); }
|
||||
finally { _cts?.Dispose(); _cts = null; SetBusy(false); }
|
||||
}
|
||||
|
||||
void OnStopClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
AppendLog("\n■ 중지 요청…");
|
||||
_cts?.Cancel();
|
||||
}
|
||||
|
||||
EsptoolRunner MakeRunner()
|
||||
{
|
||||
var runner = new EsptoolRunner(_esptoolPath);
|
||||
runner.Log += s => Dispatcher.BeginInvoke(() => AppendLog(s));
|
||||
runner.Progress += (p, t) => Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
barProgress.Value = p;
|
||||
txtPercent.Text = $"{p * 100:0}%";
|
||||
txtStep.Text = t;
|
||||
});
|
||||
return runner;
|
||||
}
|
||||
|
||||
// ===== 상태 표시 =====
|
||||
void SetBusy(bool busy)
|
||||
{
|
||||
_busy = busy;
|
||||
foreach (Control c in new Control[]
|
||||
{ cboModel, cboApps, cboPorts, btnRefresh, btnTest, btnBrowse, rbAppOnly, rbStandard, rbFull, chkForce, chkEfuseAck })
|
||||
c.IsEnabled = !busy;
|
||||
btnStop.IsEnabled = busy;
|
||||
if (busy) btnFlash.IsEnabled = false; else UpdateFlashEnabled();
|
||||
}
|
||||
|
||||
void ShowResult(FlashResult res)
|
||||
{
|
||||
if (res.Success)
|
||||
{
|
||||
string tip = _profile is { NoStub: true }
|
||||
? "\n• USB-JTAG 케이블을 뽑고 전원을 재인가하면 확실히 정상 부팅합니다."
|
||||
: "";
|
||||
ShowResultBox(true, "✓ 플래시 완료. 보드 전원을 재인가(재부팅)하세요." + tip);
|
||||
}
|
||||
else if (res.BootloaderRejected)
|
||||
{
|
||||
ShowResultBox(false,
|
||||
$"부트로더 쓰기를 거부했습니다 (rc={res.ExitCode}).\n" +
|
||||
"이미 secure boot가 구워진 보드면 '정상'입니다 — 모드를 '표준'이나 '앱만'으로 바꿔 다시 하세요(부트로더 건너뜀). 꼭 부트로더를 써야 하면 '--force'를 켜세요.");
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowResultBox(false,
|
||||
$"단계 실패: {res.FailedStep} (rc={res.ExitCode}).\n" +
|
||||
"로그를 확인하세요. 포트·케이블, 다운로드 모드 진입, 모델 선택을 점검하고 재시도하세요.");
|
||||
}
|
||||
}
|
||||
|
||||
void ShowResultBox(bool ok, string text)
|
||||
{
|
||||
pnlResult.Visibility = Visibility.Visible;
|
||||
pnlResult.BorderBrush = GetBrush(ok ? "Success" : "Danger");
|
||||
pnlResult.Background = GetBrush(ok ? "SuccessDim" : "DangerDim");
|
||||
txtResult.Foreground = GetBrush("Text");
|
||||
txtResult.Text = text;
|
||||
}
|
||||
|
||||
void HideResult() => pnlResult.Visibility = Visibility.Collapsed;
|
||||
void AppendLog(string s) { txtLog.AppendText(s + "\n"); txtLog.ScrollToEnd(); }
|
||||
|
||||
async Task ProbeEsptoolAsync()
|
||||
{
|
||||
var (rc, text) = await EsptoolRunner.Capture(_esptoolPath, "version");
|
||||
string tag = _esptoolPath.Contains(@"\tools\esptool\", StringComparison.OrdinalIgnoreCase)
|
||||
? "동봉" : (Path.IsPathRooted(_esptoolPath) ? "설치본" : "PATH");
|
||||
if (rc == 0)
|
||||
{
|
||||
var ver = text.Split('\n')[0].Trim();
|
||||
txtEsptool.Text = $"{ver} · {tag} · {_esptoolPath}";
|
||||
}
|
||||
else
|
||||
{
|
||||
txtEsptool.Foreground = GetBrush("Warn");
|
||||
txtEsptool.Text = $"⚠ esptool 실행 불가 ({_esptoolPath}) — tools\\esptool\\ 에 동봉하거나 설치가 필요합니다.";
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 디버그용 화면 캡처(--shot 경로) =====
|
||||
async Task CaptureAndExitAsync(string path)
|
||||
{
|
||||
await Task.Delay(1100);
|
||||
try
|
||||
{
|
||||
UpdateLayout();
|
||||
int w = Math.Max(1, (int)Math.Ceiling(ActualWidth));
|
||||
int h = Math.Max(1, (int)Math.Ceiling(ActualHeight));
|
||||
var rtb = new RenderTargetBitmap(w, h, 96, 96, PixelFormats.Pbgra32);
|
||||
rtb.Render(this);
|
||||
var enc = new PngBitmapEncoder();
|
||||
enc.Frames.Add(BitmapFrame.Create(rtb));
|
||||
using var fs = File.Create(path);
|
||||
enc.Save(fs);
|
||||
}
|
||||
catch { }
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
|
||||
void Info(string m) => MessageBox.Show(this, m, "확인", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
Brush GetBrush(string key) => (Brush)FindResource(key);
|
||||
}
|
||||
Reference in New Issue
Block a user