using Microsoft.Win32; using Prism.Commands; using Prism.Mvvm; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; using System.Xml.Serialization; using TeamAAS_VP.Views.Dialogs; namespace TeamAAS_VP.ViewModels { public class ImageViewModel : BindableBase { private const string AppFolderName = "TeamAAS-VM"; private const string ImageFileName = "LastImage.png"; private const string XmlFileName = "LastAnno.xml"; private string _folder; private string _imagePath; private string _xmlPath; private FrameworkElement _renderTarget; private Canvas _canvas; private Grid _lastArrowGroup; public ImageViewModel() { InitPaths(); ImportImageCommand = new DelegateCommand(ImportImage); SaveCommand = new DelegateCommand(Save); ClearCommand = new DelegateCommand(Clear); Tool = "Rect"; ColorName = "Red"; Thickness = 2; } #region Commands public DelegateCommand ImportImageCommand { get; } public DelegateCommand SaveCommand { get; } public DelegateCommand ClearCommand { get; } #endregion #region Bindable Properties private ImageSource _ImageSource; public ImageSource ImageSource { get { return _ImageSource; } set { SetProperty(ref _ImageSource, value); } } private int _PixelWidth; public int PixelWidth { get { return _PixelWidth; } set { SetProperty(ref _PixelWidth, value); } } private int _PixelHeight; public int PixelHeight { get { return _PixelHeight; } set { SetProperty(ref _PixelHeight, value); } } /// /// Rect / Arrow / Text / Select /// private string _Tool; public string Tool { get { return _Tool; } set { SetProperty(ref _Tool, value); } } private string _ColorName; public string ColorName { get { return _ColorName; } set { SetProperty(ref _ColorName, value); } } private double _Thickness; public double Thickness { get { return _Thickness; } set { SetProperty(ref _Thickness, value); } } #endregion #region Public API (called from ImageView.xaml.cs) public void AttachRenderTarget(FrameworkElement renderTarget, Canvas canvas) { _renderTarget = renderTarget; _canvas = canvas; } public void LoadLast() { try { if (File.Exists(_imagePath)) { var bmp = LoadBitmap(_imagePath); ImageSource = bmp; PixelWidth = bmp.PixelWidth; PixelHeight = bmp.PixelHeight; } if (_canvas != null && File.Exists(_xmlPath)) { var list = ReadXml>(_xmlPath); if (list != null) { _canvas.Children.Clear(); foreach (var item in list) { RestoreItem(item); } } } } catch { } } public Rectangle CreateRect() { return new Rectangle { Stroke = GetStrokeBrush(), StrokeThickness = Thickness, Fill = Brushes.Transparent }; } public void CreateTextAt(Canvas canvas, Point pixelPoint) { var dlg = new TextInputDialog(); dlg.Owner = Application.Current?.MainWindow; if (dlg.ShowDialog() != true) return; var input = dlg.InputText; if (string.IsNullOrWhiteSpace(input)) return; var tb = new TextBlock { Text = input, Foreground = GetStrokeBrush(), FontSize = 20, Background = Brushes.Transparent }; Canvas.SetLeft(tb, pixelPoint.X); Canvas.SetTop(tb, pixelPoint.Y); canvas.Children.Add(tb); } public Grid CreateArrowGroup(Point start, Point end) { var brush = GetStrokeBrush(); var line = new Line { X1 = start.X, Y1 = start.Y, X2 = end.X, Y2 = end.Y, Stroke = brush, StrokeThickness = Thickness }; var head = new Polygon { Fill = brush, Stroke = brush, StrokeThickness = 1 }; var group = new Grid(); group.Children.Add(line); group.Children.Add(head); group.Tag = line; _lastArrowGroup = group; UpdateArrowHead(line, head); return group; } public void UpdateLastArrow(Canvas canvas, Point current) { if (_lastArrowGroup == null) return; var line = _lastArrowGroup.Children.OfType().FirstOrDefault(); var head = _lastArrowGroup.Children.OfType().FirstOrDefault(); if (line == null || head == null) return; line.X2 = current.X; line.Y2 = current.Y; UpdateArrowHead(line, head); } public void CommitLastArrow(Canvas canvas, Point end) { UpdateLastArrow(canvas, end); } #endregion #region Command handlers private void ImportImage() { var dlg = new OpenFileDialog(); dlg.Title = "导入图片"; dlg.Filter = "Image Files|*.png;*.jpg;*.jpeg;*.bmp|All Files|*.*"; if (dlg.ShowDialog() != true) return; var bmp = LoadBitmap(dlg.FileName); ImageSource = bmp; PixelWidth = bmp.PixelWidth; PixelHeight = bmp.PixelHeight; _canvas?.Children.Clear(); _lastArrowGroup = null; } private void Clear() { _canvas?.Children.Clear(); _lastArrowGroup = null; } private void Save() { if (ImageSource == null) { MessageBox.Show("请先导入图片", "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk); return; } if (_renderTarget == null || _canvas == null) { MessageBox.Show("保存失败:界面尚未初始化完成", "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk); return; } if (PixelWidth <= 0 || PixelHeight <= 0) { MessageBox.Show("图片像素尺寸无效", "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk); return; } try { var merged = RenderPixelElement(_renderTarget, PixelWidth, PixelHeight); SavePng(merged, _imagePath); var items = DumpAnnoItems(); WriteXml(_xmlPath, items); MessageBox.Show("保存成功", "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk); } catch (Exception ex) { MessageBox.Show(ex.Message, "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk); } } #endregion #region Persistence (PNG + XML) private void InitPaths() { _folder = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppFolderName); Directory.CreateDirectory(_folder); _imagePath = System.IO.Path.Combine(_folder, ImageFileName); _xmlPath = System.IO.Path.Combine(_folder, XmlFileName); } private BitmapImage LoadBitmap(string path) { var bmp = new BitmapImage(); bmp.BeginInit(); bmp.CacheOption = BitmapCacheOption.OnLoad; bmp.UriSource = new Uri(path, UriKind.Absolute); bmp.EndInit(); bmp.Freeze(); return bmp; } private BitmapSource RenderPixelElement(FrameworkElement element, int pixelWidth, int pixelHeight) { element.Measure(new Size(pixelWidth, pixelHeight)); element.Arrange(new Rect(0, 0, pixelWidth, pixelHeight)); element.UpdateLayout(); var rtb = new RenderTargetBitmap(pixelWidth, pixelHeight, 96, 96, PixelFormats.Pbgra32); rtb.Render(element); rtb.Freeze(); return rtb; } private void SavePng(BitmapSource source, string path) { var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(source)); using (var fs = File.Create(path)) { encoder.Save(fs); } } private void WriteXml(string path, T data) { var ser = new XmlSerializer(typeof(T)); using (var fs = File.Create(path)) { ser.Serialize(fs, data); } } private T ReadXml(string path) where T : class { var ser = new XmlSerializer(typeof(T)); using (var fs = File.OpenRead(path)) { return ser.Deserialize(fs) as T; } } #endregion #region Dump/Restore items private List DumpAnnoItems() { var list = new List(); if (_canvas == null) return list; foreach (var child in _canvas.Children) { if (child is Rectangle r) { list.Add(new AnnoItem { Type = "Rect", X = SafeGetLeft(r), Y = SafeGetTop(r), W = r.Width, H = r.Height, Color = BrushToColorString(r.Stroke), Thickness = r.StrokeThickness }); } else if (child is TextBlock t) { list.Add(new AnnoItem { Type = "Text", X = SafeGetLeft(t), Y = SafeGetTop(t), Text = t.Text, Color = BrushToColorString(t.Foreground), FontSize = t.FontSize }); } else if (child is Grid g) { var line = g.Children.OfType().FirstOrDefault(); if (line != null) { list.Add(new AnnoItem { Type = "Arrow", X1 = line.X1, Y1 = line.Y1, X2 = line.X2, Y2 = line.Y2, Color = BrushToColorString(line.Stroke), Thickness = line.StrokeThickness }); } } } return list; } private void RestoreItem(AnnoItem item) { if (_canvas == null || item == null) return; if (item.Type == "Rect") { var rect = new Rectangle { Stroke = BrushFrom(item.Color), StrokeThickness = item.Thickness <= 0 ? 2 : item.Thickness, Fill = Brushes.Transparent, Width = item.W, Height = item.H }; Canvas.SetLeft(rect, item.X); Canvas.SetTop(rect, item.Y); _canvas.Children.Add(rect); return; } if (item.Type == "Text") { var tb = new TextBlock { Text = item.Text ?? string.Empty, Foreground = BrushFrom(item.Color), FontSize = item.FontSize <= 0 ? 20 : item.FontSize, Background = Brushes.Transparent }; Canvas.SetLeft(tb, item.X); Canvas.SetTop(tb, item.Y); _canvas.Children.Add(tb); return; } if (item.Type == "Arrow") { var brush = BrushFrom(item.Color); var line = new Line { X1 = item.X1, Y1 = item.Y1, X2 = item.X2, Y2 = item.Y2, Stroke = brush, StrokeThickness = item.Thickness <= 0 ? 2 : item.Thickness }; var head = new Polygon { Fill = brush, Stroke = brush, StrokeThickness = 1 }; var group = new Grid(); group.Children.Add(line); group.Children.Add(head); group.Tag = line; UpdateArrowHead(line, head); _canvas.Children.Add(group); } } #endregion #region Helpers private Brush GetStrokeBrush() { try { var obj = ColorConverter.ConvertFromString(ColorName); if (obj != null) { return new SolidColorBrush((Color)obj); } } catch { } return Brushes.Red; } private Brush BrushFrom(string color) { try { if (!string.IsNullOrWhiteSpace(color)) { var c = (Color)ColorConverter.ConvertFromString(color); return new SolidColorBrush(c); } } catch { } return Brushes.Red; } private string BrushToColorString(Brush brush) { if (brush is SolidColorBrush s) { return s.Color.ToString(); } return Colors.Red.ToString(); } private double SafeGetLeft(UIElement e) { var v = Canvas.GetLeft(e); return double.IsNaN(v) ? 0 : v; } private double SafeGetTop(UIElement e) { var v = Canvas.GetTop(e); return double.IsNaN(v) ? 0 : v; } private void UpdateArrowHead(Line line, Polygon head) { var dx = line.X2 - line.X1; var dy = line.Y2 - line.Y1; var len = Math.Sqrt(dx * dx + dy * dy); if (len < 1) len = 1; var ux = dx / len; var uy = dy / len; const double size = 12.0; var px = -uy; var py = ux; var tip = new Point(line.X2, line.Y2); var p1 = new Point(line.X2 - ux * size + px * (size / 2), line.Y2 - uy * size + py * (size / 2)); var p2 = new Point(line.X2 - ux * size - px * (size / 2), line.Y2 - uy * size - py * (size / 2)); head.Points.Clear(); head.Points.Add(tip); head.Points.Add(p1); head.Points.Add(p2); } #endregion [Serializable] public class AnnoItem { public string Type { get; set; } public double X { get; set; } public double Y { get; set; } public double W { get; set; } public double H { get; set; } public string Text { get; set; } public double FontSize { get; set; } public double X1 { get; set; } public double Y1 { get; set; } public double X2 { get; set; } public double Y2 { get; set; } public string Color { get; set; } public double Thickness { get; set; } } public void RemoveAnnotation(UIElement element) { if (element == null) return; if (_canvas == null) return; if (_canvas.Children.Contains(element)) { _canvas.Children.Remove(element); } // 如果删的是最后一根箭头,清理引用避免后续更新异常 if (ReferenceEquals(_lastArrowGroup, element)) { _lastArrowGroup = null; } } } }