From f7aa107a62c651ee006787b0784e6e0863ef14b4 Mon Sep 17 00:00:00 2001 From: tayasui rainnya! <156585442+Tayasui-rainnya@users.noreply.github.com> Date: Sat, 28 Mar 2026 17:11:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=80=89=E5=8C=BA=E6=88=AA=E5=9B=BE?= =?UTF-8?q?=E6=97=B6=E4=BF=9D=E7=95=99=E5=B1=8F=E5=B9=95=E7=AC=94=E8=BF=B9?= =?UTF-8?q?=20(#406)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 使用选区截图时,不清除 Strokes(Keep it on screen) * fix: 浮动栏选区截图前强制保持墨迹可见 * fix: 避免选区截图回滚 inkCanvas 运行时状态 * fix: 截图前退出并在结束后恢复批注状态 * fix: 截图流程改用轻量批注暂停避免副作用 * feat: 选区截图添加包含墨迹开关 * fix: 避免选区截图墨迹重复渲染 * fix: 全屏基础截图排除主窗口后再叠加墨迹 * fix: 隐藏浮动栏后再进入选区截图 * fix: 添加到白板时不强制恢复浮动栏可见性 * fix: 防止重复启动选区截图实例 * fix: 仅在白板接管成功后跳过浮动栏恢复 * feat: 选区截图时实时预览包含墨迹开关 * fix: 合并截图选择器OnClosed逻辑避免重复定义 --- Ink Canvas/MainWindow_cs/MW_ImageInsert.cs | 129 +++++++++++++++++- Ink Canvas/MainWindow_cs/MW_Screenshot.cs | 120 ++++++++++++++-- .../Windows/ScreenshotSelectorWindow.xaml | 11 ++ .../Windows/ScreenshotSelectorWindow.xaml.cs | 37 +++++ 4 files changed, 280 insertions(+), 17 deletions(-) diff --git a/Ink Canvas/MainWindow_cs/MW_ImageInsert.cs b/Ink Canvas/MainWindow_cs/MW_ImageInsert.cs index bab3e578..7ce92c94 100644 --- a/Ink Canvas/MainWindow_cs/MW_ImageInsert.cs +++ b/Ink Canvas/MainWindow_cs/MW_ImageInsert.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Forms; +using System.Windows.Ink; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Threading; @@ -30,15 +31,17 @@ namespace Ink_Canvas public Bitmap CameraImage; public BitmapSource CameraBitmapSource; public bool AddToWhiteboard; + public bool IncludeInk; public ScreenshotResult(Rectangle area, List path = null, Bitmap cameraImage = null, - BitmapSource cameraBitmapSource = null, bool addToWhiteboard = false) + BitmapSource cameraBitmapSource = null, bool addToWhiteboard = false, bool includeInk = true) { Area = area; Path = path; CameraImage = cameraImage; CameraBitmapSource = cameraBitmapSource; AddToWhiteboard = addToWhiteboard; + IncludeInk = includeInk; } } @@ -95,7 +98,7 @@ namespace Ink_Canvas else if (screenshotResult.Value.Area.Width > 0 && screenshotResult.Value.Area.Height > 0) { // 屏幕截图 - using (var originalBitmap = CaptureScreenArea(screenshotResult.Value.Area)) + using (var originalBitmap = CaptureScreenAreaWithOptionalInk(screenshotResult.Value.Area, screenshotResult.Value.IncludeInk)) { if (originalBitmap != null) { @@ -207,7 +210,16 @@ namespace Ink_Canvas { await Application.Current.Dispatcher.InvokeAsync(() => { - var selectorWindow = new ScreenshotSelectorWindow(); + var selectorWindow = new ScreenshotSelectorWindow(shouldIncludeInk => + { + if (inkCanvas == null) + { + return; + } + + inkCanvas.Visibility = shouldIncludeInk ? Visibility.Visible : Visibility.Collapsed; + Dispatcher.Invoke(() => { }, DispatcherPriority.Render); + }); if (selectorWindow.ShowDialog() == true) { // 检查是否是摄像头截图 @@ -218,7 +230,8 @@ namespace Ink_Canvas null, // 摄像头截图不需要路径 null, // 不再使用Bitmap selectorWindow.CameraBitmapSource, // 摄像头BitmapSource - selectorWindow.ShouldAddToWhiteboard + selectorWindow.ShouldAddToWhiteboard, + selectorWindow.ShouldIncludeInk ); } else if (selectorWindow.CameraImage != null) @@ -228,7 +241,8 @@ namespace Ink_Canvas null, // 摄像头截图不需要路径 selectorWindow.CameraImage, // 摄像头图像 null, - selectorWindow.ShouldAddToWhiteboard + selectorWindow.ShouldAddToWhiteboard, + selectorWindow.ShouldIncludeInk ); } else @@ -238,7 +252,8 @@ namespace Ink_Canvas selectorWindow.SelectedPath, null, null, - selectorWindow.ShouldAddToWhiteboard + selectorWindow.ShouldAddToWhiteboard, + selectorWindow.ShouldIncludeInk ); } } @@ -304,6 +319,108 @@ namespace Ink_Canvas } } + private Bitmap CaptureScreenAreaWithOptionalInk(Rectangle area, bool includeInk) + { + Bitmap bitmap = null; + StrokeCollection strokesForOverlay = null; + Point? inkCanvasTopLeftOnScreen = null; + System.Windows.Media.Matrix? dpiTransform = null; + var originalWindowVisibility = Visibility; + + try + { + if (includeInk && inkCanvas != null && inkCanvas.Strokes.Count > 0) + { + strokesForOverlay = inkCanvas.Strokes.Clone(); + + var source = PresentationSource.FromVisual(inkCanvas); + if (source?.CompositionTarget != null) + { + dpiTransform = source.CompositionTarget.TransformToDevice; + } + + inkCanvasTopLeftOnScreen = inkCanvas.PointToScreen(new Point(0, 0)); + } + + // 先隐藏主窗口再截取屏幕,确保基础截图不包含主线程 UI(含墨迹层)。 + Visibility = Visibility.Hidden; + Dispatcher.Invoke(() => { }, DispatcherPriority.Render); + + bitmap = CaptureScreenArea(area); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"准备截图时处理墨迹失败: {ex.Message}", LogHelper.LogType.Error); + bitmap?.Dispose(); + return null; + } + finally + { + Visibility = originalWindowVisibility; + Dispatcher.Invoke(() => { }, DispatcherPriority.Render); + } + + if (bitmap == null || !includeInk || strokesForOverlay == null || strokesForOverlay.Count == 0) + { + return bitmap; + } + + try + { + OverlayInkStrokesOnBitmap(bitmap, area, strokesForOverlay, inkCanvasTopLeftOnScreen, dpiTransform); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"叠加墨迹到截图失败: {ex.Message}", LogHelper.LogType.Error); + } + + return bitmap; + } + + private void OverlayInkStrokesOnBitmap( + Bitmap bitmap, + Rectangle area, + StrokeCollection strokes, + Point? inkCanvasTopLeftOnScreen = null, + System.Windows.Media.Matrix? dpiTransform = null) + { + if (bitmap == null || strokes == null || strokes.Count == 0) + { + return; + } + + var transform = dpiTransform ?? new System.Windows.Media.Matrix(1, 0, 0, 1, 0, 0); + var topLeft = inkCanvasTopLeftOnScreen ?? new Point(area.X, area.Y); + var offsetX = topLeft.X * transform.M11 - area.X; + var offsetY = topLeft.Y * transform.M22 - area.Y; + + var drawingVisual = new DrawingVisual(); + using (var drawingContext = drawingVisual.RenderOpen()) + { + drawingContext.PushTransform(new TranslateTransform(offsetX, offsetY)); + strokes.Draw(drawingContext); + drawingContext.Pop(); + } + + var renderBitmap = new RenderTargetBitmap(bitmap.Width, bitmap.Height, 96, 96, PixelFormats.Pbgra32); + renderBitmap.Render(drawingVisual); + + var encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(renderBitmap)); + + using (var memoryStream = new MemoryStream()) + { + encoder.Save(memoryStream); + memoryStream.Position = 0; + using (var overlayBitmap = new Bitmap(memoryStream)) + using (var graphics = Graphics.FromImage(bitmap)) + { + graphics.CompositingMode = CompositingMode.SourceOver; + graphics.DrawImage(overlayBitmap, 0, 0, bitmap.Width, bitmap.Height); + } + } + } + /// /// 将截图插入到画布 /// diff --git a/Ink Canvas/MainWindow_cs/MW_Screenshot.cs b/Ink Canvas/MainWindow_cs/MW_Screenshot.cs index 61632332..a599c5f9 100644 --- a/Ink Canvas/MainWindow_cs/MW_Screenshot.cs +++ b/Ink Canvas/MainWindow_cs/MW_Screenshot.cs @@ -14,6 +14,8 @@ namespace Ink_Canvas { public partial class MainWindow : Window { + private bool _isAreaScreenshotInProgress; + /// /// 在切页/加页场景下使用:先捕获当前画面到内存并克隆墨迹,然后立即返回;截图与墨迹保存在后台异步执行,不阻塞切页。 /// 调用方应在调用本方法后立即执行 SaveStrokes、ClearStrokes、切页、RestoreStrokes 等逻辑。 @@ -157,13 +159,98 @@ namespace Ink_Canvas SaveInkCanvasStrokes(false); } + private struct AnnotationSuspendState + { + public bool WasInAnnotationMode; + public int OriginalMode; + public System.Windows.Media.Brush OriginalFakeBackground; + public double OriginalFakeBackgroundOpacity; + public Visibility OriginalBackgroundCoverHolderVisibility; + public Visibility OriginalCanvasControlsVisibility; + public object OriginalHideInkCanvasContent; + } + + private AnnotationSuspendState SuspendAnnotationForAreaScreenshotIfNeeded() + { + var state = new AnnotationSuspendState + { + WasInAnnotationMode = GridTransparencyFakeBackground.Background != System.Windows.Media.Brushes.Transparent, + OriginalMode = currentMode, + OriginalFakeBackground = GridTransparencyFakeBackground.Background, + OriginalFakeBackgroundOpacity = GridTransparencyFakeBackground.Opacity, + OriginalBackgroundCoverHolderVisibility = GridBackgroundCoverHolder.Visibility, + OriginalCanvasControlsVisibility = StackPanelCanvasControls.Visibility, + OriginalHideInkCanvasContent = BtnHideInkCanvas.Content + }; + + if (!state.WasInAnnotationMode) + { + return state; + } + + // 仅暂停批注视觉态,避免调用 BtnHideInkCanvas_Click 触发自动截图/上传及白板状态读写。 + GridTransparencyFakeBackground.Opacity = 0; + GridTransparencyFakeBackground.Background = System.Windows.Media.Brushes.Transparent; + GridBackgroundCoverHolder.Visibility = Visibility.Collapsed; + StackPanelCanvasControls.Visibility = Visibility.Collapsed; + CheckEnableTwoFingerGestureBtnVisibility(false); + HideSubPanels("cursor"); + BtnHideInkCanvas.Content = "显示\n画板"; + + return state; + } + + private void RestoreAnnotationAfterAreaScreenshot(AnnotationSuspendState state) + { + if (!state.WasInAnnotationMode || currentMode != state.OriginalMode) + { + return; + } + + GridTransparencyFakeBackground.Opacity = state.OriginalFakeBackgroundOpacity; + GridTransparencyFakeBackground.Background = state.OriginalFakeBackground; + GridBackgroundCoverHolder.Visibility = state.OriginalBackgroundCoverHolderVisibility; + StackPanelCanvasControls.Visibility = state.OriginalCanvasControlsVisibility; + CheckEnableTwoFingerGestureBtnVisibility(state.OriginalCanvasControlsVisibility == Visibility.Visible); + BtnHideInkCanvas.Content = state.OriginalHideInkCanvasContent; + } + internal async Task SaveAreaScreenShotToDesktop() { - var originalVisibility = Visibility; + if (_isAreaScreenshotInProgress) + { + ShowNotification("截图进行中,请先完成当前截图"); + return; + } + + _isAreaScreenshotInProgress = true; + + var annotationState = SuspendAnnotationForAreaScreenshotIfNeeded(); + var originalFloatingBarVisibility = ViewboxFloatingBar.Visibility; + var shouldRestoreFloatingBarVisibility = true; try { - Visibility = Visibility.Hidden; - await Task.Delay(200); + if (annotationState.WasInAnnotationMode) + { + // 等待一次 UI 刷新,确保批注暂停状态已完成。 + await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Render); + } + + // 从浮动栏触发选区截图时,临时隐藏浮动栏,避免遮挡选区与误入截图。 + if (originalFloatingBarVisibility == Visibility.Visible) + { + ViewboxFloatingBar.Visibility = Visibility.Collapsed; + await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Render); + } + + // 选区截图时确保墨迹层可见,避免从浮动栏触发时出现“先隐藏再截图”。 + if (inkCanvas.Visibility != Visibility.Visible) + { + inkCanvas.Visibility = Visibility.Visible; + } + + // 等待一次 UI 刷新,确保可见性状态已生效。 + await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Render); var screenshotResult = await ShowScreenshotSelector(); @@ -175,7 +262,12 @@ namespace Ink_Canvas if (screenshotResult.Value.AddToWhiteboard) { - await AddScreenshotToNewWhiteboardPage(screenshotResult.Value); + // 仅在白板接管流程已确认完成时,才跳过本方法对浮动栏可见性的恢复。 + var whiteboardHandoffCompleted = await AddScreenshotToNewWhiteboardPage(screenshotResult.Value); + if (whiteboardHandoffCompleted) + { + shouldRestoreFloatingBarVisibility = false; + } return; } @@ -189,7 +281,7 @@ namespace Ink_Canvas Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.png"); - using (var originalBitmap = CaptureScreenArea(screenshotResult.Value.Area)) + using (var originalBitmap = CaptureScreenAreaWithOptionalInk(screenshotResult.Value.Area, screenshotResult.Value.IncludeInk)) { if (originalBitmap == null) { @@ -235,11 +327,16 @@ namespace Ink_Canvas } finally { - Visibility = originalVisibility; + _isAreaScreenshotInProgress = false; + if (shouldRestoreFloatingBarVisibility) + { + ViewboxFloatingBar.Visibility = originalFloatingBarVisibility; + } + RestoreAnnotationAfterAreaScreenshot(annotationState); } } - private async Task AddScreenshotToNewWhiteboardPage(ScreenshotResult screenshotResult) + private async Task AddScreenshotToNewWhiteboardPage(ScreenshotResult screenshotResult) { // 先在当前场景准备截图数据,再进白板,避免误截到白板页面 BitmapSource bitmapSourceForClipboard = null; @@ -259,15 +356,15 @@ namespace Ink_Canvas if (screenshotResult.Area.Width <= 0 || screenshotResult.Area.Height <= 0) { ShowNotification("未选择有效截图区域"); - return; + return false; } - using (var originalBitmap = CaptureScreenArea(screenshotResult.Area)) + using (var originalBitmap = CaptureScreenAreaWithOptionalInk(screenshotResult.Area, screenshotResult.IncludeInk)) { if (originalBitmap == null) { ShowNotification("截图失败"); - return; + return false; } Bitmap finalBitmap = originalBitmap; @@ -296,7 +393,7 @@ namespace Ink_Canvas if (bitmapSourceForClipboard == null) { ShowNotification("截图转换失败"); - return; + return false; } // 图像已拷贝到内存后再进入白板 @@ -311,6 +408,7 @@ namespace Ink_Canvas BtnWhiteBoardAdd_Click(null, EventArgs.Empty); await InsertBitmapSourceToCanvas(bitmapSourceForClipboard); + return true; } /// diff --git a/Ink Canvas/Windows/ScreenshotSelectorWindow.xaml b/Ink Canvas/Windows/ScreenshotSelectorWindow.xaml index 0e9584d8..1e198242 100644 --- a/Ink Canvas/Windows/ScreenshotSelectorWindow.xaml +++ b/Ink Canvas/Windows/ScreenshotSelectorWindow.xaml @@ -139,6 +139,17 @@ + + +