// GuideOverlayWindow.xaml.cs using System; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; using System.Windows.Media.Imaging; using System.IO; using System.Windows.Input; namespace VisionCommonUI.UserGuide { public partial class GuideOverlayWindow : Window { public Action OnNextClicked; public Action OnPrevClicked; public Action OnExitClicked; public bool IsClosing { get; private set; } = false; protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { IsClosing = true; base.OnClosing(e); } private RectangleGeometry _highlightGeometry; public GuideOverlayWindow() { InitializeComponent(); Loaded += (_, __) => { Mouse.Capture(this, CaptureMode.SubTree); }; Deactivated += (_, __) => OnExitClicked?.Invoke(); } public void Highlight(GuideStep step) { var area = step.HighlightArea; double x = Math.Floor(area.X); double y = Math.Floor(area.Y); double w = Math.Ceiling(area.Width); double h = Math.Ceiling(area.Height); double canvasW = Math.Ceiling(RootCanvas.ActualWidth); double canvasH = Math.Ceiling(RootCanvas.ActualHeight); // Top OverlayTop.Width = canvasW; OverlayTop.Height = y; Canvas.SetLeft(OverlayTop, 0); Canvas.SetTop(OverlayTop, 0); // Bottom OverlayBottom.Width = canvasW; OverlayBottom.Height = canvasH - (y + h); Canvas.SetLeft(OverlayBottom, 0); Canvas.SetTop(OverlayBottom, y + h); // Left OverlayLeft.Width = x; OverlayLeft.Height = h; Canvas.SetLeft(OverlayLeft, 0); Canvas.SetTop(OverlayLeft, y); // Right OverlayRight.Width = canvasW - (x + w); OverlayRight.Height = h; Canvas.SetLeft(OverlayRight, x + w); Canvas.SetTop(OverlayRight, y); HighlightRect.Width = w; HighlightRect.Height = h; Canvas.SetLeft(HighlightRect, x); Canvas.SetTop(HighlightRect, y); MessageText.Text = step.Message; MessageText.FontSize = step.FontSize; MessageText.Width = this.ActualWidth; MessageText.TextAlignment = TextAlignment.Left; MessageText.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); var textSize = MessageText.DesiredSize; Point textPosition = (step.Placement == MessagePlacement.Above) ? new Point(area.X, Math.Max(area.Y - textSize.Height - 10, 0)) : new Point(area.X, area.Bottom + 10); Canvas.SetLeft(MessageText, textPosition.X); Canvas.SetTop(MessageText, textPosition.Y); if (step.Imagesc != null) { MessageImage.Source = step.Imagesc; MessageImage.Visibility = Visibility.Visible; // 초기 크기 설정 double baseWidth = this.ActualWidth * 0.7; double baseHeight = baseWidth * 0.66; Size imageSize = new Size(baseWidth, baseHeight); // 적절한 위치 계산 Point textPos = new Point(Canvas.GetLeft(MessageText), Canvas.GetTop(MessageText)); var smartPos = GetSmartImagePosition(step.HighlightArea, textPos, imageSize, out Size finalSize); MessageImage.Width = finalSize.Width; MessageImage.Height = finalSize.Height; Canvas.SetLeft(MessageImage, smartPos.X); Canvas.SetTop(MessageImage, smartPos.Y); } else { MessageImage.Visibility = Visibility.Collapsed; } } private Point GetSmartImagePosition(Rect highlight, Point textPosition, Size originalSize, out Size finalSize) { double canvasWidth = RootCanvas.ActualWidth; double canvasHeight = RootCanvas.ActualHeight; double padding = 10; double textBottom = textPosition.Y + MessageText.DesiredSize.Height; var below = new Point(highlight.X, textBottom + padding); var right = new Point(highlight.Right + padding, highlight.Top); var top = new Point(highlight.X, highlight.Y - originalSize.Height - padding); // 기본: 크기 유지 finalSize = originalSize; bool FitsBelow = below.Y + originalSize.Height < canvasHeight; bool FitsRight = right.X + originalSize.Width < canvasWidth; bool FitsTop = top.Y > 0; if (FitsBelow) { return below; } if (FitsRight) { return right; } if (FitsTop) { return top; } // 축소 시도 double scale = 0.8; while (scale > 0.3) { var scaled = new Size(originalSize.Width * scale, originalSize.Height * scale); if (textBottom + padding + scaled.Height < canvasHeight) { finalSize = scaled; return new Point(highlight.X, textBottom + padding); } scale -= 0.1; } // Fallback finalSize = new Size(200, 150); return new Point(10, 10); } private void Grid_MouseDown(object sender, MouseButtonEventArgs e) { if (e.OriginalSource is DependencyObject source && IsInsideGuideOverlay(source)) { OnNextClicked?.Invoke(); } else { OnExitClicked?.Invoke(); } } private void Grid_PreviewMouseWheel(object sender, MouseWheelEventArgs e) { if (e.Delta > 0) { OnPrevClicked?.Invoke(); e.Handled = true; } } private bool IsInsideGuideOverlay(DependencyObject source) { while (source != null) { if (source == this) { return true; } source = VisualTreeHelper.GetParent(source); } return false; } private void Window_PreviewMouseDown(object sender, MouseButtonEventArgs e) { Point clickPos = e.GetPosition(RootCanvas); // HitTest 대상 찾기 HitTestResult result = VisualTreeHelper.HitTest(RootCanvas, clickPos); FrameworkElement hit = result != null ? result.VisualHit as FrameworkElement : null; if (hit == null || string.IsNullOrEmpty(hit.Name)) { OnExitClicked?.Invoke(); } } } }