windowcapture
исходный код / UI/FrozenOverlay.cs

FrozenOverlay.cs

511 строк · 21,201 байт · модуль UI
  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}