Skip to content
Open
201 changes: 187 additions & 14 deletions Ink Canvas/MainWindow_cs/MW_ImageInsert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,19 @@ public struct ScreenshotResult
public Bitmap CameraImage;
public BitmapSource CameraBitmapSource;
public bool AddToWhiteboard;
public bool IncludeInk;
public BitmapSource InkOverlayBitmapSource;

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 = false, BitmapSource inkOverlayBitmapSource = null)
{
Area = area;
Path = path;
CameraImage = cameraImage;
CameraBitmapSource = cameraBitmapSource;
AddToWhiteboard = addToWhiteboard;
IncludeInk = includeInk;
InkOverlayBitmapSource = inkOverlayBitmapSource;
}
}

Expand All @@ -60,6 +64,8 @@ private async Task CaptureScreenshotAndInsert()
{
try
{
var inkOverlayPreview = CreateInkOverlayPreviewBitmapSource();

// 隐藏主窗口以避免截图包含窗口本身
var originalVisibility = Visibility;
Visibility = Visibility.Hidden;
Expand All @@ -68,7 +74,7 @@ private async Task CaptureScreenshotAndInsert()
await Task.Delay(200);

// 启动区域选择截图
var screenshotResult = await ShowScreenshotSelector();
var screenshotResult = await ShowScreenshotSelector(inkOverlayPreview);

// 恢复窗口显示
Visibility = originalVisibility;
Expand Down Expand Up @@ -104,11 +110,33 @@ private async Task CaptureScreenshotAndInsert()

try
{
if (screenshotResult.Value.IncludeInk && screenshotResult.Value.InkOverlayBitmapSource != null)
{
var withInkBitmap = OverlayInkOnCapturedBitmap(finalBitmap, screenshotResult.Value.Area, screenshotResult.Value.InkOverlayBitmapSource);
if (withInkBitmap != null && withInkBitmap != finalBitmap)
{
if (needDisposeFinalBitmap && finalBitmap != originalBitmap)
{
finalBitmap.Dispose();
}
finalBitmap = withInkBitmap;
needDisposeFinalBitmap = true;
}
}

// 如果有路径信息,应用形状遮罩
if (screenshotResult.Value.Path != null && screenshotResult.Value.Path.Count > 0)
{
finalBitmap = ApplyShapeMask(originalBitmap, screenshotResult.Value.Path, screenshotResult.Value.Area);
needDisposeFinalBitmap = true; // 标记需要释放新创建的位图
var maskedBitmap = ApplyShapeMask(finalBitmap, screenshotResult.Value.Path, screenshotResult.Value.Area);
if (maskedBitmap != null && maskedBitmap != finalBitmap)
{
if (needDisposeFinalBitmap && finalBitmap != originalBitmap)
{
finalBitmap.Dispose();
}
finalBitmap = maskedBitmap;
needDisposeFinalBitmap = true; // 标记需要释放新创建的位图
}
}

// 将截图转换为WPF Image并插入到画布
Expand Down Expand Up @@ -190,24 +218,27 @@ private async Task CaptureFullScreenAndInsert()
}

/// <summary>
/// 显示截图区域选择器
/// 显示截图区域选择器并返回用户的截图结果(区域截图或摄像头截图)。
/// </summary>
/// <returns>截图结果,包含区域、路径和摄像头截图信息</returns>
/// <param name="inkOverlayPreview">当用户选择包含墨迹的区域截图时,用于作为墨迹叠加的预览 <see cref="BitmapSource"/>;可为 <c>null</c>。</param>
/// <returns>若用户确认截图则返回 <see cref="ScreenshotResult"/>,否则返回 <c>null</c>。返回的结果可能为摄像头截图或区域截图,摄像头截图会包含 <see cref="ScreenshotResult.CameraBitmapSource"/> 或 <see cref="ScreenshotResult.CameraImage"/>,区域截图会包含有效的区域与路径。</returns>
/// <remarks>
/// 该方法会:
/// 1. 显示截图选择器窗口
/// 2. 获取用户选择的区域或摄像头截图
/// 3. 返回截图结果
/// 1. 在 UI 线程(通过 <see cref="Application.Current.Dispatcher"/>)上显示截图选择器窗口 <see cref="ScreenshotSelectorWindow"/>;
/// 2. 获取用户选择的区域截图或摄像头截图;
/// 3. 根据用户选择构建并返回 <see cref="ScreenshotResult"/>;
/// 4. 若用户取消对话框或未确认截图,返回 <c>null</c>;
/// 5. 方法内部捕获异常并记录日志(不会向调用方抛出异常),如需外部处理请调整实现以重新抛出或传回错误信息。
/// </remarks>
private async Task<ScreenshotResult?> ShowScreenshotSelector()
private async Task<ScreenshotResult?> ShowScreenshotSelector(BitmapSource inkOverlayPreview = null)
{
ScreenshotResult? result = null;

try
{
await Application.Current.Dispatcher.InvokeAsync(() =>
{
var selectorWindow = new ScreenshotSelectorWindow();
var selectorWindow = new ScreenshotSelectorWindow(inkOverlayPreview);
if (selectorWindow.ShowDialog() == true)
{
// 检查是否是摄像头截图
Expand All @@ -218,7 +249,9 @@ await Application.Current.Dispatcher.InvokeAsync(() =>
null, // 摄像头截图不需要路径
null, // 不再使用Bitmap
selectorWindow.CameraBitmapSource, // 摄像头BitmapSource
selectorWindow.ShouldAddToWhiteboard
selectorWindow.ShouldAddToWhiteboard,
false,
null
);
}
else if (selectorWindow.CameraImage != null)
Expand All @@ -228,7 +261,9 @@ await Application.Current.Dispatcher.InvokeAsync(() =>
null, // 摄像头截图不需要路径
selectorWindow.CameraImage, // 摄像头图像
null,
selectorWindow.ShouldAddToWhiteboard
selectorWindow.ShouldAddToWhiteboard,
false,
null
);
}
else
Expand All @@ -238,7 +273,9 @@ await Application.Current.Dispatcher.InvokeAsync(() =>
selectorWindow.SelectedPath,
null,
null,
selectorWindow.ShouldAddToWhiteboard
selectorWindow.ShouldAddToWhiteboard,
selectorWindow.IncludeInkInScreenshot,
selectorWindow.IncludeInkInScreenshot ? inkOverlayPreview : null
);
}
}
Expand Down Expand Up @@ -304,6 +341,142 @@ private Bitmap CaptureScreenArea(Rectangle area)
}
}

private BitmapSource CreateInkOverlayPreviewBitmapSource()
{
try
{
if (inkCanvas == null)
{
return null;
}

if ((inkCanvas.Strokes?.Count ?? 0) == 0 && inkCanvas.Children.Count == 0)
{
return null;
}

if (inkCanvas.ActualWidth <= 0 || inkCanvas.ActualHeight <= 0)
{
return null;
}

var virtualScreen = SystemInformation.VirtualScreen;
var source = PresentationSource.FromVisual(inkCanvas);
var transformToDevice = source?.CompositionTarget?.TransformToDevice ?? System.Windows.Media.Matrix.Identity;

// PointToScreen 返回WPF坐标(DIP),统一转换为设备像素后再与 VirtualScreen 对齐
var inkTopLeftDip = inkCanvas.PointToScreen(new Point(0, 0));
var inkTopLeftPx = transformToDevice.Transform(inkTopLeftDip);
var offsetX = inkTopLeftPx.X - virtualScreen.Left;
var offsetY = inkTopLeftPx.Y - virtualScreen.Top;
var widthPx = inkCanvas.ActualWidth * transformToDevice.M11;
var heightPx = inkCanvas.ActualHeight * transformToDevice.M22;

var drawingVisual = new DrawingVisual();
using (var dc = drawingVisual.RenderOpen())
{
// 使用完整 InkCanvas 视觉树,确保包含图片等子元素
var visualBrush = new VisualBrush(inkCanvas)
{
Stretch = Stretch.Fill
};
dc.DrawRectangle(visualBrush, null, new Rect(offsetX, offsetY, widthPx, heightPx));
}

var rtb = new RenderTargetBitmap(
Math.Max(1, virtualScreen.Width),
Math.Max(1, virtualScreen.Height),
96,
96,
PixelFormats.Pbgra32);
rtb.Render(drawingVisual);
rtb.Freeze();
return rtb;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"创建截图墨迹预览失败: {ex.Message}", LogHelper.LogType.Warning);
return null;
}
}

