1using System; 2using System.Collections.Generic; 3using System.Drawing; 4using System.Drawing.Drawing2D; 5using System.Windows.Forms; 6using WindowCapture.Native; 7 8namespace WindowCapture.UI 9{ 10 public class FrozenOverlay : Form 11 { 12 private Bitmap screenshot; 13 private GraphicsPath regionPath; 14 private Timer pulseTimer; 15 private DateTime animationStart; 16 private int expandLevel; 17 private Label levelLabel; 18 19 // Manual rectangle selection with RMB 20 private bool isDrawingManualRect; 21 private Point manualRectStart; 22 private Rectangle manualRect; 23 24 // Multi-region selection with Alt+LMB drag 25 private bool isAddingRegions; 26 private bool altPressedOnMouseDown; // Track if Alt was pressed when LMB was clicked 27 private List<GraphicsPath> additionalPaths = new List<GraphicsPath>(); 28 29 // Window selection mode (MMB) 30 private bool windowSelectMode; 31 private Rectangle windowRect; 32 33 // Media parser mode (RMB held + LMB) 34 private bool mediaParseMode; 35 private GraphicsPath mediaParsePath; 36 private Point mediaParsePoint; 37 38 public event EventHandler<int> WheelScrolled; // +1 up, -1 down 39 public event EventHandler<Rectangle> ManualRectSelected; // when manual rect is finished 40 public event EventHandler<Rectangle> WindowSelected; // when window is selected with MMB 41 public event Action<Point> AddRegionRequested; // when dragging with LMB to add regions 42 public event Action<Point> MediaParseRequested; // when RMB+LMB to parse media 43 public event Func<Point, GraphicsPath> MediaParseRegionRequested; // request region detection for media parse 44 45 public bool IsVideoMode { get; set; } 46 47 public FrozenOverlay() 48 { 49 FormBorderStyle = FormBorderStyle.None; 50 ShowInTaskbar = false; 51 TopMost = true; 52 StartPosition = FormStartPosition.Manual; 53 Bounds = SystemInformation.VirtualScreen; // cover all monitors (re-set on each show) 54 DoubleBuffered = true; 55 Cursor = Cursors.Cross; 56 57 animationStart = DateTime.Now; 58 pulseTimer = new Timer(); 59 pulseTimer.Interval = 16; // ~60 FPS for smooth animation 60 pulseTimer.Tick += delegate { InvalidateRegion(); }; 61 62 // Level indicator 63 levelLabel = new Label(); 64 levelLabel.AutoSize = true; 65 levelLabel.BackColor = Color.FromArgb(200, 30, 30, 30); 66 levelLabel.ForeColor = Color.White; 67 levelLabel.Font = new Font("Segoe UI", 10f); 68 levelLabel.Padding = new Padding(8, 4, 8, 4); 69 levelLabel.Visible = false; 70 Controls.Add(levelLabel); 71 } 72 73 protected override CreateParams CreateParams 74 { 75 get 76 { 77 var cp = base.CreateParams; 78 cp.ExStyle |= WinApi.WS_EX_TOPMOST | WinApi.WS_EX_TOOLWINDOW; 79 return cp; 80 } 81 } 82 83 public void ShowWithScreenshot(Bitmap bmp) 84 { 85 // Match the current virtual desktop so overlay client coords align with the frozen bitmap. 86 Bounds = SystemInformation.VirtualScreen; 87 screenshot = bmp; 88 regionPath = null; 89 animationStart = DateTime.Now; 90 expandLevel = 0; 91 levelLabel.Visible = false; 92 isDrawingManualRect = false; 93 manualRect = Rectangle.Empty; 94 isAddingRegions = false; 95 foreach (var p in additionalPaths) p.Dispose(); 96 additionalPaths.Clear(); 97 windowSelectMode = false; 98 windowRect = Rectangle.Empty; 99 mediaParseMode = false; 100 if (mediaParsePath != null) { mediaParsePath.Dispose(); mediaParsePath = null; } 101 lastRegionBounds = Rectangle.Empty; 102 pulseTimer.Start(); 103 Show(); 104 BringToFront(); 105 Invalidate(ClientRectangle); // one full paint so the static dim covers the whole desktop 106 } 107 108 // Dirty-rect invalidation: the dim background is static, only the selection region pulses. 109 // Repainting just the region area (instead of the whole virtual desktop) every frame is the 110 // key to smooth capture on multi-monitor / large screens. 111 private Rectangle lastRegionBounds = Rectangle.Empty; 112 113 private Rectangle ActiveRegionBounds() 114 { 115 Rectangle r = Rectangle.Empty; 116 if (isDrawingManualRect && manualRect.Width > 0 && manualRect.Height > 0) r = manualRect; 117 else if (windowSelectMode && windowRect.Width > 0 && windowRect.Height > 0) r = windowRect; 118 else if (regionPath != null) 119 { 120 r = Rectangle.Round(regionPath.GetBounds()); 121 foreach (var p in additionalPaths) 122 { 123 var pb = Rectangle.Round(p.GetBounds()); 124 r = r.IsEmpty ? pb : Rectangle.Union(r, pb); 125 } 126 } 127 if (!r.IsEmpty) r.Inflate(40, 40); // border + pulse + safety margin 128 return r; 129 } 130 131 /// <summary>Invalidate only the selection area (union of its old and new bounds) instead of 132 /// the whole overlay. Media-parse mode draws an off-region label, so it falls back to full.</summary> 133 private void InvalidateRegion() 134 { 135 if (mediaParseMode) { Invalidate(ClientRectangle); lastRegionBounds = Rectangle.Empty; return; } 136 var cur = ActiveRegionBounds(); 137 Rectangle inv = lastRegionBounds; 138 if (!cur.IsEmpty) inv = inv.IsEmpty ? cur : Rectangle.Union(inv, cur); 139 lastRegionBounds = cur; 140 if (inv.IsEmpty) return; 141 inv.Intersect(ClientRectangle); 142 if (inv.Width > 0 && inv.Height > 0) Invalidate(inv); 143 } 144 145 public void SetRegion(GraphicsPath path, int level) 146 { 147 if (regionPath != null) regionPath.Dispose(); 148 regionPath = path; 149 expandLevel = level; 150 151 if (level > 0) 152 { 153 levelLabel.Text = "Level: " + level; 154 levelLabel.Location = new Point(10, 10); 155 levelLabel.Visible = true; 156 } 157 else 158 { 159 levelLabel.Visible = false; 160 } 161 162 InvalidateRegion(); 163 } 164 165 public void Reset() 166 { 167 pulseTimer.Stop(); 168 if (regionPath != null) { regionPath.Dispose(); regionPath = null; } 169 if (screenshot != null) { screenshot.Dispose(); screenshot = null; } 170 foreach (var p in additionalPaths) p.Dispose(); 171 additionalPaths.Clear(); 172 levelLabel.Visible = false; 173 isDrawingManualRect = false; 174 manualRect = Rectangle.Empty; 175 isAddingRegions = false; 176 windowSelectMode = false; 177 windowRect = Rectangle.Empty; 178 mediaParseMode = false; 179 if (mediaParsePath != null) { mediaParsePath.Dispose(); mediaParsePath = null; } 180 Hide(); 181 } 182 183 protected override void OnMouseDown(MouseEventArgs e) 184 { 185 base.OnMouseDown(e); 186 if (e.Button == MouseButtons.Right) 187 { 188 // Start manual rectangle drawing 189 isDrawingManualRect = true; 190 manualRectStart = e.Location; 191 manualRect = Rectangle.Empty; 192 InvalidateRegion(); 193 } 194 else if (e.Button == MouseButtons.Left) 195 { 196 // Check if RMB is also held - media parse mode 197 if (isDrawingManualRect) 198 { 199 // RMB + LMB = media parser mode 200 isDrawingManualRect = false; 201 manualRect = Rectangle.Empty; 202 mediaParseMode = true; 203 mediaParsePoint = e.Location; 204 UpdateMediaParsePath(e.Location); 205 InvalidateRegion(); 206 return; 207 } 208 209 // Check if Alt is pressed - only then start multi-region mode 210 altPressedOnMouseDown = (Control.ModifierKeys & Keys.Alt) != 0; 211 if (altPressedOnMouseDown) 212 { 213 // Start multi-region selection (drag to add more regions) 214 isAddingRegions = true; 215 } 216 // Without Alt - single click will capture the current region (handled by MouseClick event) 217 } 218 else if (e.Button == MouseButtons.Middle) 219 { 220 // Window selection mode 221 windowSelectMode = true; 222 UpdateWindowRect(e.Location); 223 InvalidateRegion(); 224 } 225 } 226 227 private void UpdateMediaParsePath(Point screenPt) 228 { 229 // Request region detection from controller (uses same Detector as normal mode) 230 if (MediaParseRegionRequested != null) 231 { 232 var newPath = MediaParseRegionRequested(screenPt); 233 if (mediaParsePath != null) mediaParsePath.Dispose(); 234 mediaParsePath = newPath; 235 } 236 } 237 238 protected override void OnMouseMove(MouseEventArgs e) 239 { 240 base.OnMouseMove(e); 241 if (mediaParseMode) 242 { 243 // Update media parse path (detected region) 244 mediaParsePoint = e.Location; 245 UpdateMediaParsePath(e.Location); 246 InvalidateRegion(); 247 } 248 else if (isDrawingManualRect) 249 { 250 // Update manual rectangle 251 int x = Math.Min(manualRectStart.X, e.X); 252 int y = Math.Min(manualRectStart.Y, e.Y); 253 int w = Math.Abs(e.X - manualRectStart.X); 254 int h = Math.Abs(e.Y - manualRectStart.Y); 255 manualRect = new Rectangle(x, y, w, h); 256 InvalidateRegion(); 257 } 258 else if (isAddingRegions && altPressedOnMouseDown && regionPath != null) 259 { 260 // Request to add region at this point (only when Alt+LMB drag) 261 if (AddRegionRequested != null) 262 AddRegionRequested(e.Location); 263 } 264 else if (windowSelectMode) 265 { 266 UpdateWindowRect(e.Location); 267 InvalidateRegion(); 268 } 269 } 270 271 private void UpdateWindowRect(Point clientPt) 272 { 273 // FindAppWindowAtPoint / GetWindowRectangle work in SCREEN coords, but everything else 274 // here (drawing, region path, editor crop) is in overlay-client coords. Convert in, then 275 // convert the result back to client coords by subtracting the overlay's screen origin. 276 var scr = PointToScreen(clientPt); 277 WinApi.POINT pt; 278 pt.X = scr.X; 279 pt.Y = scr.Y; 280 281 // Use FindAppWindowAtPointExcluding to exclude this overlay from detection 282 IntPtr hwnd = WinApi.FindAppWindowAtPointExcluding(pt, this.Handle); 283 if (hwnd != IntPtr.Zero) 284 { 285 var r = WinApi.GetWindowRectangle(hwnd); // screen coords 286 windowRect = new Rectangle(r.X - Left, r.Y - Top, r.Width, r.Height); // → client coords 287 } 288 } 289 290 public void AddPath(GraphicsPath path) 291 { 292 if (path != null) 293 additionalPaths.Add(path); 294 InvalidateRegion(); 295 } 296 297 protected override void OnMouseUp(MouseEventArgs e) 298 { 299 base.OnMouseUp(e); 300 if (e.Button == MouseButtons.Left && mediaParseMode) 301 { 302 // LMB released in media parse mode - trigger parse 303 mediaParseMode = false; 304 if (MediaParseRequested != null) 305 MediaParseRequested(mediaParsePoint); 306 if (mediaParsePath != null) { mediaParsePath.Dispose(); mediaParsePath = null; } 307 InvalidateRegion(); 308 } 309 else if (e.Button == MouseButtons.Right) 310 { 311 if (isDrawingManualRect) 312 { 313 isDrawingManualRect = false; 314 if (manualRect.Width > 5 && manualRect.Height > 5) 315 { 316 // Fire event with the selected rectangle 317 if (ManualRectSelected != null) 318 ManualRectSelected(this, manualRect); 319 } 320 manualRect = Rectangle.Empty; 321 InvalidateRegion(); 322 } 323 // Also cancel media parse mode if RMB released 324 if (mediaParseMode) 325 { 326 mediaParseMode = false; 327 if (mediaParsePath != null) { mediaParsePath.Dispose(); mediaParsePath = null; } 328 InvalidateRegion(); 329 } 330 } 331 else if (e.Button == MouseButtons.Left) 332 { 333 isAddingRegions = false; 334 } 335 else if (e.Button == MouseButtons.Middle && windowSelectMode) 336 { 337 windowSelectMode = false; 338 if (windowRect.Width > 5 && windowRect.Height > 5) 339 { 340 if (WindowSelected != null) 341 WindowSelected(this, windowRect); 342 } 343 windowRect = Rectangle.Empty; 344 InvalidateRegion(); 345 } 346 } 347 348 protected override void OnMouseWheel(MouseEventArgs e) 349 { 350 base.OnMouseWheel(e); 351 if (!isDrawingManualRect && WheelScrolled != null) 352 { 353 int delta = e.Delta > 0 ? 1 : -1; 354 WheelScrolled(this, delta); 355 } 356 } 357 358 public bool IsDrawingManualRect { get { return isDrawingManualRect; } } 359 public bool IsWindowSelectMode { get { return windowSelectMode; } } 360 public bool IsMediaParseMode { get { return mediaParseMode; } } 361 362 protected override void OnPaint(PaintEventArgs e) 363 { 364 var g = e.Graphics; 365 if (screenshot != null) 366 g.DrawImageUnscaled(screenshot, 0, 0); 367 368 double elapsed = (DateTime.Now - animationStart).TotalSeconds; 369 double t = (elapsed % 1.0); 370 double pulse = Math.Sin(t * Math.PI); 371 pulse = pulse * pulse; 372 int alpha = (int)(15 + pulse * 25); 373 374 // Build combined region to exclude from dimming 375 Region excludeRegion = null; 376 377 // Media parse mode (purple) - uses GraphicsPath like normal detection 378 if (mediaParseMode && mediaParsePath != null) 379 { 380 excludeRegion = new Region(mediaParsePath); 381 } 382 // Manual rect (green) 383 else if (isDrawingManualRect && manualRect.Width > 0 && manualRect.Height > 0) 384 { 385 excludeRegion = new Region(manualRect); 386 } 387 // Window rect (orange) 388 else if (windowSelectMode && windowRect.Width > 0 && windowRect.Height > 0) 389 { 390 excludeRegion = new Region(windowRect); 391 } 392 // Auto-detected region (cyan) 393 else if (regionPath != null) 394 { 395 excludeRegion = new Region(regionPath); 396 // Add additional paths (multi-select) 397 foreach (var p in additionalPaths) 398 excludeRegion.Union(p); 399 } 400 401 // Draw dimmed overlay 402 using (var dimBrush = new SolidBrush(Color.FromArgb(40, 0, 0, 0))) 403 { 404 if (excludeRegion != null) 405 { 406 var screenRegion = new Region(new Rectangle(0, 0, Width, Height)); 407 screenRegion.Exclude(excludeRegion); 408 g.FillRegion(dimBrush, screenRegion); 409 screenRegion.Dispose(); 410 } 411 else 412 { 413 g.FillRectangle(dimBrush, 0, 0, Width, Height); 414 } 415 } 416 417 g.SmoothingMode = SmoothingMode.None; 418 419 // Draw media parse path (purple - pulsing) - element under cursor 420 if (mediaParseMode && mediaParsePath != null) 421 { 422 // Faster pulse for media mode 423 double fastPulse = Math.Sin(elapsed * 3 * Math.PI); 424 fastPulse = fastPulse * fastPulse; 425 int purpleAlpha = (int)(10 + fastPulse * 20); 426 427 Color fill = Color.FromArgb(purpleAlpha, 180, 100, 255); 428 Color border = Color.FromArgb(purpleAlpha + 40, 180, 100, 255); 429 using (var brush = new SolidBrush(fill)) 430 g.FillPath(brush, mediaParsePath); 431 using (var pen = new Pen(border, 2f)) 432 g.DrawPath(pen, mediaParsePath); 433 434 // Draw label above the path bounds 435 string label = "Media Parser - Release LMB to scan"; 436 var pathBounds = mediaParsePath.GetBounds(); 437 using (var font = new Font("Segoe UI", 11f, FontStyle.Bold)) 438 using (var bgBrush = new SolidBrush(Color.FromArgb(200, 40, 20, 60))) 439 using (var textBrush = new SolidBrush(Color.FromArgb(255, 200, 150, 255))) 440 { 441 var size = g.MeasureString(label, font); 442 int labelX = (int)pathBounds.X + ((int)pathBounds.Width - (int)size.Width) / 2; 443 int labelY = (int)pathBounds.Y - 30; 444 if (labelY < 5) labelY = (int)pathBounds.Bottom + 5; 445 446 g.FillRectangle(bgBrush, labelX - 5, labelY - 2, size.Width + 10, size.Height + 4); 447 g.DrawString(label, font, textBrush, labelX, labelY); 448 } 449 } 450 // Draw manual rect 451 else if (isDrawingManualRect && manualRect.Width > 0 && manualRect.Height > 0) 452 { 453 Color fill = IsVideoMode ? Color.FromArgb(alpha, 255, 80, 80) : Color.FromArgb(alpha, 100, 220, 100); 454 Color border = IsVideoMode ? Color.FromArgb(alpha + 50, 255, 100, 100) : Color.FromArgb(alpha + 50, 100, 220, 100); 455 using (var brush = new SolidBrush(fill)) 456 g.FillRectangle(brush, manualRect); 457 using (var pen = new Pen(border, 2f)) 458 g.DrawRectangle(pen, manualRect); 459 } 460 // Draw window rect 461 else if (windowSelectMode && windowRect.Width > 0 && windowRect.Height > 0) 462 { 463 Color fill = IsVideoMode ? Color.FromArgb(alpha, 255, 50, 50) : Color.FromArgb(alpha, 255, 180, 80); 464 Color border = IsVideoMode ? Color.FromArgb(alpha + 50, 255, 80, 80) : Color.FromArgb(alpha + 50, 255, 180, 80); 465 using (var brush = new SolidBrush(fill)) 466 g.FillRectangle(brush, windowRect); 467 using (var pen = new Pen(border, 2f)) 468 g.DrawRectangle(pen, windowRect); 469 } 470 // Draw auto-detected region 471 else if (regionPath != null) 472 { 473 Color fill = IsVideoMode ? Color.FromArgb(alpha, 255, 40, 40) : Color.FromArgb(alpha, 80, 200, 255); 474 Color border = IsVideoMode ? Color.FromArgb(alpha + 50, 255, 60, 60) : Color.FromArgb(alpha + 50, 80, 200, 255); 475 476 using (var brush = new SolidBrush(fill)) 477 g.FillPath(brush, regionPath); 478 using (var pen = new Pen(border, 2f)) 479 g.DrawPath(pen, regionPath); 480 481 // Draw additional paths 482 Color fillAdd = IsVideoMode ? Color.FromArgb(alpha, 255, 100, 100) : Color.FromArgb(alpha, 200, 220, 100); 483 Color borderAdd = IsVideoMode ? Color.FromArgb(alpha + 50, 255, 120, 120) : Color.FromArgb(alpha + 50, 200, 220, 100); 484 foreach (var p in additionalPaths) 485 { 486 using (var brush = new SolidBrush(fillAdd)) 487 g.FillPath(brush, p); 488 using (var pen = new Pen(borderAdd, 2f)) 489 g.DrawPath(pen, p); 490 } 491 } 492 493 if (excludeRegion != null) 494 excludeRegion.Dispose(); 495 } 496 497 protected override void Dispose(bool disposing) 498 { 499 if (disposing) 500 { 501 if (pulseTimer != null) pulseTimer.Dispose(); 502 if (levelLabel != null) levelLabel.Dispose(); 503 if (screenshot != null) screenshot.Dispose(); 504 if (regionPath != null) regionPath.Dispose(); 505 if (mediaParsePath != null) mediaParsePath.Dispose(); 506 foreach (var p in additionalPaths) p.Dispose(); 507 } 508 base.Dispose(disposing); 509 } 510 } 511}