ImageViewModel.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. using Microsoft.Win32;
  2. using Prism.Commands;
  3. using Prism.Mvvm;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.IO;
  7. using System.Linq;
  8. using System.Windows;
  9. using System.Windows.Controls;
  10. using System.Windows.Media;
  11. using System.Windows.Media.Imaging;
  12. using System.Windows.Shapes;
  13. using System.Xml.Serialization;
  14. using TeamAAS_VP.Views.Dialogs;
  15. namespace TeamAAS_VP.ViewModels
  16. {
  17. public class ImageViewModel : BindableBase
  18. {
  19. private const string AppFolderName = "TeamAAS-VM";
  20. private const string ImageFileName = "LastImage.png";
  21. private const string XmlFileName = "LastAnno.xml";
  22. private string _folder;
  23. private string _imagePath;
  24. private string _xmlPath;
  25. private FrameworkElement _renderTarget;
  26. private Canvas _canvas;
  27. private Grid _lastArrowGroup;
  28. public ImageViewModel()
  29. {
  30. InitPaths();
  31. ImportImageCommand = new DelegateCommand(ImportImage);
  32. SaveCommand = new DelegateCommand(Save);
  33. ClearCommand = new DelegateCommand(Clear);
  34. Tool = "Rect";
  35. ColorName = "Red";
  36. Thickness = 2;
  37. }
  38. #region Commands
  39. public DelegateCommand ImportImageCommand { get; }
  40. public DelegateCommand SaveCommand { get; }
  41. public DelegateCommand ClearCommand { get; }
  42. #endregion
  43. #region Bindable Properties
  44. private ImageSource _ImageSource;
  45. public ImageSource ImageSource
  46. {
  47. get { return _ImageSource; }
  48. set { SetProperty(ref _ImageSource, value); }
  49. }
  50. private int _PixelWidth;
  51. public int PixelWidth
  52. {
  53. get { return _PixelWidth; }
  54. set { SetProperty(ref _PixelWidth, value); }
  55. }
  56. private int _PixelHeight;
  57. public int PixelHeight
  58. {
  59. get { return _PixelHeight; }
  60. set { SetProperty(ref _PixelHeight, value); }
  61. }
  62. /// <summary>
  63. /// Rect / Arrow / Text / Select
  64. /// </summary>
  65. private string _Tool;
  66. public string Tool
  67. {
  68. get { return _Tool; }
  69. set { SetProperty(ref _Tool, value); }
  70. }
  71. private string _ColorName;
  72. public string ColorName
  73. {
  74. get { return _ColorName; }
  75. set { SetProperty(ref _ColorName, value); }
  76. }
  77. private double _Thickness;
  78. public double Thickness
  79. {
  80. get { return _Thickness; }
  81. set { SetProperty(ref _Thickness, value); }
  82. }
  83. #endregion
  84. #region Public API (called from ImageView.xaml.cs)
  85. public void AttachRenderTarget(FrameworkElement renderTarget, Canvas canvas)
  86. {
  87. _renderTarget = renderTarget;
  88. _canvas = canvas;
  89. }
  90. public void LoadLast()
  91. {
  92. try
  93. {
  94. if (File.Exists(_imagePath))
  95. {
  96. var bmp = LoadBitmap(_imagePath);
  97. ImageSource = bmp;
  98. PixelWidth = bmp.PixelWidth;
  99. PixelHeight = bmp.PixelHeight;
  100. }
  101. if (_canvas != null && File.Exists(_xmlPath))
  102. {
  103. var list = ReadXml<List<AnnoItem>>(_xmlPath);
  104. if (list != null)
  105. {
  106. _canvas.Children.Clear();
  107. foreach (var item in list)
  108. {
  109. RestoreItem(item);
  110. }
  111. }
  112. }
  113. }
  114. catch
  115. {
  116. }
  117. }
  118. public Rectangle CreateRect()
  119. {
  120. return new Rectangle
  121. {
  122. Stroke = GetStrokeBrush(),
  123. StrokeThickness = Thickness,
  124. Fill = Brushes.Transparent
  125. };
  126. }
  127. public void CreateTextAt(Canvas canvas, Point pixelPoint)
  128. {
  129. var dlg = new TextInputDialog();
  130. dlg.Owner = Application.Current?.MainWindow;
  131. if (dlg.ShowDialog() != true) return;
  132. var input = dlg.InputText;
  133. if (string.IsNullOrWhiteSpace(input)) return;
  134. var tb = new TextBlock
  135. {
  136. Text = input,
  137. Foreground = GetStrokeBrush(),
  138. FontSize = 20,
  139. Background = Brushes.Transparent
  140. };
  141. Canvas.SetLeft(tb, pixelPoint.X);
  142. Canvas.SetTop(tb, pixelPoint.Y);
  143. canvas.Children.Add(tb);
  144. }
  145. public Grid CreateArrowGroup(Point start, Point end)
  146. {
  147. var brush = GetStrokeBrush();
  148. var line = new Line
  149. {
  150. X1 = start.X,
  151. Y1 = start.Y,
  152. X2 = end.X,
  153. Y2 = end.Y,
  154. Stroke = brush,
  155. StrokeThickness = Thickness
  156. };
  157. var head = new Polygon
  158. {
  159. Fill = brush,
  160. Stroke = brush,
  161. StrokeThickness = 1
  162. };
  163. var group = new Grid();
  164. group.Children.Add(line);
  165. group.Children.Add(head);
  166. group.Tag = line;
  167. _lastArrowGroup = group;
  168. UpdateArrowHead(line, head);
  169. return group;
  170. }
  171. public void UpdateLastArrow(Canvas canvas, Point current)
  172. {
  173. if (_lastArrowGroup == null) return;
  174. var line = _lastArrowGroup.Children.OfType<Line>().FirstOrDefault();
  175. var head = _lastArrowGroup.Children.OfType<Polygon>().FirstOrDefault();
  176. if (line == null || head == null) return;
  177. line.X2 = current.X;
  178. line.Y2 = current.Y;
  179. UpdateArrowHead(line, head);
  180. }
  181. public void CommitLastArrow(Canvas canvas, Point end)
  182. {
  183. UpdateLastArrow(canvas, end);
  184. }
  185. #endregion
  186. #region Command handlers
  187. private void ImportImage()
  188. {
  189. var dlg = new OpenFileDialog();
  190. dlg.Title = "导入图片";
  191. dlg.Filter = "Image Files|*.png;*.jpg;*.jpeg;*.bmp|All Files|*.*";
  192. if (dlg.ShowDialog() != true) return;
  193. var bmp = LoadBitmap(dlg.FileName);
  194. ImageSource = bmp;
  195. PixelWidth = bmp.PixelWidth;
  196. PixelHeight = bmp.PixelHeight;
  197. _canvas?.Children.Clear();
  198. _lastArrowGroup = null;
  199. }
  200. private void Clear()
  201. {
  202. _canvas?.Children.Clear();
  203. _lastArrowGroup = null;
  204. }
  205. private void Save()
  206. {
  207. if (ImageSource == null)
  208. {
  209. MessageBox.Show("请先导入图片", "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk);
  210. return;
  211. }
  212. if (_renderTarget == null || _canvas == null)
  213. {
  214. MessageBox.Show("保存失败:界面尚未初始化完成", "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk);
  215. return;
  216. }
  217. if (PixelWidth <= 0 || PixelHeight <= 0)
  218. {
  219. MessageBox.Show("图片像素尺寸无效", "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk);
  220. return;
  221. }
  222. try
  223. {
  224. var merged = RenderPixelElement(_renderTarget, PixelWidth, PixelHeight);
  225. SavePng(merged, _imagePath);
  226. var items = DumpAnnoItems();
  227. WriteXml(_xmlPath, items);
  228. MessageBox.Show("保存成功", "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk);
  229. }
  230. catch (Exception ex)
  231. {
  232. MessageBox.Show(ex.Message, "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk);
  233. }
  234. }
  235. #endregion
  236. #region Persistence (PNG + XML)
  237. private void InitPaths()
  238. {
  239. _folder = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppFolderName);
  240. Directory.CreateDirectory(_folder);
  241. _imagePath = System.IO.Path.Combine(_folder, ImageFileName);
  242. _xmlPath = System.IO.Path.Combine(_folder, XmlFileName);
  243. }
  244. private BitmapImage LoadBitmap(string path)
  245. {
  246. var bmp = new BitmapImage();
  247. bmp.BeginInit();
  248. bmp.CacheOption = BitmapCacheOption.OnLoad;
  249. bmp.UriSource = new Uri(path, UriKind.Absolute);
  250. bmp.EndInit();
  251. bmp.Freeze();
  252. return bmp;
  253. }
  254. private BitmapSource RenderPixelElement(FrameworkElement element, int pixelWidth, int pixelHeight)
  255. {
  256. element.Measure(new Size(pixelWidth, pixelHeight));
  257. element.Arrange(new Rect(0, 0, pixelWidth, pixelHeight));
  258. element.UpdateLayout();
  259. var rtb = new RenderTargetBitmap(pixelWidth, pixelHeight, 96, 96, PixelFormats.Pbgra32);
  260. rtb.Render(element);
  261. rtb.Freeze();
  262. return rtb;
  263. }
  264. private void SavePng(BitmapSource source, string path)
  265. {
  266. var encoder = new PngBitmapEncoder();
  267. encoder.Frames.Add(BitmapFrame.Create(source));
  268. using (var fs = File.Create(path))
  269. {
  270. encoder.Save(fs);
  271. }
  272. }
  273. private void WriteXml<T>(string path, T data)
  274. {
  275. var ser = new XmlSerializer(typeof(T));
  276. using (var fs = File.Create(path))
  277. {
  278. ser.Serialize(fs, data);
  279. }
  280. }
  281. private T ReadXml<T>(string path) where T : class
  282. {
  283. var ser = new XmlSerializer(typeof(T));
  284. using (var fs = File.OpenRead(path))
  285. {
  286. return ser.Deserialize(fs) as T;
  287. }
  288. }
  289. #endregion
  290. #region Dump/Restore items
  291. private List<AnnoItem> DumpAnnoItems()
  292. {
  293. var list = new List<AnnoItem>();
  294. if (_canvas == null) return list;
  295. foreach (var child in _canvas.Children)
  296. {
  297. if (child is Rectangle r)
  298. {
  299. list.Add(new AnnoItem
  300. {
  301. Type = "Rect",
  302. X = SafeGetLeft(r),
  303. Y = SafeGetTop(r),
  304. W = r.Width,
  305. H = r.Height,
  306. Color = BrushToColorString(r.Stroke),
  307. Thickness = r.StrokeThickness
  308. });
  309. }
  310. else if (child is TextBlock t)
  311. {
  312. list.Add(new AnnoItem
  313. {
  314. Type = "Text",
  315. X = SafeGetLeft(t),
  316. Y = SafeGetTop(t),
  317. Text = t.Text,
  318. Color = BrushToColorString(t.Foreground),
  319. FontSize = t.FontSize
  320. });
  321. }
  322. else if (child is Grid g)
  323. {
  324. var line = g.Children.OfType<Line>().FirstOrDefault();
  325. if (line != null)
  326. {
  327. list.Add(new AnnoItem
  328. {
  329. Type = "Arrow",
  330. X1 = line.X1,
  331. Y1 = line.Y1,
  332. X2 = line.X2,
  333. Y2 = line.Y2,
  334. Color = BrushToColorString(line.Stroke),
  335. Thickness = line.StrokeThickness
  336. });
  337. }
  338. }
  339. }
  340. return list;
  341. }
  342. private void RestoreItem(AnnoItem item)
  343. {
  344. if (_canvas == null || item == null) return;
  345. if (item.Type == "Rect")
  346. {
  347. var rect = new Rectangle
  348. {
  349. Stroke = BrushFrom(item.Color),
  350. StrokeThickness = item.Thickness <= 0 ? 2 : item.Thickness,
  351. Fill = Brushes.Transparent,
  352. Width = item.W,
  353. Height = item.H
  354. };
  355. Canvas.SetLeft(rect, item.X);
  356. Canvas.SetTop(rect, item.Y);
  357. _canvas.Children.Add(rect);
  358. return;
  359. }
  360. if (item.Type == "Text")
  361. {
  362. var tb = new TextBlock
  363. {
  364. Text = item.Text ?? string.Empty,
  365. Foreground = BrushFrom(item.Color),
  366. FontSize = item.FontSize <= 0 ? 20 : item.FontSize,
  367. Background = Brushes.Transparent
  368. };
  369. Canvas.SetLeft(tb, item.X);
  370. Canvas.SetTop(tb, item.Y);
  371. _canvas.Children.Add(tb);
  372. return;
  373. }
  374. if (item.Type == "Arrow")
  375. {
  376. var brush = BrushFrom(item.Color);
  377. var line = new Line
  378. {
  379. X1 = item.X1,
  380. Y1 = item.Y1,
  381. X2 = item.X2,
  382. Y2 = item.Y2,
  383. Stroke = brush,
  384. StrokeThickness = item.Thickness <= 0 ? 2 : item.Thickness
  385. };
  386. var head = new Polygon
  387. {
  388. Fill = brush,
  389. Stroke = brush,
  390. StrokeThickness = 1
  391. };
  392. var group = new Grid();
  393. group.Children.Add(line);
  394. group.Children.Add(head);
  395. group.Tag = line;
  396. UpdateArrowHead(line, head);
  397. _canvas.Children.Add(group);
  398. }
  399. }
  400. #endregion
  401. #region Helpers
  402. private Brush GetStrokeBrush()
  403. {
  404. try
  405. {
  406. var obj = ColorConverter.ConvertFromString(ColorName);
  407. if (obj != null)
  408. {
  409. return new SolidColorBrush((Color)obj);
  410. }
  411. }
  412. catch { }
  413. return Brushes.Red;
  414. }
  415. private Brush BrushFrom(string color)
  416. {
  417. try
  418. {
  419. if (!string.IsNullOrWhiteSpace(color))
  420. {
  421. var c = (Color)ColorConverter.ConvertFromString(color);
  422. return new SolidColorBrush(c);
  423. }
  424. }
  425. catch { }
  426. return Brushes.Red;
  427. }
  428. private string BrushToColorString(Brush brush)
  429. {
  430. if (brush is SolidColorBrush s)
  431. {
  432. return s.Color.ToString();
  433. }
  434. return Colors.Red.ToString();
  435. }
  436. private double SafeGetLeft(UIElement e)
  437. {
  438. var v = Canvas.GetLeft(e);
  439. return double.IsNaN(v) ? 0 : v;
  440. }
  441. private double SafeGetTop(UIElement e)
  442. {
  443. var v = Canvas.GetTop(e);
  444. return double.IsNaN(v) ? 0 : v;
  445. }
  446. private void UpdateArrowHead(Line line, Polygon head)
  447. {
  448. var dx = line.X2 - line.X1;
  449. var dy = line.Y2 - line.Y1;
  450. var len = Math.Sqrt(dx * dx + dy * dy);
  451. if (len < 1) len = 1;
  452. var ux = dx / len;
  453. var uy = dy / len;
  454. const double size = 12.0;
  455. var px = -uy;
  456. var py = ux;
  457. var tip = new Point(line.X2, line.Y2);
  458. var p1 = new Point(line.X2 - ux * size + px * (size / 2), line.Y2 - uy * size + py * (size / 2));
  459. var p2 = new Point(line.X2 - ux * size - px * (size / 2), line.Y2 - uy * size - py * (size / 2));
  460. head.Points.Clear();
  461. head.Points.Add(tip);
  462. head.Points.Add(p1);
  463. head.Points.Add(p2);
  464. }
  465. #endregion
  466. [Serializable]
  467. public class AnnoItem
  468. {
  469. public string Type { get; set; }
  470. public double X { get; set; }
  471. public double Y { get; set; }
  472. public double W { get; set; }
  473. public double H { get; set; }
  474. public string Text { get; set; }
  475. public double FontSize { get; set; }
  476. public double X1 { get; set; }
  477. public double Y1 { get; set; }
  478. public double X2 { get; set; }
  479. public double Y2 { get; set; }
  480. public string Color { get; set; }
  481. public double Thickness { get; set; }
  482. }
  483. public void RemoveAnnotation(UIElement element)
  484. {
  485. if (element == null) return;
  486. if (_canvas == null) return;
  487. if (_canvas.Children.Contains(element))
  488. {
  489. _canvas.Children.Remove(element);
  490. }
  491. // 如果删的是最后一根箭头,清理引用避免后续更新异常
  492. if (ReferenceEquals(_lastArrowGroup, element))
  493. {
  494. _lastArrowGroup = null;
  495. }
  496. }
  497. }
  498. }