private Bitmap OverlayInkOnCapturedBitmap(Bitmap capturedBitmap, Rectangle captureArea, BitmapSource inkOverlayBitmapSource)
{
if (capturedBitmap == null || inkOverlayBitmapSource == null)
{
return capturedBitmap;
}

try
{
var virtualScreen = SystemInformation.VirtualScreen;
var sourceRect = new Rectangle(
captureArea.X - virtualScreen.X,
captureArea.Y - virtualScreen.Y,
captureArea.Width,
captureArea.Height);

sourceRect.Intersect(new Rectangle(0, 0, inkOverlayBitmapSource.PixelWidth, inkOverlayBitmapSource.PixelHeight));
if (sourceRect.Width <= 0 || sourceRect.Height <= 0)
{
return capturedBitmap;
}

using (var inkOverlayBitmap = ConvertBitmapSourceToBitmap(inkOverlayBitmapSource))
{
if (inkOverlayBitmap == null)
{
return capturedBitmap;
}

Bitmap resultBitmap = null;
try
{
resultBitmap = new Bitmap(capturedBitmap.Width, capturedBitmap.Height, PixelFormat.Format32bppArgb);
using (var g = Graphics.FromImage(resultBitmap))
{
g.DrawImage(capturedBitmap, 0, 0, capturedBitmap.Width, capturedBitmap.Height);

var targetRect = new Rectangle(0, 0, Math.Min(sourceRect.Width, capturedBitmap.Width), Math.Min(sourceRect.Height, capturedBitmap.Height));
g.DrawImage(inkOverlayBitmap, targetRect, sourceRect, GraphicsUnit.Pixel);
}

return resultBitmap;
}
catch
{
resultBitmap?.Dispose();
throw;
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"叠加截图墨迹失败: {ex.Message}", LogHelper.LogType.Warning);
return capturedBitmap;
}
}

private Bitmap ConvertBitmapSourceToBitmap(BitmapSource bitmapSource)
{
if (bitmapSource == null)
{
return null;
}

using (var memoryStream = new MemoryStream())
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
encoder.Save(memoryStream);
memoryStream.Position = 0;
using (var tempBitmap = new Bitmap(memoryStream))
{
return new Bitmap(tempBitmap);
}
}
}

