ImageDisplayControl.xaml.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. using System;
  2. using System.Drawing;
  3. using System.IO;
  4. using System.Runtime.InteropServices;
  5. using System.Windows;
  6. using System.Windows.Controls;
  7. using System.Windows.Input;
  8. using System.Windows.Interop;
  9. using System.Windows.Media.Imaging;
  10. namespace TeamAAS.Camera.UI.Controls
  11. {
  12. /// <summary>
  13. /// 图像显示控件:支持拖放图片、鼠标缩放平移、定位十字、保存图像。
  14. /// 使用 Canvas + Image.Width/Height + Canvas.SetLeft/Top 实现缩放平移,
  15. /// 完全不使用 RenderTransform,避免 DPI/布局尺寸不匹配的裁剪问题。
  16. /// </summary>
  17. public partial class ImageDisplayControl : UserControl
  18. {
  19. #region Dependency Properties
  20. public static readonly DependencyProperty SourceProperty =
  21. DependencyProperty.Register(nameof(Source), typeof(BitmapSource),
  22. typeof(ImageDisplayControl),
  23. new PropertyMetadata(null, OnSourceChanged));
  24. public static readonly DependencyProperty ShowCrosshairProperty =
  25. DependencyProperty.Register(nameof(ShowCrosshair), typeof(bool),
  26. typeof(ImageDisplayControl),
  27. new PropertyMetadata(false, OnShowCrosshairChanged));
  28. /// <summary>
  29. /// 显示的图像源(BitmapSource)。支持数据绑定。
  30. /// </summary>
  31. public BitmapSource Source
  32. {
  33. get => (BitmapSource)GetValue(SourceProperty);
  34. set => SetValue(SourceProperty, value);
  35. }
  36. /// <summary>
  37. /// 是否显示定位十字。
  38. /// </summary>
  39. public bool ShowCrosshair
  40. {
  41. get => (bool)GetValue(ShowCrosshairProperty);
  42. set => SetValue(ShowCrosshairProperty, value);
  43. }
  44. #endregion
  45. [DllImport("gdi32.dll")]
  46. private static extern bool DeleteObject(IntPtr hObject);
  47. // 缩放与平移状态(全部基于像素,不涉及 DPI 转换)
  48. private double _zoom = 1.0;
  49. private double _offsetX = 0;
  50. private double _offsetY = 0;
  51. private bool _isDragging;
  52. private System.Windows.Point _dragStart;
  53. private double _startOffsetX;
  54. private double _startOffsetY;
  55. private bool _autoFit = true;
  56. // 图像像素尺寸(来自 BitmapSource.PixelWidth/Height)
  57. private int _imgPixelWidth = 0;
  58. private int _imgPixelHeight = 0;
  59. public ImageDisplayControl()
  60. {
  61. InitializeComponent();
  62. SizeChanged += OnSizeChanged;
  63. }
  64. #region Source Management
  65. private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  66. {
  67. var ctrl = (ImageDisplayControl)d;
  68. var newSource = e.NewValue as BitmapSource;
  69. var oldSource = e.OldValue as BitmapSource;
  70. ctrl.ImageControl.Source = newSource;
  71. ctrl.EmptyText.Visibility = newSource == null ? Visibility.Visible : Visibility.Collapsed;
  72. if (newSource != null)
  73. {
  74. ctrl._imgPixelWidth = newSource.PixelWidth;
  75. ctrl._imgPixelHeight = newSource.PixelHeight;
  76. // 仅在从 null → 非 null(第一帧)时自动适应
  77. if (oldSource == null && ctrl._autoFit)
  78. {
  79. ctrl.Dispatcher.BeginInvoke(new Action(() =>
  80. {
  81. if (ctrl._autoFit)
  82. ctrl.FitToWindow();
  83. }), System.Windows.Threading.DispatcherPriority.Render);
  84. }
  85. else
  86. {
  87. // 后续帧:保持当前缩放和偏移,仅更新尺寸
  88. ctrl.UpdateImageLayout();
  89. }
  90. }
  91. }
  92. private static void OnShowCrosshairChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  93. {
  94. var ctrl = (ImageDisplayControl)d;
  95. var show = (bool)e.NewValue;
  96. ctrl.OverlayCanvas.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
  97. ctrl.CrosshairMenuItem.IsChecked = show;
  98. ctrl.UpdateCrosshair();
  99. }
  100. /// <summary>
  101. /// 设置 System.Drawing.Bitmap 图像。
  102. /// </summary>
  103. public void SetImage(Bitmap bitmap)
  104. {
  105. if (bitmap == null)
  106. {
  107. Clear();
  108. return;
  109. }
  110. var bmpSource = BitmapToBitmapSource(bitmap);
  111. SetCurrentValue(SourceProperty, bmpSource);
  112. }
  113. /// <summary>
  114. /// 设置 BitmapSource(线程安全,可从非UI线程调用)。
  115. /// </summary>
  116. public void SetImage(BitmapSource source)
  117. {
  118. if (source == null)
  119. {
  120. Clear();
  121. return;
  122. }
  123. if (!source.IsFrozen)
  124. source.Freeze();
  125. Dispatcher.BeginInvoke(new Action(() =>
  126. {
  127. SetCurrentValue(SourceProperty, source);
  128. }));
  129. }
  130. /// <summary>
  131. /// 清除图像。
  132. /// </summary>
  133. public void Clear()
  134. {
  135. SetCurrentValue(SourceProperty, null);
  136. ImageControl.Source = null;
  137. EmptyText.Visibility = Visibility.Visible;
  138. }
  139. private BitmapSource BitmapToBitmapSource(Bitmap bitmap)
  140. {
  141. var hBitmap = bitmap.GetHbitmap();
  142. try
  143. {
  144. var bmpSource = Imaging.CreateBitmapSourceFromHBitmap(
  145. hBitmap, IntPtr.Zero, Int32Rect.Empty,
  146. BitmapSizeOptions.FromEmptyOptions());
  147. bmpSource.Freeze();
  148. return bmpSource;
  149. }
  150. finally
  151. {
  152. DeleteObject(hBitmap);
  153. bitmap.Dispose();
  154. }
  155. }
  156. #endregion
  157. #region Zoom & Pan
  158. /// <summary>
  159. /// 核心方法:根据 _zoom 和 _offset 更新 Image 的 Width/Height 和 Canvas 位置。
  160. /// 所有尺寸计算基于像素,不涉及 DPI 转换。
  161. /// </summary>
  162. private void UpdateImageLayout()
  163. {
  164. if (_imgPixelWidth == 0 || _imgPixelHeight == 0) return;
  165. ImageControl.Width = _imgPixelWidth * _zoom;
  166. ImageControl.Height = _imgPixelHeight * _zoom;
  167. Canvas.SetLeft(ImageControl, _offsetX);
  168. Canvas.SetTop(ImageControl, _offsetY);
  169. UpdateZoomText();
  170. }
  171. private void OnMouseWheel(object sender, MouseWheelEventArgs e)
  172. {
  173. if (ImageControl.Source == null) return;
  174. _autoFit = false;
  175. var position = e.GetPosition(MainGrid);
  176. var oldZoom = _zoom;
  177. if (e.Delta > 0)
  178. _zoom *= 1.2;
  179. else
  180. _zoom /= 1.2;
  181. _zoom = Math.Max(0.05, Math.Min(50, _zoom));
  182. // 以鼠标位置为中心缩放
  183. var ratio = _zoom / oldZoom;
  184. _offsetX = position.X - (position.X - _offsetX) * ratio;
  185. _offsetY = position.Y - (position.Y - _offsetY) * ratio;
  186. UpdateImageLayout();
  187. }
  188. private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  189. {
  190. if (ImageControl.Source == null) return;
  191. // 双击适应窗口
  192. if (e.ClickCount == 2)
  193. {
  194. FitToWindow();
  195. return;
  196. }
  197. _isDragging = true;
  198. _dragStart = e.GetPosition(MainGrid);
  199. _startOffsetX = _offsetX;
  200. _startOffsetY = _offsetY;
  201. MainGrid.CaptureMouse();
  202. }
  203. private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
  204. {
  205. _isDragging = false;
  206. MainGrid.ReleaseMouseCapture();
  207. }
  208. private void OnMouseMove(object sender, MouseEventArgs e)
  209. {
  210. var pos = e.GetPosition(MainGrid);
  211. // 更新坐标显示(直接像素坐标,无需 DPI 转换)
  212. if (ImageControl.Source != null && _imgPixelWidth > 0)
  213. {
  214. var imgX = (int)((pos.X - _offsetX) / _zoom);
  215. var imgY = (int)((pos.Y - _offsetY) / _zoom);
  216. if (imgX >= 0 && imgX < _imgPixelWidth && imgY >= 0 && imgY < _imgPixelHeight)
  217. {
  218. CoordText.Text = $"X: {imgX}, Y: {imgY}";
  219. }
  220. else
  221. {
  222. CoordText.Text = "";
  223. }
  224. }
  225. // 拖拽平移
  226. if (_isDragging)
  227. {
  228. var delta = pos - _dragStart;
  229. _offsetX = _startOffsetX + delta.X;
  230. _offsetY = _startOffsetY + delta.Y;
  231. UpdateImageLayout();
  232. }
  233. }
  234. private void OnMouseLeave(object sender, MouseEventArgs e)
  235. {
  236. _isDragging = false;
  237. CoordText.Text = "";
  238. }
  239. /// <summary>
  240. /// 适应窗口大小。
  241. /// </summary>
  242. public void FitToWindow()
  243. {
  244. if (_imgPixelWidth == 0 || _imgPixelHeight == 0) return;
  245. if (MainGrid.ActualWidth == 0 || MainGrid.ActualHeight == 0)
  246. {
  247. _autoFit = true;
  248. return;
  249. }
  250. var scaleX = MainGrid.ActualWidth / _imgPixelWidth;
  251. var scaleY = MainGrid.ActualHeight / _imgPixelHeight;
  252. _zoom = Math.Min(scaleX, scaleY);
  253. if (_zoom <= 0) _zoom = 1;
  254. // 居中
  255. _offsetX = (MainGrid.ActualWidth - _imgPixelWidth * _zoom) / 2;
  256. _offsetY = (MainGrid.ActualHeight - _imgPixelHeight * _zoom) / 2;
  257. UpdateImageLayout();
  258. }
  259. /// <summary>
  260. /// 实际大小(100%)。
  261. /// </summary>
  262. public void ActualSize()
  263. {
  264. _zoom = 1.0;
  265. _offsetX = (MainGrid.ActualWidth - _imgPixelWidth) / 2;
  266. _offsetY = (MainGrid.ActualHeight - _imgPixelHeight) / 2;
  267. _autoFit = false;
  268. UpdateImageLayout();
  269. }
  270. private void UpdateZoomText()
  271. {
  272. if (ImageControl.Source != null)
  273. {
  274. ZoomText.Text = $"{_zoom * 100:F0}% | GW:{MainGrid.ActualWidth:F0} GH:{MainGrid.ActualHeight:F0} PW:{_imgPixelWidth} PH:{_imgPixelHeight} IW:{ImageControl.Width:F0} OX:{_offsetX:F0} OY:{_offsetY:F0}";
  275. }
  276. else
  277. {
  278. ZoomText.Text = $"{_zoom * 100:F0}%";
  279. }
  280. }
  281. #endregion
  282. #region Crosshair
  283. private void UpdateCrosshair()
  284. {
  285. if (!ShowCrosshair) return;
  286. var cx = MainGrid.ActualWidth / 2;
  287. var cy = MainGrid.ActualHeight / 2;
  288. CrossLineH.X1 = 0;
  289. CrossLineH.Y1 = cy;
  290. CrossLineH.X2 = MainGrid.ActualWidth;
  291. CrossLineH.Y2 = cy;
  292. CrossLineV.X1 = cx;
  293. CrossLineV.Y1 = 0;
  294. CrossLineV.X2 = cx;
  295. CrossLineV.Y2 = MainGrid.ActualHeight;
  296. }
  297. private void OnSizeChanged(object sender, SizeChangedEventArgs e)
  298. {
  299. UpdateCrosshair();
  300. if (_autoFit && ImageControl.Source != null)
  301. {
  302. FitToWindow();
  303. }
  304. }
  305. #endregion
  306. #region Drag & Drop
  307. private void OnDragOver(object sender, DragEventArgs e)
  308. {
  309. e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop)
  310. ? DragDropEffects.Copy
  311. : DragDropEffects.None;
  312. e.Handled = true;
  313. }
  314. private void OnDrop(object sender, DragEventArgs e)
  315. {
  316. if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
  317. var files = (string[])e.Data.GetData(DataFormats.FileDrop);
  318. if (files == null || files.Length == 0) return;
  319. try
  320. {
  321. var bmp = new BitmapImage();
  322. bmp.BeginInit();
  323. bmp.UriSource = new Uri(files[0]);
  324. bmp.CacheOption = BitmapCacheOption.OnLoad;
  325. bmp.EndInit();
  326. bmp.Freeze();
  327. SetCurrentValue(SourceProperty, bmp);
  328. _autoFit = true;
  329. FitToWindow();
  330. }
  331. catch (Exception ex)
  332. {
  333. MessageBox.Show($"无法加载图片: {ex.Message}", "图像显示", MessageBoxButton.OK, MessageBoxImage.Warning);
  334. }
  335. }
  336. #endregion
  337. #region Context Menu Actions
  338. private void MenuToggleCrosshair_Click(object sender, RoutedEventArgs e)
  339. {
  340. ShowCrosshair = CrosshairMenuItem.IsChecked;
  341. }
  342. private void MenuSaveImage_Click(object sender, RoutedEventArgs e)
  343. {
  344. if (ImageControl.Source == null)
  345. {
  346. MessageBox.Show("没有图像可保存", "图像显示");
  347. return;
  348. }
  349. var dlg = new Microsoft.Win32.SaveFileDialog
  350. {
  351. Filter = "PNG 图像|*.png|JPEG 图像|*.jpg|BMP 图像|*.bmp",
  352. DefaultExt = ".png",
  353. FileName = $"image_{DateTime.Now:yyyyMMdd_HHmmss}"
  354. };
  355. if (dlg.ShowDialog() != true) return;
  356. try
  357. {
  358. BitmapEncoder encoder = Path.GetExtension(dlg.FileName).ToLower() switch
  359. {
  360. ".jpg" or ".jpeg" => new JpegBitmapEncoder(),
  361. ".bmp" => new BmpBitmapEncoder(),
  362. _ => new PngBitmapEncoder()
  363. };
  364. encoder.Frames.Add(BitmapFrame.Create((BitmapSource)ImageControl.Source));
  365. using var fs = new FileStream(dlg.FileName, FileMode.Create);
  366. encoder.Save(fs);
  367. }
  368. catch (Exception ex)
  369. {
  370. MessageBox.Show($"保存失败: {ex.Message}", "图像显示", MessageBoxButton.OK, MessageBoxImage.Error);
  371. }
  372. }
  373. private void MenuFitWindow_Click(object sender, RoutedEventArgs e)
  374. {
  375. _autoFit = true;
  376. FitToWindow();
  377. }
  378. private void MenuActualSize_Click(object sender, RoutedEventArgs e)
  379. {
  380. ActualSize();
  381. }
  382. #endregion
  383. }
  384. }