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

EditorForm.Zoom.cs

312 строк · 10,921 байт · модуль UI
  1using System;
  2using System.Collections.Generic;
  3using System.Drawing;
  4using System.Drawing.Drawing2D;
  5using System.Drawing.Imaging;
  6using System.Drawing.Text;
  7using System.IO;
  8using System.Linq;
  9using System.Runtime.InteropServices;
 10using System.Windows.Forms;
 11using WindowCapture.App;
 12using WindowCapture.Detection;
 13using WindowCapture.Effects;
 14using WindowCapture.Helpers;
 15using WindowCapture.Integration;
 16using WindowCapture.Models;
 17using WindowCapture.Native;
 18
 19namespace WindowCapture.UI
 20{
 21    public partial class EditorForm
 22    {
 23        // ===== Zoom, D2D, auto-detect, crop =====
 24
 25        private bool IsPathRectangular(GraphicsPath path, Rectangle pathBounds)
 26        {
 27            var pts = path.PathPoints;
 28            if (pts.Length != 4) return false;
 29
 30            foreach (var pt in pts)
 31            {
 32                bool atCorner = (Math.Abs(pt.X - pathBounds.Left) < 5 || Math.Abs(pt.X - pathBounds.Right) < 5) &&
 33                               (Math.Abs(pt.Y - pathBounds.Top) < 5 || Math.Abs(pt.Y - pathBounds.Bottom) < 5);
 34                if (!atCorner) return false;
 35            }
 36            return true;
 37        }
 38
 39        private void CreateBlurredBitmap()
 40        {
 41            blurredBitmap = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb);
 42            using (var g = Graphics.FromImage(blurredBitmap))
 43            {
 44                g.DrawImageUnscaled(captured, 0, 0);
 45                using (var dimBrush = new SolidBrush(Color.FromArgb(Settings.DimAlpha, 0, 0, 0)))
 46                    g.FillRectangle(dimBrush, 0, 0, bounds.Width, bounds.Height);
 47            }
 48        }
 49
 50        public void SetZoom(float level)
 51        {
 52            targetZoom = level;
 53            PerformanceLogger.Log("SetZoom", -1, string.Format("Target: {0:F2}", level));
 54            if (!zoomTimer.Enabled)
 55            {
 56                canvas.BeginZoomAnimation();
 57                zoomTimer.Start();
 58            }
 59        }
 60
 61        public void FitToWindow()
 62        {
 63            int maxW = scrollContainer.Width - 40;
 64            int maxH = scrollContainer.Height - 40;
 65            if (bounds.Width > 0 && bounds.Height > 0)
 66            {
 67                float scaleW = (float)maxW / bounds.Width;
 68                float scaleH = (float)maxH / bounds.Height;
 69                SetZoom(Math.Min(1.0f, Math.Min(scaleW, scaleH)));
 70            }
 71        }
 72
 73        public void UpdateAutoDetect(Point mousePos)
 74        {
 75            if (!altPressed) return;
 76
 77            var path = editorDetector.Detect(mousePos, 0);
 78            if (path != null)
 79            {
 80                autoDetectedRect = Rectangle.Ceiling(path.GetBounds());
 81                path.Dispose();
 82            }
 83            else
 84            {
 85                autoDetectedRect = Rectangle.Empty;
 86            }
 87            canvas.Invalidate();
 88        }
 89
 90        public void SetCropRect(Rectangle rect)
 91        {
 92            cropRect = rect;
 93            if (canvas != null) canvas.Invalidate();
 94        }
 95
 96        // Get scaled image size (единый метод для избежания рассогласования)
 97        public Size GetScaledImageSize()
 98        {
 99            return new Size(
100                (int)Math.Round(bounds.Width * zoomLevel),
101                (int)Math.Round(bounds.Height * zoomLevel));
102        }
103
104        private void InitializeDirect2D()
105        {
106            // Delay init until canvas has a handle
107            if (!canvas.IsHandleCreated)
108            {
109                canvas.HandleCreated += delegate { InitializeDirect2D(); };
110                return;
111            }
112
113            try
114            {
115                // In Static Viewport, D2D target is always viewport size
116                int vw = scrollContainer.ClientSize.Width;
117                int vh = scrollContainer.ClientSize.Height;
118                if (vw < 1) vw = 1; if (vh < 1) vh = 1;
119
120                bool ok = D2DRenderer.Initialize(canvas.Handle, vw, vh);
121                if (ok && captured != null)
122                {
123                    D2DRenderer.UploadBitmap(captured);
124                    canvas.SetD2DActive(true);
125                }
126                System.Diagnostics.Debug.WriteLine("D2D init: " + (ok ? "OK" : "FAILED (using GDI+ fallback)"));
127            }
128            catch (Exception ex)
129            {
130                System.Diagnostics.Debug.WriteLine("D2D init exception: " + ex.Message);
131            }
132        }
133
134        /// <summary>Re-create the D2D device after a device-loss (GPU reset / RDP / driver update).
135        /// Called by the canvas when it notices GPU rendering went unavailable mid-session.</summary>
136        public void TryRecoverD2D()
137        {
138            InitializeDirect2D();
139        }
140
141        private Point zoomMousePos; // Screen coordinates for stable reference
142
143        public void ApplyZoom(float delta, Point canvasMousePos = default(Point))
144        {
145            float direction = delta > 0 ? 1f : -1f;
146            float newTarget = targetZoom * (float)Math.Pow(zoomFactor, direction);
147            
148            // Limit to min/max
149            newTarget = Math.Max(zoomMin, Math.Min(zoomMax, newTarget));
150
151            if (Math.Abs(newTarget - targetZoom) < 0.0001f && !zoomTimer.Enabled) return;
152
153            targetZoom = newTarget;
154
155            // Only capture mouse position on first scroll (when animation not running)
156            if (!zoomTimer.Enabled && canvasMousePos != default(Point))
157            {
158                zoomMousePos = canvasMousePos;
159                canvas.BeginZoomAnimation();
160                zoomTimer.Start();
161            }
162            else if (!zoomTimer.Enabled)
163            {
164                zoomMousePos = default(Point);
165                canvas.BeginZoomAnimation();
166                zoomTimer.Start();
167            }
168        }
169
170        private void ZoomTimer_Tick(object sender, EventArgs e)
171        {
172            // Exponential interpolation for smoother feel
173            float diff = targetZoom - zoomLevel;
174
175            if (Math.Abs(diff) < 0.001f * zoomLevel) // scaled precision for higher zooms
176            {
177                zoomLevel = targetZoom;
178                zoomTimer.Stop();
179                canvas.EndZoomAnimation();
180            }
181            else
182            {
183                // Smooth interpolation proportional to current zoom
184                zoomLevel += diff * ZoomInterpolationFactor;
185            }
186
187            ApplyZoomInternal();
188        }
189
190        private void UpdateCanvasSize()
191        {
192            canvas.Size = GetScaledImageSize();
193            // Resize D2D render target to match canvas
194            if (D2DRenderer.IsAvailable)
195            {
196                D2DRenderer.Resize(canvas.Width, canvas.Height);
197            }
198        }
199
200        private void ApplyZoomInternal()
201        {
202            if (IsDisposed || scrollContainer == null || !scrollContainer.IsHandleCreated || scrollContainer.IsDisposed || canvas == null || canvas.IsDisposed) return;
203
204            int vw = scrollContainer.ClientSize.Width;
205            int vh = scrollContainer.ClientSize.Height;
206            if (vw < 10) vw = Width; // Fallback if not layouted yet
207            if (vh < 10) vh = Height;
208
209            // Target image size at current zoomLevel
210            float imgW = bounds.Width * zoomLevel;
211            float imgH = bounds.Height * zoomLevel;
212
213            // GLUED TO CENTER LOGIC:
214            // Calculate offsets so the center of the image always matches the center of the viewport.
215            // This eliminates the 'top-left' jump and the 'cursor drift'.
216            canvasDrawX = (vw - imgW) / 2f;
217            canvasDrawY = (vh - imgH) / 2f;
218            canvasDrawScale = zoomLevel;
219
220            // GPU Target management
221            if (D2DRenderer.IsAvailable && (D2DRenderer.TargetWidth != vw || D2DRenderer.TargetHeight != vh))
222            {
223                D2DRenderer.Resize(vw, vh);
224            }
225
226            // UNIFIED GPU TRANSFORM
227            canvas.SetViewportTransform(canvasDrawScale, canvasDrawX, canvasDrawY);
228
229            ShowZoomIndicator();
230        }
231
232        private float canvasDrawX = 0f;
233        private float canvasDrawY = 0f;
234        private float canvasDrawScale = 1.0f;
235
236        // Direct zoom for keyboard (без анимации для точного контроля)
237        public void ApplyZoomDirect(float delta)
238        {
239            targetZoom = Math.Max(zoomMin, Math.Min(zoomMax, zoomLevel + delta));
240            zoomLevel = targetZoom;
241            zoomMousePos = default(Point);
242            ApplyZoomInternal();
243        }
244
245        private void ShowZoomIndicator()
246        {
247            zoomIndicatorAlpha = 1.0f;
248            zoomLastChangeTime = DateTime.Now;
249            if (!zoomFadeTimer.Enabled)
250                zoomFadeTimer.Start();
251        }
252
253        private void ZoomFadeTimer_Tick(object sender, EventArgs e)
254        {
255            double elapsed = (DateTime.Now - zoomLastChangeTime).TotalMilliseconds;
256
257            if (elapsed < ZoomFadeDelayMs)
258            {
259                // Still showing — keep alpha at 1
260                zoomIndicatorAlpha = 1.0f;
261            }
262            else
263            {
264                // Fading out
265                float fadeProgress = (float)(elapsed - ZoomFadeDelayMs) / ZoomFadeDurationMs;
266                zoomIndicatorAlpha = Math.Max(0f, 1.0f - fadeProgress);
267            }
268
269            if (zoomIndicatorAlpha <= 0f)
270            {
271                zoomIndicatorAlpha = 0f;
272                zoomFadeTimer.Stop();
273            }
274
275            if (scrollContainer != null && scrollContainer.IsHandleCreated)
276            {
277                var r = new Rectangle(0, 0, 150, 80); // top-left area for indicator
278                scrollContainer.Invalidate(r);
279                
280                if (canvas != null && canvas.IsHandleCreated)
281                {
282                    var cr = new Rectangle(r.X - canvas.Left, r.Y - canvas.Top, r.Width, r.Height);
283                    if (canvas.ClientRectangle.IntersectsWith(cr))
284                        canvas.Invalidate(cr);
285                }
286            }
287        }
288
289        private void CenterImage()
290        {
291            if (canvas == null || bounds.Width <= 0) return;
292
293            int vw = scrollContainer.ClientSize.Width;
294            int vh = scrollContainer.ClientSize.Height;
295            if (vw < 10) vw = Width;
296            if (vh < 10) vh = Height;
297
298            float imgW = bounds.Width * zoomLevel;
299            float imgH = bounds.Height * zoomLevel;
300
301            canvasDrawX = (vw - imgW) / 2f;
302            canvasDrawY = (vh - imgH) / 2f;
303            canvasDrawScale = zoomLevel;
304
305            // Keep D2D render target in sync with viewport size
306            if (D2DRenderer.IsAvailable && (D2DRenderer.TargetWidth != vw || D2DRenderer.TargetHeight != vh))
307                D2DRenderer.Resize(vw, vh);
308
309            canvas.SetViewportTransform(canvasDrawScale, canvasDrawX, canvasDrawY);
310        }
311    }
312}