/// <summary>
/// 将截图插入到画布
/// </summary>
Expand Down
55 changes: 50 additions & 5 deletions Ink Canvas/MainWindow_cs/MW_Screenshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,11 @@ internal async Task SaveAreaScreenShotToDesktop()
var originalVisibility = Visibility;
try
{
var inkOverlayPreview = CreateInkOverlayPreviewBitmapSource();
Visibility = Visibility.Hidden;
await Task.Delay(200);

var screenshotResult = await ShowScreenshotSelector();
var screenshotResult = await ShowScreenshotSelector(inkOverlayPreview);

if (!screenshotResult.HasValue)
{
Expand Down Expand Up @@ -202,10 +203,32 @@ internal async Task SaveAreaScreenShotToDesktop()

try
{
if (screenshotResult.Value.IncludeInk && screenshotResult.Value.InkOverlayBitmapSource != null)
{
var withInkBitmap = OverlayInkOnCapturedBitmap(finalBitmap, screenshotResult.Value.Area, screenshotResult.Value.InkOverlayBitmapSource);
if (withInkBitmap != null && withInkBitmap != finalBitmap)
{
if (needDisposeFinalBitmap && finalBitmap != originalBitmap)
{
finalBitmap.Dispose();
}
finalBitmap = withInkBitmap;
needDisposeFinalBitmap = true;
}
}

if (screenshotResult.Value.Path != null && screenshotResult.Value.Path.Count > 0)
{
finalBitmap = ApplyShapeMask(originalBitmap, screenshotResult.Value.Path, screenshotResult.Value.Area);
needDisposeFinalBitmap = true;
var maskedBitmap = ApplyShapeMask(finalBitmap, screenshotResult.Value.Path, screenshotResult.Value.Area);
if (maskedBitmap != null && maskedBitmap != finalBitmap)
{
if (needDisposeFinalBitmap && finalBitmap != originalBitmap)
{
finalBitmap.Dispose();
}
finalBitmap = maskedBitmap;
needDisposeFinalBitmap = true;
}
}

var directory = Path.GetDirectoryName(desktopPath);
Expand Down Expand Up @@ -275,10 +298,32 @@ private async Task AddScreenshotToNewWhiteboardPage(ScreenshotResult screenshotR

try
{
if (screenshotResult.IncludeInk && screenshotResult.InkOverlayBitmapSource != null)
{
var withInkBitmap = OverlayInkOnCapturedBitmap(finalBitmap, screenshotResult.Area, screenshotResult.InkOverlayBitmapSource);
if (withInkBitmap != null && withInkBitmap != finalBitmap)
{
if (needDisposeFinalBitmap && finalBitmap != originalBitmap)
{
finalBitmap.Dispose();
}
finalBitmap = withInkBitmap;
needDisposeFinalBitmap = true;
}
}

if (screenshotResult.Path != null && screenshotResult.Path.Count > 0)
{
finalBitmap = ApplyShapeMask(originalBitmap, screenshotResult.Path, screenshotResult.Area);
needDisposeFinalBitmap = true;
var maskedBitmap = ApplyShapeMask(finalBitmap, screenshotResult.Path, screenshotResult.Area);
if (maskedBitmap != null && maskedBitmap != finalBitmap)
{
if (needDisposeFinalBitmap && finalBitmap != originalBitmap)
{
finalBitmap.Dispose();
}
finalBitmap = maskedBitmap;
needDisposeFinalBitmap = true;
}
}

bitmapSourceForClipboard = ConvertBitmapToBitmapSource(finalBitmap);
Expand Down
Loading
Loading