feat: 选区截图时保留屏幕笔迹 (#406)

* feat: 使用选区截图时,不清除 Strokes(Keep it on screen)

* fix: 浮动栏选区截图前强制保持墨迹可见

* fix: 避免选区截图回滚 inkCanvas 运行时状态

* fix: 截图前退出并在结束后恢复批注状态

* fix: 截图流程改用轻量批注暂停避免副作用

* feat: 选区截图添加包含墨迹开关

* fix: 避免选区截图墨迹重复渲染

* fix: 全屏基础截图排除主窗口后再叠加墨迹

* fix: 隐藏浮动栏后再进入选区截图

* fix: 添加到白板时不强制恢复浮动栏可见性

* fix: 防止重复启动选区截图实例

* fix: 仅在白板接管成功后跳过浮动栏恢复

* feat: 选区截图时实时预览包含墨迹开关

* fix: 合并截图选择器OnClosed逻辑避免重复定义
This commit is contained in:
tayasui rainnya!
2026-03-28 17:11:18 +08:00
committed by GitHub
parent fd137ae787
commit f7aa107a62
4 changed files with 280 additions and 17 deletions
+123 -6
View File
@@ -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<Point> 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);
}
}
}
/// <summary>
/// 将截图插入到画布
/// </summary>
+109 -11
View File
@@ -14,6 +14,8 @@ namespace Ink_Canvas
{
public partial class MainWindow : Window
{
private bool _isAreaScreenshotInProgress;
/// <summary>
/// 在切页/加页场景下使用:先捕获当前画面到内存并克隆墨迹,然后立即返回;截图与墨迹保存在后台异步执行,不阻塞切页。
/// 调用方应在调用本方法后立即执行 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<bool> 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;
}
/// <summary>
@@ -140,6 +140,17 @@
Margin="8,0"
Background="#404040" />
<!-- 选项开关 -->
<CheckBox Name="IncludeInkCheckBox"
Content="包含墨迹"
IsChecked="True"
Margin="8,0"
VerticalAlignment="Center"
Foreground="White"
FontWeight="Medium"
Checked="IncludeInkCheckBox_Checked"
Unchecked="IncludeInkCheckBox_Unchecked" />
<!-- 操作按钮 -->
<Button Name="ConfirmButton"
Content="确认截图"
@@ -37,6 +37,7 @@ namespace Ink_Canvas
private Bitmap _capturedCameraImage = null;
private DateTime _lastBlankClickTime = DateTime.MinValue;
private WpfPoint _lastBlankClickPosition;
private readonly Action<bool> _includeInkPreviewChanged;
private const int DoubleClickTimeThresholdMs = 300; // 双击判定时间阈值(常见范围 200~500ms)
private const double DoubleClickDistanceThresholdPx = 12; // 双击判定位置阈值(像素)
@@ -55,10 +56,17 @@ namespace Ink_Canvas
public Bitmap CameraImage { get; private set; }
public System.Windows.Media.Imaging.BitmapSource CameraBitmapSource { get; private set; }
public bool ShouldAddToWhiteboard { get; private set; }
public bool ShouldIncludeInk { get; private set; } = true;
public ScreenshotSelectorWindow()
: this(null)
{
}
public ScreenshotSelectorWindow(Action<bool> includeInkPreviewChanged)
{
InitializeComponent();
_includeInkPreviewChanged = includeInkPreviewChanged;
// 设置窗口覆盖所有屏幕
SetupFullScreenOverlay();
@@ -84,6 +92,8 @@ namespace Ink_Canvas
timer.Stop();
};
timer.Start();
ApplyIncludeInkPreviewState(ShouldIncludeInk);
}
private void InitializeFreehandMode()
@@ -529,6 +539,30 @@ namespace Ink_Canvas
ConfirmSelection();
}
private void IncludeInkCheckBox_Checked(object sender, RoutedEventArgs e)
{
ShouldIncludeInk = true;
ApplyIncludeInkPreviewState(ShouldIncludeInk);
}
private void IncludeInkCheckBox_Unchecked(object sender, RoutedEventArgs e)
{
ShouldIncludeInk = false;
ApplyIncludeInkPreviewState(ShouldIncludeInk);
}
private void ApplyIncludeInkPreviewState(bool shouldIncludeInk)
{
try
{
_includeInkPreviewChanged?.Invoke(shouldIncludeInk);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新“包含墨迹”实时预览失败: {ex.Message}", LogHelper.LogType.Warning);
}
}
private void ConfirmCameraCapture()
{
try
@@ -1480,6 +1514,9 @@ namespace Ink_Canvas
{
try
{
// 关闭窗口时恢复墨迹预览状态
ApplyIncludeInkPreviewState(true);
// 清理摄像头资源
if (_cameraService != null)
{