595 lines
20 KiB
C#
595 lines
20 KiB
C#
using MainShell.Common;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Windows;
|
||
using System.Windows.Input;
|
||
using System.Windows.Media;
|
||
using System.Windows.Threading;
|
||
using System.Windows.Controls;
|
||
using System.Windows.Controls.Primitives;
|
||
using System.ComponentModel;
|
||
|
||
namespace MainShell.Resources.CustomControl
|
||
{
|
||
/// <summary>
|
||
/// 高性能芯片Map图控件,支持5-10万个点的动态状态更新,支持鼠标拖拽和缩放
|
||
/// </summary>
|
||
public class DieMapControl : FrameworkElement
|
||
{
|
||
private int _rows;
|
||
private int _columns;
|
||
private DieState[,] _dieStates;
|
||
private Point[,] _dieCoordinates;
|
||
private double _minX, _maxX, _minY, _maxY;
|
||
private Dictionary<DieState, Brush> _stateBrushes;
|
||
|
||
// 渲染相关
|
||
private DrawingVisual _visual;
|
||
private VisualCollection _visuals;
|
||
private DispatcherTimer _renderTimer;
|
||
private bool _needsRedraw = false;
|
||
|
||
// 交互相关
|
||
private MatrixTransform _transform = new MatrixTransform();
|
||
private Point _lastMousePosition;
|
||
private Point _mouseDownPosition;
|
||
private bool _isDragging;
|
||
private bool _isViewChangedByUser;
|
||
private Popup _infoPopup;
|
||
private TextBlock _infoText;
|
||
|
||
public static readonly DependencyProperty BackgroundProperty =
|
||
DependencyProperty.Register("Background", typeof(Brush), typeof(DieMapControl), new FrameworkPropertyMetadata(Brushes.Transparent, FrameworkPropertyMetadataOptions.AffectsRender));
|
||
|
||
public Brush Background
|
||
{
|
||
get { return (Brush)GetValue(BackgroundProperty); }
|
||
set { SetValue(BackgroundProperty, value); }
|
||
}
|
||
|
||
public static readonly DependencyProperty MapModelProperty =
|
||
DependencyProperty.Register("MapModel", typeof(DieMapModel), typeof(DieMapControl), new PropertyMetadata(null, OnMapModelChanged));
|
||
|
||
public DieMapModel MapModel
|
||
{
|
||
get { return (DieMapModel)GetValue(MapModelProperty); }
|
||
set { SetValue(MapModelProperty, value); }
|
||
}
|
||
|
||
public static readonly DependencyProperty DieClickedCommandProperty =
|
||
DependencyProperty.Register("DieClickedCommand", typeof(ICommand), typeof(DieMapControl), new PropertyMetadata(null));
|
||
|
||
public ICommand DieClickedCommand
|
||
{
|
||
get { return (ICommand)GetValue(DieClickedCommandProperty); }
|
||
set { SetValue(DieClickedCommandProperty, value); }
|
||
}
|
||
|
||
public event EventHandler<DieClickedEventArgs> DieClicked;
|
||
|
||
public class DieClickedEventArgs : EventArgs
|
||
{
|
||
public int Row { get; }
|
||
public int Col { get; }
|
||
public DieState State { get; }
|
||
|
||
public DieClickedEventArgs(int row, int col, DieState state)
|
||
{
|
||
Row = row;
|
||
Col = col;
|
||
State = state;
|
||
}
|
||
}
|
||
|
||
private static void OnMapModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||
{
|
||
var control = (DieMapControl)d;
|
||
if (e.OldValue is DieMapModel oldModel)
|
||
{
|
||
oldModel.MapInitialized -= control.OnMapInitialized;
|
||
oldModel.DieStateChanged -= control.OnDieStateChanged;
|
||
oldModel.DieStatesChanged -= control.OnDieStatesChanged;
|
||
}
|
||
if (e.NewValue is DieMapModel newModel)
|
||
{
|
||
newModel.MapInitialized += control.OnMapInitialized;
|
||
newModel.DieStateChanged += control.OnDieStateChanged;
|
||
newModel.DieStatesChanged += control.OnDieStatesChanged;
|
||
control.InitializeFromModel(newModel);
|
||
}
|
||
}
|
||
|
||
private void OnMapInitialized(object sender, EventArgs e)
|
||
{
|
||
InitializeFromModel(MapModel);
|
||
}
|
||
|
||
private void OnDieStateChanged(object sender, (int Row, int Col, DieState State) e)
|
||
{
|
||
_needsRedraw = true;
|
||
}
|
||
|
||
private void OnDieStatesChanged(object sender, IEnumerable<(int Row, int Col, DieState State)> e)
|
||
{
|
||
_needsRedraw = true;
|
||
}
|
||
|
||
public static readonly DependencyProperty DieSizeProperty =
|
||
DependencyProperty.Register("DieSize", typeof(double), typeof(DieMapControl), new FrameworkPropertyMetadata(4.0, FrameworkPropertyMetadataOptions.AffectsRender, OnSizeChanged));
|
||
|
||
public double DieSize
|
||
{
|
||
get { return (double)GetValue(DieSizeProperty); }
|
||
set { SetValue(DieSizeProperty, value); }
|
||
}
|
||
|
||
public static readonly DependencyProperty SpacingProperty =
|
||
DependencyProperty.Register("Spacing", typeof(double), typeof(DieMapControl), new FrameworkPropertyMetadata(1.0, FrameworkPropertyMetadataOptions.AffectsRender, OnSizeChanged));
|
||
|
||
public double Spacing
|
||
{
|
||
get { return (double)GetValue(SpacingProperty); }
|
||
set { SetValue(SpacingProperty, value); }
|
||
}
|
||
|
||
private static void OnSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||
{
|
||
var control = (DieMapControl)d;
|
||
control.RedrawAll();
|
||
}
|
||
|
||
public DieMapControl()
|
||
{
|
||
_visuals = new VisualCollection(this);
|
||
_visual = new DrawingVisual();
|
||
_visuals.Add(_visual);
|
||
|
||
// 初始化画刷字典,冻结以提高性能
|
||
_stateBrushes = new Dictionary<DieState, Brush>
|
||
{
|
||
{ DieState.Available, CreateFrozenBrush(Color.FromRgb(211, 211, 211)) }, // 浅灰色
|
||
{ DieState.Used, CreateFrozenBrush(Color.FromRgb(50, 205, 50)) }, // 绿色
|
||
{ DieState.Error, CreateFrozenBrush(Color.FromRgb(255, 0, 0)) }, // 红色
|
||
{ DieState.NotExist, Brushes.Transparent }, // 透明
|
||
{ DieState.Current, CreateFrozenBrush(Color.FromRgb(0, 191, 255)) }, // 深天蓝 - 当前位置
|
||
{ DieState.Target, CreateFrozenBrush(Color.FromRgb(255, 165, 0)) } // 橙色 - 目标位置
|
||
};
|
||
|
||
// 初始化Popup
|
||
_infoPopup = new Popup
|
||
{
|
||
Placement = PlacementMode.Mouse,
|
||
StaysOpen = false,
|
||
AllowsTransparency = true
|
||
};
|
||
Border popupBorder = new Border
|
||
{
|
||
Background = new SolidColorBrush(Color.FromArgb(220, 30, 30, 30)),
|
||
BorderBrush = Brushes.Gray,
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(4),
|
||
Padding = new Thickness(8)
|
||
};
|
||
_infoText = new TextBlock
|
||
{
|
||
Foreground = Brushes.White,
|
||
FontSize = 12
|
||
};
|
||
popupBorder.Child = _infoText;
|
||
_infoPopup.Child = popupBorder;
|
||
|
||
// 鼠标事件
|
||
this.MouseWheel += OnMouseWheel;
|
||
this.MouseDown += OnMouseDown;
|
||
this.MouseUp += OnMouseUp;
|
||
this.MouseMove += OnMouseMove;
|
||
this.MouseLeave += OnMouseLeave;
|
||
|
||
// 裁剪超出边界的内容
|
||
this.ClipToBounds = true;
|
||
|
||
// 初始化定时器,1秒3次 (约333ms)
|
||
_renderTimer = new DispatcherTimer(DispatcherPriority.Render);
|
||
_renderTimer.Interval = TimeSpan.FromMilliseconds(333);
|
||
_renderTimer.Tick += RenderTimer_Tick;
|
||
_renderTimer.Start();
|
||
}
|
||
|
||
private void RenderTimer_Tick(object sender, EventArgs e)
|
||
{
|
||
if (_needsRedraw)
|
||
{
|
||
RedrawAll();
|
||
_needsRedraw = false;
|
||
}
|
||
}
|
||
|
||
private SolidColorBrush CreateFrozenBrush(Color color)
|
||
{
|
||
var brush = new SolidColorBrush(color);
|
||
brush.Freeze();
|
||
return brush;
|
||
}
|
||
|
||
protected override int VisualChildrenCount => _visuals.Count;
|
||
|
||
protected override Visual GetVisualChild(int index)
|
||
{
|
||
if (index < 0 || index >= _visuals.Count)
|
||
throw new ArgumentOutOfRangeException();
|
||
return _visuals[index];
|
||
}
|
||
|
||
protected override void OnRender(DrawingContext drawingContext)
|
||
{
|
||
base.OnRender(drawingContext);
|
||
if (Background != null)
|
||
{
|
||
drawingContext.DrawRectangle(Background, null, new Rect(0, 0, ActualWidth, ActualHeight));
|
||
}
|
||
}
|
||
|
||
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
|
||
{
|
||
base.OnRenderSizeChanged(sizeInfo);
|
||
if (!_isViewChangedByUser && _rows > 0)
|
||
{
|
||
FitToScreen();
|
||
}
|
||
}
|
||
|
||
private void InitializeFromModel(DieMapModel model)
|
||
{
|
||
if (model == null) return;
|
||
|
||
_rows = model.Rows;
|
||
_columns = model.Columns;
|
||
_dieStates = model.States;
|
||
_dieCoordinates = model.Coordinates;
|
||
|
||
if (_dieCoordinates != null)
|
||
{
|
||
CalculateBounds();
|
||
}
|
||
|
||
// 重置变换
|
||
_transform.Matrix = Matrix.Identity;
|
||
|
||
// 延迟执行FitToScreen,确保控件已经布局完成
|
||
Dispatcher.BeginInvoke(new Action(() =>
|
||
{
|
||
if (ActualWidth > 0 && ActualHeight > 0)
|
||
{
|
||
FitToScreen();
|
||
}
|
||
else
|
||
{
|
||
RedrawAll();
|
||
}
|
||
}), DispatcherPriority.Loaded);
|
||
}
|
||
|
||
private void CalculateBounds()
|
||
{
|
||
_minX = double.MaxValue;
|
||
_maxX = double.MinValue;
|
||
_minY = double.MaxValue;
|
||
_maxY = double.MinValue;
|
||
|
||
for (int r = 0; r < _rows; r++)
|
||
{
|
||
for (int c = 0; c < _columns; c++)
|
||
{
|
||
var pt = _dieCoordinates[r, c];
|
||
if (pt.X < _minX) _minX = pt.X;
|
||
if (pt.X > _maxX) _maxX = pt.X;
|
||
if (pt.Y < _minY) _minY = pt.Y;
|
||
if (pt.Y > _maxY) _maxY = pt.Y;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void RedrawAll()
|
||
{
|
||
if (_rows == 0 || _columns == 0 || _dieStates == null) return;
|
||
|
||
using (DrawingContext dc = _visual.RenderOpen())
|
||
{
|
||
// 应用当前的缩放和平移变换
|
||
dc.PushTransform(_transform);
|
||
|
||
double size = DieSize;
|
||
double spacing = Spacing;
|
||
|
||
double physicalWidth = _maxX - _minX;
|
||
double physicalHeight = _maxY - _minY;
|
||
|
||
if (physicalWidth == 0) physicalWidth = 1;
|
||
if (physicalHeight == 0) physicalHeight = 1;
|
||
|
||
// 基础绘制区域大小(未缩放前)
|
||
double baseWidth = _columns * (size + spacing);
|
||
double baseHeight = _rows * (size + spacing);
|
||
|
||
for (int r = 0; r < _rows; r++)
|
||
{
|
||
for (int c = 0; c < _columns; c++)
|
||
{
|
||
DieState state = _dieStates[r, c];
|
||
if (state == DieState.NotExist) continue;
|
||
|
||
Brush brush = _stateBrushes.ContainsKey(state) ? _stateBrushes[state] : Brushes.Transparent;
|
||
if (brush == Brushes.Transparent) continue;
|
||
|
||
double x, y;
|
||
|
||
if (_dieCoordinates != null)
|
||
{
|
||
var pt = _dieCoordinates[r, c];
|
||
x = ((pt.X - _minX) / physicalWidth) * baseWidth;
|
||
y = (1.0 - (pt.Y - _minY) / physicalHeight) * baseHeight;
|
||
}
|
||
else
|
||
{
|
||
x = c * (size + spacing);
|
||
y = r * (size + spacing);
|
||
}
|
||
|
||
dc.DrawRectangle(brush, null, new Rect(x, y, size, size));
|
||
}
|
||
}
|
||
|
||
dc.Pop();
|
||
}
|
||
|
||
_needsRedraw = false;
|
||
}
|
||
|
||
#region 鼠标交互 (缩放与平移)
|
||
|
||
private void OnMouseWheel(object sender, MouseWheelEventArgs e)
|
||
{
|
||
Point mousePos = e.GetPosition(this);
|
||
double scaleFactor = e.Delta > 0 ? 1.1 : 1 / 1.1;
|
||
|
||
Matrix matrix = _transform.Matrix;
|
||
matrix.ScaleAt(scaleFactor, scaleFactor, mousePos.X, mousePos.Y);
|
||
_transform.Matrix = matrix;
|
||
_isViewChangedByUser = true;
|
||
|
||
RedrawAll();
|
||
e.Handled = true;
|
||
}
|
||
|
||
private void OnMouseDown(object sender, MouseButtonEventArgs e)
|
||
{
|
||
if (e.ChangedButton == MouseButton.Middle)
|
||
{
|
||
FitToScreen();
|
||
}
|
||
else if (e.ChangedButton == MouseButton.Left)
|
||
{
|
||
_mouseDownPosition = e.GetPosition(this);
|
||
_lastMousePosition = _mouseDownPosition;
|
||
_isDragging = true;
|
||
this.CaptureMouse();
|
||
_infoPopup.IsOpen = false;
|
||
}
|
||
}
|
||
|
||
private void OnMouseUp(object sender, MouseButtonEventArgs e)
|
||
{
|
||
if (e.ChangedButton == MouseButton.Left)
|
||
{
|
||
_isDragging = false;
|
||
this.ReleaseMouseCapture();
|
||
|
||
Point currentPosition = e.GetPosition(this);
|
||
if ((currentPosition - _mouseDownPosition).Length < 5)
|
||
{
|
||
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
|
||
{
|
||
ShowDieInfo(currentPosition);
|
||
}
|
||
else
|
||
{
|
||
HandleDieClick(currentPosition);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private void HandleDieClick(Point mousePos)
|
||
{
|
||
if (_rows == 0 || _columns == 0 || _dieStates == null) return;
|
||
|
||
if (!_transform.Matrix.HasInverse) return;
|
||
Matrix inverse = _transform.Matrix;
|
||
inverse.Invert();
|
||
Point canvasPos = inverse.Transform(mousePos);
|
||
|
||
double size = DieSize;
|
||
double spacing = Spacing;
|
||
double physicalWidth = _maxX - _minX;
|
||
double physicalHeight = _maxY - _minY;
|
||
if (physicalWidth == 0) physicalWidth = 1;
|
||
if (physicalHeight == 0) physicalHeight = 1;
|
||
|
||
double baseWidth = _columns * (size + spacing);
|
||
double baseHeight = _rows * (size + spacing);
|
||
|
||
int clickedRow = -1;
|
||
int clickedCol = -1;
|
||
|
||
for (int r = 0; r < _rows; r++)
|
||
{
|
||
for (int c = 0; c < _columns; c++)
|
||
{
|
||
DieState state = _dieStates[r, c];
|
||
if (state == DieState.NotExist) continue;
|
||
|
||
double x, y;
|
||
if (_dieCoordinates != null)
|
||
{
|
||
var pt = _dieCoordinates[r, c];
|
||
x = ((pt.X - _minX) / physicalWidth) * baseWidth;
|
||
y = (1.0 - (pt.Y - _minY) / physicalHeight) * baseHeight;
|
||
}
|
||
else
|
||
{
|
||
x = c * (size + spacing);
|
||
y = r * (size + spacing);
|
||
}
|
||
|
||
if (canvasPos.X >= x && canvasPos.X <= x + size &&
|
||
canvasPos.Y >= y && canvasPos.Y <= y + size)
|
||
{
|
||
clickedRow = r;
|
||
clickedCol = c;
|
||
break;
|
||
}
|
||
}
|
||
if (clickedRow != -1) break;
|
||
}
|
||
|
||
if (clickedRow != -1)
|
||
{
|
||
var dieInfo = (Row: clickedRow, Col: clickedCol, State: _dieStates[clickedRow, clickedCol]);
|
||
|
||
// 触发事件
|
||
DieClicked?.Invoke(this, new DieClickedEventArgs(clickedRow, clickedCol, _dieStates[clickedRow, clickedCol]));
|
||
|
||
// 触发命令
|
||
if (DieClickedCommand != null && DieClickedCommand.CanExecute(dieInfo))
|
||
{
|
||
DieClickedCommand.Execute(dieInfo);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void OnMouseMove(object sender, MouseEventArgs e)
|
||
{
|
||
if (_isDragging)
|
||
{
|
||
Point currentPosition = e.GetPosition(this);
|
||
Vector delta = currentPosition - _lastMousePosition;
|
||
|
||
Matrix matrix = _transform.Matrix;
|
||
matrix.Translate(delta.X, delta.Y);
|
||
_transform.Matrix = matrix;
|
||
_isViewChangedByUser = true;
|
||
|
||
_lastMousePosition = currentPosition;
|
||
RedrawAll();
|
||
}
|
||
}
|
||
|
||
private void OnMouseLeave(object sender, MouseEventArgs e)
|
||
{
|
||
if (_isDragging)
|
||
{
|
||
_isDragging = false;
|
||
this.ReleaseMouseCapture();
|
||
}
|
||
}
|
||
|
||
private void FitToScreen()
|
||
{
|
||
if (_rows == 0 || _columns == 0) return;
|
||
|
||
double size = DieSize;
|
||
double spacing = Spacing;
|
||
double baseWidth = _columns * (size + spacing);
|
||
double baseHeight = _rows * (size + spacing);
|
||
|
||
if (baseWidth == 0 || baseHeight == 0 || ActualWidth == 0 || ActualHeight == 0) return;
|
||
|
||
double scaleX = ActualWidth / baseWidth;
|
||
double scaleY = ActualHeight / baseHeight;
|
||
double scale = Math.Min(scaleX, scaleY) * 0.9; // 90% to leave some margin
|
||
|
||
if (scale <= 0) scale = 1;
|
||
|
||
_transform.Matrix = new Matrix(scale, 0, 0, scale,
|
||
(ActualWidth - baseWidth * scale) / 2,
|
||
(ActualHeight - baseHeight * scale) / 2);
|
||
_isViewChangedByUser = false;
|
||
|
||
RedrawAll();
|
||
}
|
||
|
||
private void ShowDieInfo(Point mousePos)
|
||
{
|
||
if (_rows == 0 || _columns == 0 || _dieStates == null) return;
|
||
|
||
if (!_transform.Matrix.HasInverse) return;
|
||
Matrix inverse = _transform.Matrix;
|
||
inverse.Invert();
|
||
Point canvasPos = inverse.Transform(mousePos);
|
||
|
||
double size = DieSize;
|
||
double spacing = Spacing;
|
||
double physicalWidth = _maxX - _minX;
|
||
double physicalHeight = _maxY - _minY;
|
||
if (physicalWidth == 0) physicalWidth = 1;
|
||
if (physicalHeight == 0) physicalHeight = 1;
|
||
|
||
double baseWidth = _columns * (size + spacing);
|
||
double baseHeight = _rows * (size + spacing);
|
||
|
||
int clickedRow = -1;
|
||
int clickedCol = -1;
|
||
|
||
for (int r = 0; r < _rows; r++)
|
||
{
|
||
for (int c = 0; c < _columns; c++)
|
||
{
|
||
DieState state = _dieStates[r, c];
|
||
if (state == DieState.NotExist) continue;
|
||
|
||
double x, y;
|
||
if (_dieCoordinates != null)
|
||
{
|
||
var pt = _dieCoordinates[r, c];
|
||
x = ((pt.X - _minX) / physicalWidth) * baseWidth;
|
||
y = (1.0 - (pt.Y - _minY) / physicalHeight) * baseHeight;
|
||
}
|
||
else
|
||
{
|
||
x = c * (size + spacing);
|
||
y = r * (size + spacing);
|
||
}
|
||
|
||
if (canvasPos.X >= x && canvasPos.X <= x + size &&
|
||
canvasPos.Y >= y && canvasPos.Y <= y + size)
|
||
{
|
||
clickedRow = r;
|
||
clickedCol = c;
|
||
break;
|
||
}
|
||
}
|
||
if (clickedRow != -1) break;
|
||
}
|
||
|
||
if (clickedRow != -1)
|
||
{
|
||
DieState state = _dieStates[clickedRow, clickedCol];
|
||
string info = $"行 (Row): {clickedRow}\n列 (Col): {clickedCol}\n状态: {MainShell.Converter.EnumHelper.GetEnumDescription(state)}";
|
||
if (_dieCoordinates != null)
|
||
{
|
||
var pt = _dieCoordinates[clickedRow, clickedCol];
|
||
info += $"\nX: {pt.X:F3}\nY: {pt.Y:F3}";
|
||
}
|
||
|
||
_infoText.Text = info;
|
||
_infoPopup.IsOpen = true;
|
||
}
|
||
else
|
||
{
|
||
_infoPopup.IsOpen = false;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
} |